资讯动态

百万级城市犯罪数据实战:从脏数据清洗到可解释热点预测

发布时间:2026/10/29 15:19:22 来源:尧图企业网站定制
1. 项目缘起与整体设计思路拆解城市犯罪数据这个领域很多人第一反应是数据敏感、不好碰但我实际做下来发现真正难的不是数据本身而是从一堆脏乱差的原始记录里提炼出可解释、可复现的预测逻辑。我手上这个项目原始数据量在百万级——大约一百二十万条案件记录时间跨度五年字段包括案件类型、发生时间、经纬度、辖区编号、处理状态等。目标很明确清洗出一份能用的结构化数据做时空热点分析最后跑一个可解释的热点预测模型。先说清楚这个内容适合谁看。如果你是有一定数据处理基础、想完整走一遍脏数据到预测模型链路的从业者这篇基本可以照着抄作业如果你是刚入门的数据分析新手建议先把 Pandas 和地理数据处理的基本操作过一遍再回来不然中间某些步骤会卡住。整条链路我拆成四块数据清洗、时空特征工程、热点识别、预测建模。每一块都有坑我会把踩过的和绕过去的都写出来。为什么这么设计因为犯罪数据有三个天然特性决定了不能上来就建模。第一记录质量极差同一个案件在不同系统里可能有三四条重复记录时间格式五花八门经纬度缺失率能到百分之十几。第二时空强相关犯罪不是随机撒点它有明显的时间周期比如夜间盗窃高发和空间聚集某些街区案件密度是平均值的数倍。第三类别极度不平衡轻微案件占大头严重案件样本极少直接训练分类模型会严重偏向多数类。所以我的思路是先把数据洗干净到可信再用时空聚合把信号放大最后选对模型和评估方式。跳过任何一步后面的结论都站不住。这里有个关键取舍值得说。很多人拿到这类数据第一反应是上深度学习觉得模型越复杂越准。我试过在小样本类别上效果反而不如传统的树模型加特征工程。原因很简单犯罪数据的信噪比低深度模型容易过拟合噪声而梯度提升树配合精心设计的时空特征在可解释性和稳定性上都更胜一筹。所以整个方案我走的是重特征工程、轻模型复杂度的路线这也是我在实际项目里反复验证过的稳妥选择。2. 数据清洗百万级脏数据的实战处理要点2.1 先去重但别急着全删百万级数据里重复记录是头号问题。我的原始数据里重复率大概在百分之八到百分之十二之间波动不同辖区差异很大。去重的难点在于完全相同的行好处理但同一案件不同录入的重复才是大头。比如同一个案件一条记录时间是2021-03-05 14:30另一条是2021/3/5 14:30:00经纬度还差了几米这种靠简单的drop_duplicates()根本去不掉。我的做法是构造一个复合指纹把案件类型、辖区编号、发生日期精确到天、经纬度四舍五入到小数点后三位拼成一个字符串作为去重键。精确到天而不是精确到秒是因为同一案件多次录入时秒级时间经常有偏差经纬度保留三位大约是一百米精度足以区分不同地点又不会因为录入误差误判为不同案件。import pandas as pd df[fingerprint] ( df[case_type].astype(str) _ df[district_id].astype(str) _ df[occur_date].dt.strftime(%Y%m%d) _ df[lat].round(3).astype(str) _ df[lon].round(3).astype(str) ) df_dedup df.drop_duplicates(subsetfingerprint, keepfirst)注意keepfirst之前一定要先按录入时间排序保留最早录入的那条因为后续录入的往往是补录或修正字段完整性反而不如原始记录。我一开始没排序结果保留了一批字段缺失的补录记录后面清洗多花了两天。2.2 时间字段的归一化陷阱时间格式混乱是第二个大坑。原始数据里我统计出至少七种时间格式标准 ISO、斜杠分隔、中文年月日、时间戳、甚至还有 Excel 序列号混进来的。统一用pd.to_datetime加errorscoerce处理转换失败的单独拎出来人工核对。df[occur_time] pd.to_datetime(df[occur_time], errorscoerce, formatmixed) bad_time df[df[occur_time].isna()]这里有个经验转换失败率超过百分之二就要警惕说明原始数据里可能有系统性的格式问题不是零星脏数据。我当时失败率到了百分之三排查发现是某个辖区整批数据用了本地化的日期格式单独写了个解析规则才解决。另外时间字段一定要拆出小时、星期、月份三个衍生特征后面热点分析全靠它们。2.3 经纬度缺失与异常值的处理策略经纬度缺失率我这边是百分之十三左右异常值比如落在城市边界外的坐标大概百分之二。缺失值不能简单删因为删掉的可能恰好是某些辖区的系统性缺失会导致空间分布偏差。我的处理分三层第一层能用地址反查的补全。原始数据里有地址文本字段虽然不规范但提取出关键路段名后可以匹配到辖区中心点坐标精度虽然只到街区级但比直接删掉强。第二层辖区中心点兜底。实在补不全的用该辖区所有有效坐标的中位数填充并打一个coord_filled标记位后续建模时把这个标记作为特征传入让模型自己决定要不要信任这条记录。第三层异常值裁剪。用城市行政边界做空间裁剪落在边界外的坐标直接置为缺失再走兜底逻辑。这里我用的是geopandas做空间判断比手写经纬度范围判断可靠得多。import geopandas as gpd from shapely.geometry import Point city_boundary gpd.read_file(city_boundary.geojson) gdf gpd.GeoDataFrame(df, geometrygpd.points_from_xy(df[lon], df[lat])) valid_mask gdf.within(city_boundary.unary_union) df.loc[~valid_mask, [lat, lon]] None实操心得坐标填充一定要留标记位。我早期版本没留模型把填充坐标当成真实信号导致某些辖区热点被虚假放大后来加了标记位重新训练才纠正过来。这个细节在常规文档里基本不会提但实际影响很大。3. 时空特征工程把信号从噪声里捞出来3.1 时间维度的周期特征构造犯罪数据的时间规律非常强但直接喂原始时间戳给模型等于没喂。我构造了三组时间特征小时段、星期几、月份并且做了周期编码而不是简单的 one-hot。为什么用周期编码因为小时和星期是循环的23 点和 0 点在数值上差 23但实际只差 1 小时one-hot 又会让特征维度爆炸且丢失邻近关系。用正弦余弦编码能保留这种循环性。import numpy as np df[hour_sin] np.sin(2 * np.pi * df[occur_time].dt.hour / 24) df[hour_cos] np.cos(2 * np.pi * df[occur_time].dt.hour / 24) df[dow_sin] np.sin(2 * np.pi * df[occur_time].dt.dayofweek / 7) df[dow_cos] np.cos(2 * np.pi * df[occur_time].dt.dayofweek / 7)实测下来周期编码比 one-hot 在树模型上的表现略好而且特征维度从几十维降到四维训练速度快了不少。另外我加了一个是否节假日的布尔特征这个对盗窃类案件的影响很明显节假日期间某些商圈的案件密度会翻倍。3.2 空间网格化热点分析的基础单元空间特征的核心是网格化。我把城市按五百米乘五百米切网格每个网格作为一个分析单元。为什么是五百米这是个权衡网格太大热点被平均掉看不出来网格太小每个格子样本太少统计不稳定。我试过二百米、五百米、一千米三档五百米在样本量和分辨率之间平衡最好百万级数据下平均每格有两百条左右记录统计上够用。网格编号用geohash或者简单的行列号都行我用的是行列号因为后续做邻域特征时计算方便。每个网格聚合出这些统计量案件总数、各类案件占比、夜间案件占比、周末案件占比、近三十天案件数滑动窗口。这些聚合特征直接构成了热点识别和预测的输入。df[grid_x] ((df[lon] - lon_min) / 0.005).astype(int) df[grid_y] ((df[lat] - lat_min) / 0.005).astype(int) grid_stats df.groupby([grid_x, grid_y]).agg( total_cases(case_id, count), night_ratio(is_night, mean), weekend_ratio(is_weekend, mean) ).reset_index()注意网格边界要固定不能每次分析重新计算否则不同批次的结果没法对比。我一开始每次跑都重新算边界导致两次分析的热点位置对不上排查了半天才发现是网格原点漂移了。固定边界后这个问题再没出现过。3.3 邻域特征让每个格子知道邻居的情况单看一个网格的案件数不够犯罪有空间溢出效应一个高发格子旁边的格子往往也不低。所以我给每个网格加了邻域特征周围八个格子的案件数均值、最大值、以及本格与邻域均值的差值。差值这个特征特别有用它能区分本身高发和被高发区包围两种情况前者是真正的热点核心后者是边缘扩散区。计算邻域特征用卷积的思路最快把网格统计量排成矩阵用一个三乘三的全一核做卷积再减去中心值就是邻域和。百万级数据下这个操作几秒钟就跑完了比逐格循环快几个数量级。from scipy.signal import convolve2d grid_matrix grid_stats.pivot(indexgrid_y, columnsgrid_x, valuestotal_cases).fillna(0) kernel np.ones((3, 3)) neighbor_sum convolve2d(grid_matrix.values, kernel, modesame) - grid_matrix.values这套邻域特征加上时间周期特征构成了我整个特征体系的主干。后面模型效果好不好八成取决于这部分做得细不细。4. 热点识别从统计到可视化的落地方法4.1 核密度估计找热点核心热点识别我用的是核密度估计比简单的网格计数更平滑能避免网格边界造成的锯齿效应。带宽选择是个关键参数太大热点糊成一片太小全是孤立尖峰。我的经验值是带宽取网格边长的两到三倍也就是一千到一千五百米这个尺度下热点区域连片且边界清晰。from sklearn.neighbors import KernelDensity coords df[[lat, lon]].values kde KernelDensity(bandwidth0.012, metrichaversine) kde.fit(np.radians(coords))提示用 haversine 距离而不是欧氏距离因为经纬度直接算欧氏距离在高纬度地区会严重失真。这个坑我踩过早期用欧氏距离算出来的热点在南北方向被拉长了换成球面距离后形状才正常。4.2 热点分级与显著性检验光有密度值不够还得判断哪些密度是统计显著的哪些只是随机波动。我用的是蒙特卡洛模拟把案件点在城市范围内随机重排一千次每次算一遍核密度取每个位置一千次模拟的百分之九十五分位数作为显著性阈值。超过阈值的才算真热点这样能有效过滤掉小样本造成的假热点。分级上我分了三档核心热点密度超过阈值两倍以上、次级热点超过阈值但不到两倍、潜在热点接近阈值。这个分级对后续警力部署的参考价值比单一密度值大得多因为核心热点和潜在热点的处置策略完全不同。4.3 热点的时间切片对比静态热点图只能看整体分布真正有用的是分时段热点对比。我按白天夜间、工作日周末切了四张热点图发现同一个区域白天是商业区盗窃热点夜间变成另一类案件热点空间上重叠但时间上错开。这种时空耦合的规律单看整体热点图完全发现不了。做时间切片时要注意样本量切太细每片样本不够核密度估计会不稳定。我的做法是每个切片至少保证五万条以上记录不够就合并相邻时段。四张切片图出来后配合网格统计表基本能把一个城市的安全态势讲清楚。5. 预测建模可解释优先的模型选型与评估5.1 为什么选梯度提升树而不是深度学习前面提过我最终选的是梯度提升树具体用的是 LightGBM。理由有三第一特征里有大量类别型和聚合统计特征树模型天然适配不需要复杂的嵌入层设计第二样本类别不平衡严重树模型的类别权重调节直接有效第三也是最重要的可解释性。这类预测结果最终要给人看、给人用能说清楚这个格子因为夜间案件占比高、邻域密度大所以被判为高风险比一个黑箱概率值有用得多。深度学习我也试过用时空卷积网络做验证集上的 AUC 比 LightGBM 高了不到一个百分点但训练时间长十倍而且解释不了。在业务场景里这一个百分点换不来可解释性的损失所以我果断选了树模型。5.2 类别不平衡的处理与评估指标选择案件类别不平衡到什么程度最多的类别占百分之六十多最少的不到百分之一。直接训练模型会几乎全预测成多数类。我的处理是分层采样加类别权重双管齐下训练集对少数类做适度过采样不超过三倍避免过拟合同时给 LightGBM 设置class_weightbalanced。评估指标坚决不用准确率因为不平衡数据下准确率毫无意义——全猜多数类也能有百分之六十。我用的是宏平均 F1 和 AUC宏平均 F1 对每个类别一视同仁能真实反映少数类的识别能力。另外加了一个混淆矩阵做人工检查看模型到底在哪些类别之间混淆这个比单一指标信息量大得多。from lightgbm import LGBMClassifier model LGBMClassifier( n_estimators500, learning_rate0.05, num_leaves63, class_weightbalanced, objectivemulticlass )实操心得num_leaves别设太大我试过一百二十七训练集 F1 很高但验证集掉得厉害明显过拟合。六十三是个比较稳的值配合早停基本不会翻车。5.3 时间序列交叉验证别用随机划分骗自己这是整个项目里我认为最容易被忽视的一点。犯罪数据是时间序列随机划分训练测试集会导致未来信息泄漏——用后面的数据训练去预测前面的数据指标虚高。我一开始就犯了这个错随机划分下 AUC 到了零点九二换成时间序列划分直接掉到零点八出头。正确做法是按时间切分用前四年的数据训练第五年的数据测试中间留一段做验证。更进一步可以做滚动窗口验证每个月用之前所有数据训练、当月数据测试这样得到的指标才接近真实上线效果。虽然麻烦但这是唯一能反映模型实际预测能力的方式。5.4 特征重要性分析与模型解释模型训完不算完得看它到底学到了什么。LightGBM 自带特征重要性我按增益排序后发现前五名是邻域案件密度、夜间案件占比、近三十天案件数、网格历史案件总数、是否节假日。这个排序符合业务直觉说明模型抓到了真信号而不是噪声。如果要做单条预测的解释可以用 SHAP 值它能给出每个特征对单次预测的贡献方向和大小。我给业务方演示时用 SHAP 画了瀑布图一眼就能看出这个格子被判高风险主要是因为邻域密度和历史案件数沟通成本大幅降低。6. 常见问题与排查技巧实录6.1 数据清洗阶段的典型故障问题现象可能原因排查与解决去重后数据量骤降超过两成指纹键设计过粗误删真实不同案件检查指纹字段组合经纬度精度适当放宽时间转换失败率偏高存在本地化格式或系统导出异常抽样查看失败记录单独写解析规则坐标大量落在边界外坐标系不一致如偏移坐标系混入检查数据来源统一坐标系后再裁剪网格统计出现空值部分网格无案件记录补零而不是删格空网格本身是负样本信息6.2 建模阶段的隐蔽陷阱第一个坑是特征穿越。我早期构造了一个该网格未来七天案件数的特征本意是捕捉趋势结果这个特征直接泄漏了标签信息模型指标虚高得离谱。后来严格规定所有特征只能用预测时点之前的数据计算指标才回归正常。这个坑非常隐蔽因为特征本身看起来完全合理只有仔细检查时间窗口才能发现。第二个坑是网格样本量不均。城区网格样本多郊区网格样本少模型在郊区网格上的表现明显差。我的应对是给样本量过少的网格做空间平滑把邻域信息加权融合进来相当于借邻居的样本增强自己。这个操作让郊区网格的预测稳定性提升了不少。第三个坑是类别定义漂移。五年数据里案件分类标准改过两次早期某些类别后来被拆分或合并了。如果不做类别映射对齐模型会把同一类案件当成两类学。我花了一周时间做了类别映射表把历史类别统一到最新标准这一步虽然枯燥但绝对不能省。6.3 热点结果的可信度自查清单热点图出来之后我习惯做三项自查再交付。第一和已知高发区域对照如果热点完全偏离常识大概率是数据或参数出了问题。第二检查热点的时间稳定性如果某个热点只在某一个月出现要确认是真实事件还是数据异常。第三看热点边缘的梯度真实热点应该是密度从中心向外平滑衰减如果边缘突变可能是网格边界效应没处理好。最后分享一个我反复用的小技巧每次跑完完整流程随机抽十条原始记录手工走一遍清洗和特征计算看结果和批量处理的是否一致。这个抽查习惯帮我抓到过好几次批量代码里的隐蔽 bug比看日志管用得多。这套方法论我在实际项目里跑过不止一遍从百万级原始数据到最终热点预测报告整个链路大概两到三周能走完其中清洗占一半时间。数据质量永远是这类项目的地基地基不牢后面模型再花哨都是空中楼阁。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