🎓 レベル:標準 | 重要度:A(必須)
要点(BLUF)
- 差分の差分(DID) は、処置群の「事後−事前」変化から対照群の「事後−事前」変化を引き、時間不変の群差と共通の時間トレンドを同時に消して因果効果を取り出す。
- 識別の心臓部は 並行トレンド仮定:処置が無ければ両群は同じ傾きで動いたはず。これが破れるとDIDは堂々と偏る。
- 推定は 処置群×事後の交互作用 一本。仮定の点検は イベントスタディでプレトレンドを見る。
概念:2つの素朴比較はどちらも偏る
ある施策の効果を測りたい。手元には処置群と対照群、施策前後のデータがある。素朴な比較は2通りあり、どちらも偏る。
- 処置群の事後−事前:施策の効果に加え、景気など共通の時間変化が混ざる。
- 事後の処置群−対照群:施策の効果に加え、もともとの**群の差(選択)**が混ざる。
DIDは両者を組み合わせ、時間不変の群差と共通の時間トレンドを相殺する。
flowchart LR
A["群固定効果 α(時間不変の選択)"] --> T["処置 D(処置群×事後)"]
A --> Y["結果 Y"]
L["時間ショック λ(両群共通)"] --> Y
T --> Y
群差 は群内の前後差分で消え、共通ショック は対照群を引くことで消える。残るのが効果 ――ただし が両群共通である場合に限る。
識別の仮定:並行トレンド
処置なしの潜在結果を加法的に置く。
観測結果は、処置群かつ事後でのみ効果 が乗る()。
並行トレンド仮定は、処置が無かったときの両群の変化が等しいこと。
これは「対照群の時間変化 が処置群の反実仮想トレンドと一致する」という主張で、検証不能(処置群の反実仮想は見えない)。このとき DID 推定量
が に一致する。回帰では交互作用項の係数として一発で出る。
コード:真の効果を仕込んでDIDで回収する
2群×2期のパネルに、群固定効果(処置群が高い=選択)と両群共通の時間効果(並行トレンド成立)を入れ、処置群×事後にだけ真の効果 を加える。素朴比較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で真値をほぼ回収した。前後差分が群差 を、対照群の引き算が時間効果 を消したからだ。
並行トレンドが破れると偏る
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近傍)なら、仮定の信憑性が高まる。これがイベントスタディ。処置開始を として、 を基準に各時点の相対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回差分することで、時間不変の群差(能力・地域性など観測不能でも一定なら可)と、両群共通の時間ショック(景気・季節)を同時に消す。代償は並行トレンド――「もし処置が無ければ両群は平行に動いた」という、効果が乗る期間には決して観測できない反実仮想への信頼である。
⚠️ よくある誤解・落とし穴
- プレトレンドが平らでも並行トレンドの証明にはならない。過去が平行でも将来も平行とは限らない。あくまで状況証拠。
- 時間で変化する交絡には無力。群ごとに異なる時間変化(景気感応度の差など)はDIDで消えない。
- アンティシペーション(予期)。処置を見越して事前に行動が変わると、事前期間が汚染される。
- 構成変化。各期でサンプルの顔ぶれが変わる(移住・脱落)と、群差一定の前提が崩れる。
- 多期間・段階的処置の罠:時点をずらして処置が入る設計に普通の双方向固定効果回帰を使うと、すでに処置済みの群を対照に使ってしまい偏りうる(要最新確認=近年は Callaway–Sant’Anna 等の推定量が提案されている)。
関連ノート
- 操作変数法と2SLS — 未観測交絡が時間で変わるならIV、一定ならDID
- 回帰不連続デザイン — 閾値の連続性で識別する別系統の準実験
- 合成コントロール法 — 対照群を重み付き合成で作るDIDの一般化
- 潜在結果モデル — と反実仮想トレンドの定義
- 重回帰分析(統計)— 交互作用項とダミー変数回帰