ronghuaiyang · 2020年04月30日

用机器学习来提升你的用户增长:第九步,A/B测试的设计和执行

作者:Barış KaramanFollow
编译:ronghuaiyang
首发:AI公园公众号

A/B测试时验证模型结果,持续改进的必备手段,也是本系列的最后一部分。另外,很多同学问我要文中用到的数据集,我整理了一下,放在了文章最后。

第九部分: A/B测试的设计和执行

作为一个(数据驱动的)增长黑客,其主要职责之一是试验新思想并持续不断地学习。实验是测试你的机器学习模型、新行为和改进现有行为的好方法。举个例子:

你有一个95%准确率的客户流失模型。通过给那些可能会流失的客户打电话,并给出一个有吸引力的优惠,你可以假设他们中有10%的人会留下,每个人每月带来20美元的收入。

这里面有很多的假设,分解一下:

  • 该模型的准确率为95%。这是真的吗?你根据上个月的数据训练了您的模型。下个月有新用户、新产品特征、营销与品牌活动、季节变化等。在这种情况下,历史准确性和实际准确性很少匹配。不经过检验你就不能得出结论。
  • 通过查看以前的活动结果,你假设有10%的转化率。由于上述因素,不能保证你的新活动也会有10%的转化率。此外,由于这是一个新的群体,他们的反应在一定程度上是不可预测的。
  • 最后,如果这些客户现在每月带来20美元,并不意味着他们在你采取新活动后也会带来同样的钱。

为了知道会发生什么,我们需要进行A/B测试。在本文中,我们将重点讨论如何用代码的方式执行测试并报告测试背后的统计数据。在开始编写代码之前,在设计和A/B测试时,有两个要点需要考虑。

1、你的假设是什么?

继续上面的例子,我们的假设是,实验组会有更高的留存率:

A组→提供优惠→更高的留存率

B组→无优惠→留存率较低

这也帮助我们测试模型的准确性。如果B组的留存率为50%,则可以清楚地看出我们的模型是好用的。这同样适用于衡量来自这些用户的收入。

2、你的成功指标是什么?

在这种情况下,我们要检查两组的留存率。

编程实现A/B测试

对于这个代码示例,我们使用numpy库创建自己的数据集,并评估A/B测试的结果。

我们导入必要的库:

#import librariesfrom datetime import datetime, timedelta,dateimport pandas as pd%matplotlib inlinefrom sklearn.metrics import classification_report,confusion_matriximport matplotlib.pyplot as pltimport numpy as npimport seaborn as snsfrom __future__ import divisionfrom sklearn.cluster import KMeansimport plotly.plotly as pyimport plotly.offline as pyoffimport plotly.graph_objs as goimport plotly.figure_factory as ffimport sklearnimport xgboost as xgbfrom sklearn.model_selection import KFold, cross_val_score, train_test_splitimport warningswarnings.filterwarnings("ignore")#initiate plotlypyoff.init_notebook_mode()

现在我们创建自己的数据集,数据集将包含以下列:

  • customer\_id:客户的唯一标识
  • segment:客户的分群,高价值或者低价值
  • group:表示客户是在测试组还是控制组
  • purchase\_count:客户的购买次数

前3个非常简单:

df_hv = pd.DataFrame()df_hv['customer_id'] = np.array([count for count in range(20000)])df_hv['segment'] = np.array(['high-value' for _ in range(20000)])df_hv['group'] = 'control'df_hv.loc[df_hv.index<10000,'group'] = 'test'

理想情况下,购买数量应该是泊松分布。会有没有购买的客户,也会有少量的购买次数高的客户。我们使用numpy.random.poisson()来完成这项工作,并将不同的分布分配给测试组和控制组:

df_hv.loc[df_hv.group == 'test', 'purchase_count'] = np.random.poisson(0.6, 10000)df_hv.loc[df_hv.group == 'control', 'purchase_count'] = np.random.poisson(0.5, 10000)

我们看一下数据集:

太棒了。我们现在做好了A/B测试的准备工作。假设我们向50%的高价值用户提供了优惠,并在给定的时间段内观察他们的购买行为。查看分布的最佳方法是:

test_results = df_hv[df_hv.group == 'test'].purchase_countcontrol_results = df_hv[df_hv.group == 'control'].purchase_counthist_data = [test_results, control_results]group_labels = ['test', 'control']# Create distplot with curve_type set to 'normal'fig = ff.create_distplot(hist_data, group_labels, bin_size=.5,                         curve_type='normal',show_rug=False)fig.layout = go.Layout(        title='High Value Customers Test vs Control',        plot_bgcolor  = 'rgb(243,243,243)',        paper_bgcolor  = 'rgb(243,243,243)',    )# Plot!pyoff.iplot(fig)

输出:

结果看起来真的很好。试验组购买的密度从1开始就变的更好了。但是,我们怎么能肯定地说这个实验是成功的,而这种差异不是由于其他因素造成的呢?

为了回答这个问题,我们需要查看试验组的uplift是否有统计学意义。使用scipy库来实现:

from scipy import stats test_result = stats.ttest_ind(test_results, control_results)print(test_result)

输出:

ttest\_ind() 方法返回两个输出:

  • t-statistic:表示测试组与控制组的平均值之差,单位为标准差。t-statistic越大,差异越大,越支持我们的假设。
  • p-value:衡量零假设为真的概率。

那,什么是零假设?

如果零假设为真,则意味着你的测试组和控制组之间没有显著差异。p值越低越好。作为行业标准,我们接受**p-value<5%**使结果具有统计意义(但这取决于你的业务逻辑,有些情况下人们使用10%甚至1%)。

def eval_test(test_results,control_results):    test_result = stats.ttest_ind(test_results, control_results)    if test_result[1] < 0.05:        print('result is significant')    else:        print('result is not significant')

如果我们把这个应用到我们的数据集:

看起来不错,但不幸的是,事情没那么简单。如果你选择一个有偏差的测试组,默认情况下你的结果在统计上是显著的。例如,如果我们将更多的高价值客户分配给测试组,而将更多的低价值客户分配给控制组,那么我们的实验从一开始就失败了。这就是为什么分组是正确的A/B测试的关键。

选择测试组和控制组

选择测试和控制组最常用的方法是随机抽样。让我们看看如何通过编程来实现它。我们首先创建数据集。在这个版本中,会有20k的高价值客户和80k的低价值客户:

#create hv segmentdf_hv = pd.DataFrame()df_hv['customer_id'] = np.array([count for count in range(20000)])df_hv['segment'] = np.array(['high-value' for _ in range(20000)])df_hv['prev_purchase_count'] = np.random.poisson(0.9, 20000)df_lv = pd.DataFrame()df_lv['customer_id'] = np.array([count for count in range(20000,100000)])df_lv['segment'] = np.array(['low-value' for _ in range(80000)])df_lv['prev_purchase_count'] = np.random.poisson(0.3, 80000)df_customers = pd.concat([df_hv,df_lv],axis=0)

通过使用pandas的**sample()**函数,我们可以选择测试组,假设我们把90%数据放到测试组,10%给控制组:

df_test = df_customers.sample(frac=0.9)df_control = df_customers[~df_customers.customer_id.isin(df_test.customer_id)]

在本例中,我们提取了整个组的90%,并将其标记为“test”。但是有一个小问题可能会破坏我们的实验。如果你的数据集中有明显不同的多个组(在本例中是高价值和低价值),那么最好分别进行随机抽样。否则,我们不能保证高价值和低价值的比例对于测试组和控制组是相同的。

为了确保正确地创建测试组和控制组,我们需要使用以下代码:

df_test_hv = df_customers[df_customers.segment == 'high-value'].sample(frac=0.9)df_test_lv = df_customers[df_customers.segment == 'low-value'].sample(frac=0.9)df_test = pd.concat([df_test_hv,df_test_lv],axis=0)df_control = df_customers[~df_customers.customer_id.isin(df_test.customer_id)]

这使得这两个组的分配都是正确的:

我们已经探讨了如何进行t-test的测试组和控制组的选择。但是如果我们做A/B/C测试,或者像上面那样在多个组上做A/B测试,结果会怎样呢?是时候引入ANOVA测试了。

单因子方差分析

假设我们在相同的组上测试2+个变量(即2个不同的优惠和无优惠给低价值的高价值客户)。然后我们需要应用单因子方差分析来评估我们的实验。让我们开始创建我们的数据集:

