未记录事实核验 · 状态:现行内容

记录仅说明核验时间与维护状态,不保证适用于所有环境;使用前请留意正文中的版本、适用条件与风险提示。

warning 适用边界与维护说明

本文适用于学习R中的样本量估计、假设检验、多重比较校正、Beta-Binomial后验模拟及报告模板复用,不是完整的生产实验决策系统。各示例使用模拟或设定数据,报告图中的转化率对比与累计趋势来自独立模拟;94.2%与3.7%仅为假设性报告措辞。单侧检验方向须在观察结果前确定,分析以有效随机分流和独立观测为前提;上线决策仍需结合业务效应、置信区间、护栏指标与预定停止规则。正文未提供SRM检查实现,相关待创建文档不属于完成所列模拟练习的必要材料。贝叶斯概率须结合所用模型和先验解释。本次仅作全文结构审阅,未运行代码或进行外部事实、包版本及链接可达性核验。 已取消无法确定目标的可点击引用「待创建:实验平台设计与SRM检验」,保留原提及;Missing link target: 待创建:实验平台设计与SRM检验

R语言在A/B测试与实验分析中的应用

abstract 学习目标

  • 掌握A/B测试的完整统计分析流程(从样本量计算到结论输出)
  • 理解多重比较问题及其校正方法
  • 能够用贝叶斯方法做A/B测试并给出概率性结论
  • 具备编写可复用A/B测试分析模板的能力

quote 核心洞察

频率派A/B测试可用假设检验、效应量和置信区间描述差异;贝叶斯A/B测试可在指定模型和先验下估计"B比A好的后验概率"。前者不应只简化为显著或不显著,后者也不等于上线风险已被完整量化。当业务方问"新版本有多大把握比旧版本好"时,可报告后验概率,并同时说明模型、先验及业务效应。


一、A/B测试的统计基础

1.1 核心概念速查

概念 含义 业务含义
零假设 H0 两组无差异 新版本和旧版本效果一样
备择假设 H1 两组有差异 新版本效果不同于旧版本
显著性水平 alpha 犯第一类错误的概率(通常0.05) 误报率:H0为真但拒绝H0的概率
统计功效 power 在指定备择效应下正确拒绝H0的概率(常设为0.8) 检出真实效应的概率;漏报率为1-power,即第二类错误概率beta
p值 在H0及检验模型假设成立时,得到至少与当前统计量一样极端结果的概率 在预先设定alpha=0.05且检验程序适用时,p < 0.05 则拒绝H0
效应量 effect size 两组差异的实际大小 转化率从3%提升到3.1%,效应量很小

1.2 第一类错误与第二类错误

# 可视化两类错误的关系
library(ggplot2)
library(patchwork)

x <- seq(-4, 4, 0.01)
null_dist <- dnorm(x, 0, 1)
alt_dist  <- dnorm(x, 1.5, 1)  # 效应量1.5

df <- data.frame(
  x = rep(x, 2),
  y = c(null_dist, alt_dist),
  dist = rep(c("H0(零假设)", "H1(备择假设)"), each = length(x))
)

ggplot(df, aes(x, y, color = dist)) +
  geom_line(size = 1.2) +
  geom_vline(xintercept = 1.645, linetype = "dashed", color = "red", size = 1) +
  annotate("text", x = 1.645, y = 0.42, label = "临界值(alpha=0.05)", hjust = 1.1, color = "red") +
  annotate("rect", xmin = 1.645, xmax = 4, ymin = 0, ymax = Inf,
           fill = "red", alpha = 0.1) +
  annotate("rect", xmin = -4, xmax = 1.645, ymin = 0, ymax = Inf,
           fill = "blue", alpha = 0.05) +
  annotate("text", x = 0.5, y = 0.05, label = "beta (第二类错误)", color = "blue") +
  annotate("text", x = 2.5, y = 0.05, label = "alpha (第一类错误)", color = "red") +
  scale_color_manual(values = c("#2196F3", "#E91E63")) +
  labs(title = "两类错误示意图", x = "统计量", y = "概率密度", color = "分布") +
  theme_minimal()

二、样本量计算:实验前的必修课

在启动A/B测试前,必须先确定需要多少样本量,否则实验可能:

  • 样本太少:有真实效果但检测不出(功效不足)
  • 样本太多:浪费资源,且增加线上风险

2.1 连续变量的样本量计算(如平均订单金额)

# power.t.test:内置函数
power.t.test(
  delta = 5,       # 期望检测到的差异(元)
  sd = 50,         # 历史标准差
  sig.level = 0.05,  # alpha
  power = 0.8,     # 统计功效
  type = "two.sample",
  alternative = "two.sided"
)
# 输出:每组需要的样本量 n

