老周是社区诊所的家庭医生,他手里多了一种新药,但他很困惑:这药对所有人都管用吗?有的病人吃了精神抖擞,有的病人吃了毫无反应,还有的似乎更难受了。老周很想穿越到“病人没吃药”的平行宇宙,对比一下同一个人的两种结局——可惜现实只允许每个病人经历一种人生。于是老周召集了一支“百人医生天团”,让每位医生用自己的经验视角,把病人不断按特征切成越来越小的堆,再一小堆一小堆地评估药效。这个“以药效差异为导向、把人群递归切开再综合判断”的方法,就是今天的主角——Causal Forest 因果森林。 因果森林解决的核心问题叫“异质性处理效应”:同样是吃一种药、发一张券、做一个动作,不同的人收益天差地别。它告诉你的是“哪些人该吃、哪些人别吃”,而不是笼统地报一个平均疗效。本文会用老周诊所的故事贯穿始终,带你搞懂因果森林的原理、它与普通随机森林的区别,以及它在精准营销、医药评估中的落地姿势。

岗位关联

学完本节,你将掌握异质性处理效应估计与 uplift 建模的核心思路,能够胜任因果推断工程师、增长策略分析师等岗位中“该对谁施加干预”的决策任务。

目录
  1. 可视化演示
  2. 1. 反事实框架:看不见的平行宇宙
  3. 2. 随机森林基础:百人医生天团
  4. 3. 异质性分裂:让病人按“药效”分堆
  5. 4. 诚实估计:出题组和阅卷组分开
  6. 5. 森林推断:从投票到置信区间
  7. 6. 对比评估:不是所有森林都一样
  8. 7. 实践应用:从药房到营销战场
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

本节配有交互动效(进入视口后自动加载,约 3KB)。

因果树——递归分裂 叶节点:估计CATE=τ(x) 分裂:最大化效应方差 τ(x)=E[Y(1)-Y(0)|X=x] 不同子群处理效应不同

1. 反事实框架:看不见的平行宇宙

老周的病人张三住在 A 宇宙,吃了新药,一个月后血压降了 5 个单位。老周永远不知道,住在 B 宇宙的“张三”如果不吃药,血压会怎样——也许也会降 5 个单位,也许反而升。这两种可能的结局,统计学里叫“潜在结果”:$Y_i(1)$ 是“吃药宇宙”里的结果,$Y_i(0)$ 是“不吃药宇宙”里的结果。一个人的因果效应本该是这两个结果的差,可惜每个患者只能经历一个宇宙,真实的个体效应像薛定谔的猫一样永远无法被直接观测。

既然看不了单个个体,老周换个思路:把特征相近的人放进同一个小组,比如“35岁、有高血压史、平时不运动”的这一堆人,在组内比较“吃药的平均血压降幅”和“不吃药的平均血压降幅”。这个按特征分组的平均效应,就是条件平均处理效应(CATE)。它回答的不是“全人群平均有效”,而是“这一小撮人有效吗”。

老周终于明白:他不需要执着于偷看张三的另一个宇宙,只要把“长得像”的病人归成堆,堆内可比,就能给出“哪类人值得吃”的答案。这个“按特征分组、组内比差异”的思路,正是因果森林一切动作的起点。

τ(x)=E[ Yi(1)−Yi(0)  ∣  Xi=x ]\tau(x) = \mathbb{E}\bigl[\,Y_i(1) - Y_i(0) \;\big|\; X_i = x\,\bigr]
这个公式说的是:在特征取值为 $x$ 的一类人中,吃药与不吃药的期望结果差异。$\tau(x)$ 越大,这类人越该吃药。
  1. 潜在结局 $Y_i(1)$第 $i$ 个患者在“接受处理”的平行宇宙中的结果,例如吃了新药后的血压降幅。
    Yi(1)Y_i(1)
  2. 潜在结局 $Y_i(0)$同一个患者在“未接受处理”的平行宇宙中的结果,即不吃药时血压会怎么变。
    Yi(0)Y_i(0)
  3. 个体差值这两个平行宇宙结果的差,才是真正的因果效应,可惜永远只能看到其中一个。
    Yi(1)−Yi(0)Y_i(1) - Y_i(0)
  4. 条件期望在 $X_i = x$ 这样一个特征相同的子群里求平均,把不可观测的个体效应转化为可估计的群体平均效应。
    E[ Yi(1)−Yi(0)  ∣  Xi=x ]\mathbb{E}\bigl[\,Y_i(1) - Y_i(0) \;\big|\; X_i = x\,\bigr]

