Machine Learning: A/B测试与实验设计 — 统计学驱动的在线实验指南

没有A/B测试的模型上线就像没有安全网的走钢丝——你不知道它真的更好还是只是运气。

1. 你将学到


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) 假设检验框架

100%
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),其余为辅助指标。

📖 小节


📝 作业

  1. 基础题(难度⭐):用proportions_ztest检验两组转化率(3.0% vs 3.5%,各10k用户)是否有显著差异,计算p-value和95%置信区间。提示:stats.proportions_ztest
  2. 进阶题(难度⭐⭐):计算不同MDE下的样本量,绘制"MDE vs 样本量"曲线图。提示:zt_ind_solve_power + matplotlib。
  3. 挑战题(难度⭐⭐⭐):实现Thompson Sampling模拟器——3个臂各有不同真实转化率,跑10000轮后对比Thompson Sampling与ε-Greedy的总奖励和最优臂选择率。提示:Beta分布采样 + 随机探索对比。

← 上一课:模型部署 | 下一课:生产监控与模型漂移 →

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