傾向スコアのAUCの良さは推定量の性質の良さにつながるのか?

Propensity score & Conditional Independence Assumption(続編)

公開日: 2023-07-07
更新日: 2023-11-05

  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とされている
\[\text{Normalized Difference}_j = \frac{|\bar x_{treated, j} - \bar x_{control, j}|}{(s_{treated, j}^2 + s_{control, j}^2)^{1/2}}\]

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_modelfull_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



Share Buttons
Share on:

Feature Tags
Leave a Comment
(注意:GitHub Accountが必要となります)