# 用pwr包做更多类型的功效分析
library(pwr)

# 二比例检验(如转化率)
pwr.2p.test(
  h = ES.h(0.035, 0.03),  # ES.h计算比例差异的效应量(Cohen's h)
  sig.level = 0.05,
  power = 0.8,
  alternative = "greater"  # 单侧检验(只关心B是否优于A)
)

2.2 样本量敏感性分析

library(tidyverse)

# 不同效应量需要的样本量
delta_range <- seq(1, 20, by = 0.5)
n_required <- map_dbl(delta_range, function(d) {
  power.t.test(delta = d, sd = 50, sig.level = 0.05, power = 0.8)$n
})

tibble(delta = delta_range, n = ceiling(n_required)) %>%
  ggplot(aes(x = delta, y = n)) +
  geom_line(color = "#2196F3", size = 1.2) +
  geom_point(size = 2, color = "#E91E63") +
  scale_y_log10(labels = scales::comma) +
  labs(
    title = "效应量 vs 所需样本量",
    subtitle = "alpha=0.05, power=0.8, SD=50",
    x = "期望检测差异(元)",
    y = "每组所需样本量(对数轴)"
  ) +
  theme_minimal()

三、假设检验的R实现

3.1 独立样本t检验(连续变量)

set.seed(42)
# 模拟A/B实验数据
control <- rnorm(500, mean = 200, sd = 50)  # 对照组:平均订单200元
treatment <- rnorm(500, mean = 212, sd = 52)  # 实验组:平均订单212元

# Welch's t检验(不要求方差相等)
result_t <- t.test(treatment, control, alternative = "greater")

cat("=== t检验结果 ===\n")
cat("对照组均值:", round(mean(control), 2), "\n")
cat("实验组均值:", round(mean(treatment), 2), "\n")
cat("差异:", round(mean(treatment) - mean(control), 2), "\n")
cat("p值:", round(result_t$p.value, 4), "\n")
cat("95%置信区间:", round(result_t$conf.int, 2), "\n")
cat("结论:", ifelse(result_t$p.value < 0.05, "拒绝H0,差异显著", "不拒绝H0,差异不显著"), "\n")

3.2 比例检验(转化率)

# 场景:改版按钮颜色,测试转化率是否提升
n_control   <- 5000  # 对照组用户数
n_treatment <- 5000  # 实验组用户数
x_control   <- 150   # 对照组转化人数(转化率3%)
x_treatment <- 185   # 实验组转化人数(转化率3.7%)

result_prop <- prop.test(
  x = c(x_treatment, x_control),
  n = c(n_treatment, n_control),
  alternative = "greater",  # 单侧:实验组>对照组
  correct = FALSE            # 不做连续性校正(大样本下影响小)
)

# 结果解读
cat("对照组转化率:", scales::percent(x_control / n_control), "\n")
cat("实验组转化率:", scales::percent(x_treatment / n_treatment), "\n")
cat("相对提升:", scales::percent((x_treatment/n_treatment - x_control/n_control)/(x_control/n_control)), "\n")
cat("p值:", round(result_prop$p.value, 4), "\n")

3.3 卡方检验(多类别变量)

# 场景:测试不同推送文案对用户行为的影响
behavior_matrix <- matrix(
  c(300, 150, 50,    # 方案A:点击/加购/购买
    280, 180, 80,    # 方案B
    320, 130, 60),   # 方案C
  nrow = 3, byrow = TRUE,
  dimnames = list(
    c("方案A", "方案B", "方案C"),
    c("只点击", "加购", "购买")
  )
)

result_chisq <- chisq.test(behavior_matrix)
cat("卡方统计量:", round(result_chisq$statistic, 3), "\n")
cat("p值:", round(result_chisq$p.value, 4), "\n")

# 标准化残差(找出差异最大的单元格)
result_chisq$stdres

四、多重比较:同时测试多个变体

4.1 为什么多重比较是陷阱

# 模拟:同时做20个A/A测试(零假设成立时仍可能出现p值小于0.05)
set.seed(123)
p_values <- replicate(20, {
  t.test(rnorm(100), rnorm(100))$p.value
})

# 在检验假设成立时,单次检验的第一类错误概率约为5%,本次模拟的数量会随机波动
cat("20次测试中显著的数量:", sum(p_values < 0.05), "\n")  # 期望为1个假阳性,不代表每次模拟恰有1个
cat("Familywise错误率:", 1 - (1-0.05)^20, "\n")  # 约64%

4.2 多重比较校正方法

