ねぎとろ放浪記

ねぎとろ放浪記

個人的備忘録です。勉強したことをまとめていきます。

causallibでIPWをやってみる

因果推論の勉強をはじめました。
IPW法を学んだのでPythonで実際にやってみます。

causallibというライブラリを使ってIPW法をやってみました。

こちらのサンプルに沿って進めていきます。

実行環境

  • Googl Colaboratory
  • causallib (0.6.0)
  • sklearn (0.22.2)

使用するデータの確認

喫煙をやめたことによる体重の変化を検証します。

import causallib
import sklearn

data = load_nhefs()
data.X.join(data.a).join(data.y).head()

f:id:neginegitoro:20201226183344p:plain
データの一部

データの内容はざっくりと以下のようになっています。

  • data.X : 共変量 (年齢、性別、喫煙年数、運動習慣など)
  • data.a : 割当(喫煙をやめていれば1、やめていなければ0)
  • data.y : 目的変数 (体重の増減(ポンド))


割当を確認します。

#割当
data.a.value_counts()
0    1163
1     403
Name: qsmk, dtype: int64

1163人は喫煙のまま、403人が禁煙したようです。

目的変数を確認します。

#目的変数
data.y.describe()
count    1566.000000
mean        2.638300
std         7.879913
min       -41.280470
25%        -1.478399
50%         2.603811
75%         6.689581
max        48.538386
Name: wt82_71, dtype: float64

体重は平均で2.6ポンド増加したようです。


禁煙した人としなかった人での体重の変化を比較してみます。

df = data.X.join(data.a).join(data.y)

print("割当1 : ", df[df.qsmk == 1]["wt82_71"].mean())
print("割当0 : ", df[df.qsmk == 0]["wt82_71"].mean())
割当1 :  4.525078989702233
割当0 :  1.9844975347463472

禁煙した人の方が体重が増加していることがわかります。

IPW

傾向スコアの予測

サンプルの通りに実装してみます。

#train
learner = LogisticRegression(solver="liblinear") #傾向スコア予測に使うモデル
ipw = IPW(learner)
ipw.fit(data.X, data.a) #共変量から割当を予測

#傾向スコア
probs = ipw.compute_propensity(data.X, data.a, treatment_values=1)#割当1になる確率

probs.head()
0    0.131687
1    0.168435
2    0.169179
3    0.481203
4    0.251937
Name: 1, dtype: float64

ここの値は割当1になる確率です。

重み付けの確認

傾向スコアの逆数で重み付けされていることを確認します。

ipw.compute_weights(data.X, data.a).head()
0    1.151658
1    1.202552
2    1.203629
3    1.927535
4    1.336785
dtype: float64
  • 割当1のデータは 1/傾向スコア
  • 割当0のデータは 1/(1 - 傾向スコア)

の値になっています。

結果を確認

#割当ごとに重みづけして結果を計算
outcomes = ipw.estimate_population_outcome(data.X, data.a, data.y)

#ATE
effect = ipw.estimate_effect(outcomes[1], outcomes[0])
effect
diff    3.490373
dtype: float64

禁煙によって3.4ポンド体重が増加したようです。

パラメータを変化させる

次にパラメータを変化させて実行してみます。

傾向スコアが0に近すぎると、逆数をとった時に極端に大きい値になります。

また、1に近すぎると、影響が小さくなってしまいます。

今回は傾向スコアを0.2から0.8の範囲に限定します。

learner = LogisticRegression(penalty='l1', C = 0.01, max_iter=500, solver = 'liblinear')

#傾向スコアを0.2から0.8に限定
truncate_eps = 0.2
ipw = IPW(learner, truncate_eps = truncate_eps, use_stabilized=False)
ipw.fit(data.X, data.a) #共変量から割当を予測

傾向スコアの確認

#傾向スコア
probs = ipw.compute_propensity(data.X, data.a, treatment_values=1)#割当1になる確率

print(probs.min())
print(probs.max())
Fraction of values being truncated: 0.23499.
0.7047037999493072
0.2

ちゃんと0.2から0.8の間に収まっています。

結果の確認

#割当ごとに結果を重み付け
outcomes = ipw.estimate_population_outcome(data.X, data.a, data.y)

#ATE
effects = ipw.estimate_effect(outcomes[1], outcomes[0], effect_types=["diff", "ratio"])

effects
Fraction of values being truncated: 0.23499.
diff     3.376068
ratio    2.917687
dtype: float64

禁煙による体重の変化は3.37ポンドとなりました。

モデルの評価を行う

最後にモデルの評価をします。

evaluator = PropensityEvaluator(ipw)
results = evaluator.evaluate_simple(data.X, data.a, data.y, plots=None)

results.scores.prediction_scores

f:id:neginegitoro:20201226183419p:plain

