- セレクションバイアス:比較しているグループの潜在的な傾向が違うことにより発生するバイアス
- RCT (Randomized Controlled Trial):介入の割り当てをランダムにすること。セレクションバイアスを解消できる。
例:メールマーケティングにおいて、「過去の購買量が一定以上」、「最近購買した」のような条件をつけて顧客にメールを配信しているとすると、「メールを配信したユーザ」と「配信していないユーザ」の購買量の差にはメールの効果に加えて潜在的な購買量の差も含まれてしまう。
1.1 メールマーケティングの効果の検証
RCTを適用したメールマーケティングのデータを用いてセレクションバイアスの影響を確かめる。介入は男性向けEMail、女性向けEMailとEMailを送らない、という選択肢を含めて3つある。今回は「男性向けEMailを送信」する介入の効果をEMailを送らなかった場合と比較して分析したいとする。
1.1.1 RCTデータの分析
- 'segment'が'Mens E-Mail'と'No E-Mail'のデータのみ抽出する。
- 'conversion' (メール配信以降2週以内にサイトに来訪したか)、'spend' (購入した際の購入額)について統計量を算出する。
- 'conversion'、'spend'についてのプロットを表示する。
- EMailを配信したか否かで'conversion'、'spend'に差が生じているかを検定する。
import math
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
#---
# 男性向けメールを送ったユーザとメール配信をしなかったユーザのデータを取得
#---
email_data = pd.read_csv('Kevin_Hillstrom_MineThatData_E-MailAnalytics_DataMiningChallenge_2008.03.20.csv')
male_df_EMail = email_data[email_data['segment'] == 'Mens E-Mail']
male_df_No_EMail = email_data[email_data['segment'] == 'No E-Mail']
#---
# 統計量の算出
#---
conversion_rate = [sum(male_df_EMail['conversion']) / len(male_df_EMail),
sum(male_df_No_EMail['conversion']) / len(male_df_No_EMail)]
spend_mean = [sum(male_df_EMail['spend']) / len(male_df_EMail),
sum(male_df_No_EMail['spend']) / len(male_df_No_EMail)]
count = [len(male_df_EMail), len(male_df_No_EMail)]
treatment = [1, 0]
summary_by_segment = pd.DataFrame({'treatment':treatment,
'conversion_rate': conversion_rate,
'spend mean': spend_mean,
'count': count})
summary_by_segment = summary_by_segment.set_index('treatment')
summary_by_segment
#---
# 'conversion'をプロット
#---
fig = plt.figure(figsize = (10, 8))
ax1 = fig.add_subplot(231)
ax1.pie(x = [male_df_EMail['conversion'].value_counts()[0], male_df_EMail['conversion'].value_counts()[1]],
labels = ['not visited', 'visited'],
autopct = '%.2f%%')
ax1.axis('equal')
ax1.set_title('conversion of male_df_EMail')
ax2 = fig.add_subplot(233)
ax2.pie(x = [male_df_No_EMail['conversion'].value_counts()[0], male_df_No_EMail['conversion'].value_counts()[1]],
labels = ['not visited', 'visited'],
autopct = '%.2f%%')
ax2.axis('equal')
ax2.set_title('conversion of male_df_No_EMail')
plt.show()
#---
# 'spend'をプロット
#---
fig, ax = plt.subplots(nrows=2, ncols = 2, figsize=(2,3), dpi=160, sharex='col',
gridspec_kw={'height_ratios': (1,3)} )
ax1, ax2, ax3, ax4 = ax[0][0], ax[0][1], ax[1][0], ax[1][1]
ax1.hist(male_df_EMail['spend'], label = 'EMail')
ax1.set_ylim(200, male_df_EMail['spend'].value_counts()[0] + 1000)
ax3.hist(male_df_EMail['spend'])
ax3.set_ylim(0, 70)
ax2.hist(male_df_No_EMail['spend'], label = 'No EMail')
ax2.set_ylim(200, male_df_No_EMail['spend'].value_counts()[0] + 1000)
ax4.hist(male_df_No_EMail['spend'])
ax4.set_ylim(0, 70)
ax2.tick_params(labelleft = False,left = False)
ax4.tick_params(labelleft = False, left = False)
ax3.spines['top'].set_visible(False)
ax1.spines['bottom'].set_visible(False)
ax1.tick_params(axis='x', which='both', bottom=False, labelbottom=False)
ax4.spines['top'].set_visible(False)
ax2.spines['bottom'].set_visible(False)
ax2.tick_params(axis='x', which='both', bottom=False, labelbottom=False)
ax3.set_xlabel('spend of male_df_EMail', fontsize = 5)
ax4.set_xlabel('male_df_No_EMail', fontsize = 5)
plt.show()
#---
# 母比率検定
#---
n_EMail, n_No_EMail = len(male_df_EMail), len(male_df_No_EMail)
p_EMail, p_No_EMail = conversion_rate[0], conversion_rate[1]
p_hat = (n_EMail * p_EMail + n_No_EMail * p_No_EMail) / (n_EMail + n_No_EMail)
z = (p_EMail - p_No_EMail) / math.sqrt( p_hat * (1 - p_hat) * (1/n_EMail + 1/n_No_EMail) )
p_value = stats.norm.sf(z)
print(f'pp_value = {p_value:.19f}') #出力:p_value = 0.0000000000000761612
1.1.2 バイアスのあるデータの分析
- EMailを配信したユーザのうち、「'history' (昨年の購入額)が300以上」、「'recency' (最後の購入からの経過月数)が6以内」、「'channel' (昨年どのチャネルから購入したか)が'Multichannel'」の条件のいずれかを満たすもののみを残す。
- EMailを配信しなかったユーザのうち、上記の条件を全て満たさなかったもののみを残す。これにより、上記の条件を満たすユーザを選んでメールを配信した、というセレクションバイアスを発生させる。
- 'conversion'、'spend'についてのプロットを表示する。
- EMailを配信したか否かで'conversion'、'spend'に差が生じているかを検定する。
#---
# 条件を満たすデータを抽出
#---
biased_male_df_EMail = male_df_EMail[(male_df_EMail['history'] > 300) | (male_df_EMail['recency'] < 6) |
(male_df_EMail['channel'] == 'Multichannel')]
biased_male_df_No_EMail = male_df_No_EMail[(male_df_No_EMail['history'] < 300) & (male_df_No_EMail['recency'] < 6)
& (male_df_No_EMail['channel'] != 'Multichannel')]
#---
# 統計量の算出
#---
biased_conversion_rate = [sum(biased_male_df_EMail['conversion']) / len(biased_male_df_EMail),
sum(biased_male_df_No_EMail['conversion']) / len(biased_male_df_No_EMail)]
biased_spend_mean = [sum(biased_male_df_EMail['spend']) / len(biased_male_df_EMail),
sum(biased_male_df_No_EMail['spend']) / len(biased_male_df_No_EMail)]
biased_count = [len(biased_male_df_EMail), len(biased_male_df_No_EMail)]
treatment = [1, 0]
biased_summary_by_segment = pd.DataFrame({'treatment':treatment,
'conversion_rate': biased_conversion_rate,
'spend mean': biased_spend_mean,
'count': biased_count})
biased_summary_by_segment = biased_summary_by_segment.set_index('treatment')
biased_summary_by_segment
#---
# 以下、RCTデータと同じ処理を行うので省略。
#---
#出力:biased p_value = 0.0000001824626546622
有意差検定の結果ではp-valueはセレクションバイアスのあるデータに対してさらに小さくなっている。これは、潜在的な購買可能性が高いグループに絞って介入を行ったことが原因だと考えられる。このように、データにバイアスがかかることによって介入の効果を過大評価してしまう恐れがある。