# 示例:同时测试5个按钮颜色的转化率
set.seed(42)
p_values_raw <- c(0.032, 0.041, 0.068, 0.002, 0.158)

results_correction <- data.frame(
  方案     = paste0("方案", LETTERS[1:5]),
  原始p值  = p_values_raw,
  # Bonferroni(最保守):p_adjusted = p * n_tests
  Bonferroni = p.adjust(p_values_raw, method = "bonferroni"),
  # BH(Benjamini-Hochberg):控制FDR,比Bonferroni宽松
  BH = p.adjust(p_values_raw, method = "BH"),
  # Holm(比Bonferroni略宽松,仍控制FWER)
  Holm = p.adjust(p_values_raw, method = "holm")
)

results_correction %>%
  mutate(across(where(is.numeric), ~ round(.x, 4)))

选择建议:

  • 若需控制至少一次误报的概率(FWER),可考虑Bonferroni或Holm,并结合预先指定的检验族与分析方案。
  • 若目标是控制被拒绝假设中的预期错误发现比例(FDR),可在满足相应依赖条件时考虑BH;不能仅按行业选择校正方法。

五、贝叶斯A/B测试:给老板看的概率语言

频率派结果:"p=0.03,差异显著"(很多人不理解这是什么意思) 贝叶斯报告措辞示例(假设性数字,非下方代码的计算结果):"在指定模型和先验下,B比A好的后验概率是94.2%,期望相对提升3.7%"。

library(bayesAB)  # 贝叶斯A/B测试专用包

# 二项分布(转化率场景)
AB_test_result <- bayesAB::bayesTest(
  A_data = rbinom(n_control, 1, 0.03),    # 对照组:3%转化率
  B_data = rbinom(n_treatment, 1, 0.037), # 实验组:3.7%转化率
  priors = c("alpha" = 1, "beta" = 1),    # Beta(1,1)均匀先验
  distribution = "bernoulli"
)

summary(AB_test_result)
# 关键输出:P(B > A) = ?  期望提升 = ?

plot(AB_test_result)  # 后验分布可视化

手动实现贝叶斯A/B测试(不依赖包):

# Beta-Binomial贝叶斯更新
bayes_ab_test <- function(alpha_A, beta_A, alpha_B, beta_B, n_samples = 100000) {
  # 从后验分布中采样
  samples_A <- rbeta(n_samples, alpha_A, beta_A)
  samples_B <- rbeta(n_samples, alpha_B, beta_B)

  prob_B_better <- mean(samples_B > samples_A)
  expected_lift <- mean((samples_B - samples_A) / samples_A)

  # 可视化
  df_plot <- data.frame(
    value = c(samples_A[seq_len(min(10000, n_samples))], samples_B[seq_len(min(10000, n_samples))]),
    group = rep(c("对照组A", "实验组B"), each = min(10000, n_samples))
  )

  p <- ggplot(df_plot, aes(x = value, fill = group)) +
    geom_density(alpha = 0.5) +
    scale_fill_manual(values = c("#2196F3", "#E91E63")) +
    labs(
      title = "贝叶斯A/B测试:后验分布",
      subtitle = paste0("P(B > A) = ", scales::percent(prob_B_better, 0.1),
                       "  |  期望相对提升 = ", scales::percent(expected_lift, 0.1)),
      x = "转化率", y = "概率密度", fill = "分组"
    ) +
    theme_minimal()

  list(
    prob_B_better = prob_B_better,
    expected_lift = expected_lift,
    plot = p
  )
}

# 使用(后验参数 = 先验 + 观测数据)
# Alpha = 成功次数 + 先验alpha,Beta = 失败次数 + 先验beta
result <- bayes_ab_test(
  alpha_A = 1 + x_control,    # 1 + 150
  beta_A  = 1 + (n_control - x_control),  # 1 + 4850
  alpha_B = 1 + x_treatment,  # 1 + 185
  beta_B  = 1 + (n_treatment - x_treatment)
)

cat("B比A好的概率:", scales::percent(result$prob_B_better), "\n")
cat("期望相对提升:", scales::percent(result$expected_lift), "\n")
result$plot

六、A/B测试报告的标准图表

library(tidyverse)
library(patchwork)

# 模拟实验数据
set.seed(42)
experiment_data <- tibble(
  group = rep(c("对照组A", "实验组B"), each = 1000),
  converted = c(rbinom(1000, 1, 0.03), rbinom(1000, 1, 0.037))
)

# 图1:转化率对比(含置信区间)
conv_summary <- experiment_data %>%
  group_by(group) %>%
  summarise(
    n = n(),
    x = sum(converted),
    rate = x / n,
    # prop.test默认返回含连续性校正的得分置信区间;若需未校正的Wilson区间,应设置correct = FALSE
    ci_low  = prop.test(x, n)$conf.int[1],
    ci_high = prop.test(x, n)$conf.int[2]
  )

