☰
俄罗斯刑事犯罪数据集预测实战:从数据清洗到模型部署的完整闭环
2026/9/28 15:17:42 网站建设 项目流程

简介:这是一份俄罗斯刑事犯罪数据集分析预测实战资源,面向希望用 Python 完成完整数据挖掘流程的初学者和数据分析人员,覆盖趋势分析、回归预测与聚类等典型任务。压缩包共 5 个文件,含 3 个可直接运行的 Python 源代码、1 个 txt 说明文档和 1 个 CSV 数据集;源代码合计约 35KB,CSV 数据约 589.70KB,压缩后整体约 107KB。代码手工整理、无语法错误,以 2008—2023 年俄罗斯犯罪统计数据为基础,分别实现犯罪趋势可视化、酒精消费与犯罪率回归分析、犯罪率变化规律与模式挖掘;过程中使用 pandas、numpy、seaborn、matplotlib、scikit-learn、plotly 等主流库,并包含 train_test_split、RandomForestRegressor、KMeans、curve_fit 等具体方法,整体适合对照练习。包内附 readme 说明文件,便于核对环境依赖与运行顺序,减少上手阻力;目前已有 55 人学习。下载后即可对照数据复现图表与模型结果,也适合作为课程设计或机器学习入门练习的参考。

1. 俄罗斯刑事犯罪数据集:学AI预测不是找数据集,而是找到能跑通的完整闭环

很多人找AI实战项目,眼睛只盯着“数据集”三个字,以为数据够多就能学会预测。真上手才发现,真正的门槛不在模型,而在从解压zip到产出预测结果这条链路上——数据脏不脏、标签平不平衡、时间字段怎么解析、三个源代码和数据集对不对得上。这份“俄罗斯的刑事犯罪数据集分析预测实例”恰好是这么一条完整链路:一份589.70 KB的犯罪记录数据,配上3个源代码,从数据清洗一路做到模型预测。它能帮你练的不是“调参玄学”,而是拿到一份陌生数据集后,怎么在最短时间内摸清结构、踩平坑、跑出可信的预测结果。适合正在学机器学习分类任务的初学者,也适合想看看别人怎么组织数据分析和建模流程的从业者。下面我按自己拿到这类项目时的处理顺序,把整条路走一遍。

2. 拿到压缩包先做这三件事:解压结构核对、数据清洗、标签分布体检

2.1 先别急着训练:目录结构与数据字典的核对顺序

任何zip包到手,第一步不是解压完就敲pd.read_csv(),而是先看目录结构。刑事犯罪数据集几乎不可能只有一个CSV,常见做法是包含原始数据、清洗后的数据、代码文件和一个README或数据字典。我一般会先在命令行里把目录树打出来,确认文件数量和命名,再决定后面代码里路径怎么写。

unzip "AI实战-俄罗斯的刑事犯罪数据集分析预测实例(含3个源代码+589.70 KB完整的数据集).zip" -d russian_crime cd russian_crime find . -type f | sort

这段命令把压缩包解压到russian_crime目录,然后用find列出所有文件。-d参数指定解压目标目录,避免文件散落到当前文件夹到处乱放。列出文件后,重点看三样东西:CSV文件的命名规律、代码文件是.py还是.ipynb、有没有说明文档。如果发现代码里的文件名和你实际解压出来的不一致,后面跑起来一定会报FileNotFoundError,这一眼就能排查掉。

数据字典是这步更关键的产出。打开CSV看一眼列名和每列的前几行,把字段含义记下来,尤其是预测目标列(通常是status或is_solved这类二值字段)。俄罗斯刑事犯罪数据的字段一般围绕犯罪类型、发生地、时间、受害人或嫌疑人信息展开,字段可能是俄语转写的英文,也可能是缩写。我习惯把列名和推断含义写在一个文本文件里,后续做特征工程随时回查。

2.2 用pandas做第一轮数据质量体检:缺失值、类型、时间字段

