☰
机器学习实战:治安案件预警系统从特征工程到XGBoost调优
2026/10/31 1:03:13 网站建设 项目流程

简介:面向高校毕业设计、课程设计及期末大作业场景的治安案件预警系统,以机器学习算法为核心,围绕治安案件数据进行采集、分析、管理与预警,完整覆盖Java后端、HTML/CSS/JS前端页面以及模型调用模块,能够帮助学习者理解从数据录入、案件管理到预警输出的整个项目实现链路。压缩包内共225个文件,包含15个Java源码、30个class编译文件、22个HTML页面、36个JavaScript与32个CSS样式文件,同时提供SQL数据库脚本、XML配置及字体图片等辅助素材;整体大小仅4.71MB,但前端资源与后端逻辑拆分清晰,按目录索引即可快速定位关键代码。目前已有37人学习下载。需要完成毕设或课程设计的学生,可借助这套系统快速搭建项目骨架,梳理案件登记、人员档案、预警规则调用等功能模块的编码思路;同时可参考数据库表设计、接口配置和页面交互方式,方便进行二次开发、功能扩展或演示讲解。

1. 治安案件预警系统到底在预警什么:先想通这几个问题

拿到“基于机器学习的治安案件预警系统”这种毕业设题目,很多人的第一反应是找模型、调参,把机器学习入门视频重新刷一遍。但我实际带过完整项目后可以告诉你:这类预警系统的难点不在模型,而在数据怎么按时间切分、特征怎么构造才不泄露未来信息、模型输出的概率怎么转成预警指令这三件事上。这个 zip 包把“历史案件数据清洗 → 特征工程 → 算法建模 → 预警等级输出 → 可视化展示”这条链路完整串了起来,源码和数据集都齐。适合三类人:做毕业设计、课程设计、期末大作业的学生;刚入门想找一个机器学习实战项目案例来练手的新人;以及想理解预警系统在治安场景里到底能做什么、不能做什么的从业者。下面我先讲数据怎么喂进模型,再讲选型和评估,最后集中说踩坑和进阶方向。

2. 数据处理与特征工程:把案件记录变成模型能吃的特征矩阵

一套典型的治安案件数据集,字段上大致有这么几类:案发时间、案发地经纬度或地址描述、场所类型、案件类型,以及一些受害人基础信息。原始 CSV 里一行一个案件,格式和脏数据都比较多,直接扔给模型肯定不行。治安发案有一个很突出的特点:强时空聚集性。某个地铁站附近、某个夜市街区,发案密度天然比其他地方高几个量级;深夜和节假日前后,案件结构又会明显改变。数据处理阶段要做的,就是把这几个结构搬进特征矩阵里。我下面按三步走:先做基础清洗和时间特征,再做空间与场所特征,最后讲切分方式和目标变量定义。

2.1 字段清洗与时间特征拆解

第一步先把数据读进来,看看形状、字段类型和缺失情况。

import pandas as pd import numpy as np df = pd.read_csv("cases.csv", parse_dates=["alarm_time"]) print(df.info()) print(df["case_type"].value_counts())

读进来之后先不要急着做特征,先看case_type的分布。这一步很重要,因为后面要确定正负样本怎么切,类别不平衡到什么程度会直接影响模型选型。治安案件里,“盗窃”“诈骗”“打架斗殴”通常是最多的几类,如果拿全量数据做多分类,类别之间样本量差几倍甚至几十倍,模型很容易被多数类带偏。

清洗阶段我只做两件必要的事:删除关键字段缺失的样本,把明显异常的时间过滤掉。经纬度缺失的案件没法做空间特征,只能删;案发时间落在未来或者早于数据采集年份的,也删。

df = df.dropna(subset=["case_type", "longitude", "latitude"]) df = df[(df["alarm_time"] >= "2020-01-01") & (df["alarm_time"] <= "2024-12-31")]

时间字段拆分是这一步的核心。一个完整的时间戳对模型来说是黑匣子,但把“星期几”“几点”“是否周末”拆出来之后,模型才能抓到治安发案的时间规律。