p1 <- ggplot(conv_summary, aes(x = group, y = rate, fill = group)) +
  geom_col(width = 0.5, alpha = 0.8) +
  geom_errorbar(aes(ymin = ci_low, ymax = ci_high), width = 0.15, size = 1) +
  geom_text(aes(label = scales::percent(rate, 0.1)), vjust = -0.5, fontface = "bold") +
  scale_y_continuous(labels = scales::percent, limits = c(0, 0.06)) +
  scale_fill_manual(values = c("#2196F3", "#E91E63")) +
  labs(title = "A) 转化率对比(含95%置信区间)", x = NULL, y = "转化率") +
  theme_minimal() +
  theme(legend.position = "none")

# 图2:累计转化率随时间变化(漏斗监控)
daily_data <- tibble(
  day = 1:14,
  rate_A = cumsum(rbinom(14, 100, 0.03)) / cumsum(rep(100, 14)),
  rate_B = cumsum(rbinom(14, 100, 0.037)) / cumsum(rep(100, 14))
) %>%
  pivot_longer(c(rate_A, rate_B), names_to = "group", values_to = "rate") %>%
  mutate(group = ifelse(group == "rate_A", "对照组A", "实验组B"))

p2 <- ggplot(daily_data, aes(x = day, y = rate, color = group)) +
  geom_line(size = 1.2) +
  geom_point(size = 2) +
  scale_y_continuous(labels = scales::percent) +
  scale_color_manual(values = c("#2196F3", "#E91E63")) +
  labs(title = "B) 累计转化率趋势", x = "实验天数", y = "累计转化率", color = "分组") +
  theme_minimal()

p1 + p2 + plot_annotation(
  title = "A/B测试结果报告",
  subtitle = "独立模拟数据:转化率对比各1000用户;趋势为14天、每天各100用户"
)

七、完整代码框架:从业务问题到统计结论

# ============================================
# A/B测试完整分析框架
# ============================================
analyze_ab_test <- function(
  control_n, control_convert,
  treatment_n, treatment_convert,
  alpha = 0.05,
  n_bayesian_samples = 100000
) {

  # 1. 基本指标
  cr_control   <- control_convert / control_n
  cr_treatment <- treatment_convert / treatment_n
  lift         <- (cr_treatment - cr_control) / cr_control

  # 2. 频率派检验
  test_result <- prop.test(
    x = c(treatment_convert, control_convert),
    n = c(treatment_n, control_n),
    alternative = "greater"
  )

  # 3. 贝叶斯估计
  samples_A <- rbeta(n_bayesian_samples, 1 + control_convert,   1 + control_n - control_convert)
  samples_B <- rbeta(n_bayesian_samples, 1 + treatment_convert, 1 + treatment_n - treatment_convert)
  prob_B_better <- mean(samples_B > samples_A)

  # 4. 输出报告
  cat("========================================\n")
  cat("           A/B测试分析报告\n")
  cat("========================================\n")
  cat(sprintf("对照组:%d用户,%d转化,转化率 %.2f%%\n", control_n, control_convert, cr_control * 100))
  cat(sprintf("实验组:%d用户,%d转化,转化率 %.2f%%\n", treatment_n, treatment_convert, cr_treatment * 100))
  cat(sprintf("相对提升:%.1f%%\n", lift * 100))
  cat("----------------------------------------\n")
  cat(sprintf("频率派检验 p值:%.4f\n", test_result$p.value))
  cat(sprintf("结论(alpha=%.2f):%s\n", alpha,
    ifelse(test_result$p.value < alpha, "在预设单侧检验下差异显著;是否上线仍需评估业务效应、区间与护栏指标", "未达到预设显著性阈值;按预定停止规则处理,不据此认定无差异或自动延长实验")))
  cat("----------------------------------------\n")
  cat(sprintf("贝叶斯估计:B比A好的概率 = %.1f%%\n", prob_B_better * 100))
  cat("========================================\n")
}

# 使用
analyze_ab_test(5000, 150, 5000, 185)

相关文档

tip 立即可用的技巧

在每次A/B测试报告中,除了p值,一定要同时报告效应量和置信区间。p值衡量在零假设及检验模型假设成立时,得到至少与当前统计量一样极端结果的概率,不能直接判定"有没有差异";效应量描述估计差异的大小,置信区间表达估计的不确定性。三者结合有助于解释统计结果,但业务决策还需结合实验设计、护栏指标与预定停止规则。