A/B 测试

既然学习了置信区间和假设检验,在本次案例研究中,你将应用所学知识,帮一家公司决定公司网站要不要推出两个新元素。为此,你需要分析 A/B 测试结果——业内一种相当宝贵且得到广泛应用的方法。

A/B 测试

A/B 测试用于测试网页的修改效果,测试需进行一场实验,实验中对照组为网页旧版本,实验组为网页新版本,实验还需选出一个指标 来衡量每组用户的参与度,然后根据实验结果来判断哪个版本效果更好。从以下假设来看,A/B 测试很像假设检验:

  • 零假设: 新版本不比旧版本好,甚至比旧版本差
  • 对立假设:新版本比旧版本好

如果我们无法推翻零假设,那得到的实验结果就会暗示我们得保留旧版本;如果我们推翻了零假设,那得到的实验结果就会暗示我们可实现网页改动。通过这些测试,我们可以观察什么样的改动能最大化指标,测试适用的改动类型十分广泛,上到增加元素的大改动,下到颜色小变动都可使用这些测试。

但 A/B 测试也有不足之处。虽然测试能帮你比较两种选择,但无法告诉你你还没想到的选择,在对老用户进行测试时,抗拒改变心理、新奇效应等因素都可能使测试结果出现偏差。

  • 抗拒改变心理:老用户可能会因为纯粹不喜欢改变而偏爱旧版本,哪怕从长远来看新版本更好。
  • 新奇效应:老用户可能会觉得变化很新鲜,受变化吸引而偏爱新版本,哪怕从长远看来新版本并无益处。

关于这些因素,稍后你会得到进一步的了解。

商业案例

在本次案例研究中,你将为 某公司 分析 A/B 测试的结果,以下是该公司网站典型新用户的客户漏斗模型:

浏览主页 > 探索课程 > 浏览课程概述页面 > 注册课程 > 完成课程

越深入漏斗模型,公司 就流失了越多的用户,能进入最后阶段的用户寥寥无几。为了提高学员参与度,提高每个阶段之间的转化率,公司试着做出一些改动,并对改动进行了 A/B 测试。

针对 公司 想做的两个改动,我们将分析相关测试结果,并根据结果建议是否该实现那两个改动。

实验 I

公司 想做的第一个改动是在主页上,他们希望靠这个更吸引人的新设计来增加探索课程的用户,也就是说,增加会进入下一漏斗阶段的用户。

我们要使用的指标是主页探索课程按钮的点击率。点击率 (CTR)通常是点击数与浏览数的比例。因为 该公司 有用 cookies,所以我们可以确认单独用户,确保不重复统计同一个用户的点击率。为了进行该实验,我们对点击率作出如下定义:

CTR: # 单独用户点击数 / # 单独用户浏览数

确定了指标,我们可以把零假设和对立假设定义如下。

H_0: CTR_{new} \leq CTR _{old}

H_1: CTR_{new} > CTR _{old}

'对立假设即我们想证明正确的假设,本案例的对立假设就是新主页比旧主页有更高的点击率。零假设则是我们在分析数据前假设为真的假设,即新主页点击率小于或等于旧主页的点击率。正如你之前所见,我们可对假设进行如下处理:

H_0: CTR_{new} - CTR_{old} \leq 0

H_1: CTR_{new} - CTR_{old} > 0

import pandas as pd

df = pd.read_csv('homepage_actions.csv')
df.head()

这个数据集包括 网址主页的浏览和点击行为,显示了控制组和实验组的用户,我们的任务是观察两种版本的性能是否存在巨大差异。首先了解一下数据集情况。我们可以知道数据集包含8188条记录,其中独立用户有6328个,控制组的独立用户有3332个,实验组的独立用户用2996个。

# total number of actions
df.action.shape  8188

# number of unique users
df.id.nunique()   6328

计算唯一值个数可以用df.Series.nunique()

df.groupby('group')['id'].nunique()

# size of control group and experiment group
df.groupby('group')['id'].nunique()['control']  3332

df.groupby('group')['id'].nunique()['experiment']  2996

# duration of this experiment
df.timestamp.max(),df.timestamp.min()

# action types in this experiment
df.action.unique()
array(['view', 'click'], dtype=object)

指标 —— 点击率

我们的任务是观察两种版本的性能是否存在巨大差异。因此计算各组的点击率。

点击率 (CTR)通常是点击数与浏览数的比例。CTR: # 单独用户点击数 / # 单独用户浏览数

1.计算点击率差异

# get actions from control group
control_df = df.query('group == "control"')

# compute click through rate
control_ctr = control_df.query('action == "click"').id.nunique() / control_df.query('action == "view"').id.nunique()

# view click through rate
control_ctr   0.2797118847539016

# get actions from experiment group
experiment_df = df.query('group == "experiment"')

# compute click through rate
experiment_ctr = experiment_df.query('action == "click"').id.nunique() / experiment_df.query('action == "view"').id.nunique()

