本ページでは、 パネルデータ分析と因果推論を統合的に解説します。 固定効果・変量効果・RCT・DID・IV・RDD・傾向スコア・交絡を一気通貫で扱います。
「相関は因果ではない」は有名な格言。 では観察データから因果を取り出すにはどうすればよいか? 計量経済学・統計疫学・社会科学で発展した方法論を体系的に学びます。
論文記事から各用語のリンクをクリックすると、 該当箇所が開きます:
機械学習・統計学は「相関や予測」に強い一方、 「もし介入したら何が起きるか」という問いには直接答えられない。
政策評価・医療効果検証・マーケティングの ROI 計算など、 実務の多くは因果推論の問題。
古典的事例:「アイスクリーム消費量と水難事故数」は強く正相関するが、 因果関係はない(共通原因=気温)。
処置 $T$ と結果 $Y$ の両方に影響する第三変数 $C$ があると、 単純な相関では因果を取り出せない。
$$T \leftarrow C \rightarrow Y$$
例:教育年数 $T$ → 賃金 $Y$ の関係。 親の年収 $C$ が両方に影響 → 単純比較では教育の効果が過大評価される。
Pearl による因果推論の言語。 矢印 $X \to Y$ で「$X$ が $Y$ の原因」を表す。 「バックドア基準」を満たす調整変数を特定すれば、 観察データから因果効果が同定できる。
個体 $i$ について:
根本問題:同じ個体について両方は観測できない。 観測されるのは $Y_i = T_i Y_i(1) + (1-T_i) Y_i(0)$ のみ。
「もし処置を受けていなかったら、 この個体の結果はどうだったか?」を推測することが因果推論の核心。
処置をランダムに割り当てれば、 処置群と対照群が平均的に類似 → $T \perp \{Y(0), Y(1)\}$ が成り立つ。 ATE は単純な差で不偏推定可能:
$$\widehat{\mathrm{ATE}} = \bar{Y}_{\text{処置}} - \bar{Y}_{\text{対照}}$$
data/raw/SSDSE-B-2026.csv。 MultiIndex (都道府県, 年度) で整形。 説明:高齢化率、 目的:食料費。1 2 3 4 5 6 7 8 9 | import numpy as np from scipy import stats # 想定 RCT 結果:処置群 n=100、 対照群 n=100 treat = np.array([...]) ctrl = np.array([...]) t, p = stats.ttest_ind(treat, ctrl, equal_var=False) ate = treat.mean() - ctrl.mean() se = np.sqrt(treat.var(ddof=1)/len(treat) + ctrl.var(ddof=1)/len(ctrl)) print(f'ATE = {ate:.3f}, 95% CI = [{ate-1.96*se:.3f}, {ate+1.96*se:.3f}]') |
処置群と対照群の時間変化の差から因果効果を推定。 共通トレンド仮定が鍵。
$$\widehat{\mathrm{DID}} = (\bar{Y}^{\text{処置}}_{\text{後}} - \bar{Y}^{\text{処置}}_{\text{前}}) - (\bar{Y}^{\text{対照}}_{\text{後}} - \bar{Y}^{\text{対照}}_{\text{前}})$$
$$Y_{it} = \beta_0 + \beta_1 \mathrm{Treat}_i + \beta_2 \mathrm{Post}_t + \beta_3 (\mathrm{Treat}_i \times \mathrm{Post}_t) + \varepsilon_{it}$$
$\beta_3$ が DID 推定量。
data/raw/SSDSE-B-2026.csv。 MultiIndex (都道府県, 年度) で整形。 説明:高齢化率、 目的:食料費。1 2 3 4 5 | import statsmodels.formula.api as smf # パネルデータ:複数都道府県の時点 × 処置有無 model = smf.ols('Y ~ Treat + Post + Treat:Post + C(都道府県)', data=panel).fit() print(model.summary()) # Treat:Post の係数が DID 推定量 |
処置前期間で処置群・対照群のトレンドが平行か → イベントスタディ・プラセボ検定。
処置 $T$ と結果 $Y$ の両方に影響する未観測交絡があるとき、 次の条件を満たす変数 $Z$(操作変数)を使って因果を識別:
data/raw/SSDSE-B-2026.csv。 MultiIndex (都道府県, 年度) で整形。 説明:高齢化率、 目的:食料費。1 2 3 | from linearmodels.iv import IV2SLS mod = IV2SLS.from_formula('Y ~ 1 + [T ~ Z] + X1 + X2', data=df).fit() print(mod.summary) |
連続変数 $X$ の閾値 $c$ を境に処置が変わる場合、 閾値前後で他の条件はほぼ同じ → 閾値での結果のジャンプが因果効果。
$$\tau_{\mathrm{RDD}} = \lim_{x\downarrow c}\mathbb{E}[Y|X=x] - \lim_{x\uparrow c}\mathbb{E}[Y|X=x]$$
data/raw/SSDSE-B-2026.csv。 MultiIndex (都道府県, 年度) で整形。 説明:高齢化率、 目的:食料費。1 2 3 4 | import numpy as np from rdrobust import rdrobust result = rdrobust(y=df['Y'], x=df['X'], c=cutoff) print(result) |
処置確率 $e(x) = P(T=1|X=x)$ を共変量から推定(ロジスティック等)。 「条件付き独立性(CIA)」が成り立てば、 傾向スコアでバランスを取れば因果効果が同定可。
処置群の各個体に「傾向スコアが近い対照群個体」をマッチ。
data/raw/SSDSE-B-2026.csv。 MultiIndex (都道府県, 年度) で整形。 説明:高齢化率、 目的:食料費。1 2 3 4 5 6 7 8 9 10 | from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler # 傾向スコア推定 ps_model = LogisticRegression(max_iter=1000).fit(X_covariates, T) df['ps'] = ps_model.predict_proba(X_covariates)[:, 1] # 最近傍マッチング from sklearn.neighbors import NearestNeighbors nn = NearestNeighbors(n_neighbors=1).fit(df[df.T==0][['ps']]) dist, idx = nn.kneighbors(df[df.T==1][['ps']]) # マッチ後の差で ATT 推定 |
$$\widehat{\mathrm{ATE}}_{\mathrm{IPW}} = \frac{1}{n}\sum_{i} \left[\frac{T_i Y_i}{\hat{e}(X_i)} - \frac{(1-T_i)Y_i}{1-\hat{e}(X_i)}\right]$$
処置確率の逆数で重み付けして単純差を取る。
傾向スコアと結果モデルの両方を使い、 どちらか一方が正しければ一致推定量。
同じ個体(人・企業・都道府県)を複数時点で観察したデータ。 個体固有の異質性を制御できる。
$$Y_{it} = \alpha_i + \boldsymbol{\beta}^\top \mathbf{x}_{it} + \varepsilon_{it}$$
$\alpha_i$ は個体ごとの切片(時間不変の異質性)。 個体内変動だけで $\beta$ を識別。 時間不変の交絡が自動的に消える。
data/raw/SSDSE-B-2026.csv。 MultiIndex (都道府県, 年度) で整形。 説明:高齢化率、 目的:食料費。1 2 3 4 | from linearmodels.panel import PanelOLS df_p = panel.set_index(['都道府県', '年']) fe = PanelOLS.from_formula('Y ~ X1 + X2 + EntityEffects', data=df_p).fit() print(fe.summary) |
$\alpha_i$ をランダム変数とみなす。 説明変数と $\alpha_i$ が独立なら FE より効率的だが、 仮定が崩れると不偏でなくなる。
FE と RE の係数が体系的に違うか検定。 違いが有意なら FE を採用すべき。
data/raw/SSDSE-B-2026.csv。 MultiIndex (都道府県, 年度) で整形。 説明:高齢化率、 目的:食料費。1 2 3 4 | from linearmodels.panel import PanelOLS, RandomEffects fe = PanelOLS.from_formula('Y ~ X + EntityEffects', data=df_p).fit() re = RandomEffects.from_formula('Y ~ X', data=df_p).fit() # 手動でHausman統計量を計算(または compare で) |
個体効果 $\alpha_i$ + 時間効果 $\delta_t$。 DID と同等。
未観測交絡があった場合に結論がどれだけ変わるか評価。
| 手法 | 必要な条件 | 推定する効果 |
|---|---|---|
| RCT | ランダム割当 | ATE |
| DID | 共通トレンド + 処置タイミング | ATT |
| IV | 関連性 + 外生性 | LATE |
| RDD | 明確な閾値 | 境界での効果 |
| 傾向スコア | 条件付き独立性 (CIA) | ATE/ATT |
| 固定効果 | パネルデータ + 時間不変交絡 | 時間内変動効果 |
| 合成統制法 | 十分な対照ユニット | 単一処置ユニットの効果 |
| 落とし穴 | 対処 |
|---|---|
| 「相関=因果」と書く | 識別戦略を明示しない限り「関連が見られた」に留める。 |
| 媒介変数を調整 | 処置の下流変数を入れると効果が消える。 DAG で確認。 |
| 合流点を調整 | 処置と結果の両方に影響される変数を調整するとバイアスが新たに生じる。 |
| DID で共通トレンド未確認 | 処置前期間のプレトレンドをプロット。 |
| 弱いIV | 第1段階 F > 10 を目安。 弱IVは強いバイアス。 |
| 傾向スコアの overlap 不足 | 分布を可視化、 共通サポート領域に絞る。 |
| 標準誤差をクラスタリングしない | パネルでは個体クラスタロバスト SE を使う。 |
注意:SSDSE-B は単一年度の横断データなので、 因果推論の練習には e-Stat の都道府県別パネル(複数年)を組み合わせてください。
data/raw/SSDSE-B-2026.csv。 MultiIndex (都道府県, 年度) で整形。 説明:高齢化率、 目的:食料費。1 2 3 4 5 6 7 | from linearmodels.panel import PanelOLS import statsmodels.formula.api as smf df_p = panel.set_index(['都道府県', '年']) ols = smf.ols('Y ~ X', data=panel).fit() fe = PanelOLS.from_formula('Y ~ X + EntityEffects', data=df_p).fit() print('OLS β:', ols.params['X']) print('FE β:', fe.params['X']) |
data/raw/SSDSE-B-2026.csv。 MultiIndex (都道府県, 年度) で整形。 説明:高齢化率、 目的:食料費。1 2 3 4 | import statsmodels.formula.api as smf m = smf.ols('Y ~ Treat*Post + C(都道府県) + C(年)', data=panel, cluster_kwds={'groups': panel['都道府県']}).fit() print(m.summary()) |
data/raw/SSDSE-B-2026.csv。 MultiIndex (都道府県, 年度) で整形。 説明:高齢化率、 目的:食料費。1 2 3 4 5 6 7 8 9 10 | import pandas as pd from sklearn.linear_model import LogisticRegression df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='utf-8', skiprows=1) df['T'] = (df['人口密度'] >= df['人口密度'].median()).astype(int) X = df[['高齢化率', '世帯人員']] ps = LogisticRegression().fit(X, df['T']).predict_proba(X)[:, 1] df['w'] = df['T'] / ps + (1-df['T']) / (1-ps) # IPW で ATE 推定(要:注意深く解釈) ate = (df['T'] * df['一人当たり県民所得'] / ps).mean() - ((1-df['T']) * df['一人当たり県民所得'] / (1-ps)).mean() print(f'IPW-ATE = {ate:.0f}') |
「都市規模が高い県ほど所得が高い、 という因果関係を確認した。」
「DID(処置:政策導入 2022 年、 処置群 5 県、 対照群 42 県、 期間 2020-2024)で推定。 処置効果 = 23.4 万円 (95% CI [12.1, 34.7]、 都道府県クラスタロバスト SE)。 プレトレンドはイベントスタディで概ね平行(処置前のリード係数は有意でない)。 ただし共通トレンド仮定の検証は限定的で、 結果は条件付き因果関係として解釈すべき。」
| 用途 | 関数・パッケージ |
|---|---|
| パネル回帰 | linearmodels.panel.PanelOLS, RandomEffects |
| 2SLS | linearmodels.iv.IV2SLS |
| DID | statsmodels.formula.api.ols, differences (PyPI) |
| RDD | rdrobust (PyPI) |
| 傾向スコア | causalinference, doWhy, EconML |
| マッチング | causalml, dowhy |
| 合成統制法 | SyntheticControlMethods (PyPI) |
| DAG | networkx, daggity (R), pgmpy |
| CATE推定 | EconML, causalml |
| DoWhy(統合) | dowhy (Microsoft) |
2003 Abadie ら。 単一処置ユニット(例:1 つの州・国)に対し、 他のユニットの加重平均で「人工的な対照」を作る。
例:カリフォルニア州のタバコ税引上の効果評価。 他の州の加重平均で「タバコ税のない仮想カリフォルニア」を作り比較。
個体・部分集団ごとの処置効果。 機械学習で推定(Causal Forest、 X-Learner、 R-Learner、 DR-Learner)。
data/raw/SSDSE-B-2026.csv。 MultiIndex (都道府県, 年度) で整形。 説明:高齢化率、 目的:食料費。1 2 3 4 | from econml.dml import CausalForestDML cf = CausalForestDML(model_y='auto', model_t='auto', discrete_treatment=True) cf.fit(Y=y, T=t, X=X_features, W=W_controls) cate = cf.effect(X_features) # 各個体の処置効果 |
Chernozhukov ら 2018。 結果と処置を高次元共変量から ML で予測し、 残差で因果効果を推定。 高次元共変量にも対応。
傾向スコアと結果モデルの両方を ML で推定し、 どちらか一方が正しければ一致。
「処置すれば反応する人」を予測(CATE が大きい人)。 マーケティング配信の効率化。
A. すべての交絡が観測されていれば原理的には可能(CIA)。 ただし「すべてを観測」は実務でほぼ不可能。 識別戦略を明示し、 未観測交絡への感度分析が必須。
A. 数学的にはほぼ同じ。 DID は「2 群 × 2 時点」の単純設定、 双方向固定効果は同じロジックを多群・多時点に一般化したもの。
A. RCT は黄金律ですが、 倫理・コスト・遵守率・外的妥当性の問題があり、 観察データの分析が必要な場面は多い。 また RCT 内でも CATE 推定は ML 因果推論が必要。
A. 横断データだけでは因果推論は本来困難。 ドメイン知識で交絡を網羅し、 傾向スコアで「条件付き因果関係」と限定的に解釈するのが現実的。
DID・自然実験を使うときの最も重要な仮定は「平行トレンド」です。
パネル因果推論に関連する手法・概念のチップ集。
SSDSE-B-2026 は 47都道府県 × 複数年のパネル構造を持つので、 固定効果モデル・DiD の練習に最適。
data/raw/SSDSE-B-2026.csv。 MultiIndex (都道府県, 年度) で整形。 説明:高齢化率、 目的:食料費。1 2 3 4 5 6 7 8 9 10 11 12 13 | import pandas as pd from linearmodels import PanelOLS df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1) # パネル構造:県 × 年 が複数行 df = df.set_index(['都道府県', df.columns[0]]) # 1列目を年と仮定 num_cols = df.select_dtypes('number').columns y = df[num_cols[0]] X = df[num_cols[1:3]] model = PanelOLS(y, X, entity_effects=True, time_effects=True).fit( cov_type='clustered', cluster_entity=True) print(model) |
例えば「2020年に高齢化率が上位10県に入った県」を「処置群」とみなし、 介入前後で死亡率の変化を比較する設計(あくまで教育用の擬似 DiD)。
data/raw/SSDSE-B-2026.csv。 MultiIndex (都道府県, 年度) で整形。 説明:高齢化率、 目的:食料費。1 2 3 4 5 6 7 8 9 10 11 12 | import pandas as pd import statsmodels.formula.api as smf df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1) num_cols = df.select_dtypes('number').columns df['処置'] = (df[num_cols[0]] >= df[num_cols[0]].quantile(0.8)).astype(int) df['post'] = (df[num_cols[1]] >= df[num_cols[1]].median()).astype(int) df['DiD'] = df['処置'] * df['post'] model = smf.ols(f'{num_cols[2]} ~ 処置 + post + DiD', data=df.rename(columns={num_cols[2]:'y'})).fit() print(model.summary()) |
data/raw/SSDSE-B-2026.csv。 MultiIndex (都道府県, 年度) で整形。 説明:高齢化率、 目的:食料費。1 2 3 4 5 6 7 8 9 10 11 | from linearmodels.panel import PanelOLS, RandomEffects, compare import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1) df = df.set_index(['都道府県', df.columns[0]]) num_cols = df.select_dtypes('number').columns y, X = df[num_cols[0]], df[num_cols[1:3]] fe = PanelOLS(y, X, entity_effects=True).fit(cov_type='clustered', cluster_entity=True) re = RandomEffects(y, X).fit() print(compare({'FE': fe, 'RE': re})) |
data/raw/SSDSE-B-2026.csv。 MultiIndex (都道府県, 年度) で整形。 説明:高齢化率、 目的:食料費。1 2 3 4 5 6 7 8 9 10 11 12 | import statsmodels.formula.api as smf import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1) num_cols = df.select_dtypes('number').columns df['処置'] = (df[num_cols[0]] >= df[num_cols[0]].quantile(0.8)).astype(int) df['post'] = (df[num_cols[1]] >= df[num_cols[1]].median()).astype(int) df = df.rename(columns={num_cols[2]:'y'}) model = smf.ols('y ~ 処置 * post + C(都道府県)', data=df).fit( cov_type='cluster', cov_kwds={'groups': df['都道府県']}) print(model.summary()) |
data/raw/SSDSE-B-2026.csv。 MultiIndex (都道府県, 年度) で整形。 説明:高齢化率、 目的:食料費。1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | from econml.dml import CausalForestDML from sklearn.ensemble import RandomForestRegressor, RandomForestClassifier import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1) num = df.select_dtypes('number') Y = num.iloc[:, 2].values T = (num.iloc[:, 0] >= num.iloc[:, 0].median()).astype(int).values X = num.iloc[:, 3:8].values cf = CausalForestDML( model_y=RandomForestRegressor(n_estimators=100), model_t=RandomForestClassifier(n_estimators=100), discrete_treatment=True ) cf.fit(Y, T, X=X) print('CATE 平均:', cf.effect(X).mean()) |
data/raw/SSDSE-B-2026.csv。 MultiIndex (都道府県, 年度) で整形。 説明:高齢化率、 目的:食料費。1 2 3 4 5 6 7 8 9 10 11 12 13 14 | import numpy as np from scipy.stats import chi2 # fe, re は linearmodels の推定結果 b_fe = fe.params.values b_re = re.params.values v_fe = fe.cov.values v_re = re.cov.values diff = b_fe - b_re var_diff = v_fe - v_re stat = diff @ np.linalg.pinv(var_diff) @ diff pval = 1 - chi2.cdf(stat, df=len(diff)) print(f'Hausman 統計量={stat:.3f}, p={pval:.4f}') |
data/raw/SSDSE-B-2026.csv。 MultiIndex (都道府県, 年度) で整形。 説明:高齢化率、 目的:食料費。1 2 3 4 5 6 7 8 9 10 11 12 13 | from dowhy import CausalModel import pandas as pd df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', header=1) num = df.select_dtypes('number') df['T'] = (num.iloc[:, 0] >= num.iloc[:, 0].median()).astype(int) df['Y'] = num.iloc[:, 1] model = CausalModel(data=df, treatment='T', outcome='Y', common_causes=list(num.columns[2:5])) identified = model.identify_effect() estimate = model.estimate_effect(identified, method_name='backdoor.linear_regression') print('ATE:', estimate.value) |
パネル因果分析は「同じ個体(人・地域・企業)を複数時点で観察し、 個体の固定効果を制御した上で介入効果を推定する」枠組み。 観察できない個体差をパネル構造で吸収できるのが強み。 SSDSE-B-2026 は 2008-2023 の 16 年×47 県のパネルなので、 県固定効果+年固定効果+政策ダミーで効果検証できる。
パネル因果分析 は「因果推論」カテゴリの中核概念。 初めて触れる読者は、 まずこの「🎨 直感」セクションだけ通読し、 必要になった時点で「📐 数式」「🐍 Python」「⚠️ 落とし穴」へ戻る読み方が定着しやすいです。
直感の次は、 厳密な定義を確認します。 数式は言語の一種で、 一度書き慣れれば「言葉より速く伝えられる」便利な道具。 慣れていない方は、 各記号が何を表すかを下の「🔬 記号読み解き」で 1 つずつ確認してください。
上の数式を眺めるだけでは身につかないので、 各記号がどんな役割を担っているかを言葉で押さえます。 「数式を音読する習慣」がつくと、 論文や教科書を読むスピードが体感で 2 倍ほど上がります。
数式だけでは「実感」が湧きにくいので、 実データ data/raw/SSDSE-B-2026.csv(47 都道府県 × 16 年)で 1 度手計算してみると理解が定着します。
SSDSE-B-2026 を使い、 「2020 年コロナ以降で L3221 が下がったか」を Fixed Effects 回帰で推定する。 県固定効果 47 個+年固定効果 16 個+ Post 2020 ダミーで OLS。 47×16=752 観測。 Post 2020 係数は -8,000 円程度(標本依存)になる場合があり、 県差・時点差を取り除いた純粋な「政策・パンデミック効果」が推定できる。
| 都道府県 | A1101 総人口 | A1303 65 歳以上 | L3221 消費支出 |
|---|---|---|---|
| 東京都 | 14,086,000 | 3,205,000 | 341,320 |
| 神奈川県 | 9,229,000 | 2,390,000 | 306,565 |
| 大阪府 | 8,763,000 | 2,424,000 | 271,246 |
| 愛知県 | 7,477,000 | 1,923,000 | 300,221 |
| 埼玉県 | 7,331,000 | 2,012,000 | 344,092 |
| 千葉県 | 6,257,000 | 1,756,000 | 306,943 |
上記は SSDSE-B-2026 (2023) からの抜粋。 手計算で確認した値が、 後述の Python 実装で得る値と一致することを確認すると、 「数式とコードの対応関係」がクリアに見えるようになります。
公的統計(SSDSE-B-2026)を題材に、 最小限の Python コードで パネル因果分析 を動作させます。 まずはこのまま実行してみてください。
# パネル因果分析 を SSDSE-B-2026 で実行する最小コード
import pandas as pd
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df[df['SSDSE-B-2026'] == 2023] # 2023 年のみ抽出
print(df.shape) # (47, 112)
print(df[['Prefecture','A1101','A1303','L3221']].head())
import pandas as pd
import statsmodels.api as sm
from statsmodels.regression.linear_model import OLS
# panel = 全年度を使う
panel = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
panel['post'] = (panel['SSDSE-B-2026'] >= 2020).astype(int)
# 県・年ダミー
X = pd.get_dummies(panel[['Prefecture','SSDSE-B-2026','post']],
columns=['Prefecture','SSDSE-B-2026'], drop_first=True)
X = X.astype(float)
X = sm.add_constant(X)
y = panel['L3221'].astype(float)
res = OLS(y, X).fit()
print('post係数:', res.params['post'])
print('post t:', res.tvalues['post'])
上のコードで動かない場合は、 ①必要なパッケージがインストール済みか(pip install pandas scikit-learn scipy statsmodels matplotlib)、 ②データファイルが data/raw/SSDSE-B-2026.csv に存在するか、 ③encoding='cp932' になっているかを確認してください。
パネル因果分析 を使うときに初学者が踏みやすい失敗パターン。 1 度経験してしまえば次から避けられますが、 先に知っておくに越したことはありません。