news 2026/9/20 18:55:33

数据挖掘驱动案件串并:从特征工程到图分析排嫌疑人

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
数据挖掘驱动案件串并:从特征工程到图分析排嫌疑人

简介:《数据挖掘技术在案件串并和嫌疑人排查中的应用》是一份面向公安情报分析人员、侦查办案人员及数据挖掘学习者的技术文档型PDF,围绕把海量犯罪数据转化为破案线索这一目标,梳理从决策支持系统背景到具体算法落地的完整思路。内容先介绍关联规则分析、聚类分析、协同过滤、分类与回归四类方法在犯罪规律挖掘、高危人群库构建、相似案件合并侦查、嫌疑人轨迹推荐与预警中的用法,再结合“金盾工程”业务背景,展开案件特征库与违法犯罪人员特征库建设、串并案模型与排查模型设计、出租车与卡口手机基站等轨迹数据叠加比对等落地场景,兼顾算法原理与公安实战需求。整包仅1个PDF文件,约981KB,便于随时查阅与打印研读。目前已有79人学习下载,适合希望系统了解数据挖掘在刑侦情报领域应用路径的读者参考。

1. 案件串并的难点不在相似度算法,而在两个案子怎么变成可比向量

辖区一年几千起案件,靠人工翻卷宗串出关系的往往只有几十组。剩下的要么散在不同办案单位的记录里,要么因为一份笔录写“撬门入室”、另一份写“破坏门锁后进入”,被关键词匹配判成两回事。数据挖掘在案件串并和嫌疑人排查里干的是一件很朴素的事:把每起案件压成一个向量,让机器比谁和谁像,再把人、案、物、地之间的关系铺成一张图,从图上把需要优先核查的人排到前面。它不替代侦查判断,只负责把几千起案件里的高相似对和高关联人捞出来,让有限的人力盯住那几十条真正该看的线索。做数据治理、特征工程、图分析的工程师,以及要评估这类系统好不好用的业务方,是这套方法的主要读者。下面从数据建模一路讲到误报压降,每一步都给可复现的代码和参数口径。

2. 数据挖掘的特征底座:把接报案记录拆成可计算的案件特征向量

案件相似度算不准,九成问题出在特征上,而不是模型上。原始接报案记录里能直接用来比较的字段极少,时间是一串时间戳,地点是两个经纬度,作案手法是一段自由文本,涉案物品是口语化描述。数据挖掘要做的第一件事是定义特征字典,把这几类字段统一成同一种数学形态:集合型、向量型、数值型。集合型走 Jaccard,向量型走余弦,数值型走衰减核,三种距离不能混着算,否则量纲会互相污染。

2.1 案件特征字典:时间、空间、手法、物品四类字段怎么定

先定字段,再写代码。字段定义错了,后面调多少参数都是白费。

维度原始字段加工后特征类型加工口径
时间occur_tshour、dow、is_night数值/布尔拆出昼夜节律,保留绝对时间用于衰减
空间lon、lat经纬度对、辖区编码、点位类型数值+类别保留原始坐标做距离,辖区做兜底聚合
手法method_text分词后 TF-IDF 向量、手法标签集合向量+集合文本走向量,标签走 Jaccard
物品target物品类别集合集合映射到统一类目表,避免同物异名
侵入entry_type侵入方式标签类别归并同义表述,如“撬门”与“撬砸门锁”
损失loss分箱后的损失档位有序类别分箱比原始金额更能抗异常值

有了这张表,下面三段代码就是把表落成矩阵的过程,全程只用 pandas 加 scikit-learn。

