Mímisbrunnr知恵の泉

← 因果推論 一覧

🎓 レベル:標準 | 重要度:A(必須)

📎 前提:バックドア基準と識別 | 識別の仮定 | 回帰による調整とその限界 | 数理:ロジスティック回帰(機械学習)

要点(BLUF)


1. 概念:処置の確率をモデル化する

回帰による調整とその限界では結果 YY の関数形を当てる必要があった。傾向スコア法は発想を変え、「処置を受ける確率」だけをモデル化する。

e(x)=P(X=1C=x)e(x) = P(X{=}1 \mid C{=}x)

共変量 CCバックドア基準と識別のバックドア基準を満たすとき、e(C)e(C) を使えば調整ができる。直観は「傾向スコアが同じ人どうしを比べれば、処置・対照の振り分けはコイン投げと同じ」――つまり局所的にRCTに近づく。


2. 識別:バランシングスコアの定理

バランシング性

傾向スコアの核心は次の性質である。

CXe(C)C \perp X \mid e(C)

つまり e(C)e(C) を固定すると、処置 XX はそれ以上 CC に依存しない。証明は一行:

P(X=1C,e(C))=P(X=1C)=e(C)=P(X=1e(C))P(X{=}1 \mid C, e(C)) = P(X{=}1\mid C) = e(C) = P(X{=}1\mid e(C))

左辺が CC に依らず e(C)e(C) だけで決まるので、e(C)e(C) を与えれば CCXX は独立。結果として、e(C)e(C) が等しいグループの中では、共変量 CC の分布が処置群と対照群で同じになる。

強く無視できる割り当ての縮約

識別の仮定の条件付き交換可能性 (Y(1),Y(0))XC(Y(1),Y(0)) \perp X \mid C が成り立つなら、Rosenbaum–Rubin(1983)の定理により

(Y(1),Y(0))XC        (Y(1),Y(0))Xe(C)(Y(1),Y(0)) \perp X \mid C \;\;\Longrightarrow\;\; (Y(1),Y(0)) \perp X \mid e(C)

が言える。高次元ベクトル CC で条件づける代わりに、スカラー e(C)e(C) で条件づければ十分になる。これが傾向スコアの最大の御利益(次元削減)。さらに正値性 0<e(C)<10<e(C)<1 が要る点は回帰による調整とその限界と同じ。

❓ 確認:傾向スコアで「予測がよく当たる」ことは目的だろうか? 違う。目的はバランスを取ること。極端に言えば ee の予測精度が高すぎる(処置が完全に予測できる)と、それは正値性の崩壊を意味し、むしろ調整できなくなる。


3. 推定:層別とマッチングで効果を回収する

真の効果を仕込んだ擬似データで確かめる。2つの共変量 X1,X2X_1,X_2 が処置にも結果にも線形に効く(交絡する)状況で、真の効果は均一 ATE=3.0\text{ATE}=3.0 とする。傾向スコアをロジスティック回帰で推定し、(1) 5分位での層別、(2) 最近傍マッチング、で効果を回収する。共変量バランスは 標準化平均差 (SMD) で測る(目安は SMD<0.1|\text{SMD}|<0.1)。

import warnings
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.neighbors import NearestNeighbors

warnings.filterwarnings("ignore")

# === 傾向スコアの層別・マッチングで共変量バランスを改善し ATE を回収する ===
rng = np.random.default_rng(7)
n = 6000
X1 = rng.normal(0, 1, size=n)
X2 = rng.normal(0, 1, size=n)

# 処置割り当て: 両共変量に依存(交絡)
prop_true = 1.0 / (1.0 + np.exp(-(0.7 * X1 + 0.7 * X2)))
T = rng.binomial(1, prop_true)

# 結果: 共変量が線形に交絡。真の効果は均一
ATE_true = 3.0
Y = 1.0 + ATE_true * T + 2.0 * X1 + 2.0 * X2 + rng.normal(0, 1, size=n)

naive = Y[T == 1].mean() - Y[T == 0].mean()

# 傾向スコア推定(ロジスティック回帰)
covariates = np.column_stack([X1, X2])
e_hat = LogisticRegression().fit(covariates, T).predict_proba(covariates)[:, 1]