df["hour"] = df["alarm_time"].dt.hour df["day_of_week"] = df["alarm_time"].dt.dayofweek df["is_weekend"] = (df["day_of_week"] >= 5).astype(int) df["month"] = df["alarm_time"].dt.month

hour取 0 到 23 的整点值,day_of_week周一为 0、周日为 6。is_weekend是我手动从 day_of_week 映射出来的,虽然树模型也能自己学这个逻辑,但显式给出来之后,线性模型同样能用上,后面跑逻辑回归时就省事。

还有一个容易被忽略的特征:距离最近法定节假日的天数。节假日前后人员流动大,盗窃和诈骗的发案规律和平时明显不同。

holidays = pd.to_datetime(["2024-01-01", "2024-02-10", "2024-04-04", "2024-05-01", "2024-06-10", "2024-10-01"]) df["nearest_holiday_dist"] = df["alarm_time"].apply( lambda x: min(abs((x - h).days) for h in holidays) )

nearest_holiday_dist计算的是每条案件距离最近节假日的天数,0 表示当天就是节假日,负数表示节前,正数表示节后。这个特征的逻辑是:节假日当天和前后一两天,重点区域案件密度往往陡增,模型能据此学到日历效应。

2.2 空间网格与场所类别特征

治安数据里,经纬度是最有价值的原始信息,但它不能直接用。经度和纬度是两个连续数值,模型只能学到“这两个数值和案件之间的关系”,学不到“某一片区域发案密度比其他地方高”这种空间结构。常见做法是把目标区域切成网格,让模型在网格粒度上做判断。

grid_size = 0.01 lon_min, lon_max = df["longitude"].min(), df["longitude"].max() lat_min, lat_max = df["latitude"].min(), df["latitude"].max() def grid_id(lon, lat): gx = int((lon - lon_min) // grid_size) gy = int((lat - lat_min) // grid_size) return f"{gx}_{gy}" df["grid"] = df.apply(lambda r: grid_id(r["longitude"], r["latitude"]), axis=1)

grid_size = 0.01对应大约一公里的网格。这个参数不是随便定的,治安数据的空间聚集性通常在 500 米到 2 公里这个尺度上体现。网格设得太细,比如 0.003,每个格子里的案件数太少,统计特征全是稀疏值;设得太粗,比如 0.05,会把好几个高发区域并到一个格子里,空间区分度就丢了。我一般会先画一下案件分布热力图,根据城市实际范围调整网格大小。

场所类型也需要编码。常见字段里有“商业场所”“住宅小区”“教育机构”“交通枢纽”这些取值,是文本。树模型可以直接用整数标签,但要注意别把类别编码成有顺序的数字。

from sklearn.preprocessing import LabelEncoder le = LabelEncoder() df["loc_type_le"] = le.fit_transform(df["location_type"])

这里有个边界要讲清楚:标签编码喂给随机森林或 XGBoost 没问题,因为树模型做的是划分,不关心特征数值的相对大小;但如果后面要跑逻辑回归,同一个特征会被当成有序数值,8 号场所会被模型理解成比 7 号大、比 9 号小,这是错的。逻辑回归场景下,我会把location_type改回 OneHot 编码再进入模型。

2.3 按时间切分与目标变量定义

数据集切分是治安预警系统和普通分类任务最大的区别。很多课程作业里随机切分训练集和测试集,在治安场景里是灾难:同一天发生的案件会被同时分到两边,模型等于开卷考试,测试分数虚高,换到真实数据立刻打回原形。预警系统的预测目标是“未来一段时间”,所以必须按时间切分。

split_date = pd.Timestamp("2023-12-31") train_df = df[df["alarm_time"] <= split_date].copy() test_df = df[df["alarm_time"] > split_date].copy() print(f"train: {len(train_df)}, test: {len(test_df)}")

切完之后,前文说的网格密度特征就要注意了。如果直接在全量数据上做groupby("grid").count(),再回填到每一条案件上,这个特征就包含了未来信息——测试集年份的案件数也参与统计了,属于数据泄露。

grid_stats = train_df.groupby("grid")["case_id"].count().rename("grid_case_count") train_df = train_df.merge(grid_stats, how="left", on="grid") test_df = test_df.merge(grid_stats, how="left", on="grid").fillna({"grid_case_count": 0})

正确做法是先切分,再用训练集时期的案件去统计网格密度,然后映射到测试集。测试集里如果出现训练期没有案件的新网格,密度填 0。这段代码里的fillna就是在处理这种情况。

最后是目标变量。课程设计里一般不做多分类,而是把任务定义成二分类:预测某一条案件记录“是不是盗窃案”或者“是否属于高风险案件”。正类建议选数据里样本量最大、业务上最关心的那个类型,通常是盗窃。

feature_cols = ["hour", "day_of_week", "is_weekend", "month", "nearest_holiday_dist", "loc_type_le", "grid_case_count", "longitude", "latitude"] X_train = train_df[feature_cols].values y_train = (train_df["case_type"] == "盗窃").astype(int).values

到这一步,数据算是真正变成了模型能吃的特征矩阵。后面所有模型,都在这份特征上做实验。

3. 模型训练与算法选型:逻辑回归、随机森林到 XGBoost 的实践对比

这一章讲建模。治安预警场景里,样本量通常几千到几万,特征维度几十个,这种规模下没有哪个模型天生无敌。我习惯的实验顺序是:先用逻辑回归跑出一个可解释的基线,再用随机森林和 XGBoost 把效果往上顶,最后根据情况决定要不要加一个简单的神经网络做对照。这个顺序也是机器学习基础知识里反复强调的“先简单后复杂”,别一上来就上深度学习。

3.1 逻辑回归基线:先看可解释的下限

逻辑回归在预警系统里的价值有两层:一是快速验证特征是否有效,二是它输出的概率可以被业务方理解。治安民警不关心什么是特征重要度,他们关心“为什么这条案子被评为高风险”,逻辑回归的系数就能直接给出答案:因为发生在凌晨、地点是商业街区、该区域历史发案密度高。

from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) lr = LogisticRegression(max_iter=200, C=1.0) lr.fit(X_train_scaled, y_train) print("train acc:", lr.score(X_train_scaled, y_train)) print("test acc:", lr.score(X_test_scaled, y_test))