import pandas as pd import numpy as np # 模拟一批接报案记录,字段结构贴近常见业务系统导出,数据为构造示例 cases = pd.DataFrame({ "case_id": [f"C{i:04d}" for i in range(1, 9)], "occur_ts": pd.to_datetime(["2024-03-01 02:10", "2024-03-01 23:40", "2024-03-04 01:30", "2024-03-09 03:05", "2024-03-09 22:15", "2024-04-02 02:20", "2024-04-02 02:55", "2024-04-15 13:00"]), "lon": [116.41, 116.43, 116.40, 116.52, 116.51, 116.42, 116.41, 116.88], "lat": [39.91, 39.90, 39.92, 39.87, 39.86, 39.90, 39.91, 39.95], "entry_type": ["撬门", "技术开锁", "撬门", "翻窗", "翻窗", "撬门", "撬门", "尾随"], "target": ["电动自行车", "电动自行车", "电动自行车", "手机", "手机", "电动自行车", "电动自行车", "现金"], "method_text": ["撬门入室,破坏门锁后进入,翻动物品", "使用工具技术开锁,未破坏门体,直取目标", "撬砸门锁后进入室内,现场翻动明显", "翻越窗户进入,剪断防盗网", "由窗户翻入,剪断窗栅", "撬门进入,门锁被破坏", "破坏门锁后进入,作案后恢复原状", "尾随受害人至楼下,趁其不备夺取财物"], "loss": [3200, 2800, 3500, 4500, 5200, 3000, 3300, 1200], }) # 时间:拆成昼夜节律特征,绝对时间单独留给衰减核使用 cases["hour"] = cases["occur_ts"].dt.hour cases["dow"] = cases["occur_ts"].dt.dayofweek # 0 表示周一 cases["is_night"] = cases["hour"].isin([0, 1, 2, 3, 4, 5]).astype(int) # 损失:分箱,避免单笔特大金额主导相似度 cases["loss_bin"] = pd.cut(cases["loss"], bins=[0, 2000, 4000, 99999], labels=["低", "中", "高"]) print(cases[["case_id", "hour", "is_night", "loss_bin"]])

逻辑上,这里把“什么时候作案”拆成了两套表示:is_night用于集合相似度,occur_ts原值留给后面的时间衰减核。参数上,夜间档边界写死在 0 到 5 点只是常见口径,实际要按辖区的作息分布做一次直方图,把案件量最低的那几个小时切出来。损失分箱的断点也不是固定的,看损失字段的分位数,用 5% 和 90% 分位数当断点比拍脑袋更靠谱。

2.2 中文作案手法文本的向量化:jieba 自定义词典加 TF-IDF

自由文本是案件数据里信息密度最高、也最难对齐的部分。直接用字面匹配会漏掉大量同义表述,直接上通用分词又会把“技术开锁”切成“技术”和“开锁”。自定义词典是绕不开的一步。

import jieba from sklearn.feature_extraction.text import TfidfVectorizer # 业务口径里的手法词必须先切对,否则关键词会被切碎后失去区分度 for w in ["撬门入室", "技术开锁", "翻窗", "撬砸门锁", "破坏门锁", "剪断防盗网", "尾随", "翻动物品", "恢复原状"]: jieba.add_word(w, freq=20000) cases["method_seg"] = cases["method_text"].map(lambda s: " ".join(jieba.lcut(s))) vectorizer = TfidfVectorizer( token_pattern=r"(?u)\S+", # 文本已切分好,按空白取词即可 min_df=1, # 样本少时先不丢词,上线后调到 2 到 3 max_df=0.6, # 压掉“进入”“物品”这类高频泛词 sublinear_tf=True, # 长文本的词频做对数压缩 ) M_method = vectorizer.fit_transform(cases["method_seg"]) print(M_method.shape, vectorizer.get_feature_names_out()[:8])

min_df控制的是“只在极少数案件里出现的词”,这类词往往是写笔录时的个人用词习惯,留着会让相似度虚高;max_df=0.6反过来压掉出现在六成以上案件里的词,它们接近停用词。sublinear_tf解决的是笔录长短不一的问题,一份 300 字的笔录和一份 50 字的笔录不该因为字数差异拉出距离。这套词表要定期回流,把新增的手法表述补进自定义词典,否则半年后新出现的作案方式会切得七零八落。

2.3 类别编码、数值标准化与缺失值:别让空字段把相似度拉低

类别字段最容易踩的坑是缺失值。把缺失填成 0 或者“未知”这一类标签,在 Jaccard 计算时会变成两个案件“共享同一个未知特征”,凭空拉高相似度。正确做法是给缺失单独一个占位,并在计算相似度时把该维度整体跳过、重新归一化权重。

