目录
简介
A/B 测试是数据分析师和数据科学家需要经常完成的工作,非常重要。
本项目设定了一家电子商务网站运行 A/B 测试的情境。目标是通过这个项目来帮助公司分析和决定他们是否应该使用新的页面,保留旧的页面,或者应该将测试时间延长,之后再做出决定。
I - 概率
先导入数据。
In [1]:
import pandas as pd
import numpy as np
import random
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)
1.
导入 ab_data.csv
数据,并将其存储在 df
中。
a. 导入数据集,查看前几行数据:
In [2]:
df=pd.read_csv('ab_data.csv')
df.head()
Out[2]:
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 |
b. 查看数据集的行数。
In [3]:
df.shape[0]
Out[3]:
294478
c. 查看数据集中的用户数量(不同的user_id数)。
In [4]:
df.user_id.nunique()
Out[4]:
290584
d. 转化用户的占比。
In [5]:
df.converted.mean()
Out[5]:
0.11965919355605512
e. 计算数据中 new_page
与 treatment
不匹配的次数。提示:在一个合理的实验设计里,控制组(control) 应该对应旧页面(old_page),实验组(treatment) 对应新页面(new_page)。
In [6]:
df.query("(group == 'control' and landing_page == 'new_page') or (group == 'treatment' and landing_page == 'old_page')")['user_id'].count()
Out[6]:
3893
f. 是否有任何行空缺数值?
In [7]:
df.isnull().any()
Out[7]:
user_id False
timestamp False
group False
landing_page False
converted False
dtype: bool
2.
对于 treatment 和 new_page 不匹配的行或 control 与 old_page 不匹配的行,我们不能确定该行是否接收到了新页面还是旧页面。如何处理这些行?
a. 将新 dataframe 存储在 df2 中。
In [8]:
df2 = df.query("(group == 'control' and landing_page == 'old_page') or (group == 'treatment' and landing_page == 'new_page')")
In [9]:
# Double Check all of the correct rows were removed - this should be 0
df2[((df2['group'] == 'treatment') == (df2['landing_page'] == 'new_page')) == False].shape[0]
Out[9]:
0
3.
根据 df2 来回答以下问题。
a. df2 中的用户数量(不同的 user_id) ?
In [10]:
df2['user_id'].nunique()
Out[10]:
290584
b. df2 中有一个重复的 user_id 。它是什么?
In [11]:
df2[df2['user_id'].duplicated()]['user_id']
Out[11]:
2893 773192
Name: user_id, dtype: int64
c. 这个重复 user_id 的行信息是什么?
In [12]:
df2[df2['user_id'].duplicated()]
Out[12]:
user_id | timestamp | group | landing_page | converted | |
---|---|---|---|---|---|
2893 | 773192 | 2017-01-14 02:55:59.590927 | treatment | new_page | 0 |
d. 删除一行重复行,但仍然存储 dataframe 为 df2。
In [13]:
df2 = df2.drop_duplicates(subset='user_id')
4.
使用优化过的 df2 来回答以下问题。
a. 用户成功转化的整体概率是多少?(不分旧页面或者新页面)
In [14]:
df2['converted'].mean()
Out[14]:
0.11959708724499628
b. control
组用户的转化率是多少?
In [15]:
df2[df2['group'] == 'control']['converted'].mean()
Out[15]:
0.1203863045004612
c. treatment
组用户的转化率是多少?
In [16]:
df2[df2['group'] == 'treatment']['converted'].mean()
Out[16]:
0.11880806551510564
d. 一个用户收到新页面的概率是多少?
In [17]:
len(df2[df2['landing_page'] == 'new_page'])/len(df2)
Out[17]:
0.5000619442226688
e. 分析到这里,是否认为有足够的数据支持来证明旧页面或者新页面可以带来更高的转化率?
用户有大约50%的概率接收到新页面作为登录页面,控制组和实验组的用户数量大致相同。 控制组比实验组的转化率略高,但差异很小。无论属于哪个组,和用户成功转化的整体概率差异都不大。
没有足够的数据支持来证明旧页面或者新页面可以带来更高的转化率。
II - A/B 测试
因为每个事件都对应有一个时间记录(time stamp 时间戳),所以技术上你可以实现每次观察都连续运行假设检验。
然而,问题的难点在于,什么时候停止你的试验:是在发现其中一组的试验效果足够好时立即停止?还是在这样的观察结果又持续发生了一段时间再停止?需要运行多长时间才能确认两个页面没有带来用户转化率的显著差异?
1.
现在,你需要根据我们提供的数据做出决策:你假设旧页面效果更佳,除非在一类错误在5%以内,新页面被证明更好。基于此,你的零假设和备择假设是什么? 你可以用文字表述或用 poldpold 与 pnewpnew (即,旧页面转化率与新页面转化率)来陈述你的假设。
零假设:pnewpnew-poldpold<=0, 旧页面效果比新页面效果好或者差不多
备择假设:pnewpnew-poldpold>0,新页面效果比旧页面效果好
2.
假定在零假设中,不管是新页面还是旧页面, pnewpnew 与 poldpold 都有相同的转化成功的概率,也就是说, pnewpnew 与 poldpold 是相等的。此外,我们还假设它们都等于ab_data.csv 中的 转化率(converted)。
现在,在新旧页面上执行抽样分布,并计算 转化(converted) 差异。记住,每个页面的样本大小要与 ab_data.csv 相同。计算零假设中10000次迭代计算的估计值。
a. 在零假设中,pnewpnew 转化率 是多少?
In [18]:
p_new = df2['converted'].mean()
p_new
Out[18]:
0.11959708724499628
b. 在零假设中, poldpold 转化率 是多少?
In [19]:
p_old = df2['converted'].mean()
p_old
Out[19]:
0.11959708724499628
c. nnewnnew 是多少?
In [20]:
n_new = len(df2[df2['landing_page'] == 'new_page'])
n_new
Out[20]:
145310
d. noldnold?是多少?
In [21]:
n_old=len(df2.query('landing_page=="old_page"'))
n_old
Out[21]:
145274
e. 在零假设中,使用 pnewpnew (新页面的转化率)模拟 nnewnnew 个新页面的转化,并将这些 nnewnnew 个 1 和 0 存储在 new_page_converted 中。(可以使用numpy.random.choice。)
In [22]:
new_page_converted = np.random.choice(2, n_new, p=[p_new,1-p_new])
new_page_converted
Out[22]:
array([1, 1, 0, ..., 1, 1, 1])
f. 在零假设中,使用 poldpold (旧页面的转化率)模拟 noldnold 个旧页面的转化,并将这些 noldnold 个 1 和 0 存储在 old_page_converted 中。
In [23]:
old_page_converted = np.random.choice(2, n_old, p=[p_old,1-p_old])
old_page_converted
Out[23]:
array([1, 1, 1, ..., 1, 1, 1])
g. 根据 e 和 f,计算 pnewpnew 和 poldpold 的差异值(pnewpnew - poldpold)。
In [24]:
p_diff = new_page_converted.mean() - old_page_converted.mean()
p_diff
Out[24]:
-0.0024480971790489248
h. 由于单个数值不能形成分布图形,请参考以上a-g的过程,模拟 10,000 个 pnewpnew 与 poldpold 差异值(pnewpnew - poldpold),将这 10,000 个值存储在 p_diffs 中。
In [25]:
p_diffs=[]
for i in range(10000):
new_page_converted = np.random.choice(2, n_new, p=[p_new,1-p_new])
old_page_converted = np.random.choice(2, n_old, p=[p_old,1-p_old])
p_diffs.append(new_page_converted.mean() - old_page_converted.mean())
p_diffs = np.array(p_diffs)
i. 绘制一个 p_diffs 分布图形。图形跟你的预期相符吗?
In [26]:
plt.hist(p_diffs)
Out[26]:
(array([ 8., 82., 388., 1338., 2453., 2881., 1947., 701.,
183., 19.]),
array([-0.00470562, -0.00380745, -0.00290927, -0.0020111 , -0.00111293,
-0.00021475, 0.00068342, 0.00158159, 0.00247976, 0.00337794,
0.00427611]),
<a list of 10 Patch objects>)
j. p_diffs列表的数值中,有多少比例的数值会大于 ab_data.csv 中观察到的实际转化率差异 ?
In [27]:
actual_new_mean = df2[df2['group'] == 'treatment']['converted'].mean()
actual_old_mean = df2[df2['group'] == 'control']['converted'].mean()
actual_diff = actual_new_mean - actual_old_mean
actual_diff
Out[27]:
-0.0015782389853555567
In [28]:
(p_diffs > actual_diff).mean()
Out[28]:
0.90290000000000004
k. 用文字解释一下你刚才在 j. 中计算出来的结果。在数据研究中,这个值是什么? 根据这个数值,请判断新旧页面的转化率是否有显著差异。
上述计算过程中,模拟了 10,000 个 𝑝𝑛𝑒𝑤 与 𝑝𝑜𝑙𝑑 的差异值,并以此绘制图形。根据所提供数据计算出实际转化率差异,比较模拟了10,000个转化率的差异值和实际转化率差异。这个值是 p 值。大约有90%的概率大于 ab_data.csv 中观察到的实际转化率差异,P值非常大说明无法拒绝零假设,没有足够的证据支持新旧页面的转化率有显著差异。
l. 我们也可以使用一个内置程序 (built-in)来实现类似的结果。使用内置程序可能很容易就能取得结果,但上面的内容仍然很重要,它可以训练你具有正确的数据统计思维。填写下面的内容来计算每个页面的转化次数,以及收到每个页面的用户数。计算新旧页面出现的次数,也就是数据中 n_old
与 n_new
分别出现的行数。
In [42]:
import statsmodels.api as sm
convert_old = len(df2[(df2['landing_page'] == 'old_page') & (df2['converted'] == 1)])
convert_new = len(df2[(df2['landing_page'] == 'new_page') & (df2['converted'] == 1)])
n_old = len(df2[df2['landing_page'] == 'old_page'])
n_new = len(df2[df2['landing_page'] == 'new_page'])
m. 现在使用 stats.proportions_ztest
来计算你的 z-score (什么是 z-score) 与 p-value。这里 的资料可以提供额外信息。
In [30]:
z_score,p_value=sm.stats.proportions_ztest([convert_old, convert_new], [n_old, n_new], alternative='smaller')
z_score,p_value
Out[30]:
(1.3109241984234394, 0.90505831275902449)
In [31]:
import scipy.stats as st
st.norm.cdf(z_score)
Out[31]:
0.90505831275902449
In [32]:
st.norm.ppf(1-(0.05))
Out[32]:
1.6448536269514722
n. 根据上题算出的 z-score 和 p-value,你认为新旧页面的转化率是否有区别?它们与 j. 与 k. 问题中的结果一致吗?
z-score 1.31 小于临界值 1.64,无法拒绝零假设。P 值 0.905 不小于 0.05,无法拒绝零假设。从 P 值看,这个概率大约有90%。因此,没有足够的证据支持新旧页面的转化率有显著差异,与 j. 与 k. 问题中的结果一致。
III - 回归分析法之一
1.
在最后一部分中,你会看到,你在之前的A / B测试中获得的结果也可以通过执行回归来获取。
a. 既然每行的值是转化或不转化,那么在这种情况下,我们应该执行哪种类型的回归?
逻辑回归
b. 目标是使用 statsmodels 来拟合你在 a. 中指定的回归模型,以查看用户收到的不同页面是否存在显著的转化差异。但是,首先,你需要为这个截距创建一个列( 原文:column) ,并为每个用户收到的页面创建一个虚拟变量列。添加一个 截距 列,一个 ab_page 列,当用户接收 treatment 时为1, control 时为0。
In [33]:
df2[['new_page','old_page']] = pd.get_dummies(df2['landing_page'])
df2['intercept'] = 1
df2['ab_page'] = pd.get_dummies(df['group']) ['treatment']
df2.head()
Out[33]:
c. 使用 statsmodels 导入你的回归模型。 实例化该模型,并使用你在 b. 中创建的2个列来拟合该模型,用来预测一个用户是否会发生转化。
In [34]:
logit_mod=sm.Logit(df2['converted'],df2[['intercept','ab_page']])
result=logit_mod.fit()
Optimization terminated successfully.
Current function value: 0.366118
Iterations 6
d. 请在下方提供你的模型摘要,并根据需要使用它来回答下面的问题。
In [35]:
result.summary()
Out[35]:
Dep. Variable: | converted | No. Observations: | 290584 |
---|---|---|---|
Model: | Logit | Df Residuals: | 290582 |
Method: | MLE | Df Model: | 1 |
Date: | Sun, 17 Mar 2019 | Pseudo R-squ.: | 8.077e-06 |
Time: | 11:48:05 | Log-Likelihood: | -1.0639e+05 |
converged: | True | LL-Null: | -1.0639e+05 |
LLR p-value: | 0.1899 |
coef | std err | z | P>|z| | [0.025 | 0.975] | |
---|---|---|---|---|---|---|
intercept | -1.9888 | 0.008 | -246.669 | 0.000 | -2.005 | -1.973 |
ab_page | -0.0150 | 0.011 | -1.311 | 0.190 | -0.037 | 0.007 |
e. 与 ab_page 关联的 p-值是多少? 为什么它与你在 II 中发现的结果不同?
提示: 与你的回归模型相关的零假设与备择假设分别是什么?它们如何与 Part II 中的零假设和备择假设做比较?
ab_page 的 p 值为0.190。
在回归模型中是双侧检验, Part II 中是单侧检验。这个检验中检验的是不相等, Part II中检验的是大于或等于。
在这个回归模型中
零假设:pnewpnew=poldpold
备择假设:pnewpnew≠poldpold
f. 现在,你一定在考虑其他可能影响用户是否发生转化的因素。讨论为什么考虑将其他因素添加到回归模型中是一个不错的主意。在回归模型中添加附加项有什么弊端吗?
可能有多种因素会影响到转换率,比如时间戳变量,可以检查转换率是否取决于当天的特定时间或用户浏览网站的特定时间。将其他因素添加到回归模型中,也许可以做出更准确的决策。
弊端是,当一些因素组合在一起时,影响会消失或者逆转,如课程中提及的辛普森悖论。
如果添加的自变量彼此相关,会出现多重共线性,影响模型。多重共线性会导致简单线性回归系数偏离我们想要的方向。
g. 现在,除了测试不同页面的转化率是否会发生变化之外,还要根据用户居住的国家或地区添加一个 effect 项。你需要导入 countries.csv 数据集,并将数据集合并在适当的行上。 这里 是链接表格的文档。
这个国家项对转化有影响吗?不要忘记为这些国家的列创建虚拟变量—— 提示: 你将需要为这三个虚拟变量增加两列。 提供统计输出,并书面回答这个问题。
In [36]:
df_countries=pd.read_csv('countries.csv')
df_countries.head(1)
Out[36]:
user_id | country | |
---|---|---|
0 | 834778 | UK |
In [37]:
df_countries['country'].unique()
Out[37]:
array(['UK', 'US', 'CA'], dtype=object)
In [38]:
df3 = df_countries.set_index('user_id').join(df2.set_index('user_id'), how='inner')
In [39]:
df3['intercept'] = 1
df3[['UK','US']] = pd.get_dummies(df3['country'])[['UK','US']]
df3.head()
Out[39]:
In [40]:
logit_mod_3 = sm.Logit(df3['converted'], df3[['intercept', 'UK', 'US']])
result_3 = logit_mod_3.fit()
result_3.summary()
Optimization terminated successfully.
Current function value: 0.366116
Iterations 6
Out[40]:
Dep. Variable: | converted | No. Observations: | 290584 |
---|---|---|---|
Model: | Logit | Df Residuals: | 290581 |
Method: | MLE | Df Model: | 2 |
Date: | Sun, 17 Mar 2019 | Pseudo R-squ.: | 1.521e-05 |
Time: | 11:48:06 | Log-Likelihood: | -1.0639e+05 |
converged: | True | LL-Null: | -1.0639e+05 |
LLR p-value: | 0.1984 |
coef | std err | z | P>|z| | [0.025 | 0.975] | |
---|---|---|---|---|---|---|
intercept | -2.0375 | 0.026 | -78.364 | 0.000 | -2.088 | -1.987 |
UK | 0.0507 | 0.028 | 1.786 | 0.074 | -0.005 | 0.106 |
US | 0.0408 | 0.027 | 1.518 | 0.129 | -0.012 | 0.093 |
从 p 值来看,只有 intercept 的 P 值等于 0,国家项对转化率的影响并不显著,说明国家项对转化率没有多大影响。
h. 虽然你现在已经查看了国家与页面在转化率上的个体性因素,但现在我们要查看页面与国家/地区之间的相互作用,测试其是否会对转化产生重大影响。创建必要的附加列,并拟合一个新的模型。
提供你的摘要结果,以及根据结果得出的结论。
提示:页面与国家/地区的相互作用
df3['new_CA'] = df3['new_page'] * df3['CA']
df3['new_UK'] = df3['new_page'] * df3['UK']
In [41]:
df3['new_UK'] = df3['new_page'] * df3['UK']
df3['new_US'] = df3['new_page'] * df3['US']
logit_mod_new = sm.Logit(df3.converted,df3[['intercept', 'new_page', 'UK', 'US', 'new_UK', 'new_US']])
result_new = logit_mod_new.fit()
result_new.summary()
Optimization terminated successfully.
Current function value: 0.366109
Iterations 6
Out[41]:
Dep. Variable: | converted | No. Observations: | 290584 |
---|---|---|---|
Model: | Logit | Df Residuals: | 290578 |
Method: | MLE | Df Model: | 5 |
Date: | Sun, 17 Mar 2019 | Pseudo R-squ.: | 3.482e-05 |
Time: | 11:48:07 | Log-Likelihood: | -1.0639e+05 |
converged: | True | LL-Null: | -1.0639e+05 |
LLR p-value: | 0.1920 |
coef | std err | z | P>|z| | [0.025 | 0.975] | |
---|---|---|---|---|---|---|
intercept | -2.0040 | 0.036 | -55.008 | 0.000 | -2.075 | -1.933 |
new_page | -0.0674 | 0.052 | -1.297 | 0.195 | -0.169 | 0.034 |
UK | 0.0118 | 0.040 | 0.296 | 0.767 | -0.066 | 0.090 |
US | 0.0175 | 0.038 | 0.465 | 0.642 | -0.056 | 0.091 |
new_UK | 0.0783 | 0.057 | 1.378 | 0.168 | -0.033 | 0.190 |
new_US | 0.0469 | 0.054 | 0.872 | 0.383 | -0.059 | 0.152 |
从 p 值来看,只有 intercept 的 P 值等于 0,页面与国家/地区之间的相互作用对转化率的影响并不显著。
这只是依据样本得出的结论,还有其他方面的因素可以考虑。
总结
从转化率来看,新旧页面并没有太大差异。不推荐这个新页面。可以考虑使用其它因素做相关测试。
参考资料
-
pandas.DataFrame.duplicated
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.duplicated.html
-
pandas.DataFrame.drop_duplicates
https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.drop_duplicates.html
-
返回DataFrame或者array的行数
-
Convert Z-score (Z-value, standard score) to p-value for normal distribution in Python
-
pandas.get_dummies
链接: http://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.get_dummies.html