跑逻辑回归前必须做标准化。逻辑回归的损失函数里,特征的量纲会直接影响梯度更新和正则化效果,经纬度这种数值大、小时这种数值小的特征不统一量纲,模型会把注意力全放在数值大的特征上。C=1.0是正则强度的倒数,C 越小正则越强、系数越向 0 收缩;如果发现训练集和测试集准确率差得大,可以把 C 调小到 0.1 试试。

3.2 随机森林与 XGBoost 调参:树模型才是主力

治安预警这类表格数据,随机森林和 XGBoost 才是真正的主力,原因有三个:不需要归一化,特征尺度不影响树的分裂;能捕捉特征之间的非线性关系;自带特征重要性,方便做特征筛选。周志华西瓜书里对随机森林的介绍和 XGBoost 官方文档的实操案例我都按这个路子对照过。

from sklearn.ensemble import RandomForestClassifier rf = RandomForestClassifier( n_estimators=300, max_depth=8, min_samples_leaf=5, class_weight="balanced", random_state=42 ) rf.fit(X_train, y_train)
from xgboost import XGBClassifier xgb = XGBClassifier( n_estimators=200, max_depth=6, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, scale_pos_weight=5, eval_metric="auc" ) xgb.fit(X_train, y_train, eval_set=[(X_test, y_test)], verbose=50)

说一下这几个参数的实际含义。class_weight="balanced"是让随机森林按类别比例自动调整权重,正类样本少的就多给惩罚。XGBoost 里对应的参数是scale_pos_weight,我习惯先按“负样本数除以正样本数”的比值设置,比如正负比是 1:5 就填 5,之后再微调。max_depth=6和min_samples_leaf=5是控制复杂度的,治安数据噪声不小,树太深容易把个别案件的特征当规律。subsample=0.8和colsample_bytree=0.8表示每棵树随机用 80% 样本、80% 特征,这是 XGBoost 对抗过拟合最常用的手段。

另外提一句版本问题:旧版 XGBoost 在标签不是 0 和 1 起始时会报错,需要加use_label_encoder=False;新版 1.7 之后已经不需要这个参数了,加了反而会影响自动对齐逻辑。