目录结构核对完,就要把数据真正读进来做体检。刑事犯罪记录这类人工录入的数据,脏的程度往往超出预期:日期字段混着多种格式、地区名称有拼写差异、年龄和武器字段可能留空。下面这段代码覆盖了最基础的三项体检:缺失值比例、每列数据类型、时间字段的取值区间。

import pandas as pd df = pd.read_csv("russian_crime_data.csv", encoding="utf-8") # 1. 缺失值概览,按缺失比例从高到低排列 missing = df.isnull().sum() missing = missing[missing > 0].sort_values(ascending=False) print("缺失字段及比例:") print((missing / len(df)).round(4)) # 2. 每列数据类型,重点看时间列是否被识别成object print("\n数据类型:") print(df.dtypes) # 3. 检查日期列的范围是否合理 if "date" in df.columns: df["date"] = pd.to_datetime(df["date"], errors="coerce") print("\n日期范围:", df["date"].min(), "->", df["date"].max()) print("解析失败行数:", df["date"].isnull().sum())

这里errors="coerce"是血泪经验的产物:俄罗斯数据里日期格式可能同时存在2017-05-01和01.05.2017两种写法,直接pd.to_datetime不指定格式会解析失败或产生歧义。先把解析不了的置成NaT,统计出失败行数,再回头查是哪些格式没覆盖到,比一次性强转后报错更可控。

缺失值比例的判断标准不固定,但一般超过30%的字段就基本告别建模了,缺失率低于5%的字段可以考虑填充。类型检查的重点在于:预测标签列必须是int或bool,时间列必须是datetime64,数值列不能是object。这三项确认完,数据才算过了第一关。

2.3 标签分布不均衡,是预测模型的第一道坎

刑事犯罪数据里,预测目标如果是“案件是否侦破”,侦破率通常在20%到40%之间,这意味着负样本(未侦破)可能占多数。这个分布不处理,后面模型会学成“全都预测未侦破”也能拿到高准确率的假象。

target_col = "status" # 假设1表示已侦破,0表示未侦破 value_counts = df[target_col].value_counts(normalize=True) print(value_counts) # 若类别严重失衡,记录一个比例备用 print("正样本占比:", round(value_counts.get(1, 0), 4))

一句话逻辑:先看正样本占比,如果低于20%,后续建模就必须考虑类别不均衡的处理方式。常见的处理路径有两条——一是用class_weight="balanced"让模型自动调整权重,二是在评估指标上改成F1分数而不是准确率。这一步不做,后面第5章的翻车现场就提前埋下了。

3. 特征工程与训练集划分:把俄语犯罪记录变成能喂给模型的数值特征

3.1 犯罪类型、地区、时间三类特征的编码方式选择

刑事犯罪数据集里文本类特征占大头:犯罪类型(偷窃、抢劫、诈骗等)、地区名、街道名。这三类特征的编码方式选择,直接决定模型能学到什么信息。

犯罪类型通常是低基数的分类变量,常见做法是直接用序号编码(Label Encoding)或独热编码。但如果类别数量在10个以内,我更推荐独热编码,因为犯罪类型之间没有天然顺序,序号编码会给模型传递“类型A大于类型B”的错误信号。地区特征就复杂了——俄罗斯的地区名细分到区级可能上百个,全做独热编码会让特征矩阵膨胀得很厉害。我一般会先看地区字段的基数再说:

# 检查文本特征基数,决定编码策略 for col in ["crime_type", "district", "street"]: if col in df.columns: print(f"{col} 的类别数量:{df[col].nunique()}") # 低基数类别:独热编码 crime_dummies = pd.get_dummies(df["crime_type"], prefix="crime") # 中高基数类别:按频率编码,出现次数映射为数值 district_freq = df["district"].value_counts() df["district_freq"] = df["district"].map(district_freq)

逻辑说明:pd.get_dummies对犯罪类型这类低基数字段特别合适,生成的是稀疏但可解释的0/1特征。而地区字段用频率编码(该地区出现的次数)来替代独热,既能保留“这个地区案件多不多”的信息,又不会把特征维度撑爆。街道名我直接丢弃,因为街道级别的粒度太细,模型学不透,还容易过拟合。