2. 随机森林基础:百人医生天团

光靠老周一个人看诊,难免有主观偏见。于是他召集了 100 位医生组成“天团”,每位医生都有自己独特的训练经历——这就是随机森林的雏形。要给 100 位医生各自攒“经验”,靠的是 Bootstrap 采样:老周手头有 1000 份病历,他让每位医生有放回地随机抽 1000 份,有的病历被抽了 3 次,有的病历一次也没出现。于是每个医生的“知识结构”都略有不同。

有了各自的病历,每位医生开始用“决策树”的方式看病:先看年龄是否大于 50 岁,再看血压是否偏高,再看有没有家族史……每一条判断都像在问一个“是/否”问题,把病人一层层分裂成更纯的子群。分裂时,医生希望每个子群内部的病情尽可能一致,也就是“不纯度”下降最大。如果只看一位医生,他的判断方差很大;但 100 位医生的平均判断就稳定多了。这就是“天团智慧”。

医生天团的“每位医生不同、合起来稳定”的特性,就是随机森林的根基。不过老周很快发现:普通医生天团擅长预测“病情会不会恶化”,但老周真正想知道的是“这药对谁有效”——预测病情和评估药效,根本不是一回事。

I(P)=1NP∑i∈P(Yi−YˉP)2I(\mathcal{P}) = \frac{1}{N_{\mathcal{P}}} \sum_{i \in \mathcal{P}} \bigl(Y_i - \bar{Y}_{\mathcal{P}}\bigr)^2
这是决策树分裂常用的不纯度度量(均方误差)。$Y_i$ 是患者 $i$ 的观测结果,$\bar{Y}_{\mathcal{P}}$ 是当前子群的平均结果。分裂目标就是让切分后左右子群的不纯度之和最小。
  1. 单个样本偏差每个患者的观测值减去所在子群的平均值,反映“这个患者有多偏离群体常态”。
    Yi−YˉPY_i - \bar{Y}_{\mathcal{P}}
  2. 平方聚合把所有偏差平方后相加,避免正负抵消,惩罚偏离大的患者。
    ∑i∈P(Yi−YˉP)2\sum_{i \in \mathcal{P}} \bigl(Y_i - \bar{Y}_{\mathcal{P}}\bigr)^2
  3. 平均不纯度除以子群人数 $N_{\mathcal{P}}$,得到该子群的平均混乱程度,数值越大代表子群内部越五花八门。
    1NP∑i∈P(Yi−YˉP)2\frac{1}{N_{\mathcal{P}}} \sum_{i \in \mathcal{P}} \bigl(Y_i - \bar{Y}_{\mathcal{P}}\bigr)^2

3. 异质性分裂:让病人按“药效”分堆

普通决策树看病,把“症状相似”的人分到一堆;因果森林则把“药效相似”的人分到一堆。老周的天团要评判新药,就不能按病情严重度分裂,而要按“吃药的收益差”分裂。同一刀切下去,左边这群人平均收益 +2,右边这群人平均收益 -1,这一刀就有价值——因为它分离出了“该吃药”和“不该吃药”的人。

这种分裂准则叫“效应差异分裂”:每次候选切分都计算左右子群的处理效应估计值 $\hat\tau_l$、$\hat\tau_r$,让它们与父节点的效应差异最大化。普通随机森林分裂时只看 $Y$ 的预测误差,因果森林分裂时盯着“处理效应”的差异,这是两者最本质的区别。此外,当处理组和对照组在特征上不平衡时,还可以用倾向得分加权来修正:给每个人一个“被分配到处理的概率”权重,让两组的特征分布更可比。

老周的天团从此不再按“血压高不高”切病人,而是按“吃了药有没有用”切病人。一刀一刀切下去,诊所的地图被切成了若干块“药效领地”:绿色领地是获益人群,红色领地是受害人群,灰色领地是无效人群——这正是异质性处理效应的可视化地图。