3.3 深度学习对照实验:什么时候才值得上神经网络

很多课程设计把“深度学习”当成必选项,我理解这个诉求,但要说清楚边界。治安案件数据是典型的结构化表格,样本量几千条、特征几十个,这个规模下简单 MLP 很难打过调好的 XGBoost。我一般会加一个 MLP 做对照实验,目的不是让它当主模型,而是让报告里有一个“深度学习方案对比”的结论。

import torch import torch.nn as nn class SimpleMLP(nn.Module): def __init__(self, in_dim): super().__init__() self.net = nn.Sequential( nn.Linear(in_dim, 64), nn.ReLU(), nn.Dropout(0.3), nn.Linear(64, 1) ) def forward(self, x): return self.net(x) model = SimpleMLP(X_train.shape[1]) criterion = nn.BCEWithLogitsLoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) for epoch in range(30): model.train() logits = model(torch.tensor(X_train_scaled, dtype=torch.float32)).squeeze() loss = criterion(logits, torch.tensor(y_train, dtype=torch.float32)) optimizer.zero_grad() loss.backward() optimizer.step() if epoch % 5 == 0: print(f"epoch {epoch}: loss {loss.item():.4f}")

Dropout(0.3)是防止 MLP 在小样本上过拟合。如果做课设时选了李宏毅老师的作业思路,或者照着吴恩达的课程框架做,把这段代码放进去作为对照实验,结论通常是“MLP 与 XGBoost 在同一量级,但调参成本更高”。这个结论本身就是有说服力的实验结果。

4. 预警评估与阈值设定:模型概率怎么变成预警等级

模型训练完只是完成了前半程。治安预警系统的输出不能是“这条案子概率 0.6”,业务方看不懂,也没法执行。真正要做的,是把模型输出的概率映射成预警等级和处置建议。这一章讲三件事:阈值怎么定、用哪些指标评估、怎么验证预警效果在时间上是稳定的。

4.1 从概率到预警等级:阈值不能拍脑袋

二分类模型默认输出一个概率,sklearn 里predict按概率是否大于 0.5 来给标签。但 0.5 在极度不平衡的治安数据里往往不是最优切分点。我一般不用固定阈值,而是先按业务承载力估算:一个派出所辖区,每天能实地核查的预警条数是有限的,预警系统一天推 50 条,处置力量跟不上,民警很快就不信这个系统了。

def warn_level(prob, low=0.3, high=0.65): if prob >= high: return "Ⅰ级预警" elif prob >= low: return "Ⅱ级预警" else: return "暂不预警" test_df["prob"] = xgb.predict_proba(X_test)[:, 1] test_df["level"] = test_df["prob"].apply(lambda p: warn_level(p))

low=0.3表示概率超过 0.3 就进入关注范围,超过 0.65 直接列为重点。这两个数不是算出来的,是先定业务目标再回推的。

更稳妥的做法是使用分位数:把测试集所有预测概率从小到大排序,取前 15% 作为重点预警区间,再反过来求这个概率值作为阈值。

top_n = int(len(test_df) * 0.15) threshold = test_df["prob"].sort_values(ascending=False).iloc[top_n - 1] print(f"top 15% 阈值: {threshold:.4f}")

按分位数定阈值的优势是对样本分布变化不敏感,不管数据怎么波动,每天推给业务方的预警量都是稳定的,不会出现某天概率整体偏高导致预警爆炸的情况。

4.2 不平衡分类的评估指标:别被准确率带偏

治安预警是典型的不平衡分类,正类占比可能只有 5% 到 10%。这种数据下,准确率是陷阱:模型把所有样本都预测成负类,准确率也有 90% 以上,但预警系统完全失效。我自己的习惯是先看混淆矩阵,再看精确率和召回率随阈值变化的 PR 曲线。

from sklearn.metrics import precision_recall_curve, f1_score, confusion_matrix import matplotlib.pyplot as plt precision, recall, thresholds = precision_recall_curve(y_test, test_df["prob"]) plt.plot(recall, precision) plt.xlabel("Recall") plt.ylabel("Precision") plt.title("PR Curve") plt.show() best_f1_idx = ((2 * precision * recall) / (precision + recall)).argmax() print(f"best F1 threshold: {thresholds[best_f1_idx]:.4f}")

