🐍

Pythonで因果推論(2)~反実仮想と因果効果~

2022/05/30に公開

はじめに

反実仮想や平均処置効果(ATE)をはじめとした様々な因果効果の考え方は、「施策が、ある変数に与える影響を推定する」因果推論において非常に重要です。そのような反実仮想や因果効果について、Pythonによる実装を交えてまとめました。内容について誤り等がありましたら、コメントにてご指摘いただけますと幸いです。

疑似データの作成

本記事で紹介する用語や指標の具体例として利用するため、「特講の受講と試験の得点」という疑似データを自作します。

設定

とある大学の経済学部生60人を対象に、夏季休暇期間に計量経済学の特別講義(特講)を開催し、その効果を考えます。特講は、あくまでも大学の単位とは無関係の任意参加であるため、介入操作が行われない限りは受講するかどうかは学生の意欲に依存すると考えます。夏季休暇終了後に、特講の受講にかかわらず対象者60人に対して計量経済学の試験を実施し、その得点のデータを観測します。

説明変数

  • 学習意欲X: 一様分布(0, 100)に従う
  • 特講の受講D: 学習意欲Xに依存する
    • X + noise \leq 50であれば、D = 0(受講しない)
    • X + noise > 50であれば、D = 1(受講する)
    • noiseは、一様分布(-20, 20)に従う

被説明変数

  • 試験の得点Y: 学習意欲Xと特講の受講Dに依存する
    • Y = 0.5X + treatmentD + noise
    • noiseは、正規分布(0, 10)に従う
    • treatmentは、一様分布(10, 30)に従う
    • Yは、0 \leq Y \leq 100を満たす整数

潜在的結果変数も含めた被説明変数

便宜上、潜在的結果変数(本来であれば観測できないデータ)も含めて生成します。

  • Y_0: 特講を受講していなかった場合の試験の得点
    • Y = 0.5X + noise
  • Y_1: 特講を受講していた場合の試験の得点
    • Y = 0.5X + treatment + noise
  • noiseは、正規分布(0, 10)に従う
  • Y_0, Y_1は、0 \leq Y_0, Y_1 \leq 100を満たす整数

Pythonによる実装

Pythonで、上記の条件を満たすデータを生成します。

# ライブラリのインポート
import numpy as np
import pandas as pd

# シードの設定
np.random.seed(0)
# データのサイズ
size = 60

# 学習意欲を表す変数X
X = np.random.uniform(0, 100, size)
# 特講の受講を表す変数D
D_noise = np.random.uniform(-20, 20, size)
D = np.where(X+D_noise>50, 1, 0)

# 試験の得点Y
treatment = np.random.uniform(10, 30, size)
Y_noise = np.random.normal(0, 10, size)
Y = np.clip(0.5*X + treatment*D + Y_noise, 0, 100).astype("int")

# 特講を受講していた場合の得点
Y0 = np.clip(0.5*X + Y_noise, 0, 100).astype("int")
# 特講を受講していた場合の得点
Y1 = np.clip(0.5*X + treatment + Y_noise, 0, 100).astype("int")

# データフレームに格納
df = pd.DataFrame({"X": X, "D": D, "Y": Y, "Y0": Y0, "Y1": Y1})
df.head()

(出力結果)

対象の経済学部生60人中、特講を受講した学生の人数・受講していない学生の人数を確認してみます。

# 特講の受講者数
df["D"].value_counts()

(出力結果)

1    33
0    27
Name: D, dtype: int64

D = 1(特講を受講した学生)が33人、D = 0(特講を受講していない学生)が27人ということが確認できます。

要約統計量やヒストグラムも確認してみます。

# データの概要
display(df.describe())

# ヒストグラムを描画
import matplotlib.pyplot as plt
%matplotlib inline

df[["X", "Y", "Y0", "Y1"]].hist(figsize=(8, 6), sharex=True, sharey=True)
plt.xticks(np.arange(0, 110, 20))
plt.yticks(np.arange(0, 16, 3))
plt.show()

(出力結果)

要約統計量やヒストグラムを見てみると、学習意欲Xと試験の成績Yの分布が似ていることや

Y_0 \leq Y \leq Y_1
すなわち、(誰も特講を受講していない場合の試験の得点)\leq(実際の試験の得点)\leq(全員が特講を受講した場合の試験の得点)という傾向がありそうなことが確認できます。

反実仮想(反事実)

反実仮想とは、「実際には〜したが、仮に〜しなかったとしたら...」と、実際の選択とは別の選択をした"if"のケースを想定することです。