时间字段的处理比文本特征容易被忽视。把date拆成年、月、星期几,把time拆成小时,是预测任务的标准做法。犯罪预测里有个先验:夜间街头犯罪和白天入室盗窃的规律完全不同,所以hour这种周期性特征建议做环形编码,保序的同时还能让模型理解“23点和0点很近”:

df["hour"] = df["time"].dt.hour df["hour_sin"] = np.sin(2 * np.pi * df["hour"] / 24) df["hour_cos"] = np.cos(2 * np.pi * df["hour"] / 24) df["weekday"] = df["date"].dt.weekday

这段代码把小时拆成sin和cos两列,代价是特征多了一列,收益是模型不会再误以为“22点”和“2点”是两个远到天边的数值。刑事犯罪预测里,时间特征是除了犯罪类型之外最重要的信号,值得为它多花这点存储空间。

3.2 特征相关性筛查与数值标准化

特征工程做完之后,拿着几十列特征直接扔进模型,是一种常见的偷懒方式。特征之间高度相关(比如hour和hour_sin在某些区间近乎共线)会让部分模型的权重解释变形,甚至影响收敛速度。这时候跑一轮相关性矩阵,把相关系数超过0.8的特征对挑出来人工判断:

import numpy as np # 只对数值列做相关性计算 numeric_cols = df.select_dtypes(include=[np.number]).columns.tolist() corr = df[numeric_cols].corr() # 找出高相关特征对 high_pairs = [] for i in range(len(corr.columns)): for j in range(i+1, len(corr.columns)): if abs(corr.iloc[i, j]) > 0.8: high_pairs.append((corr.columns[i], corr.columns[j], round(corr.iloc[i, j], 3))) print("高相关特征对:", high_pairs)

逻辑说明:select_dtypes限定数值列,避免把犯罪类型这种编码后的列也拉进来算相关性(那没有意义)。corr()默认算皮尔逊相关系数,适合线性关系判断。如果发现hour_sin和hour_cos没有高相关,说明环形编码起作用了;发现其他来自同一原始字段的分解列高度相关,就考虑只保留其中一列。

标准化这一步要不要做,取决于你选的模型。逻辑回归和KNN这类基于距离的模型必须做标准化,不然后果是量纲大的特征直接主导权重。随机森林和LightGBM这类树模型不用标准化,因为它们只在特征值上做比较切分。所以先把要跑的三个模型定下来,再决定是否调用StandardScaler。

from sklearn.preprocessing import StandardScaler # 只对逻辑回归需要的特征做标准化 scaler = StandardScaler() X_scaled = scaler.fit_transform(X[["district_freq", "victim_age", "hour_sin", "hour_cos"]])

参数说明:StandardScaler把每列变成均值为0、方差为1的分布,fit_transform在训练集上拟合同时完成转换。这里只选了四个量纲不一致的列做标准化,像weekday这种取值范围本来就小的列不处理问题不大。注意,scaler在训练集上fit完之后,预测时要用同一个scaler.transform处理新数据,不能重新拟合,否则数据分布就错位了。

3.3 按时间切分训练集:模拟真实预测场景,不做随机打乱

刑事犯罪预测和电商点击率预测有个共同点:数据自带时间属性,而你真正要预测的是“未来”。如果把数据集随机打乱再切分,模型会看到未来数据去推断过去样本,测试集分数虚高,上线就翻车。正确姿势是按时间排序后切分。

df = df.sort_values("date").reset_index(drop=True) cutoff = int(len(df) * 0.8) train = df.iloc[:cutoff].copy() test = df.iloc[cutoff:].copy() y_train = train[target_col] X_train = train.drop(columns=[target_col, "date", "time"]) y_test = test[target_col] X_test = test.drop(columns=[target_col, "date", "time"])

切分逻辑:先按日期升序排列,前80%做训练集,后20%做测试集。这样测试集在时间上永远晚于训练集,最大限度模拟真实的上线预测场景。drop掉的date和time是原始时间字段,它们已经被拆成了hour_sin、weekday等特征,保留原始列只会让模型偷看时间本身。