精确率表示“预警的案件里有多少是真的”,召回率表示“真实案件里有多少被预警了”,两者天然互相制约。治安场景里,我一般优先保精确率,因为预警过载会消耗一线警力,等业务方建立起信任后,再逐步降低阈值提升召回率。F1 最大时对应的阈值可以作为参考起点,但最终阈值还是要结合每天能接受的预警条数来定。

指标含义治安场景里的实际意义
准确率预测对的占比数据不平衡时基本无参考价值
精确率预警中真实案件占比预警信息的水分
召回率真实案件被预警占比漏掉案件的比例
F1精确率与召回率的调和平均均衡评估模型性价比

4.3 滚动时间窗回测:验证预警系统的稳定性

单次时间切分只能说明模型在某一个时间段表现尚可,不能证明它在不同月份都稳定。治安案件有明显的季节性波动,暑期和年底的发案结构就不一样。我会对最近的 12 个月做滚动回测,每个月单独切片评估一次。

def rolling_backtest(df, model, feature_cols, months=12): results = [] for i in range(1, months + 1): test_start = pd.Timestamp(f"2024-{i:02d}-01") test_end = pd.Timestamp(f"2024-{i:02d}-28") test_mask = (df["alarm_time"] >= test_start) & (df["alarm_time"] <= test_end) test_m = df[test_mask] if len(test_m) < 20: continue probs = model.predict_proba(test_m[feature_cols])[:, 1] p, r, _ = precision_recall_curve( (test_m["case_type"] == "盗窃").astype(int), probs) f1 = (2 * p * r / (p + r)).max() results.append({"month": f"2024-{i:02d}", "best_f1": f1}) return pd.DataFrame(results)

这个函数里每个月都在重新训练吗?严格说应该每月重新训练一次,模拟线上不断更新的状态。但如果只是为了课程实验,用同一个模型在不同月份上评估,也能看出模型在时间维度上的稳定性。我建议在报告里说明这一点,别把两种口径混在一起。

5. 避坑:治安预警系统实战中的五个典型问题

这一章直接给结论。以下五个问题是我在类似项目里真正踩过的,也都收到过群里同学的反馈,按“现象、原因、解决”写清楚。

5.1 数据泄露:训练集里混进了未来的案件量

现象:训练集上 AUC 跑到 0.97,换成按时间切分的测试集后 AUC 掉到 0.68,模型整体失效。

原因:网格发案密度特征在全量数据上统计,测试时间段内未来发生的案件也被统计进去了,等于把答案提前写进了特征。

解决:所有统计类特征必须先按时间切分,只用训练期数据做统计,再映射到测试期。我在 2.3 节里已经展示了正确写法,这里再强调一次:在代码里把特征构造封装成独立函数,入参是训练集和测试集,内部先算训练集统计量再回填,这样就不会手滑。

5.2 类别不平衡:模型倾向“全都不预警”

现象:测试集上召回率接近 0,所有预警概率都低于 0.2,模型输出全部是“暂不预警”。

原因:正负样本比可能到 1:10 甚至更低,模型默认倾向于把样本判为多数类,因为这样总体损失最小。

解决:优先调权重而不是盲目做采样。随机森林加class_weight="balanced",XGBoost 设置scale_pos_weight为负正样本比值。如果这样效果还不够,再考虑对多数类做下采样。我不太建议对治安数据用 SMOTE 做过采样,空间网格特征很容易生成实际上不存在的点位,比如某个偏僻角落突然冒出一条高发案件。

5.3 小时特征没有周期编码,深夜案件几乎漏光

现象:白天时段的案件识别效果尚可,凌晨 2 点到 4 点的案件几乎全部漏掉,模型认为凌晨发案概率极低。

原因:hour特征是 0 到 23 的线性数值,凌晨 23 点和 0 点的实际间隔是 1 小时,但数值距离是 23。模型拿到的“距离感”是错的,学不到昼夜循环的结构。

解决:对小时做周期编码,用正弦和余弦把 23 点和 0 点拉近。

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

