Mímisbrunnr知恵の泉

← 因果推論 一覧

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

📎 前提:潜在結果モデル | 識別の仮定 | 数理:重回帰分析(統計)

要点(BLUF)


概念:2つの素朴比較はどちらも偏る

ある施策の効果を測りたい。手元には処置群と対照群、施策前後のデータがある。素朴な比較は2通りあり、どちらも偏る。

DIDは両者を組み合わせ、時間不変の群差共通の時間トレンドを相殺する。

flowchart LR
    A["群固定効果 α(時間不変の選択)"] --> T["処置 D(処置群×事後)"]
    A --> Y["結果 Y"]
    L["時間ショック λ(両群共通)"] --> Y
    T --> Y

群差 α\alpha は群内の前後差分で消え、共通ショック λ\lambda は対照群を引くことで消える。残るのが効果 δ\delta――ただし λ\lambda両群共通である場合に限る。

識別の仮定:並行トレンド

処置なしの潜在結果を加法的に置く。

Yigt(0)=αg+λt+εigtY_{igt}(0) = \alpha_g + \lambda_t + \varepsilon_{igt}

観測結果は、処置群かつ事後でのみ効果 δ\delta が乗る(Dgt=1{g=処置,t=事後}D_{gt}=\mathbf{1}\{g=\text{処置},\,t=\text{事後}\})。

Yigt=Yigt(0)+δDgtY_{igt} = Y_{igt}(0) + \delta\,D_{gt}

並行トレンド仮定は、処置が無かったときの両群の変化が等しいこと。

E[Y1,post(0)Y1,pre(0)]=E[Y0,post(0)Y0,pre(0)]E[Y_{1,\text{post}}(0) - Y_{1,\text{pre}}(0)] = E[Y_{0,\text{post}}(0) - Y_{0,\text{pre}}(0)]

これは「対照群の時間変化 λpostλpre\lambda_{\text{post}}-\lambda_{\text{pre}} が処置群の反実仮想トレンドと一致する」という主張で、検証不能(処置群の反実仮想は見えない)。このとき DID 推定量

δ^=(Yˉ1,postYˉ1,pre)(Yˉ0,postYˉ0,pre)\hat\delta = \big(\bar Y_{1,\text{post}} - \bar Y_{1,\text{pre}}\big) - \big(\bar Y_{0,\text{post}} - \bar Y_{0,\text{pre}}\big)

δ\delta に一致する。回帰では交互作用項の係数として一発で出る。

Y=β0+β1treated+β2post+δ(treated×post)+εY = \beta_0 + \beta_1\,\text{treated} + \beta_2\,\text{post} + \delta\,(\text{treated}\times\text{post}) + \varepsilon

コード:真の効果を仕込んでDIDで回収する

2群×2期のパネルに、群固定効果(処置群が高い=選択)と両群共通の時間効果(並行トレンド成立)を入れ、処置群×事後にだけ真の効果 δ=5\delta=5 を加える。素朴比較2種とDIDを比べる。

import numpy as np
import pandas as pd
import statsmodels.formula.api as smf

# === 2群×2期のパネルに真の効果を仕込み、DID(交互作用)で回収する ===
rng = np.random.default_rng(0)
n = 3000                                  # 各 群×期 のサンプル数
delta_true = 5.0                          # 仕込んだ真の処置効果(ATT)

def make_cell(group, period, mean0):
    # mean0 = 処置なしの潜在結果 Y(0) の水準(群固定効果+時間効果)
    y0 = mean0 + rng.normal(0, 1, n)
    return pd.DataFrame({"treated": group, "post": period, "Y0": y0})

# 群固定効果:treated=3, control=0/時間効果:post で +2(両群共通=並行トレンド)
ctrl_pre = make_cell(0, 0, 0 + 0)
ctrl_post = make_cell(0, 1, 0 + 2)
trt_pre = make_cell(1, 0, 3 + 0)
trt_post = make_cell(1, 1, 3 + 2)

data = pd.concat([ctrl_pre, ctrl_post, trt_pre, trt_post], ignore_index=True)
# 処置群×事後にだけ真の効果を加える
data["Y"] = data["Y0"] + delta_true * (data["treated"] * data["post"])

# 素朴比較
m = data.groupby(["treated", "post"])["Y"].mean()
naive_prepost = m[(1, 1)] - m[(1, 0)]            # 処置群の事後−事前
naive_cross = m[(1, 1)] - m[(0, 1)]              # 事後の処置群−対照群

# DID:交互作用項の係数
did = smf.ols("Y ~ treated + post + treated:post", data=data).fit()

print(f"真の効果 delta_true       = {delta_true:.3f}")
print(f"素朴:処置群の事後−事前   = {naive_prepost:.3f}")
print(f"素朴:事後の処置群−対照群 = {naive_cross:.3f}")
print(f"DID(交互作用係数)       = {did.params['treated:post']:.3f}")

出力は次の通り。

真の効果 delta_true       = 5.000
素朴:処置群の事後−事前   = 6.981
素朴:事後の処置群−対照群 = 7.971
DID(交互作用係数)       = 4.916

素朴な前後比較は6.981(共通の時間効果 +2 が上乗せ)、素朴な群間比較は7.971(群差 +3 が上乗せ)と、どちらも真値 5 から偏る。DIDは4.916で真値をほぼ回収した。前後差分が群差 α\alpha を、対照群の引き算が時間効果 λ\lambda を消したからだ。

並行トレンドが破れると偏る

DIDの正しさは並行トレンドに全面的に依存する。処置群が処置と無関係に独自のトレンド(+5)を持つ設定にすると、DIDはその差分トレンドを効果と取り違える。