这里踩过真坑:有次我图省事直接train_test_split随机切分,本地交叉验证F1分数0.72,换到按时间切分后直接跌到0.52。差距就是随机切分让同一个月的数据同时出现在训练和测试里,模型等于开了上帝视角。刑事犯罪预测这种事关公共安全的场景,宁可分数低但真实,也不要自欺欺人的高指标。

4. 三个源代码怎么用:从逻辑回归到随机森林再到LightGBM的递进

4.1 模型一:逻辑回归基线,先跑出及格线

不管最终选什么模型,第一个代码我都建议先跑逻辑回归。不是因为逻辑回归效果最好,而是它训练快、可解释、能给出概率输出,天然适合作为基线。刑事犯罪数据集的预测目标如果是“是否侦破”,逻辑回归能告诉你每个特征的系数方向——比如“夜间发生”是正系数还是负系数,这对业务理解极有帮助。

from sklearn.linear_model import LogisticRegression from sklearn.metrics import classification_report, f1_score model_lr = LogisticRegression( max_iter=1000, class_weight="balanced", C=1.0, solver="liblinear", random_state=42 ) model_lr.fit(X_train_scaled, y_train) y_pred_lr = model_lr.predict(X_test_scaled) print(classification_report(y_test, y_pred_lr))

参数说明:class_weight="balanced"是给正样本(通常是侦破的案件)更高权重,自动抵消类别不均衡;solver="liblinear"适合中小规模数据集和二分类场景,收敛稳定;max_iter=1000是防止部分特征量纲差异大导致迭代次数不够——如果收敛警告出现,优先调大这个值。C=1.0是正则化强度的倒数,越小正则化越强,先保持默认,后续可以按网格搜索再调。

逻辑回归跑完,看分类报告里三类指标:精确率(Precision)、召回率(Recall)、F1分数。刑事犯罪预测场景里,如果目标是“尽早发现可能侦破的案件”,召回率比精确率重要——宁可多预测几个错的,也不要漏掉真正能侦破的。基线模型的F1如果能有0.6以上,后面两个模型才有提升的空间;如果连0.5都不到,先回头查特征工程哪里出了问题,而不是急着换模型。

4.2 模型二:随机森林找出特征重要性

第二个代码用随机森林,目的不是拿它做最终预测,而是借助它的feature_importances_属性做特征筛选。随机森林对特征交互和异常值都比较鲁棒,即使特征没做标准化也能直接训练,适合机器辅助审查特征质量。

from sklearn.ensemble import RandomForestClassifier model_rf = RandomForestClassifier( n_estimators=200, max_depth=8, min_samples_leaf=10, class_weight="balanced", n_jobs=-1, random_state=42 ) model_rf.fit(X_train, y_train) # 输出特征重要性并用for循环打印 import numpy as np importances = model_rf.feature_importances_ indices = np.argsort(importances)[::-1][:15] for i in indices: print(f"{X_train.columns[i]}: {importances[i]:.4f}")

逻辑说明:随机森林在训练时会给每个特征计算“减少不纯度的贡献”,归一化后就是feature_importances_。n_estimators=200提供了足够的树数量去平均掉单棵树的噪声,max_depth=8限制树深避免对训练集过拟合,min_samples_leaf=10让每个叶子节点至少有10个样本才能被切分。这组参数适合几千行到几万行的数据集,如果数据量更大,可以适当加大深度。

特征重要性打印出来后,重点关注排名靠前的那几个,并把重要性几乎为0的列记下来。这里有个常见误区:不是直接把重要性低的特征全部删掉,而是要看它是否和别的特征高度相关。比如hour_sin和hour_cos重要性都不高,但它俩是时间周期的拆分,删掉一个会失去完整性。特征筛选的正确姿势是“先删冗余,再删无关”,重要性只提供参考,不做一刀切。

4.3 模型三:LightGBM做提升,用早停控制过拟合

第三个代码上LightGBM。它在中小规模数据集上训练速度快,对类别型特征也友好,是这类表格型预测任务的常见主力模型。LightGBM的直方图算法把特征值离散化成桶,内存占用小,训练速度比传统GBDT快一个量级,缺点是树多了容易过拟合,所以早停参数必开。