# 標準化平均差(SMD): 共変量バランスの指標
def smd(values, treat):
    m1, m0 = values[treat == 1].mean(), values[treat == 0].mean()
    s1, s0 = values[treat == 1].std(), values[treat == 0].std()
    return (m1 - m0) / np.sqrt((s1**2 + s0**2) / 2)


smd_X1_before = abs(smd(X1, T))
smd_X2_before = abs(smd(X2, T))

# (1) 傾向スコア層別(5分位)で ATE と層内バランス
df = pd.DataFrame({"Y": Y, "T": T, "X1": X1, "X2": X2, "e": e_hat})
df["stratum"] = pd.qcut(df["e"], 5, labels=False)
ate_list, size_list, smd1_list, smd2_list = [], [], [], []
for s in range(5):
    sub = df[df["stratum"] == s]
    if (sub["T"] == 1).sum() > 0 and (sub["T"] == 0).sum() > 0:
        ate_s = sub.loc[sub["T"] == 1, "Y"].mean() - sub.loc[sub["T"] == 0, "Y"].mean()
        ate_list.append(ate_s)
        size_list.append(len(sub))
        smd1_list.append(abs(smd(sub["X1"].values, sub["T"].values)))
        smd2_list.append(abs(smd(sub["X2"].values, sub["T"].values)))
ate_stratified = np.average(ate_list, weights=size_list)
smd_X1_after = np.average(smd1_list, weights=size_list)
smd_X2_after = np.average(smd2_list, weights=size_list)

# (2) 最近傍マッチング(傾向スコアが最も近い対照を処置に対応づけ -> ATT)
treated = df[df["T"] == 1]
control = df[df["T"] == 0]
nn = NearestNeighbors(n_neighbors=1).fit(control[["e"]].values)
_, idx = nn.kneighbors(treated[["e"]].values)
att_matching = (treated["Y"].values - control["Y"].values[idx.flatten()]).mean()

print(f"真の ATE             : {ATE_true:.3f}")
print(f"素朴比較             : {naive:.3f}")
print(f"傾向スコア層別 ATE   : {ate_stratified:.3f}")
print(f"最近傍マッチング ATT : {att_matching:.3f}")
print(f"X1: 調整前 {smd_X1_before:.3f} -> 層別後 {smd_X1_after:.3f}")
print(f"X2: 調整前 {smd_X2_before:.3f} -> 層別後 {smd_X2_after:.3f}")

実行結果は次の通り。

真の ATE             : 3.000
素朴比較             : 5.247
傾向スコア層別 ATE   : 3.249
最近傍マッチング ATT : 2.968
X1: 調整前 0.575 -> 層別後 0.077
X2: 調整前 0.611 -> 層別後 0.104

出力の意味:素朴比較は 5.247 と真値 3.0 を大きく上回る(処置群は X1,X2X_1,X_2 が大きい個体に偏り、それが YY を押し上げる)。傾向スコアで調整すると、層別で 3.249、マッチングで 2.968 と真値近くまで戻る。重要なのは下2行の 共変量バランス:調整前は SMD が 0.58, 0.61 と大きく偏っていたのが、層別後は 0.08, 0.10 とほぼ揃う。バランスが取れたからこそ効果推定が正しくなった、という因果が数値で見える。層別の 3.249 がまだ真値からわずかにずれるのは、5分位という粗い層分けで層内に残差交絡が残るため(後述)。


4. なぜバランスが鍵なのか(直観)

傾向スコア法のゴールは予測精度ではなく 疑似的なランダム化 だ。e(C)e(C) が同じ人を集めれば、その小集団では処置・対照の振り分けが(観測共変量に関する限り)コイン投げと同等になる。だから層別やマッチングで小集団ごとに差を取れば、交絡が相殺される。

どちらも結果 YY の関数形を仮定していない点が回帰による調整とその限界との決定的な違い。傾向スコアの設計(共変量の選択・モデル)が正しければ、結果がどんな非線形でも調整できる。


⚠️ よくある誤解・落とし穴


関連ノート