先ほどの「特講の受講と試験の得点」を例に挙げますと、

  • 実際には特講を受講したが、仮に特講を受講しなかったとしたら...
  • 実際には特講を受講しなかったが、仮に特講を受講したとしたら...

というのが反実仮想になります。

結果変数と潜在的結果変数

とある学生「iさん」の、試験の得点をY^iとします。
そして、「iさん」が特講を受講した場合をD^i=1とし、このときの試験の得点をY^i_1(特講を受講していなかった場合をD^1=0、試験の得点をY^i_0)とします。

このときに重要なのが、"Y^i_1Y^i_0は同時に存在し得ない" という点です。
そして、実際に観測できた(存在する)方の変数を結果変数、観測できない(存在しない)方の変数のことを潜在的結果変数と言います。

  • 「iさん」が特講を受講した場合
    • 結果変数: 特講を受講した場合の試験の得点Y^i_1
    • 潜在的結果変数: 特講を受講しなかった場合の試験の得点Y^i_0(存在しない)
  • 「iさん」が特講を受講しなかった場合
    • 結果変数: 特講を受講しなかった場合の試験の得点Y^i_0
    • 潜在的結果変数: 特講を受講しなかった場合の試験の得点Y^i_0(存在しない)

Pythonによる実装

実際には存在しない潜在的結果変数

  • 「実際には特講を受講したが、仮に特講を受講しなかった場合の得点」
  • 「実際には特講を受講しなかったが、仮に特講を受講した場合の得点」

を各々5つずつ出力します。

# 実際には特講を受講したが、仮に特講を受講しなかった場合の得点
df_D1 = df[df["D"]==1]
display(df_D1["Y0"].head())

# 実際には特講を受講していないが、仮に特講を受講した場合の得点
df_D0 = df[df["D"]==0]
display(df_D0["Y1"].head())

(出力結果)

1    24
2    15
5    38
6    45
7    54
Name: Y0, dtype: int64
0     55
3     54
4     41
9     46
14    17
Name: Y1, dtype: int64

しつこいですが、実際に分析を行う際にはこれらのデータは存在しません。
あくまでもコードを読みながら、潜在的結果変数とはこのようなものだというイメージをしていただけたらと思います。

因果効果

因果効果とは、「施策が、ある変数に与える影響」のことを指します。

先ほどの「特講の受講と試験の得点」を例に挙げますと、「特講を受講したことによって、特講を受講しなかった場合よりもどれだけ試験の得点が変動したか」が因果効果にあたり、「iさん」の試験の得点に対する因果効果は

Y^i_1 - Y^i_0
という数式で表されます。

一見、因果効果は簡単に求められそうですが、現実問題ではY^i_1Y^i_0いずれかしか観測できません。そのため、さまざまな観点からアプローチすることで、因果効果を計算できるように工夫していきます。

個体レベルの因果効果(ITE)

個体レベルの因果効果(ITE: Individual Treatment Effect)とは、その名の通り個体レベルのデータを元に計算された因果効果です。

「特講の受講と試験の得点」を例に挙げますと、先ほど、現実では計算できないと説明した「iさん」の試験の得点に対する因果効果のことで、

ITE = Y^i_1 - Y^i_0
と表されます。

Pythonによる実装

今回は自作の疑似データであるため、ITEが計算できる仕様となっており、計算結果を5つ出力します。

# 個体レベルの因果効果(ITE)
df["ITE"] = df["Y1"] - df["Y0"]
df["ITE"].head()

(出力結果)

0    24
1    20
2    29
3    23
4    19
Name: ITE, dtype: int64

しつこいですが、実際に分析を行う際にはこれらは容易に計算できません。
あくまでもコードを読みながら、ITEとはこのようなものだというイメージをしていただけたらと思います。

平均処置効果(ATE)

平均処置効果(ATE: Average Treatement Effect)とは、グループ全体にもたらした影響の期待値(平均) を表す因果効果です。

「特講の受講と試験の得点」を例に挙げますと、対象となるすべての経済学部生のITEの期待値がATEにあたり、

ATE = E(Y_1 - Y_0) = E(Y_1) - E(Y_0)
と表されます。

Pythonによる実装

自作の疑似データをもとにATEを算出します。

# 平均処置効果(ATE)
df["Y1"].mean() - df["Y0"].mean()

(出力結果)

19.749999999999996

一般的に、因果推論や効果検証を実施するという場合には、ATEを算出することを意味します。
実際に分析を行う際は、これほど容易にATEを算出できるパターンは少なく、さまざまな仮定や介入操作の下で因果推論手法を利用し、ATEを算出を実現します。

処置群における平均処置効果(ATT)

