A/B测试统计学基础
约 1930 字大约 6 分钟
ab-testingstatistics
2025-09-14
A/B 测试是数据驱动决策的核心方法,通过随机对照实验比较不同方案的效果。本文介绍 A/B 测试背后的统计学原理、样本量计算、进阶方法和常见陷阱。
A/B 测试流程
假设检验
A/B 测试本质上是一个假设检验问题:
- 零假设 H0:两个版本没有差异(基线版本 A = 实验版本 B)
- 备择假设 H1:两个版本存在差异(A 不等于 B,或 B > A)
import numpy as np
from scipy import stats
# 示例:比较两组的转化率
n_control = 10000
n_treatment = 10000
conversions_control = 500 # 5.0% 转化率
conversions_treatment = 550 # 5.5% 转化率
# 方法 1:Z 检验(大样本比例检验)
p_control = conversions_control / n_control
p_treatment = conversions_treatment / n_treatment
p_pooled = (conversions_control + conversions_treatment) / (n_control + n_treatment)
se = np.sqrt(p_pooled * (1 - p_pooled) * (1/n_control + 1/n_treatment))
z_stat = (p_treatment - p_control) / se
p_value = 2 * (1 - stats.norm.cdf(abs(z_stat)))
print(f"Control CR: {p_control:.4f}")
print(f"Treatment CR: {p_treatment:.4f}")
print(f"Lift: {(p_treatment - p_control) / p_control * 100:.2f}%")
print(f"Z-statistic: {z_stat:.4f}")
print(f"P-value: {p_value:.4f}")
# 方法 2:卡方检验
contingency_table = np.array([
[conversions_control, n_control - conversions_control],
[conversions_treatment, n_treatment - conversions_treatment]
])
chi2, p_value_chi2, dof, expected = stats.chi2_contingency(contingency_table)
print(f"Chi2 p-value: {p_value_chi2:.4f}")P 值与置信区间
P 值
P 值是在零假设为真的前提下,观察到当前(或更极端)结果的概率。P 值小于显著性水平 alpha(通常 0.05)时拒绝零假设。
P 值不是:
- 效果为真的概率
- 效果大小的度量
- 实际重要性的指标
置信区间
95% 置信区间表示:如果重复实验很多次,有 95% 的区间会包含真实参数值。
def confidence_interval_proportion(successes, n, confidence=0.95):
"""比例的置信区间"""
p = successes / n
z = stats.norm.ppf(1 - (1 - confidence) / 2)
se = np.sqrt(p * (1 - p) / n)
return (p - z * se, p + z * se)
def confidence_interval_difference(p1, n1, p2, n2, confidence=0.95):
"""两个比例之差的置信区间"""
diff = p2 - p1
z = stats.norm.ppf(1 - (1 - confidence) / 2)
se = np.sqrt(p1 * (1 - p1) / n1 + p2 * (1 - p2) / n2)
return (diff - z * se, diff + z * se)
ci = confidence_interval_difference(p_control, n_control, p_treatment, n_treatment)
print(f"差异的 95% CI: [{ci[0]:.4f}, {ci[1]:.4f}]")第一类和第二类错误
| H0 为真(无差异) | H1 为真(有差异) | |
|---|---|---|
| 拒绝 H0 | 第一类错误 (alpha) | 正确 (Power = 1 - beta) |
| 不拒绝 H0 | 正确 | 第二类错误 (beta) |
- alpha (显著性水平):通常设为 0.05(容忍 5% 的假阳性概率)
- beta:通常设为 0.2(容忍 20% 的假阴性概率)
- Power (统计功效):1 - beta = 0.8(有 80% 概率检测到真实差异)
功效分析与样本量计算
样本量取决于四个参数:alpha、power、基线指标、最小可检测效应(MDE)。
from statsmodels.stats.power import NormalIndPower, TTestIndPower
from statsmodels.stats.proportion import proportion_effectsize
# 方法 1:比例检验的样本量
baseline_rate = 0.05 # 基线转化率 5%
mde = 0.005 # 最小可检测效应 0.5%(绝对值)
alpha = 0.05
power = 0.8
effect_size = proportion_effectsize(baseline_rate, baseline_rate + mde)
analysis = NormalIndPower()
sample_size = analysis.solve_power(
effect_size=effect_size,
alpha=alpha,
power=power,
alternative='two-sided'
)
print(f"每组所需样本量: {int(np.ceil(sample_size))}")
# 约 31,300 每组
# 方法 2:连续指标的样本量(如 ARPU)
def sample_size_continuous(mu_control, mu_treatment, sigma, alpha=0.05, power=0.8):
"""连续指标的样本量计算"""
effect_size = (mu_treatment - mu_control) / sigma
analysis = TTestIndPower()
n = analysis.solve_power(
effect_size=effect_size,
alpha=alpha,
power=power,
alternative='two-sided'
)
return int(np.ceil(n))
n = sample_size_continuous(
mu_control=10.0, # 控制组均值
mu_treatment=10.5, # 实验组预期均值
sigma=5.0 # 标准差
)
print(f"每组所需样本量: {n}")实验持续时间估算
def experiment_duration(sample_size_per_group, daily_traffic, traffic_fraction=1.0):
"""估算实验需要运行的天数"""
daily_per_group = daily_traffic * traffic_fraction / 2
days = np.ceil(sample_size_per_group / daily_per_group)
return int(days)
duration = experiment_duration(
sample_size_per_group=31300,
daily_traffic=50000,
traffic_fraction=0.5 # 只用 50% 流量做实验
)
print(f"预计实验持续天数: {duration}")序贯检验(Sequential Testing)
传统的固定样本量检验需要等到预设样本量全部收集完毕才能分析。序贯检验允许在数据收集过程中多次查看结果,提前停止实验。
# 简化的 Alpha Spending Function (O'Brien-Fleming)
def alpha_spending_obf(t, alpha=0.05):
"""O'Brien-Fleming alpha spending function
t: 已收集样本比例 (0, 1]
"""
z_alpha = stats.norm.ppf(1 - alpha / 2)
boundary = z_alpha / np.sqrt(t)
return 2 * (1 - stats.norm.cdf(boundary))
# 在不同检查点的累积 alpha 消耗
checkpoints = [0.25, 0.5, 0.75, 1.0]
for t in checkpoints:
spent = alpha_spending_obf(t)
z_boundary = stats.norm.ppf(1 - spent / 2)
print(f"信息比例 {t:.0%}: z-boundary = {z_boundary:.3f}, 累积 alpha = {spent:.5f}")多臂老虎机(Multi-Armed Bandit)
MAB 在实验过程中动态调整流量分配,将更多流量导向表现更好的方案,减少"遗憾"(regret)。
class ThompsonSampling:
"""Thompson Sampling 多臂老虎机"""
def __init__(self, n_arms):
self.n_arms = n_arms
# Beta 分布参数 (成功数, 失败数)
self.alpha = np.ones(n_arms)
self.beta_param = np.ones(n_arms)
def select_arm(self):
"""根据后验分布采样选择臂"""
samples = [
np.random.beta(self.alpha[i], self.beta_param[i])
for i in range(self.n_arms)
]
return np.argmax(samples)
def update(self, arm, reward):
"""更新后验分布"""
if reward == 1:
self.alpha[arm] += 1
else:
self.beta_param[arm] += 1
def get_probabilities(self):
"""获取每个臂的估计胜率"""
return [
self.alpha[i] / (self.alpha[i] + self.beta_param[i])
for i in range(self.n_arms)
]
# 使用示例
mab = ThompsonSampling(n_arms=3)
for trial in range(10000):
arm = mab.select_arm()
# 模拟奖励(实际中是真实用户行为)
true_rates = [0.05, 0.055, 0.048]
reward = 1 if np.random.random() < true_rates[arm] else 0
mab.update(arm, reward)
print("估计胜率:", [f"{p:.4f}" for p in mab.get_probabilities()])| 方法 | 优点 | 缺点 |
|---|---|---|
| 固定样本量 A/B | 理论严谨,统计保证强 | 实验结束前不能查看 |
| 序贯检验 | 可提前停止 | 统计调整复杂 |
| 多臂老虎机 | 减少遗憾,动态优化 | 统计推断较弱 |
常见陷阱
Peeking(过早查看)
在样本量不足时频繁查看结果并做决策,会严重膨胀第一类错误率。
# 模拟 peeking 导致的假阳性膨胀
def simulate_peeking(n_simulations=10000, n_checks=20, n_per_check=500, alpha=0.05):
false_positives = 0
for _ in range(n_simulations):
# 两组来自同一分布(H0 为真)
for check in range(1, n_checks + 1):
n = check * n_per_check
control = np.random.binomial(1, 0.05, n)
treatment = np.random.binomial(1, 0.05, n)
_, p_value = stats.ttest_ind(control, treatment)
if p_value < alpha:
false_positives += 1
break
return false_positives / n_simulations
fpr = simulate_peeking()
print(f"Peeking 导致的实际假阳性率: {fpr:.2%}") # 远高于 5%多重检验(Multiple Testing)
同时测试多个指标时,至少一个出现假阳性的概率急剧增加。
# Bonferroni 校正
n_metrics = 10
alpha_original = 0.05
alpha_corrected = alpha_original / n_metrics
print(f"Bonferroni 校正后的 alpha: {alpha_corrected}")
# Benjamini-Hochberg (FDR) 校正
from statsmodels.stats.multitest import multipletests
p_values = [0.001, 0.008, 0.039, 0.041, 0.052, 0.10, 0.15, 0.22, 0.40, 0.85]
rejected, corrected_pvals, _, _ = multipletests(p_values, method='fdr_bh')
for i, (p, cp, r) in enumerate(zip(p_values, corrected_pvals, rejected)):
print(f"指标 {i+1}: p={p:.3f}, corrected_p={cp:.3f}, 显著={r}")其他常见问题
- Simpson 悖论:总体趋势与子群体趋势相反
- Novelty Effect:新方案短期内因新鲜感表现好
- 溢出效应:实验组和控制组相互影响
- Day-of-Week Effect:未跑完整数周导致偏差
总结
A/B 测试的统计学基础包括假设检验、P 值解读、置信区间计算和样本量规划。序贯检验和多臂老虎机提供了传统固定样本量方法的替代方案。避免 peeking、正确处理多重检验、运行足够长的实验周期是获得可靠结论的关键。统计显著性只是决策的输入之一,还需要综合考虑效果大小、业务影响和实施成本。
贡献者
更新日志
2026/3/14 13:09
查看所有更新日志
9f6c2-feat: organize wiki content and refresh site setup于