from sklearn.preprocessing import MinMaxScaler # 类别字段:缺失单独编码成 UNKNOWN,不与其他类别混用 for col in ["entry_type", "target", "loss_bin"]: cases[col] = cases[col].astype(object).where(cases[col].notna(), "UNKNOWN") # 数值字段:如果要做欧氏距离或输入树模型,先做极值压缩 scaler = MinMaxScaler() cases[["lon_n", "lat_n"]] = scaler.fit_transform(cases[["lon", "lat"]]) missing_ratio = cases[["entry_type", "target", "method_text"]].isna().mean() print(missing_ratio)

判断一个维度能不能用,看缺失率而不是看它“看起来重要”。某维度缺失率超过三成,它在相似度里的权重就应该被自动降下来,或者干脆改用“已知才计算、未知不计入”的加权方案。这套处理看起来琐碎,但它决定了后面相似度矩阵是不是可信,比调模型参数优先级高得多。

3. 案件串并的相似度计算:分维度打分、加权融合与 DBSCAN 成串

特征矩阵建好之后,案件串并就变成一个打分加聚类的问题。这里的关键判断是:不同维度的相似度不能直接相加,必须先在每个维度内部归一化到 0 到 1,再按业务重要性加权。

3.1 分维度相似度:Jaccard、余弦与时空衰减核

三类特征对应三种相似度算法,写成一个函数组,后面复用起来方便。

import numpy as np from sklearn.metrics.pairwise import cosine_similarity def jaccard(a, b): """集合型特征:侵入方式、涉案物品、手法标签""" sa, sb = set(a), set(b) return len(sa & sb) / max(len(sa | sb), 1) def haversine_km(lon1, lat1, lon2, lat2): """球面距离,单位公里""" R = 6371.0 p1, p2 = np.radians(lat1), np.radians(lat2) dp, dl = p2 - p1, np.radians(lon2 - lon1) a = np.sin(dp / 2) ** 2 + np.cos(p1) * np.cos(p2) * np.sin(dl / 2) ** 2 return 2 * R * np.arcsin(np.sqrt(a)) def spatial_sim(d_km, tau_km=2.0): """空间衰减核:tau 取作案半径的量级""" return np.exp(-d_km / tau_km) def temporal_sim(dt_hours, tau_h=72.0): """时间衰减核:72 小时是常见串并口径里的近期尺度""" return np.exp(-abs(dt_hours) / tau_h) # 手法文本向量之间的余弦相似度 S_method = cosine_similarity(M_method)

tau_km取 2 公里、tau_h取 72 小时,是常见起点。空间衰减核的形状意味着 2 公里处相似度降到 0.37,5 公里处降到 0.08,符合“同一人不太可能跨大半个城市连续作案”的经验。但城区和郊县的作案半径差异很大,郊区要把tau_km放宽到 5 到 8 公里,这个参数必须按辖区分别标定,不能全局一个值。

3.2 加权融合与串并判定:时间窗、空间半径与综合阈值三个必调参数

合成相似度之前,先用硬性窗口做一次粗筛:时间差超过 90 天、空间距离超过 20 公里的对子直接判为不相似。这一步能把 O(n²) 的候选对砍掉九成以上,几千起案件的规模下尤其重要。

