📑 本页目录(点开跳转)
12 · A/B 实验:你以为你懂了
⏱ 48 分钟 | ⭐ 每一条都有人踩过
🎯 一句话
A/B 实验的原理五分钟能讲完,但把它做对需要避开十几个坑 —— 而这些坑不会报错,只会给你一个看起来很合理的错误结论。
🎲 一、先把原理说清(五分钟)
随机把用户分成两组 → 一组用旧的(对照),一组用新的(实验)
→ 因为是【随机】分的,两组在所有方面都可比
→ 观察到的差异只可能来自「你改的那个东西」 ⭐
🔑 随机化的全部意义:它让两组在你知道的和你不知道的因素上都均衡。 「你不知道的」那部分,是任何观察性方法都做不到的 —— 这就是为什么 A/B 是因果推断的黄金标准(第 13 章会讲没它时怎么办)。
📏 二、坑一:样本量不够就下结论
先算需要多少样本,再开始实验。
$$n \approx \frac{16\,\sigma^2}{\delta^2}\quad(\text{每组,}\alpha=0.05,\ \text{power}=0.8)$$
💡 人话:要检测的差异(δ)越小,需要的样本量按平方增长。
⭐ 一个必须建立的直觉:
要检测的提升【减半】→ 样本量【×4】
转化率 5%,想检测 +0.5%(相对 +10%)
→ 每组约需 3 万人
想检测 +0.25%(相对 +5%)
→ 每组约需 12 万人 ⭐
from statsmodels.stats.power import zt_ind_solve_power
from statsmodels.stats.proportion import proportion_effectsize
def sample_size(p0, lift_abs, alpha=0.05, power=0.8):
"""p0: 基线转化率;lift_abs: 想检测的绝对提升"""
es = proportion_effectsize(p0 + lift_abs, p0)
return int(zt_ind_solve_power(effect_size=es, alpha=alpha, power=power))
print(sample_size(0.05, 0.005)) # 想检测 5% → 5.5%
但实践中更该跑的是反方向:我只有这么多流量,到底能检测出多大的提升?
def mde(n_per_group, p0, alpha=0.05, power=0.8):
"""MDE = 最小可检测提升。⭐ 实验设计时比 sample_size 更常用"""
es = zt_ind_solve_power(nobs1=n_per_group, alpha=alpha, power=power)
lo, hi = 0.0, 1.0 - p0 # 提升越大,效应量越大 → 单调,可二分
for _ in range(60):
mid = (lo + hi) / 2
if proportion_effectsize(p0 + mid, p0) < es:
lo = mid
else:
hi = mid
return (lo + hi) / 2
print(mde(30000, 0.05)) # 每组 3 万人、基线 5% → ≈ 0.005(即 5% → 5.5%)
print(mde(5000, 0.05)) # 每组 5 千人 → ≈ 0.0123(要涨 25% 才测得出)💀
⭐⭐ 这个函数应该在实验开始前跑一次,而不是结束后: 如果算出来的 MDE 比「业务上有意义的提升」还大,这个实验就不该跑 —— 跑了也只会得到一个"不显著",然后你还得花两周才知道自己白跑了。 上面第二行就是典型:每组只有 5 千人时,除非提升 25%,否则什么都测不出来。
流量不够怎么办:CUPED(方差缩减) ⭐⭐
核心观察:用户之间本来就差别巨大
(重度用户天天点,轻度用户一周点一次)
→ 指标的方差里,绝大部分是【个体差异】,不是【实验效果】
⭐ 如果你有这批用户【实验开始前】的同口径指标,
就能把这部分个体差异扣掉 —— 均值不变,方差变小
import numpy as np
def cuped(y, y_pre):
"""y: 实验期指标;y_pre: 同一批用户实验【开始前】同口径的指标 ⭐"""
theta = np.cov(y, y_pre)[0, 1] / np.var(y_pre) # ⭐ 方差最小化的最优系数
y_adj = y - theta * (y_pre - y_pre.mean()) # ⭐ 均值不变,所以估计仍无偏
return y_adj, 1 - np.var(y_adj) / np.var(y) # 第二个返回值 = 方差降低比例
| 实验前后指标相关系数 ρ | 方差降低 | 等价效果 |
|---|---|---|
| 0.3 | 9% | 聊胜于无 |
| 0.6 | 36% ⭐ | 两周的实验可以缩到 9 天 |
| 0.8 | 64% ⭐⭐ | 同样时长下 MDE 降到原来的 0.6 倍 |
💡 理论上方差正好降低 ρ²(ρ 是实验期与实验前指标的相关系数)。 对留存、时长、GMV 这类"个体稳定性高"的指标,ρ 常常在 0.6–0.8 —— 这是一个改十行代码就能把实验效率翻倍的方法,却常年被忽略。
⚠️ 一条铁律:
y_pre必须取自实验开始之前。 用实验期内的数据当协变量 = 把实验效果本身扣掉了, 这就是第 4 章的特征穿越在实验分析里的形态。⚠️ 样本量不够时会发生什么: 你会得到一个不显著的结果,然后误以为「新模型没用」—— 但实际可能是你的实验根本没有能力检测出这个大小的提升。 这叫 power 不足,它造成的损失是"把好东西扔掉",而且你永远不会知道。
👀 三、坑二:偷看(peeking)⭐⭐
❌ 错误做法:
每天看一眼,一旦 p < 0.05 就宣布成功、停止实验
为什么错:
p 值本身是波动的。
即使两组【完全没有差异】,
连续看 10 次,也有很大概率某一次会 p < 0.05 💀
💡 一个能说服人的数字:
两组真的没差异(A/A 实验)
· 只在实验结束时看 1 次 → 假阳性率 5% ✅
· 每天看一次,看 10 天 → 假阳性率约 30% 💀
· 一直盯着看 → 假阳性率趋近 100% 💀💀
三个正确做法:
| 做法 | 说明 |
|---|---|
| 提前定好样本量和时长,到点才看 ⭐ | 最简单、最可靠 |
| 用序贯检验(如 mSPRT、always-valid p-value) | 允许随时看,但方法本身要换 |
| 只把中途的观察用于止损,不用于宣布成功 ⭐ | 实用折中:跌得厉害可以停,涨了不能提前宣布 |
⭐ 第三条是实践中最常用的: 「负向可以早停,正向必须等满」 —— 因为早停止损的代价是可接受的, 而提前宣布成功会系统性地高估效果。
⏱️ 四、坑三:实验时长不够一个周期
⭐ 最少跑满一个完整的【周】—— 而不是"样本量够了就停"
原因:
· 工作日和周末的用户行为完全不同
· 新奇效应(novelty effect):新东西刚上线时用户会多点几下 ⭐
→ 前 2-3 天的数据系统性偏高
· 有些行为周期长(下单、续费)
⚠️ 新奇效应是最容易骗人的: 前三天涨 8%,一周后变成 +2%,一个月后变成 0。 只跑三天的实验,会让你把「新鲜感」当成「效果」。
对策:看逐日的效果曲线,而不只是累计值 —— 如果效果在稳定衰减,那多半是新奇效应。
🧪 五、坑四:多重比较
同时看 20 个指标,每个都用 p<0.05
→ 即使新模型完全没用,
平均也会有 1 个指标"显著" 💀
同理:同时跑 20 个实验组也一样
三个应对:
① 提前指定【一个】主指标(primary metric)⭐⭐
其他都是次要指标,只用来看有没有副作用
② 多重比较校正(Bonferroni:阈值除以比较次数)
简单但保守
③ 分层:主指标决定成败,护栏指标只看有没有明显恶化
⭐ 第 ① 条是最重要的实验纪律: 实验开始前就写下「我用哪个指标判断成败」。 否则事后从 20 个指标里挑一个涨了的,在统计上毫无意义 (这叫 HARKing —— 先看结果再编假设)。
🕸️ 六、坑五:分流单元错了 / 有干扰
分流单元
❌ 按【请求】分流 → 同一用户一会儿新一会儿旧
✅ 按【用户】分流 → 同一用户始终在同一组 ⭐
⚠️ 但如果你的效应会在用户之间传播,按用户分也不够
网络效应(SUTVA 违背)⭐
A/B 的基本假设:一个用户的处理不影响另一个用户的结果
(统计上叫 SUTVA)
什么时候会破:
· 社交产品:实验组用户发了内容,对照组用户也看到了 💀
· 双边市场:实验组司机被多派单 → 对照组司机接不到单 💀
· 共享库存/预算:实验组多花了预算 → 对照组预算少了 💀
对策:换分流单元 —— 按城市、按时间片、按社交圈分流(cluster randomization), 代价是样本量大幅下降(有效样本变成"簇"的个数,不是用户数)。
📊 七、坑六:只看均值
总体 +2%,但可能是:
· 90% 的用户没变化,10% 的重度用户 +20%
· 或者:新用户 −5%,老用户 +4% ⭐
⭐ 一定要看【分布】和【分人群】:
· 分位数(P50 / P90)而不只是均值
· 按新老、渠道、活跃度分层看
🔗 分人群看还能避免第 14 章那种反转。
✅ 八、实验前的检查清单
设计
[ ] 主指标只有一个,且实验前就写下来 ⭐⭐
[ ] 算过样本量,知道能检测多大的提升
[ ] 时长 ≥ 一个完整周期(通常 ≥ 1 周)
[ ] 分流单元正确,且哈希加了实验名的盐(第 3 章)
[ ] 想过有没有网络效应
运行
[ ] 先跑 A/A 验证分流系统本身没问题 ⭐
[ ] 中途只用于止损,不提前宣布成功
[ ] 护栏指标(延迟、错误率、核心业务)在看
分析
[ ] 报告置信区间,不只报 p 值和均值 ⭐
[ ] 分人群看过
[ ] 检查了两组的样本量是否符合预期(SRM 检查)⭐⭐
⭐ 最后一条 SRM(Sample Ratio Mismatch)值得单独说: 如果你设的是 50/50,实际却是 50.4/49.6 —— 在大样本下这个偏差在统计上是不可能的,说明分流系统有 bug。 SRM 一旦出现,整个实验结果都不可信,必须先修分流。 这是业界公认最有效的实验健康检查,一行代码就能做:
from scipy.stats import chisquare
def srm_check(counts, expect=None, alpha=1e-3):
"""counts: 各组实际样本量,如 [n_a, n_b];expect: 期望比例,默认等分
⭐ 阈值用 1e-3 而不是 0.05 —— 这个检查每天跑很多次,要防多重比较"""
counts = list(counts)
k, total = len(counts), sum(counts)
expect = expect or [1 / k] * k
p = chisquare(counts, [total * e for e in expect]).pvalue
return p, ("🔴 SRM!分流有问题,结果不可信" if p < alpha else "✅ 正常")
print(srm_check([1043221, 1051884])) # 差 0.83% → p ≈ 3e-9,铁定有问题 💀
print(srm_check([50120, 49880])) # 差 0.24% → p ≈ 0.23,正常波动 ✅
⭐ 更该做的是「分层 SRM」:总体比例正常,不代表每一层都正常。 按平台、App 版本、地域、新老用户各跑一次 —— 哪一层 SRM 了,根因几乎就写在那一层的名字上。
💀 一个真实案例:一次 +6.2% 的胜利,三个月后发现是假的
实验:新排序模型 vs 旧模型,主指标 CTR
结果:+6.2%,p = 0.003,跑满 14 天,全量上线 🎉
三个月后:大盘 CTR 纹丝不动
复盘时唯一被跳过的检查:
| 项 | 数字 |
|---|---|
| 实验组样本量 | 1,043,221 |
| 对照组样本量 | 1,051,884 |
| 差异 | 0.83% |
| SRM 卡方 p 值 | ≈ 3×10⁻⁹ 💀 |
根因链条:
新模型的响应里多了一个字段
→ 某个老版本 App(占总流量 4.1%)解析失败、直接崩溃
→ 这批用户【没有产生后续曝光日志】
→ 他们从实验组的分母里「消失」了 💀
⭐ 而消失的恰好是【老版本用户 = 低活跃用户 = CTR 本来就低的那批】
→ 实验组剩下的人天生 CTR 更高
→ +6.2% 全部来自「实验组少了一批 CTR 低的用户」
💀 代价不止是这一个实验: 后续 5 个实验都以这个"新基线"为对照,三个月的迭代方向被整体带偏; 而且崩溃的那 4.1% 用户里,有相当一部分直接卸载了。
⭐ 而发现它只需要一行
srm_check,耗时 10 秒。 这就是为什么 SRM 被公认为投入产出比最高的一项实验检查。
SRM 出现时的排查顺序(照抄即可):
① 分层看:哪个平台/版本/地域 SRM?→ 根因通常就在那一层 ⭐
② 是不是有一组用户【根本没上报日志】(崩溃、白屏、超时)⭐⭐
③ 分流哈希是不是没加实验名的盐(和上一个实验相关了,第 3 章)
④ 有没有中途改过分流比例 —— 改比例前后的数据不能合并算
⑤ 过滤条件是不是对两组不对称(例如"只看有曝光的用户")💀
⑥ 机器人/爬虫流量是不是只被其中一组吸引
⚠️ 在修好之前,不要看任何实验结论 —— 包括"看起来很合理"的那些
⚠️ 九、一张坑表(每一条都有人踩过)
| 坑 | 后果 | 怎么修 |
|---|---|---|
| 不算样本量就开跑 | 两周后得到"不显著",白跑 | 开跑前算 MDE,比业务有意义的提升还大就别跑 ⭐ |
| 偷看,一显著就停 ⭐⭐ | 假阳性率从 5% 涨到 30% | 定好时长;中途只用于止损 |
| 只跑 3 天 | 新奇效应被当成效果 | 至少一个完整周 + 看逐日曲线 |
| 事后从 20 个指标里挑赢的 | 结论在统计上毫无意义(HARKing) | 实验前写死唯一主指标 ⭐⭐ |
| 不查 SRM 💀 | 整个结论是假的,还会污染后续实验 | 一行卡方,分层各跑一次 |
| 只看有曝光的用户 | 过滤本身对两组不对称 → 人为造出 SRM | 分母固定为进组的全部用户 ⭐ |
| 中途改分流比例后合并算 | 两段数据结构不同,均值被污染 | 改比例即视为新实验,重新计时 |
| 用实验期数据做 CUPED 协变量 | 把实验效果本身扣掉了 | y_pre 只能取实验开始前 ⭐ |
| 多个实验共用同一个哈希 | 实验之间相关,效应互相污染 | 哈希加实验名的盐(第 3 章) |
| 只报 p 值不报置信区间 | 读者无法判断"这个提升值不值得做" | 永远报 点估计 + 置信区间 + 绝对值 ⭐ |
🔗 十、和站内其他章的关系
| 相关的地方 | 和这一章的关系 |
|---|---|
| 推荐算法 12 A/B 基础 | 这里是它的深水区 |
| 第 3 章哈希加盐 | 分流单元 |
| Kaggle 22 shake-up | 同样是"小样本上的结论不稳" |
| 数学原理 09 | 样本量和精度的平方关系同源 |
| 数学原理 08方差 | CUPED 缩的就是它 |
| 智能体工程 14 Evals | Agent 场景的同一套实验纪律 |
✅ 检查点
- 随机化的全部意义是什么?为什么它是黄金标准?
- 要检测的提升减半,样本量要变几倍?power 不足会造成什么损失?
- 为什么说实验前该算的是 MDE 而不是样本量?每组 5 千人、基线 5% 时 MDE 大约多少?
- CUPED 在做什么?方差降低和相关系数是什么关系?ρ=0.6 时相当于省了多少时间?CUPED 有哪条铁律?
- 什么是 peeking?每天看一次看 10 天,假阳性率大约多少?实践中最常用的折中做法是什么?
- 新奇效应怎么骗人?怎么识别?
- 多重比较问题怎么解?最重要的实验纪律是什么?
- 什么是 SUTVA 违背?举两个场景。对策的代价是什么?
- 什么是 SRM?为什么它一出现整个实验就不可信?用那个 +6.2% 的案例说明它的根因链条。
👀 答案
- 它让两组在你知道的和你不知道的因素上都均衡。"你不知道的"那部分是任何观察性方法都做不到的——所以它是因果推断的黄金标准。
- ×4(样本量按 δ 的平方反比增长)。power 不足会让你得到不显著的结果,误以为"新模型没用"而把好东西扔掉,且你永远不会知道。
- 因为实验里流量通常是给定的,真正的问题是"这些流量够不够"。MDE 比业务上有意义的提升还大 → 这个实验根本不该跑,跑了也只会拿到一个"不显著",而且要两周后才知道白跑了。每组 5 千人、基线 5% 时 MDE ≈ 0.0123,即要涨 25% 才测得出来。
- 用这批用户实验开始前的同口径指标做协变量,把个体差异从方差里扣掉;均值不变所以估计仍无偏,方差变小。方差正好降低 ρ²。ρ=0.6 → 方差降 36% → 两周的实验可以缩到 9 天。铁律:
y_pre必须取自实验开始之前,用实验期内的数据当协变量等于把实验效果本身扣掉了(实验分析版的特征穿越)。 - 每天看一眼、一旦 p 跌破 0.05 就宣布成功。看 10 天假阳性率约 30%(只看一次是 5%,一直盯着趋近 100%)。折中做法是「负向可以早停,正向必须等满」——中途观察只用于止损,不用于宣布成功。
- 新东西刚上线用户会多点几下,前 2-3 天数据系统性偏高;前三天 +8%,一周后 +2%,一个月后 0。识别:看逐日效果曲线而不只是累计值,效果稳定衰减就多半是它。
- ①提前指定一个主指标 ②多重比较校正 ③分层(主指标定成败、护栏指标看恶化)。最重要的纪律是实验前就写下用哪个指标判断成败,否则事后挑一个涨的(HARKing)在统计上毫无意义。
- 「一个用户的处理不影响另一个用户的结果」被违反。场景:社交产品(实验组发的内容对照组也看到)、双边市场(实验组司机多派单→对照组接不到)、共享预算。对策是换分流单元(按城市/时间片/社交圈),代价是有效样本变成"簇"的个数,样本量大幅下降。
- 设 50/50 实际却是 50.4/49.6 —— 大样本下这个偏差统计上不可能,说明分流系统有 bug。分流都错了,两组就不再可比,所有结论都失去基础。 案例:实验组 1,043,221 / 对照组 1,051,884,差 0.83%,卡方 p ≈ 3×10⁻⁹。链条是:新模型响应多了一个字段 → 某老版本 App(占 4.1%)解析失败崩溃 → 这批用户没产生曝光日志,从实验组分母里消失 → 而消失的恰好是低活跃、CTR 本来就低的人 → +6.2% 全部来自"实验组少了一批 CTR 低的用户"。代价是后续 5 个实验都建立在错误基线上,三个月方向被带偏;而发现它只要一行卡方、10 秒钟。
🛑 可以停在这里
⚡ 走神救援
⭐随机化的全部意义:让两组在你知道的和不知道的因素上都均衡——"不知道的"那部分是观察性方法做不到的,所以 A/B 是黄金标准。坑一样本量:⭐要检测的提升减半→样本量×4;power 不足的损失是把好东西扔掉且你永远不知道。⭐⭐坑二 peeking:每天看、一旦 p 跌破 0.05 就宣布 → 看10天假阳性率约30%(看1次是5%,一直盯趋近100%);实践折中是⭐「负向可以早停,正向必须等满」。坑三时长:至少一个完整周;⚠️新奇效应前三天+8%、一周后+2%、一月后0 → 看逐日曲线而非累计值。坑四多重比较:看20个指标必然有1个"显著" → ⭐⭐实验前就写下唯一主指标(事后挑涨的叫 HARKing,统计上无意义)。坑五 SUTVA 违背:社交/双边市场/共享预算里实验组会影响对照组 → 换成按城市或时间片分流,代价是有效样本变成簇的个数。坑六只看均值 → 看分位数和分人群。⭐⭐SRM 检查:设50/50实际50.4/49.6 在大样本下统计上不可能 = 分流系统有bug,整个实验不可信,一行卡方就能查——业界公认最有效的健康检查。⭐实验前该算的是 MDE 不是样本量(流量通常是给定的):每组5千人、基线5% → MDE≈1.23个百分点,要涨25%才测得出;MDE 比业务上有意义的提升还大就别跑这个实验。⭐⭐流量不够时用 CUPED:用这批用户实验开始前的同口径指标当协变量,把个体差异从方差里扣掉,均值不变所以无偏、方差降低正好 ρ²;ρ=0.6 → 方差降36% → 两周实验缩到9天,改十行代码的事却常年被忽略;⚠️铁律:y_pre 只能取实验开始之前,否则等于把实验效果本身扣掉了(实验版特征穿越)。💀真实案例:新排序模型 CTR +6.2%、p=0.003 全量上线,三个月后大盘纹丝不动 —— 唯一被跳过的检查是 SRM:实验组 1,043,221 vs 对照组 1,051,884,差 0.83%,p≈3e-9;根因是新模型响应多了一个字段,某老版本 App(占4.1%)解析崩溃 → 这批用户没产生曝光日志、从实验组分母里消失,而消失的恰好是 CTR 本来就低的低活跃用户 → +6.2% 全部是"实验组少了一批差用户"。代价:后续5个实验全建立在错误基线上,三个月方向被带偏;而发现它只要一行卡方 10 秒。⭐SRM 排查顺序:分层看哪一层出问题 → 有没有一组用户根本没上报日志 → 哈希有没有加实验名的盐 → 中途改过分流比例 → 过滤条件对两组不对称(如"只看有曝光的用户") → 爬虫流量。修好之前不要看任何结论。
下一节 👉 13-不能做AB时的因果推断.md ⭐⭐