Δτ(s)=NlN(τ^l−τ^P)2+NrN(τ^r−τ^P)2\Delta_\tau(s) = \frac{N_l}{N}\bigl(\hat\tau_l - \hat\tau_{\mathcal{P}}\bigr)^2 + \frac{N_r}{N}\bigl(\hat\tau_r - \hat\tau_{\mathcal{P}}\bigr)^2
候选切分 $s$ 的得分 = 左右子群效应与父群效应偏离度的加权平均。得分越高,说明这一刀越能把“药效不同的人”分开。
  1. 父群效应估计$\hat\tau_{\mathcal{P}}$ 是切分前整个父节点里治疗组与对照组的结果均值之差,代表这一大堆人的平均药效。
    τ^P\hat\tau_{\mathcal{P}}
  2. 子群效应偏离左右子群各自的效应估计减去父群效应,得到每个子群对“平均药效”的偏离程度。
    τ^l−τ^P,τ^r−τ^P\hat\tau_l - \hat\tau_{\mathcal{P}}, \quad \hat\tau_r - \hat\tau_{\mathcal{P}}
  3. 加权聚合按左右子群样本占比 $N_l/N$ 和 $N_r/N$ 加权求和,样本多的子群对分裂得分的贡献更大。
    NlN(τ^l−τ^P)2+NrN(τ^r−τ^P)2\frac{N_l}{N}\bigl(\hat\tau_l - \hat\tau_{\mathcal{P}}\bigr)^2 + \frac{N_r}{N}\bigl(\hat\tau_r - \hat\tau_{\mathcal{P}}\bigr)^2

4. 诚实估计:出题组和阅卷组分开

老周发现一个陷阱:如果让医生们先偷偷看了一眼病人吃药后的表现,再决定把谁分到哪个子群,评估结果就会“自我实现”——数据被反复使用,估计出来的药效虚高,就像考试时出题人偷偷兼任了阅卷人。因果森林用“诚实估计”来解决这个问题:把样本一分为二,一半样本负责“找分裂线”(出题组),另一半样本负责“估药效”(阅卷组),两组互不见面。

诚实估计的核心是“样本分离”。树的生长结构只由分裂样本决定,叶节点里的处理效应只用估计样本计算。这样做的代价是用于分裂的数据变少,但换来的是估计的无偏性——就像高考命题组和阅卷组分开,分数才真实可信。在估计叶节点药效时,方差来源于治疗组和对照组两边的波动,两者都要算进去。

诚实估计让老周的天团学会了“避嫌”:定规则的人不参与打分,打分的人不参与出题。虽然每位医生手头能用的病历少了,但报出来的药效再也不会因为“自己出题自己考”而虚胖。

V(τ^(x))=σ12(x)n1(x)+σ02(x)n0(x)V\bigl(\hat\tau(x)\bigr) = \frac{\sigma_1^2(x)}{n_1(x)} + \frac{\sigma_0^2(x)}{n_0(x)}
处理效应估计的方差 = 治疗组结果方差除以治疗组人数 + 对照组结果方差除以对照组人数。样本越少、组内波动越大,估计越不可信。
  1. 治疗组波动项$\sigma_1^2(x)$ 是子群 $x$ 中吃药者的结果方差,除以吃药者人数 $n_1(x)$,衡量治疗组均值估计的不确定性。
    σ12(x)n1(x)\frac{\sigma_1^2(x)}{n_1(x)}
  2. 对照组波动项$\sigma_0^2(x)$ 是子群中未吃药者的结果方差,除以未吃药者人数 $n_0(x)$,衡量对照组均值估计的不确定性。
    σ02(x)n0(x)\frac{\sigma_0^2(x)}{n_0(x)}
  3. 总和为效应方差两个独立样本的均值之差,方差就是两方差之和;误差棒越宽,说明这个子群的药效越说不准。
    V(τ^(x))V\bigl(\hat\tau(x)\bigr)

5. 森林推断:从投票到置信区间

100 位医生各自给出判断后,老周怎么汇总?最简单的做法是投票取平均:张三落在树 1 的叶子里,树 1 说药效 +0.4;落在树 2 的叶子里,树 2 说药效 +0.6……最后把所有树的结果一平均,就是因果森林对张三的最终估计。这就是“聚合策略”:每棵树在它对应的叶子节点上给出一个处理效应估计,森林再做等权平均。

