A/B 测试实操指南¶
从实验设计到统计决策——用 Python 把 A/B 测试跑通
🤔 为什么需要 A/B 测试?¶
A/B 测试(又称对照实验、RCT)是因果推断的黄金标准。它回答的问题是:
"如果我们做了 X,Y 会怎样?"
举例:
| 场景 | 对照组(A) | 实验组(B) | 核心指标 |
|---|---|---|---|
| 按钮颜色 | 蓝色按钮 | 红色按钮 | 点击率 |
| 定价策略 | 原价 ¥99 | 折扣 ¥79 | 转化率 |
| 推荐算法 | 协同过滤 | 深度学习 | 用户停留时长 |
| 邮件标题 | "本周特惠" | "仅限今日" | 打开率 |
什么时候不该用 A/B 测试:
- ❌ 样本量太小(< 1000),统计效力不足
- ❌ 效应量极小,需要天文数字样本(如:点击率从 2.000% 提升到 2.001%)
- ❌ 存在网络效应(如社交产品,A 组用户的行为会影响 B 组用户)
- ❌ 伦理不允许(如:测试"不治疗"对重症患者的影响)
📐 五步法实操¶
第一步:定义假设¶
关键参数设定:
- 显著性水平 α = 0.05(允许 5% 的假阳性率)
- 统计效力 1-β = 0.80(80% 的概率检测到真实效应)
- 最小可检测效应(MDE):你关心的最小业务提升量
import numpy as np
from scipy import stats
# 业务参数
baseline_rate = 0.10 # 对照组转化率 10%
mde = 0.02 # 最小可检测效应 2%(相对提升 20%)
alpha = 0.05
power = 0.80
第二步:计算样本量¶
样本量太小 → 没有统计效力 → 做不出显著结果(白做) 样本量太大 → 浪费时间/流量,可能检测出无业务意义的微效应
from statsmodels.stats.power import NormalIndPower
# 效应量(Cohen's h for proportions)
effect_size = 2 * (np.arcsin(np.sqrt(baseline_rate + mde))
- np.arcsin(np.sqrt(baseline_rate)))
analysis = NormalIndPower()
n_per_group = analysis.solve_power(
effect_size=effect_size,
power=power,
alpha=alpha,
ratio=1.0
)
print(f"每组需要样本量: {int(np.ceil(n_per_group))}")
print(f"总样本量: {int(np.ceil(2 * n_per_group))}")
经验法则: - 转化率实验(10% baseline, +2% lift)→ 每组 ~3,800 - 点击率实验(2% baseline, +0.5% lift)→ 每组 ~2,400 - 收入实验(高方差)→ 每组 10,000+
第三步:随机分流¶
import hashlib
def assign_variant(user_id: str, salt: str = "experiment_001") -> str:
"""基于用户 ID 的确定性哈希分流"""
hash_val = int(hashlib.md5(f"{user_id}{salt}".encode()).hexdigest(), 16)
return "B" if hash_val % 100 < 50 else "A"
# 示例
print(assign_variant("user_12345")) # 总是返回相同结果
分流检查清单: - [ ] A/B 组用户量接近 1:1 - [ ] 关键特征分布均衡(年龄、性别、活跃度……) - [ ] 没有「A 组老用户更多」或「B 组 iOS 更多」的偏差 - [ ] 同一用户始终在同一组(用哈希而非随机数)
第四步:运行实验并收集数据¶
import pandas as pd
# 模拟实验数据
np.random.seed(42)
n = 5000
df = pd.DataFrame({
'variant': ['A'] * n + ['B'] * n,
'converted': (
list(np.random.binomial(1, baseline_rate, n)) +
list(np.random.binomial(1, baseline_rate + mde, n))
)
})
# 快速检查
print(df.groupby('variant').agg(
users=('converted', 'count'),
conversions=('converted', 'sum'),
rate=('converted', 'mean')
).round(4))
实验期间不要做的 3 件事: 1. ❌ 不要提前 peek:每天看一眼 p 值,"咦,p < 0.05 了,停!"——这会导致假阳性率飙升到 25%+ 2. ❌ 不要改样本量:"数据不够,再多跑几天"——这同样破坏统计保证 3. ❌ 不要改指标:"点击率不显著,改成加购率试试"——这是 p-hacking
第五步:统计检验与决策¶
from scipy import stats
# 分离数据
a_data = df[df['variant'] == 'A']['converted']
b_data = df[df['variant'] == 'B']['converted']
# Z 检验(比例检验)
p_a = a_data.mean()
p_b = b_data.mean()
n_a = len(a_data)
n_b = len(b_data)
# 合并比例
p_pool = (a_data.sum() + b_data.sum()) / (n_a + n_b)
se = np.sqrt(p_pool * (1 - p_pool) * (1/n_a + 1/n_b))
z_stat = (p_b - p_a) / se
p_value = 2 * (1 - stats.norm.cdf(abs(z_stat)))
# 置信区间
se_diff = np.sqrt(p_a*(1-p_a)/n_a + p_b*(1-p_b)/n_b)
ci_lower = (p_b - p_a) - 1.96 * se_diff
ci_upper = (p_b - p_a) + 1.96 * se_diff
print(f"""
=== A/B Test Results ===
A 组转化率: {p_a:.4f} ({n_a} users)
B 组转化率: {p_b:.4f} ({n_b} users)
提升: {(p_b-p_a)/p_a*100:.2f}%
95% CI: [{ci_lower:.4f}, {ci_upper:.4f}]
p-value: {p_value:.4f}
{'✅ 显著' if p_value < alpha else '❌ 不显著'}
""")
解读 p 值: - p < 0.05 → "如果零假设为真,观察到这个结果的概率 < 5%" → 拒绝 H₀ - p > 0.05 → "无法排除随机波动的可能" → 不拒绝 H₀(不是"接受 H₀")
更重要的:关注置信区间而非 p 值
即使 p < 0.05,如果 95% CI 是 [0.01%, 0.5%],业务上也可能无意义。如果 CI 是 [2%, 8%],那才是真正的业务信号。
🚫 常见误区¶
误区 1:"p < 0.05 就是成功"¶
p 值 ≠ 效应量。巨大样本下,0.01% 的提升也能 p < 0.05——但业务上毫无意义。
误区 2:多重比较不校正¶
如果同时测 20 个指标,随机就会出现 1 个 p < 0.05。用 Bonferroni 校正:
误区 3:忽略新奇效应¶
B 组转化率高可能只是因为用户对「变化」好奇。对长期指标(如 30 日留存),至少跑 2 周再下结论。
误区 4:幸存者偏差¶
只看"完成了实验的用户",忽略了中途退出的用户。用 Intent-to-Treat(ITT) 分析:所有被分到该组的用户,无论是否完成。
🛠 常用工具¶
| 工具 | 用途 | 适用场景 |
|---|---|---|
| Evan Miller Calculator | 在线样本量计算 | 快速估算 |
statsmodels |
Python 统计检验 | 比例 / 均值 / 方差检验 |
scipy.stats |
Python 统计分布 | 自定义检验逻辑 |
| Eppo | 企业级实验平台 | 团队协作、自动化分析 |
| GrowthBook | 开源实验平台 | 自建实验系统 |
🔗 进阶阅读¶
- 因果推断资源合集 — 从 A/B 到 DID、IV、RDD 的完整方法链
- 相关性 ≠ 因果性 —— 辛普森悖论实战演示
- Trustworthy Online Controlled Experiments — A/B 测试圣经(Kohavi、Tang、Xu)
- 相关案例:信用评分卡 A/B 验证 · 安全控制 ROI 分析