改成两个特征之后,任何时刻的小时都能表示成圆上的一个点,0 点和 23 点在特征空间里相邻。加这两个特征后,凌晨案件召回率往往能明显回升。

5.4 评估指标选错,把高准确率当成了好模型

现象:汇报材料里写“准确率 98%”,但每天实际预警的命中率不到 10%,业务方开始不信任系统输出。

原因:在不平衡数据里,准确率是虚高指标,全部预测成负类就有 98% 准确率,但警情预测系统没有发挥任何作用。

解决:把报告口径从“准确率”改成“预警命中率(精确率)+ 覆盖率(召回率)+ 每日预警条数”。这三个指标才是业务方能感知的。模型对比时,用 PR 曲线下面积而不是 accuracy 来选模型。这个坑在几乎所有不平衡分类项目里都会出现,治安场景尤其致命,因为信任一旦丢了,后面再优化都很难拉回来。

5.5 特征因果边界不清,把案发后的统计量也当特征

现象:某个特征在模型里的重要度高得离谱,但换一个时间段验证,模型效果断崖式下跌。

原因:特征清单里混进了“案发后才有值”的字段,比如是否已破案、嫌疑人是否已抓获、损失金额的最终核定值。建模时这些字段确实能强烈区分正负样本,但预测时根本取不到,属于典型的因果倒置。

解决:每写一个特征就问自己一句:在预测那一刻,这个值能不能拿到?拿不到,就别进特征矩阵。我习惯在项目里维护一张特征清单,每一列标注三个信息:特征名称、数据产生时间、预测时是否可得。这张表能挡住 90% 以上的低级泄露问题。

6. 进阶:把模型输出做成预警看板与日报,让结果落地

模型效果调好之后,最后一步是把结果展示出来。治安预警系统的价值要通过“信息可视化”体现,业务方看的是大屏和日报,不是 Jupyter Notebook 里的精确率和召回率。这一部分我给两个可以直接用的思路。

6.1 Flask + ECharts 预警看板

用 Flask 起一个轻量服务,把每日预警结果通过 JSON 接口输出,前端用 ECharts 渲染地图热力图和案件类型分布。课程设计的演示环节,这一个看板比十张图表都好用。

from flask import Flask, jsonify import pandas as pd app = Flask(__name__) @app.route("/api/warning_today") def warning_today(): today = pd.Timestamp.now().normalize() data = pd.read_sql_query( "SELECT * FROM warnings WHERE alarm_date = ?", conn, params=(today,) ) return jsonify(data.to_dict(orient="records")) if __name__ == "__main__": app.run(debug=True, host="0.0.0.0", port=5000)

接口返回每日预警案件列表,前端按网格聚合后渲染热力图。注意每次接口调用都要重新查询数据库,不要在服务启动时把数据缓存在内存里,因为预警结果是每天更新的。

6.2 按日生成重点预警区域清单

光有看板还不够,一线处置需要的是“明天重点关注哪几个区域”的可执行清单。我一般会把每日预测概率排序后截取前 20 条,生成一份 Excel 日报,随看板一起输出。

def generate_daily_report(pred_df, top_n=20): pred_df = pred_df.sort_values("prob", ascending=False) focus_areas = pred_df.head(top_n)[["grid", "location_type", "prob", "level"]] focus_areas["reason"] = focus_areas.apply( lambda r: f"场所类型:{r['location_type']}, 风险等级:{r['level']}", axis=1 ) focus_areas.to_excel("daily_alert_report.xlsx", index=False)

这份日报里有网格编号、场所类型、风险等级和预警理由。网格编号可以直接对到电子地图上,处置民警看一眼就知道明天重点巡查哪片区域。比直接扔一个概率值有用得多。

从那一次数据泄露翻车之后,我每次做治安预警这类带时间属性的项目,第一步都不是写模型,而是先在纸面上画一条时间轴,标清楚每个特征的产生时间点和模型的预测时点,特征构造脚本一律按时间窗口封装后再跑。这个习惯帮我避掉了后续绝大多数隐蔽的坑,建议你拿到这套资源后,也照这个流程先梳理一遍再动手改代码。希望帮到你。

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

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

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

立即咨询