简介:面向机器学习与数据分析初学者的俄罗斯刑事犯罪数据集分析预测实战包,覆盖犯罪趋势可视化、酒精与犯罪回归建模、犯罪率预测三个典型任务,可帮助学习者完整走通数据清洗、特征工程、模型训练与评估流程。压缩包共5个文件,包含3个Python源代码、1个readme说明与1个589.70 KB的CSV数据集,整体约107KB;数据为2008—2023年俄罗斯犯罪记录,源码分别实现犯罪趋势分析、酒精消费与犯罪回归分析、随机森林预测及KMeans聚类,并配有plotly交互可视化与回归诊断代码,readme可作为快速上手指引。已有56人学习。借助该资源可掌握pandas、numpy、seaborn、sklearn、scipy等工具在真实数据中的综合运用,学会将回归、聚类与可视化结合解决业务问题;尤其适合课程设计、期末项目、竞赛练手或机器学习入门后的综合实战,便于对照运行结果加深理解。
1. 俄罗斯刑事犯罪数据集分析预测:一张16年CSV串起四条机器学习链路
把一份俄罗斯2008到2023年的犯罪统计表丢给刚学完机器学习的人,大部分人第一步是画个折线图就收工。这份俄罗斯刑事犯罪数据集分析预测实例显然不想停留在这一步,它把趋势可视化、酒精消费与犯罪回归、随机森林预测、KMeans聚类全部串进3个可运行的Python源代码里,从pandas清洗一路走到sklearn建模和plotly交互出图。数据不花哨,但正好覆盖了一条完整分析流水线的每个节点。适合两类人:一是刚学完pandas和sklearn、手里缺真实数据练手的开发者,二是要交作业或做数据分析汇报、希望代码开箱即用的技术人员。它解决的核心问题不是某个算法难题,而是“拿到一张时间序列表格后,怎么有条理地做出结论”。
2. 数据集与脚本分工:动手前先探清CSV结构和三个源码的边界
这套资源最容易被低估的地方是那张CSV。很多人解压后第一件事是直接跑脚本,跑通了就觉得完事,却没弄明白数据长什么样、三个脚本为什么这么分工。这一章我把拿到资源后的完整探查过程写出来。
2.1 先跑一段探查代码,别急着画图
解压后能看到一个readme.txt、三个py文件和一份名为“The number of crimes in Russia 2008-2023.csv”的数据文件。readme写得比较克制,没有交代行列结构、编码方式,这种时候我的习惯是先把表格的shape、列名、缺失情况摸清楚,再决定怎么给脚本传参。
import pandas as pd df = pd.read_csv("The number of crimes in Russia 2008-2023.csv", encoding="utf-8") print("shape:", df.shape) print("columns:", df.columns.tolist()) print(df.head(10)) print(df.info()) print(df.describe())这段代码做了四件事:shape告诉你这张表有多少行多少列,columns列出全部字段名,head(10)让你直观看到头部记录长什么样,info()则一次性暴露每列的数据类型和非空数量。describe()只对数值列生效,输出均值、标准差、最小最大值,是判断量级最直接的途径。
这里的编码参数值得专门说一句。我遇到过一份东欧国家的时间序列表,readme没写编码,用默认utf-8读出来全是乱码,换成cp1251之后一切正常。所以第一遍读取如果看到奇怪的字符,优先怀疑的不是文件坏了,而是编码不匹配,把encoding换成latin1或cp1251再试一次即可。
从文件大小推算,这份数据大概率不是简单的16行年度汇总,而是按地区拆分的细粒度表。俄罗斯联邦有85个左右联邦主体,16年乘85个地区,长表格式下大约有一千多行,这个体量和文件大小吻合。你实际跑出来的shape以输出为准,我在这里只是提示一个判断方向:如果Year列有大量重复值,那就必须做聚合再画趋势图,否则折线图会被地区维度搅乱。
2.2 三个源代码脚本的分工与依赖关系
readme里三个脚本的命名已经说明了它们各自的定位,我按执行顺序把功能和依赖拆成一张表:
| 脚本 | 核心任务 | 主要依赖 |
|---|---|---|
| 1-Crime Trends Analysis in Russia 20082023.py | 绘制2008-2023年犯罪数量和犯罪率的趋势图,静态图与交互图并出 | pandas、seaborn、matplotlib、plotly |
| 2-Alcohol and Crime Regression Analysis.py | 分析酒精消费量指标与犯罪数量之间的相关性,做回归拟合与优度评估 | scipy.stats、scipy.optimize.curve_fit、sklearn.metrics.r2_score |
| 3-The trends in crime rates in Russia.py | 在犯罪率维度上做特征工程、随机森林预测,并附带聚类分组 | sklearn整套、numpy、plotly、urllib、json |
三个脚本的递进关系很清晰:脚本1回答“犯罪数量16年怎么变”,脚本2回答“哪些因素和犯罪数量相关”,脚本3回答“能不能用历史数据预测未来趋势”。很多初学者会跳过前两个直接跑第三个,结果就是连Crime_Count和Crime_Rate两个字段的区别都没搞清楚就开始调参,后面对模型输出自然无法解释。
模块清单里出现了urllib.request.urlopen和json,说明第3个脚本不只是读本地CSV,还从外部公开接口抓过补充数据。最常见的做法是用年度人口数据把犯罪总数换算成每10万人犯罪率,这样不同年份之间才可比——人口总量变化会直接影响犯罪绝对数,不看人均只看总数容易得出错误结论。这个细节后面讲预测时还会提到。
2.3 模块选型理由:为什么是随机森林、curve_fit和KMeans组合
看到一个项目同时用RandomForestRegressor、scipy.optimize.curve_fit和KMeans,第一反应可能是“堆了一堆库”,实际上这三个东西解决的问题完全不同。随机森林负责预测,核心优势是表格数据上不需要做大量特征工程,能自动捕捉非线性关系,对这份只有十几个特征的小样本数据集非常稳。curve_fit负责解释性分析,酒精消费和犯罪数量之间如果存在函数关系,它能拟合出具体的表达式参数,这是黑匣子模型给不了的。KMeans则做无监督分组,把年份或地区按特征相似度聚成几类,用聚类结果反过来验证监督模型的划分是否合理。
选型逻辑里最值得学习的是“够用就好”。这份数据规模很小,用深度学习纯属杀鸡用牛刀,线性回归又难以刻画非线性波动,随机森林处在两者之间:训练快、可复现、带特征重要性,还能扛住小样本下的过拟合风险。理解这个取舍,比记住某个具体API重要得多。
3. 犯罪趋势分析实战:Seaborn静态图与Plotly交互图的配合方式
脚本1是整个项目的入口,它做的事情本质上只有一件:把16年的犯罪数量和犯罪率变化讲清楚。但同一个结论,静态图和交互图的表达侧重点完全不同,脚本里两个都给了。
3.1 年度聚合与Seaborn趋势线
如果原始表是按地区拆分的,第一步必须做年度聚合,否则画出来的是85条纠缠在一起的细线,什么结论都读不出来。
import pandas as pd import matplotlib.pyplot as plt import seaborn as sns import warnings warnings.filterwarnings("ignore") df = pd.read_csv("The number of crimes in Russia 2008-2023.csv") df["Year"] = df["Year"].astype(int) yearly = df.groupby("Year", as_index=False)["Crime_Count"].sum() sns.set_style("whitegrid") fig, ax = plt.subplots(figsize=(11, 5)) sns.lineplot(data=yearly, x="Year", y="Crime_Count", marker="o", linewidth=2.2, color="#C62828", ax=ax) ax.set_title("Crime Count in Russia 2008-2023") ax.set_xlabel("Year") ax.set_ylabel("Number of Crimes") plt.tight_layout() plt.show()groupby("Year", as_index=False)把相同年份的所有地区记录汇总,as_index=False保证年份保留为列而不是变成索引,方便后续传给seaborn。marker="o"在每年数据点上画实心圆,适合样本点只有16个的时间序列——点线结合能直接看出每年间的跳变。linewidth=2.2控制线宽,color="#C62828"是深红色,深色的主趋势线在白色网格背景下辨识度最高。
warnings.filterwarnings("ignore")是脚本里常见的一行,作用是屏蔽seaborn和sklearn版本升级时抛出的DeprecationWarning,避免控制台被无关信息刷屏。如果你跑脚本时发现某些警告信息指向sklearn的旧API,别急着改代码,先看警告级别,DeprecationWarning不影响当前运行。
这张图画完,核心结论已经出来了:俄罗斯犯罪绝对数量在这16年里整体下行,中间存在明显波动。看到这样的趋势,正常的下一步不是急着做预测,而是问一句“为什么”,这就把接力棒交到了脚本2手上。
3.2 Plotly交互图与HTML导出
静态图适合写进报告,但数据密的地方想看具体某年数值,就得靠交互图。脚本里用plotly.express实现的版本我一般会这样写:
import plotly.express as px fig = px.line(yearly, x="Year", y="Crime_Count", title="Crime Count in Russia (Interactive)", markers=True) fig.update_traces(line=dict(width=3, color="#C62828")) fig.write_html("crime_trend.html") fig.show()px.line一行就能生成带悬浮提示的交互折线图,鼠标悬停后自动显示年份和数值。write_html会把整个图打包成一个独立HTML文件,这个文件可以直接发给不会跑Python的同事或客户,对方用浏览器打开就能看、能放大、能悬停读数,这是静态图做不到的。
这里有一个我在Jupyter环境里反复踩过的坑:fig.show()在有些环境下只会输出一行文字,图片不渲染。这不是代码问题,而是plotly的renderer没配好。排查顺序是:先确认你用的是Jupyter Notebook还是Jupyter Lab,然后看plotly版本是否匹配;不想折腾环境的话,直接改成fig.write_html("crime_trend.html")然后浏览器打开,绕开renderer问题。
3.3 数量与率两套图别混着看
脚本1画的是犯罪数量,脚本3画的是犯罪率,这两个维度在视觉趋势上可能差很多。犯罪数量下降可能只是因为人口减少,人均犯罪率未必同步下降。所以我拿到这张表之后习惯把两张图放在一起对比:如果数量下降但犯罪率上升,说明社会安全状况实际在恶化,只看总数会得出完全相反的结论。脚本1给的是数量层面的趋势打好底,脚本3再在犯罪率维度上补一层,两个脚本互为补充而不是重复建设。
4. 酒精与犯罪回归分析:相关、拟合和R²的解读边界
脚本2把注意力从“随时间怎么变”转移到“和什么相关”。俄罗斯的禁酒政策与犯罪率之间的关系是社会学里被研究很多的话题,这份数据集里带了酒精消费相关字段,正好支撑这个分析。
4.1 Pearson相关系数与显著性检验
相关性分析最容易犯的错误是只算一个相关系数就下结论,忽略p值。脚本里用scipy.stats计算相关性的部分我一般会写成这样:
from scipy.stats import pearsonr import pandas as pd df = pd.read_csv("The number of crimes in Russia 2008-2023.csv") sub = df[["Alcohol_Liters", "Crime_Count"]].dropna() r, p = pearsonr(sub["Alcohol_Liters"], sub["Crime_Count"]) print(f"Pearson r = {r:.4f}, p-value = {p:.2e}")pearsonr返回两个值:相关系数r和p值。r的范围在-1到1之间,正数表示正相关,负数表示负相关,绝对值越接近1线性关系越强。p值回答的是“这个相关性能不能排除随机性”,行业里常用的判断线是0.05——p小于0.05才认为统计显著,否则即使r算出来0.4也不能当真。
sub = df[["Alcohol_Liters", "Crime_Count"]].dropna()这一步非常关键:pearsonr不接受含NaN的数组,如果原始数据某个年份的酒精消费字段是空的,不dropna直接跑会报错或者算出一个错误结果。dropna()按行删除缺失值,删除前可以先看一眼sub.shape确认删掉了多少行,如果删掉的比例超过10%,就得回头检查原始数据采集是否有问题。
俄罗斯的实际背景是2010年前后出台了一系列限制酒精销售的政策,同期犯罪率出现明显变化。如果你跑出来的相关系数为正且p值很小,只能说明两个变量在统计上存在正向关联,不能直接写成“酒精导致了犯罪”——这个因果性判断需要政策干预节点、滞后效应等更多证据支撑,脚本里只到相关性为止是合理的边界。
4.2 curve_fit非线性拟合与R²评估
相关分析回答“有没有关系”,回归拟合回答“关系长什么样”。脚本2里用scipy.optimize.curve_fit拟合的代码我复现时这样处理:
import numpy as np from scipy.optimize import curve_fit from sklearn.metrics import r2_score x = sub["Alcohol_Liters"].values y = sub["Crime_Count"].values def linear_fit(x, a, b): return a * x + b popt, pcov = curve_fit(linear_fit, x, y) a, b = popt y_pred = linear_fit(x, a, b) print(f"y = {a:.2f} * x + {b:.2f}") print("R2:", r2_score(y, y_pred))curve_fit的第一个参数是自定义函数,第二个和第三个分别是自变量和因变量数组。popt是拟合得到的最优参数列表,在这里对应a和b;pcov是参数协方差矩阵,对角线元素开根号就是参数的标准差,标准差过大说明拟合不稳定。r2_score计算决定系数,取值范围0到1,代表模型解释了因变量百分之多少的方差。
这里有一个新手很容易误解的点:curve_fit本身不返回R²,需要自己调用sklearn.metrics.r2_score来计算。脚本模块清单里同时出现scipy.optimize.curve_fit和sklearn.metrics.r2_score,正是因为这个分工——前者做参数拟合,后者做拟合优度评估。如果把R²高达0.9理解为“找到了因果机制”,那就过度解读了,R²只描述拟合程度,不描述机制。
4.3 回归诊断:看一眼残差再信结论
拟合跑完别急着截图,我一般会再补一个残差检查:把回归的残差算出来,按年份画散点图。如果残差在某个时间段内系统性为正、另一个时间段内系统性为负,说明模型存在结构性问题,可能漏掉了关键变量或非线性关系。这个步骤脚本不一定写了,但对解释“为什么酒精和犯罪的相关性在特定年份断开”很有帮助。残差分析的代码很简单:residual = y - y_pred,然后按年份做个散点图观察是否随机分布在0轴两侧。
5. 随机森林预测与常见问题排查:时间序列切分、MSE量纲和可复现性
脚本3是全项目技术含量最高的一块,它把犯罪率预测当成一个标准的监督学习问题处理。但这里藏着一个几乎所有初学者都会踩的暗坑:时间序列数据能不能直接用train_test_split随机切分。
5.1 数据划分与随机森林建模
先给出一版可以跑的建模代码,再解释为什么划分方式在时间序列场景下要格外谨慎:
from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_squared_error, r2_score X = sub[["Alcohol_Liters", "Year"]].values y = sub["Crime_Count"].values X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.2, random_state=42 ) model = RandomForestRegressor( n_estimators=300, max_depth=4, min_samples_leaf=3, random_state=42, n_jobs=-1 ) model.fit(X_train, y_train) y_pred = model.predict(X_test) mse = mean_squared_error(y_test, y_pred) print("MSE:", mse) print("R2:", r2_score(y_test, y_pred)) print("Feature importance:", model.feature_importances_)几个参数的实际含义值得展开。n_estimators=300表示建300棵决策树,树越多预测越稳定,但超过一定数量后收益递减,300在这个样本量下是性价比比较高的值。max_depth=4限制每棵树的最大深度,防止单棵树记住训练集噪声——深度越大越容易过拟合,4层对十几个特征来说已经够用。min_samples_leaf=3强制叶子节点至少3个样本,进一步抑制过拟合。random_state=42同时出现在train_test_split和模型里,是一个好习惯:固定随机种子后,每次运行结果完全一致,否则同样的代码跑两次得到不同R²,排查问题时会被随机性引入死胡同。n_jobs=-1让模型用满所有CPU核心,300棵树的训练时间能明显缩短。
feature_importances_输出每个特征对预测的贡献度。这一行信息经常被忽略,实际上它价值极高:如果酒精消费量的重要性远高于年份,说明犯罪率的年度变化主要由消费习惯驱动;如果年份占主导,说明趋势性因素才是关键。看重要性再决定后续特征工程方向,比盲目堆特征靠谱得多。
5.2 MSE量纲陷阱:为什么数值大得吓人
脚本第一次跑完,MSE输出可能是几十万甚至上百万。很多初学者看到这个数字直接判定模型失败,其实这是量纲问题:因变量Crime_Count是绝对数量,俄罗斯全国年度犯罪数量在几十万这个量级,误差平方后自然变成百万级别。MSE不是百分比指标,它的大小完全取决于因变量的单位。
更直观的做法是看RMSE,也就是mean_squared_error开根号,单位回到原始量纲,比如“平均偏差约8000起”,马上能理解模型精度。另一个更稳的做法是把预测目标从犯罪总数换成每10万人犯罪率,这样数值落在几百到几千的区间,MSE就不会夸张到让人误判模型失效。这也是脚本3专门用urllib拉人口数据的原因——换成犯罪率后,模型训练目标本身的数值分布合理得多。
5.3 避坑清单:五个复现时容易翻车的细节
这一节把我在复现这套代码时实际遇到的坑按“现象、原因、解决”列出来,照着排查能省不少时间。
第一个坑是R²奇高但预测曲线完全不符合业务直觉。有一次我跑出来的R²是0.98,但预测值在最后几年出现明显倒挂,后来发现是train_test_split随机切分导致测试集里混进了“未来”的数据——随机森林记住了时间趋势,测试集年份和训练集年份重叠,等于开卷考试。解决方法是按年份顺序切分,比如前80%的年份作训练集、后20%作测试集,或者用sklearn.model_selection.TimeSeriesSplit做交叉验证。
第二个坑是MSE输出几十万直接慌了神。原因上面已经说过,绝对数量和平方误差双重放大导致数值无参考意义。解决方法是同时输出RMSE和R²,如果还不放心就把目标字段换成犯罪率再训练一版对比。
第三个坑是构造滞后特征后模型整体崩溃或早期预测全是空值。犯罪数据天然适合用前一年的值预测后一年,但df["Lag_1"] = df["Crime_Count"].shift(1)会让第一行变成NaN,如果不处理,整条特征列带着空值喂给随机森林,结果不可控。解决方法是dropna()丢掉头部的空行,或用fillna(method="ffill")用后一值填充,前者更干净。
第四个坑是KMeans聚类结果两次运行不一致。KMeans的初始质心是随机选择的,不固定种子每次出来的分组都不同。解决方法是固定random_state并把n_init设成10或更高,n_init表示用多少个不同初始质心跑完取最优,能明显降低落入局部最优的概率。
第五个坑是plotly图形在Jupyter里不显示,只看到一行文本。原因是renderer配置和当前环境不匹配,排查顺序是先跑plotly.io.renderers.default看当前默认渲染器,再确认你是不是在Jupyter Lab里用了不兼容的版本。不想折腾的话直接用fig.write_html("crime_trend.html")导出后用浏览器打开,速度最快。
6. KMeans聚类验证:用无监督方法给预测结果兜底
随机森林给出预测值之后,这套流程似乎已经完整了。但其实还有一个值得做的验证步骤:用KMeans对特征空间做无监督分组,看聚类结果和监督模型的预测逻辑是否互相印证。如果随机森林说某一年犯罪率会高,而聚类也把这一年归到“高风险”组,结论的可信度就更高。
6.1 标准化处理与肘部法则
聚类正式跑之前必须先做标准化。Alcohol_Liters和Crime_Count两个字段的量级差可能超过百倍,不标准化的话欧氏距离会被大数量字段完全主导,聚类结果等于没做。
from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans import matplotlib.pyplot as plt features = sub[["Alcohol_Liters", "Crime_Count"]].values scaler = StandardScaler() X_scaled = scaler.fit_transform(features) kmeans = KMeans(n_clusters=3, n_init=10, random_state=42) sub["Cluster"] = kmeans.fit_predict(X_scaled) print(sub.groupby("Cluster").mean())StandardScaler先fit计算每列的均值和标准差,再transform把数据映射成均值为0、标准差为1的分布。fit_predict一步完成训练和标签分配,返回的Cluster列可以直接拼回DataFrame做分组统计。n_clusters=3不是乱设的,我习惯先用肘部法则确认:
inertia = [] for k in range(1, 8): m = KMeans(n_clusters=k, n_init=10, random_state=42) m.fit(X_scaled) inertia.append(m.inertia_) plt.plot(range(1, 8), inertia, marker="o") plt.xlabel("k") plt.ylabel("inertia") plt.show()inertia_是簇内误差平方和,k从1增加到7,看曲线哪个位置出现明显的拐点,那个k就是自然的分组数。拐点之后误差下降速度明显变缓,继续增加k只是把数据切得更碎,没有实际意义。这个方法不完美,但配合业务背景——比如把俄罗斯的年份分成“高发期、过渡期、低位期”三段——基本够用。
6.2 一个更稳的验证习惯
聚类跑完之后,我通常会把每个簇对应的年份打印出来,和随机森林的特征重要性对照着看:如果重要性最高的特征恰好是区分簇间差异最大的那个字段,监督和无监督两条路就互相验证了。这一步代码很少,但对结论的信心提升是实打实的。
这套流程走过来,最大的感受是:真正增加价值的不是某个模型的精度,而是每一步都问一句“这个结论能被另外一种方法验证吗”。从那以后我每次拿到没摸过的数据集,都强制走一遍“探结构、画趋势、做相关、跑模型、聚类兜底”的完整流程,遇到问题先查切分方式和量纲,再怀疑模型本身。这套习惯帮我少走了很多弯路,希望帮到你。
本文还有配套的精品资源,点击获取