简介:基于互信息贝叶斯网络的交通事故严重程度分析文档,面向交通安全研究人员、数据分析师及省际客运行业管理者。资源以上海市2014—2019年省际客运事故数据为对象,针对传统回归模型处理非线性问题受限、问卷数据主观性强等不足,引入CACC有监督离散算法对有限且分布不均的样本进行有效划分,并提出改进互信息方法结合交叉验证来排序变量关联度,进而构造多个先验网络进行贝叶斯结构学习,从人、车、路、环境四个维度综合剖析事故严重程度的影响因素。文档完整呈现建模流程、公式推导、算法执行步骤及最优模型比选思路,有助于读者深入理解基于互信息的贝叶斯网络在交通安全分析中的实际应用,为制定针对性行业安全策略提供科学的数据支撑。资源为单个docx文档,大小344KB,已有96人学习下载,适合具备一定统计或机器学习基础、需要事故致因分析参考的研究者与从业者。
1. 互信息贝叶斯网络:省际客运事故严重程度分析怎么做才不踩坑
省际客运的事故数据有个特点,看着不少,真洗干净能用的不多。这篇资源里用到的上海 2005—2019 年省际客运事故数据,原始 790 条,清洗后剩下 741 条。这么小的样本量,还要从人、车、路、环境里找出哪些因素真正影响事故严重程度,用传统 Logit 回归很容易陷入非线性拟合不良的泥潭,问卷数据又主观。这篇论文的方法路线很直接:先用 CACC 有监督离散算法把连续变量切成合理区间,再用改进的互信息方法做特征相关性排序、构造先验网络,最后用贝叶斯网络学习结构和参数,得到一张可解释的事故致因图。如果你手头也有类似的小样本事故数据、安全评价数据,或者想学怎么把互信息特征选择和贝叶斯网络组合成一条可复现的分析流水线,这份资源值得仔细拆一遍。
2. 数据清洗与变量离散:为什么选 CACC 而不是等宽或 CAIM
2.1 事故数据变量长什么样
建模用到的有效数据是 741 条,每条事故记录涉及人员、车辆、道路、环境、后果等多个维度。论文筛选出 14 个变量进入模型,其中 12 个离散变量、2 个连续变量。连续变量包括年龄、死亡人数、重伤人数、轻伤人数、财产损失等,离散变量则涵盖性别、号牌种类、车辆类型、事故地点、季节、天气、时间、行驶状态、事故类型等。
关键在于,贝叶斯网络要求变量是离散的。年龄、死亡人数这类连续量不能直接丢进网络里学习,必须先做离散化。离散化分有监督和无监督两类:等宽离散、Hierarchical 聚类离散属于无监督;CAIM、CDD、CACC 属于有监督。无监督离散不考虑目标变量,分出来的区间对后续分类没有针对性。CAIM 虽然是有监督,但它在评分时用的是类-区间依赖程度的最大值,容易忽略部分样本分布导致过拟合。CACC 的评分函数用的是所有类在所有区间上的整体依赖程度,如式cacc = sqrt(y / (y + M)),其中 y 由各类样本在每个区间内的占比相对于全局分布计算得到,衡量的是变量离散后与目标类之间的整体关联强度,对样本分布不平均的数据更友好。这就是论文选 CACC 的理由——样本量小且类别分布不均时,CACC 能保留更多有效知识。
2.2 用 Python 复现 CACC 离散的核心逻辑
论文用 Matlab R2020a 实现 CACC,但思路完全可以移植到 Python。算法流程是这样的:给定 M 个样本、l 个待离散变量、S 个目标类,先把每个变量的取值范围作为初始区间,按升序排列,计算相邻值的中点作为候选切点,迭代尝试划分区间,每次划分后计算 CACC 评分,如果评分不再提升就输出当前最优区间划分。论文里 M=741,l=7,S=3,三个目标类分别是“死亡事故”“受伤事故”“财产损失事故”,最大区间数限制为 5。
import numpy as np import pandas as pd from itertools import combinations def cacc_score(data, target, intervals): """ 计算 CACC 评分 data: 待离散变量列 target: 目标类列 intervals: 当前区间边界列表 """ M = len(data) n = len(intervals) - 1 # 区间数 y = 0.0 for i in range(n): left, right = intervals[i], intervals[i + 1] mask = (data >= left) & (data <= right if i == n - 1 else data < right) bin_total = mask.sum() if bin_total == 0: return -np.inf for c in np.unique(target): class_in_bin = (target[mask] == c).sum() if class_in_bin > 0: M_plus_r = bin_total Mi_plus = (target == c).sum() y += (class_in_bin ** 2) / (Mi_plus * M_plus_r) y = M * (y - 1) / np.log2(n) return np.sqrt(y / (y + M)) def cacc_discretize(series, target, max_intervals=5): """ 贪心搜索最优区间划分 """ values = np.sort(series.values) candidates = [(values[i] + values[i + 1]) / 2 for i in range(len(values) - 1)] best_intervals = [np.min(values), np.max(values)] best_score = -np.inf for k in range(1, max_intervals): for cut_points in combinations(candidates, k): intervals = [np.min(values)] + list(cut_points) + [np.max(values)] score = cacc_score(series.values, target.values, intervals) if score > best_score: best_score = score best_intervals = intervals return best_intervals这段代码保留的是 CACC 的评分核心,实际做工程时可以用动态规划替代暴力组合搜索,否则数据量稍大运行会非常慢。cacc_score 里最重要的一行是y += (class_in_bin ** 2) / (Mi_plus * M_plus_r),它同时考虑了类内样本在该区间的占比和该区间在全局的占比,这就是 CACC 比 CAIM 更稳的原因。使用时有几个参数要留意:max_intervals 控制区间数上限,设太大会让每个区间样本太少,条件概率估计不稳定;设太小又损失信息,论文试下来 5 是合适的,你换数据集可以从 4~6 开始扫。
2.3 CACC 离散结果长什么样
论文里带星号的变量就是经过 CACC 离散的:年龄、行驶状态、死亡人数、重伤人数、轻伤人数、财产损失、天气。以年龄为例,被切成五段:0~27、28~47、48~51、52~54、>54。注意年龄段不是等宽的,27 岁以下是一档,48~51 和 52~54 各占一档,说明这几个年龄段在事故严重程度分布上确实有差异,CACC 找出了这些差异边界。死亡人数切成 0、1、2、3、>3 五档,轻伤人数切成 0~7、8~13、14~25、>25 四档。天气分为晴、阴、雨、雪/大风/雾四类,其中雪、大风、雾合并成一类,样本量太少导致没法细分,这个合并动作本身就是一种先验知识。拿到离散结果后,就可以进入下一步构造先验网络了。
3. 互信息与先验网络构造:小样本下如何避免边定向翻车
3.1 为什么不能直接用传统互信息
贝叶斯网络结构学习最怕两件事:搜索空间太大导致陷入局部最优,以及小样本下网络结构过度拟合。常见缓解办法是给定一个先验网络,缩小搜索范围。构造先验网络有三条路:纯专家知识、因果推断框架、互信息方法。专家知识主观性强,因果推断只支持二值变量,DBe 方法需要至少 3000 条样本。这篇论文的数据只有 741 条,多值变量居多,直接套因果推断或 DBe 都不合适。
传统互信息方法有两个痛点:一是基于直方图或核密度估计的熵估计在小样本下偏差很大;二是互信息算出的是变量间的关联强度,但给不出边的方向。论文的解法是用基于 KNN 的改进互信息公式,如式I(X,Y)=ψ(k)−⟨ψ(mx+1)+ψ(my+1)⟩+ψ(M),其中 k 是近邻数,mx 和 my 是水平和垂直方向落入 k 邻域的样本点数,ψ 是 digamma 函数。这个公式比传统直方图估计在小样本上更稳,代价是对 k 值敏感。k 太小系统误差大,k 太大统计误差大,所以论文引入交叉验证来挑 k。
3.2 k 值选择与互信息矩阵计算
论文把经过 CACC 离散的数据集按 70%/30% 拆成训练集和测试集,计算训练样本到测试样本的欧氏距离,建立距离降序矩阵,取第 k 个距离作为邻域半径,用 k 近邻分类器在测试集上跑分类准确率,选出准确率最高的 k。最终结果 k=21。这一步我建议你自己做一遍,因为 k 的取值直接决定后续 MI 矩阵的数值分布,不同数据集最优 k 差得很远。样本量更小可能在 10~15 之间,样本更均衡可能到 30 以上。
计算出 MI 矩阵后,论文把每个变量与“事故类型”之间的互信息值单独拎出来排序,形成变量节点序列。排序结果是:重伤人数 10.66、死亡人数 10.65、轻伤人数 10.40、财产损失 9.94,这四个是结果变量;事故地点 10.78、号牌种类 10.64、性别 10.63、季节 10.51、时间 10.31、车辆类型 10.25、天气 9.97、行驶状态 9.67、年龄 9.65,这九个是影响因素变量。这里有个容易被忽略的点:排序时结果变量排在前面,影响因素变量排在后面,节点排列顺序会直接影响先验网络的初始拓扑方向,不能乱调。
3.3 阈值扫描构造先验网络
有了互信息排序序列,接下来是给边定向。论文的定向策略是:从 9.6 到 10.7 以 0.1 为间隔设 12 个阈值,对每个阈值,把 MI 值大于阈值的影响因素变量节点向结果变量节点连有向边,形成一个先验网络。同时设置“全连”“全不连”两个先验作为对照组,一共 14 个先验网络。对这 14 个网络分别做贝叶斯结构学习,然后用留一法(LOO)交叉验证测精度。最终在阈值 10.5 时网络精度最优。这意味着只保留 MI ≥ 10.5 的边,也就是事故地点、号牌种类、性别、季节、时间、车辆类型与事故类型之间直接相连,而天气、行驶状态、年龄的互信息低于阈值,不与事故类型直接相连,但它们仍可以通过中间节点间接影响结果。这个阈值选择逻辑本质上是特征选择,用交叉验证来定阈值比拍脑袋定 0.5 或 0.6 靠谱得多。留一法在样本量不大时是最稳妥的验证方式,因为每次只留一条样本,模型几乎用到了全部数据,评估方差最小。
4. 贝叶斯网络建模与模型验证:从先验网络到可解释的致因图
4.1 GTT 算法与 EM 参数学习
选好先验网络后,就要做结构学习和参数学习了。论文用 GTT(greedy thick thinning)算法,分两阶段迭代:先是加边阶段,在给定先验网络基础上,贪心添加能让评分提升的边,直到评分不再上升;再是减边阶段,逐步移除冗余边,直到评分达到最优。这种方式的好处是,先验网络已经给出了一个大致的拓扑骨架,GTT 只需要在局部做微调,避免了从零开始搜索导致陷入局部最优的风险。
参数学习用的是期望最大化算法,即 EM 算法。EM 在贝叶斯网络里的作用是:当部分节点存在缺失值时,交替执行 E 步(用当前参数估计缺失值)和 M 步(用补齐后的数据更新条件概率参数),迭代直到收敛。论文用 GeNIe 3.0 软件完成可视化和 EM 学习,最终得到各变量节点的条件概率分布表。这里要强调一个工作习惯:结构学习和参数学习要分开保存结果。结构学习得到的是 DAG 拓扑,参数学习得到的是每个节点的 CPT(条件概率表),后续做推理和敏感度分析时,这两个文件缺一不可。
4.2 模型验证:ROC 曲线与命中率
模型建完不能直接信,论文用 ROC 曲线和 AUC 值做泛化能力验证。AUC > 0.5 表示模型比随机猜测好,越接近 1 越好。对比实验做得很完整:在相同建模方法下,把 CACC 离散结果与等宽离散、Hierarchical 聚类离散做比较;在相同离散方法下,把“互信息先验”与“纯数据先验”“专家知识先验”做比较。最终结果是:本文模型的 AUC 均值 0.644588,优于所有对照组。这个数字看起来不高,但交通事故事故严重程度预测本身就是低区分度问题,类别不均衡、噪声大,0.64 的 AUC 已经具备实用价值。
交叉验证的命中率更直接:103 条“死亡事故”命中 102 条,512 条“受伤事故”命中 497 条,125 条“财产损失事故”命中 121 条,整体命中率 97.3%。这个命中率建立在留一法验证基础上,说明学习到的条件概率表与真实数据分布高度吻合。但要注意,命中率高不等于预测能力强,因为在类别不均衡的数据集上,模型可以靠预测多数类获得高准确率。所以论文才用 ROC 和 AUC 作为补充指标,两者一起看才不会被单一指标带偏。
做完验证,这个贝叶斯网络就可以用来做推理了。GeNIe 里可以设置某个节点为证据,比如把天气状态设为“雪、大风、雾”,然后观察“事故类型”节点的后验概率变化,这个推理能力是贝叶斯网络比回归模型实用的核心原因——它不止给出系数,还能回答“如果某因素变成某种状态,结果分布怎么变”。
4.3 模型推理的关键应用场景
从条件概率表里提取信息要讲究方法。论文的做法是:以“死亡事故”“受伤事故”“财产损失事故”三个结果变量为目标项做敏感度分析,再对每个影响因素类的后验概率变化做归一化权重计算。最终结果显示“女性驾驶员”“中型客车”“雪大风雾天气”对事故严重程度影响最大。敏感度分析揭示的是节点微小变化对结果的扰动程度,权重分析揭示的是每个状态对结果的贡献度。这两个指标不是一回事,前者看的是整体敏感性,后者看的是具体类别的作用,用的时候要区分。
5. 避坑指南:从数据清洗到建模推理的五个常见翻车点
5.1 数据清洗丢失样本过多,离散区间分布崩了
现象:原始 790 条事故数据,清洗后只剩 741 条,损失接近 6%。如果自己复现时清洗逻辑过于激进,比如把含有任一缺失值的记录整条删除,样本量可能跌破 700,离散时某些区间只剩个位数的样本。
原因:省际客运事故记录里存在字段缺失和录入错误,原始记录质量参差不齐。清洗时没有区分“关键变量缺失”和“非关键变量缺失”,一刀切全删。
解决:我只删除建模必需变量存在缺失的记录,比如事故类型、车辆类型、天气这些核心字段;对非关键变量,用众数或中位数填充后再进入离散流程。清洗后一定要重新统计每个变量的取值分布,确保每个离散区间有足够样本,一般低于 20 条样本的区间就该考虑合并。
5.2 CACC 离散区间数设太大,条件概率表稀疏
现象:离散结果看起来很“精细”,年龄被切成七八档,但贝叶斯网络学习出的条件概率表里大量单元格是 0 或者极小值,推理结果明显不合理。
原因:max_intervals 设得过大,而样本量只有 741 条,细分区间后每个区间的样本量不足以支撑稳定的概率估计。这不是 CACC 的问题,是超参没调好。
解决:论文里最大区间数 5 是有道理的。我做类似数据时习惯先扫 3~6 之间的区间数,每个候选值都跑一遍后续建模流程,看验证集 AUC 的变化,选 AUC 最高的配置。别只看离散结果的“精细程度”,一切以最终模型性能为准。
5.3 互信息 k 值不优化直接默认用 5 或 10
现象:互信息矩阵算出来后,阈值扫描得到的最优网络和论文结果对不上,甚至出现边的方向与业务常识完全相反的情况,比如“死亡人数导致性别”这种荒唐结构。
原因:k 值是 KNN 互信息估计的核心参数,k 不同,MI 值的大小排序就不同,直接影响阈值扫描结果和边定向结果。用默认参数不等于用最优参数。
解决:老老实实跑一遍交叉验证选 k。把样本拆成 70% 训练集和 30% 测试集,对每个候选 k 值计算分类准确率,画一条 k 值-准确率曲线,选曲线峰值对应的 k。这个步骤耗时不大,但对结果的稳定性提升非常明显。论文在 741 条样本上选出的最优 k 是 21,这个值离默认值很远,说明这一步绝对不能省。
5.4 阈值扫描步长太粗,错过最优先验网络
现象:阈值从 9.6~10.7 按 0.1 步长扫了 12 个值,最优网络在 10.5,但如果把步长改成 0.05,最优值可能落在 10.35 或 10.45,得到更好的 AUC。
原因:步长太大时,真实的峰值可能落在两个扫描点之间。步长太小呢,计算量又成倍增加,每个阈值都要做一遍结构学习加留一法验证,耗不起。
解决:我一般分两轮扫,第一轮用粗步长比如 0.2 确定最优值的大致范围,第二轮在该范围内用 0.02~0.05 的细步长加密。第一轮可能跑十几个模型,第二轮只需跑几个,总耗时可控,精度显著提升。同时对照“全连”和“全不连”两个基线网络,它们能告诉你先验网络相对随机起点有没有实质增益。
5.5 只盯准确率不看类别均衡,模型被多数类带偏
现象:模型总命中率达到 97%,但仔细看,被正确命中的大部分是“受伤事故”这类样本量大的类别,“死亡事故”命中率不足一半。
原因:“受伤事故”512 条,占总量的 69%,如果模型把所有测试样本都判为“受伤事故”,整体命中率也能接近 70%。论文之所以敢说模型有效,是因为用的是留一法交叉验证并汇报每个类别的命中率,而不是只看总量。
解决:评估时必须分别统计每个结果类别的命中率。死亡事故、受伤事故、财产损失事故三类样本量差异极大,只有分类别统计才能看出模型对小类别的泛化能力。必要时可以做简单的重采样平衡,但样本量只有 741 条时我不建议过采样,容易过拟合,优先用类别权重或阈值移动来调节。
6. 复现这篇模型的完整流程:从原始数据到事故致因图
如果你想在自己的数据集上复现这套方法,我把它拆成六个步骤,每一步对应一套明确产出物。
第 1 步,数据清洗。把原始事故数据导出后,先按关键字段完整性过滤,再统一变量取值口径。时间字段要统一为 24 小时制并划分四个时段,季节按气象学标准划分而不是按农历,天气里的“雾”“大风”等低频类别要合并归类。连续变量先保留原始值,等离散化处理。
第 2 步,CACC 有监督离散。以“事故类型”为监督变量,对年龄、行驶状态、死亡人数、重伤人数、轻伤人数、财产损失等连续或高基数变量做离散。最大区间数从 4 开始试,记录每种区间数下训练集的 CACC 评分,选评分最高且区间样本量均匀的配置。产出是每个变量的区间划分表,这份表要保存好,后续推理阶段需要对新的数据做同样映射。
第 3 步,互信息计算与 k 值选择。对离散后的全量数据做交叉验证选最优 k,再用选定的 k 计算所有变量对的互信息值。产出是 MI 矩阵和一个变量-结果变量 MI 值排序表。这一步是整条流水线里最容易被跳过的,但也是决定模型上限的关键,因为 MI 矩阵直接决定先验网络的边集合。
第 4 步,阈值扫描与先验网络选择。把 MI 值从最小值到最大值按 0.1 步长生成一组合适的阈值,对每个阈值构造先验网络。同时构造全连、全不连两个基线。每个先验网络都输入贝叶斯网络结构学习算法执行 GTT,再用留一法评估精度。选 AUC 或命中率最高的阈值作为最终先验网络。产出是一个最优先验网络 DAG 文件。
第 5 步,参数学习与模型验证。用 EM 算法在最优网络结构上学习条件概率表。验证时同时看 AUC 和分类别命中率,不能只看总命中率。如果 AUC 低于 0.6,回头检查离散区间和 k 值,大概率是这两步的某个参数没调好。产出是标准的贝叶斯网络模型文件。
第 6 步,敏感度分析与后验推理。在 GeNIe 或 Python 的 pgmpy 里加载模型,对结果变量做敏感度分析,找到影响最大的因素节点。再逐个设置证据节点为不同状态,观察目标节点后验概率变化。这个阶段才能真正回答“女性驾驶员导致死亡事故的概率变化是多少”“雪天对受伤人数的影响有多大”这类业务问题。
# 用 pgmpy 复现贝叶斯网络推理的骨架代码 from pgmpy.models import BayesianNetwork from pgmpy.estimators import BayesianEstimator, HillClimbSearch from pgmpy.inference import VariableElimination # 根据论文的互信息矩阵排序手动指定网络结构 edges = [ ("事故地点", "事故类型"), ("号牌种类", "事故类型"), ("性别", "事故类型"), ("季节", "事故类型"), ("时间", "事故类型"), ("车辆类型", "事故类型"), ("事故类型", "重伤人数"), ("事故类型", "死亡人数"), ("事故类型", "轻伤人数"), ("事故类型", "财产损失"), ] model = BayesianNetwork(edges) # 参数学习:用贝叶斯估计做平滑,避免小样本下出现零概率 model.fit(data, estimator=BayesianEstimator, prior_type="K2") # 推理:设置天气为雪/大风/雾,观察死亡事故概率变化 infer = VariableElimination(model) query_result = infer.query(variables=["事故类型"], evidence={"天气": "雪大风雾"}) print(query_result)这段代码里,edges 列表来自论文阈值 10.5 时选出的最优先验网络。变量名和状态名要根据你实际离散后的数据调整。prior_type 用 K2 是贝叶斯估计里比较通用的平滑先验,能避免零概率导致的推理崩溃。若你不需要证据推理,只想验证结构,可以把 HillClimbSearch 加进来做一次纯数据驱动的结构对比,看看与互信息先验给出的边差异在哪。
我自己的习惯是:先跑一遍纯数据驱动的结构学习作为参照,再叠加互信息先验看精度变化。如果互信息先验没有带来提升,就需要回到第 2 步和第 3 步,检查离散区间划分是否合理、k 值选得对不对。回归模型给的是系数表,贝叶斯网络给的是可交互的因果图,这个差别在实际汇报时很有用——管理层更愿意看“天气变差会导致死亡概率提高多少”,而不是一个带负号的回归系数。从那以后我每次做类似的小样本安全数据分析,都强制走一遍 CACC 离散→互信息排序→阈值扫描→结构学习→分类别验证的完整链条,不再跳过任何一环。希望这套流程对你复现也有帮助。
本文还有配套的精品资源,点击获取