Machine Learning: A/B测试与实验设计 — 统计学驱动的在线实验指南
没有A/B测试的模型上线就像没有安全网的走钢丝——你不知道它真的更好还是只是运气。
1. 你将学到
- A/B测试原理:假设检验、p-value、置信区间、统计功效(Power)
- 实验设计:样本量计算、实验周期确定、分流策略
- ML模型A/B测试:新模型vs旧模型的在线对比,业务指标评估
- 多臂老虎机(MAB):Thompson Sampling/UCB,探索与利用平衡
- Alice的SaaS场景:新推荐模型A/B测试验证转化率提升
2. 一个产品经理的真实故事
(1) 痛点:新模型上线后不确定是否真的更好
Alice的SaaS平台上线了新的推荐模型,后台数据显示转化率从3.2%升到3.5%。但CEO质疑:"这可能只是自然波动,你怎么证明是模型带来的?"Alice无法回答——没有对照实验,任何提升都无法归因。
(2) A/B测试的解法
A/B测试将用户随机分为A组(旧模型)和B组(新模型),同时运行,用统计学判断差异是否显著。
PYTHON
from scipy import stats
# Group A: old model, Group B: new model
conversions_a, n_a = 320, 10000 # 3.2%
conversions_b, n_b = 350, 10000 # 3.5%
z_stat, p_value = stats.proportions_ztest(
[conversions_a, conversions_b], [n_a, n_b]
)
print(f"p-value: {p_value:.4f}")
print(f"Significant: {p_value < 0.05}")
(3) 收益:3周实验证明转化率提升2.3%,年增收50万USD
Alice用3周A/B测试科学验证了新模型的提升——p=0.03,统计显著。基于此信心全量上线,年增收约500 thousand USD。
3. A/B测试统计学基础
(1) 假设检验框架
sequenceDiagram
participant Design as Design Experiment
participant Split as Random Split
participant Collect as Collect Data
participant Test as Statistical Test
participant Decision as Decision
Design->>Split: Define H0/H1, sample size
Split->>Collect: Group A (control) vs Group B (treatment)
Collect->>Test: After experiment period
Test->>Decision: p-value calculation
Decision->>Decision: p < α → Reject H0 (B is better)
Decision->>Decision: p ≥ α → Cannot reject H0
(2) 核心统计概念
▶ 示例:假设检验计算
PYTHON
from scipy import stats
import numpy as np
# Two-proportion z-test
def ab_test_proportions(conversions_a, n_a, conversions_b, n_b, alpha=0.05):
p_a = conversions_a / n_a
p_b = conversions_b / n_b
p_pool = (conversions_a + conversions_b) / (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 = 1 - stats.norm.cdf(z_stat)
ci_low = (p_b - p_a) - 1.96 * se
ci_high = (p_b - p_a) + 1.96 * se
return {
"conversion_a": f"{p_a:.4f}",
"conversion_b": f"{p_b:.4f}",
"lift": f"{(p_b - p_a) / p_a * 100:.2f}%",
"z_stat": f"{z_stat:.3f}",
"p_value": f"{p_value:.4f}",
"ci_95": f"({ci_low:.4f}, {ci_high:.4f})",
"significant": p_value < alpha,
}
result = ab_test_proportions(320, 10000, 350, 10000)
for k, v in result.items():
print(f"{k:15s}: {v}")
输出:
TEXT
📖 仅展示
# 函数定义成功
| 概念 | 含义 | 典型阈值 |
|---|---|---|
| H0(零假设) | A和B没有差异 | — |
| H1(备择假设) | B优于A | — |
| α(显著性水平) | 犯第一类错误(假阳性)概率 | 0.05 |
| p-value | 在H0下观察到当前或更极端结果的概率 | <0.05 |
| Power(功效) | 正确拒绝H0的概率(1-β) | 0.8 |
| 置信区间 | 真实差异的可能范围 | 95% CI |
4. 实验设计
(1) 样本量计算
▶ 示例:计算A/B测试所需样本量
PYTHON
from statsmodels.stats.power import zt_ind_solve_power
from statsmodels.stats.proportion import proportion_effectsize
import numpy as np
def calculate_sample_size(p_baseline, mde, alpha=0.05, power=0.8):
"""Calculate required sample size per group.
Args:
p_baseline: Baseline conversion rate
mde: Minimum Detectable Effect (relative lift)
alpha: Significance level
power: Statistical power
"""
p_new = p_baseline * (1 + mde)
effect_size = proportion_effectsize(p_new, p_baseline)
n = zt_ind_solve_power(effect_size=effect_size, alpha=alpha, power=power)
return int(np.ceil(n))
# Alice's SaaS: baseline 3.2%, wants to detect 10% relative lift
baseline = 0.032
for mde in [0.05, 0.10, 0.15, 0.20]:
n = calculate_sample_size(baseline, mde)
print(f"MDE={mde*100:.0f}%: Need {n:,} users per group "
f"(total {n*2:,}, ~{n*2/baseline/1000:.0f}k visitors)")
输出:
TEXT
📖 仅展示
# 函数定义成功
| MDE(最小可检测效应) | 每组样本量 | 总样本量 | 实验周期(50k日活) |
|---|---|---|---|
| 5%相对提升 | 152,000 | 304,000 | 6天 |
| 10%相对提升 | 38,000 | 76,000 | 1.5天 |
| 15%相对提升 | 17,000 | 34,000 | <1天 |
| 20%相对提升 | 9,600 | 19,200 | <1天 |
📌 重点: MDE越小(你想检测的提升越小),需要的样本量越大。如果Baseline只有3.2%,要检测5%的相对提升(即3.2%→3.36%),需要超过30万用户。
(2) 分流策略
| 策略 | 方式 | 优点 | 缺点 |
|---|---|---|---|
| 用户级 | 按user_id hash | 用户体验一致 | 需要登录 |
| 请求级 | 按request_id | 简单 | 同用户可能看到不同结果 |
| 设备级 | 按device_id | 覆盖未登录用户 | 多设备用户可能冲突 |
▶ 示例:一致性哈希分流
PYTHON
import hashlib
def assign_group(user_id, salt="experiment_1", num_groups=2):
"""Consistent hash assignment for A/B testing."""
hash_input = f"{salt}_{user_id}".encode()
hash_val = int(hashlib.md5(hash_input).hexdigest(), 16)
return hash_val % num_groups # 0=A, 1=B
# Verify equal split
user_ids = range(100000)
groups = [assign_group(uid) for uid in user_ids]
print(f"Group A: {groups.count(0)} ({groups.count(0)/len(groups)*100:.1f}%)")
print(f"Group B: {groups.count(1)} ({groups.count(1)/len(groups)*100:.1f}%)")
输出:
TEXT
📖 仅展示
# 函数定义成功
5. ML模型A/B测试
(1) 在线模型对比
▶ 示例:Alice的推荐模型A/B测试
PYTHON
import numpy as np
from scipy import stats
rng = np.random.default_rng(42)
# Simulate 2-week A/B test for recommendation model
n_users = 20000 # Per group
# Group A: Old model (baseline conversion 3.2%)
conversions_a = rng.binomial(1, 0.032, n_users)
revenue_a = conversions_a * rng.exponential(200, n_users) # Avg 200 USD per conversion
# Group B: New model (true conversion 3.5%, 9.4% lift)
conversions_b = rng.binomial(1, 0.035, n_users)
revenue_b = conversions_b * rng.exponential(200, n_users)
# Analyze conversion rate difference
conv_rate_a = conversions_a.mean()
conv_rate_b = conversions_b.mean()
z_stat, p_value = stats.proportions_ztest(
[conversions_a.sum(), conversions_b.sum()],
[n_users, n_users]
)
# Analyze revenue difference
rev_per_user_a = revenue_a.mean()
rev_per_user_b = revenue_b.mean()
t_stat, t_pvalue = stats.ttest_ind(revenue_a, revenue_b)
print("=" * 50)
print("A/B TEST RESULTS: Recommendation Model")
print("=" * 50)
print(f"Conversion: A={conv_rate_a:.3%} vs B={conv_rate_b:.3%}")
print(f" Lift: {(conv_rate_b - conv_rate_a) / conv_rate_a * 100:.1f}%")
print(f" p-value: {p_value:.4f} {'✅ Significant' if p_value < 0.05 else '❌ Not significant'}")
print(f"\nRevenue/User: A={rev_per_user_a:.2f} vs B={rev_per_user_b:.2f} USD")
print(f" Lift: {(rev_per_user_b - rev_per_user_a) / rev_per_user_a * 100:.1f}%")
print(f" p-value: {t_pvalue:.4f}")
# Business impact
annual_lift = (rev_per_user_b - rev_per_user_a) * 50000 * 12 # 50k users * 12 months
print(f"\nProjected annual revenue lift: {annual_lift:,.0f} USD")
输出:
TEXT
📖 仅展示
=
A/B TEST RESULTS: Recommendation Model
=
(2) A/B测试常见陷阱
| 陷阱 | 说明 | 解决方案 |
|---|---|---|
| 偷看结果 | 未到期就判定显著 | 固定实验周期,到期才分析 |
| 多重检验 | 测试多个指标/多组 | Bonferroni校正 |
| 新奇效应 | 新UI初期效果好 | 延长实验周期 |
| 污染 | A组用户看到B的内容 | 用户级分流+日志验证 |
| 样本量不足 | 未达到统计功效 | 提前计算样本量 |
6. 多臂老虎机(MAB)
(1) 探索与利用平衡
传统A/B测试固定流量分配(50/50),MAB动态调整——表现好的模型获得更多流量。
▶ 示例:Thompson Sampling
PYTHON
import numpy as np
rng = np.random.default_rng(42)
# 3 model variants with true conversion rates
true_rates = [0.030, 0.035, 0.028] # Model B is best
n_arms = len(true_rates)
n_rounds = 10000
# Thompson Sampling
successes = np.zeros(n_arms)
trials = np.zeros(n_arms)
total_reward = 0
for t in range(n_rounds):
# Sample from Beta distribution for each arm
samples = [rng.beta(successes[i] + 1, trials[i] - successes[i] + 1) for i in range(n_arms)]
chosen = np.argmax(samples)
# Simulate user interaction
reward = rng.binomial(1, true_rates[chosen])
successes[chosen] += reward
trials[chosen] += 1
total_reward += reward
print(f"Thompson Sampling after {n_rounds} rounds:")
for i in range(n_arms):
print(f" Model {i}: chosen {int(trials[i]):5d} times, "
f"observed rate={successes[i]/max(trials[i],1):.4f} (true: {true_rates[i]:.4f})")
print(f"Total conversions: {int(total_reward)}")
输出:
TEXT
📖 仅展示
# 执行成功
| MAB算法 | 策略 | 探索方式 | 复杂度 |
|---|---|---|---|
| ε-Greedy | 以ε概率随机探索 | 固定比例 | 低 |
| UCB | 选置信上界最大的臂 | 不确定性驱动 | 中 |
| Thompson Sampling | 从后验采样选臂 | 概率驱动 | 中 |
| 维度 | A/B测试 | MAB |
|---|---|---|
| 流量分配 | 固定(50/50) | 动态调整 |
| 探索成本 | 高(50%给差的) | 低(自适应) |
| 统计严谨性 | 高(假设检验) | 低(渐进) |
| 适用场景 | 需要统计证明 | 快速迭代 |
❓ 常见问题
Q A/B测试要跑多久?
A 至少2周(覆盖完整周期效应如周末差异),且达到预定样本量。两者取较长者。不要提前终止(即使p<0.05)。
Q p-value < 0.05就够了吗?
A 不够。还需要——1) 统计功效(power ≥ 0.8);2) 足够的样本量;3) 业务意义(lift是否值得投入);4) 周期效应排除。
Q A/B测试和B/A测试结果矛盾怎么办?
A 检查——1) 是否有时间段效应;2) 分流是否均匀;3) 是否存在交互效应(新模型对某些用户好、对某些差)。用分层分析深入排查。
Q MAB能替代A/B测试吗?
A 不能完全替代。MAB适合快速探索(如推荐排序),A/B测试适合需要统计证明的决策(如定价策略、模型上线)。两者互补。
Q 如何处理A/A测试(两组都用旧模型)?
A A/A测试验证分流系统——如果A/A测试出现显著差异,说明分流有问题。建议每次A/B测试前先跑A/A验证。
Q 多个指标同时测试怎么办?
A 用Bonferroni校正——如果有k个指标,显著性水平用α/k。或者预先指定1个主要指标(Primary Metric),其余为辅助指标。
📖 小节
- A/B测试核心:随机分流 + 同时运行 + 统计检验,科学归因提升
- 统计三要素:α(假阳性率) < 0.05, Power(功效) ≥ 0.8, 样本量达标
- 样本量取决于baseline转化率和MDE(最小可检测效应)——检测越小提升需要越多样本
- 分流策略:用户级(推荐) > 设备级 > 请求级,一致性哈希确保稳定
- ML模型A/B测试:对比转化率/Revenue等业务指标,而非模型指标(AUC/R²)
- MAB动态分配流量,减少探索成本,适合快速迭代场景
📝 作业
- 基础题(难度⭐):用
proportions_ztest检验两组转化率(3.0% vs 3.5%,各10k用户)是否有显著差异,计算p-value和95%置信区间。提示:stats.proportions_ztest。 - 进阶题(难度⭐⭐):计算不同MDE下的样本量,绘制"MDE vs 样本量"曲线图。提示:
zt_ind_solve_power+ matplotlib。 - 挑战题(难度⭐⭐⭐):实现Thompson Sampling模拟器——3个臂各有不同真实转化率,跑10000轮后对比Thompson Sampling与ε-Greedy的总奖励和最优臂选择率。提示:Beta分布采样 + 随机探索对比。