数据驱动分析实践九 - A/B测试设计和实施

数据驱动分析实践(九)

A/B测试设计和实施

    作为一个数据驱动增长的实施者,一个最主要的责任就是实践新的想法和持续学习。实践是检验机器学习模型的最有力的手段,新方法改善旧方法,举例来看:

    如果你有一个具有95%正确率的留存模型,从中获得最可能流失的客户,并给这些客户一个诱人的优惠政策。你认为这些客户的10%会留下来,且每人每月为你带来20元的收入。

这里有很多假设,让我们来详细讨论一下:

  • 模型的正确率为95%,这一点是真实的吗?模型是根据上个月的数据训练得到的。到了下个月,新的用户、新的产品特性、市场活动、季节效应等因素都会使模型不那么可信。历史上正确不代表未来。所以,我们不能在未经测试的情况下得到这个结论。
  • 你假设10%的转化率是基于上次的促销活动。这并不能保证在新的活动中还能够获得同样的转化率。进一步说,由于情况已经改变,客户的反应很大程度上是难以预测的。
  • 最后,这些客户在今天为你带来了每天20元的收入并不代表以后也是如此。

    如果希望知道下一步会发生什么,我们需要构造一个A/B测试。下面我们将会通过程序来设计和执行A/B测试。在此之前,我们需要了解关于A/B测试的两个关键点。

  • 假设是什么?
    继续刚才的例子中的假设,测试组会有更多留存: 组A -> 促销策略 -> 更高的留存
    组B -> 无策略 -> 低留存
    这样做也可以帮助我们测试我们的模型的正确率。如果组B的留存率为50%,很明显我们的模型无效。同样的判断也适用于测量收入。
  • 成功指标是什么? 每组的留存率

A/B测试

#import libraries
from __future__ import division
from datetime import datetime, timedelta,date
import pandas as pd
from sklearn.metrics import classification_report,confusion_matrix
import matplotlib.pyplot as plt
import numpy as np
import seaborn as sns

from sklearn.cluster import KMeans


import chart_studio.plotly as py
import plotly.offline as pyoff
import plotly.graph_objs as go
import plotly.figure_factory as ff

import sklearn
import xgboost as xgb
from sklearn.model_selection import KFold, cross_val_score, train_test_split
import warnings
%matplotlib inline
warnings.filterwarnings("ignore")

#initiate plotly
pyoff.init_notebook_mode()

现在我们要创建数据集,这个虚拟的数据集会包含四列:

  • 客户ID
  • 客户分段:高价值或低价值
  • 组:控制组或者测试组
  • 购买次数
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'

一般情况下,购买次数会呈现泊松分布

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)
df_hv.query('group == "test"').head()

在这里插入图片描述

df_hv.query('group == "control"').head()

在这里插入图片描述
假如我们为50%的高价值客户提供了促销优惠,观察他们的购买次数。

test_results = df_hv[df_hv.group == 'test'].purchase_count
control_results = df_hv[df_hv.group == 'control'].purchase_count

hist_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开始就好于控制组。但是我们是否可以肯定这个实验是成功的,其他因素不会带来任何差异?为了回答这个问题,我们需要检查结果是否具有统计显著性。

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

Ttest_indResult(statistic=9.178854780044302, pvalue=4.770085341930782e-20)

ttest_ind输出两个结果:

  • t-statistics:表示测试组和控制组的平均差异,以标准差计量。高t-statistics值意味着很大的差异。从上面的结果上看是支持我们的假设的。
  • p-value:表示原假设为真的概率
    这里的原假设为真就意味着测试组和控制组没有区别。所以低p-value对我们来说是好事情。从行业标准上来说,我们接受p-value < 5%就意味着具备统计显著性。但这一点往往需要依赖于你的业务逻辑。

为了理解我们的测试是否具备统计显著性,我们对我们的数据集应用如下函数。

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')
eval_test(test_results,control_results)

result is significant

    看上去非常好!
    不幸的是事情并没有这么简单。如果你选择了一个有偏的测试组,结果大概率是具备统计显著性的。举例来说,如果测试组的高价值客户更多而控制组的低价值客户更多,那么这个实验从一开始就错了。这就是为什么组的选择是A/B测试的一个关键点。

