AB测试实战(一)+https://developer.aliyun.com/article/1544766?spm=a2c6h.13148508.setting.14.22454f0eHFZZj3
3、AB测试代码实战
3.1 项目背景
UI设计师设计了最新的产品页面想通过页面更新提高转化率
- 目前的转化率全年平均在13%左右
- 目标转化率达到 15%。
- 在新的页面上线之前,我们要通过一小部分用户上测试页面的效果, 所以需要进行A/B测试
3.2 设计实验
- 提出假设
首先,我们要确保在项目开始时就制定了一个假设
鉴于我们不知道新设计的性能是否会比我们当前的设计更好或更差(或相同?),我们将选择双尾测试:
H 0 原假设 老的设计比较好, 新版设计没有用
H 1 备选假设 新的设计比较好
- 选择变量
对照组: 看到旧的设计
实验组: 看到新的设计
虽然我们已经知道了旧的设计的转化率(13%左右), 但是我们依然要需要设计两组, 原因是为了避免其他因素带来的误差, 比如季节因素, 促销因素。这两组人在其它条件都相同的只是页面设计不同的情况下进行实验, 这样能保证两组间的差异是由于设计导致的
我们的设计的目标KPI是转化率, 所以,我们会添加一个字段来记录用户的购买情况
购买了产品的用户 值为1
未购买产品的用户值为0
- 确定实验人数
AB测试只会选择一小部分用户来参与实验, 用小部分的实验结果来估计整体的结果,每组的人数越多,我们得到的结果就越精准,但同时我们付出的成本就越大,通过功效分析我们可以计算出满足实验条件的最小人群
检验功效 (1 — β) :一般设置为0.8
α :在设计实验的时候我们设置为0.05
效果大小:旧的页面转化率为13%,新页面我们希望转化率能提升2%,所以我们可以用13%和15%来计算预期效果大小
确定实验人数的计算过程可以通过Python代码实现
import numpy as np import pandas as pd import scipy.stats as stats import statsmodels.stats.api as sms import matplotlib as mpl import matplotlib.pyplot as plt import seaborn as sns from math import ceil %matplotlib inline effect_size = sms.proportion_effectsize(0.13, 0.15) required_n = sms.NormalIndPower().solve_power( effect_size, power=0.8, alpha=0.05, ratio=1 ) required_n = ceil(required_n) print(required_n)
4720
- 计算结果说明每组至少需要4720人
- 在实践中将功率参数设置为 0.8 意味着如果我们的设计之间存在转化率的实际差异,假设差异是我们估计的差异(13% 对 15%),我们有大约 80% 的机会检测到它 与我们计算的样本量在我们的测试中具有统计显着性。
3.3 收集准备数据
- 在企业场景下,我们需要与工程团队配合,收集满足要求的数据, 这里我们使用准备好的数据集, 对数据进行处理
- 加载数据到DataFrame
- 检查和清理数据
- 从DataFrame中采样数据每组4720行
df = pd.read_csv('data/ab_data.csv') df.head()
user_id |
timestamp | group | landing_page | converted | |
0 | 851104 | 2017-01-21 22:11:48.556739 | control | old_page | 0 |
1 | 804228 | 2017-01-12 08:01:45.159739 | control | old_page | 0 |
2 | 661590 | 2017-01-11 16:55:06.154213 | treatment | new_page | 0 |
3 | 853541 | 2017-01-08 18:28:03.143765 | treatment | new_page | 0 |
4 | 864975 | 2017-01-21 01:52:26.210827 | control | old_page | 1 |
- 查看数据基本情况
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 294478 entries, 0 to 294477 Data columns (total 5 columns): --- ------ -------------- ----- 0 user_id 294478 non-null int64 1 timestamp 294478 non-null object 2 group 294478 non-null object 3 landing_page 294478 non-null object 4 converted 294478 non-null int64 dtypes: int64(2), object(3) memory usage: 11.2+ MB
数据一共294478行,每一条数据代表一次用户访问,共五列:
- user_id - 访问的用户ID
- timestamp - 访问的时间
- group - 该用户被放到那一组 {control对照, treatment实验}
- landing_page -该用户看到的是哪一种落地页 {old_page老页面, new_page新页面}
- converted - 改次访问是否有转化 (binary, 0=无转化, 1=转化)
在后续的分析中,我们实际上主要用到的是 group 和converted 这两个字段
创建透视表, 查询是否对照组看到的都是老页面
df.pivot_table(index = 'group',columns='landing_page',values = 'user_id',aggfunc='count')
anding_page | new_page | old_page |
group | ||
control | 1928 | 145274 |
treatment | 145311 | 1965 |
- 在我们进行后续处理之前, 还要查看是否有用户进行了多次操作
session_counts = df['user_id'].value_counts(ascending=False) multi_users = session_counts[session_counts>1].count() multi_users
3894
- 说明一共有3894个用户访问了不止一次, 整体数据有20多万条, 所以我们直接把这部分数据删除
users = session_counts[session_counts < 2].index df = df[df['user_id'].isin(users)]
数据采样
- 接下来我们从处理后的数据中,每组采样4720条数据, 我们这里使用DataFrame的sample()方法进行简单随机采样
control_sample = df[df['group'] == 'control'].sample(n=required_n, random_state=22) treatment_sample = df[df['group'] == 'treatment'].sample(n=required_n, random_state=22) ab_test = pd.concat([control_sample, treatment_sample], axis=0) ab_test.reset_index(drop=True, inplace=True) ab_test
user_id |
timestamp | group | landing_page | converted | user_id |
group | landing_page | converted | |
0 | 763854 | 2017-01-21 03:43:17.188315 | control | old_page | 0 | 0 | |||
1 | 690555 | 2017-01-18 06:38:13.079449 | control | old_page | 0 | 1 | |||
2 | 861520 | 2017-01-06 21:13:40.044766 | control | old_page | 0 | 2 | |||
3 | 630778 | 2017-01-05 16:42:36.995204 | control | old_page | 0 | 3 | |||
4 | 656634 | 2017-01-04 15:31:21.676130 | control | old_page | 0 | 4 | |||
… | … | … | … | … | … | … | |||
9435 | 908512 | 2017-01-14 22:02:29.922674 | treatment | new_page | 0 | 9435 | |||
9436 | 873211 | 2017-01-05 00:57:16.167151 | treatment | new_page | 0 | 9436 | |||
9437 | 631276 | 2017-01-20 18:56:58.167809 | treatment | new_page | 0 | 9437 | |||
9438 | 662301 | 2017-01-03 08:10:57.768806 | treatment | new_page | 0 | 9438 | |||
9439 | 944623 | 2017-01-19 10:56:01.648653 | treatment | new_page | 1 | 9439 |
- 查看两组数据情况
ab_test.groupby('group')['landing_page'].value_counts()
group landing_page
control old_page 4720
treatment new_page 4720
Name: landing_page, dtype: int64
3.4 分析实验结果
- 首先我们来计算一下两组的转化率和标准差
conversion_rates = ab_test.groupby('group')['converted'].mean().to_frame() conversion_rates conversion_rates.style.format('{:.3f}')
group | conversion_rate |
control | 0.123 |
treatment | 0.126 |
- 从上面的统计数据来看,旧的和新的落地页表现非常相似,新设计表现略好, 12.3% 与 12.6%
- 从对照组的数据看, 转换率为12.3% 比之前我们的整体表现要差一些, 可能与采样人群的差异有关
- 测试组的数据比对照组要好一些, 但是这个结果是否具有统计意义?
3.5 假设检验
- 最后一步是假设检验, 我们的样本量比较大,可以应用Z检验来计算P值, 如果P值<0.05, 说明
- 我们可以使用statsmodels.stats.proportion 模块来计算P值和置信区间
from statsmodels.stats.proportion import proportions_ztest, proportion_confint control_results = ab_test[ab_test['group'] == 'control']['converted'] #获取对照组是否转化的数据 treatment_results = ab_test[ab_test['group'] == 'treatment']['converted'] #获取实验组是否转化的数据 n_con = control_results.count() # 获取对照组人数 n_treat = treatment_results.count() # 获取实验组人数 successes = [control_results.sum(), treatment_results.sum()] # 获取实验组和对照组成功转化的人数 nobs = [n_con, n_treat] z_stat, pval = proportions_ztest(successes, nobs=nobs) #计算P值 (lower_con, lower_treat), (upper_con, upper_treat) = proportion_confint(successes, nobs=nobs, alpha=0.05) #计算置信区间 print(f'z statistic: {z_stat:.2f}') print(f'p-value: {pval:.3f}') print(f'ci 95% for control group: [{lower_con:.3f}, {upper_con:.3f}]') print(f'ci 95% for treatment group: [{lower_treat:.3f}, {upper_treat:.3f}]')
🥂小结
AB测试的应用场景
- 互联网行业应用广泛:页面结构调整, 换新图标,添加新功能等等
- 实体行业应用相对复杂一些:不同优惠券效果测试
AB测试还是ABC测试
- AB测试:一次测试一个方案
- ABC…… 测试: 一次测试多个方案,但需要流量足够大,否则难以满足实验要求的最少人数
AB测试需要注意如下几点
- 流量分配
- 确定有效的最小参与人数
- 确定基准指标和提升目标
- 设置显著性水平α (一般是5%)和 统计功效 1-β (一般是80%)
- 出结果之后计算P值 如果P<5% 则可以拒绝原假设算P值 如果P<5% 则可以拒绝原假设
- 我们可以通过AB测试工具网站帮助确定人数,也可以使用statsmodels 模块来通过代码实现
- import statsmodels.stats.api 计算需要人数
- statsmodels.stats.proportion 计算P值和置信区间