结论先行:A/B 实验最贵的成本不是算 p 值,而是「攒样本的等待时间」。CUPED(Controlled-experiments Using Pre-existing Data)用一个实验前就存在的协变量 X 去「吸收」指标 Y 里与处理无关的固有方差,等效于把方差降低 ρ²(Y 与 X 的相关系数平方),从而把达到同样 power 所需的样本量砍掉 ρ² 那一大块。在我们一个日均转化 UV 只有 40 万的 B 端场景里,pre/post 指标相关性 ρ≈0.7,实测方差降了 48%,等效把「跑够样本」的周期从 14 天压到 9 天。本文给 θ 的无偏估计方法、可直接跑的 SQL/Python、比率指标的处理,以及 4 个会把 CUPED 用废的坑。
一、现象与背景:样本量不够,实验要么「提前看」要么「拖死」
我接手一个 B 端增长团队的数据分析。痛点很具体:
- 一个季度跑了 11 个 A/B 实验,其中 5 个因为流量太小,跑到预定周期(14 天)时 p 值还在 0.08–0.20 之间,既不显著也不够「不显著」,产品只能拍脑袋「先上了再说」或「再等两周」。
- 团队里反复出现「第 3 天 p=0.03 就全量」的冲动(参见 postId 48《A/B 实验「一显著就停」的陷阱》),根子都是等待成本太高——攒够样本要 2 周,谁都不想等。
- 核心指标是转化率 + 人均 GMV,分母 UV 日均 40 万,单臂要攒到 MDE=3%、power=0.8、α=0.05 需要约 每臂 6.7 万 UV × 足够天数,实测要 14 天。
一句话:流量是硬约束,但「方差」不是。只要指标 Y 里混着大量「用户天生差异」带来的噪声,这部分噪声是可以被一个实验前就有的变量解释掉、从而减掉的。这就是 CUPED 的立足点。
二、原理与根因:为什么 pre-treatment 的 X 能「合法地」降低方差
2.1 方差可以拆成两部分
对任意用户 i,实验期指标 Y_i = (真正的处理效应 + 用户固有差异 + 随机噪声)。CUPED 的直觉是:用户固有差异里,有一部分能被「实验前他已经长什么样」预测。设 X_i 是实验前 28 天的同口径指标(pre-GMV / pre-订单数 / pre-时长),则:
Y_adj = Y - θ · (X - E[X])
其中 θ = Cov(Y, X) / Var(X)。
关键性质(这是 CUPED 能「无偏减方差」的数学保证):
- 不减偏:因为 X 是 pre-treatment 的(实验开始前就定死),它对实验组/对照组是「共同」的量,E[Y_adj|处理组] 与 E[Y_adj|对照组] 的差 = 原处理效应差,处理效应估计无偏。
- 降方差:Var(Y_adj) = Var(Y)·(1 − ρ²),其中 ρ 是 Y 与 X 的 Pearson 相关系数。
所以方差缩减比例 = ρ²。ρ=0.5 → 降 25%;ρ=0.7 → 降 49%;ρ=0.85 → 降 72%。
2.2 ρ 从哪来、为什么 B 端/留存型场景收益最大
ρ 反映「用户实验前的行为」对「实验期行为」的预测力。经验区间(我们实测 11 个实验的 ρ 分布):
| 指标类型 | 典型 ρ(对 pre-同口径) | 方差缩减 | |---|---|---| | 人均 GMV / 订单数 | 0.65–0.80 | 42%–64% | | 人均时长 / 会话数 | 0.60–0.75 | 36%–56% | | 转化率(0/1) | 0.30–0.45 | 9%–20% | | 全新功能首启(无历史) | ≈0 | ≈0(CUPED 帮不上) |
规律:越是「老用户、有稳定行为基线」的指标,CUPED 收益越大;越是「冷启动、无历史」的指标,几乎没有可借的先验,CUPED 失效。这和 postId 1502《小分母比率指标为什么天天抖》是同一枚硬币的两面——比率指标本身方差大、且历史相关性低,所以对它 CUPED 收益最小,得靠 delta 方法或比值调整。
三、具体做法:θ 怎么估、代码怎么写(可复现)
3.1 协变量 X 的选取
- 用实验开始前 28 天的同口径指标(对齐你的 Y 的口径:GMV 对 GMV、时长对时长)。
- 严禁用实验期数据(见坑 2)。
- 单协变量最简单;多协变量用回归调整(3.3)。
3.2 θ 的估计(无偏版)
import numpy as np
def cuped_estimate(y_treat, y_ctrl, x_treat, x_ctrl):
"""
θ 用「全量合并」估(实验组+对照组一起),避免只用单臂带来的偏差。
注意:x 必须是 pre-treatment。
"""
y = np.concatenate([y_treat, y_ctrl])
x = np.concatenate([x_treat, x_ctrl])
theta = np.cov(y, x, bias=True)[0, 1] / np.var(x, ddof=1)
ex = x.mean()
y_adj_treat = y_treat - theta * (x_treat - ex)
y_adj_ctrl = y_ctrl - theta * (x_ctrl - ex)
# 方差缩减比例
rho2 = 1 - (np.var(y_adj_treat, ddof=1) + np.var(y_adj_ctrl, ddof=1)) \
/ (np.var(y_treat, ddof=1) + np.var(y_ctrl, ddof=1))
return theta, y_adj_treat, y_adj_ctrl, rho2
3.3 多协变量(回归调整)
# X 是 [n, k] 的 pre-treatment 协变量矩阵(含截距)
theta_vec = np.linalg.pinv(X_all) @ (y_all - X_all.mean(axis=0))
y_adj = y - (X - X.mean(axis=0)) @ theta_vec
3.4 SQL 版(ClickHouse / PostgreSQL 通用)
WITH pre AS ( -- 实验前 28 天同口径
SELECT user_id, SUM(gmv) AS x
FROM dwd_order
WHERE dt BETWEEN date_sub(:exp_start, 28) AND date_sub(:exp_start, 1)
GROUP BY user_id
),
exp AS ( -- 实验期指标
SELECT e.user_id, e.arm, SUM(o.gmv) AS y
FROM exp_assignment e JOIN dwd_order o ON e.user_id=o.user_id
WHERE o.dt BETWEEN :exp_start AND :exp_end
GROUP BY e.user_id, e.arm
),
theta AS (
SELECT (SUM(y*x)-SUM(y)*SUM(x)/COUNT(*)) / (SUM(x*x)-SUM(x)*SUM(x)/COUNT(*)) AS th
FROM exp JOIN pre USING(user_id)
)
SELECT e.arm,
AVG(e.y - t.th*(p.x - (SELECT AVG(p.x) FROM pre))) AS y_adj_mean,
VAR(e.y - t.th*(p.x - (SELECT AVG(p.x) FROM pre))) AS y_adj_var
FROM exp e JOIN pre p USING(user_id) CROSS JOIN theta t
GROUP BY e.arm;
3.5 读数方式
对 y_adj 做和普通 A/B 一样的 t 检验 / 置信区间即可,只是方差小了、样本「等效」变多。报告时务必同时给「原始 Y 的效应」和「CUPED 调整后的效应」,两者应接近(差多少就是 X 解释掉的噪声量级),便于评审方判断合理性。
四、踩过的坑 / 反直觉之处(4 个)
-
θ 别只用实验组估。只用处理臂估 θ,会让处理臂的方差被「过度吸收」、对照臂不吸收,制造出虚假的处理效应差异。正确做法是全量(两臂合并)估 θ,或更保守地只用对照臂估、再对两臂同样调整。
-
X 必须是 pre-treatment,否则「方差缩减」是假的。我用过一次实验期内的中间指标当 X,方差缩减显示 71%(很诱人),但处理效应直接被抹到接近 0——因为 X 已经「看到了」处理结果,等于信息泄漏。CUPED 的合法性 100% 建立在「X 在随机化之前就已确定」上。
-
ρ 高 ≠ 效应被抹掉,ρ 低时 CUPED 可能反噬。当 X 信噪比很差(样本小、X 噪声大),θ 估计不稳,
y_adj的方差可能比原始 Y 更大(过拟合)。上线前先用历史数据估一遍 ρ²,ρ²<0.1 的指标直接放弃 CUPED,别硬套。 -
比率指标(转化率)不能直接套。对「分子/分母」型指标,要对分子分母分别做调整再用 delta 方法估方差,否则 CUPED 的无偏性不成立。这类指标收益本来就小(见上表),性价比低,通常不值得。
五、结论与可复用清单
核心判断:CUPED 不增加流量、不改变随机化,只把「用户固有差异」这块噪声用实验前数据合法地减掉;收益 = ρ²,老用户/留存型指标收益大,冷启动指标几乎无效。
落地清单(可直接抄):
- [ ] 选 X = 实验前 28 天、与 Y 同口径的指标;确认它在随机化前已定死
- [ ] θ 用两臂合并(或仅对照臂)估计,禁止只处理臂
- [ ] 上线前用历史数据估 ρ²;ρ² < 0.1 的指标跳过 CUPED
- [ ] 0/1 转化率类:改用 delta 方法 / 比值调整,别直接套
- [ ] 报告同时给「原始 Y」与「CUPED 调整」两个效应,供交叉校验
- [ ] 把「等效样本量提升 ≈ 1/ρ² 倍」写进实验设计文档,作为周期压缩的依据
一句话收口:流量是硬约束,方差是软约束——先把方差里能解释的那部分解释掉,再谈「要不要等更久」。