W = {"method": 0.40, "space": 0.20, "time": 0.15, "entry": 0.15, "target": 0.10} ids = cases["case_id"].tolist() n = len(ids) S = np.zeros((n, n)) for i in range(n): for j in range(i + 1, n): d_km = haversine_km(cases.lon[i], cases.lat[i], cases.lon[j], cases.lat[j]) dt_h = abs((cases.occur_ts[i] - cases.occur_ts[j]).total_seconds()) / 3600 if d_km > 20 or dt_h > 24 * 90: # 硬窗口粗筛 continue s = (W["method"] * S_method[i, j] + W["space"] * spatial_sim(d_km) + W["time"] * temporal_sim(dt_h) + W["entry"] * jaccard([cases.entry_type[i]], [cases.entry_type[j]]) + W["target"] * jaccard([cases.target[i]], [cases.target[j]])) S[i, j] = S[j, i] = s np.fill_diagonal(S, 1.0) print(pd.DataFrame(S.round(2), index=ids, columns=ids))
参数含义常用取值调大后的效果
tau_km空间衰减尺度城区 2,郊区 5 到 8远距离案件更容易被判为同一人
tau_h时间衰减尺度48 到 168 小时长周期作案序列更容易成串
method 权重手法文本贡献0.35 到 0.45手法相似但不相关的对子变多,误报上升
硬窗口时间/空间粗筛90 天、20 公里漏串风险上升,但计算量显著下降
综合阈值成串判定线0.60 到 0.70阈值越低串越大,需要的人工复核量越高

权重的分配逻辑是手法权重最高,因为它最难伪装也最难改变;时空权重加起来不超过 0.35,因为同一人跨区作案、隔月作案都很常见。阈值不建议一步到位调到 0.5,先设在 0.65 左右跑一轮,看候选对数量落在什么量级,再决定往哪个方向微调。

3.3 DBSCAN 聚类成串与人工复核队列输出

有了相似度矩阵,成串就是把它转成距离矩阵之后做密度聚类。DBSCAN 比层次聚类更适合这个场景,因为它不需要预先指定串的数量,也能把孤案留在噪声里。

from sklearn.cluster import DBSCAN THRESHOLD = 0.65 D = np.clip(1 - S, 0, 1) # 相似度转距离 db = DBSCAN(eps=1 - THRESHOLD, min_samples=2, metric="precomputed") labels = db.fit_predict(D) result = pd.DataFrame({"case_id": ids, "cluster": labels}) for cid, grp in result[result.cluster >= 0].groupby("cluster"): print(f"串 {cid}: {list(grp.case_id)} 规模 {len(grp)}")

eps=1-THRESHOLD这个换算要记住,因为 sklearn 收的是距离而不是相似度。min_samples=2表示两案即可成串,实务里常见做法是先按 2 起步,把结果分档:两案的串单独一个队列,三案以上且案均相似度高于 0.75 的放高优先队列。输出给复核人员的表里一定要带解释字段——每个对子贡献最大的那个维度是什么,是手法一致、地点邻近,还是时间连贯,人看的是理由不是分数。

4. 嫌疑人排查的关联规则与图挖掘:从共现到社区

案件串并解决的是“哪些案子是一伙人干的”,嫌疑人排查要解决的是“这伙人里都有谁”。这两步共享同一套特征底座,但用的算法不一样:前一步是相似度,后一步是关联规则和图结构。

4.1 Apriori 找高频共现:支持度、置信度、提升度怎么读

关联规则挖掘的输入是事务表,每起案件是一条事务,事务里的项是这起案件涉及的特征组合:手法、目标、时段、点位类型。跑 Apriori 之前先把连续量离散化,时间段切成凌晨、上午、下午、夜间四档,点位类型切成老旧小区、商住楼、沿街商铺、开放式院落。

import pandas as pd from mlxtend.preprocessing import TransactionEncoder from mlxtend.frequent_patterns import apriori, association_rules # 事务 = 一起案件的特征组合 tx = [ ["撬门", "电动自行车", "夜间", "老旧小区"], ["技术开锁", "电动自行车", "夜间", "老旧小区"], ["撬门", "电动自行车", "夜间", "老旧小区"], ["翻窗", "手机", "夜间", "商住楼"], ["翻窗", "手机", "夜间", "商住楼"], ["撬门", "电动自行车", "凌晨", "老旧小区"], ["撬门", "电动自行车", "凌晨", "开放式院落"], ["尾随", "现金", "下午", "沿街商铺"], ] te = TransactionEncoder() onehot = pd.DataFrame(te.fit(tx).transform(tx), columns=te.columns_) freq = apriori(onehot, min_support=0.25, use_colnames=True, max_len=3) rules = association_rules(freq, metric="confidence", min_threshold=0.6) rules = rules.sort_values("lift", ascending=False) print(rules[["antecedents", "consequents", "support", "confidence", "lift"]].head(8))

