Pythonで部分識別①
はじめに
計量経済を勉強していた後輩から以下の本を勧められました。
部分識別という、計量経済学で急速に発展している研究手法がまとめられた本です。
発想が面白かったので簡単にご紹介しようと思います。技術系記事感を出すために、Pythonでちょこっとコードを書いてみました。
部分識別とは?
識別
計量経済学で使われる言葉ですが、そもそも「識別」とは何でしょうか。
「識別できる」とは、「データが無限個ある(=母集団全てが標本)時に、求めたいパラメータの真の値が決定できる」ということらしいです。識別はできる or できないで表されます。
一方で、部分識別は求めたいパラメータをバウンド(幅)で識別する方法だそうです。あまり自分もこの辺は腹落ちできておらず上手く説明できないので、上述の本を読んでいただければと思います。
データセットを用いた説明
話を分かりやすくするために、具体例を出して話を進めます。
今回は効果検証入門でも取り扱われていた MineThatData E-Mail Analytics And Data Mining Challenge datasetというデータを利用します。ECサイトのユーザーに対してRCTでメール送信したデータです。
今回はこのデータセットに対して部分識別の考え方を適用することで、「メール送付による売上への効果」のバウンドを算出します。
なお、ここでは以下のように記号を使用します。
-
: 処置変数。メールの送付有無であり、0と1の二値変数である。z -
: メール送付されなかった場合の購買金額y(0) -
: メール送付された場合の購買金額y(1)
全てのユーザに対して
また、簡便のために女性向けメールが送信されたユーザーのデータを除去し、男性向けメールが送信されたユーザーは
import pandas as pd
df = pd.read_csv('http://www.minethatdata.com/Kevin_Hillstrom_MineThatData_E-MailAnalytics_DataMiningChallenge_2008.03.20.csv')
# 女性向けメールが送信されたユーザーを除去
df = df.query('segment != "Womens E-Mail"')
# 処置変数を作成
df['treatment'] = df['segment'].apply(lambda x: 1 if x == 'Mens E-Mail' else 0)
以下数式がしばらく続きます。
算出したい効果
今回知りたいのは平均処置効果(ATE)です。具体的には「全てのユーザーが、メールを送付されることで平均的に購買金額がどれくらい増加するのか」ということです。これは以下の式で表せます。
E[y(1)] の識別
ここで
各項を日本語で説明すると
-
…メール送付されたユーザーが、メール送付された場合の購買金額の期待値E[y(1)|z=1] -
…メール送付されていないユーザーが、メール送付された場合のユーザーの購買金額の期待値E[y(1)|z=0]
となります。
上の説明で明らかですが、前者は観測することができますが、後者は観測することができません。これを計量経済学では「
E[y(0)] の識別
各項を説明すると
-
…メール送付されたユーザーが、メール送付されていない場合の購買金額の期待値E[y(0)|z=1] -
…メール送付されていないユーザーが、メール送付されていない場合のユーザーの購買金額の期待値E[y(0)|z=0]
となります。
こちらでは
伝統的な計量経済学での効果算出
伝統的な計量経済学では、
今回のデータセットはRCTを行っているデータのため、
つまり観測できる値を使って
以上のようにメール送付の効果を算出することができます。
Pythonにて効果を算出してみます。
agged = df.groupby('treatment')['spend'].mean().reset_index()
agged.query('treatment == 1')['spend'].values[0] - agged.query('treatment == 0')['spend'].values[0]
### => return
0.7698271558945375
メール送付による売上増加は、0.77ドル
ということが分かります。t検定をすると有意であることが確認されます。
RCTを行っていないデータでは、傾向スコアを用いたマッチングや操作変数法などを用いて、観測できない値を観測できる値から推測することで、求めたい値を識別しようとします。
部分識別
これに対し、部分識別は全く違うアプローチで識別問題に立ち向かいます。
何も仮定しなければ、識別できない値は
ここで
これによって、識別できなかった
識別できなかった
同様に、識別できなかった
識別できなかった
ATEのバウンド算出
以上によって
今回知りたい、メール送信による売上増加効果は
で算出することができます。これが何も仮定しない場合のバウンドとなります。
Pythonにて実装
これまで説明してきた、何も仮定を置かない場合の部分識別について、バウンドの算出をPythonにて実装しました。
def calc_bound(df, y, z):
# yの値域を算出
y_min = df[y].min()
y_max = df[y].max()
# P(z=1), P(z=0), E[y(1)|z=1], E[y(0)|z=0] を算出する
agged = df.groupby(z)[y].agg(['mean', 'count']).reset_index()
agged['prob'] = agged['count'] / agged['count'].sum()
# z=0, 1でデータを分けておく
a_z0 = agged[agged[z] == 0]
a_z1 = agged[agged[z] == 1]
# E[y(1)]のバウンドを算出
e_y1_bound_min = a_z1['mean'].values[0] * a_z1['prob'].values[0] + y_min * a_z0['prob'].values[0]
e_y1_bound_max = a_z1['mean'].values[0] * a_z1['prob'].values[0] + y_max * a_z0['prob'].values[0]
# E[y(0)]のバウンドを算出
e_y0_bound_min = a_z0['mean'].values[0] * a_z0['prob'].values[0] + y_min * a_z1['prob'].values[0]
e_y0_bound_max = a_z0['mean'].values[0] * a_z0['prob'].values[0] + y_max * a_z1['prob'].values[0]
# E[y(1)] - E[y(0)]のバウンドを算出
bound_min = e_y1_bound_min - e_y0_bound_max
bound_max = e_y1_bound_max - e_y0_bound_min
return bound_min, bound_max
# メール送信による売上増加効果を算出
calc_bound(df, y='spend', z='treatment')
### => return
(-249.1209170910286, 249.87908290897144)
メール送付による売上増加効果は -249 ~ 249ドル
という結果になります。何も仮定していない分、バウンドが広すぎるのであまり嬉しくない結果になってしまいました。
実装していて気になった点としては、
おわりに
今回は部分識別の簡単な説明と、メールマーケティングデータに対してメール送付による売上増加効果のバウンドを求めるためのPythonコードを書きました。
部分識別の発想自体は非常にシンプルなものの、これまで勉強してきた因果推論の考え方とは全く違う考え方で、個人的には目から鱗でした。とても面白かったです。
今回の結果を見ても分かる通り、実用性の観点で言うと、何も仮定せずに算出するとバウンドの広さから分かる通り正直使い物にならないでしょう。
これに対して『部分識別入門』の2章以降は、皆が認めやすいような弱い仮定を課した場合に「どのようにバウンドを算出するのか」、「追加する仮定によってバウンドがどれくらい狭まるのか?」が書かれています。元気があればこちらもPythonで実装できたらなと思います。
以上です。
Discussion