# view click through rate
experiment_ctr  0.3097463284379172

# compute observed difference in click through raet
obs_diff = experiment_ctr - control_ctr
obs_diff  0.030034443684015644

2.建立点击率差异的抽样分布模型

# simulate sampling distribution for difference in proportions, or CTRs
diffs = []
for _ in range(10000):
    b_samp = df.sample(df.shape[0], replace=True)
    control_df = b_samp.query('group == "control"')
    experiment_df = b_samp.query('group == "experiment"')
    control_ctr = control_df.query('action == "click"').id.nunique() / control_df.query('action == "view"').id.nunique()
    experiment_ctr = experiment_df.query('action == "click"').id.nunique() / experiment_df.query('action == "view"').id.nunique()
    diffs.append(experiment_ctr - control_ctr)

# plot sampling distribution
plt.hist(diffs);

3.用抽样分布模型为零假设建立正态分布模型

# simulate distribution under the null hypothesis
null_vals = np.random.normal(0, np.std(diffs), diffs.size)

# plot null distribution and line at our observed differece
plt.hist(null_vals)
plt.axvline(x=obs_diff, color='red');

4.计算P值

# compute p-value
(null_vals > obs_diff).mean()  0.0053

由于P= 0.0053< \alpha = 5%%,因此我们推翻零假设,建议公司使用新的主页。

我们来复习下分析这个 A/B 测试结果涉及了哪些操作。

  1. 我们计算了对照组和实验组的指标观察差异,即点击率;
  2. 我们为比例差异 (即点击率差异)建立了抽样分布 模型;
  3. 我们用这个抽样分布模型来为 零假设分布 建立模型,也即创建了一个随机正态分布模型,模型以 0 为中心,大小和宽度与抽样分布的一样。
  4. 我们通过找出零假设分布中大于观察差异(样本差异)的那部分比值,从而计算出了 p 值
  5. 我们用 p 值来确定观察差异是否有 统计显著性

实验 II

公司 的第二个改动是让课程概述页面更强调职业发展,他们想对这个改动进行 A/B 测试,希望改动能鼓励更多用户注册并完成课程。本次实验我们将分析如下指标:

  1. 报名率: 课程概述页面 _注册_ 按钮的点击率
  2. 平均浏览时长:用户在课程概述页面停留的平均秒数
  3. 平均课堂逗留时长:课程注册学员在课室逗留的平均天数
  4. 完成率:课程注册学员的课程完成率

我们先来逐一判断各指标的观察差异是否具有统计显著性。

指标——注册率

df = pd.read_csv('course_page_actions.csv')
df.head()

     

# Get dataframe with all records from control group
control_df = df.query('group == "control"')

# Compute click through rate for control group
control_ctr = control_df.query('action == "enroll"').id.nunique() / control_df.query('action == "view"').id.nunique()

# Display click through rate
control_ctr  0.2364438839848676
 
# Get dataframe with all records from experiment group
experiment_df = df.query('group == "experiment"')

# Compute click through rate for experiment group 
experiment_ctr = experiment_df.query('action == "enroll"').id.nunique() / experiment_df.query('action == "view"').id.nunique()

# Display click through rate
experiment_ctr  0.2668693009118541

# Compute the observed difference in click through rates
obs_diff = experiment_ctr - control_ctr

# Display observed difference
obs_diff   0.030425416926986526

# Create a sampling distribution of the difference in proportions
# with bootstrapping
diffs = []
size = df.shape[0]
for _ in range(10000):
    b_samp = df.sample(size, replace=True)
    control_df = b_samp.query('group == "control"')
    experiment_df = b_samp.query('group == "experiment"')
    control_ctr = control_df.query('action == "enroll"').id.nunique() / control_df.query('action == "view"').id.nunique()
    experiment_ctr = experiment_df.query('action == "enroll"').id.nunique() / experiment_df.query('action == "view"').id.nunique()
    diffs.append(experiment_ctr - control_ctr)

# Convert to numpy array
diffs = np.array(diffs)

# Plot sampling distribution
plt.hist(diffs);

# Simulate distribution under the null hypothesis
null_vals = np.random.normal(0,diffs.std(),diffs.size)

# Plot the null distribution
plt.hist(null_vals);
plt.axvline(x=obs_diff,color = 'r');

# Compute p-value
(null_vals > obs_diff).mean()  0.019900000000000001

如果 p 值小于显著性水平,那结果就具有统计显著性。在第一类错误率为 0.05 的情况下,你能否证明在概述页面使用实验描述提高了课程的注册率?能。

指标——平均浏览时长

 每个用户浏览网页的平均时长。

     

# compute average reading durations for each group
control_mean = df.query('group == "control"').duration.mean()
experiment_mean = df.query('group == "experiment"').duration.mean()
control_mean, experiment_mean  (115.40710650582038, 130.9441601154441)

# compute observed difference in means
obs_diff = experiment_mean - control_mean
obs_diff  15.537053609623726
  