見慣れない指標について

  • hinge : ヒンジ損失。(参考)
  • matthews : マシューズの相関係数。-1から1。1に近いほど良い分類ができている。(参考
  • 0/1 : ???知ってる人教えてください
  • brier : ブライアスコア。0から1で、0に近いほど正確であることを表す。(参考)


trainとvalidで表示されます。

from sklearn import metrics

plots = ["roc_curve", "pr_curve", "weight_distribution", "calibration", "covariate_balance_love", "covariate_blance_slope"]
metrics = {"roc_auc": metrics.roc_auc_score,
           "avg_precision": metrics.average_precision_score,}

ipw = IPW(LogisticRegression(solver="liblinear"))
evaluator = PropensityEvaluator(ipw)
results = evaluator.evaluate_cv(data.X, data.a, data.y, 
                                plots=plots, metrics_to_evaluate=metrics)

f:id:neginegitoro:20201226183636p:plain

  • 左上:ROC曲線。AUCは0.7以上だと良いとされています。
  • 右上:PrecisionとRecallのグラフ
  • 左中:傾向スコアの分布。割当0は0に近いほど多く、割当1は1に近いほど多いと良いようです。
  • 右中:キャリブレーションカーブ。横軸は傾向スコア。縦軸は割当1のデータの割合。x=yの直線に近いほど良い。
  • 左下:共変量の平均差を標準誤差で割ったものの絶対値??(ここがよくわかりませんでした)
  • 右下:左下と同じ。


独自データに適用してみる

予め因果効果のわかっているデータを作成し、精度を検証したいと思います。

データの作成

年齢と性別によって目的変数yが変化するデータを作成します。
割当zは 年齢と性別からシグモイド関数によって決定しました。

from numpy.random import *
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np

#データ数
num_data = 10000

#性別
gender = randint(0, 2, num_data)

#年齢
age = randint(20, 81, num_data)

#ノイズの生成,randnは標準正規分布
e_z = randn(num_data)

#シグモイド関数に入れる部分
#これで割当を決める
#ここを調節することで傾向スコアが満遍なく散らばるようにする
z_base = 5*gender + age - 50 + 5*e_z

#シグモイド関数
def sigmoid(x):
  return 1/(1+np.exp(-x))

#シグモイド関数を計算
z_prob = sigmoid(0.1*z_base)

#割当の配列を作成
z = np.array([])
for i in range(num_data):
  tmp = np.random.choice([0,1], 
                         size=1, 
                         p=[1-z_prob[i], z_prob[i]])[0]#pは選ぶ確率
  z = np.append(z, tmp)#配列の更新

#目的変数のノイズを生成
e_y = randn(num_data)

#目的変数を決める
#ここのzの係数が因果効果
y = 5000 + 10*gender + 30*age + 60*z + 10*e_y

割当zによる目的変数の変化は60にしました。

データフレームの作成

causallibで使えるようにデータを整形します。

#データフレームの作成
df = pd.DataFrame({'gender':gender, 'age': age, 'z':z, 'y':y})

df_x = df[['gender', 'age']]
df_y = df['y']
df_z = df['z']

df.head()
gender age z y
0 0 76 1 7350.62
1 1 60 0 6784.8
2 1 34 0 6036.7
3 1 67 1 7089.36
4 0 50 0 6514.17

このようなデータを作成しました。

IPW法を適用する

これまで通りIPW法を適用します。

truncate_epsは0.01にしました。

#train
learner = LogisticRegression(solver="liblinear")
ipw = IPW(learner, truncate_eps=0.01)
ipw.fit(df_x, df_z)

#効果の確認
outcomes = ipw.estimate_population_outcome(df_x, df_z, df_y)

#(割当1の目的変数 * 1/傾向スコア) - (割当0の目的変数 * 1/(1-傾向スコア))の合計
effect = ipw.estimate_effect(outcomes[1], 
                            outcomes[0], 
                            effect_types=["diff", "ratio"])
effect

出力

Fraction of values being truncated: 0.00000.
diff     65.454931
ratio     1.010065
dtype: float64

割当の有無によるyの差は65.5と予測されました。
zの係数は60にしていたので、近い値ではありますね。

なお、同じ方法でデータを作成しても、予測値はばらつきました。 (55~80くらい)
何回か実行して平均をとった方が良いのかもしれないです。

評価

最後に評価を行います。

#評価
from sklearn import metrics

plots=["roc_curve", "pr_curve", "weight_distribution", 
       "calibration", "covariate_balance_love", "covariate_balance_slope"]

metrics = {"roc_auc": metrics.roc_auc_score,
           "avg_precision": metrics.average_precision_score,}

ipw = IPW(LogisticRegression(solver="liblinear"),
          truncate_eps=0.01)

evaluator = PropensityEvaluator(ipw)

results = evaluator.evaluate_cv(df_x, df_z, df_y, 
                                plots=plots, metrics_to_evaluate=metrics)

f:id:neginegitoro:20210103153906p:plain

AUCが0.85と高く、 傾向スコアは理想的な形に分布しています。

データとしてはかなり予測しやすいものだったのではないでしょうか。

最後に

以上causallibによるIPW法でした!

因果推論初心者ですが、公式のサンプルがわかりやすかったのでなんとか理解できたかと。。。

評価方法やその基準についてわかっていないことが多いので、今後勉強していこうと思います。