三个指标要分清楚:support是这条组合在全部案件里出现的比例,用来过滤长尾噪声;confidence是前件出现时后件也出现的条件概率,用来看预测强度;lift大于 1 才说明两者不是各自独立发生的。排查里最有用的是 lift 高但 support 中等的规则,比如“老旧小区加夜间加电动自行车”这种组合,support 可能只有 0.15,但 lift 能到 3 以上,它描述的是一个具体的行为模式,比泛泛的高频组合有信息量得多。

4.2 NetworkX 建“人-案-物-地”异构图并做社区发现

关联规则只能看到两三个特征的共现,看不到整张关系网。把涉案人员、案件、涉案物品、案发点位都建成节点,用边表示出现关系,整批案件的关联结构就显出来了。

import networkx as nx G = nx.Graph() for _, r in cases.iterrows(): G.add_node(r["case_id"], ntype="case") G.add_node(f'LOC_{r["entry_type"]}', ntype="loc") G.add_node(f'ITEM_{r["target"]}', ntype="item") G.add_edge(r["case_id"], f'LOC_{r["entry_type"]}', etype="occur") G.add_edge(r["case_id"], f'ITEM_{r["target"]}', etype="target") # 涉案人员节点:同一个名字出现在多起案件里时,边权累加 for p in r["persons"]: G.add_node(p, ntype="person") G.add_edge(p, r["case_id"], etype="involved", weight=1.0) # 社区发现:较新版本 networkx 直接提供 louvain_communities comms = nx.community.louvain_communities(G, seed=42) for k, c in enumerate(comms): persons = [n for n in c if G.nodes[n].get("ntype") == "person"] print(f"社区 {k}: 人 {len(persons)},节点总数 {len(c)}")

seed固定是为了结果可复现,社区发现本身有随机性,不固定种子每次跑出来的人分组会略有差异。判断一个社区有没有价值看两点:一是社区里有没有重复出现的人节点,同一个人出现在不同社区说明拆分过度;二是社区规模,超过三十个节点的社区基本没有核查价值,说明阈值太松。实际排查里,跨社区连边多的人比社区内部度数高的人更值得关注,前者是把两个团伙连起来的桥接节点。

4.3 嫌疑人排序打分与可解释字段输出

把相似度传导和图结构合并成一个排序分数,比单独看任何一个都准。分数本身不重要,重要的是每条分数后面跟着什么理由。

import numpy as np def suspect_score(case_sim_sum, hops, related_cnt, w=(0.5, 0.3, 0.2)): """case_sim_sum: 该人关联案件与目标串的平均相似度之和 hops: 该人到目标串在图上最短跳数 related_cnt: 历史关联案件数量""" return (w[0] * case_sim_sum + w[1] * (1.0 / (1.0 + hops)) + w[2] * np.log1p(related_cnt)) # 示例输出结构 rows = [ {"person": "P001", "case_sim_sum": 1.85, "hops": 1, "related_cnt": 4}, {"person": "P002", "case_sim_sum": 1.20, "hops": 2, "related_cnt": 2}, {"person": "P003", "case_sim_sum": 0.95, "hops": 1, "related_cnt": 7}, ] for r in rows: r["score"] = round(suspect_score(r["case_sim_sum"], r["hops"], r["related_cnt"]), 3) print(pd.DataFrame(rows).sort_values("score", ascending=False))
字段来源默认权重解释方式
case_sim_sum相似度矩阵聚合0.5与该人关联案件最像的串有多像
hops图最短路径0.3到目标案件隔了几层关系
related_cnt历史关联统计0.2过往涉案记录的次数,做对数压缩
解释列各维度贡献分解明确写出“手法一致 + 同一小区”这类理由

权重不是固定的,如果数据里人员关联记录质量高,related_cnt的权重可以提到 0.3;如果人员关联数据稀疏、大量缺失,就把它降到 0.1,让图结构承担更多。每条结果必须能展开成“该人为什么排在这里”的三行解释,复核人员凭解释决定看不看,而不是凭一个分数。