import numpy as np
import pandas as pd
import statsmodels.formula.api as smf

# === 並行トレンドが破れると DID が偏ることを数値で見る ===
rng = np.random.default_rng(0)
n = 3000
delta_true = 5.0

def make_cell(group, period, mean0):
    y0 = mean0 + rng.normal(0, 1, n)
    return pd.DataFrame({"treated": group, "post": period, "Y0": y0})

# 対照群の時間効果は +2、処置群は処置が無くても +5(差分トレンド=並行が破れる)
ctrl_pre = make_cell(0, 0, 0 + 0)
ctrl_post = make_cell(0, 1, 0 + 2)
trt_pre = make_cell(1, 0, 3 + 0)
trt_post = make_cell(1, 1, 3 + 5)        # 処置なしでも +5 上がる群

data = pd.concat([ctrl_pre, ctrl_post, trt_pre, trt_post], ignore_index=True)
data["Y"] = data["Y0"] + delta_true * (data["treated"] * data["post"])

did = smf.ols("Y ~ treated + post + treated:post", data=data).fit()
bias = did.params["treated:post"] - delta_true

print(f"真の効果 delta_true   = {delta_true:.3f}")
print(f"DID推定               = {did.params['treated:post']:.3f}")
print(f"偏り(差分トレンド分) = {bias:.3f}")

出力は次の通り。

真の効果 delta_true   = 5.000
DID推定               = 7.916
偏り(差分トレンド分) = 2.916

DIDは7.916と**+2.9も過大評価**した。対照群の時間効果は +2、処置群の反実仮想は +5 で、その差(並行トレンドの破れ)がそのまま偏りになっている。DIDは並行トレンドという仮定の上でしか因果でない

イベントスタディ:プレトレンドを目で見る

並行トレンドは反実仮想なので直接は検証できない。だが処置前の複数期で「処置群−対照群」の相対差が一定(係数が0近傍)なら、仮定の信憑性が高まる。これがイベントスタディ。処置開始を k=0k=0 として、k=1k=-1 を基準に各時点の相対DIDを描く。

import numpy as np
import matplotlib.pyplot as plt
import japanize_matplotlib

# === イベントスタディ:プレトレンドを図示して並行トレンドを目視点検する ===
rng = np.random.default_rng(3)
n = 4000
event_times = np.array([-3, -2, -1, 0, 1, 2])   # 0 から処置開始
delta_true = 5.0

def cell_mean(group_base, trend_per_period, e, treat_on):
    # 処置なしの水準=群ベース+(差分)トレンド×時点、処置は e>=0 でのみ加わる
    y0 = group_base + trend_per_period * e + rng.normal(0, 1, n)
    y = y0 + (delta_true if (treat_on and e >= 0) else 0.0)
    return y.mean()

def event_coefs(treated_trend):
    # 各時点の「処置群−対照群」を t=-1 基準で差し引いた相対DID
    trt = {e: cell_mean(3.0, treated_trend, e, True) for e in event_times}
    ctr = {e: cell_mean(0.0, 0.0, e, False) for e in event_times}
    base = trt[-1] - ctr[-1]
    return np.array([(trt[e] - ctr[e]) - base for e in event_times])

coef_parallel = event_coefs(treated_trend=0.0)    # 並行トレンドが成立
coef_violated = event_coefs(treated_trend=1.2)    # 処置群が独自トレンドを持つ

fig, axes = plt.subplots(1, 2, figsize=(11, 4.2), sharey=True)
for ax, coef, title in [(axes[0], coef_parallel, "並行トレンド成立"),
                        (axes[1], coef_violated, "並行トレンド違反")]:
    ax.axhline(0, color="gray", lw=0.8)
    ax.axvline(-0.5, color="red", ls="--", lw=1, label="処置開始")
    ax.plot(event_times, coef, "o-", color="steelblue")
    ax.axhline(delta_true, color="green", ls=":", lw=1, label="真の効果=5")
    ax.set_title(title); ax.set_xlabel("イベント時点(処置=0)")
    ax.legend(fontsize=9)
axes[0].set_ylabel("相対DID(t=-1基準)")
plt.tight_layout()
plt.show()
print("並行:プレ期間係数 =", np.round(coef_parallel[:2], 2), " 事後 =", np.round(coef_parallel[3:], 2))
print("違反:プレ期間係数 =", np.round(coef_violated[:2], 2), " 事後 =", np.round(coef_violated[3:], 2))

出力は次の通り。

並行:プレ期間係数 = [-0.02 -0.01]  事後 = [4.96 4.97 5.01]
違反:プレ期間係数 = [-2.39 -1.2 ]  事後 = [6.21 7.39 8.57]

並行トレンド成立ではプレ期間の係数が −0.02、−0.01 とほぼ0で、事後は 4.96〜5.01 と真値 5 にフラットに乗る(左図)。並行トレンド違反ではプレ期間が −2.39、−1.20 と右肩上がりに傾き、事後の係数も 6.21→7.39→8.57 と膨らみ続ける(右図)。プレ期間の傾きは「処置前から両群が乖離していた」警告灯であり、これが立つDIDは信用できない。

仮定の直観:なぜ「差の差」なのか

1回の差分(前後 or 群間)では交絡が1つしか消えない。DIDは2回差分することで、時間不変の群差(能力・地域性など観測不能でも一定なら可)と、両群共通の時間ショック(景気・季節)を同時に消す。代償は並行トレンド――「もし処置が無ければ両群は平行に動いた」という、効果が乗る期間には決して観測できない反実仮想への信頼である。

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

関連ノート