Table of Contents
問題設定
Question
- 会社Aは複数の事業所に分かれており, 事業所単位でスキルアップクラスを提供したりしていなかったりする
- 事業所がスキルアップクラスを提供するかどうかは完全にrandomに定まっているとする
このとき, このrandom assignment変数$Z_i$をpropensity scoreに組み込んだとき, 推定量に対する影響としては
- Nothing (in expectation) to the effect
- standard errorsは増加する
propensity scoreによるATEの推定(IPWを含む)にあたって, CIAが成立しているもとでのバランシングスコアが理論的根拠なので, Potential Outcomeに対するCIAが少ない変数の集合で成立しているならば,
- 変数を増やしても影響はない
- $\mathbb E[Y_{ij} \vert D_i, X_i, Z_i]$のノイズが大きくなるので推定量のstandard errorsは増加する
というのは直感的には理解できると思います. これを数値計算で確認するのが本記事の目的です
シミュレーション設定
datasetはsample size, $N = 10000$として以下のData Generating Processに従うとします
\[\begin{align*} x_1 &\sim Uniform(0, 1)\\[3pt] x_2 &\sim Uniform(0, 1)\\[3pt] x_3 &\sim Uniform(0, 1)\\[3pt] \epsilon_i &\sim N(0, 0.5)\\[3pt] prob_i &= \frac{1}{1 + \exp(0.5 - 2x_1 + 2x_2 + 2x_1x_2 - x_3)}\\ D_i &\sim binom(1, prob_i)\\[3pt] Y_{0i} &= \exp(2x_1) + \epsilon_i\\[3pt] Y_{1i} &= Y_{0i} + 1\\[3pt] Y_i &= D_iY_{1i} + (1 - D_i)Y_{0i} \end{align*}\]施策$D_i$の割当確率はlogitに従うとし, $(x_1, x_2, x_3)$によって決定される. 一方, potential outcomeの分布に影響を与えるのは $x_1$ のみ. つまり
\[\mathbb E[Y_{ji}|D_i,x_1, x_2, x_3] = \mathbb E[Y_{ji}|D_i,x_1] = \mathbb E[Y_{ji}|x_1]\]という状況を考えます. また, uplift effect = 1という形で施策効果はconstantとしています.
DGPの結果の一例として, observed outcome $Y_i$の$D_i$ごとの平均は以下となります. 単純比較では施策効果が1.5近辺となりupward biasが発生してしまっています,
D | 0 | 1 | |
---|---|---|---|
observed | count | 6149.000000 | 3851.000000 |
mean | 2.955953 | 4.465919 | |
std | 1.774988 | 1.893814 | |
min | -0.501517 | -0.031790 | |
25% | 1.564539 | 2.893273 | |
50% | 2.508593 | 4.125529 | |
75% | 4.070802 | 5.898188 | |
max | 8.575620 | 9.463890 | |
logit_cia | count | 6149.000000 | 3851.000000 |
mean | 0.378282 | 0.395986 | |
std | 0.063560 | 0.064856 | |
min | 0.278799 | 0.278796 | |
25% | 0.322965 | 0.339708 | |
50% | 0.371775 | 0.399320 | |
75% | 0.429691 | 0.453660 | |
max | 0.502034 | 0.501983 | |
logit_full | count | 6149.000000 | 3851.000000 |
mean | 0.317717 | 0.492693 | |
std | 0.176661 | 0.197903 | |
min | 0.068672 | 0.078593 | |
25% | 0.172645 | 0.333182 | |
50% | 0.272893 | 0.500188 | |
75% | 0.429088 | 0.654429 | |
max | 0.886679 | 0.897462 |
推定量について
推定量方針
- propensity score推定後のATE推定量はIPW推定量を用いる
- propensity score自体の推定は分析者はlogitが妥当するとしっているとする
- propensity scoreの推定にあたっては以下2つのパターンを試す
cia_model
: 分析者は$x_1$のみを用いた単回帰logitを実行するパターンfull_model
: $x_1, x_2, x_3$をtrue formでlogit回帰を実行するパターン
- シミュレーションは1000回実施する(1000回DGPを回す)
なお, 2つのpropensity score推定量の性能比較として今回は簡易的にAUCを用います.
予想される結果としては
full_model
のほうがAUCスコアが圧倒的に良い- ATE推定量としては
cia_model
,full_model
どちらもバイアスはない - 推定量の分散を加味すると
cia_model
のほうが効率的である
REMARKS
- 今回はtrain-testに分けようがデータの分布は同じと確信があるので, めんどくさいので推定の段階では分けていないです
シミュレーション結果
true | estimate_cia | estimate_full | auc_cia | auc_full | |
---|---|---|---|---|---|
count | 1.000000e+03 | 1000.000000 | 1000.000000 | 1000.000000 | 1000.000000 |
mean | 1.000000e+00 | 0.996162 | 0.999949 | 0.575555 | 0.743761 |
std | 5.450743e-16 | 0.012943 | 0.018541 | 0.006137 | 0.004751 |
min | 1.000000e+00 | 0.956735 | 0.931434 | 0.556370 | 0.728875 |
25% | 1.000000e+00 | 0.987529 | 0.987892 | 0.571321 | 0.740480 |
50% | 1.000000e+00 | 0.996294 | 1.000240 | 0.575663 | 0.743732 |
75% | 1.000000e+00 | 1.004821 | 1.012625 | 0.579839 | 0.746824 |
max | 1.000000e+00 | 1.037352 | 1.056248 | 0.594460 | 0.757005 |
cia_model
,full_model
ともにバイアスはないと解釈できる形で推定値が出ている- 若干,
full_model
のほうがバイアスは少ないように見受けられる
- 若干,
- AUCに着目すると
full_model
のほうが圧倒的にcia_model
より性能が良いと解釈できる - 一方, 予想されていたようにATE推定量は
full_model
のほうが分散がcia_model
よりも大きい
推定値の傾向について
以下のように傾向としては一致しているので, Robustness checkで両方の推定量が大きくかけ離れていないか?を確認することは今回のケースでは有用であると示唆してくれている.
Normalized Differenceの確認
傾向スコアはそもそも, バランシングスコアであることがモットーなので各特徴量のバランシング, 特に$x_1$特徴量のバランシングがどのように調整されているか確認してみる.
Normalized Differnce
- covariateのバランスはnormalized differenceがよく用いられる
- 差が小さいほどよく,0.25を超えるか否かがrule of thumbとされている
simulationの最後に出てきたdatasetを用いて簡易的に比較してみると以下のような結果になる.
features | snd_cia | snd_fill | |
---|---|---|---|
0 | x1 | 0.160808 | 0.209398 |
1 | x2 | 0.594803 | 0.470987 |
2 | x3 | 0.145993 | 0.125899 |
cia_model
とfull_model
を比較すると$x_1$のバランシングはcia_model
のほうが良い- Potential outcomeの分布は$x_1$に依存するのでこの変数がバランシングしている方が良い推定量であるはず
cia_model
推定量のほうが推定値の分散が小さいことと整合的である- $x_2, x_3$を見てみると,
full_model
のほうが比較的バランシングしている(考慮した形で推定しているのでそれはそう) - ただし, $x_2, x_3$のバランシングはCIAの成立形態を踏まえると重要ではない
結び
REMARKS
- propensity scoreという推定量のクラスを考えた場合, AUCがよいモデルが本当に良いとは限らない(あくまで反例の紹介)
- propensity scoreという推定量が機能するか否かはあくまでCIAがどのような形で成立しているかに依存する
- Normazlied differenceは重要な特徴量がバランスしているかを判断する指標にはなるが, 解釈はあくまでCIAがどのような形で成立しているかに依存する
ただし, 上記のシミュレーションではcommon supportの仮定を満たすようにいい感じにパラメータを設定した状況での数値計算の結果であって, 現実的なpropensity scoreの問題はどちらかというとpropensity score推定量のmisspecificationに起因するバイアスの発生です.
また, 今回のケースで$Y_{0i}$のheterogeneityを$\exp(10x_1)$などのように大きくすると, IPW推定量は大きくずれたりするので, Propensity scoreはRobustness checkで複数のspecificationでの結果を踏まえながら解釈する必要がありそうです.
Appendix: Python code
DGP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
#%%
import numpy as np
import pandas as pd
import plotly.express as px
import statsmodels.api as sm
from sklearn import metrics
#-------------
# DGP
#-------------
def dgp():
N = 10000
x1 = np.random.uniform(0, 1, N)
x2 = np.random.uniform(0, 1, N)
x3 = np.random.uniform(0, 1, N)
logit_model = np.exp(-0.5 + 2 * x1 - 2 * x2 - 2 * x1*x2 + 1 * x3)
assignment_prob = logit_model / (1 + logit_model)
assigment = np.random.binomial(1, assignment_prob)
potential_untreated = np.exp(2 * x1) + np.random.normal(0, 0.5, N)
uplift = 1
potential_treated = potential_untreated + uplift
df = pd.DataFrame({
'const': 1,
'x1': x1,
'x2': x2,
'x3': x3,
'x1x2': x1*x2,
'prob': assignment_prob,
'D': assigment,
'uplift':uplift,
'y0': potential_untreated,
'y1': potential_treated
})
df['observed'] = (1 - df['D']) * df['y0'] + (df['D']) * df['y1']
return df
def logit_compute(df):
y = df.loc[:, 'D'].values
x_simple = df.loc[:, ['const', 'x1']]
x_true = df.loc[:, ['const', 'x1', 'x2', 'x1x2', 'x3']]
logit_reg_simple = sm.Logit(y, x_simple).fit()
logit_reg_true = sm.Logit(y, x_true).fit()
df['logit_cia'] = logit_reg_simple.predict(x_simple)
df['logit_full'] = logit_reg_true.predict(x_true)
return df
def get_auc(df, col):
fpr, tpr = metrics.roc_curve(df['D'], df[col], pos_label=1)[:2]
return metrics.auc(fpr, tpr)
def compute_normalized_diff(df, prob='logit_cia'):
features = ['x1', 'x2', 'x3']
snd = []
wtavg = lambda x: np.average(x.loc[:,features], weights = x[prob].values, axis = 0)
tmp_df = df.loc[:, features + ['D', prob]]
weighted_mean = tmp_df.groupby(['D']).apply(wtavg)
weighted_var = tmp_df.groupby(['D']).var()
for i, j in enumerate(features):
mean_dif = abs(weighted_mean[0][i] - weighted_mean[1][i])
denominator = np.sqrt(np.sum(weighted_var[j]))
smd = mean_dif/denominator
snd.append(smd)
return pd.DataFrame({'features': features, 'snd': snd})
df = dgp()
df = logit_compute(df)
Estimator
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#--------------------
# True ATE, ATT, ATU
#--------------------
def get_true_effect(df):
stats_df = df.groupby('D')[['y0', 'y1']].mean().reset_index(drop=True)
stats_df = pd.concat([stats_df,
pd.DataFrame(df.loc[:, ['y0', 'y1']].mean(axis=0)).T],
axis=0).set_index(pd.Index(['ATU', 'ATT', 'ATE']))
stats_df['effect'] = stats_df['y1'] - stats_df['y0']
return stats_df
def estimate_effect(df, col='logit_cia'):
y0_treated_weight = ((1 - df['D']) * df[col]) / (1 - df[col])
y1_untreated_weight = (df['D']) * (1 - df[col]) / df[col]
y1_ate_weight = df['D'] / df[col]
y0_ate_weight = (1 - df['D'])/(1 - df[col])
y0_untreated, y1_treated = df.groupby('D')['observed'].mean().values
y0_treated = np.sum(y0_treated_weight * df['observed']) / np.sum(y0_treated_weight)
y1_untreated = np.sum(y1_untreated_weight * df['observed']) / np.sum(y1_untreated_weight)
y1_ate = np.sum(y1_ate_weight * df['observed'])/ np.sum(y1_ate_weight)
y0_ate = np.sum(y0_ate_weight * df['observed'])/ np.sum(y0_ate_weight)
res_df = pd.DataFrame({'y0':[y0_untreated, y0_treated, y0_ate],
'y1':[y1_untreated, y1_treated, y1_ate]})
res_df = res_df.set_index(pd.Index(['ATU', 'ATT', 'ATE']))
res_df['effect'] = res_df['y1'] - res_df['y0']
return res_df
Simulation code
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
#-----------------
# simulation
#-----------------
np.random.seed(42)
res_true = []
res_estimates_cia = []
res_estimates_full = []
cia_auc = []
cia_full = []
for i in range(1000):
tmp = dgp()
tmp = logit_compute(tmp)
res_true.append(get_true_effect(tmp).loc['ATE', 'effect'])
res_estimates_cia.append(estimate_effect(tmp).loc['ATE', 'effect'])
res_estimates_full.append(estimate_effect(tmp, col='logit_full').loc['ATE', 'effect'])
cia_auc.append(get_auc(tmp, 'logit_cia'))
cia_full.append(get_auc(tmp, 'logit_full'))
compare_df = pd.DataFrame({'true':res_true,
'cia_model_ATE':res_estimates_cia,
'full_model_ATE':res_estimates_full,
'cia_model_AUC': cia_auc,
'full_model_AUC': cia_full})
compare_df.describe()
References
- Mostly Harmless Econometrics: An Empiricist’s Companion
- Ryo’s Tech Blog > Propensity score & Conditional Independence Assumption
(注意:GitHub Accountが必要となります)