但老周还想知道:这个平均结果靠不靠谱?如果 100 位医生给出的结果都集中在 +0.5 附近,说明共识强,置信区间窄;如果从 -0.3 到 +1.2 都有,说明分歧大,区间宽。森林输出的点估计配上置信区间,就完成了“显著性检验”:区间不包含 0,才敢拍板说“这类人真的有效”。

天团的会诊有了正式流程:先听各位医生分别发言,再算平均结论,最后看分歧大小决定敢不敢下处方。置信区间窄的区域,老周敢直接建议用药;区间宽的区域,他就建议再做一轮观察——这种“既给结论又给信心度”的方式,正是因果森林在决策场景中比单一模型更受青睐的原因。

τ^(x)=1B∑b=1Bτ^b(x)\hat\tau(x) = \frac{1}{B} \sum_{b=1}^{B} \hat\tau_b(x)
森林对特征 $x$ 的最终处理效应估计,等于 $B$ 棵树各自叶节点估计的平均值。$B$ 越大,平均越稳定。
  1. 单棵树的估计$\hat\tau_b(x)$ 是第 $b$ 棵树中,特征 $x$ 落到叶子后该叶子的处理效应估计值(诚实样本计算)。
    τ^b(x)\hat\tau_b(x)
  2. 求和把 $B$ 棵树的估计全部加总,让每一棵树的“个性”都被纳入。
    ∑b=1Bτ^b(x)\sum_{b=1}^{B} \hat\tau_b(x)
  3. 取平均除以树的棵数 $B$,得到森林共识;平均同时削减了单棵树的随机波动。
    1B∑b=1Bτ^b(x)\frac{1}{B} \sum_{b=1}^{B} \hat\tau_b(x)

6. 对比评估:不是所有森林都一样

老周之前也用过普通随机森林预测病情,现在又用因果森林评估药效,两者到底什么关系?打个比方:普通随机森林是“体检报告”,告诉你“这个人明年得高血压的概率是 70%”;因果森林是“用药处方”,告诉你“这个人吃降压药能降多少血压”。前者回答“会怎样”,后者回答“干预后比不干预强多少”。目标不同,分裂准则自然不同。

普通随机森林分裂时让子群内 $Y$ 的均方误差最小;因果森林分裂时让子群间的处理效应差异最大。忽略这个区别,直接拿预测模型的特征重要性去谈“谁有效”,就会犯“把相关性当因果性”的大错。从偏差-方差角度看,因果森林同样面临权衡:树太深,单棵树拟合过度、方差大;树太浅,偏差大、找不到细粒度异质性。森林通过平均来压方差,通过诚实估计来控制偏差。

老周把“体检报告”和“用药处方”分开放进两个抽屉,从此不再混用。想要预测结局,用普通随机森林;想要判断干预效果,用因果森林。选对工具,比优化工具本身更重要。

MSE=Bias2+Variance\mathrm{MSE} = \mathrm{Bias}^2 + \mathrm{Variance}
估计误差的总期望由两部分组成:偏差(系统性打偏)和方差(随机抖动)。打靶时,枪准但手抖和手稳但枪歪,都不是好射手。
  1. 偏差模型系统性地偏离真相的程度,例如把负效应子群误判为正效应。
    Bias2\mathrm{Bias}^2
  2. 方差不同训练样本下模型估计的波动幅度,树越深波动越大。
    Variance\mathrm{Variance}
  3. 总误差两者相加才是真正的估计质量;只看其一,都会误判模型的优劣。
    MSE=Bias2+Variance\mathrm{MSE} = \mathrm{Bias}^2 + \mathrm{Variance}

7. 实践应用:从药房到营销战场

老周的经验被一家电商公司看中了:他们想给用户发优惠券,但不知道发给谁。这不就是“评估药效”吗?发优惠券是处理 $T=1$,不发是 $T=0$,目标是“购买金额” $Y$。全量发券浪费预算,从不出券又错过增量。因果森林刚好能回答:哪些用户“发券才买”,哪些用户“不发也买”。

