网页优化A/B测试

本项目是关于某电商平台在更新网页版本后,随机选取了两组用户,并跟踪该两组用户对于新旧网页的点击量的数据。本项目旨在通过A/B测试的方法,检验新页面相对于旧页面是否存在显著效果的提升,从而为公司决策从数据层面提供方向。

目录

分析框架

  • 数据评估与清洗
    • 数据评估
    • 数据预处理
    • 数据可视化
  • A/B测试
    • 假设检验
      • 公式计算
      • 模拟抽样分布(自助抽样法)
      • 调包—statsmodel.api功能库
    • 回归分析法
      • sklearn.Logicregression
      • statsmodel.api
      • 添加特征项
      • 增加模型的复杂度
  • 结论

数据评估与清洗

导入功能库,评估数据,对评估结果进行数据清洗

import pandas as pd
import numpy as np
import random
import warnings
warnings.filterwarnings('ignore')
import matplotlib.pyplot as plt
%matplotlib inline
#We are setting the seed to assure you get the same answers on quizzes as we set up
random.seed(42)
# 导入数据`ab_data.csv`
df = pd.read_csv(r'E:\Data Analysis\统计学\simple_abtest\ab_data.csv')

评估和清理数据

df.head(10)
user_idtimestampgrouplanding_pageconverted
08511042017-01-21 22:11:48.556739controlold_page0
18042282017-01-12 08:01:45.159739controlold_page0
26615902017-01-11 16:55:06.154213treatmentnew_page0
38535412017-01-08 18:28:03.143765treatmentnew_page0
48649752017-01-21 01:52:26.210827controlold_page1
59369232017-01-10 15:20:49.083499controlold_page0
66796872017-01-19 03:26:46.940749treatmentnew_page1
77190142017-01-17 01:48:29.539573controlold_page0
88173552017-01-04 17:58:08.979471treatmentnew_page1
98397852017-01-15 18:11:06.610965treatmentnew_page1

数据基本信息

df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 294478 entries, 0 to 294477
Data columns (total 5 columns):
 #   Column        Non-Null Count   Dtype 
---  ------        --------------   ----- 
 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: 7.9+ MB
# 是否存在重复值
df.duplicated().sum()
0
# 查看每一列的独立值
for col in df.columns:
    print('{}\n------'.format(col))
    print(df[col].unique())
user_id
------
[851104 804228 661590 ... 734608 697314 715931]
timestamp
------
['2017-01-21 22:11:48.556739' '2017-01-12 08:01:45.159739'
 '2017-01-11 16:55:06.154213' ... '2017-01-22 11:45:03.439544'
 '2017-01-15 01:20:28.957438' '2017-01-16 12:40:24.467417']
group
------
['control' 'treatment']
landing_page
------
['old_page' 'new_page']
converted
------
[0 1]
# 更改user_id为字符串
df['user_id'] = df['user_id'].astype('str')
# 更改timestamp数据属性为日期属性
df['date'] = pd.to_datetime(df.timestamp).dt.date
df.date.head()
0    2017-01-21
1    2017-01-12
2    2017-01-11
3    2017-01-08
4    2017-01-21
Name: date, dtype: object
# 查看page_view by day分布
plt.figure(figsize=(12,5))
plt.plot(df.date.value_counts().sort_index());

在这里插入图片描述

本项目共有294478条数据记录,5个数据特征,时间跨度持续2017年1月2日至2017年1月25日,无缺失值,无重复值;
具体特征属性如下所示:

列名数据属性解释说明
user_id字符串用户ID,用户唯一的身份识别码
timestamp日期时间序列用户浏览网页的时间点
group字符串用户所属的组别,控制组 or 实验组
landing_page字符串用户浏览的网页所属类别, 新网页 or 旧网页
converted整数型用户对该页面是否发起了点击行为, 0:否,1:是

数据预处理

本项目主要是通过A/B测试检测新旧页面是否会带来点击量的变化,所以两组用户数量之间的平衡以及所对应的新旧页面的一致性很关键

