简介:这份资源是斯坦福大学Stefan Wager撰写的《Causal Inference: A Statistical Learning Approach》2024年9月草稿版教科书,面向数据科学、统计学、经济学与公共政策领域的学习者和研究者,帮助其系统掌握因果推断的统计学习方法。全书从随机对照试验中的差异均值估计与回归调整讲起,依次覆盖无混淆性与倾向得分、双重稳健方法与双重机器学习、异质处理效应估计、政策学习、自适应实验、平衡估计器以及回归断点设计,并延伸至内生处理、局部平均处理效应、溢出与干扰等进阶主题,兼顾基础理论与前沿技术。资源包为1个PDF文件,大小约1.66MB,单文件结构便于检索与随身阅读。目前已有2003人学习下载,适合希望借助目录框架循序渐进理解因果关系、并对照自身研究场景查阅相应章节的读者。
1. 因果推断落地:从一份 2024 年 9 月草稿版教材说起
做数据科学的同行大概都有过这种体验:业务方问“这个策略到底有没有用”,你跑完 A/B 测试,均值差一减,置信区间一算,报告交上去,心里却清楚——样本小、分流不干净、用户互相影响,结论未必站得住。因果推断这门手艺,卡人的从来不是“会不会跑回归”,而是“知不知道什么时候不能跑、跑了之后怎么解释”。手头这份 Stefan Wager 写的《Causal Inference: A Statistical Learning Approach》,标注为 2024 年 9 月 6 日草稿版,正文 16 章加习题,从随机对照试验一路铺到回归断点、工具变量、干扰与溢出、动态策略,正好补的是“从实验设计到统计学习”这条链上的方法论缺口。它适合已经会用 Python 做建模、但一遇到“观测数据能不能当实验用”就发怵的从业者,也适合要把因果模块塞进推荐、风控、增长系统里的工程同学。下面按我拆书时的实际顺序,讲清它怎么用、参数怎么设、坑在哪。
2. 随机对照试验与回归调整:先把基准估计器跑通
2.1 差分均值估计器:三行代码背后的两个假设
教材第 1 章开篇就把最朴素的差分均值估计器(difference-in-means)摆出来,公式是处理组均值减对照组均值。它之所以能作为整本书的地基,是因为在随机化成立时它是有限样本无偏的,而且几乎不依赖分布假设。但“几乎不依赖”不等于“不依赖”,原文明确点了两个前提:SUTVA(个体处理值稳定假设,即一个人的结果不受别人处理分配影响)和随机分配条件(在给定全部潜在结果与处理组人数的条件下,每个单元被处理的概率相同)。我一般会先把这两个假设写成检查清单,再动手写代码,否则后面所有置信区间都是空中楼阁。
import numpy as np import pandas as pd from scipy import stats def diff_in_means(df, y_col, w_col): """差分均值估计 + 渐近正态置信区间 df: 含处理列与结果列的数据框 y_col: 结果变量名 w_col: 二值处理变量名(1 处理,0 对照) """ treated = df.loc[df[w_col] == 1, y_col].to_numpy() control = df.loc[df[w_col] == 0, y_col].to_numpy() n1, n0 = len(treated), len(control) tau_hat = treated.mean() - control.mean() # 教材式 (1.10) 的插件方差估计 v_hat = (n1 / n0) * treated.var(ddof=1) / n1 + (n0 / n1) * control.var(ddof=1) / n0 se = np.sqrt(v_hat / (n1 + n0)) ci = (tau_hat - 1.96 * se, tau_hat + 1.96 * se) return {"tau": tau_hat, "se": se, "ci95": ci, "n1": n1, "n0": n0}这段代码对应教材式 (1.4) 与式 (1.10)。参数上要注意两点:ddof=1是样本方差的无偏修正,样本量小于 30 时别偷懒用总体方差;置信区间用的是正态近似,教材定理 1.2 要求潜在结果二阶矩有界且处理分配是伯努利试验,如果你的实验是完全随机化(固定处理组人数)而非逐个体独立抛硬币,这个方差公式只是近似,严格做法要换用基于固定人数的方差表达式。我踩过的坑是拿分层随机化的数据直接套这个函数,层间处理概率不同,合并后的均值差会有偏,正确做法是先分层估计再加权,这正是第 2 章的内容。
2.2 回归调整:协变量不是越多越好
第 1.2 节讲随机试验里的回归调整,核心结论是:在随机化成立的前提下,把处理变量和预处理协变量一起放进线性回归,处理项系数仍是处理效应的无偏估计,而且当协变量与结果强相关时,方差会明显下降。教材给的直觉是回归调整相当于对差分均值做了一次“用协变量解释掉结果波动”的修正。实操里我用 statsmodels 的 OLS 加稳健标准误,处理项放第一位,协变量只放实验开始前就能观测到的字段。
import statsmodels.api as sm import statsmodels.formula.api as smf def regression_adjust(df, y_col, w_col, covars): """随机试验中的回归调整估计 covars: 预处理协变量列表,禁止放入处理后变量 """ formula = f"{y_col} ~ {w_col} + " + " + ".join(covars) model = smf.ols(formula, data=df).fit(cov_type="HC2") return model.params[w_col], model.bse[w_col], model.conf_int().loc[w_col]参数说明:cov_type="HC2"是异方差稳健标准误,教材虽以同方差推导为主,但真实数据里结果变量方差常随协变量变化,用稳健版本更稳。协变量选择上有个血泪经验——千万别把处理后才产生的变量(比如“是否点击了活动页”)塞进去,那是典型的处理后偏倚,会把效应估计带偏甚至反号。教材在这一节末尾的文献注记里也提醒,回归调整的方差优势依赖于协变量对结果的解释力,如果协变量和结果几乎无关,加进去只会浪费自由度,小样本下反而让区间变宽。
2.3 从实验到观测:为什么第 2 章必须接着读
第 1 章的价值在于给你一个“干净基准”,但真实业务里能跑严格随机试验的场景是少数。教材从第 2 章开始转向无混淆假设(unconfoundedness)和倾向得分,逻辑上是把随机试验的“随机分配”替换成“给定协变量后处理分配与潜在结果独立”。这个替换不是免费的,它不可检验,只能靠领域知识论证。我的习惯是:每接一个观测数据项目,先写一段“为什么我认为无混淆成立”的备忘录,列出所有可能同时影响处理与结果的变量,能测的测、不能测的做敏感性分析。教材第 2 章的分层估计和逆倾向加权,本质上都是在无混淆成立的前提下,用倾向得分把观测数据“伪装”成随机试验。这一步没想清楚,后面双重稳健、异质效应全是白搭。
3. 倾向得分与双重稳健:把观测数据往实验上靠
3.1 逆倾向加权:权重截断是保命操作
第 2.2 节的逆倾向加权(IPW)估计器,思路是给每个样本乘上处理概率的倒数,让加权后的处理组和对照组在协变量分布上对齐。公式不复杂,但实操里最大的坑是倾向得分接近 0 或 1 时权重爆炸,一个样本能顶几百个样本,方差直接失控。教材在文献注记里提到截断与稳定化权重,我一般会在代码里强制加一个截断阈值,并在报告里写明截断比例。
from sklearn.linear_model import LogisticRegression def ipw_estimate(df, y_col, w_col, x_cols, clip=0.05): """逆倾向加权估计,clip 为倾向得分截断下界/上界 返回加权均值差与截断样本占比 """ X = df[x_cols].to_numpy() W = df[w_col].to_numpy() Y = df[y_col].to_numpy() ps = LogisticRegression(max_iter=2000).fit(X, W).predict_proba(X)[:, 1] ps_clipped = np.clip(ps, clip, 1 - clip) clipped_ratio = np.mean((ps < clip) | (ps > 1 - clip)) w1 = W / ps_clipped w0 = (1 - W) / (1 - ps_clipped) tau = (w1 * Y).sum() / w1.sum() - (w0 * Y).sum() / w0.sum() return {"tau_ipw": tau, "clipped_ratio": clipped_ratio}参数上,clip=0.05是我常用的起点,样本量大、协变量维度高时可以放宽到 0.02,但截断比例超过 5% 就要警惕——说明处理组和对照组在协变量上重叠太差,无混淆假设的可信度存疑。教材强调倾向得分模型是“设计阶段”的工具,不是拿来追求预测精度的,所以逻辑回归这种保守模型往往比梯度提升更合适,因为它不容易把得分推到极端。另一个常被忽略的点:IPW 的方差估计不能直接用加权样本的朴素方差,教材建议用影响函数或自助法,我一般用 200 次自助重采样拿区间,虽然慢但省心。
3.2 双重机器学习:正交化让模型误差不传染
第 3.1 节的双重机器学习(DML)是整本书里工程落地价值最高的部分之一。它的核心是“正交化”:用机器学习模型分别拟合结果对协变量的关系和倾向得分,然后用残差做回归,使得两个 nuisance 模型的估计误差在一阶上不影响处理效应的估计。教材给的直观解释是,普通插件法里倾向得分模型的误差会直接传进效应估计,而正交矩条件把这个一阶项消掉了。实操上我用 DoubleML 或手写交叉拟合,关键是样本分割要独立。
from sklearn.ensemble import GradientBoostingRegressor, GradientBoostingClassifier from sklearn.model_selection import KFold def dml_partialling_out(df, y_col, w_col, x_cols, n_splits=5): """部分线性 DML:交叉拟合残差后做最终回归 返回处理效应估计与标准误 """ X = df[x_cols].to_numpy() Y = df[y_col].to_numpy() W = df[w_col].to_numpy() res_y = np.zeros_like(Y, dtype=float) res_w = np.zeros_like(W, dtype=float) kf = KFold(n_splits=n_splits, shuffle=True, random_state=42) for train_idx, test_idx in kf.split(X): m_y = GradientBoostingRegressor(random_state=0).fit(X[train_idx], Y[train_idx]) m_w = GradientBoostingClassifier(random_state=0).fit(X[train_idx], W[train_idx]) res_y[test_idx] = Y[test_idx] - m_y.predict(X[test_idx]) res_w[test_idx] = W[test_idx] - m_w.predict_proba(X[test_idx])[:, 1] # 最终正交矩:res_y 对 res_w 回归 theta = (res_w * res_y).sum() / (res_w ** 2).sum() resid = res_y - theta * res_w se = np.sqrt((resid ** 2).mean() / (res_w ** 2).mean() / len(Y)) return {"theta": theta, "se": se}参数说明:n_splits=5是教材推荐的折数下限,折数太少 nuisance 模型欠拟合会破坏正交性,折数太多小样本下每折训练数据不足。random_state固定是为了可复现,但正式报告里我至少跑三个随机种子看稳定性。这里有个容易翻车的细节:结果模型和倾向模型必须用同一套交叉拟合划分,否则残差不对齐,正交性失效。教材第 3.2 节进一步讨论了无混淆下的有效估计,理论部分偏难,但落地结论很实用——DML 的置信区间在 nuisance 模型收敛速度足够快时是渐近有效的,如果协变量维度高、样本量小,区间会偏窄,这时候要退回到保守的方差估计或做敏感性分析。
3.3 双重稳健到底稳在哪:一个反直觉的边界
双重稳健估计器的卖点是“倾向模型或结果模型有一个对,估计就一致”。但教材在文献注记里点出一个容易被误读的边界:双重稳健性保证的是一致性,不保证方差最优,也不保证在有限样本里一定比 IPW 好。我见过团队因为“双重稳健”四个字就放心大胆用复杂模型拟合结果,结果倾向得分重叠差、权重截断狠,最终区间比简单回归调整还宽。我的做法是:DML 跑完必须和差分均值、回归调整、IPW 三个基准对齐,如果四个估计量方向一致、量级接近,结论才敢往外说;如果 DML 和 IPW 差出一个数量级,先查倾向得分分布,再查结果模型有没有把处理后变量混进去。教材第 3 章末尾的文献注记列了敏感性分析的方向,值得顺着读。
4. 异质效应与策略学习:从“平均有用”到“对谁有用”
4.1 半参数建模:CATE 不是分组回归
第 4 章讲异质处理效应,核心对象是条件平均处理效应(CATE)。很多人的第一反应是按某个协变量分组、每组跑一次差分均值,这在教材框架里是“半参数建模”的粗糙版本,问题在于分组一多样本就碎,组内方差大,而且分组边界是人为选的。教材给的半参数思路是把 CATE 建模成协变量的低维函数,再用类似 DML 的正交化方法估计。实操上我用因果森林或 R-learner,但前提是先把平均效应估准,否则异质性估计只是在噪声上雕花。
# 以 econml 的 R-learner 为例(需 pip install econml) from econml.dml import NonParamDML from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier def cate_rlearner(df, y_col, w_col, x_cols, t_cols): """R-learner 估计 CATE t_cols: 用于刻画异质性的协变量子集 """ est = NonParamDML( model_y=RandomForestRegressor(n_estimators=300, min_samples_leaf=20, random_state=0), model_t=RandomForestClassifier(n_estimators=300, min_samples_leaf=20, random_state=0), model_final=RandomForestRegressor(n_estimators=300, min_samples_leaf=20, random_state=0), discrete_treatment=True, ) est.fit(df[y_col], df[w_col], X=df[t_cols], W=df[x_cols]) return est参数说明:min_samples_leaf=20是控制过拟合的关键,叶子太小 CATE 曲线会剧烈抖动,业务方没法用;t_cols只放你真正相信会调节效应的变量,全量协变量塞进去等于没做异质性分析。教材第 4.2 节专门讨论了异质性的损失函数,核心思想是 CATE 的评估不能用预测精度,而要用“按估计的 CATE 排序后,高分组和低分组的实际效应差”,这个指标在策略落地时比 R² 有用得多。
4.2 策略学习:别把 CATE 符号直接当决策
第 5 章政策学习把异质效应推到决策层:给定估计的 CATE,怎么定一个分配规则使期望福利最大。教材第 5.2 节的实证福利最大化,结论是当 CATE 估计足够准且处理成本已知时,最优策略是“CATE 超过成本阈值就处理”。但这里有个我踩过的坑:CATE 估计的符号在接近零的区域极不稳定,直接按符号决策会把大量边缘样本分错。我的做法是设一个“不决策带”,CATE 置信区间跨零的样本不纳入策略,只对区间明显为正或明显为负的样本下规则。教材第 5.1 节的策略评估部分给了离线评估方法,用逆倾向加权评估新策略价值,实操里要注意新策略的支持集不能超出原数据的倾向得分重叠区,否则评估值不可信。
4.3 自适应实验:低遗憾不等于随便停
第 6 章自适应实验讲的是在数据收集过程中动态调整分配比例,把更多样本分给表现好的臂。教材第 6.1 节的低遗憾数据收集,直觉是“边学边分配”,但第 6.2 节紧接着提醒:自适应收集之后做推断,常规的固定样本置信区间不再有效,因为处理分配依赖历史结果。我见过团队用多臂老虎机跑完实验,直接拿最终各臂均值差做 t 检验,区间明显偏窄。正确做法是用教材提到的 anytime-valid 推断或对最终估计做去偏,实操成本不低,所以我的建议是:如果业务方只是要一个方向性结论,自适应实验够用;如果要写进正式决策的效应量,老老实实跑固定比例的随机试验,别为了省样本把推断搞复杂。
5. 避坑排查:五个让因果结论翻车的现场
5.1 现象:DML 估计和差分均值差出三倍,区间还很窄
原因通常是倾向得分模型把处理后变量当成了协变量,或者交叉拟合的折划分不一致导致残差不对齐。解决:先做协变量审计,把所有字段按“实验前可观测/实验后产生”分类,后者一律剔除;再检查结果模型和倾向模型是否共用同一组KFold划分,不一致就重写循环。区间异常窄往往伴随残差方差被低估,用自助法复核一遍。
5.2 现象:IPW 权重截断比例 15%,估计量剧烈波动
原因是处理组和对照组在协变量上重叠太差,倾向得分被推到极端。解决:先看是哪些协变量造成的,做分布对比;如果重叠差是结构性的(比如某类用户从来不会被分到处理组),说明无混淆假设在这个子群体上不成立,要么缩小研究人群,要么改用回归断点等局部设计。截断阈值不能为了“让结果好看”随意调大,截断比例要写进报告。
5.3 现象:CATE 排序在训练集上很漂亮,上线后策略收益为负
原因是异质性模型过拟合,或者 CATE 评估用了预测精度而非策略价值。解决:用样本外数据做策略评估,按估计 CATE 分位数分组看实际效应单调性;如果单调性不成立,减少t_cols维度或增大叶子样本量。上线前必须做小流量随机验证,别拿离线 CATE 排序直接全量。
5.4 现象:回归断点设计里断点两侧协变量不平衡
原因是断点附近样本太少,或者断点选择本身和处理分配有关。解决:先画断点附近协变量的分布图,做 McCrary 式的密度检查;局部线性回归的带宽不能拍脑袋定,教材第 8 章给了偏差感知的带宽选择思路,实操里至少跑三个带宽看估计稳定性。断点两侧样本量差异过大时,区间要用教材提到的有限样本方法,别直接套正态近似。
5.5 现象:有干扰的场景下,个体效应估计明显偏高
原因是 SUTVA 被违反,处理组和对照组之间存在溢出。解决:教材第 11、12 章给的暴露映射(exposure mapping)思路是把“邻居被处理的比例”编码成新变量,再用置换检验做推断。实操里先要能定义干扰结构(社交网络、地理相邻、同一库存池),定义不清楚就别硬套,否则暴露映射本身就是错的。
6. 回归断点与工具变量:局部设计的参数手感
第 8 章的回归断点设计是我认为全书最考验参数手感的一章。局部线性回归的核心是带宽:带宽太宽,断点远处的样本把非线性曲率带进来,估计有偏;带宽太窄,断点附近样本不够,方差爆炸。教材第 8.2 节的优化估计与偏差感知推断,思路是把偏差和方差放在一起权衡,而不是只看均方误差。我一般会先画断点附近 20 个分箱的均值散点图,肉眼确认有没有跳跃,再跑三个带宽(比如样本量的 0.1、0.2、0.3 分位对应的距离)看估计是否稳定,三个带宽下符号和量级一致才敢下结论。
import numpy as np def local_linear_rdd(x, y, cutoff, bandwidth): """局部线性回归断点估计 x: 驱动变量(running variable) y: 结果变量 cutoff: 断点位置 bandwidth: 带宽,仅使用 |x - cutoff| <= bandwidth 的样本 """ mask = np.abs(x - cutoff) <= bandwidth xc = x[mask] - cutoff # 中心化,避免共线 treat = (xc >= 0).astype(float) # 断点两侧各自线性,交互项允许斜率不同 X = np.column_stack([np.ones(mask.sum()), treat, xc, treat * xc]) beta, *_ = np.linalg.lstsq(X, y[mask], rcond=None) return beta[1] # treat 的系数即断点处的跳跃参数说明:bandwidth是唯一需要反复调的参数,cutoff由制度规则决定不能动。中心化xc = x - cutoff是为了数值稳定,否则x和treat * x高度共线。这个函数只给点估计,区间要用教材第 8.2 节的偏差修正方法或自助法,直接对beta[1]套普通 OLS 标准误会低估不确定性,因为带宽选择本身引入了随机性。第 9、10 章的工具变量与局部平均处理效应,落地场景相对窄,但教材把“依从性”讲得很透——随机分配了处理但有人不依从时,意向性分析估的是分配效应,工具变量估的是依从者的局部效应,这两个数在业务汇报里经常被混为一谈,写报告时务必标清楚估的是哪个群体。
第 13 章的事件研究设计(双重差分与合成控制)在业务里用得极多,教材第 13.1 节的双重差分前提是平行趋势,这个假设不可检验只能靠事前趋势图佐证;第 13.2 节的合成控制适合单个处理单元的场景,权重求解是约束优化,实操里要注意 donor pool 里不能混入受处理影响的单元。第 14、15 章的动态策略与马尔可夫决策过程偏研究前沿,工程落地还早,但第 15.2 节的 switchback 实验设计对平台类业务有直接参考价值——当处理会在一段时间内影响整个池子(比如定价策略),个体随机化会互相污染,按时间片轮换处理才是干净做法。
最后说个我自己的习惯:每读完一章,我会挑一个手头的历史项目,用那一章的方法重跑一遍,和当时的结论对照。这份 2024 年 9 月草稿版教材的好处是每章末尾都有文献注记和习题,习题不是摆设,第 16 章的练习里有一半是让你构造反例——证明某个看似合理的估计器在特定假设违反下会失效。这种“先证伪再应用”的训练,比背公式有用得多。从那以后我每次交因果报告前,都强制自己把 SUTVA、无混淆、重叠、平行趋势这几个假设逐条写成“成立/存疑/不成立”三态,存疑的必须附敏感性分析,不成立的直接换设计。希望这份拆解帮到你,少走几个我踩过的弯路。
本文还有配套的精品资源,点击获取