·
关注 发表在 Towards Data Science ·13 min read·2023 年 1 月 3 日
--
数据科学家工作的一大部分是测量差异。更具体地说,是比较产品或服务的不同版本,并确定哪个表现更好。这是通过随机对照试验 (RCT) 或其更高级的商业名称A/B 测试来完成的。
思路非常简单。我们想要改进一个特定的 KPI,比如结账页面的转化率。然后我们假设某些变化可能对我们的 KPI 产生积极影响。例如,我们可能想测试将结账按钮的颜色从灰色改为绿色。然后,我们将随机分配结账页面的访客,一半看到当前版本(灰色按钮),另一半看到新版本(绿色按钮)。当前版本和新版本的术语分别为控制和处理。作为数据科学家,我们的工作是测量两个版本之间的转化率差异,并确定这是否具有统计显著性或仅仅是运气(随机变异)。
在这个系列中,我将带你全面了解 A/B 测试的整个过程,从设计实验到分析和展示结果。案例研究旨在模拟在现实环境中进行 A/B 测试时遇到的挑战,同时涵盖数据科学面试中常见的关键问题。指南将分为以下两部分:
-
思路: 商业案例研究概述
-
设计测试: 选择目标人群和关注的 KPI
-
测试前分析: 使用功效和模拟来估计样本量
-
EDA: 合理性检查,时间序列可视化
-
分析实验结果: P 值、置信区间、自助法等。
照片由 Riccardo Annandale 提供,来源于 Unsplash
对于我们的案例研究,假设一个电商零售商在英国销售巴西咖啡豆。每周五,公司提供每消费 50 英镑减 10 英镑的优惠。一个产品经理一直在尝试提高促销效果,并通过研究提出了一个想法。如果我们将优惠的描述改为百分比折扣,那么每消费 50 英镑将获得 20%的折扣。思路是 20 比 10 高,所以客户可能会认为同样的优惠具有更高的价值(尽管 20%和 10 英镑折扣是相同的交易)。
这时我们的主角登场了。对这个想法非常兴奋的产品经理安排了一次与公司数据科学家的电话会议,并向他讲解了这个想法。我们的数据科学家对 A/B 测试并不陌生,因为他过去多次帮助业务利益相关者做出这样的决策。于是,我们的旅程开始了。
在进一步进行之前,重要的是明确我们实验的目标人群。在我们的例子中,就是任何在星期五(即优惠活动上线时)访问网站的人。这是一个重要的区别。如果用户在星期一访问了网站,那么将他们纳入实验没有意义,因为他们不会受到优惠活动变化的影响。
按照这个逻辑,你可能还会问是否适合包括所有星期五的访问者。优惠可能并非在所有页面上都有展示,因此只应包括访问了有优惠展示的页面的用户。你说得对。对于我们的案例研究,我们假设优惠是在网站每个页面顶部的横幅(如下所示),以简化问题。
为 A/B 测试设计两个版本 [图片来源:作者]
无论实验的想法是来自 CEO 还是初级分析师,明确 KPIs 的需求总是存在的。但是为什么会这样呢?我们为什么不能先进行实验,然后查看所有可能受到影响的指标?
除了影响效率之外,还有另一个理由需要在开始测试之前定义我们的 KPI。为了估算(关键词)进行准确推断所需的样本量,我们还必须了解 KPI 的方差。一个波动性较大的指标,如平均支出,可能会偏斜,并具有更高的方差和异常值,因此需要比对称正态分布指标更高的样本量。
好的,让我们开始正式工作。对于我们的实验,我们关注两个 KPI。两者都在访客层面,并且与我们期望的新版本的效果有合理的关联。更多的访客兑换优惠(即更高的 ATPV),而不降低他们的整体支出(更高的 ARPV 或至少对 ARPV 没有负面影响)。
每访客平均收入 (ARPV): 总收入 / 总访客数
每访客平均交易数 (ATPV): 总交易数 / 总访客数
你可能已经注意到,周五没有出现在任何一个指标的公式中。这是因为如果周五的访客支出减少会影响到其他日期的销售,从而导致每周或每月支出保持不变,我们并不关心在周五增加访客支出。我们真正关心的是在设定的时间窗口内逐步增加他们的整体交易量和支出。
由于大多数公司每月报告其关键 KPI,我们也将以 28 天为窗口报告我们的两个 KPI(其他公司可能按周或每两周运营,因此 7 天或 14 天的 KPI 可能更合适):
-
每个访客在测试上线后第一次访问网站的周五进入实验。这将是他们的第 1 天。
-
接下来,我们收集接下来的 27 天的数据,从而获得每个访客的 28 天 KPI。
-
因此,28 天 ARPV 将是访客进入测试后的 28 天内收入总和,除以访客数量。
-
仅包括在测试中待满 28 天的访客(不是所有访客,因为我们希望给每个人相同的购买时间窗口)。
-
28 天窗口还确保没有新奇效应(访客对变化最初感到兴奋,但这种兴奋在一两周后很快消退)。
KPI 的 28 天窗口示例 [由作者提供的图片]
这些都是面试中需要覆盖的重要点。展示你对总体情况的理解,以及用户进入测试的漏斗的哪个部分(是所有页面还是特定页面)。最后,始终将其与业务需求联系起来。解释清晰地与利益相关者沟通定义 KPI 的必要性,以及你如何设计实验以使其成为资产!
图片来源于 AbsolutVision 在 Unsplash
这就是令人兴奋的部分开始的地方。我们在这一部分的主要目标是估计所需的样本量,以准确推断我们的两个 KPI。这部分被称为功效分析。功效分析 允许我们在给定以下三个量的情况下估计所需的样本量:
-
效应大小
-
显著性水平 = P(I 型错误)= 发现实际上不存在的效应的概率
-
功效 = 1 — P(II 型错误)= 发现实际上存在的效应的概率
我们将显著性水平设定为 5%,功效设定为 80%(我们将在第二部分中详细讨论这些概念)。然后我们只需要计算效应量来获得所需的样本量。幸运的是,Cohen 1988 为我们提供了在比较两个均值时计算效应量的公式(我还添加了比例的公式,因为它是一个常选的 KPI,例如付款率等)。
Cohen 1988 提供的效应量公式 [作者提供的图片]
但我们仍未脱离困境。我们需要找出当前版本和新版本的均值及其标准差。由于我们不知道我们的 KPI 在未来会如何表现,我们可以查看历史数据以找到这些数字(作为我们对测试上线时期望结果的最佳估计)。因此,我们将回溯两个月,假设我们当时开始了实验。然后我们将计算 28 天 ARPV 和 28 天 ATPV。这为 20000 名访问者提供了数据(这些用户至少在测试中待了 28 天)。
##########################################
# Load Libraries
##########################################
library("ggplot2")
library("dplyr")
library("scales")
library("patchwork")
#############
# ARPV
#############
# Plot
revenue_plot <-
dataset %>%
ggplot(aes(revenue)) +
geom_histogram(fill ="turquoise3", colour = "white", binwidth = 2, boundary = 0) +
scale_x_continuous(breaks = seq(0, max(dataset$revenue), 14) ) +
ylab("No of Visitors") +
xlab("28-day Revenue (each bar is £2)") +
ggtitle("Histogram of 28-day Revenue") +
theme_classic()
# Statistics
dataset %>% select(revenue) %>% summary()
dataset %>% summarise(sqr_rt = sd(revenue))
#############
# ATPV
#############
# Plot
transactions_plot <-
dataset %>%
ggplot(aes(x = transactions)) +
geom_bar(fill ="turquoise3", colour = "white") +
scale_x_continuous(breaks = seq(0, max(dataset$transactions), 1) ) +
ylab("No of Visitors") +
xlab("28-day Transactions") +
ggtitle("Histogram of 28-day Transactions") +
theme_classic()
# Statistics
dataset %>% select(transactions) %>% summary()
dataset %>% summarise(sqr_rt = sd(transactions))
#################
# Output plot
#################
revenue_plot + transactions_plot
历史收入和交易的直方图 [作者提供的图片]
从这些数据中,我们可以初步了解我们两个 KPI 的分布——从上述图表中可以看出,一半的用户(10000 人)在 28 天窗口期内没有进行任何购买(没有收入和交易)。我们还可以计算当前版本的均值(ARPV 和 ATPV)以及效应量公式的标准差。然而,我们仍然缺少新版本的均值数据。由于我们没有关于新版本表现的数据,这一点尚不可知。不过,我们可以声明我们感兴趣的最小可检测效应(MDE),在我们的情况下为 5%的差异。然后我们可以计算新版本的均值为当前版本增加 5%。
##########################################
# Load Libraries
##########################################
library("pwr")
##########################################
# Cohen's power for ARPV
##########################################
std_arpv <- dataset %>% summarise(std = sd(revenue))
arpv_current_vers <- dataset %>% summarise(avg = mean(revenue))
arpv_new_vers <- arpv_current_vers * 1.05
effect_size_arpv <- as.numeric(abs(arpv_current_vers - arpv_new_vers)/std_arpv)
pwr_results_arpv <- pwr.t.test(
d = effect_size_arpv,
sig.level = 0.05,
power = 0.8,
type = c("two.sample")
)
plot(pwr_results_arpv)
pwr_results_arpv
##########################################
#Cohen's power for ATPV
##########################################
std_atpv <- dataset %>% summarise(std = sd(transactions))
atpv_current_vers <- dataset %>% summarise(avg = mean(transactions))
atpv_new_vers <- atpv_current_vers * 1.05
effect_size_atpv <- as.numeric(abs(atpv_current_vers - atpv_new_vers)/std_atpv)
pwr_results_atpv <- pwr.t.test(
d = effect_size_atpv,
sig.level = 0.05,
power = 0.8,
type = c("two.sample")
)
plot(pwr_results_atpv)
pwr_results_atpv
ARPV的功效图 [作者提供的图片]
ATPV的功效图 [作者提供的图片]
从上述内容来看,我们估算 ARPV 和 ATPV 的样本量分别为 6464 和 7279。这些估算指的是控制组和处理组的样本量,因此总共我们需要大约 14000 名访问者的样本。
好奇的读者可能已经注意到,功效计算是基于我们将使用双样本 t 检验进行分析的假设,并向后推导,以给出对于给定效应大小、显著水平和功效所需要的样本量。但该技术有一些假设。特别是,它假设我们的数据大致遵循正态分布(即大致对称且围绕平均值中心)。通过观察直方图,可以清楚地看到营收和交易数据明显不同。这可能仍然可以,因为中心极限定理。我们将在第二部分详细介绍 CLT,但一般来说,具有足够样本量时,违背正态分布并不会导致问题(数据越倾斜和非对称,我们需要的样本量越大)。尽管如此,我还想估计无需假设即可估计所需样本量的方法,即使用非参数方法。
另一种方法是使用历史数据并生成给定大小的 2000 个样本。因此,我们可以从 20000 个观察值中模拟出 100 个大小的 2000 个随机样本。在这些样本中的每一个中,我们然后可以随机将一半用户分配到对照组,另一半用户分配到实验组,并计算指标之间的差异。最后,我们可以在 ARPV 和 ATPV 的差异的 2000 个 95%区间(从第 0.025 到第 0.975 分位数)中可视化,并了解 KPI 的可变性。通过尝试不同的样本量,我们可以找到我们的 MDE 所需的样本量。因此,让我们创建我们将用于模拟样本的函数。
##########################################
# Load Libraries
##########################################
library("caret")
##########################################
# Function for Simulation
##########################################
simulating_sample_size <-
function(dataset, iterations, sample_sizes_vector, kpi) {
n <- iterations
output_df <- data.frame(NULL)
for (j in sample_sizes_vector) {
# create 2,000 samples for sample size j
sampling_df <- data.frame(NULL)
for (i in 1:n) {
sampling_temporary <-
data.frame(kpi = sample(dataset[[kpi]], j, replace = TRUE))
trainIndex <-
createDataPartition(
sampling_temporary$kpi,
p = 0.5,
list = FALSE,
times = 1
)
## split into control and treatment
sampling_df[i, 1] <- mean(sampling_temporary[trainIndex,])
sampling_df[i, 2] <- mean(sampling_temporary[-trainIndex,])
}
# compute aggregates for sample size j
# and union with old entries
output_df <- output_df %>%
union_all(
.,
sampling_df %>%
mutate(diff = round((V2 - V1) / V1, 2)) %>%
summarize(
trim_0.05_diff = round(quantile(diff, c(0.025)),2),
trim_0.95_diff = round(quantile(diff, c(0.975)),2),
mean_0.05_abs = round(mean(V1),2),
mean_0.95_abs = round(mean(V2),2)
) %>%
mutate(iter = j)
)
}
return(output_df)
}
我们现在可以使用我们的函数从我们的历史数据中生成给定大小的样本。我们将使用 500、1000、2000、3000、4000、10000、15000 和 20000 的样本量,并生成每个大小的 2000 个样本。
##########################################
# ARPV simulation
##########################################
simulation_arpv <-
simulating_sample_size (
dataset = dataset,
iterations = 2000,
sample_sizes_vector = c(500,1000,2000,3000,4000, 10000, 15000, 20000),
kpi = "revenue"
)
##########################################
# ATPV simulation
##########################################
simulation_atpv <-
simulating_sample_size (
dataset = dataset,
iterations = 2000,
sample_sizes_vector = c(500,1000,2000,3000,4000, 10000, 15000, 20000),
kpi = "transactions"
)
##########################################
# Plot ARPV from simulations
##########################################
arpv_sim_plot <- simulation_arpv %>%
ggplot(aes(x = as.factor(iter), y = trim_0.05_diff )) +
geom_col(aes(y = trim_0.05_diff ), fill="turquoise3", alpha=0.9,width = 0.5) +
geom_text(aes(label = scales::percent(trim_0.05_diff)), vjust = -0.5, size = 5) +
geom_col(aes(y = trim_0.95_diff ), fill="turquoise3", alpha=0.9,width = 0.5) +
geom_text(
aes(x = as.factor(iter), y = trim_0.95_diff, label = scales::percent(trim_0.95_diff)),
vjust = -0.5, size = 5
) +
scale_y_continuous(labels = scales::percent) +
ylab("% Difference in ARPV") +
xlab("Sample size") +
theme_classic() +
geom_hline(yintercept= 0, linetype="dashed", color = "red")
##########################################
# Plot ATPV from simulations
##########################################
atpv_sim_plot <- simulation_atpv %>%
ggplot(aes(x = as.factor(iter), y = trim_0.05_diff )) +
geom_col(aes(y = trim_0.05_diff ), fill="turquoise3", alpha=0.9,width = 0.5) +
geom_text(aes(label = scales::percent(trim_0.05_diff)), vjust = -0.5, size = 5) +
geom_col(aes(y = trim_0.95_diff ), fill="turquoise3", alpha=0.9,width = 0.5) +
geom_text(
aes(x = as.factor(iter), y = trim_0.95_diff, label = scales::percent(trim_0.95_diff)),
vjust = -0.5, size = 5
) +
scale_y_continuous(labels = scales::percent) +
ylab("% Difference in ATPV") +
xlab("Sample size") +
theme_classic() +
geom_hline(yintercept= 0, linetype="dashed", color = "red")
#################
# Output plot
#################
(arpv_sim_plot + coord_flip()) + (atpv_sim_plot + coord_flip())
模拟不同大小的 2000 个样本中的指标变化[作者提供的图片]
上述结果与我们的功效分析的结果相吻合。看一个样本量为 15000,两个 KPI 的 95%区间在-2%和 2%变化之间。所以如果我们取一个 15000 的样本,并将 7500 分配给对照组和实验组,我们期望在没有差异的情况下,具有四个百分点的变异性(从-2%到 2%)。因此,如果新版本的提升为 5%,我们预计 15000 个样本会返回 3%至 7%的结果(5%-2% = 3%,5% + 2% = 7%)。
通过最后一步,我们已经准备好启动我们的RCT 实验了!让我们总结一下我们的现状:
✅ 我们定义了测试的两个版本
✅ 我们将人口定义为星期五的所有访问者
✅ 一旦访问者进入测试,他们将被随机分成对照组和实验组(50% — 50%)
✅ 我们定义了我们感兴趣的两个 KPI:28 天 ARPV 和 28 天 ATPV
✅ 我们使用功效分析和模拟估算了实验的样本量为 15000(对照组和处理组各 7500)。
在本系列的下一篇文章中,我们将深入探讨如何分析 A/B 测试的结果。如果你想自己动手操作数据,可以查看下面我用来生成 20000 个观察值的数据集的代码。
set.seed(15)
##########################################
# Create normal skewed ARPV attribute
##########################################
sigma = 0.6
mu = 2
delta = 1
samples = 10000
revenue <- rnorm(samples, rlnorm(samples, mu, sigma) , delta)
revenue <- revenue + 40
##########################################
# Create normal symmetric ATPV attribute
##########################################
transactions <- round(rnorm(10000, 2, 0.5),0)
##########################################
# Create data set with both attributes
##########################################
dataset <-
data.frame(revenue, transactions) %>%
# fixing records with 0 or negative atpv but positive arpv
mutate(transactions = case_when(transactions <= 0 & revenue > 0 ~ 1 , TRUE ~ transactions)) %>%
# adding non purchasing visitors
union_all(., data.frame(revenue = rep(0,10000), transactions = rep(0,10000)))
summary(dataset)
如果你喜欢阅读这篇文章并想了解更多,不要忘记订阅以便直接将我的故事发送到你的收件箱。
在下面的链接中,你还可以找到一个免费的 PDF 指南,讲解如何在实际业务场景中使用数据科学技术和最佳实践在 R 中完成客户集群分析。