#create hv segmentdf_hv = pd.DataFrame()df_hv['customer_id'] = np.array([count for count in range(30000)])df_hv['segment'] = np.array(['high-value' for _ in range(30000)])df_hv['group'] = 'A'df_hv.loc[df_hv.index>=10000,'group'] = 'B' df_hv.loc[df_hv.index>=20000,'group'] = 'C' df_hv.loc[df_hv.group == 'A', 'purchase_count'] = np.random.poisson(0.4, 10000)df_hv.loc[df_hv.group == 'B', 'purchase_count'] = np.random.poisson(0.6, 10000)df_hv.loc[df_hv.group == 'C', 'purchase_count'] = np.random.poisson(0.2, 10000)a_stats = df_hv[df_hv.group=='A'].purchase_countb_stats = df_hv[df_hv.group=='B'].purchase_countc_stats = df_hv[df_hv.group=='C'].purchase_counthist_data = [a_stats, b_stats, c_stats]group_labels = ['A', 'B','C']# Create distplot with curve_type set to 'normal'fig = ff.create_distplot(hist_data, group_labels, bin_size=.5,                         curve_type='normal',show_rug=False)fig.layout = go.Layout(        title='Test vs Control Stats',        plot_bgcolor  = 'rgb(243,243,243)',        paper_bgcolor  = 'rgb(243,243,243)',    )# Plot!pyoff.iplot(fig)

输出:

为了评估结果,我们使用以下函数:

def one_anova_test(a_stats,b_stats,c_stats):    test_result = stats.f_oneway(a_stats, b_stats, c_stats)    if test_result[1] < 0.05:        print('result is significant')    else:        print('result is not significant')

逻辑类似于t\_test,如果p值低于5%,我们的测试是显著的:

让我们来看看如果两组之间没有差异,会是什么样子:

df_hv.loc[df_hv.group == 'A', 'purchase_count'] = np.random.poisson(0.5, 10000)df_hv.loc[df_hv.group == 'B', 'purchase_count'] = np.random.poisson(0.5, 10000)df_hv.loc[df_hv.group == 'C', 'purchase_count'] = np.random.poisson(0.5, 10000)a_stats = df_hv[df_hv.group=='A'].purchase_countb_stats = df_hv[df_hv.group=='B'].purchase_countc_stats = df_hv[df_hv.group=='C'].purchase_counthist_data = [a_stats, b_stats, c_stats]group_labels = ['A', 'B','C']# Create distplot with curve_type set to 'normal'fig = ff.create_distplot(hist_data, group_labels, bin_size=.5,                         curve_type='normal',show_rug=False)fig.layout = go.Layout(        title='Test vs Control Stats',        plot_bgcolor  = 'rgb(243,243,243)',        paper_bgcolor  = 'rgb(243,243,243)',    )# Plot!pyoff.iplot(fig)

输出和测试结果:

如果我们想看看A和B或C之间是否有区别,我们可以使用上面我解释过的t\_test。

双因子方差分析

假设我们正在对高价值和低价值客户进行相同的测试。在这种情况下,我们需要应用双因子方差分析。我们将再次创建我们的数据集,并建立我们的评估方法:

#create hv segmentdf_hv = pd.DataFrame()df_hv['customer_id'] = np.array([count for count in range(20000)])df_hv['segment'] = np.array(['high-value' for _ in range(20000)])df_hv['group'] = 'control'df_hv.loc[df_hv.index<10000,'group'] = 'test' df_hv.loc[df_hv.group == 'control', 'purchase_count'] = np.random.poisson(0.6, 10000)df_hv.loc[df_hv.group == 'test', 'purchase_count'] = np.random.poisson(0.8, 10000)df_lv = pd.DataFrame()df_lv['customer_id'] = np.array([count for count in range(20000,100000)])df_lv['segment'] = np.array(['low-value' for _ in range(80000)])df_lv['group'] = 'control'df_lv.loc[df_lv.index<40000,'group'] = 'test' df_lv.loc[df_lv.group == 'control', 'purchase_count'] = np.random.poisson(0.2, 40000)df_lv.loc[df_lv.group == 'test', 'purchase_count'] = np.random.poisson(0.3, 40000)df_customers = pd.concat([df_hv,df_lv],axis=0)

