Mímisbrunnr知恵の泉

← 因果推論 一覧

🎓 レベル:発展 | 重要度:B(標準)

📎 前提:Double/Debiased Machine Learning(DML) | 識別の仮定 | 数理:勾配ブースティング(機械学習)

要点(BLUF)


ATE から CATE へ

ここまで(Double/Debiased Machine Learning(DML))は効果が一定の ATE θ\theta を推定してきた。だが現実の効果は人によって違う。それを表すのが CATE。

τ(x)=E[Y(1)Y(0)X=x]\tau(x) = E\big[Y(1)-Y(0)\mid X=x\big]

識別の仮定

識別の仮定の条件付き交換可能性 {Y(0),Y(1)}TX\{Y(0),Y(1)\}\perp T\mid X・正値性 0<e(x)<10<e(x)<1・SUTVA の下で、CATE は観測量で書ける。

τ(x)=E[YX=x,T=1]μ1(x)E[YX=x,T=0]μ0(x)\tau(x) = \underbrace{E[Y\mid X=x,T=1]}_{\mu_1(x)} - \underbrace{E[Y\mid X=x,T=0]}_{\mu_0(x)}
flowchart LR
    X["共変量 X(交絡+効果修飾)"] --> T["処置 T"]
    X --> Y["結果 Y"]
    T -->|"効果 τ(x):X で変化"| Y

ここで XX は二役を持つ。XT, XYX\to T,\ X\to Y の経路は交絡(調整が必要)、そして処置→結果の矢の太さ τ(x)\tau(x) を変える効果修飾識別は交絡を塞げるかの問題、推定は τ(x)\tau(x) の関数形をどう当てるかの問題で、両者は別物である。

3 つのメタ学習器

任意の回帰器 μ^\widehat\mu を部品にして、μ1,μ0\mu_1,\mu_0 の当て方で流派が分かれる。

D~i1=Yiμ^0(Xi) (処置群),D~i0=μ^1(Xi)Yi (対照群)\tilde D^1_i = Y_i-\hat\mu_0(X_i)\ (\text{処置群}),\qquad \tilde D^0_i = \hat\mu_1(X_i)-Y_i\ (\text{対照群}) τ^1=reg(D~1X処置群),τ^0=reg(D~0X対照群)\hat\tau_1 = \text{reg}(\tilde D^1 \sim X\,|\,\text{処置群}),\qquad \hat\tau_0 = \text{reg}(\tilde D^0 \sim X\,|\,\text{対照群}) τ^X(x)=e^(x)τ^0(x)+(1e^(x))τ^1(x)\hat\tau_X(x) = \hat e(x)\,\hat\tau_0(x) + \big(1-\hat e(x)\big)\,\hat\tau_1(x)

加重に傾向 e^(x)\hat e(x) を使うのが要点。処置群が少数(e^\hat e 小)なら、(1e^)(1-\hat e) が大きくなり、大きい対照群で当てた μ^0\hat\mu_0 に支えられた τ^1\hat\tau_1 を重視する。これが不均衡時に効く。

コード:S/T/X-learner で τ(x) を推定し比較

真の τ(x)=1+3x0\tau(x)=1+3x_0x0x_0 で効果が変わる)を仕込み、処置割り当ては傾向 e(x)e(x) で不均衡にする。3 学習器の RMSE異質性スロープ(真値 3.0)、平均効果(真 ATE 1.0)を比べる。

# === 共変量X0で処置効果が変わる擬似データを作り、3つのメタ学習器で τ(x) を推定 ===
import numpy as np
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.linear_model import LogisticRegression

rng = np.random.default_rng(7)
n, p = 1500, 5
X = rng.standard_normal((n, p))
e = 1.0 / (1.0 + np.exp(-(-1.2 + 0.8 * X[:, 0] + 0.8 * X[:, 1])))  # 傾向スコア(交絡+不均衡)
T = rng.binomial(1, e)
mu0 = 3 * X[:, 1] + 2 * X[:, 2] + 2 * X[:, 3]                      # 大きな基準応答 E[Y(0)|x]
tau_true_fn = lambda Z: 1.0 + 3.0 * Z[:, 0]                        # 真のCATE: X0 で変化
Y = mu0 + tau_true_fn(X) * T + rng.standard_normal(n)
print("処置を受けた割合:", round(T.mean(), 3))

def base():
    return GradientBoostingRegressor(max_depth=3, n_estimators=200, random_state=0)

# --- S-learner: 1つのモデルに T も特徴量として入れる ---
s_model = base().fit(np.column_stack([X, T]), Y)
def s_tau(Z):
    y1 = s_model.predict(np.column_stack([Z, np.ones(len(Z))]))
    y0 = s_model.predict(np.column_stack([Z, np.zeros(len(Z))]))
    return y1 - y0

