🏠 总目录📚 本教程 13 · 不能做 A/B 时
📑 本页目录(点开跳转)

13 · 不能做 A/B 时

50 分钟 | ⭐⭐ 从观察数据里得出可信的因果结论


🎯 一句话

很多时候你不能做实验 —— 不能只给一半用户涨价、不能随机让一半人得病、 政策已经全量上线了。 这一章讲:这种情况下,怎么还能说出「是它造成的」。


🚫 一、什么时候做不了 A/B

情况 例子
伦理 / 合规不允许 不能随机给一半用户更差的风控
已经全量上线了 事后才想评估效果
无法随机分配 政策、外部环境、竞品动作
样本量根本不够 To B 场景只有 50 个客户
网络效应太强 分流本身就会互相污染(第 12 章

💀 二、为什么不能直接比

   「用了新功能的用户」 vs 「没用的用户」
   → 前者留存率高 30%
   → 结论:新功能提升留存 30%?❌❌

   真相:
   本来就活跃的用户【更可能去用新功能】
   → 你比较的是「活跃的人」和「不活跃的人」
   → 这叫【选择偏差】(selection bias)

🔑 核心问题:观察数据里,「是否接受处理」不是随机的,它由某些因素决定 —— 而那些因素同时也影响结果。这类因素叫混杂变量(confounder)

        混杂变量(活跃度)
         ╱            ╲
        ↓              ↓
   用不用新功能  ──?──→  留存

   ⭐ 你看到的相关,可能全部来自那条绕行的路径

🧰 三、四把工具

① 倾向得分匹配(PSM)—— 最常用 ⭐

干预时点之前之后实验组对照组← 效应上线前必须平行实验组实际走的路反事实:没有干预本来会走的路DID 效应 = 实际 − 反事实 —— 用对照组的变化,扣掉了大盘趋势
DID 的关键不是「前后比」,而是用对照组的变化扣掉大盘趋势。⚠️ 它成立的前提是干预前两条线必须平行 —— 这一步必须画图验证,不能假设。
   思路:既然不能随机分,那就【人为配对】

   ① 用协变量(年龄、活跃度、历史行为…)建模,
      预测「这个人有多大概率会用新功能」→ 这个概率叫【倾向得分】
   ② 给每个处理组用户,找一个倾向得分【几乎相同】的对照组用户
   ③ 比较这些配对的结果差异

   ⭐ 直觉:把「本来就长得很像、但一个用了一个没用」的人配成对
import torch.nn as nn
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import NearestNeighbors

def psm(df, treat_col, covariates, caliper=0.05):
    ps = LogisticRegression(max_iter=1000).fit(
        df[covariates], df[treat_col]).predict_proba(df[covariates])[:, 1]
    df = df.assign(_ps=ps)
    t, c = df[df[treat_col] == 1], df[df[treat_col] == 0]
    nn = NearestNeighbors(n_neighbors=1).fit(c[["_ps"]])
    dist, idx = nn.kneighbors(t[["_ps"]])
    keep = dist.ravel() < caliper          # ⭐ 卡钳:差太多的不配对,宁可丢掉
    return t[keep], c.iloc[idx.ravel()[keep]]

⚠️ PSM 的三个必做检查

检查 为什么
共同支撑区间 两组倾向得分的分布要有重叠。没重叠的部分无法比较,必须丢掉
配对后协变量均衡 配完要验证:两组的每个协变量分布是否接近(标准化均值差 < 0.1)
敏感性分析 假设存在一个未观测混杂,它要多强才能推翻结论 ⭐

第二项检查有标准做法,十行代码

import numpy as np, pandas as pd

def smd(t, c):
    """标准化均值差:|均值差| / 合并标准差。⭐ 判据是 0.1,与样本量无关"""
    return abs(t.mean() - c.mean()) / np.sqrt((t.var() + c.var()) / 2 + 1e-12)

def balance_report(t_before, c_before, t_after, c_after, covariates):
    rows = []
    for v in covariates:
        rows.append({"协变量": v,
                     "配对前SMD": smd(t_before[v], c_before[v]),
                     "配对后SMD": smd(t_after[v],  c_after[v])})
    r = pd.DataFrame(rows)
    r["结论"] = np.where(r["配对后SMD"] < 0.1, "✅ 均衡", "🔴 没配平")
    return r.sort_values("配对后SMD", ascending=False)      # ⭐ 最差的排最前

一定要同时报「配对前」和「配对后」两列: 只看配对后,你看不出 PSM 到底起没起作用。 健康的结果长这样:配对前有几个 SMD 是 0.4–0.8,配对后全部掉到 0.05 以下。 如果某个协变量配对后仍然 > 0.1,说明共同支撑不足 —— 要么缩小卡钳、要么承认这部分人群不可比。

⚠️ 不要用 p 值判断均衡:样本量一大,任何微小差异都会"显著"; 样本量一小,再大的差异也"不显著"SMD 不受样本量影响,这就是它被选中的原因。

💀 PSM 最大的局限它只能平衡你观测到的变量。 没进模型的混杂变量,它一点办法都没有 —— 这和 A/B 的随机化有本质差别(随机化连你不知道的都能平衡)。

💀 一个真实案例:PSM 说 +12 分,全量之后掉了 9 分

   问题:智能客服 vs 人工客服,哪个满意度高?
   (不能随机分配 —— 用户自己选择转不转人工)

   PSM 做得很规范:
   · 协变量:问题类型(8 类)、用户等级、历史工单数、时段、渠道
   · 共同支撑区间重叠良好
   · 配对后所有协变量 SMD < 0.06         ✅ 教科书级别的均衡
   · 结论:智能客服满意度 +12 分,置信区间不含 0

决策:半年内把人工客服缩编,智能客服承接主要流量。 结果:整体满意度下降 9 分,投诉量上升。

根因:一个没进模型、也进不了模型的混杂变量 ——

   ⭐ 【问题的真实复杂度】

   简单问题 → 用户自己就选了智能客服(因为更快)
   复杂问题 → 用户一开始就点"转人工"

   而「问题类型」只有 8 个粗类,
   同一个类里既有"忘了密码"也有"跨境退款纠纷"  💀

   → PSM 配平的是【表面特征】,
     没配平的是【用户自己知道、而你的数据里没有的那件事】

🔑 这个案例的普适教训当「是否接受处理」由用户自己选择时,未观测混杂几乎必然存在 —— 因为用户在做这个选择时,用到的信息比你的数据多

敏感性分析本来能预警:事后算出来, 只要存在一个能让人接受处理的几率变成 1.15 倍的未观测因素,结论就翻了 —— 这是一个极弱的假设。团队跳过了这一步。

② 双重差分(DID)⭐⭐ —— 有前后数据时的首选

   适用:某个变化在【某些组】发生了,另一些组没有,且你有【前后】数据

   例:新推荐算法先在华东上线,华北还没上

              上线前   上线后    差值
   华东(实验)   100      130      +30
   华北(对照)   100      110      +10
                                  ────
   DID 估计 = +30 − +10 = +20   ⭐

   ⭐ 妙处:用对照组的变化,扣掉了【大盘趋势】
      (那 +10 是不管有没有新算法都会涨的部分)

🔑 DID 的关键假设:平行趋势 —— 如果没有这个变化,两组本来会以相同的趋势变化。

必须验证:画出上线前多期的两条曲线,看它们是不是平行的。 如果上线前就已经在分叉,DID 的结论不成立。这一步不能省。

DID 其实就是一个带交互项的回归(这样才能拿到置信区间):

import statsmodels.formula.api as smf

# df 每行 = 一个「地区 × 周」的观测
#   treated: 该地区是否属于实验组(0/1)    post: 该周是否在上线之后(0/1)
m = smf.ols("y ~ treated + post + treated:post", data=df).fit(
        cov_type="cluster", cov_kwds={"groups": df["region"]})   # ⭐⭐ 必须按地区聚类

print(m.params["treated:post"])          # ⭐ 交互项系数 = DID 估计值
print(m.conf_int().loc["treated:post"])  # ⭐ 置信区间,别只报点估计

💀 cov_type="cluster" 这一行是最容易漏、代价最大的一行: 不聚类时,statsmodels 会把每个用户当成一个独立样本, 但你实际上只有 12 个地区这么多独立单元。 后果是标准误被低估到真实值的 1/3 ~ 1/5,p=0.001 实际可能是 p=0.3。判断方法很简单:处理是在哪个层级分配的,就按哪个层级聚类。

平行趋势不要用眼睛看,用「事件研究」跑一遍

df["rel"] = df["week"] - df["launch_week"]     # 相对周:0 = 上线那一周
m2 = smf.ols("y ~ C(rel, Treatment(-1)):treated + C(region) + C(week)",
             data=df).fit(cov_type="cluster", cov_kwds={"groups": df["region"]})

# ⭐ 看 rel = -6 … -2 这些【上线之前】的交互项系数:
#    全都接近 0 且不显著 → 平行趋势成立,DID 可用
#    有任何一个显著     → 上线前两组就在分叉,DID 结论不成立 🔴

至少要看 4–6 个前期。只画两期是没有意义的 —— 任意两个点永远可以连成两条"平行"的线。

③ 断点回归(RDD)—— 有明确阈值时

   适用:处理由一个【阈值】决定

   例:信用分 ≥ 600 才能借款
   → 比较 599 分和 601 分的人  ⭐
   → 这两群人几乎一模一样,唯一差别就是"能不能借"
   → 差异就是因果效应

   ⚠️ 结论只对【阈值附近】成立,不能外推到 300 分或 900 分的人

💡 一个必查的前提阈值不能被操纵。 如果人们能把自己的分数从 599 调到 601,那两侧的人就不再可比了 (做法:画分数的分布直方图,看阈值处有没有异常堆积)。

④ 工具变量(IV)—— 最强也最难

   找一个变量 Z,它满足:
   ① 影响「是否接受处理」        (相关性)
   ② 【只能】通过处理影响结果     (排他性)⭐⭐

   经典例子:研究"上大学对收入的影响"
   Z = 家离大学的距离
   · 离得近 → 更可能上大学        ✅ ①
   · 距离本身不直接影响收入        ← 这条【无法验证】,只能论证 ⚠️

⚠️ IV 的排他性假设无法用数据检验,只能靠领域知识论证。 它是这四把工具里最强的,也是最容易被质疑的。

⑤ 合成控制 —— 只有一个处理单元时 ⭐

   适用:只有【一个】地区/客户/城市上线了,
        找不到任何单一的对照能和它相像

   做法:用【其余多个地区的加权组合】拼出一个"假的华东"
        权重的确定标准:让它在【上线前】的历史曲线拟合得最好  ⭐

   上线后:真实华东 − 合成华东 = 效应

   ⚠️ 三个前提:
   · 上线前要有足够长的历史(一般 ≥ 10 期)
   · 对照池里的地区【自己没有也受到影响】
   · 权重非负且和为 1(不许外推,只许内插)⭐

💡 为什么限制「非负、和为 1」: 允许负权重时,你几乎总能拟合出完美的上线前曲线 —— 但那是过拟合,不是可比。 这个约束是合成控制方法的核心,别自己去掉它。

推断怎么做:把「假上线」轮流套到每个对照地区上跑一遍 (安慰剂检验)。如果真实处理地区的效应不比大多数安慰剂大,那就是噪声。


🧭 四、怎么选(决策表)

你有什么 用什么
能随机分配 A/B(永远首选)⭐
有处理前后的数据 + 有对照组 DID ⭐⭐
处理由明确阈值决定 RDD
只有横截面数据 + 丰富的协变量 PSM
有合适的外生变量 IV
只有一个处理单元 + 长历史 合成控制
什么都没有 ⚠️ 老实说"无法确定因果"

最后一行很重要承认不知道,比给一个错误的因果结论好。 很多分析的正确结论是「数据只能支持相关性,无法支持因果」。


🚧 五、三个必须诚实面对的事

① 所有观察性方法都依赖「无未观测混杂」假设

   PSM、DID、RDD、IV 都需要某种版本的这个假设
   而它【无法被数据证明】 ⭐

   → 所以观察性结论永远弱于 A/B
   → 报告时要明确写出你依赖了什么假设

② 一定要做敏感性分析

   问:需要一个多强的未观测混杂,才能把我的结论推翻?

   ⭐ 用一个数 Γ 来表达「有多强」:
   Γ = 1.0 → 假设完全没有未观测混杂(= 随机实验)
   Γ = 1.5 → 存在某个看不见的因素,能让一个人接受处理的几率变成 1.5 倍

   报告方式:「结论在 Γ ≤ ___ 时依然显著」

   · 临界 Γ < 1.2  → 💀 结论极脆弱,几乎任何遗漏变量都能推翻
   · 临界 Γ 1.2–2.0 → ⚠️ 需要论证「没有这么强的遗漏变量」
   · 临界 Γ > 2.0  → ✅ 相当稳健

上面那个客服案例的临界 Γ 是 1.15 —— 这个数字如果被算出来并写进报告,那个决策大概率不会被做出来。 敏感性分析只要半小时,它是观察性研究里性价比最高的一步。

③ 多种方法互相印证 ⭐

   同一个问题,用 PSM 和 DID 各做一次
   · 结论一致  → 可信度大幅提升
   · 结论矛盾  → 至少有一个的假设不成立,回去找

   ⭐ 这是观察性研究里最实用的一条纪律

🔙 六、回到第 9 章那个例子

   「联系客服次数」对流失预测很重要 —— 但因果方向是反的

   怎么用这一章的工具查清楚?

   ① DID:找一批客服流程改版的地区(对照:没改版的)
      看改版前后流失率的变化差
   ② RDD:如果客服资源按某个阈值分配(如VIP等级),比较阈值两侧
   ③ 最强:直接做实验 —— 随机给一部分用户更主动的客服介入  ⭐

   ⭐ 注意 ③ 才是真正能回答问题的,前两个是退而求其次

⚠️ 七、一张坑表

后果 怎么修
DID 不聚类标准误 💀 标准误低估到 1/3~1/5,p=0.001 实际是 p=0.3 cov_type="cluster"按处理分配的层级聚类
平行趋势只画两期 任意两点都能"平行",等于没验证 事件研究法看 4–6 个前期
把处理之后才产生的变量放进协变量 ⭐⭐ 控制掉了效应本身(bad control),估计被压到接近 0 协变量只能取处理发生之前的
只报配对后的 SMD 看不出 PSM 到底有没有起作用 前后两列一起报
用 p 值判断协变量均衡 大样本下什么都"不均衡",小样本下什么都"均衡" SMD < 0.1,它不受样本量影响
共同支撑没重叠还硬配 拿一个完全不像的人当对照 卡钳,宁可丢样本
处理是用户自己选的,却不做敏感性分析 💀 就是那个 +12 变 −9 的案例 必算临界 Γ,<1.2 就别下因果结论
RDD 带宽随便取 换个带宽结论就反了 多个带宽的稳健性;同时做操纵检验
结论用因果语言写,假设写在附录 决策者只读结论,假设没人看见 依赖的假设写进结论那一段

🔗 八、和站内其他章的关系

相关的地方 和这一章的关系
第 9 章重要性≠因果 这一章给了那个边界的解法
第 12 章 A/B 能做实验时永远首选
推荐算法 12 推荐场景的实验设计
数学原理 11 「没有假设就没有结论」同源

✅ 检查点

  1. 为什么不能直接比较「用了新功能」和「没用」的用户?
  2. 什么是混杂变量?画出那个因果图。
  3. PSM 的思路是什么?三个必做检查是什么?为什么用 SMD 而不是 p 值判断均衡?
  4. PSM 最大的局限是什么?和 A/B 的本质差别在哪?用那个客服案例说明。
  5. DID 的妙处是什么?它的关键假设是什么、怎么验证?跑 DID 回归时最容易漏掉哪一行、后果多严重?
  6. RDD 适用什么场景?必查的前提是什么、怎么查?
  7. IV 的两个条件是什么?哪一个无法用数据检验?
  8. 合成控制什么时候用?为什么要限制权重非负且和为 1?它怎么做推断?
  9. 敏感性分析里的 Γ 是什么意思?临界 Γ 多少算脆弱、多少算稳健?什么都没有时该怎么办?观察性研究里最实用的一条纪律是什么?
👀 答案
  1. 因为本来就活跃的用户更可能去用新功能,你比较的是"活跃的人"和"不活跃的人"——选择偏差
  2. 同时影响「是否接受处理」和「结果」的变量。图:活跃度 → 用不用新功能,活跃度 → 留存;你看到的相关可能全来自这条绕行路径。
  3. 人为配对:用协变量预测"有多大概率接受处理"(倾向得分),给每个处理组用户找一个得分几乎相同的对照。三检查:共同支撑区间(没重叠的必须丢掉)、配对后协变量均衡(标准化均值差小于 0.1)、敏感性分析。用 SMD 是因为它不受样本量影响:用 p 值的话,样本量一大任何微小差异都会"显著",样本量一小再大的差异也"不显著"。另外要同时报配对前和配对后两列,否则看不出 PSM 有没有起作用。
  4. 只能平衡观测到的变量,没进模型的混杂它无能为力。A/B 的随机化连你不知道的因素都能平衡——这是本质差别。案例:智能客服 PSM 做得教科书级(配对后所有 SMD 小于 0.06),结论 +12 分,全量替换人工后整体满意度掉了 9 分。漏掉的混杂是问题的真实复杂度——简单问题用户自己选了智能客服,复杂问题一开始就转人工,而「问题类型」只有 8 个粗类,同一类里既有"忘密码"也有"跨境退款纠纷"。普适教训:当"是否接受处理"由用户自己选择时,未观测混杂几乎必然存在,因为用户做这个选择时用到的信息比你的数据多
  5. 用对照组的变化扣掉了大盘趋势。关键假设是平行趋势(没有这个变化两组本会同趋势变化)。验证:用事件研究法把上线前 4–6 期的交互项都放进回归,全都接近 0 且不显著才算成立;只画两期没有意义,任意两点都能连成平行线。最容易漏的是 cov_type="cluster"——不聚类会把每个用户当独立样本,而实际独立单元只有十几个地区,标准误低估到真实值的 1/3~1/5,p=0.001 实际可能是 p=0.3
  6. 处理由明确阈值决定(如信用分不低于 600)。必查前提:阈值不能被操纵;查法是画分数分布直方图看阈值处有没有异常堆积。另外要报多个带宽的稳健性。
  7. ①影响是否接受处理(相关性)②只能通过处理影响结果(排他性)。排他性无法用数据检验,只能靠领域知识论证。
  8. 只有一个处理单元、找不到单一可比对照时用(配合足够长的上线前历史,一般 10 期以上)。用其余地区的加权组合拼一个"合成对照",权重由上线前历史拟合最好决定。限制非负且和为 1 是因为允许负权重时几乎总能完美拟合历史——那是过拟合不是可比。推断用安慰剂检验:把"假上线"轮流套到每个对照地区,真实效应不比大多数安慰剂大就是噪声。
  9. Γ 表示存在某个看不见的因素,能让一个人接受处理的几率变成 Γ 倍。临界 Γ 小于 1.2 极脆弱(几乎任何遗漏变量都能推翻)、1.2–2.0 需要论证大于 2.0 相当稳健。客服那个案例的临界 Γ 只有 1.15。什么都没有时老实说"无法确定因果"——承认不知道比给一个错误的因果结论好。最实用的纪律是多种方法互相印证:同一问题用 PSM 和 DID 各做一次,一致则可信度大增,矛盾则至少有一个假设不成立。

🛑 可以停在这里

走神救援

做不了A/B时(伦理/已全量/无法随机/样本太少/网络效应)怎么说因果。💀不能直接比:「用了新功能的留存高30%」是选择偏差——本来就活跃的人更可能去用;根源是混杂变量(同时影响"是否处理"和"结果")。四把工具:①⭐PSM(用协变量预测倾向得分,把"长得像但一个用一个没用"的配成对;三检查:共同支撑区间、配对后协变量均衡、敏感性分析;💀最大局限:只能平衡观测到的变量,而A/B的随机化连你不知道的都能平衡)②⭐⭐DID(华东上线华北没上,+30 − +10 = +20,妙在用对照组扣掉大盘趋势;关键假设平行趋势必须画上线前多期曲线验证是否平行)③RDD(阈值两侧比,599 vs 601 分几乎一样;必查阈值不能被操纵——画直方图看有没有异常堆积结论只对阈值附近成立)④IV(要满足相关性+排他性,而排他性无法用数据检验,最强也最易被质疑)。⑤合成控制(只有一个处理单元时,用其他地区的加权组合拼一个"假的华东",权重非负且和为1,用安慰剂检验做推断)。⭐什么都没有时,老实说"无法确定因果"——比给错误结论好。三条诚实:所有观察性方法都依赖无法证明的"无未观测混杂"假设、要做敏感性分析(多强的混杂能推翻结论)、⭐多种方法互相印证(一致则可信,矛盾说明有假设不成立)。💀真实案例:智能客服 PSM 做得教科书级(配对后所有 SMD 小于 0.06)得出+12 分,据此缩编人工客服,半年后整体满意度掉了 9 分 —— 漏掉的混杂是问题的真实复杂度(简单问题用户自己选智能客服、复杂问题一开始就转人工,而"问题类型"只有 8 个粗类)。⭐⭐普适教训:当"是否接受处理"由用户自己选择时,未观测混杂几乎必然存在——因为用户做选择时用到的信息比你的数据多;这个案例的临界 Γ 只有 1.15(极脆弱),做半小时敏感性分析就能拦住。⚠️两个必须记住的技术细节:①均衡性用 SMD 不用 p 值(SMD 不受样本量影响),且配对前后两列一起报 ②⭐⭐DID 回归必须 cov_type="cluster" 按处理分配的层级聚类,不聚类会把标准误低估到 1/3~1/5,p=0.001 实际可能是 p=0.3平行趋势要用事件研究法看 4–6 个前期,只画两期等于没验证。

下一节 👉 14-辛普森悖论与七个陷阱.md

打卡记录保存在你的浏览器里,首页能看到总进度