双因子方差分析需要构建如下的模型:

import statsmodels.formula.api as smf from statsmodels.stats.anova import anova_lmmodel = smf.ols(formula='purchase_count ~ segment + group ', data=df_customers).fit()aov_table = anova_lm(model, typ=2)

通过分群分组,模型可以得到purchase\_count. aov\_table,帮助我们看看我们的实验是不是成功了:

最后一列表示结果,我们的差别是显著的。如果不是,它看起来就像下面这样:

这表明,segment(高价值或低价值)对购买数量有显著影响,但group没有影响,其占比接近66%,远高于5%。

现在我们知道如何选择我们的组和评估结果。但还少了一个部分。为了达到统计显著性,我们的样本量应该足够多。我们来看看怎么计算它。

样本大小的计算

为了计算所需的样本量,首先我们需要理解两个概念:

  • 效果大小:表示试验组与控制组的平均值差异的大小。它是测试组和控制组之间的平均方差除以控制组的标准差。
  • 功率:这是指在你的测试中发现统计具有显著性的概率。计算样本大小的时候,常用的值是0.8。

我们构建我们的数据集,并在例子中看看如何计算的样本大小:

from statsmodels.stats import powerss_analysis = power.TTestIndPower()#create hv segmentdf_hv = pd.DataFrame()df_hv['customer_id'] = np.array([count for count in range(20000)])df_hv['segment'] = np.array(['high-value' for _ in range(20000)])df_hv['prev_purchase_count'] = np.random.poisson(0.7, 20000)purchase_mean = df_hv.prev_purchase_count.mean()purchase_std = df_hv.prev_purchase_count.std()

在本例中,购买的平均值(purchase\_mean)是0.7,标准偏差(purchase\_std)是0.84。

假设在这个实验中,我们想要将purchase\_mean增加到0.75。我们可以计算的有效大小如下:

effect_size = (0.75 - purchase_mean)/purchase_std

之后,样本量的计算就很简单了:

alpha = 0.05power = 0.8ratio = 1ss_result = ss_analysis.solve_power(effect_size=effect_size, power=power,alpha=alpha, ratio=ratio , nobs1=None) print(ss_result)

Alpha是统计显著性的阈值(5%),我们的测试和控制样本量之比为1(相等)。因此,我们需要的样本量是(ss\_result的输出)4868.

让我们建立一个函数来使用这个功能:

def calculate_sample_size(c_data, column_name, target,ratio):    value_mean = c_data[column_name].mean()    value_std = c_data[column_name].std()        value_target = value_mean * target        effect_size = (value_target - value_mean)/value_std        power = 0.8    alpha = 0.05    ss_result = ss_analysis.solve_power(effect_size=effect_size, power=power,alpha=alpha, ratio=ratio , nobs1=None)     print(int(ss_result))

对于这个函数,我们需要提供数据集、column\_name(在我们的例子中是purchase\_count)、目标平均值(在前面的例子中是0.75)和比率。

在上面的数据集中,让我们假设我们想要增加购买数量的平均值为5%,我们保持两个组的大小相同:

calculate_sample_size(df_hv, 'prev_purchase_count', 1.05,1)

然后得到结果8961.

代码:https://gist.github.com/karam...

这是数据驱动增长系列的结尾,感谢阅读!

数据集下载地址:

链接:https://pan.baidu.com/s/1lMoG...\_G\_unoV2w 

提取码:xrqj

—END—

英文原文:https://towardsdatascience.com/a-b-testing-design-execution-6cf9e27c6559

推荐阅读


关注图像处理,自然语言处理,机器学习等人工智能领域,请点击关注AI公园专栏
欢迎关注微信公众号
AI公园 公众号二维码.jfif
推荐阅读
关注数
8257
内容数
210
关注图像处理,NLP,机器学习等人工智能领域
目录
极术微信服务号
关注极术微信号
实时接收点赞提醒和评论通知
安谋科技学堂公众号
关注安谋科技学堂
实时获取安谋科技及 Arm 教学资源
安谋科技招聘公众号
关注安谋科技招聘
实时获取安谋科技中国职位信息