简介:这份资源是斯坦福大学Stefan Wager于2024年9月更新的《Causal Inference: A Statistical Learning Approach》教科书草稿,面向数据科学、统计学、经济学与公共政策领域的学习者和研究者,帮助读者用统计学习方法系统理解因果关系的估计与推断。全书共十二章,从随机对照试验中的差异均值估计与回归调整讲起,依次覆盖无混淆性与倾向得分、双重稳健方法与双重机器学习、异质处理效应、政策学习、自适应实验、平衡估计器、回归断点设计,并延伸至内生处理、局部平均处理效应、溢出与干扰等进阶主题,每章附有文献注释便于延伸阅读。资源包为1个PDF文件,大小约1.66MB,轻量便携,适合在电脑或平板上随时查阅。目前已有1996人学习下载,可作为因果推断课程教材或自学参考,帮助读者掌握从实验设计到复杂观测数据分析的完整方法论。
1. 因果推断这本2024教材,为什么让做AB测试的人集体破防
做增长的人大多有过这种经历:AB测试跑出来一个指标显著上涨,全组欢天喜地上了线,三个月后复盘发现大盘纹丝不动,甚至略微下滑。你回头去查实验设计,样本量够、分流随机、埋点没丢,一切看起来都对,但结论就是错的。问题往往不在工程侧,而在你问的那个问题本身——你测的是相关性,却当成了因果性。
因果推断这门学问,干的就是把「相关」和「因果」分开这件事。2024年斯坦福出的这本因果推断教材,把过去散落在计量经济学、流行病学、机器学习三个圈子里的话语体系捏到了一起,用潜在结果框架统一讲清楚:什么条件下你能从观测数据里估出因果效应,什么条件下必须做实验,什么条件下两者都不行。它适合三类人:天天跑AB测试但总被质疑结论的产品数据同学、想从预测模型转向决策模型的算法工程师、以及做用户增长却总在「归因」上翻车的运营分析岗。这篇笔记不逐章翻译教材,而是把它拆成能上手复现的路径,告诉你哪些章节值得死磕、哪些公式可以跳过、代码怎么落地。
2. 潜在结果框架:把「如果」变成可计算的量
2.1 因果推断到底在估什么
先建立一个最小认知:因果推断的核心不是算一个数,而是定义清楚你要算的那个数。教材里反复强调的潜在结果(potential outcomes)框架,说白了就是给每个个体想象两个平行世界:一个接受了干预的世界,一个没接受干预的世界。个体 i 的因果效应就是这两个世界里结果的差:
τ_i = Y_i(1) - Y_i(0)问题在于,你永远只能观测到其中一个。一个人吃了药就是吃了,你没法同时看到他没吃的样子。这就是因果推断的根本难题(fundamental problem of causal inference)。所以真实世界里我们退而求其次,去估平均处理效应(ATE):
ATE = E[Y(1) - Y(0)]教材前几章花大量篇幅讲这个定义,不是啰嗦,是因为后面所有方法——倾向得分、双重稳健、工具变量——都是在不同假设下逼近这个量。你如果跳过定义直接看方法,很容易在选型时抓瞎。
2.2 三个假设决定你能不能做因果
教材把识别因果效应需要的假设归纳得很清楚,我按落地时的检查顺序重排一下:
| 假设 | 含义 | 落地检查点 |
|---|---|---|
| 一致性 | 观测到的结果等于对应干预下的潜在结果 | 干预定义是否明确无歧义 |
| 可交换性 | 处理组和对照组在协变量上可比 | 有没有做分层或匹配 |
| 正值性 | 每个协变量组合下都有接受干预的可能 | 有没有某些人群从未被干预 |
一致性最容易被忽视。比如你定义「干预=用户看了推荐位」,但推荐位有五种样式,那你的干预其实是个复合体,估出来的效应没法解释。可交换性在观测数据里几乎永远不成立,所以才需要后面那些调整方法。正值性在做分层分析时会直接暴露——某些层里处理组样本为零,那一层的效应就估不出来。
2.3 用 Python 跑通一个最小因果估计
教材偏理论,但落地必须落到代码。我用一个模拟数据集演示最基础的分层估计,这也是理解后续所有方法的起点:
import numpy as np import pandas as pd np.random.seed(42) n = 5000 # 协变量:用户活跃度,同时影响是否被干预和结果 activity = np.random.normal(50, 15, n) # 干预分配:活跃度越高越容易被干预(这就是混淆) propensity = 1 / (1 + np.exp(-(activity - 50) / 10)) treatment = np.random.binomial(1, propensity) # 结果:干预有真实效应 +5,活跃度本身也影响结果 outcome = 2 * activity + 5 * treatment + np.random.normal(0, 10, n) df = pd.DataFrame({ 'activity': activity, 'treatment': treatment, 'outcome': outcome }) # 朴素比较:直接算组间均值差(有偏) naive = df[df.treatment == 1].outcome.mean() - df[df.treatment == 0].outcome.mean() print(f"朴素估计: {naive:.2f}") # 分层估计:按活跃度分箱后在每层内比较,再加权平均 df['stratum'] = pd.qcut(df.activity, q=10, labels=False) stratum_effects = df.groupby('stratum').apply( lambda g: g[g.treatment == 1].outcome.mean() - g[g.treatment == 0].outcome.mean() ) # 按每层样本量加权 weights = df.groupby('stratum').size() / len(df) stratified = (stratum_effects * weights).sum() print(f"分层估计: {stratified:.2f}")这段代码的关键在最后两步。朴素估计直接把所有处理组和对照组混在一起比,混淆变量 activity 的差异会污染结果,跑出来通常明显偏离真实的 +5。分层估计先按 activity 分箱,保证每层内处理组和对照组活跃度接近,再在层内比较,最后按层样本量加权还原到总体。参数上,q=10 是分箱数,箱数太少残余混淆还在,箱数太多每层样本不足导致方差爆炸,实践中一般取 5 到 20 之间,看样本量定。跑完你会发现分层估计明显更接近 5,这就是控制混淆的直观效果。
3. 从倾向得分到双重稳健:观测数据里的三板斧
3.1 倾向得分匹配为什么在业务里最常用
分层估计的致命伤是维度。协变量一多,分层就组合爆炸,每层样本稀得没法看。倾向得分(propensity score)的思路是把所有协变量压缩成一个一维分数——个体接受干预的概率,然后在这个分数上做匹配或加权。教材里证明了:只要可交换性在协变量上成立,它在倾向得分上也成立。这个定理是整套方法的基石。
业务里最常用的是倾向得分匹配(PSM):给每个处理组个体,在对照组里找一个倾向得分最接近的。实现上要注意,匹配是有放回还是无放回、用最近邻还是卡尺匹配,结果会差不少。我一般先用逻辑回归估倾向得分,检查一下得分分布的重叠区域,重叠太差说明两组人根本不可比,这时候硬做匹配就是自欺欺人。
3.2 逆概率加权:一行公式背后的坑
逆概率加权(IPW)是另一个高频方法,思路是给每个样本按倾向得分的倒数加权,让加权后的样本看起来像随机实验:
from sklearn.linear_model import LogisticRegression # 估倾向得分 X = df[['activity']].values y = df['treatment'].values ps_model = LogisticRegression().fit(X, y) df['ps'] = ps_model.predict_proba(X)[:, 1] # 逆概率加权估计 ATE # 处理组权重 1/ps,对照组权重 1/(1-ps) df['weight'] = np.where( df['treatment'] == 1, 1 / df['ps'], 1 / (1 - df['ps']) ) ate_ipw = ( (df[df.treatment == 1].outcome * df[df.treatment == 1].weight).sum() / df[df.treatment == 1].weight.sum() - (df[df.treatment == 0].outcome * df[df.treatment == 0].weight).sum() / df[df.treatment == 0].weight.sum() ) print(f"IPW 估计: {ate_ipw:.2f}")逻辑说明:处理组个体的权重是 1/ps,意思是「这个人本来不太可能被干预,但他被干预了,所以给他更大权重来代表那些和他相似但没被干预的人」。对照组同理。参数上最大的坑是倾向得分接近 0 或 1 的样本,权重会爆炸,一个样本就能把整个估计带偏。常见做法是给倾向得分做截断(trimming),比如把小于 0.05 或大于 0.95 的样本剔掉,或者用稳定权重(stabilized weights)把权重除以边际干预概率。教材里专门有一节讲这个,别跳过。
3.3 双重稳健:两个模型都错一点也没关系
IPW 依赖倾向得分模型估得准,回归调整依赖结果模型估得准。双重稳健(doubly robust)把两者结合,只要有一个模型估对了,最终估计就是一致的。这是它名字的由来,也是它在工业界越来越受青睐的原因:
from sklearn.linear_model import LinearRegression # 结果模型:用协变量和干预预测结果 Xr = df[['activity', 'treatment']].values outcome_model = LinearRegression().fit(Xr, df['outcome']) # 预测每个样本在干预和对照下的结果 df['y1_hat'] = outcome_model.predict(df[['activity']].assign(treatment=1).values) df['y0_hat'] = outcome_model.predict(df[['activity']].assign(treatment=0).values) # 双重稳健估计 dr = ( df['y1_hat'] - df['y0_hat'] + df['treatment'] * (df['outcome'] - df['y1_hat']) / df['ps'] - (1 - df['treatment']) * (df['outcome'] - df['y0_hat']) / (1 - df['ps']) ).mean() print(f"双重稳健估计: {dr:.2f}")公式看着复杂,拆开就三段:第一段是结果模型直接给出的效应预测,第二段是处理组里结果模型预测残差的加权修正,第三段是对照组的对称修正。当结果模型准的时候,残差项趋近于零,估计退化成纯回归;当倾向得分模型准的时候,修正项把回归的偏差拉回来。参数上,两个模型的特征工程要分开做,别偷懒用同一套特征,否则两个模型的相关误差会抵消掉双重稳健的优势。实践中我一般倾向得分用逻辑回归保证可解释性,结果模型用梯度提升树抓非线性。
4. 工具变量与断点回归:当干预没法随机分配时
4.1 工具变量:找一个只通过干预影响结果的变量
有些场景你根本没法控制谁接受干预,比如用户自己选择要不要开通会员,这时候可交换性彻底不成立。工具变量(IV)的思路是找一个变量 Z,它影响干预 T,但除了通过 T 之外不影响结果 Y。教材里用「距离」举过例子,我换成业务场景:某公司想估会员对消费的影响,但开通会员是用户自选的。如果公司做过一轮随机发放的会员优惠券,那「是否收到优惠券」就是一个候选工具变量——它影响开通行为,但理论上不直接影响消费(除非优惠券本身有独立效果,那就要小心)。
IV 的估计量最常见的是两阶段最小二乘(2SLS):第一阶段用 Z 预测 T,第二阶段用预测出来的 T 去预测 Y。代码上 statsmodels 有现成实现,但坑在于弱工具变量——如果 Z 对 T 的解释力太弱,估计量方差会大到没法用。经验规则是第一阶段的 F 统计量要大于 10,低于这个数就别硬做了。
4.2 断点回归:用一条人为的线做准实验
断点回归(RDD)适合那种「按某个连续变量卡阈值」的场景。比如某平台规定粉丝数超过 1 万才能开通某个功能,那 9999 和 10001 的创作者在各方面几乎一样,唯一区别就是有没有跨过阈值。比较阈值两侧的结果差异,就能估出功能的效果。教材里把清晰断点和模糊断点分开讲,业务里大多数是模糊断点——跨过阈值不必然接受干预,只是概率大幅提高。
RDD 落地最关键是带宽选择。带宽太宽,两侧样本可比性下降;带宽太窄,样本量不够。常见做法是先用交叉验证选带宽,再做局部线性回归,并且一定要画图看断点两侧有没有明显的跳跃,图比数字更能暴露问题。如果协变量在断点处也跳了,说明你的断点设计有问题,不是干净的准实验。
5. 避坑指南:因果推断落地时最容易翻车的五件事
5.1 把预测模型当因果模型用
现象:用 XGBoost 训了一个用户流失预测模型,特征重要性显示「客服投诉次数」排第一,于是决定减少投诉来降低流失。上线后流失率没变。
原因:特征重要性衡量的是预测力,不是因果效应。投诉多的用户本来就更容易流失,减少投诉记录并不会改变他们的流失倾向。预测模型学的是相关性,因果推断要的是干预后的反事实。
解决:任何要指导干预的结论,必须走因果推断流程。先画因果图(DAG)明确混淆变量,再选对应方法。特征重要性只能用来做特征筛选,不能用来做决策依据。
5.2 倾向得分模型过拟合
现象:IPW 估计出来的效应大得离谱,或者置信区间宽到没有意义。
原因:倾向得分模型用了太多特征或者太复杂,导致部分样本的倾向得分接近 0 或 1,权重爆炸。这在样本量不大、特征维度高的时候特别常见。
解决:倾向得分模型宁简勿繁,逻辑回归加少量关键协变量通常够用。估完得分先看分布直方图,两组重叠区域太窄就说明可交换性假设不成立,这时候应该回头改设计而不是硬调模型。必要时做权重截断,把极端权重压到合理范围。
5.3 忽略干预的一致性定义
现象:估计出「推送对留存有正向效应」,但运营照着做之后没效果。
原因:干预定义太模糊。「推送」可能包含多种文案、多个时段、多个渠道,你估的是一个混合效应,运营落地时只用了其中一种,自然对不上。
解决:干预定义要细到可操作。如果确实想估一类干预的平均效应,那就在论文或报告里写清楚这个平均是对什么分布平均的,别让下游误以为可以随便挑一种执行。
5.4 用观测数据估的效应直接指导个体决策
现象:ATE 显示干预平均有效,于是对所有用户都上干预,结果一部分用户被伤害了。
原因:ATE 是平均效应,个体效应可能正负不一。教材里专门讲了异质性处理效应(HTE),平均有效不代表对每个人都有效。
解决:如果要做个体决策,得估条件平均处理效应(CATE),用因果森林或者元学习器(S/T/X-learner)。但要注意,CATE 估计的方差比 ATE 大得多,样本量不够时估出来的个体效应基本是噪声。
5.5 混淆变量没测全就下结论
现象:控制了用户活跃度、注册时长、设备类型之后,估计出的效应仍然和后续实验对不上。
原因:还有未观测的混淆变量。可交换性假设在观测数据里永远无法完全验证,你只能尽量控制已知混淆,对未知的保持警惕。
解决:做敏感性分析。教材里介绍了 E-value 之类的工具,用来量化「需要多强的未观测混淆才能把结论推翻」。如果 E-value 很小,说明结论很脆弱,别急着上线。更稳妥的做法是,观测数据得出的结论只作为假设,最终用随机实验验证。
6. 把教材读薄:我的学习路径和一个验证技巧
教材厚是正常的,但读法可以讲究。我的习惯是分三轮:第一轮只读每章开头的例子和结尾的结论,建立直觉;第二轮挑和自己业务最相关的两三个方法精读,把公式推一遍;第三轮才动手复现,用模拟数据验证自己真的理解了假设。绝大多数人卡在第一轮就放弃了,其实没必要逐页啃。
这里分享一个我常用的验证技巧:用模拟数据做「已知答案」测试。你自己设定一个真实的因果效应,比如 +5,然后生成带混淆的数据,看你的方法能不能估回 5 附近。这个技巧能帮你快速判断一个方法在你的数据特征下是否可靠。下面是一个模板:
def simulate_and_estimate(true_effect, n=5000, confounding_strength=1.0): """生成带混淆的数据,返回真实效应和估计效应""" np.random.seed(0) x = np.random.normal(0, 1, n) # 混淆强度控制干预分配对 x 的依赖 p = 1 / (1 + np.exp(-confounding_strength * x)) t = np.random.binomial(1, p) y = true_effect * t + confounding_strength * x + np.random.normal(0, 1, n) # 这里换成你要验证的方法 naive = y[t == 1].mean() - y[t == 0].mean() return true_effect, naive for strength in [0.5, 1.0, 2.0]: true, est = simulate_and_estimate(5.0, confounding_strength=strength) print(f"混淆强度 {strength}: 真实 {true}, 朴素估计 {est:.2f}")跑一遍你会直观看到:混淆越强,朴素估计偏离真实值越远。然后把你自己的方法(分层、IPW、DR)替换进去,对比不同方法在不同混淆强度下的表现。这个练习做上十几次,你对每种方法的适用边界就有手感了,比看十篇推导都管用。
最后说一个我踩过的坑:我曾经在一个项目里用倾向得分匹配估出了一个很漂亮的效应,兴冲冲去汇报,结果被问「你的倾向得分模型用了哪些变量、为什么用这些」,我答不上来,因为我是照着教程抄的。因果推断里,每一个变量进模型都要有因果图上的理由,不是特征越多越好。从那以后我养成了一个习惯:动手之前先画 DAG,把每个变量为什么进调整集写清楚,画不出来就说明我还没想明白,那就先别做。这个习惯帮我省下了至少三次返工。希望帮到你。
本文还有配套的精品资源,点击获取