选择测试组和控制组

    最常用的选择测试组和控制组的方法是随机采样。让我们通过程序来实现这一点,现在我们的数据集中将包含20k高价值客户和80k低价值客户。

#create hv segment
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['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)
df_customers.head()

在这里插入图片描述

df_customers.tail()

在这里插入图片描述
假设我们需要测试组中有90%的客户,而控制组中有10%的客户。

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

这里还有一个小问题。我们并不能保证实际上高价值客户数量 / 低价值客户数量 = 测试组数量 / 控制组数量

所以我们需要进行一下优化,

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)]
df_test.segment.value_counts()

low-value 72000
high-value 18000
Name: segment, dtype: int64

df_control.segment.value_counts()

low-value 8000
high-value 2000
Name: segment, dtype: int64

数据比例正确。

我们已经探索了如何分组和进行t检验。但是如果我们需要做A/B/C测试或者在多个组上做A/B测试,就需要引入方差分析(ANOVA)

单因素方差分析(One Way ANOVA)

    假如我们正在同样的多个组上测试变量,而该变量有多于2个取值(例如,2个不同的优惠策略和没有优惠),那么我们需要应用单因素方差分析来评估我们的实验。

#create hv segment
df_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_count
b_stats = df_hv[df_hv.group=='B'].purchase_count
c_stats = df_hv[df_hv.group=='C'].purchase_count

hist_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检验类似,好的结果是具备统计显著性。

one_anova_test(a_stats,b_stats,c_stats)

result is significant

我们看一下如果没有统计显著性的例子。

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_count
b_stats = df_hv[df_hv.group=='B'].purchase_count
c_stats = df_hv[df_hv.group=='C'].purchase_count
hist_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)

在这里插入图片描述

one_anova_test(a_stats,b_stats,c_stats)

result is not significant

如果我们想看看A,B,C三组是否存在差别,我们可以使用前面的t检验。

双因素方差分析(Two-Way ANOVA)

    如果我们需要同时对高价值客户和低价值客户做上面的测试,我们使用双因素方差分析。

#create hv segment
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' 
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_lm
model = smf.ols(formula='purchase_count ~ segment + group ', data=df_customers).fit()
aov_table = anova_lm(model, typ=2)

aov_table帮助我们检查实验是否成功。

print(np.round(aov_table,4))

在这里插入图片描述
最后一列告诉我们组间的差别是具备统计显著性的。

    现在我们已经了解了如何分组且评估结果,但是另外一个事情是:为了达到统计显著性,我们的采样数据必须足够多。让我们来看一下如何计算采样样本大小。

样本大小计算

首先,需要了解两个概念:

  • 效应量(Effec Size): 代表测试组和控制组平均差别的量级,是平均差别的方差除以控制组的标准差。
  • 功效(Power):测试中发现统计显著性的概率,0.8是常被采用的一个值。
from statsmodels.stats import power
ss_analysis = power.TTestIndPower()#create hv segment

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['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()
print(purchase_mean,purchase_std)

0.69945 0.8428257324388024

这个例子中,购买次数平均值是0.7,标准差为0.84。
如果我们想在此实验中把平均值提升到0.75,我们计算效应量如下:

effect_size = (0.75 - purchase_mean)/purchase_std
print(effect_size)

0.059976811402908156

之后,样本大小非常容易计算,

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

4364.811097627248

alpha是统计显著性门槛值0.05,测试中测试组和控制组的比为1,结果我们得到了所需的样本大小为4365。
我们来写一个封装的函数,

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))

如果我们希望测试为购买次数带来5%的增长,

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

9118

所需样本大小为9118。

总结

    本系列暂时完结。整体来说,我们经历了一个完整的数据驱动分析过程。它包括数据处理、数据分析、机器学习等多种技术方法。但笔者认为这些方法还在其次,最为重要的是提出正确的问题、分析和解决问题的思路和如何回答问题。也就是说,我们需要抓住问题的本质和底层逻辑。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值