统计PV,UV数据
print('总浏览量:{}'.format(df.user_id.count()))
print('总独立用户数量:{}'.format(len(df.user_id.unique())))
总浏览量:294478
总独立用户数量:290584

说明有部分用户存在重复浏览网页的行为

for grp in df.group.unique():
    print('{}组的独立用户数量:{}'.format(grp,len(df[df.group == grp]['user_id'].unique())))
control组的独立用户数量:146195
treatment组的独立用户数量:146284
# 检查同一个用户是否存在在不同组别的问题
user_in_grp = df.groupby(['user_id','group']).size().reset_index(name='counts')['user_id'].value_counts()
(user_in_grp > 1).sum()
1895
both_group_records = df[df.user_id.isin(user_in_grp[user_in_grp > 1].index)].sort_values(by='user_id')
both_group_records.head(10)
user_idtimestampgrouplanding_pageconverteddate
2072116303202017-01-07 18:02:43.626318controlold_page02017-01-07
2557536303202017-01-12 05:27:37.181803treatmentold_page02017-01-12
398526308052017-01-22 12:01:15.144329controlold_page02017-01-22
735996308052017-01-12 20:45:39.012189treatmentold_page02017-01-12
2133776308712017-01-08 14:00:05.181612controlold_page02017-01-08
185966308712017-01-03 05:23:01.269988treatmentold_page02017-01-03
1143206308872017-01-22 22:23:47.432563treatmentnew_page02017-01-22
281776308872017-01-07 08:34:14.373905controlnew_page02017-01-07
1136356310082017-01-22 13:07:48.144552treatmentold_page02017-01-22
1377666310082017-01-21 08:52:17.147531controlold_page02017-01-21

1895个用户同时属于控制组和实验组,而导致同一用户同时属于不同group的问题可能是由于landing_page或者group标记错误导致的

检查landing_page和group的一致性

理论上treatment对应的是new_page,而control对应的是old_page

print('treatment & old_page:{}'.format(df.query('group == "treatment" and landing_page == "old_page"').count()['user_id']))
print('treatment & new_page:{}'.format(df.query('group == "treatment" and landing_page == "new_page"').count()['user_id']))
treatment & old_page:1965
treatment & new_page:145311
print('control & old_page:{}'.format(df.query('group == "control" and landing_page == "old_page"').count()['user_id']))
print('control & new_page:{}'.format(df.query('group == "control" and landing_page == "new_page"').count()['user_id']))
control & old_page:145274
control & new_page:1928

因为不能确认是landing_page标记错误还是所属group标记错误,所以只能将不符合数据集要求规范的问题都删除

# 查看错误记录占比
(1965 + 1928)/(294478)
0.013220002852505111

landing_page有group不一致的数据量占总数据量是***1.3%***,占比不大,删除后对于整体的分析影响不大

df2 = df.query('group =="treatment" and landing_page =="new_page" or group =="control" and landing_page =="old_page"')
# 查看筛选之后不合理页面与组别对应关系是否还存在
df2.query('group =="treatment" and landing_page =="old_page" or group =="control" and landing_page =="new_page"').shape[0]
0
# 查看筛选之后同一个用户是否还存在于不同组别
(df2.groupby(['user_id','group']).size().reset_index(name='counts')['user_id'].value_counts() > 1).sum()
0

说明同一用户存在不同组别的问题是由于landing_page或group标记不一致导致

len(df2.user_id.unique())
290584

删除不合理数据记录后,总浏览量是***290,585***,而总独立用户数量是***290,584***,说明用户不存在重复浏览的行为

# 删除user_id重复记录
df2[df2.duplicated(subset='user_id',keep=False)]
user_idtimestampgrouplanding_pageconverteddate
18997731922017-01-09 05:37:58.781806treatmentnew_page02017-01-09
28937731922017-01-14 02:55:59.590927treatmentnew_page02017-01-14
df2.drop_duplicates(subset='user_id',inplace=True)
df2.info()
<class 'pandas.core.frame.DataFrame'>
Int64Index: 290584 entries, 0 to 294477
Data columns (total 6 columns):
 #   Column        Non-Null Count   Dtype 