import lightgbm as lgb from sklearn.metrics import roc_auc_score model_lgb = lgb.LGBMClassifier( n_estimators=500, learning_rate=0.05, max_depth=6, num_leaves=31, subsample=0.8, colsample_bytree=0.8, class_weight="balanced", random_state=42 ) model_lgb.fit( X_train, y_train, eval_set=[(X_test, y_test)], eval_metric="auc", callbacks=[lgb.early_stopping(stopping_rounds=50)] ) y_pred_proba_lgb = model_lgb.predict_proba(X_test)[:, 1] print("LightGBM AUC:", round(roc_auc_score(y_test, y_pred_proba_lgb), 4))

参数说明:n_estimators=500是树的最大数量,但早停在50轮内AUC无提升时会把训练截断,所以这个数设大点没关系;learning_rate=0.05是每棵树的步长,调小能让训练更稳定,代价是需要的树更多;num_leaves=31是LightGBM特有的叶子数参数,过大容易过拟合,一般从31开始调;subsample=0.8和colsample_bytree=0.8分别让每棵树只用80%的样本和80%的特征列,起到随机化防过拟合的作用。

这段代码里eval_set传的是测试集,这是为了早停做监控,但要注意:早停用的评估集不是用来调参的,它更像一个“哨兵”,一旦连续50轮验证AUC不再上涨就停。真正的模型效果评估,还是要在没被早停看过的独立测试集上做。很多人忽略这一点,导致模型多少沾上了测试集的信息泄露。

5. 避坑指南:刑事犯罪数据集预测的5个典型翻车现场

5.1 现象:预测准确率95%,召回率却是0

有次我拿一个犯罪数据集跑完,看准确率0.95,心里正美,点开F1才傻眼——模型把所有案件都预测成“未侦破”,因为未侦破案件本身就占95%。准确率被多数类抬起来了,但模型对真正关心的正样本一个都抓不到。

原因:类别不均衡没有处理。训练时模型发现“全预测为多数类”的损失最小,就选了这条偷懒路径。

解决:检查标签分布,正样本占比低于20%时,在模型里设置class_weight="balanced",或者用SMOTE做少数类过采样,更关键的是评估指标改成F1和AUC,别再看准确率。刑事犯罪预测场景里,准确率是典型的误导性指标,F1才是跟业务对齐的度量。

5.2 现象:时间字段解析报错,程序直接崩溃

pd.to_datetime(df["date"])跑出一堆ParserError,原因多半是数据集里日期格式混着ISO写法和俄式日.月.年写法。俄罗斯数据里05.11.2017表示的到底是5月11日还是11月5日,是个老坑。

原因:日期格式不统一,pandas默认解析器无法自动区分日月的先后顺序。

解决:读入CSV时就指定parse_dates并带上格式参数。更稳妥的做法是用errors="coerce"先解析一遍,解析失败的行单独打印出来看格式,再根据不同格式分段转换,最后合并回原列。不要嫌麻烦,时间字段是后续所有时间切分和周期特征的基础,这里省事后面全是洞。

5.3 现象:独热编码后特征矩阵爆炸,内存直接吃不消

地区字段细到区级有上百个取值,我图省事一把pd.get_dummies全转成01列,数据量一大内存先告急,训练速度也慢成幻灯片。

原因:高基数分类特征用独热编码,维度随类别数线性膨胀。一百个地区就是一百列,虽然每一列都是稀疏的,但存储和计算成本都上去了。

解决:高位基数特征改用频率编码或者目标编码。频率编码把每个地区的案件数映射成该地区的“活跃度”,一列解决战斗。目标编码(用该地区正样本比例做特征)信息量更大,但容易过拟合,需要配合交叉验证使用。我在这类数据上一般先用频率编码,信息不够再尝试目标编码。

5.4 现象:测试集分数高得离谱,上线后预测却失灵

模型在验证集上AUC到0.85,落地到新数据上直接掉回0.5,基本等于瞎猜。排查到最后发现,原因是数据按时间收集,随机切分让训练和测试集混着同一时期的案件。

