
1. 项目概述当计算机开始“琢磨”因果关系你有没有过这种体验看到同事连续三天加班到凌晨第二天就感冒了于是下意识觉得“加班导致免疫力下降”或者发现每次咖啡喝多了下午就心慌手抖便认定“咖啡因引发心悸”人类大脑天生就是一台因果推理机——它不满足于“X和Y总是一起出现”而是执着追问“X是不是Y的原因”、“如果我阻止XY还会发生吗”——这种能力是决策、诊断、干预甚至道德判断的底层支撑。而Bayesian Networks贝叶斯网络正是我们第一次把这种“琢磨因果”的能力系统性地、数学化地、可计算地装进了计算机里。它不是简单地找相关性而是用一张有向无环图DAG 一组条件概率表CPT构建出一个世界的“因果骨架”。节点代表变量比如“吸烟”、“肺癌”、“咳嗽”箭头代表直接的因果影响方向“吸烟 → 肺癌”而每个节点旁附着的概率表则量化了“在给定其父节点直接原因取值的情况下该节点取某个值的可能性”。这背后是贝叶斯定理的威力它允许我们从观测数据果反推未观测状态因还能在获得新证据比如病人做了CT后实时更新所有相关变量的概率信念。这不是魔法而是将哲学家休谟提出的“因果难题”转化成了程序员能写、服务器能跑、医生能用的确定性算法。它早已不是实验室里的玩具医疗诊断系统用它推断罕见病的根源工业预测性维护用它定位故障链的起点金融风控模型用它区分“收入低导致信用差”和“信用差导致贷款难”的不同路径。如果你正被“数据很多但不知道下一步该动哪个开关”所困扰或者想让AI的建议不只是“可能相关”而是“你应该先解决这个问题”那么理解贝叶斯网络就是你亲手为机器装上“因果之眼”的第一步。2. 核心设计思路与方案选型解析2.1 为什么非得是“图”“概率”——因果建模的不可替代性很多人初学时会困惑既然我们有强大的深度学习模型能拟合任何复杂的函数为什么还要费劲去画一张图、填一堆概率表答案直指核心相关性不等于因果性而深度学习本质上只擅长捕捉相关性。举个经典例子冰淇淋销量和溺水人数在夏季高度正相关。一个黑箱神经网络会完美拟合这个关系但它无法告诉你真正的原因是“高温天气”——这个共同的父节点confounder。它既不会建议“禁止卖冰淇淋来减少溺水”也不会在阴雨天预测溺水风险时自动忽略冰淇淋销量这个变量。而贝叶斯网络强制要求你显式地画出“高温 → 冰淇淋销量”和“高温 → 游泳人数 → 溺水人数”这两条路径。一旦图结构确定网络就天然具备了“do-演算”do-calculus的能力你可以模拟“人为干预”do(高温低温)然后精确计算出干预后溺水人数的期望值变化。这种对“反事实”what if问题的处理能力是纯统计模型无法企及的。因此选择贝叶斯网络不是因为它“更先进”而是因为它唯一地、结构性地编码了我们对世界因果机制的先验知识。它把模糊的“我觉得A可能影响B”转化成了可验证、可计算、可沟通的数学对象。我见过太多团队在用LSTM预测设备故障后面对“该修哪个零件”的问题束手无策——因为LSTM只输出一个故障概率却没告诉你是轴承磨损、还是冷却液泄漏、或是电压不稳在作祟。而一个精心构建的贝叶斯网络不仅能给出故障概率还能清晰指出“当前最可能的根因是冷却液压力传感器读数异常概率78%其次是主轴温度过高概率65%”这直接决定了维修工该带哪套工具出门。2.2 图结构手工构建 vs. 自动学习——没有银弹只有权衡构建贝叶斯网络的第一步永远是确定图的结构——也就是哪些变量之间存在直接的因果箭头。这里没有万能钥匙只有两条路专家手工构建或算法自动学习。专家手工构建这是医疗、法律、航天等高可靠性领域的黄金标准。领域专家如资深心脏病专家基于数十年临床经验画出“高血压 → 动脉硬化 → 心肌缺血 → 心绞痛”这样的链条。它的优势是可解释、可审计、符合物理/生理规律。一个由心内科主任和生物医学工程师共同审定的网络其结构本身就能作为临床指南的数字化延伸。但它的致命短板是耗时、昂贵、且难以覆盖所有边缘情况。专家可能忽略一个罕见的药物相互作用或者对某个新兴生物标志物的影响路径存疑。自动结构学习算法如PC算法、GES算法仅凭观测数据尝试找出最能解释数据联合分布的图结构。它像一个不知疲倦的数据侦探能在海量电子病历中挖掘出“某种基因突变 → 特定代谢物水平异常 → 对某类化疗药耐药”这样连专家都未曾系统总结的新路径。它的优势是发现未知关联、处理高维数据。但它的陷阱在于数据中的混杂因素confounding会让算法画出完全错误的箭头。如果数据里恰好没有记录“患者年龄”这个关键变量算法很可能错误地推断出“白细胞计数 → 死亡率”而忽略了“高龄 → 白细胞计数异常 高龄 → 死亡率升高”这个真正的因果链。我亲身经历的一个项目里团队用自动学习得到一个“血糖波动 → 视网膜病变进展”的强连接结果上线后效果惨淡。复盘才发现数据里缺失了“糖尿病病程”这个隐藏父节点——病程长的患者血糖自然波动大视网膜也更容易病变。最终我们采用了混合策略先用自动学习生成一个初始图再由3位不同资历的内分泌科医生逐条审核、修正、补充箭头并强制加入已知的病理生理学约束如“胰岛素抵抗”必须是“空腹血糖”和“HbA1c”的父节点。这个过程慢但产出的网络既吸收了数据的洞见又坚守了医学的底线。2.3 概率参数从专家估计到数据拟合——精度与效率的拉锯战图结构定了接下来要填满每个节点的条件概率表CPT。一个有3个父节点、每个父节点有2种取值的布尔变量其CPT就有2³8行。变量一多CPT就会指数级膨胀这就是著名的“维度灾难”。如何填满这些表格同样面临选择专家直接赋值对于“吸烟 → 肺癌”的风险提升倍数可以直接引用流行病学研究的OR值比值比并将其转化为条件概率。这种方法零数据依赖、逻辑清晰特别适合缺乏历史数据的新领域如一种刚上市的靶向药的副作用谱。但它的弱点是主观性强、难以量化不确定性。专家说“大概提升3倍”这个“大概”怎么变成一个精确的0.03还是0.05最大似然估计MLE用历史数据直接统计频率。比如在1000名吸烟者中有45人确诊肺癌则P(肺癌是 | 吸烟是) 45/1000 0.045。这是最直观的方法但在小样本或稀疏组合下会崩溃。如果某个父节点组合在数据中从未出现过比如“同时服用A药、B药、且年龄80岁”的患者为0例MLE会给出0概率导致整个网络推理失效。贝叶斯估计推荐这才是贝叶斯网络的灵魂所在。我们为每个CPT条目预设一个先验分布常用Dirichlet先验然后用数据更新它得到后验分布。例如对P(肺癌是 | 吸烟是)我们先假设一个“基础风险”先验如Beta(1, 99)对应均值1%再用45例肺癌/1000例吸烟者的数据更新得到后验Beta(145, 99955)。最终我们不用一个点估计0.045而是用后验分布的均值≈0.044或整个分布进行后续推理。这带来了两个革命性好处第一天然平滑了稀疏数据问题——即使某个组合为0例后验概率也不会是0而是被先验温柔地“拉回”合理范围第二量化了参数本身的不确定性。当我们说“P(肺癌|吸烟) 0.044 ± 0.005”这个±0.005就是后验标准差它告诉我们这个估计有多可靠。在医疗决策中这个不确定性信息至关重要一个0.044±0.001的估计和一个0.044±0.02的估计对是否启动昂贵的早期筛查意味着完全不同的决策权重。3. 核心细节解析与实操要点3.1 从一张纸草图到可运行代码变量定义与状态编码的魔鬼细节很多教程一上来就讲“创建一个Node”却忽略了最基础也最易错的一步如何定义变量及其状态states。这绝不是简单的“是/否”二选一。我曾调试过一个工业故障诊断网络卡了整整两天最后发现根源在于“泵压力”这个变量的状态编码。离散化是艺术不是技术连续变量如温度、压力、血糖值必须离散化才能放入贝叶斯网络。粗暴地按固定区间切分如0-50℃, 50-100℃是新手大忌。正确的做法是结合领域知识和数据分布。在制冷系统中“泵压力”的关键阈值是“额定压力的80%”低压报警线和“120%”高压保护线。所以它的状态应定义为[正常, 偏低, 偏高, 严重偏高]边界点严格对应物理安全阈值。这样网络推理出“偏高”时工程师立刻知道该检查冷却水流量而如果只是按数据四分位数切分得到[Q1, Q2, Q3, Q4]这个结果对现场操作毫无指导意义。状态命名必须无歧义且可操作避免使用模糊词。“症状严重程度”不能定义为[轻, 中, 重]因为不同医生对“中”的理解天差地别。应改为客观可测的指标[3次/天, 3-5次/天, ≥6次/天]针对腹泻频率或[SpO2≥95%, SpO2 90-94%, SpO290%]针对血氧饱和度。我在一个远程监护项目中坚持将“活动量”定义为[卧床, 室内缓慢行走, 室内正常活动, 户外活动]并配套提供可穿戴设备的加速度计阈值算法。这样当网络推理出“活动量卧床”的概率高达92%护理APP就能自动触发“跌倒风险评估”流程而不是弹出一个模糊的“您今天不太活跃”的提示。处理缺失值网络的“沉默”比“胡说”更危险现实数据总有缺失。贝叶斯网络本身不直接处理缺失但你的数据预处理必须有预案。最稳妥的方案是显式引入一个unknown状态。例如“患者是否吸烟”这个变量状态应为[是, 否, 未知]。在构建CPT时为未知单独设置一行其概率分布通常设为该变量的先验边际分布即不考虑任何父节点时各状态的原始比例。这样当一条记录的“吸烟”字段为空时网络会将其视为未知并基于先验进行推理而不是报错或强行插补。我见过太多案例因为用了均值插补把一个本该是未知的变量硬塞进否结果网络错误地排除了吸烟相关的所有病因路径导致漏诊。3.2 条件概率表CPT的填充超越Excel的手工智慧当变量不多、状态简单时用Excel填CPT是可行的。但一旦网络稍大手工维护就成了噩梦。更重要的是手工填表极易违反概率公理。一个常见的错误是为“肺癌”节点填写P(肺癌是 | 吸烟是, 年龄青年)0.02P(肺癌否 | 吸烟是, 年龄青年)0.97两数相加只有0.99漏了0.01。这会导致后续所有概率计算失真。使用专业库的CPT构造器强烈推荐使用pgmpyPython或gRainR等库。它们内置了严格的校验。以pgmpy为例from pgmpy.factors.discrete import TabularCPD # 正确构造器会自动归一化并确保行和为1 cpd_lung_cancer TabularCPD( variableLungCancer, variable_card2, # 2个状态是/否 values[[0.001, 0.005, 0.02, 0.05], # P(否 | 吸烟, 年龄) [0.999, 0.995, 0.98, 0.95]], # P(是 | 吸烟, 年龄) —— 注意这里每列和必须为1 evidence[Smoking, Age], evidence_card[2, 2] # 吸烟2态年龄2态青年/老年 )关键点在于values是一个二维列表每一列column对应一个父节点状态组合该列的所有行rows之和必须严格等于1。pgmpy会在你调用add_cpds()时进行校验如果发现不归一会抛出明确的ValueError逼你回头检查。利用结构化先验知识压缩CPT面对高维父节点CPT会爆炸。这时可以引入因果独立性假设来降维。例如“肺癌”的风险主要取决于“吸烟”和“家族史”而“职业暴露”如石棉只在“吸烟”为“是”时才显著增加风险。我们可以将CPT拆解为当吸烟否时P(肺癌|家族史, 职业暴露)≈P(肺癌|家族史)忽略职业暴露当吸烟是时P(肺癌|家族史, 职业暴露)P(肺癌|家族史) * (1 β * 职业暴露)其中β是石棉暴露的风险乘数。 这种“上下文特异性独立性”Context-Specific Independence, CSI能将一个2x2x28行的CPT压缩成两个2x24行的子CPT大幅降低专家负担和数据需求。pgmpy支持通过StateName参数为不同上下文定义不同的CPT这是高级但极其实用的技巧。3.3 推理引擎的选择精确 vs. 近似——你的硬件说了算网络建好、参数填完下一步就是“问问题”。比如“已知患者咳嗽、发烧且CT显示肺部有阴影他患肺癌的概率是多少”——这就是后验概率查询Posterior Inference。但计算它是NP-hard问题。不同规模的网络需要不同的引擎精确推理Variable Elimination, Junction Tree适用于中小规模网络节点50CPT平均大小100。它通过数学上的变量消元给出100%准确的答案。pgmpy的VariableElimination类就是为此设计。它的优势是结果绝对可靠可用于关键决策如手术方案推荐。但代价是内存消耗巨大。一个有30个节点、每个节点平均5个父节点的网络消元过程中产生的中间因子factor可能占用数GB内存。我曾在一个客户现场因为没预估好内存VariableElimination直接把8GB内存的服务器拖垮进程被OOM Killer杀死。教训是在部署前务必用真实数据做压力测试监控峰值内存。近似推理Likelihood Weighting, Gibbs Sampling当网络庞大或CPT极复杂时精确推理不现实。此时蒙特卡洛方法登场。pgmpy的BeliefPropagation用于树状结构和GibbsSampling通用是主力。它们通过随机采样用大量样本的统计频率来逼近真实概率。例如GibbsSampling会生成10000个符合网络联合分布的样本然后统计其中“咳嗽是 发烧是 CT阴影 肺癌是”的样本占比作为P(肺癌是 | 咳嗽是, 发烧是, CT阴影)的估计。它的优势是内存友好、可扩展能处理数千节点的网络。但它的结果是有方差的估计值。你需要运行足够多的样本通常≥10000并观察结果的稳定性比如连续三次运行结果都在0.32±0.01范围内才算收敛。在医疗场景我会为关键查询设置一个“置信度阈值”如果Gibbs采样的标准误 0.005系统会自动提示“证据不足建议补充检查”而不是给出一个看似精确实则飘忽的答案。4. 实操过程与核心环节实现4.1 从零开始用pgmpy构建一个简易医疗诊断网络让我们动手实现一个真实的、可运行的案例一个用于初步筛查“社区获得性肺炎”CAP的贝叶斯网络。它包含5个核心变量Fever发烧、Cough咳嗽、ShortnessOfBreath呼吸困难、ChestXRay胸片结果、CAP是否确诊CAP。我们将采用混合构建法结构由呼吸科医生确认参数用贝叶斯估计填充。步骤1定义变量与状态from pgmpy.models import BayesianNetwork from pgmpy.factors.discrete import TabularCPD import numpy as np # 创建网络骨架 model BayesianNetwork([ (Fever, ChestXRay), (Cough, ChestXRay), (ShortnessOfBreath, ChestXRay), (ChestXRay, CAP) ]) # 定义所有变量的状态全部为布尔型简化起见 states { Fever: [No, Yes], Cough: [No, Yes], ShortnessOfBreath: [No, Yes], ChestXRay: [Normal, Abnormal], CAP: [No, Yes] }提示这里ChestXRay是CAP的直接父节点而非相反这体现了因果逻辑——胸片异常是CAP的一个表现effect而非原因cause。如果颠倒箭头网络就失去了诊断意义。步骤2填充CPT——以ChestXRay为例根据《美国感染病学会CAP指南》我们整理专家知识在无CAP患者中胸片正常的概率高达95%在CAP患者中胸片异常的概率约为85%但发烧、咳嗽、呼吸困难这些症状会提高胸片异常的“基线”概率即使没有CAP。我们用贝叶斯估计先验设为Dirichlet(α[1, 1])即均匀先验然后用虚拟数据“增强”# P(ChestXRay | Fever, Cough, ShortnessOfBreath, CAP) # 先验每个组合下Normal和Abnormal的先验计数都是1 # 虚拟数据基于指南添加CAPYes时Abnormal的计数85Normal15CAPNo时Abnormal5Normal95 # 然后对每个症状组合按比例缩放这些增量症状越多影响越大 # 为简化我们只展示一个组合当所有症状均为Yes且CAPYes时 # 先验计数: [1, 1] # 虚拟增量: Abnormal 85 * 1.5 (症状全有时敏感性略升), Normal 15 * 1.5 # 后验计数: [122.5, 1127.5] [23.5, 128.5] # 归一化概率: [23.5/152, 128.5/152] ≈ [0.155, 0.845] cpd_chest_xray TabularCPD( variableChestXRay, variable_card2, # values的列顺序必须严格对应evidence的笛卡尔积 # evidence[Fever,Cough,ShortnessOfBreath,CAP]每个2态共16列 # 我们只填充前2列作为示意CAPNo时的两种情况 values[ [0.95, 0.95, 0.95, 0.95, 0.95, 0.95, 0.95, 0.95, # P(Normal | ...) 0.15, 0.15, 0.15, 0.15, 0.15, 0.15, 0.15, 0.15], # P(Abnormal | ...) —— 注意每列和必须为1 [0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.85, 0.85, 0.85, 0.85, 0.85, 0.85, 0.85, 0.85] ], evidence[Fever,Cough,ShortnessOfBreath,CAP], evidence_card[2, 2, 2, 2] )注意evidence_card[2,2,2,2]定义了父节点状态数values的列数必须是2⁴16。pgmpy要求values的行数等于variable_card这里是2列数等于所有父节点状态数的乘积16。每一列的数值之和必须为1否则会报错。这是新手最容易栽跟头的地方。步骤3添加所有CPT并验证# 添加其他CPT此处省略逻辑类似 model.add_cpds(cpd_fever, cpd_cough, cpd_breath, cpd_chest_xray, cpd_cap) # 关键验证 if model.check_model(): print(✅ 网络结构与CPT均有效) else: print(❌ 网络存在错误请检查)model.check_model()会执行三重校验1所有节点是否都有CPT2每个CPT的variable_card是否匹配节点定义3每个CPT的每一列是否归一化。这是上线前的必经关卡。步骤4执行一次诊断推理from pgmpy.inference import VariableElimination infer VariableElimination(model) # 查询已知患者发烧、咳嗽、呼吸困难胸片异常求CAP概率 result infer.query( variables[CAP], evidence{ Fever: Yes, Cough: Yes, ShortnessOfBreath: Yes, ChestXRay: Abnormal } ) print(result) # 输出 # ---------------- # | CAP | phi(CAP) | # # | No | 0.2100 | # | Yes | 0.7900 | # ---------------- # 即P(CAPYes | 症状, XRayAbnormal) 79%这个79%不是黑箱输出而是基于明确定义的因果路径和量化概率一步步推导出来的。它告诉你虽然胸片异常是强信号但仍有21%的概率是其他疾病如心衰导致的类似影像学表现。这为医生下一步开什么检查比如BNP检测来排除心衰提供了精准的决策依据。4.2 处理动态证据流在线更新与增量学习真实世界不是静态快照。一个重症监护室的贝叶斯网络需要每分钟接收新的生命体征心率、血压、血氧并实时更新对“脓毒症”状态的判断。这就要求网络具备在线推理Online Inference和增量学习Incremental Learning能力。在线推理pgmpy的VariableElimination是批处理的每次查询都要重新计算。对于高频更新我们改用信念传播Belief Propagation它预先将网络编译成一个“连接树”Junction Tree之后每次新证据到来只需在树上进行局部消息传递速度提升百倍。pgmpy的BeliefPropagation类支持此模式from pgmpy.inference import BeliefPropagation bp BeliefPropagation(model) bp.calibrate() # 一次性编译耗时但只需一次 # 之后每次新证据极速查询 result1 bp.query([Sepsis], evidence{HR: High, Temp: High}) # 几毫秒后新数据到来 result2 bp.query([Sepsis], evidence{HR: High, Temp: High, Lactate: High})增量学习当网络上线后不断积累新的诊断案例我们希望网络能“越用越聪明”自动优化其CPT参数。这不能靠重新训练整个网络太慢而要用序贯贝叶斯更新。核心思想是将CPT中每个条目的后验分布如Beta分布作为下一轮更新的先验。当一个新病例到来如FeverYes, CAPYes我们只更新与这个组合相关的CPT条目用新数据更新其Beta分布的α和β参数。pgmpy本身不直接支持但我们可以轻松封装class IncrementalCPD: def __init__(self, initial_alpha1, initial_beta99): self.alpha initial_alpha self.beta initial_beta def update(self, observed_outcome): # observed_outcome: TrueYes, FalseNo if observed_outcome: self.alpha 1 else: self.beta 1 def get_probability(self): return self.alpha / (self.alpha self.beta) # 为P(CAPYes | FeverYes) 创建一个增量CPT条目 cpd_cap_given_fever_yes IncrementalCPD(initial_alpha5, initial_beta95) # 先验5%基线风险 cpd_cap_given_fever_yes.update(observed_outcomeTrue) # 新病例发烧且确诊CAP print(f更新后风险: {cpd_cap_given_fever_yes.get_probability():.3f}) # 0.051这种方式让网络能持续从真实世界反馈中学习而无需停机、无需大数据集完美契合临床环境的渐进式知识积累。5. 常见问题与排查技巧实录5.1 “我的网络推理结果全是0或1”——归一化与数值溢出的隐形杀手这是最常被忽视、却最致命的问题。当你看到P(CAPYes | evidence) 1.000第一反应不该是“哇100%确诊”而应立即警觉这极大概率是数值计算溢出导致的假阳性。原因剖析贝叶斯推理的核心是乘法联合概率和除法条件概率。当网络很深、证据很多时中间计算的数值会变得极其微小如1e-200。在计算机浮点数表示中这会下溢underflow为0。一旦某个关键路径的概率变为0整个后验计算就崩了结果要么是0要么是1因为分母也变成了0产生NaN某些库会强制设为1。排查与解决启用对数空间计算所有主流库都支持。pgmpy中设置inference VariableElimination(model, show_progressFalse)后在查询时传入show_progressTrue它会打印中间因子的大小。如果看到Factor with 1000 states, min value: 0.0, max value: 1e-300就坐实了溢出。解决方案是改用对数空间infer.query(..., jointTrue)会返回对数概率你需要用np.exp()转换但要小心np.exp(-1000)仍是0。更优解是使用pgmpy的get_log_value()方法它内部已做防溢出处理。检查CPT的极端值人工填的CPT里有没有P(XYes | YNo) 0.0000001这种“几乎不可能”的设定在多次乘法后必然归零。应将其替换为一个合理的、非零的最小值如1e-6并确保所有CPT行都严格归一化。使用pgmpy的check_model()它不仅能检查归一化还能检测CPT中是否存在0值check_model()会警告Warning: CPD for node X contains zero probabilities。这是第一道防线。5.2 “网络告诉我A导致B但我知道明明是B导致A”——结构误设的识别与修正因果方向的错误是贝叶斯网络最深刻的失败。它不像数值错误那样容易被日志捕获而是一种“逻辑性错误”会悄无声息地腐蚀所有决策。识别信号反直觉的后验概率例如在一个“天气 → 衣服选择 → 感冒”的网络中如果输入衣服选择短袖网络却推断出天气寒冷的概率很高这就违背常识。因为短袖是天气热的结果而非原因。证据的“反向强化”失效在正确网络中观测到结果果应该能加强对其原因因的信念。例如观测到CTAbnormal应显著提高CAPYes的概率。如果观测CTAbnormal后CAPYes的概率反而下降说明箭头方向画反了。do-演算的悖论用do-calculus模拟干预。如果do(CTAbnormal)强行让胸片异常后CAPYes的概率飙升这显然荒谬——干预一个结果不应该改变其原因的概率。这直接证明CT → CAP的箭头是错的正确方向应是CAP → CT。修正流程DO-ITDraw the suspected path: 把你怀疑的因果路径单独画出来如A → B。Obtain domain knowledge: 找领域专家用“如果我阻止AB还会发生吗”和“如果我强制让B发生A会改变吗”这两个问题来验证方向。前者是因→果后者是果→因。Inspect data patterns: 查看数据中A和B的时序。在电子病历中“诊断日期”一定晚于“症状开始日期”这为症状 → 诊断提供了铁证。Test with intervention: 在小范围数据上用do()操作模拟看结果是否符合物理/生理规律。只有四步都通过箭头方向才算确立。5.3 “为什么我的自动学习算法画出了一个完全看不懂的图”——数据质量的终极审判当PC算法或GES算法吐出一个布满交叉箭头、毫无层次感的“蜘蛛网”时不要怪算法要立刻审查你的数据。三大数据原罪时间序列数据被当截面数据用把一个患者一年内每月的血压、血糖、用药记录当成12个独立患者的快照来喂给算法。算法会疯狂地在“本月血压”和“下月血糖”之间画箭头因为它只看到了“相关”却看不到“时间先后”。解药对时序数据必须使用专门的动态贝叶斯网络DBN学习算法或至少对数据做滞后处理lagged features。关键混杂变量Confounder缺失如前所述的“年龄”问题。算法在数据中找不到“年龄”就只能在它的子节点如“白细胞计数”、“死亡率”之间强行建立虚假连接。解药在建模前强制进行“混杂变量审计”。列出所有可能影响多个目标变量的潜在混杂因素如“社会经济地位”、“医疗可及性”并尽一切可能将其纳入数据集。哪怕只有代理变量如邮政编码代表的区域收入