5. 案件串并结果的误报压降与回溯验证

上线初期最常见的抱怨是“串出来的案子太多,看不过来”。这不是算法问题,是阈值和权重没做分层。压误报最有效的三个动作:按特征稀有度降权、要求多维度同时命中、按时段分组标定阈值。

5.1 IDF 降权与多维度命中约束

“入室”“被盗”这类词在笔录里到处都是,它们贡献的相似度应该接近于零。TF-IDF 本身自带 IDF 降权,但在手工加权的那部分维度,比如侵入方式和涉案物品,权重还是静态的。更细的做法是把每个特征值的全局频率算出来,频率越高权重越低。

freq_map = cases["entry_type"].value_counts(normalize=True).to_dict() idf_weight = {k: 1.0 / (0.2 + v) for k, v in freq_map.items()} # 多维度命中约束:至少两个非时空维度同时命中才保留为候选 def keep_pair(s_detail, min_dims=2): hit = sum(1 for k, v in s_detail.items() if k not in ("space", "time") and v >= 0.5) return hit >= min_dims

min_dims=2意味着光靠地点近、时间近不够,必须在手法或物品上也有实质重合。这一条能把候选对砍掉一半以上,代价是会漏掉少量只靠时空吻合的串,实务上这个取舍是划算的,因为纯时空吻合的对子人工核起来最费时间、命中率也最低。

5.2 留一法回溯与复核标签回流

阈值调得对不对,不能靠感觉。留一法是这个场景里最实用的验证手段:把已知的串并结果拆开,每次藏起一起案件,看系统能不能把它重新召回同一个串里。

def loo_recall(S, labeled_groups, threshold=0.65): hit, total = 0, 0 for group in labeled_groups: for cid in group: others = [x for x in group if x != cid] total += 1 if any(S[cid][o] >= threshold for o in others): hit += 1 return hit / max(total, 1) # labeled_groups 为历史人工确认过的串,形如 [["C0001","C0003","C0006"], ...] print(loo_recall(S, [["C0001", "C0003", "C0006"]]))

这个指标比精确率更能反映实际能力,它回答的是“已知的关联,系统能找回多少”。每天把复核人员的判断写成标签回流到特征库和阈值配置里,是这套流程里唯一能持续降低误报的动作。

最后一个技巧:复核队列里排第一位的永远不该是分数最高的那条,而应该是分数高、且解释字段最完整的那条。分数高但解释是空的记录,交给谁看都是浪费时间,这类记录应该先回到特征补全环节,而不是继续往后流转。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/20 18:55:09

HashMap 源码深度解析:JDK 1.8 中数组 + 链表 + 红黑树的底层实现原理

HashMap 源码深度解析:JDK 1.8 中数组 链表 红黑树的底层实现原理 【免费下载链接】source-code-hunter 😱 从源码层面,剖析挖掘互联网行业主流技术的底层实现原理,为广大开发者 “提升技术深度” 提供便利。目前开放 Spring 全…

作者头像 李华
网站建设 2026/9/20 18:53:53

基于SpringBoot的学生成长画像系统设计与实现

1. 项目背景与核心价值学生成长画像系统是当前教育信息化领域的热门研究方向。作为一名长期从事教育技术开发的工程师,我发现传统的学生评价体系存在数据碎片化、评价维度单一等问题。而基于SpringBoot和Web 2.0技术构建的成长画像系统,能够有效整合学生…

作者头像 李华
网站建设 2026/9/20 18:52:22

浏览器里跑嵌入式仿真:19块开发板零安装实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/20 18:51:13

MATLAB入门进阶路径:从矩阵操作到数据可视化与图像处理实战

简介:一套完整的MATLAB语言入门教程PPT,共410页,面向高校本科生、研究生及工程技术人员。内容系统梳理了MATLAB的发展历史与产品家族,介绍了桌面环境、数据可视化、数值计算步骤及规范编程方法,并涉及信号处理、图像处…

作者头像 李华