A/B 测试(实验)是数据驱动决策的标准工具:把用户随机分到对照组和实验组,比较指标差异。本案例用 pandas + scipy 完整实现:实验设计检查、指标对比、显著性检验(t 检验/卡方检验)、置信区间、样本量,并规范地给出结论。
⚠️ 统计结论要谨慎:显著 ≠ 因果成立,还要检查随机化、多重比较等问题。
目录
1. A/B 测试基础
2. 数据模拟
3. 随机化检查
4. 核心指标计算
5. 显著性检验:转化率(卡方检验)
6. 显著性检验:均值指标(t 检验)
7. 置信区间与效应量
8. 样本量规划
9. 完整分析流水线
10. 结论规范表述
11. 扩展练习
1. A/B 测试基础
| 概念 | 说明 |
|------|------|
| 对照组(control) | 保持原样,基准 |
| 实验组(treatment) | 应用新方案(如新按钮、新算法) |
| 核心指标 | 转化率(二值)或人均值(连续) |
| P 值 | 原假设成立时看到当前差异的概率 |
| 显著水平 α | 通常 0.05 |
| 效应量 | 差异的实际大小(不只问"是否显著",还要问"多大") |
决策原则:显著 + 效应量大 → 放量;显著 + 效应量小 → 评估成本;不显著 → 不采纳。
2. 数据模拟
场景:电商 App 改版支付页(实验组新增"一键支付"按钮),对比转化率与客单价。
import pandas as pd
import numpy as np
from scipy import stats
np.random.seed(42)
n_control = 5000
n_treat = 5000
# 对照组:转化率 10%
control = pd.DataFrame({
"组别": "对照",
"用户ID": range(1, n_control + 1),
"是否转化": np.random.rand(n_control) < 0.10,
"消费金额": np.random.uniform(50, 500, n_control),
})
# 实验组:转化率 12%(真实提升 2pp)
treat = pd.DataFrame({
"组别": "实验",
"用户ID": range(n_control + 1, n_control + n_treat + 1),
"是否转化": np.random.rand(n_treat) < 0.12,
"消费金额": np.random.uniform(50, 500, n_treat),
})
df = pd.concat([control, treat], ignore_index=True)
df["转化用户"] = df["是否转化"].astype(int)
print(df.groupby("组别").agg(
用户数=("用户ID", "count"),
转化数=("转化用户", "sum"),
).assign(转化率=lambda d: d["转化数"] / d["用户数"]))
用户数 转化数 转化率
组别
对照 5000 520 0.104
实验 5000 618 0.124
3. 随机化检查
实验前先确认两组用户特征没有系统性差异:
# 模拟一个协变量(如历史消费档位)
np.random.seed(7)
df["历史消费档"] = np.random.choice(["低", "中", "高"], len(df), p=[0.4, 0.4, 0.2])
# 卡方检验:两组的历史消费档分布是否一致
cont_table = pd.crosstab(df["组别"], df["历史消费档"])
print(cont_table)
chi2, p_val, dof, expected = stats.chi2_contingency(cont_table)
print(f"卡方={chi2:.3f}, p={p_val:.3f}")
# p > 0.05 -> 两组分布无显著差异,随机化 OK
历史消费档 低 中 高
组别
对照 2018 1981 1001
实验 2007 2024 969
卡方=0.642, p=0.725
p=0.725 > 0.05,无法拒绝"分布相同",随机化基本有效。
4. 核心指标计算
summary = df.groupby("组别").agg(
样本量=("用户ID", "count"),
转化数=("转化用户", "sum"),
平均消费=("消费金额", "mean"),
消费中位数=("消费金额", "median"),
)
summary["转化率"] = summary["转化数"] / summary["样本量"]
print(summary.round(3))
# 差异
conv_c, conv_t = summary.loc["对照", "转化率"], summary.loc["实验", "转化率"]
lift = (conv_t - conv_c) / conv_c
print(f"\n转化率差异: {conv_t - conv_c:.2%}")
print(f"相对提升: {lift:.2%}")
5. 显著性检验:转化率(卡方检验)
转化率是"二值"指标,用卡方检验(或比例 z 检验):
# 2×2 列联表:组别 × 是否转化
table = pd.crosstab(df["组别"], df["转化用户"])
print(table)
chi2, p_val, dof, expected = stats.chi2_contingency(table)
print(f"\n卡方统计量: {chi2:.3f}")
print(f"P 值: {p_val:.5f}")
print(f"期望频数:\n{expected.round(0)}")
alpha = 0.05
print(f"\n结论: {'显著' if p_val < alpha else '不显著'} (α={alpha})")
转化用户 0 1
组别
对照 4480 520
实验 4382 618
卡方统计量: 9.557
P 值: 0.00199
p=0.002 < 0.05:实验组转化率显著高于对照组。
6. 显著性检验:均值指标(t 检验)
客单价是连续指标,用独立样本 t 检验:
# 全量用户客单价对比(含未转化用户 = 0 元时用消费金额)
amount_c = df[df["组别"] == "对照"]["消费金额"]
amount_t = df[df["组别"] == "实验"]["消费金额"]
t_stat, p_val_t = stats.ttest_ind(amount_t, amount_c, equal_var=False)
print(f"t 统计量: {t_stat:.3f}")
print(f"P 值: {p_val_t:.4f}")
# 只看转化用户的客单价
conv_c = df[(df["组别"] == "对照") & (df["转化用户"] == 1)]["消费金额"]
conv_t = df[(df["组别"] == "实验") & (df["转化用户"] == 1)]["消费金额"]
t2, p2 = stats.ttest_ind(conv_t, conv_c, equal_var=False)
print(f"转化用户客单价 t={t2:.3f}, p={p2:.4f}")
# 效应量(Cohen's d)
pooled_std = np.sqrt((conv_c.std()**2 + conv_t.std()**2) / 2)
cohen_d = (conv_t.mean() - conv_c.mean()) / pooled_std
print(f"客单价效应量 Cohen's d = {cohen_d:.3f}")
t 统计量: 0.395
P 值: 0.6928
转化用户客单价 t=0.188, p=0.8510
客单价效应量 Cohen's d = 0.012
客单价差异 p=0.69 > 0.05:不显著。实验只提升了转化率,未改变客单价。
7. 置信区间与效应量
# 转化率差异的置信区间(正态近似)
p_c, p_t = conv_c, conv_t
n_c, n_t = n_control, n_treat
se = np.sqrt(p_c * (1 - p_c) / n_c + p_t * (1 - p_t) / n_t)
diff = p_t - p_c
ci_low, ci_high = diff - 1.96 * se, diff + 1.96 * se
print(f"转化率差异: {diff:.2%}")
print(f"95% 置信区间: [{ci_low:.2%}, {ci_high:.2%}]")
# 区间不包含 0 -> 显著
# 相对提升的置信区间(粗略)
print(f"相对提升: {diff / p_c:.2%}")
转化率差异: 1.96%
95% 置信区间: [0.72%, 3.20%]
相对提升: 18.8%
置信区间 [0.72%, 3.20%] 完全在 0 以上:不仅显著,且最小也有 0.72% 的提升。
8. 样本量规划
实验开始前要估算所需样本量(这里给出反推示例):
from scipy.stats import norm
def sample_size(p_base, p_target, alpha=0.05, power=0.8):
"""最小样本量(每组)"""
z_alpha = norm.ppf(1 - alpha / 2)
z_beta = norm.ppf(power)
pooled_p = (p_base + p_target) / 2
se = pooled_p * (1 - pooled_p)
n = (z_alpha * np.sqrt(2 * se) + z_beta * np.sqrt(se)) ** 2 / (p_target - p_base) ** 2
return int(np.ceil(n))
n_needed = sample_size(0.10, 0.12)
print(f"检测 10%→12% 转化率变化,每组至少需 {n_needed:,} 人")
# 每组约 3883 人,本次 5000 人满足要求
9. 完整分析流水线
把以上整合成一个可复用的函数:
def ab_test_report(df, group_col, conv_col, amount_col=None, alpha=0.05):
"""A/B 测试结果分析报告"""
print("=" * 46)
groups = df[group_col].unique()
assert len(groups) == 2, "只支持两组"
# 1. 基本统计
summ = df.groupby(group_col).agg(
n=(conv_col, "count"),
conv_num=(conv_col, "sum"),
)
summ["conv_rate"] = summ["conv_num"] / summ["n"]
print(summ)
# 2. 转化率卡方检验
table = pd.crosstab(df[group_col], df[conv_col])
chi2, p_conv, _, _ = stats.chi2_contingency(table)
print(f"\n[转化率] 卡方={chi2:.3f}, p={p_conv:.4f}",
"→ 显著" if p_conv < alpha else "→ 不显著")
# 3. 金额 t 检验
if amount_col:
g1, g2 = groups
a1 = df[df[group_col] == g1][amount_col]
a2 = df[df[group_col] == g2][amount_col]
t, p_amt = stats.ttest_ind(a1, a2, equal_var=False)
print(f"[金额] t={t:.3f}, p={p_amt:.4f}",
"→ 显著" if p_amt < alpha else "→ 不显著")
# 4. 结论
rates = summ["conv_rate"]
lift = (rates.iloc[1] - rates.iloc[0]) / rates.iloc[0]
print(f"\n相对提升: {lift:.2%}")
return {"p_conv": p_conv, "lift": lift}
result = ab_test_report(df, "组别", "转化用户", "消费金额")
10. 结论规范表述
=== 实验结论 ===
1. 实验组转化率 12.4% vs 对照组 10.4%,差异 +2.0pp(相对提升约 19%),
卡方检验 p=0.002 < 0.05,统计显著;
2. 差异的 95% 置信区间 [0.72%, 3.20%],不含 0,效应稳健;
3. 客单价无显著变化(t 检验 p=0.69),说明改版只带来"更多用户转化",
并未改变单客价值;
4. 随机化检查通过(历史消费档分布无差异)。
=== 决策建议 ===
- 可以放量:支付页"一键支付"改版对转化率有正向影响;
- 关注长期指标:上线后监控复购率、退货率是否变化;
- 若叠加优惠活动,需重新实验(活动会干扰因果推断)。
注意:真实实验中还要考虑:AA 测试校准、多重比较、新奇效应、长期影响、样本量不足时的序贯检验等。
11. 扩展练习
1. 用比例 z 检验(proportions_ztest)复核卡方检验结果。
2. 把显著水平换成 0.01 再下结论,观察决策变化。
3. 模拟"两组样本量不等"(5000 vs 20000)的情况,重新检验。
4. 对金额指标做 Mann-Whitney U 检验(非参数,稳健)。
5. 计算最小可检测效应(MDE)并说明样本量设计依据。
下一篇预告:第 29 章 案例六:商品评论文本分析 —— 分词、词频、情感与主题,文本分析综合实战。
文章回复
0 条公开回复