処置群における平均処置効果(Average Treatment effect on Treated)とは、実際に処置を受けた集団におけるATEです。

「特講の受講と試験の得点」を例に挙げますと、「実際に特講を受講した学生のITEの期待値」がATTであり

ATT = E(Y_1-Y_0|D=1) = E(Y_1|D=1) - E(Y_0|D=1)
と表されます。

Pythonによる実装

自作の疑似データをもとにATTを算出します。

# 処置群における平均処置効果(ATT)
df_D1["Y1"].mean() - df_D1["Y0"].mean()

(出力結果)

21.575757575757578

ATTを確認することで、実施した施策にどれだけの効果があったかを解釈することができます。

対照群における平均処置効果(ATU)

対照群における平均処置効果(Average Treatment effect on Untreated)とは、実際には処置を受けていない集団におけるATEです。

「特講の受講と試験の得点」を例に挙げますと、「実際に特講を受講しなかった学生のITEの期待値」がATUであり

ATU = E(Y_1-Y_0|D=0) = E(Y_1|D=0) - E(Y_0|D=0)
と表されます。

Pythonによる実装

自作の疑似データをもとにATUを算出します。

# 対照群における平均処置効果(ATU)
df_D0["Y1"].mean() - df_D0["Y0"].mean()

(出力結果)

17.51851851851852

ATUを確認することで、まだ施策が実施されていない対照群に対しても施策を実施するべきかを評価することができます。

セレクション・バイアス

因果効果を算出する上で、犯してしまいがちな過ちが、単純に結果変数の期待値

E(Y_1|D=1)-E(Y_0|D=0)
だけで効果を評価しようとしてしまうことです。

「特講の受講と試験の得点」を例に挙げますと、「特講を受講した学生の得点の平均」と「特講を受講していない学生の得点の平均」の差を評価してしまうことが、上記の過ちにあたります。

何が問題なのかというと、「特講の受講Dと試験の得点Yは、いずれも各学生の学習意欲Xに依存している」という点が問題なのです。

もう少し具体的に説明すると

  • 学習意欲の高い学生は
    • そもそも(特講を受講するしないにかかわらず)試験の得点Yが高い上に
    • 特講も受講するD=1
  • 学習意欲の低い学生は
    • そもそも(特講を受講するしないにかかわらず)試験の得点Yが低い上に
    • 特講も受講しないD=0

というケースが多いため、

  • 試験の得点&Y&が高いのは、
    • 特講を受講したD=1からだけでなく
    • そもそも学習意欲Xが高いため
  • 試験の得点Yが低いのは
    • 特講を受講しなかったD=0からだけでなく
    • そもそも学習意欲Xが低いため

というように、特講の受講とは別の要因も試験の得点Yに影響しているということが考えられるんです。

こちらの例のように、特講の受講とは別の要因が試験の得点Yに与えている影響のことをセレクション・バイアスと言います。

もう少し一般的に説明すると、セレクション・バイアスとは単純な結果変数の期待値(平均)とATEの差の量のことであり、

\{E(Y_1|D=1)-E(Y_0|D=0)\} - ATE

と表されます。ここで
ATE = E(Y_1-Y_0) = E(Y_1)-E(Y_0)

Pythonによる実装

自作の疑似データをもとに単純な結果変数の期待値とセレクション・バイアスを算出します。

# 特講を受講した学生と受講していない学生の平均得点の差
ave = df_D1["Y1"].mean() - df_D0["Y0"].mean()
print(f"特講の受講有無による平均得点の差: {ave}")

# セレクション・バイアス
ATE = df["Y1"].mean() - df["Y0"].mean()
print(f"セレクション・バイアス: {ave - ATE}")

(出力結果)

特講の受講有無による平均得点の差: 39.52861952861953
セレクション・バイアス: 19.778619528619533

因果推論では、いかにこのセレクション・バイアスを取り除いて効果を推定できるかがポイントとなってきます。

参考文献

おわりに

実際にデータ分析業務で因果推論を行ったり、因果推論のテキストで理論的な背景を学習していると、意外と平均処置効果のような基本的な概念がイメージしづらいことがあると思います。そのようなときに、疑似データを自作して、潜在的結果変数やさまざまな因果効果が容易に導出してみることで、さまざまな因果推論手法の理論的な背景もイメージしやすくなると思いますので、参考にしていただけたら幸いです。

他にも下記のような記事を書いています。ご一読いただけますと幸いです。

また、過去にLTや勉強会で発表した資料は下記リンクにまとめてあります。ぜひ、ご一読くださいませ。
https://speakerdeck.com/s1ok69oo

Discussion