---  ------        --------------   ----- 
 0   user_id       290584 non-null  object
 1   timestamp     290584 non-null  object
 2   group         290584 non-null  object
 3   landing_page  290584 non-null  object
 4   converted     290584 non-null  int64 
 5   date          290584 non-null  object
dtypes: int64(1), object(5)
memory usage: 10.0+ MB
查看转化率信息
# 查看总转化率
print('样本总体转化率: {:.2f}%'.format(df2.converted.mean()*100))
# 查看control组的转化率
print('old_page转化率: {:.2f}%'.format(df2[df2.landing_page == 'old_page'].converted.mean()*100))
# 查看treatment组的转化率
print('new_page转化率: {:.2f}%'.format(df2[df2.landing_page == 'new_page'].converted.mean()*100))
样本总体转化率: 11.96%
old_page转化率: 12.04%
new_page转化率: 11.88%

没有足够证据证明某一个页面可以带来更多的转化。虽然上述样本计算出的结果是 P o l d p a g e > P n e w p a g e P_{old_page} > P_{new_page} Poldpage>Pnewpage,但是仍不能断言旧页面的转化率大于新页面,因为以上只是一个样本,而取样有很大的随机性和不确定性,所以样本不能代表总体。

A/B 测试

假设检验

零假设和备择假设

已知,当 n p > 5 np > 5 np>5 n ( 1 − p ) > 5 n(1-p) > 5 n(1p)>5时,样本占比的抽样分布服从正态分布

零假设:新旧页面在转化率上没有差异

备择假设:新页面的转化率较旧页面的转化率高


H 0 : P d i f f = P n e w − P o l d = 0 H_0:P_{diff} = P_{new} - P_{old} = 0 H0Pdiff=PnewPold=0
H 1 : P d i f f = P n e w − P o l d > 0 H_1:P_{diff} = P_{new} - P_{old} > 0 H1Pdiff=PnewPold>0

公式计算

在零假设为真的前提下,计算实际新页面转化率与旧页面转化率的差异,在两个样本比例差异的抽样分布下的Z分数,计算p值。若p值大于显著性水平 α = 0.05 \alpha = 0.05 α=0.05,则不拒绝原假设,反之若p值小于显著性水平 α = 0.05 \alpha = 0.05 α=0.05,则拒绝原假设,接受备择假设。

  • P d i f f = P n e w − P o l d = 0 P_{diff} = P_{new} - P_{old} = 0 Pdiff=PnewPold=0

  • σ p − d i f f 2 = σ p − n e w 2 n + σ p − o l d 2 m = ( n 和 m 分 别 代 表 各 自 的 样 本 量 ) \sigma^2_{p-diff} = \frac{\sigma^2_{p-new}}{n} + \frac{\sigma^2_{p-old}}{m} = (n和m分别代表各自的样本量) σpdiff2=nσpnew2+mσpold2=(nm)

  • σ p − d i f f 2 = p n e w ( 1 − p n e w ) n + p o l d ( 1 − p o l d ) m ( n 和 m 分 别 代 表 各 自 的 样 本 量 ) \sigma^2_{p-diff} = \frac{p_{new} (1 - p_{new})}{n} + \frac{p_{old}(1- p_{old})}{m}(n和m分别代表各自的样本量) σpdiff2=npnew(1pnew)+mpold(1pold)(nm)

# 计算样本总体转化率,在零假设的前提下,p_new = p_old = p_total
P_total = df2.converted.mean()
P_new = P_old = P_total
# 计算新旧页面的样本量
n_new = (df2.landing_page == 'new_page').sum()
n_old = (df2.landing_page == 'old_page').sum()
n_new,n_old
(145310, 145274)
# 计算两个样本之间的比例差值的抽样分布的方差
sigma_diff = np.sqrt(P_new *(1 -P_new)/n_new + P_old *(1 - P_old)/n_old)
sigma_diff
0.0012039132295014454
# 计算实际样本比例的差值
p_new_actual = df2[df2.landing_page == 'new_page'].converted.mean()
p_old_actual = df2[df2.landing_page == 'old_page'].converted.mean()
p_diff_actual = p_new_actual - p_old_actual
# 计算z统计量
z_score = p_diff_actual/sigma_diff
z_score
-1.3109241984234394

