Skip to content

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 组用户)
  • ❌ 伦理不允许(如:测试"不治疗"对重症患者的影响)

📐 五步法实操

第一步:定义假设

H₀(零假设):新方案没有效果(μ_B - μ_A = 0)
H₁(备择假设):新方案有效果(μ_B - μ_A ≠ 0)

关键参数设定

  • 显著性水平 α = 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 校正

# 同时检验 10 个指标
n_tests = 10
alpha_corrected = alpha / n_tests  # 0.005

误区 3:忽略新奇效应

B 组转化率高可能只是因为用户对「变化」好奇。对长期指标(如 30 日留存),至少跑 2 周再下结论。

误区 4:幸存者偏差

只看"完成了实验的用户",忽略了中途退出的用户。用 Intent-to-Treat(ITT) 分析:所有被分到该组的用户,无论是否完成。


🛠 常用工具

工具 用途 适用场景
Evan Miller Calculator 在线样本量计算 快速估算
statsmodels Python 统计检验 比例 / 均值 / 方差检验
scipy.stats Python 统计分布 自定义检验逻辑
Eppo 企业级实验平台 团队协作、自动化分析
GrowthBook 开源实验平台 自建实验系统

🔗 进阶阅读