在医疗物资、半导体、能源、食品等关键供应链(Critical Supply Chains)场景里,最困难的问题往往不是“知道哪里出了风险”,而是“决定先干预哪里才划算”。瓶颈节点可能同时出现很多个,但可用于干预的资源有限:加急运力有限、替代供应商有限、安全库存预算有限。这时候业务负责人需要的不是一个风险最高的名单,而是一个干预优先级清单:在给定预算、成本和风险约束下,哪些干预措施最值得做,先做哪个。DACRI(Decision-Aware Causal Intervention Ranking)正是围绕这一需求提出的方法思路:它把因果推断和决策优化放在同一个框架里,先估计每个候选干预的因果效果,再结合干预成本、风险、可行性和预算约束生成排序,而不是简单按预测风险或历史相关性排序。
下面按“核心概念、方法链路、Python 示例、效果评估、常见问题、生产落地”六条线展开。阅读前不需要深厚的因果推断背景,但要了解基本机器学习建模流程,并清楚供应链中常见的数据字段,比如提前期、库存水平、供应商表现、需求波动。
1. 先理解:为什么“干预排序”和“风险排序”不是一回事
1.1 预测回答“会发生什么”,干预回答“改变了什么”
传统的风险模型输出的是节点风险分。比如模型判断供应商 A 的风险分是 0.8,供应商 B 的风险分是 0.5,管理者往往会把资源优先投向 A。但这里有一个关键假设:风险越高的节点,干预它的收益越大。这个假设在很多供应链场景里并不成立。
举一个常见例子。某物料的供应商 A 风险高,是因为该物料天然稀缺,替代方案很少,即使投入资金加急也改变不了上游产能;供应商 B 风险中等,但风险主要来自常规库存波动,多加一批安全库存就能显著降低缺货概率。这时候按风险排序会把钱花在无效的 A 上,而按“干预收益”排序应该优先干预 B。
所以判断一个节点是否值得干预,不能看它的历史风险有多高,而要看“对某个节点施加干预之后,目标指标到底改善了多少”。这就是预测问题与因果干预问题的本质区别:预测关心的是未来会怎样,干预关心的是“我们改变了什么”。
1.2 因果效果需要反事实,不能只看干预前后的对比
要估计节点 i 的干预效果,理论上要让同一个节点 i 在相同时间同时出现在两种状态里:一种是被干预的状态,一种是未被干预的状态。两者之差就是因果效果。但现实中只有一个状态能被观察到,另一个是反事实(Counterfactual)。
如果直接比较“被干预节点”和“未被干预节点”的结果差异,很容易被选择偏差污染。供应链管理里存在非常典型的选择机制:越紧急、越危险的节点,越容易被优先干预。假设公司对库存告急的仓库临时加急补货,后续这些仓库的缺货率仍然偏高,是因为加急没有用吗?不是,是因为它们本来就是最差的。这种“干预分配给更差节点”的机制,会造成干预效果被系统性低估。
因果推断要做的事情,就是利用协变量构造出一个尽可能接近真实的反事实。供应链里的协变量包括需求波动、供应商健康度、库存缓冲、地理位置、历史准时交付率等。这些变量同时影响“是否被干预”和“结果表现”,是因果关系识别中必须控制的混淆因素。
1.3 决策感知意味着排序标准从“效果最大”变成“净收益最大”
即使拿到了每个候选节点的干预效果,直接按效果排序仍然不够。因为每个干预都有成本、风险、耗时和可行性约束。DACRI 里的 Decision-Aware,指的是排序环节必须显式引入决策变量,而不是把效果估计完就结束。
一个最简单的决策感知打分形式是:
net_score = 效果估计 - 干预成本 - 风险厌恶系数 * 风险溢价 * 效果估计还可以继续叠加预算约束、可并发干预数量、区域上限、最小可行效果阈值等。下面用一个对比表说明“只看效果”和“决策感知”的差异:
| 候选节点 | 估计效果 | 干预成本 | 风险溢价 | 按效果排序 | 净收益得分 | 按净收益排序 |
|---|---|---|---|---|---|---|
| 节点 A | 100 | 90 | 0.4 | 1 | 100 - 90 - 0.3*0.4*100 = -2 | 2 |
| 节点 B | 80 | 10 | 0.2 | 2 | 80 - 10 - 0.3*0.2*80 = 65.2 | 1 |
上表里节点 A 的效果更高,但成本很高且风险溢价大,净收益反而为负;节点 B 效果略低,但投入小、风险低,实际更值得做。这就是决策感知排序的核心思想:把“哪个干预最有效”翻译成“哪个干预最划算、最可行、最符合当前预算约束”。
2. DACRI 的方法链路:从候选干预到约束排序
2.1 五步主链路
DACRI 不是一个单独模型,而是一条可以拆成五步的技术链路。实际项目中,每一步都有独立的数据、代码和验收标准。
第一步,定义干预单元和干预类型。干预单元可以是供应商、仓库、运输线路、SKU、工厂产线。干预类型可以是增加安全库存、切换供应商、加急运输、增加质检频次、调整订单分配比例。注意,每个干预单元加每种干预类型组合起来,才是一个完整的候选干预。
第二步,定义目标指标。供应链场景常见的目标指标有订单准时交付率、缺货率、提前期、库存周转天数、单位采购成本。目标指标必须可量化、可观测、并且能被干预真实影响。
第三步,建立因果模型,估计每个候选干预的条件平均干预效果(CATE)。这一步处理混淆、选择偏差和反事实,输出每个候选干预的“效果估计”。
第四步,把效果估计映射到决策变量。这里的变量包括干预成本、风险溢价、执行时间、可行性标记。未上线的候选干预没有历史成本,需要业务侧给出预估。
第五步,在预算和约束条件下排序,生成决策清单。这一步输出可以直接交给供应链计划部门执行的优先级列表。
2.2 关键数据结构
为了让这条链路稳定运行,数据层必须先造好。项目里通常会维护一张“候选干预宽表”,字段大致如下:
| 字段 | 类型 | 说明 |
|---|---|---|
| candidate_id | string | 候选干预唯一标识,例如 supplier_A_switch 或 wh_101_stock |
| node_id | string | 干预单元标识 |
| intervention_type | string | 干预类型,如 add_safety_stock、switch_supplier |
| treated | int | 历史是否执行过该干预,1 或 0 |
| observed_outcome | float | 观测到的目标指标结果 |
| demand_volatility | float | 混淆变量:需求波动 |
| supplier_health | float | 混淆变量:供应商健康度 |
| inventory_buffer | float | 混淆变量:库存缓冲水平 |
| intervention_cost | float | 决策变量:预估干预成本 |
| risk_premium | float | 决策变量:干预效果的不确定性风险溢价 |
| feasible | int | 决策变量:当前周期是否可行 |
实际项目中,这张宽表由三部分拼接而来:基础业务表、因果建模使用的特征工程输出、以及业务侧提供的决策参数。建模时只取特征列和目标列;排序时再加入成本和风险列。数据拼接顺序不同会导致脏数据,建议在项目开始时就用统一的 candidate_id 做关联。
2.3 决策参数怎么定
决策感知排序需要几个超参数,每个参数都需要业务侧共同确认,而不是模型侧单方面指定。
| 参数 | 含义 | 默认建议 | 参数调大后 |
|---|---|---|---|
| 效果系数 | 目标指标单位收益换算成货币或统一分数 | 1 | 更偏好高效果干预 |
| 干预成本 | 每次执行干预的直接费用 | 业务实际成本 | 成本越高的候选越靠后 |
| 风险厌恶系数 alpha | 对效果不确定性的厌恶程度 | 0.2-0.4 | 更偏好风险溢价低的干预 |
| 风险溢价 | 效果估计方差或业务评估的不确定性 | 0-1 | 越高表示越不确定 |
| 预算上限 | 本轮可投入的总成本 | 财务给定 | 约束越紧,排序越需精打细算 |
| 最小可行效果 | 低于该效果不执行 | 0 或业务阈值 | 过滤掉更多低收益候选 |
这些参数在没有历史数据时可以用行业经验初设,然后通过敏感性分析检查排序是否对参数过敏。如果 alpha 从 0.1 调到 0.5 排序发生剧烈变化,说明效果估计的方差很大,排序结果需要谨慎对待。
3. 用 Python 跑通一个最小可复现的干预排序示例
3.1 准备环境和依赖
这里用一个最小示例说明整条链路。示例只依赖 pandas、numpy、scikit-learn,不引入额外因果库,方便在各环境中快速跑通。实际项目里可以再引入 econml、dowhy 等库,但依赖版本需要结合项目环境确认。
pip install numpy pandas scikit-learn示例在 Python 3.10 以上的常见环境中可以直接运行。如果公司内网有固定的 Python 镜像源,优先使用镜像安装,避免版本冲突。
3.2 构造带“真实效果”的模拟供应链数据
模拟数据的好处是可以自定义真实效果,方便验证估计器是否靠谱。下面代码生成 300 个候选干预节点,特征包括需求波动、供应商健康度、库存缓冲。真实效果只和供应商健康度、需求波动相关,这样后续可以用相关性和排序结果验证模型是否找回了规律。
import numpy as np import pandas as pd np.random.seed(42) n = 300 df = pd.DataFrame({ "node_id": np.arange(n), "demand_volatility": np.random.uniform(0, 1, n), "supplier_health": np.random.uniform(0, 1, n), "inventory_buffer": np.random.uniform(0, 1, n), }) # 业务侧预估的干预成本和风险溢价 df["intervention_cost"] = np.random.uniform(5, 50, n) df["risk_premium"] = np.random.uniform(0, 1, n) # 带真实效果的数据生成过程: # y0 是未干预的潜在结果,y1 是干预后的潜在结果 df["y0"] = ( 50 + 10 * df["demand_volatility"] - 15 * df["supplier_health"] + np.random.normal(0, 5, n) ) df["y1"] = ( 50 + 10 * df["demand_volatility"] - 15 * df["supplier_health"] + 20 * df["supplier_health"] * (1 - df["demand_volatility"]) + np.random.normal(0, 5, n) ) df["true_effect"] = df["y1"] - df["y0"] # 模拟非随机干预:健康度越差的节点,历史上越容易被干预 score = 0.6 * (1 - df["supplier_health"]) + 0.4 * df["demand_volatility"] p_treat = np.clip(0.2 + 0.5 * score, 0.05, 0.95) df["treated"] = np.random.binomial(1, p_treat, n) # 观测结果只能看到一种状态 df["observed_outcome"] = np.where(df["treated"] == 1, df["y1"], df["y0"])这段代码的关键点在于treated不是随机分配的,它和supplier_health、demand_volatility相关。如果直接用“干预组均值减对照组均值”来估计效果,会因为选择偏差得到错误结论,正好可以用于演示因果估计的必要性。
3.3 用 T-learner 估计条件平均干预效果
T-learner 是最直观的因果效果估计方法之一:对干预组和对照组分别训练一个结果预测模型,再用两个模型分别预测所有样本的pred_y1和pred_y0,两者之差就是效果估计。
from sklearn.ensemble import GradientBoostingRegressor features = ["demand_volatility", "supplier_health", "inventory_buffer"] X = df[features] y = df["observed_outcome"] model_treated = GradientBoostingRegressor( n_estimators=200, max_depth=3, random_state=42 ) model_control = GradientBoostingRegressor( n_estimators=200, max_depth=3, random_state=42 ) model_treated.fit(X[df["treated"] == 1], y[df["treated"] == 1]) model_control.fit(X[df["treated"] == 0], y[df["treated"] == 0]) df["pred_y1"] = model_treated.predict(X) df["pred_y0"] = model_control.predict(X) df["estimated_effect"] = df["pred_y1"] - df["pred_y0"]T-learner 的两个子模型分离了干预组和对照组,实现简单,但干预组样本少时容易过拟合。样本量允许时可以增加交叉验证,或换用 S-learner(把treated作为特征放进同一个模型)、DR-learner(引入双稳健估计)等方法。下面是一个 S-learner 的简写版本,便于对比:
df_with_flag = df.copy() df_with_flag["treated_flag"] = df_with_flag["treated"] model_s = GradientBoostingRegressor( n_estimators=200, max_depth=3, random_state=42 ) model_s.fit(df_with_flag[features + ["treated_flag"]], y) df_treat = df.copy(); df_treat["treated_flag"] = 1 df_ctrl = df.copy(); df_ctrl["treated_flag"] = 0 df["s_pred_y1"] = model_s.predict(df_treat[features + ["treated_flag"]]) df["s_pred_y0"] = model_s.predict(df_ctrl[features + ["treated_flag"]]) df["s_effect"] = df["s_pred_y1"] - df["s_pred_y0"]两种方法都可以用在 DACRI 的效果估计阶段,关键是先评估哪种方法在当前数据上偏差更小,而不是默认某一种最优。
3.4 加入决策成本、风险系数和预算约束
效果估计完成后进入决策感知打分。先设定风险厌恶系数 alpha,然后计算每个候选干预的净收益得分,并用可行性过滤掉风险过高或收益无法覆盖成本的候选。
alpha = 0.3 df["estimated_effect"] = df["estimated_effect"].clip(lower=0) df["net_score"] = ( df["estimated_effect"] - df["intervention_cost"] - alpha * df["risk_premium"] * df["estimated_effect"] ) df["true_net_score"] = ( df["true_effect"] - df["intervention_cost"] - alpha * df["risk_premium"] * df["true_effect"] ) df["feasible"] = ( (df["estimated_effect"] > df["intervention_cost"]) & (df["risk_premium"] < 0.7) ) ranking = df.loc[df["feasible"]].sort_values("net_score", ascending=False) print(ranking[["node_id", "estimated_effect", "intervention_cost", "risk_premium", "net_score"]].head(10))如果还需要考虑总预算,可以在排序结果上做一次贪心选择:按net_score从高到低遍历,成本不超过剩余预算则选中。贪心法不是全局最优,但在候选数量和约束不复杂时已经是工程上可接受的方案。
budget = 200 selected = [] remaining_budget = budget for _, row in ranking.iterrows(): cost = row["intervention_cost"] if cost <= remaining_budget: selected.append(row["node_id"]) remaining_budget -= cost if remaining_budget <= 0: break print("选中的候选干预节点数量:", len(selected)) print("剩余预算:", remaining_budget)3.5 输出排序结果并与真实效果对比
最后做一次验证:分别计算“估计效果”与“真实效果”的相关系数,“决策感知净收益”与“真实净收益”的相关系数。相关系数越高,说明估计和排序越接近真实情况。
print("估计效果与真实效果的相关性:", df[["estimated_effect", "true_effect"]].corr().iloc[0, 1].round(4)) print("决策净收益与真实净收益的相关性:", df[["net_score", "true_net_score"]].corr().iloc[0, 1].round(4))在随机种子 42 的模拟数据下,由于存在选择偏差,直接用观测数据计算“干预组均值减对照组均值”通常会得到与真实效果偏离较大的结果,而经过 T-learner 调整后的估计效果相关性更好。这正说明 DACRI 强调因果估计的必要性:排序是否可靠,首先取决于效果估计偏差是否得到控制。
4. 怎么验证排序是好的:离线指标与线上验证
4.1 合成数据是验证因果估计器的基准
真实供应链数据里永远看不到完整反事实,因此效果估计的真实性无法直接核对。合成数据是唯一可以“知道标准答案”的验证环境。做法是:先用已知公式生成潜在结果,模拟非随机干预机制,再跑完整 DACRI 链路,最后比较估计效果与真实效果。
合成数据可以验证的问题包括:
- 效果估计是否在群体水平上接近真实平均效果。
- 排序结果是否能把真实效果高的候选排到前面。
- 在预算约束下,模型选择的决策组合是否接近真实最优组合。
4.2 离线评估指标
离线评估不建议只看一个指标,建议同时看排序相关性和预算收益。常用指标如下:
| 指标 | 含义 | 使用场景 |
|---|---|---|
| Spearman 相关系数 | 估计效果与真实效果的排序一致性 | 验证效果建模质量 |
| AUUC / Qini 系数 | 累积干预收益曲线面积 | 验证排序是否能把高收益样本提前 |
| Top-budget Lift | 按预算取前 k 个干预的总收益与随机选择的比值 | 验证决策感知排序的业务收益 |
| 决策后悔值 | 模型选择组合与真实最优组合的收益差距 | 验证预算约束下的决策损失 |
Top-budget Lift 是最贴近业务场景的指标,因为它直接回答“在 200 万预算内,按模型排序执行干预,比随便选一批节点多挽回多少损失”。
4.3 线上验证:小流量、对照和灰度
关键供应链与互联网推荐场景不同,候选干预节点数量往往只有几十到几百个,很难做大规模随机实验。完全的随机对照实验可能伤害业务,所以线上验证需要分层设计。
推荐顺序是:先在仿真环境验证,再做小范围试点,试点时选择低风险、可逆的干预类型,例如短期加急运输而不是永久切换供应商。用试点组的实际结果与未干预的对照组比较,同时结合断点回归、时间序列分析等方法减少选择偏差。灰度期间要记录干预成本实际发生值和效果实际发生值,用于校准模型中的成本参数和风险溢价。
5. 常见问题与排查链路
5.1 估计效果明显偏大或偏小
| 现象 | 常见原因 | 检查方式 | 处理建议 |
|---|---|---|---|
| 群体平均效果远大于业务预期 | 混淆变量未控制完整 | 检查干预分配与特征相关性,计算倾向得分区间 | 加入更多混淆变量,或用双重机器学习 |
| 群体平均效果接近 0 或为负 | 选择偏差掩盖真实效果 | 对比干预组与对照组的特征分布 | 检查倾向得分重叠度,使用加权或匹配 |
| 排序效果与真实效果相关性很低 | 子模型过拟合或样本不足 | 查看干预组样本量,检查训练集误差 | 增加交叉验证,换 S-learner 或 DR-learner |
5.2 排序结果与业务经验冲突
先不要急着改模型。业务经验往往来自风险感知,而模型输出的是干预收益。两者本来就是不同的目标。和业务方确认时,要展示决策得分拆解:效果贡献多少、成本扣了多少、风险扣了多少。如果业务仍认为某个节点应该排在前面,大概率是成本参数或风险溢价设置不准确,而不是效果模型错了。
5.3 干预不是随机分配导致选择偏差
这是最容易踩的坑。排查方式是绘制被干预组与未干预组的特征分布,并计算倾向得分重叠区间。如果重叠度很低,说明干预分配几乎由某些特征决定,此时任何模型都很难识别反事实。处理办法:扩大样本范围,只对倾向得分重叠区域的候选做排序,或者在效果估计中加入倾向得分加权。
5.4 成本和风险数据缺失
新干预类型往往没有历史成本。此时不要直接填 0,否则会把高成本干预误判为高收益。建议由采购或运营团队提供成本区间估计,用中位数参与排序,用上下界做敏感性分析。风险溢价缺失时,可以先用效果估计的方差代替,后续再逐步积累主观评估标准。
5.5 动态反馈循环
干预执行后,供应链系统结构会改变。一个节点被加急,可能影响相邻节点的运力和库存。如果长期不重新估计,排序结果会失真。建议按周或按月滚动重新建模,同时保留一部分节点作为长期不干预的对照,用于监测系统变化。
6. 生产落地:从 Jupyter 到可信任的决策工具
6.1 数据链路与特征治理
生产环境的 DACRI 不只是跑一个模型,而是要有稳定的数据链路。候选干预宽表需要定时更新,干预执行结果需要回流,成本参数需要与财务系统对齐。常见问题是特征在训练时和上线时口径不一致,所以特征工程代码要固化为统一模块,并用数据质量检查拦截缺失率异常、分布偏移和重复主键。
生产环境还需要把学习环境里的“一次性代码”改造成可重跑任务:输入数据版本、模型版本、参数版本都要能追溯。这样某一轮排序结果出问题时,可以快速定位是数据变化、特征变化还是模型发布引起的。
6.2 可解释性要求
关键供应链的干预决策通常要经过计划部门审批,模型不能是黑盒。建议在每个候选干预旁边输出决策得分分解,至少包含以下信息:
- 谁的干预:节点 ID 和干预类型。
- 效果估计是多少:模型预测的指标改善幅度。
- 成本是多少:本轮预估干预成本。
- 风险溢价是多少:不确定性水平。
- 约束条件:为什么被过滤或为什么被选中。
用 Shapley 值解释效果模型时可以说明“哪些特征让效果估计变高”,但业务侧更关心的是“为什么排在第一”,所以得分分解比特征归因更重要。
6.3 监控、回滚和人审
模型输出直接影响业务资源分配,必须建立监控和回滚机制。监控指标包括:
- 输入数据分布是否偏移。
- 效果估计均值是否突然跳变。
- 成本实际值与预估值的偏差是否扩大。
- 风险溢价是否长期不更新。
- 排序列表与上一周期相比的变化率。
当监控指标异常时,应自动冻结排序输出,转人工审核。回滚方案要提前准备:保留上一版本的排序结果,一旦发现当前版本有问题,可以在一小时内切回旧版本继续执行计划。
6.4 发布前检查清单
以下清单可直接用于项目评审:
- 候选干预单元和干预类型是否定义清晰,是否有遗漏。
- 目标指标是否与业务决策目标一致,是否存在多目标冲突。
- 历史干预记录是否完整,是否记录了干预时间、执行成本和结果。
- 混淆变量是否覆盖需求、供应、库存、地理、时间等主要维度。
- 效果估计是否经过合成数据或历史回测验证。
- 成本参数是否经过业务确认,是否有敏感性分析。
- 预算、可行性、风险阈值是否纳入排序约束。
- 排序结果是否有解释信息,便于人工审核。
- 是否有监控、回滚和数据版本记录机制。
- 是否保留了长期未干预的对照组,用于持续评估模型效果。
DACRI 类的方案在关键供应链场景里真正落地,难点通常不在因果模型本身,而在数据链路是否打通、决策参数是否可信、人工审批流程是否配合。从一个小范围试点开始,先把效果估计、成本校准和排序输出跑通,再逐步扩大覆盖面,比一开始就设计全量自动化系统要稳妥得多。对新手而言,最有价值的练习是先构造一份带真实效果的合成数据,亲手验证 T-learner 与朴素均值对比的差异,这一步理解了,DACRI 的因果部分就建立起来了。