这就是 uplift 建模:建模目标是增量效应 $U(x)=P(购买|发券)-P(购买|不发券)$。它把人群划分成四类:有券才买(忠实目标)、无券也买(浪费预算)、折价才买(敏感人群)、发券不买(应放弃)。因果森林对每人算出一个增量分,运营团队只对增量分为正的人发券,ROI 立刻提升。同样的思路还用于医药试验中的亚组分析——识别哪些患者真正从新药中获益。

从社区诊所到电商后台,老周的药理逻辑摇身一变成了增长逻辑:识别“谁吃这一套”,把资源投给最敏感的人。因果森林这把手术刀,切开的不是人群,而是“决策的增量价值”。

U(x)=E[ Y∣X=x,  T=1 ]−E[ Y∣X=x,  T=0 ]U(x) = \mathbb{E}[\,Y \mid X=x,\; T=1\,] - \mathbb{E}[\,Y \mid X=x,\; T=0\,]
uplift 就是在特征 $x$ 下,处理组期望结果减去对照组期望结果。这正是因果森林估计的 $\hat\tau(x)$ 在不同业务里的名字。
  1. 处理组期望$\mathbb{E}[Y \mid X=x, T=1]$ 是特征为 $x$ 的人群中,发了券之后的平均购买金额。
    E[Y∣X=x,  T=1]\mathbb{E}[Y \mid X=x,\; T=1]
  2. 对照组期望$\mathbb{E}[Y \mid X=x, T=0]$ 是同样特征的人群中,不发券的平均购买金额。
    E[Y∣X=x,  T=0]\mathbb{E}[Y \mid X=x,\; T=0]
  3. 增量之差两者相减,剥离出“券本身带来的新增购买”,而非购买人群原有的消费能力。
    U(x)U(x)

AI 应用场景

电商优惠券智能分发

阶段:个性化营销策略制定

解决的问题:传统做法按用户历史消费额(RFM 模型)发券,把券发给了“不发也买”的高价值用户,白花预算;又漏掉了“发券才买”的价格敏感用户。因果森林对每个用户估计“发券带来的购买增量”,把预算集中到增量最高的人群,ROI 平均提升 30% 以上。

具体案例:某生鲜电商对 50 万用户做 7 折券投放实验。先用因果森林在历史数据上训练 uplift 模型,按预测增量从高到低排序,对前 20% 用户发券。对比全量发券,活动销售额不变,券成本下降 60%。

临床试验亚组获益识别

阶段:临床数据的事后异质性分析

解决的问题:新药三期试验平均效应不显著,但可能某个亚组显著获益。传统分层分析只能按一个维度(如年龄或性别)手工分组,无法发现多特征交叉的复杂亚组。因果森林自动在所有特征组合中搜索“效应差异大”的子群,为精准医疗提供证据。

具体案例:某降糖药在全体受试者中效果平平,因果森林却发现“BMI>28 且病程<3 年”的亚组糖化血红蛋白降幅远超对照组,为该药申请适应症扩展提供了关键支持。

Python 演示