原因:时间泄露。模型见过了未来的数据,在测试集上等于开卷考试,一到真实预测(未来是不可见的)就原形毕露。

解决:切分数据时先按时间排序再切,测试集必须时间上晚于训练集。如果数据集跨度大,还可以按年份做分组交叉验证,每次用过去年份预测未来年份,这样评估出来的指标才是真实可上线的水平。

5.5 现象:三个源代码直接跑,文件不存在报错

解压完看到代码和数据集,直接python train.py,结果提示找不到CSV文件。查了半天,原来代码里写的文件名是crime_clean.csv,实际解压出来叫crime_data_clean.csv,一个下划线的差异就能卡半天。

原因:代码里的文件名和实际解压的文件名不一致,或者路径拼接时没有考虑解压目录的层级。

解决:拿到压缩包先ls -la或者find列全文件,再打开代码文件逐个核对引用路径。遇到路径不一致,不改代码,直接把文件重命名成代码预期的名字,或者改代码里的路径。这一步花五分钟,能省下面排错两小时。顺带提一句,代码里如果有/content/这类路径,那是作者当时跑的环境,本地要改成相对路径才能跑通。

6. 最后一步:用混淆矩阵和分类报告验证结果,再做一次最小模型部署

模型跑完不是终点,“预测结果可信”才是。我习惯用混淆矩阵收尾——四个格子里藏着一半的模型诊断信息,业务人员看着也直观。同时把训练好的模型存成本地文件,写一个几行的预测函数,这样“训练”和“使用”才算真正闭环。

import pandas as pd import numpy as np from sklearn.metrics import confusion_matrix, classification_report, ConfusionMatrixDisplay import matplotlib.pyplot as plt import joblib # 1. 混淆矩阵和分类报告 cm = confusion_matrix(y_test, y_pred_lgb) disp = ConfusionMatrixDisplay(confusion_matrix=cm) disp.plot(cmap="Blues") plt.savefig("confusion_matrix.png", dpi=150) print(classification_report(y_test, y_pred_lgb)) # 2. 把训练好的模型存成文件,留作预测用 joblib.dump(model_lgb, "crime_predict_model.joblib") joblib.dump(scaler, "crime_scaler.joblib")

这段代码前半段画混淆矩阵,后半段用joblib把模型和标准化器打包落盘。joblib.dump是sklearn生态里序列化模型的标准方式,比pickle对大对象的处理更高效。存下来的这两个文件,就是后续做预测的最小资产。

预测函数的最小闭环也很简单:新数据进来,走一遍和训练时完全相同的预处理——缺失值填充、时间字段拆分、特征编码映射,然后加载模型直接出概率。

def predict_crime_status(new_record): # 按训练时同样的路径转换:时长拆字段、编码映射 X_new = preprocess(new_record) # 假设preprocess复用训练时的特征工程 prob = model_lgb.predict_proba(X_new)[:, 1] return float(prob[0]) # 调用时返回的就是“案件侦破概率”,业务侧自行设定阈值 prob = predict_crime_status(new_case)

这里的阈值怎么定,才是刑事犯罪场景真正见水平的地方。逻辑回归和LightGBM输出的是概率,默认0.5的分类边界在类别不均衡时其实不适用。我的一般做法是:找出测试集上“召回率达到0.7对应的概率阈值”,用这个值做上线判定。比如0.3,那概率超过0.3就标记为预测侦破,宁可维持一定误报率,也不漏掉真正可侦破的案件。

整个项目做到这里,你已经把一份陌生的犯罪数据集走成了“清洗→特征→建模→验证→部署”的完整闭环。这类AI实战项目的价值也正在于此:数据集不是拿来收藏的,源代码不是拿来跑通就扔的,你把每一步的“为什么”想清楚,下次换一份数据集、换个预测目标,照样能做下来。我自己最深的体会是,模型选哪个其实无所谓,数据质量、标签定义和评估口径这三件事,才是这类预测项目真正的胜负手。希望这份拆解能帮你在自己的数据集上少走两步弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询