根据Z统计量累计分布概率表(如下),当 Z _ s c o r e = 1.31 Z\_score = 1.31 Z_score=1.31时, P 值 = 0.9049 P值 = 0.9049 P=0.9049,由于本项目是右侧检验,所以P值= 1 - P(Z=-1.31)= 1 - (1- P(Z=1.31)) = P(Z=1.31)= 0.9049,所以 P > α \alpha α =0.05,所以没有足够证据拒绝原假设,即新旧页面的转化率没有差异

模拟抽样分布(自助抽样法)

由于有两个随机变量,即需要模拟新页面的转化率的抽样分布和旧页面的转化率的抽样分布

# 在零假设的前提下,p_new = p_old = p_total
# 模拟1次新页面的抽样
new_page_converted = np.random.choice([0,1],size=n_new,p=[1-P_new,P_new])
# 模拟1次旧页面的抽样分布
old_page_converted = np.random.choice([0,1],size=n_old,p=[1-P_old,P_old])
# 计算p_diff = p_new - p_old
p_diff_simu = new_page_converted.mean() - old_page_converted.mean()
p_diff_simu
0.0014018734971285723

模拟上述抽样10000次,将每一次的抽样结果储存到p_diffs

# 迭代10000次上述抽样方法
p_diffs = []
for i in range(10000):
    new_page_converted = np.random.choice([0,1],size=n_new,p=[1-P_new,P_new])
    old_page_converted = np.random.choice([0,1],size=n_old,p=[1-P_old,P_old])
    p_diffs.append(new_page_converted.mean() - old_page_converted.mean())

绘制p_diffs分布图,并将本项目实际样本产生的差异标记到分布图上

# 绘制**p_diffs**直方图
import seaborn as sns
sns.distplot(p_diffs)
plt.axvline(x=p_diff_actual,c='r');

在这里插入图片描述

# 该分布为右侧检验,计算零假设下的抽样分布比实际差异更大的概率
(np.array(p_diffs) > p_diff_actual).mean()
0.9074

该值为P值,是指假如零假设为真,能够检测到统计量的概率,即度量样本所提供的证据对原假设的支持程度。P值大于90%,说明没有足够的证据证明新页面的转化率大于旧页面,故目前无法拒绝原假设。

调包计算

l. 我们也可以使用一个内置程序 (built-in)来实现类似的结果。尽管使用内置程序可能更易于编写代码,但上面的内容是对正确思考统计显著性至关重要的思想的一个预排。填写下面的内容来计算每个页面的转化次数,以及每个页面的访问人数。使用 n_oldn_new 分别引证与旧页面和新页面关联的行数。

import statsmodels.api as sm

convert_old = df2[(df2.landing_page == 'old_page') & (df2.converted == True)]['user_id'].count()
convert_new = df2[(df2.landing_page == 'new_page') & (df2.converted == True)]['user_id'].count()
n_old = df2[df2.landing_page == 'old_page'].count()['user_id']
n_new = df2[df2.landing_page == 'new_page'].count()['user_id']
convert_old,convert_new,n_old,n_new
(17489, 17264, 145274, 145310)

使用 stats.proportions_ztest 来计算检验统计量与 p-值

zstat, p_val = sm.stats.proportions_ztest([convert_new,convert_old],[n_new,n_old],alternative='larger')
zstat, p_val
(-1.3109241984234394, 0.9050583127590245)

根据z-score和p-value可知,当零假设为真时,能够观察到检验统计量的概率大于90%,故目前没有足够证据拒绝原假设,即没有足够证据证明新页面的转化率大于旧页面的转化率。

回归分析法

本项目还可以用线性回归模型来分析转化与否与接收页面类型之间的关系。
因为converted是一个二分类数据类型,所以适用逻辑回归来搭建线性回归模型。

添加一个截距列,一个new_page列,当用户接收new_page 时为1,old_page时为0。