python87 行
import numpy as np def generate_data(n=800, seed=42):    rng = np.random.RandomState(seed)    age = rng.uniform(18, 70, n)    income = rng.uniform(3, 50, n)    X = np.column_stack([age, income])    T = rng.binomial(1, 0.5, n)    # 真实异质性: 年轻人正效应, 老年人负效应, 中年人弱正    tau = np.where(age < 30, 2.0, np.where(age > 55, -1.5, 0.8))    Y = 1.0 + 0.1 * income + tau * T + rng.normal(0, 0.5, n)    return X, T, Y, tau def tau_est(Ys, Ts):    if (Ts == 1).sum() == 0 or (Ts == 0).sum() == 0:        return 0.0    return Ys[Ts == 1].mean() - Ys[Ts == 0].mean() def split_node(X, T, Y, idx):    best = None    for feat in range(X.shape[1]):        vals = np.unique(X[idx, feat])        for thr in (vals[:-1] + vals[1:]) / 2:            mask = X[idx, feat] <= thr            l, r = idx[mask], idx[~mask]            if len(l) < 5 or len(r) < 5:                continue            tau_p = tau_est(Y[idx], T[idx])            tau_l = tau_est(Y[l], T[l])            tau_r = tau_est(Y[r], T[r])            n = len(idx)            gain = len(l) / n * (tau_l - tau_p) ** 2 + len(r) / n * (tau_r - tau_p) ** 2            if best is None or gain > best[0]:                best = (gain, feat, thr, l, r)    return best def grow_tree(X, T, Y, idx, depth=0, max_depth=4, min_size=20):    node = {"left": None, "right": None, "feat": None, "thr": None}    if depth >= max_depth or len(idx) < min_size:        return node    res = split_node(X, T, Y, idx)    if res is None or res[0] < 1e-4:        return node    _, feat, thr, l, r = res    node["feat"], node["thr"] = feat, thr    node["left"] = grow_tree(X, T, Y, l, depth + 1, max_depth, min_size)    node["right"] = grow_tree(X, T, Y, r, depth + 1, max_depth, min_size)    return node def leaf_tau(node, X, T, Y, x, est_idx):    if node["left"] is None:        return tau_est(Y[est_idx], T[est_idx])    if x[node["feat"]] <= node["thr"]:        sub = [i for i in est_idx if X[i, node["feat"]] <= node["thr"]]        return leaf_tau(node["left"], X, T, Y, x, sub)    sub = [i for i in est_idx if X[i, node["feat"]] > node["thr"]]    return leaf_tau(node["right"], X, T, Y, x, sub) def causal_forest(X, T, Y, B=30, seed=1):    rng = np.random.RandomState(seed)    trees = []    for _ in range(B):        idx = rng.choice(len(X), len(X), replace=True)        half = len(idx) // 2        tree = grow_tree(X, T, Y, idx[:half])  # 分裂样本        trees.append((tree, idx[half:]))       # 估计样本    def predict(x):        preds = [leaf_tau(t, X, T, Y, x, est) for t, est in trees]        return np.mean(preds)    return predict def main():    X, T, Y, tau_true = generate_data()    model = causal_forest(X, T, Y, B=30, seed=1)    groups = [        ("18-30岁", X[:, 0] < 30),        ("30-55岁", (X[:, 0] >= 30) & (X[:, 0] <= 55)),        ("55-70岁", X[:, 0] > 55)    ]    for name, mask in groups:        s = np.where(mask)[0][:50]        pred = np.mean([model(X[i]) for i in s])        true = tau_true[s].mean()        print("%s 预测CATE=%.3f, 真实CATE=%.3f" % (name, pred, true)) if __name__ == "__main__":    main()
目的:用 30 棵诚实因果森林估计三个年龄子群的 CATE,并与模拟数据的真实效应对比,展示异质性处理效应的识别能力。
  1. tau = np.where(age < 30, 2.0, np.where(age > 55, -1.5, 0.8))生成数据时埋入真实异质性:年轻人正效应、老年人负效应。
  2. Ys[Ts == 1].mean() - Ys[Ts == 0].mean()叶子内治疗组与对照组结果均值相减,就是实证版 CATE。
  3. gain = len(l) / n * (tau_l - tau_p) ** 2 + len(r) / n * (tau_r - tau_p) ** 2效应差异分裂准则:子群效应偏离父群效应越大,得分越高。
  4. idx = rng.choice(len(X), len(X), replace=True)Bootstrap 有放回采样,让每棵树的“经验”不同。
  5. tree = grow_tree(X, T, Y, idx[:half])诚实估计:只用前一半样本构建树结构。
  6. trees.append((tree, idx[half:]))后一半样本单独保存,供叶节点估计处理效应。
  7. sub = [i for i in est_idx if X[i, node["feat"]] <= node["thr"]]估计样本沿分裂规则流向对应的叶子。
  8. return np.mean(preds)森林聚合:对多棵树的叶节点估计取平均,得到稳定结论。

常见误区

误区我已经会随机森林了,因果森林不就是换个数据集跑一下吗?把处理组和对照组一起丢进随机森林,输出的就是因果效应了吧?

正解普通随机森林优化的是“预测 $Y$ 的误差”,因果森林优化的是“处理效应 $\tau(x)$ 的组间差异”。训练普通随机森林时,模型根本不知道“谁是处理组谁是对照组”,它只会找与 $Y$ 相关的模式,很可能把“本来就爱买的人”当成“发券才买的人”。这就像把体检报告当用药处方——报告说“血压高”,不代表“降压药一定对这个人有效”。

