區(qū)塊鏈開發(fā)書籍推薦搜索引擎優(yōu)化課程
AB試驗(七)利用Python模擬A/B試驗
到現(xiàn)在,我相信大家理論已經(jīng)掌握了,輪子也造好了。但有的人是不是總感覺還差點什么?沒錯,還缺了實戰(zhàn)經(jīng)驗。對于AB實驗平臺完善的公司 ,這個經(jīng)驗不難獲得,但有的同學(xué)或多或少總有些原因無法接觸到AB實驗。所以本文就告訴大家,如何利用Python完整地進行一次A/B試驗?zāi)M。
現(xiàn)在,前面造好的輪子ABTestFunc.py
就起到關(guān)鍵作用了
from faker import Faker
from faker.providers import BaseProvider, internet
from random import randint
from scipy.stats import bernoulli
import pandas as pd
import numpy as np
from sklearn.model_selection import train_test_split
from scipy import stats
from collections import defaultdict
import toad
import matplotlib.pyplot as plt
import seaborn as sns
import math# 繪圖初始化
%matplotlib inline
sns.set(style="ticks")
plt.rcParams['axes.unicode_minus']=False # 用來正常顯示負(fù)號
plt.rcParams['font.sans-serif'] = ['SimHei'] # 用來正常顯示中文標(biāo)簽
plt.rcParams['axes.unicode_minus'] = False # 用來正常顯示負(fù)號# 導(dǎo)入自定義模塊
import sys
sys.path.append("/Users/heinrich/Desktop/Heinrich-blog/數(shù)據(jù)分析使用手冊")
from ABTestFunc import *
上述自定義模塊
ABTestFunc
如果有需要的同學(xué)可關(guān)注公眾號HsuHeinrich,回復(fù)【AB試驗-自定義函數(shù)】自動獲取~
均值類指標(biāo)實驗?zāi)M
實驗前準(zhǔn)備
- 背景:某app想通過優(yōu)化購物車來提高用戶的人均消費,遂通過AB實驗檢驗優(yōu)化效果。
- 實驗前設(shè)定
- 實驗為雙尾檢驗
- 實驗分流為50%/50%
- 顯著性水平為5%
- 檢驗功效為80%
# 實驗設(shè)定
alpha=0.05
power=0.8
beta=1-power
確定目標(biāo)和假設(shè)
- 目標(biāo):提高人均消費
- 假設(shè):選擇商品時,醒目提示各商品優(yōu)惠金額,并按照優(yōu)惠截止日期排序,提高緊促感。
確定指標(biāo)
- 評價指標(biāo):人均購買金額
- 護欄指標(biāo):樣本比例、特征分布一致
確定實驗單位
- 用戶ID
樣本量估算
- 模擬歷史樣本
# 假設(shè)用戶的購買金額服從正態(tài)分布
# 模擬過去一段時間的用戶購買金額
np.random.seed(0)
pays=np.random.normal(2999, 876, 50000)
plt.hist(pays, 30, density=True)
plt.show()
# 輸出當(dāng)前消費金額的均值
print(pays.mean())
# 輸出當(dāng)前消費金額的方差
print(np.std(pays, ddof=1))
# 計算歷史數(shù)據(jù)的波動區(qū)間,并假設(shè)此次提升高于最大波動上限
print(numbers_cal_ci(pays))
2995.676447900933
873.0773017854648
[2988.0237285541257, 3003.3291672477403]
- 依據(jù)提升情況計算樣本量
# 當(dāng)前消費均值為2996,方差為873,波動上限為3003。
# 假設(shè)此次實驗?zāi)芴岣呦M金額至3050元
u1=2996
u2=3050
s=np.std(pays, ddof=1)n1=n2=numbers_cal_sample_third(u1, u2, s)
print(2*n1)
8210
隨機分組
- CR法
測試時間的估算
# 假設(shè)每天用戶流量680,且用戶在周終于周末的購買行為不一致,因此至少包含一周的時間
test_time=max(math.ceil(2*n1/680), 7)
print(test_time)
13
實施測試
- 測試過程無明顯異常
- 模擬實驗數(shù)據(jù)產(chǎn)生,并在結(jié)束時收集數(shù)據(jù)
# 自定義數(shù)據(jù)
fake = Faker('zh_CN')
class MyProvider(BaseProvider):def myCityLevel(self):cl = ["一線", "二線", "三線", "四線+"]return cl[randint(0, len(cl) - 1)]def myGender(self):g = ['F', 'M']return g[randint(0, len(g) - 1)]def myDevice(self):d = ['Ios', 'Android']return d[randint(0, len(d) - 1)]
fake.add_provider(MyProvider)# 構(gòu)造假數(shù)據(jù),模擬實驗過程產(chǎn)生的樣本數(shù)據(jù)的特征
uid=[]
cityLevel=[]
gender=[]
device=[]
age=[]
activeDays=[]
for i in range(8225):uid.append(i+1)cityLevel.append(fake.myCityLevel())gender.append(fake.myGender())device.append(fake.myDevice())age.append(fake.random_int(min=18, max=45)) # 年齡分布activeDays.append(fake.random_int(min=0, max=7)) # 近7日活躍分布raw_data= pd.DataFrame({'uid':uid,'cityLevel':cityLevel,'gender':gender,'device':device,'age':age,'activeDays':activeDays,})raw_data.head()
# 數(shù)據(jù)隨機切分,模擬實驗分流
test, control= train_test_split(raw_data.copy(), test_size=.5, random_state=0)
# 模擬用戶購買金額
np.random.seed(1)
test['pays']=np.random.normal(3049, 850, test.shape[0])
control['pays']=np.random.normal(2999, 853, control.shape[0])
# 數(shù)據(jù)拼接,模擬數(shù)據(jù)收集結(jié)果
test['flag'] = 'test'
control['flag'] = 'control'
df = pd.concat([test, control])
分析測試結(jié)果
- 樣本比例合理性檢驗
# 查看樣本比例
sns.countplot(x='flag', data=df)
plt.show()# 查看離散變量的分布
fig, ax =plt.subplots(1, 3, constrained_layout=True, figsize=(12, 3))
for i, x in enumerate(['cityLevel', 'gender', 'device']):sns.countplot(x=x, data=df, hue='flag', ax=ax[i])
plt.show()# 查看連續(xù)變量的分布
fig, ax =plt.subplots(1, 3, constrained_layout=True, figsize=(12, 3))
for i, x in enumerate(['age', 'activeDays', 'pays']):sns.histplot(x=x, data=df, hue='flag', ax=ax[i])
plt.show()
# 檢驗樣本比例一致性
n1=control.size
n2=test.size
p1=p2=0.5
two_sample_proportion_test(n1, n2, p1, p2)
兩樣本比例校驗: 通過
- 樣本特征一致性校驗
# 檢驗特征分布一致性
cols=['cityLevel', 'gender', 'device', 'age', 'activeDays']
feature_dist_ks(cols, test, control)
cityLevel: 相似
gender: 相似
device: 相似
age: 相似
activeDays: 相似
- 顯著性校驗
# 顯著性檢驗
numbers_cal_significant(test['pays'], control['pays'])
方差齊性校驗結(jié)果:方差相同(3.1882855769529668,0.0014365540563265368,[23.101471736420166, 96.85309892416026])
p值小于5%,置信區(qū)間不包含0且最小提升為23,明顯高于自然波動的上線。因此可以認(rèn)為此次購物車優(yōu)化實驗有助于提高用戶的人均消費
- 拓展-維度下鉆分析
# 進行維度下鉆分析,采用BH法進行多重檢驗校正
feature=[]
value=[]
pvaules=[]
for x in ['cityLevel', 'gender', 'device']:for i in df[x].unique():feature.append(x)value.append(i)# 構(gòu)造細(xì)分維度的樣本te=test[test[x]==i]co=control[control[x]==i]# 計算細(xì)分維度的p值p=numbers_cal_significant(te['pays'], co['pays'], levene_print=False)[1]pvaules.append(p)df_multiple=pd.DataFrame({'feature':feature,'value':value,'pvaules':pvaules})
df_multiple
# 多重檢驗校正
print(multiple_tests_adjust(df_multiple['pvaules']))
df_multiple['pvaules_correct']=multiple_tests_adjust(df_multiple['pvaules'])[1]
df_multiple['reject']=multiple_tests_adjust(df_multiple['pvaules'])[0]
df_multiple
(array([False, False, False, False, False, False, True, False]), array([0.05672733, 0.19828707, 0.05672733, 0.57652105, 0.05672733,0.05672733, 0.04760055, 0.10353442]), 0.00625)
維度下鉆發(fā)現(xiàn),只有iOS設(shè)備的用戶存在顯著提升
實驗報告
# 關(guān)鍵數(shù)據(jù)展示# 樣本及均值
print('control:' ,f'sample {control.shape[0]} / mean:{control.pays.mean()}')
print('test:' ,f'sample {test.shape[0]} / mean:{test.pays.mean()}')
# 實驗周期
print('times:', test_time)
# diff
print('diff:', test['pays'].mean()-control['pays'].mean())
# p值
print('p-value:', numbers_cal_significant(test['pays'], control['pays'], levene_print=False)[1])
# diff-置信區(qū)間
print('diff-ci:', numbers_cal_significant(test['pays'], control['pays'], levene_print=False)[2])
# 維度下鉆結(jié)果
print('dim-result:')
for i,v in zip(df_multiple.value,df_multiple.reject):print(' '*2,f'{i}:{v}')
control: sample 4113 / mean:3000.5565990602113
test: sample 4112 / mean:3060.533884390513
times: 13
diff: 59.977285330301584
p-value: 0.0014365540563265368
diff-ci: [23.101471736420166, 96.85309892416026]
dim-result:三線:False二線:False四線+:False一線:FalseM:FalseF:FalseIos:TrueAndroid:False
- 實驗13天,收集到實驗組數(shù)據(jù)4112,對照組4113,共計8225。
- 實驗過程無異常,實驗組人均購買金額為3061元,較對照組提高60元
- 整體上,實驗組的提升是顯著的,且提升范圍在
[23, 97]
元之間- 通過維度下鉆,發(fā)現(xiàn)實驗組僅在Ios設(shè)備用戶有顯著提升
概率類指標(biāo)實驗?zāi)M
實驗前準(zhǔn)備
- 背景:某音樂app想通過優(yōu)化功能提示提高用戶功能使用率。
- 實驗前設(shè)定
- 實驗為雙尾檢驗
- 實驗分流為50%/50%
- 顯著性水平為5%
- 檢驗功效為80%
# 實驗設(shè)定
alpha=0.05
power=0.8
beta=1-power
確定目標(biāo)和假設(shè)
- 目標(biāo):提高【把喜歡的音樂加入收藏夾】功能的使用率
- 假設(shè):用戶從未使用過這個功能,且播放同一首歌到達(dá)4次時,在播放第5次進行彈窗提醒可以把喜歡的音樂加入收藏夾
確定指標(biāo)
- 評價指標(biāo):【把喜歡的音樂加入收藏夾】功能的使用率
- 護欄指標(biāo):樣本比例、特征分布一致
確定實驗單位
- 用戶ID
樣本量估算
- 模擬歷史樣本
# 假設(shè)用戶的購買金額服從正態(tài)分布
# 模擬過去一段時間的用戶【把喜歡的音樂加入收藏夾】
np.random.seed(1)
collect=stats.bernoulli.rvs(0.02, size=20000, random_state=0)
plt.hist(collect, 30, density=True)
plt.show()
# 輸出當(dāng)前【把喜歡的音樂加入收藏夾】功能的使用率
print(collect.mean())
# 計算歷史數(shù)據(jù)的波動區(qū)間,并假設(shè)此次提升高于最大波動上限
print(prob_cal_ci(0.02, 20000))
0.0197
[0.01805973464591045, 0.02194026535408955]
- 依據(jù)提升情況計算樣本量
# 當(dāng)前轉(zhuǎn)化率為0.02,波動上限為0.0219。
# 假設(shè)此次實驗?zāi)芴岣呤褂寐手?.022
p1=0.02
p2=0.022n1=n2=prob_cal_sample_third(p1, p2)
print(2*n1)
161276
隨機分組
- CR法
測試時間的估算
# 假設(shè)每天符合條件用戶流量1.7w,且用戶在周終于周末的聽音樂行為不一致,因此至少包含一周的時間
test_time=max(math.ceil(2*n1/17000), 7)
print(test_time)
10
實施測試
- 測試過程無明顯異常
- 模擬實驗數(shù)據(jù)產(chǎn)生,并在結(jié)束時收集數(shù)據(jù)
# 自定義數(shù)據(jù)
fake = Faker('zh_CN')
class MyProvider(BaseProvider):def myCityLevel(self):cl = ["一線", "二線", "三線", "四線+"]return cl[randint(0, len(cl) - 1)]def myGender(self):g = ['F', 'M']return g[randint(0, len(g) - 1)]def myDevice(self):d = ['Ios', 'Android']return d[randint(0, len(d) - 1)]
fake.add_provider(MyProvider)# 構(gòu)造假數(shù)據(jù),模擬實驗過程產(chǎn)生的樣本數(shù)據(jù)的特征
uid=[]
cityLevel=[]
gender=[]
device=[]
age=[]
activeDays=[]
for i in range(161280):uid.append(i+1)cityLevel.append(fake.myCityLevel())gender.append(fake.myGender())device.append(fake.myDevice())age.append(fake.random_int(min=18, max=45)) # 年齡分布activeDays.append(fake.random_int(min=0, max=7)) # 近7日活躍分布raw_data= pd.DataFrame({'uid':uid,'cityLevel':cityLevel,'gender':gender,'device':device,'age':age,'activeDays':activeDays,})raw_data.head()
# 數(shù)據(jù)隨機切分,模擬實驗分流
test, control= train_test_split(raw_data.copy(), test_size=.5, random_state=0)
# 模擬用戶收藏轉(zhuǎn)化率
test['collect']=stats.bernoulli.rvs(0.023, size=test.shape[0], random_state=0)
control['collect']=stats.bernoulli.rvs(0.02, size=control.shape[0], random_state=0)
# 數(shù)據(jù)拼接,模擬數(shù)據(jù)收集結(jié)果
test['flag'] = 'test'
control['flag'] = 'control'
df = pd.concat([test, control])
分析測試結(jié)果
- 樣本比例合理性檢驗
# 查看樣本比例
sns.countplot(x='flag', data=df)
plt.show()# 查看離散變量的分布
fig, ax =plt.subplots(1, 3, constrained_layout=True, figsize=(12, 3))
for i, x in enumerate(['cityLevel', 'gender', 'device']):sns.countplot(x=x, data=df, hue='flag', ax=ax[i])
plt.show()# 查看連續(xù)變量的分布
fig, ax =plt.subplots(1, 3, constrained_layout=True, figsize=(12, 3))
for i, x in enumerate(['age', 'activeDays', 'collect']):sns.histplot(x=x, data=df, hue='flag', ax=ax[i])
plt.show()
# 檢驗樣本比例一致性
n1=control.size
n2=test.size
p1=p2=0.5
two_sample_proportion_test(n1, n2, p1, p2)
兩樣本比例校驗: 通過
- 樣本特征一致性校驗
# 檢驗特征分布一致性
cols=['cityLevel', 'gender', 'device', 'age', 'activeDays']
feature_dist_ks(cols, test, control)
cityLevel: 相似
gender: 相似
device: 相似
age: 相似
activeDays: 相似
- 顯著性檢驗
# 顯著性檢驗
count1=test['collect'].sum()
nobs1=test['collect'].size
count2=control['collect'].sum()
nobs2=control['collect'].sizeprob_cal_significant(count1, nobs1, count2, nobs2)
(3.8761435754191123,0.00010612507775057984,[0.0013796298310413291, 0.004202600259759636])
- p值小于5%,置信區(qū)間不包含0。因此整體上可以認(rèn)為此次優(yōu)化有助于提高【把喜歡的音樂加入收藏夾】功能的使用率。
- 但是需要注意置信區(qū)間最小提升為0.0014,而在自然波動的最大提升是0.0019(0.0219-0.02),所以此次提升有可能在自然波動范圍內(nèi),可能存在業(yè)務(wù)不顯著,需要額外關(guān)注。
- 拓展-維度下鉆分析
# 進行維度下鉆分析,采用BH法進行多重檢驗校正
feature=[]
value=[]
pvaules=[]
for x in ['cityLevel', 'gender', 'device']:for i in df[x].unique():feature.append(x)value.append(i)# 構(gòu)造細(xì)分維度的樣本te=test[test[x]==i]co=control[control[x]==i]# 計算細(xì)分維度的p值c1=te['collect'].sum()n1=te['collect'].sizec2=co['collect'].sum()n2=co['collect'].sizep=prob_cal_significant(c1, n1, c2, n2)[1]pvaules.append(p)df_multiple=pd.DataFrame({'feature':feature,'value':value,'pvaules':pvaules})
df_multiple
# 多重檢驗校正
print(multiple_tests_adjust(df_multiple['pvaules']))
df_multiple['pvaules_correct']=multiple_tests_adjust(df_multiple['pvaules'])[1]
df_multiple['reject']=multiple_tests_adjust(df_multiple['pvaules'])[0]
df_multiple
(array([False, False, False, True, True, True, True, True]), array([7.60220226e-01, 7.54367218e-02, 1.49044597e-01, 3.53217899e-05,1.77798888e-02, 1.10151024e-02, 1.10151024e-02, 1.97199451e-02]), 0.00625)
維度下鉆發(fā)現(xiàn),一線、二線和四線+城市提升不顯著
實驗報告
# 關(guān)鍵數(shù)據(jù)展示# 樣本及均值
print('control:' ,f'sample {control.shape[0]} / mean:{control.collect.mean()}')
print('test:' ,f'sample {test.shape[0]} / mean:{test.collect.mean()}')
# 實驗周期
print('times:', test_time)
# diff
print('diff:', test['collect'].mean()-control['collect'].mean())
# p值
print('p-value:', prob_cal_significant(count1, nobs1, count2, nobs2)[1])
# diff-置信區(qū)間
print('diff-ci:', prob_cal_significant(count1, nobs1, count2, nobs2)[2])
# 維度下鉆結(jié)果
print('dim-result:')
for i,v in zip(df_multiple.value,df_multiple.reject):print(' '*2,f'{i}:{v}')
control: sample 80640 / mean:0.019952876984126983
test: sample 80640 / mean:0.022743055555555555
times: 10
diff: 0.002790178571428572
p-value: 0.00010612507775057984
diff-ci: [0.0013796298310413291, 0.004202600259759636]
dim-result:二線:False四線+:False一線:False三線:TrueF:TrueM:TrueAndroid:TrueIos:True
- 實驗10天,收集到實驗組數(shù)據(jù)80640,對照組80640,共計161280。
- 實驗過程無異常,實驗組人均收藏率為0.023,較對照組提高0.003
- 整體上,實驗組的提升是顯著的,且提升范圍在
[0.001, 0.004]
之間。但可能存在業(yè)務(wù)不顯著,需要額外關(guān)注- 通過維度下鉆,發(fā)現(xiàn)實驗組在一線、二線和四線+城市提升不顯著
總結(jié)
現(xiàn)在,關(guān)于均值類和概率類的所有實驗細(xì)節(jié)和模擬實戰(zhàn)都已結(jié)束,相信大家對如何科學(xué)地進行A/B試驗已經(jīng)了然于胸了吧~
共勉~