# 添加new_page列
df2['new_page'] = pd.get_dummies(df2.landing_page)['new_page']
# 添加截距列,命名为intercept
df2['intercept'] = 1
df2.head()
user_idtimestampgrouplanding_pageconverteddatenew_pageintercept
08511042017-01-21 22:11:48.556739controlold_page02017-01-2101
18042282017-01-12 08:01:45.159739controlold_page02017-01-1201
26615902017-01-11 16:55:06.154213treatmentnew_page02017-01-1111
38535412017-01-08 18:28:03.143765treatmentnew_page02017-01-0811
48649752017-01-21 01:52:26.210827controlold_page12017-01-2101

Sklearn.LogisticRegression

导入sklearn.LogisticRegression拟合样本数据,求出斜率和截距。sklearn.LogisticRegression在初始化LogisticRegression时,默认fit_intercept=True,所以不需要手动添加截距列,所以只需要输入实际的特征列和标签列输入即可。

from sklearn.linear_model import LogisticRegression
label = np.array(df2.converted)
feature = np.array(df2.new_page).reshape(-1,1)
log_reg = LogisticRegression()
log_reg.fit(feature,label)
LogisticRegression()
print(f'斜率: {log_reg.coef_[0][0]}')
print(f'截距:{log_reg.intercept_[0]}')
斜率: -0.014987153300886243
截距:-1.9887780265813249

Statsmodels.Logit

使用statsmodels.Logit导入数据,计算相应的统计数据和回归模型,生成统计报告。statsmodel需要手动添加截距共同构成特征矩阵

feature1 = np.concatenate((feature,np.array(df2.intercept).reshape(-1,1)),axis=1)
logit_mod = sm.Logit(endog=label,exog=feature1)
logit_res = logit_mod.fit()
Optimization terminated successfully.
         Current function value: 0.366118
         Iterations 6
logit_res.summary(yname='converted',
                 xname=['new_page','intercept'])
Logit Regression Results
Dep. Variable:converted No. Observations: 290584
Model:Logit Df Residuals: 290582
Method:MLE Df Model: 1
Date:Thu, 25 Mar 2021 Pseudo R-squ.: 8.077e-06
Time:16:26:41 Log-Likelihood: -1.0639e+05
converged:True LL-Null: -1.0639e+05
Covariance Type:nonrobust LLR p-value: 0.1899
coefstd errzP>|z|[0.0250.975]
new_page -0.0150 0.011 -1.311 0.190 -0.037 0.007
intercept -1.9888 0.008 -246.669 0.000 -2.005 -1.973
  • 回归中的零假设与备择假设:
    H 0 : b 1 = 0 H_0:b_1 = 0 H0:b1=0
    H 1 : b 1 ≠ 0 H_1:b_1 \neq 0 H1:b1=0
    ( b 1 是 样 本 中 某 一 个 变 量 与 反 应 变 量 的 斜 率 ) (b_1是样本中某一个变量与反应变量的斜率) (b1)

回归中的p值是用于测试特征变量与反应变量是否存在统计显著性的线性关系,这里是否new_page与是否转化的p-值为0.190(双尾检验),大于显著性水平0.05,即没有足够的证据拒绝原假设,即接收到新网页与转化率没有统计显著性的线性关系,即新旧网页与转化率的变化没有关系。

np.exp(-0.0150)
0.9851119396030626

即每接受一个new_page, 转化与非转化的对数比较优势增加的0.985倍

添加其他变量

添加国家项作为新的特征变量,观察是否转化与用户所处的国家之间是否存在一定的相关性,如无论网页类型的不同,某些国家相较于其他国家更容易转化

countries = pd.read_csv(r'E:\Data Analysis\统计学\simple_abtest\countries.csv')
countries.user_id= countries.user_id.astype(str)
# df2 with country columns as df3
df3 = df2.merge(countries,on='user_id')
# 添加country虚拟变量,为了避免特征之间的共线,只取CA和UK列作为国家特征
feature_with_coutries = np.concatenate((feature,pd.get_dummies(df3.country)[['CA','UK']].values), axis=1)
# 添加截距
feature2 = sm.add_constant(feature_with_coutries)
# 拟合数据,生成线性模型
logic_mod_country = sm.Logit(label,feature2)
logic_reg_country = logic_mod_country.fit()
logic_reg_country.summary(yname='converted',xname=['intercept','new_page','CA','UK'])
Optimization terminated successfully.
         Current function value: 0.366113
         Iterations 6