为何常见:两者长得太像了:都是树、都有 Bootstrap、都画分叉图。外表相似让人天然以为只改参数就能切换,忽视了分裂目标这个最本质的差异。

误区想算不同人群的药效,把年龄切成几段,每段里算一下吃药和不吃药的均值差,不就是 CATE 吗?何必用因果森林?

正解手工分箱有几个致命问题:一是切分边界怎么定?29 岁和 30 岁真的该被分到不同箱子里吗?二是特征一多就爆炸,年龄、收入、病史三个特征交叉起来,手工根本切不过来;三是单次切分的结果极不稳定,换一批数据边界就漂移。因果森林在全体特征上自动搜索最优分裂,并用多树平均压制方差,得到的是稳定、可推断的 CATE。

为何常见:刚接触异质性效应时,最容易想到的直觉就是“分组求均值差”。但这种直觉忽略了高维特征交互和统计稳定性,属于用简单工具对付复杂问题。

误区把样本分一半去分裂、一半来估计,白白浪费数据!直接用全部样本既建树又算效应,不是估计得更准吗?

正解用同一批数据既设计分裂又评估效应,相当于“自己出题自己考”,估计值会系统性偏向乐观——这叫过拟合。诚实估计虽然让每棵树手头样本变少、方差略增,但消除了偏差这个更难察觉的敌人。偏差和方差都会贡献 MSE,偏差大意味着你的结论系统性错误,比波动大更危险。真实项目中,诚实估计的覆盖率表现远优于非诚实版本。

为何常见:人们天然觉得“用到的数据越多越好”,却忽略了“同一批数据被用了两次”带来的信息泄露。这和考试前拿到真题的虚假高分是同一个道理。

误区我用因果森林算出两个子群的处理效应不同,一个是 +2,一个是 -1,这总能说明药物对不同人效果不同了吧?

正解当数据来自随机实验时,这个结论基本成立;但如果是观测数据,就要警惕混杂因素——比如收入高的人本来就更容易买药,导致治疗组结果虚高。因果森林需要配合可忽略性假设、倾向得分加权等工具来缓解选择偏差。效应差异不等于因果差异,除非你先说服自己和别人:处理组和对照组除了处理之外,真的没有系统性不同。

为何常见:因果推断的反事实思维反直觉,人脑天然会把“观察到的相关”等同于“背后的因果”。尤其在特征可视化呈现明显分组差异时,这种错觉格外强烈。

面试题

Q1. 关于异质性处理效应(CATE),下列说法正确的是:

单选题 · easy

  • A.全体人群只有一个固定的平均效应
  • B.每个个体的真实因果效应可以直接观测
  • C.不同特征子群的处理效应存在差异← 正确答案
  • D.CATE 与条件期望没有任何关系

答案:C。CATE 是在特征 $X=x$ 的子群里求平均处理效应,不同子群效应不同,这正是“异质性”的含义。A 把 CATE 降级为整体 ATE;B 忽略了反事实——一个人只能看到一个宇宙的结局;D 与 CATE 的定义直接矛盾。

Q2. 因果森林与普通随机森林最核心的区别是:

单选题 · mid

  • A.分裂时优化子群间的处理效应差异← 正确答案
  • B.使用更多的决策树提升预测精度
  • C.要求所有树必须限制为相同的深度
  • D.只能处理连续型的结果变量

答案:A。因果森林的分裂准则是“效应差异最大化”,普通随机森林分裂时优化的是 $Y$ 的预测误差(不纯度下降)。B 只是森林的通用做法,C、D 都不是因果森林的本质特征。

Q3. 诚实估计(honest estimation)的核心思想是:

单选题 · easy

  • A.用全部样本同时进行分裂和效应估计
  • B.通过增加树的深度来降低估计偏差
  • C.剔除数据中的异常值提升稳健性
  • D.拆分样本,分裂与估计使用不同数据← 正确答案

答案:D。诚实估计模仿“出题与阅卷分离”:分裂样本决定树结构,估计样本计算叶节点效应,避免同一批数据被重复使用导致的乐观偏差。A 是典型的非诚实做法;B 加深树只会增加方差;C 与诚实估计无关。

Q4. 因果森林在节点分裂时,选择切分的标准是:

单选题 · mid

  • A.让左右子群的样本量尽量均衡
  • B.让子群内部的预测误差达到最小
  • C.让子群之间的处理效应差异最大← 正确答案
  • D.让子群的特征取值分布完全相同

答案:C。因果森林每次分裂都寻找让左右子群 $\hat\tau$ 偏离父节点最大的切分点,从而把“药效不同的人”分开。A 是工程上的辅助约束而非目标;B 是普通随机森林的目标;D 不是分裂准则。

Q5. 关于反事实框架,下列说法正确的有:

多选题 · mid

  • A.个体的因果效应无法被同时观测← 正确答案
  • B.每个个体实际只经历一种潜在结局← 正确答案
  • C.简单平均值会掩盖子群间的效应差异← 正确答案
  • D.因果推断不需要考虑处理分配机制

答案:ABC。反事实框架下,一个人要么吃药要么不吃,$Y_i(1)$ 和 $Y_i(0)$ 只有一个能被看到,所以 A、B 正确;平均效应把 +2 和 -1 抵消成 +0.5,掩盖了异质性,C 正确。D 错误:处理分配机制(随机 or 混杂)直接决定因果效应能否被识别。

Q6. 为什么诚实估计能降低因果森林的偏差?

单选题 · hard

  • A.评估效应用的样本与分裂结构相互独立← 正确答案
  • B.训练样本的总量得到了成倍的扩充
  • C.对照组中的随机噪声被算法完全消除
  • D.分裂过程不再使用任何患者特征

答案:A。诚实估计让“定分裂规则的人”和“算药效的人”互不接触,评估样本从未参与树结构设计,因此估计值不会因结构过拟合而虚高。B 错误,样本没有变多;C 错误,噪声无法完全消除;D 错误,分裂当然要用特征。

Q7. 因果森林识别异质性处理效应依赖的关键假设包括:

多选题 · hard

  • A.无未观测混杂(可忽略性假设)← 正确答案
  • B.每个特征组合下处理组与对照组都有样本← 正确答案
  • C.个体之间满足稳定单元取值假设← 正确答案
  • D.所有个体的处理效应必须完全相同

答案:ABC。可忽略性保证观测到的效应差异来自处理而非隐藏因素;正支持保证每个子群都能同时算出两组的均值;稳定单元假设保证“你的结果不受别人是否吃药影响”。D 与异质性前提矛盾——如果效应都相同,就无需因果森林了。

Q8. 关于 CATE 估计的方差来源,下列说法正确的是:

单选题 · mid

  • A.方差只来自治疗组内部的结果波动
  • B.方差等于治疗组与对照组两组波动之和← 正确答案
  • C.方差与子群样本量完全无关
  • D.只要分组足够细方差一定降为零

答案:B。$V(\hat\tau)= \sigma_1^2/n_1 + \sigma_0^2/n_0$,两组各自的均值不确定性都要计入。A 漏掉了对照组;C 错误,样本量越大方差越小;D 错误,分组过细会让 $n$ 变小,方差反而膨胀。

Q9. uplift 建模在业务中的典型应用有:

多选题 · easy

  • A.调整广告点击率预估模型的超参数
  • B.筛选对优惠券敏感的投放人群← 正确答案
  • C.制定会员流失挽回的策略优先级← 正确答案
  • D.对用户进行无监督聚类分组展示

答案:BC。uplift 回答“干预带来的增量”,优惠券人群筛选(B)和流失挽回(C)都依赖“发券/打电话是否真的改变了行为”。A 是预测任务而非处理效应;D 是无监督分群,不涉及因果增量。

Q10. 当业务目标为“找出谁吃药获益最大”时,正确的建模思路是:

单选题 · extreme

  • A.训练一个高精度的预测模型估计发病风险
  • B.随机分两组后直接计算全体平均疗效
  • C.汇总整体人群的治疗收益并对外汇报
  • D.构建因果森林估计每个个体的处理增益← 正确答案

答案:D。“谁获益最大”是异质性处理效应问题,需要输出个体化的 $\hat\tau(x)$,因果森林正是为此设计。A 预测的是 $Y$ 本身,不是效应;B、C 都在算整体平均 ATE,掩盖了谁受益谁受害的关键信息,无法指导个体决策。