# simulate sampling distribution for the difference in means
diffs = []
for _ in range(10000):
    b_samp = df.sample(df.shape[0], replace=True)
    control_mean = b_samp.query('group == "control"').duration.mean()
    experiment_mean = b_samp.query('group == "experiment"').duration.mean()
    diffs.append(experiment_mean - control_mean)

# convert to numpy array
diffs = np.array(diffs)

# simulate the distribution under the null hypothesis
null_vals = np.random.normal(0, diffs.std(), diffs.size)

# plot null distribution and where our observed statistic falls
plt.hist(null_vals)
plt.axvline(x=obs_diff, color='red');

# compute p-value
(null_vals > obs_diff).mean()  0

  指标——平均课室逗留时长                                                                                         

df = pd.read_csv('classroom_actions.csv')
df.head()

         

# get the average classroom time for control group
control_mean = df.query('group == "control"').total_days.mean()

# get the average classroom time for experiment group
experiment_mean = df.query('group == "experiment"').total_days.mean()

# display average classroom time for each group
control_mean, experiment_mean  (73.368990384615387, 74.671593533487297)

# compute observed difference in classroom time
obs_diff = experiment_mean - control_mean

# display observed difference
obs_diff  1.3026031488719099

# create sampling distribution of difference in average classroom times
# with boostrapping
diffs = []
size = df.shape[0]
for _ in range(10000):
    b_samp = df.sample(size, replace=True)
    control_mean = b_samp.query('group == "control"').total_days.mean()
    experiment_mean = b_samp.query('group == "experiment"').total_days.mean()
    diffs.append(experiment_mean - control_mean)

# convert to numpy array
diffs = np.array(diffs)

# simulate distribution under the null hypothesis
null_vals = np.random.normal(0, diffs.std(), diffs.size)

# plot null distribution
plt.hist(null_vals)

# plot line for observed statistic
plt.axvline(obs_diff, c='red')

# compute p value
(null_vals > obs_diff).mean()  0.038399999999999997

指标——完成率

df = pd.read_csv('classroom_actions.csv')
df.head()

    

# Create dataframe with all control records
control_df = df.query('group == "control"')

# Compute completion rate
control_ctr = control_df.completed.mean()

# Display completion rate
control_ctr  0.37199519230769229

# Create dataframe with all experiment records
experiment_df = df.query('group == "experiment"')

# Compute completion rate
experiment_ctr = experiment_df.completed.mean()

# Display completion rate
experiment_ctr  0.39353348729792148

# Compute observed difference in completion rates
obs_diff = experiment_ctr - control_ctr

# Display observed difference in completion rates
obs_diff  0.02153829499022919

# Create sampling distribution for difference in completion rates
# with boostrapping
diffs = []
for _ in range(10000):
    b_sample = df.sample(df.shape[0],replace = True)
    control_ctr = b_sample.query('group == "control"').completed.mean()
    experiment_ctr = b_sample.query('group == "experiment"').completed.mean()
    diffs.append(experiment_ctr - control_ctr)

# convert to numpy array
diffs = np.array(diffs)

# create distribution under the null hypothesis
null_vals = np.random.normal(0,diffs.std(),diffs.size)

# plot null distribution
plt.hist(null_vals);

# plot line for observed statistic
plt.axvline(x = obs_diff,color = 'r' )

# compute p value
(null_vals > obs_diff).mean()  0.084599999999999995

在第一类错误率为 0.05 的情况下,不能证明在课程概述页面使用实验描述提高了课程完成率。

分析多个指标

你评估的指标越多,你观察到显著差异的偶然性就越高——之前涉及多测试的课程也有类似的情况。

一旦指标间有关联,Bonferroni 方案就显得太过保守,因此要更好地解决这个问题,我们可以用更复杂的办法,如封闭测试程序、 Boole-Bonferroni 联合校正 以及 Holm-Bonferroni 方案。这些都没有 Bonferroni 方案那么保守,而且会把指标间的相关性考虑在内。

如果你真的选了没那么保守的方案,请确保方案假设的确适用于你的情况,而不是在 糊弄 p 值。为了得到显著性结果而选择不适合的测试方法只会造成决策有失偏颇,长期下来会伤害到你的公司业绩。

A/B 测试难点

正如你在上述情景看到的,设计 A/B 测试、基于测试结果得出结论都需要考虑诸多因素。下方总结了一些常见考虑因素:

  • 老用户第一次体验改动会有新奇效应和改变抗拒心理;
  • 要得到可靠的显著结果,需要有足够的流量和转化率;
  • 要做出最佳决策,需选用最佳指标(如营收 vs 点击率);
  • 应进行足够的实验时长,以便解释天/周/季度事件引起的行为变化;
  • 转化率需具备现实指导意义(推出新元素的开支 vs 转化率提高带来的效益);
  • 对照组和实验组的测试对象要有一致性(两组样本数失衡会造成辛普森悖论等现象的发生)。

111

  • 2
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值