# --- T-learner: 処置群・対照群で別々のモデル ---
mu1 = base().fit(X[T == 1], Y[T == 1])
mu0_hat = base().fit(X[T == 0], Y[T == 0])
def t_tau(Z):
    return mu1.predict(Z) - mu0_hat.predict(Z)

# --- X-learner: 個別効果を補完してから回帰し、傾向で加重 ---
imp_treated = Y[T == 1] - mu0_hat.predict(X[T == 1])   # 処置群: 観測 − 対照予測
imp_control = mu1.predict(X[T == 0]) - Y[T == 0]       # 対照群: 処置予測 − 観測
tau1 = base().fit(X[T == 1], imp_treated)
tau0 = base().fit(X[T == 0], imp_control)
ehat = LogisticRegression(max_iter=1000).fit(X, T)
def x_tau(Z):
    g = ehat.predict_proba(Z)[:, 1]
    return g * tau0.predict(Z) + (1 - g) * tau1.predict(Z)

# --- 独立なテスト集合で真の τ(x) との誤差と異質性スロープを比較 ---
rng_te = np.random.default_rng(999)
Xte = rng_te.standard_normal((4000, p))
tau_true = tau_true_fn(Xte)
print("\n学習器     RMSE   異質性スロープ(真値3.0)   平均効果(真ATE 1.0)")
for name, fn in [("S-learner", s_tau), ("T-learner", t_tau), ("X-learner", x_tau)]:
    est = fn(Xte)
    rmse = np.sqrt(np.mean((est - tau_true) ** 2))
    slope = np.polyfit(Xte[:, 0], est, 1)[0]
    print(f"{name:10s} {rmse:5.3f}        {slope:5.3f}                {est.mean():5.3f}")

出力は次のとおり。

処置を受けた割合: 0.287

学習器     RMSE   異質性スロープ(真値3.0)   平均効果(真ATE 1.0)
S-learner  1.144        1.988                1.046
T-learner  1.368        2.583                1.227
X-learner  0.722        2.743                1.007

読み取れること。

コード:τ(x) の推定を図示

x0x_0 を動かし他の共変量を 0 に固定したスライスで、各学習器の τ^(x)\hat\tau(x) と真の直線を重ねる。

# === τ(x) の推定を図示 ===
import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib
from sklearn.ensemble import GradientBoostingRegressor
from sklearn.linear_model import LogisticRegression

rng = np.random.default_rng(7)
n, p = 1500, 5
X = rng.standard_normal((n, p))
e = 1.0 / (1.0 + np.exp(-(-1.2 + 0.8 * X[:, 0] + 0.8 * X[:, 1])))
T = rng.binomial(1, e)
mu0 = 3 * X[:, 1] + 2 * X[:, 2] + 2 * X[:, 3]
Y = mu0 + (1.0 + 3.0 * X[:, 0]) * T + rng.standard_normal(n)

def base():
    return GradientBoostingRegressor(max_depth=3, n_estimators=200, random_state=0)

s_model = base().fit(np.column_stack([X, T]), Y)
mu1 = base().fit(X[T == 1], Y[T == 1]); mu0h = base().fit(X[T == 0], Y[T == 0])
tau1 = base().fit(X[T == 1], Y[T == 1] - mu0h.predict(X[T == 1]))
tau0 = base().fit(X[T == 0], mu1.predict(X[T == 0]) - Y[T == 0])
eh = LogisticRegression(max_iter=1000).fit(X, T)

# X0 を動かし他の共変量は0に固定したスライスで τ̂(x) を描く
grid = np.linspace(-2.5, 2.5, 100)
G = np.zeros((100, p)); G[:, 0] = grid
s_hat = s_model.predict(np.column_stack([G, np.ones(100)])) - s_model.predict(np.column_stack([G, np.zeros(100)]))
t_hat = mu1.predict(G) - mu0h.predict(G)
g = eh.predict_proba(G)[:, 1]
x_hat = g * tau0.predict(G) + (1 - g) * tau1.predict(G)

plt.figure(figsize=(8, 5))
plt.plot(grid, 1.0 + 3.0 * grid, "k--", lw=2, label="真の τ(x)=1+3·X0")
plt.plot(grid, s_hat, label="S-learner")
plt.plot(grid, t_hat, label="T-learner")
plt.plot(grid, x_hat, label="X-learner")
plt.xlabel("効果修飾子 X0"); plt.ylabel("推定された処置効果 τ(x)")
plt.title("メタ学習器による異質処置効果 τ(x) の推定")
plt.legend(); plt.tight_layout(); plt.show()

図では、S-learner の直線が真の傾きより寝て(平坦化)T-learner は真の直線の周りで暴れX-learner が真の直線に最もよく沿う。異質性を見たい目的では学習器の選択が結論を変える。


直観:どれを使うか


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


関連ノート