📑 本页目录(点开跳转)
13 · 不能做 A/B 时
⏱ 50 分钟 | ⭐⭐ 从观察数据里得出可信的因果结论
🎯 一句话
很多时候你不能做实验 —— 不能只给一半用户涨价、不能随机让一半人得病、 政策已经全量上线了。 这一章讲:这种情况下,怎么还能说出「是它造成的」。
🚫 一、什么时候做不了 A/B
| 情况 | 例子 |
|---|---|
| 伦理 / 合规不允许 | 不能随机给一半用户更差的风控 |
| 已经全量上线了 | 事后才想评估效果 |
| 无法随机分配 | 政策、外部环境、竞品动作 |
| 样本量根本不够 | To B 场景只有 50 个客户 |
| 网络效应太强 | 分流本身就会互相污染(第 12 章) |
💀 二、为什么不能直接比
「用了新功能的用户」 vs 「没用的用户」
→ 前者留存率高 30%
→ 结论:新功能提升留存 30%?❌❌
真相:
本来就活跃的用户【更可能去用新功能】
→ 你比较的是「活跃的人」和「不活跃的人」
→ 这叫【选择偏差】(selection bias)
🔑 核心问题:观察数据里,「是否接受处理」不是随机的,它由某些因素决定 —— 而那些因素同时也影响结果。这类因素叫混杂变量(confounder)。
混杂变量(活跃度)
╱ ╲
↓ ↓
用不用新功能 ──?──→ 留存
⭐ 你看到的相关,可能全部来自那条绕行的路径
🧰 三、四把工具
① 倾向得分匹配(PSM)—— 最常用 ⭐
思路:既然不能随机分,那就【人为配对】
① 用协变量(年龄、活跃度、历史行为…)建模,
预测「这个人有多大概率会用新功能」→ 这个概率叫【倾向得分】
② 给每个处理组用户,找一个倾向得分【几乎相同】的对照组用户
③ 比较这些配对的结果差异
⭐ 直觉:把「本来就长得很像、但一个用了一个没用」的人配成对
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 | 「没有假设就没有结论」同源 |
✅ 检查点
- 为什么不能直接比较「用了新功能」和「没用」的用户?
- 什么是混杂变量?画出那个因果图。
- PSM 的思路是什么?三个必做检查是什么?为什么用 SMD 而不是 p 值判断均衡?
- PSM 最大的局限是什么?和 A/B 的本质差别在哪?用那个客服案例说明。
- DID 的妙处是什么?它的关键假设是什么、怎么验证?跑 DID 回归时最容易漏掉哪一行、后果多严重?
- RDD 适用什么场景?必查的前提是什么、怎么查?
- IV 的两个条件是什么?哪一个无法用数据检验?
- 合成控制什么时候用?为什么要限制权重非负且和为 1?它怎么做推断?
- 敏感性分析里的 Γ 是什么意思?临界 Γ 多少算脆弱、多少算稳健?什么都没有时该怎么办?观察性研究里最实用的一条纪律是什么?
👀 答案
- 因为本来就活跃的用户更可能去用新功能,你比较的是"活跃的人"和"不活跃的人"——选择偏差。
- 同时影响「是否接受处理」和「结果」的变量。图:活跃度 → 用不用新功能,活跃度 → 留存;你看到的相关可能全来自这条绕行路径。
- 人为配对:用协变量预测"有多大概率接受处理"(倾向得分),给每个处理组用户找一个得分几乎相同的对照。三检查:共同支撑区间(没重叠的必须丢掉)、配对后协变量均衡(标准化均值差小于 0.1)、敏感性分析。用 SMD 是因为它不受样本量影响:用 p 值的话,样本量一大任何微小差异都会"显著",样本量一小再大的差异也"不显著"。另外要同时报配对前和配对后两列,否则看不出 PSM 有没有起作用。
- 只能平衡观测到的变量,没进模型的混杂它无能为力。A/B 的随机化连你不知道的因素都能平衡——这是本质差别。案例:智能客服 PSM 做得教科书级(配对后所有 SMD 小于 0.06),结论 +12 分,全量替换人工后整体满意度掉了 9 分。漏掉的混杂是问题的真实复杂度——简单问题用户自己选了智能客服,复杂问题一开始就转人工,而「问题类型」只有 8 个粗类,同一类里既有"忘密码"也有"跨境退款纠纷"。普适教训:当"是否接受处理"由用户自己选择时,未观测混杂几乎必然存在,因为用户做这个选择时用到的信息比你的数据多。
- 用对照组的变化扣掉了大盘趋势。关键假设是平行趋势(没有这个变化两组本会同趋势变化)。验证:用事件研究法把上线前 4–6 期的交互项都放进回归,全都接近 0 且不显著才算成立;只画两期没有意义,任意两点都能连成平行线。最容易漏的是
cov_type="cluster"——不聚类会把每个用户当独立样本,而实际独立单元只有十几个地区,标准误低估到真实值的 1/3~1/5,p=0.001 实际可能是 p=0.3。 - 处理由明确阈值决定(如信用分不低于 600)。必查前提:阈值不能被操纵;查法是画分数分布直方图看阈值处有没有异常堆积。另外要报多个带宽的稳健性。
- ①影响是否接受处理(相关性)②只能通过处理影响结果(排他性)。排他性无法用数据检验,只能靠领域知识论证。
- 只有一个处理单元、找不到单一可比对照时用(配合足够长的上线前历史,一般 10 期以上)。用其余地区的加权组合拼一个"合成对照",权重由上线前历史拟合最好决定。限制非负且和为 1 是因为允许负权重时几乎总能完美拟合历史——那是过拟合不是可比。推断用安慰剂检验:把"假上线"轮流套到每个对照地区,真实效应不比大多数安慰剂大就是噪声。
- Γ 表示存在某个看不见的因素,能让一个人接受处理的几率变成 Γ 倍。临界 Γ 小于 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