Logit Regression Results
Dep. Variable:converted No. Observations: 290584
Model:Logit Df Residuals: 290580
Method:MLE Df Model: 3
Date:Thu, 25 Mar 2021 Pseudo R-squ.: 2.323e-05
Time:16:29:11 Log-Likelihood: -1.0639e+05
converged:True LL-Null: -1.0639e+05
Covariance Type:nonrobust LLR p-value: 0.1760
coefstd errzP>|z|[0.0250.975]
intercept -1.9893 0.009 -223.763 0.000 -2.007 -1.972
new_page -0.0149 0.011 -1.307 0.191 -0.037 0.007
CA -0.0408 0.027 -1.516 0.130 -0.093 0.012
UK 0.0099 0.013 0.743 0.457 -0.016 0.036
np.exp(-0.0149),np.exp(-0.0408),np.exp(0.0099)
(0.9852104557227469, 0.9600211149716509, 1.0099491671175422)

根据以上统计输出,不同国家与转化率之间没有显著性的线性关系,不管是通过p值 ( 0.191 , 0.13 , 0.457 ) (0.191,0.13, 0.457) (0.191,0.13,0.457)均大于显著性水平 α = 0.05 \alpha=0.05 α=0.05,还是通过求幂指数化系数 ( 0.985 , 0.96 , 1 ) (0.985, 0.96, 1) 0.985,0.96,1,都表明不同国家与总转化率变化之间没有统计显著性的线性关系。

添加交互项

观察不同国家的用户对于接受的网页类型(新旧网页)的不同是否与转化率之间存在一定的线性关系

  • 优点:
    在回归模型中加入交互项,它可以极大的拓展回归模型对变量之间的依赖的解释。在实际中,如果我们的变量之间有关系的话,那么加入回归项能更好地是模型反映变量之间的关系。
  • 弊端:
    • 可能出现多重共线性
    • 新附加项中的异常值
    • 出现非恒定方差和正态分布误差,从而影响置信区间和p值
    • 使用过去数据预测未来容易出现相关误差
coutries_array = pd.get_dummies(df3.country)[['CA','UK']].values
countries_inter_page = df3.new_page.values.reshape(-1,1) * coutries_array
feature3 = np.concatenate((feature2, countries_inter_page), axis=1)
Im3 = sm.Logit(label,feature3)
result3 = Im3.fit()
result3.summary(yname='converted',xname=['intercept','new_page','CA','UK','new_CA','new_UK'])
Optimization terminated successfully.
         Current function value: 0.366109
         Iterations 6
Logit Regression Results
Dep. Variable:converted No. Observations: 290584
Model:Logit Df Residuals: 290578
Method:MLE Df Model: 5
Date:Thu, 25 Mar 2021 Pseudo R-squ.: 3.482e-05
Time:16:46:52 Log-Likelihood: -1.0639e+05
converged:True LL-Null: -1.0639e+05
Covariance Type:nonrobust LLR p-value: 0.1920
coefstd errzP>|z|[0.0250.975]
intercept -1.9865 0.010 -206.344 0.000 -2.005 -1.968
new_page -0.0206 0.014 -1.505 0.132 -0.047 0.006
CA -0.0175 0.038 -0.465 0.642 -0.091 0.056
UK -0.0057 0.019 -0.306 0.760 -0.043 0.031
new_CA -0.0469 0.054 -0.872 0.383 -0.152 0.059
new_UK 0.0314 0.027 1.181 0.238 -0.021 0.084

通过以上统计输出可以得出,每个特征的P值均大于置信水平 α = 0.05 \alpha=0.05 α=0.05,说明变量与是否转化不存在统计显著性的线性关系。

结论

通过以上A/B测试和回归分析的结果,新页面与旧页面在转化率上没有显著性差异,所以不推荐电商发布新页面。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值