論文一覧に戻る 📚 用語解説(ジャストインタイム型データサイエンス教育)
回帰不連続デザイン
Regression Discontinuity Design (RDD)
「ある閾値(カットオフ)を境に処置を受けるか決まる」状況で、 境界直近の対象を比較することで因果効果を識別する準実験デザイン。
例:合格点 ±1 点の受験者は実質ランダム → 合格の効果を推定できる。 「不連続のジャンプ」 = 因果効果。
因果推論 準実験 局所線形回帰 識別戦略 政策評価

🔖 キーワード索引

rdd」は統計データ分析の文脈で扱う重要概念のひとつ。 本ページでは「rdd」を取り巻く中核キーワードを以下にチップで一覧化する。 各キーワードは関連する概念・手法・道具立てを含み、 文献検索や学習計画の起点になる。

rdd統計分析SSDSE-B-2026前提条件適用範囲落とし穴関連手法Python 実装検証方法

これらのキーワードは「rdd の理解 → 適用 → 検証」のプロセスを構成する。 各章で詳しく解説する。

💡 30秒で分かる結論

🍰 まずはやさしく

境界線で分ける方法です。

ある仕組みの効果を調べるために使います。

テストの合格点のような基準で考えます。

この手法の結論を短くまとめます。

📍 あなたが今見ているもの

🍰 まずはやさしく

データの境界線を使う手法です。

社会のルールから因果関係を探ります。

人口で市か町かが決まる例があります。

この手法がどう使われるかを読みます。

政策評価論文や因果推論教科書で、 こんな表現を見たはずです:

人口 10 万人を境に、 上回る自治体は「市」、 下回る自治体は「町」となる。
RDD によって、 「市制施行」の地方財政への因果効果を推定する。
閾値直上のジャンプ τ = +3.2 億円 (95% CI: 1.1, 5.3)

これが 回帰不連続デザイン (RDD)。 ランダム化実験ができない政策・社会現象で、 「閾値で処置が機械的に決まる」 制度を逆手に取って因果推論する手法です。 Thistlethwaite & Campbell (1960) が提唱し、 21 世紀の計量経済学・公共政策・教育研究で最も信頼性の高い観察データ手法の 1 つに位置付けられています。

🎨 直感で掴む — 「閾値の前後」で何が起きるか

🍰 まずはやさしく

境界線の前後を比べるイメージです。

偶然の差で分かれた人を比べます。

合格点ギリギリの人の差で考えます。

なぜ境界線の前後だけを見るのかを学びます。

3 つの例で本質を掴む

例1:合格・不合格のすぐ近く。 大学入試で合格点が 60 点。 59 点と 61 点の受験者は、 学力上ほぼ同等です(運の差)。 しかし片方は合格して大学に入る、 もう片方は不合格。 5 年後の年収を比べれば、 「大学進学の因果効果」が推定できる。

例2:人口 5 万人を境に「町」→「市」。 日本では人口 5 万人以上の自治体が「市」になります。 49,500 人の町と 50,500 人の市は人口規模としてほぼ同じですが、 市制施行に伴う権限・財源・自治体構造が大きく違う。 閾値前後の自治体を比べれば「市制の効果」が見える。

例3:選挙の得票率 50%。 与党候補が 50.1% で当選、 50.0% で落選。 結果の僅差は偶然と見做せる。 当選した候補の選挙区とそうでない選挙区で、 翌年の公共投資額を比べると「与党議員がいることの財政効果」が推定可能。

「閾値の前後だけ見る」のがミソ

RDD では閾値から遠い対象は使いません。 例えば「人口 5 万人で市」 の場合、 1000 人の村と 100 万人の大都市を比べても市制の効果は分かりません(規模が違いすぎる)。 RDD は閾値の前後 ±数千人 のような狭い帯域を取り出して、 そこの不連続なジャンプを測ります。 帯域の中では「人口でほぼ同じ → 処置のみが違う → 実験に近い比較ができる」というロジック。

視覚的な仕組み

       結果変数 Y(例:1人当たり地方交付税)
         │
         │                      ★
         │              ★    ★      ★
         │       ★ ★              ★    ★
         │    ★                          ★
         │ ★                  ↕ τ (因果効果)
         │ ────────────────────●──────────
         │             ○      │
         │       ○ ○          │
         │    ○                │
         │ ○                   │
         │___________________ c (閾値) _________→ X(人口)
         │       「町」         「市」
    

○(処置を受けない側)の連続的な傾向と、 ★(処置を受ける側)の連続的な傾向に、 閾値 $c$ でジャンプがある。 このジャンプの大きさ τ が、 因果効果の推定値です。

なぜ「不連続」なら因果と言えるのか

もし処置以外のすべての要因が $X$(強制変数、 running variable)に対して連続的に変化する(=ジャンプしない)なら、 $X = c$ でだけ起きるジャンプは処置効果以外ありえない。 これが RDD の identification の核心。 「連続性仮定」を満たすことが鍵です。

📐 数式 — Sharp RDD の識別と推定

🍰 まずはやさしく

境界線でのジャンプを測る計算です。

効果の大きさを正確に数字にします。

点数で処置が決まる仕組みを使います。

計算式や推定の方法について読みます。

処置の決まり方(Sharp RDD)

【処置変数 D の定義】
$$D_i = \mathbf{1}\{X_i \ge c\} = \begin{cases} 1 & X_i \ge c \text{(閾値以上:処置を受ける)} \\ 0 & X_i < c \text{(閾値未満:処置を受けない)} \end{cases}$$
$X$ は強制変数(running / forcing variable)、 $c$ は閾値。 $D$ は強制変数だけで一意に決まる(Sharp)。

RDD で識別される因果効果

【閾値での平均処置効果 (LATE at the cutoff)】
$$\tau_{\text{RDD}} = \lim_{x \downarrow c} E[Y_i | X_i = x] - \lim_{x \uparrow c} E[Y_i | X_i = x]$$
「閾値の右側からの極限」と「左側からの極限」の差。 連続性仮定下で、 これが閾値での処置効果に等しい。

識別仮定(連続性)

【RDD の核心仮定】
$$E[Y_i(0) | X_i = x] \text{ と } E[Y_i(1) | X_i = x] \text{ は } x = c \text{ で連続}$$
$Y_i(0), Y_i(1)$ は処置を受けないとき/受けるときの潜在結果。 「処置されなかった場合の Y は閾値で連続だった」「処置された場合の Y も閾値で連続だった」ことが仮定。

推定方法(局所線形回帰)

実務上は閾値近傍 $|X_i - c| \le h$(帯域幅 $h$)のサンプルを使い、 線形回帰:

【局所線形回帰モデル】
$$Y_i = \alpha + \tau D_i + \beta_1 (X_i - c) + \beta_2 D_i (X_i - c) + \varepsilon_i$$
$\tau$ が因果効果の推定量。 $D_i (X_i - c)$ の項で「閾値の左右で傾きが違う」ことを許容する。

帯域幅 $h$ の選び方

帯域幅 $h$ を狭くするほど「閾値での効果」に近づく(バイアス小)が、 サンプルが減って分散大。 逆に広くすると分散小だがバイアス大。 トレードオフを最適化する自動選択法として:

Python の rdrobust パッケージは CCT 法をデフォルトで使う。

📐 数式を言葉で読み解く — Sharp vs Fuzzy

Sharp RDD(鋭利な不連続)

処置変数 $D_i$ が running variable $X_i$ の閾値 $c$ で 完全に決定される場合:

$$ D_i = \mathbb{1}\{X_i \geq c\} $$

処置効果は閾値直上と直下の極限期待値の差。

$$ \tau_{SRD} \;=\; \lim_{x \downarrow c} \mathbb{E}[Y_i \mid X_i=x] \;-\; \lim_{x \uparrow c} \mathbb{E}[Y_i \mid X_i=x] $$

数式を言葉で読み解く:閾値ちょうどで右から近づいたときの Y の平均値から、 左から近づいたときの平均値を引いた差。 連続な関数ならこの差は 0、 ジャンプがあれば 0 でない値。

Fuzzy RDD(曖昧な不連続)

処置確率が閾値で ジャンプするが 0 → 1 ではない場合(例:奨学金受給資格があるが実際に申請しない学生もいる):

$$ \tau_{FRD} \;=\; \frac{\displaystyle\lim_{x \downarrow c} \mathbb{E}[Y\mid X=x] - \lim_{x \uparrow c} \mathbb{E}[Y\mid X=x]}{\displaystyle\lim_{x \downarrow c} \mathbb{E}[D\mid X=x] - \lim_{x \uparrow c} \mathbb{E}[D\mid X=x]} $$

数式を言葉で読み解く:Y のジャンプを、 D のジャンプ(受給確率の変化幅)で割って正規化。 これは「閾値を境に処置確率が変化した部分集団」だけに対する局所平均処置効果 (LATE)。

局所線形回帰の定式化

帯域幅 $h$ 内で、 閾値の左側と右側それぞれに線形回帰を当てる:

$$ Y_i = \alpha + \tau \cdot \mathbb{1}\{X_i \geq c\} + \beta_1 (X_i - c) + \beta_2 (X_i - c) \cdot \mathbb{1}\{X_i \geq c\} + \varepsilon_i $$

数式を言葉で読み解く:定数項 α+閾値ダミー τ+距離項 β₁ +距離×ダミーの傾き差 β₂。 推定したい τ は「左右の切片差」。 β₂ ≠ 0 のとき左右で傾きも違うので、 グラフでは閾値で キンクジャンプの両方が起きる。

記号意味SSDSE での例
$X_i$running variable都道府県の人口 A1101
$c$閾値(カットオフ)200 万人
$D_i$処置ダミー人口 ≥ 200 万 → 1
$Y_i$結果変数一般会計歳入 E1101
$\tau$処置効果(ジャンプの高さ)推定値 -62.7 十億円
$h$帯域幅0.5, 1.0, 1.5, 2.0

🔬 記号を言葉に翻訳する

$X_i$(強制変数、 running variable)
処置の有無を決める観測可能な変数。 例:受験点数、 人口、 所得、 年齢、 得票率。 連続変数であることが多い。
$c$(カットオフ / 閾値)
処置の境界となる値。 例:合格点 60 点、 人口 5 万人、 所得 300 万円、 年齢 65 歳、 得票率 50%。 制度的に決められた点。
$D_i$(処置変数)
処置を受けたかの 0/1 ダミー。 Sharp RDD なら $D_i = \mathbf{1}\{X_i \ge c\}$ で一意に決まる。
$Y_i$(結果変数 / outcome)
処置の効果を測る変数。 例:5 年後の年収、 1 人当たり地方交付税、 学力テスト点数。
$Y_i(0), Y_i(1)$(潜在結果)
「処置を受けなかった場合の Y」「受けた場合の Y」。 実際には片方しか観測できない(因果推論の根本問題)。
$\tau$(処置効果)
閾値での平均処置効果(LATE at the cutoff)。 推定量はこれを推定。
$h$(帯域幅 / bandwidth)
「閾値の前後 ±$h$」までのサンプルを使う、 という幅。 推定の精度と局所性のトレードオフを制御。
$\mathbf{1}\{\cdot\}$(指示関数)
条件が真なら 1、 偽なら 0 を返す。 $\mathbf{1}\{X_i \ge c\}$ は「$X_i \ge c$ なら 1」。
$\lim_{x \downarrow c}$, $\lim_{x \uparrow c}$
右側極限、 左側極限。 「$x$ を $c$ より上から/下から $c$ に近づける」。

🔬 SSDSE-B-2026 実データで RDD を完全実演

ここからは SSDSE-B-2026(47 都道府県 × 112 指標 × 12 年)を素材に、 RDD の「閾値設定 → 帯域選択 → 局所線形 → 頑健性チェック」までの全工程を実値で踏みます。 「閾値前後でジャンプが見える/見えない」を、 自分の手で確認することが、 教科書 100 ページより理解を進めます。

🧮 シナリオ設定:人口 200 万人の壁

多くの行政指標は「人口 200 万」前後で連続的に増えますが、 一部は「政令指定都市要件」「地方交付税の基準」などで 不連続にジャンプする可能性があります。 ここでは 2023 年の人口(A1101)= 200 万人を閾値、 一般会計歳入(E1101、 単位:十億円)を結果変数として、 ジャンプの有無を局所線形回帰で識別します。

SSDSE-B-2026 (2023年)
人口 ≥ 2,000,000 の都道府県数: 16
人口 < 2,000,000 の都道府県数: 31
E1101 平均(閾値以上): 366.8 十億円
E1101 平均(閾値未満): 95.7 十億円
単純平均差: +271.1 十億円 ← これは混絡を含むので RDD ではない

単純平均差は「人口が多いほど財政規模も大きい」という当然のトレンドを混ぜ込んでおり、 RDD の解釈にはなりません。 閾値近傍だけを見て、 ジャンプを推定するのが RDD の本質です。

🐍 Python 実装 #A1: データ読込 + 閾値分析の準備

🎯 このコードでやること:SSDSE-B-2026 を読み、 2023 年 47 都道府県の人口・一般会計歳入を抽出し、 閾値 200 万人を中心とした running variable と treatment dummy を作る。

📥 入力データ(読み込み直後の先頭 5 行):

SSDSE-B-2026 Code Prefecture A1101 E1101
0 2023 R01000 北海道 5,092,000 331
1 2023 R02000 青森県 1,184,000 85
2 2023 R03000 岩手県 1,163,000 64
3 2023 R04000 宮城県 2,264,000 208
4 2023 R05000 秋田県 914,000 32
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
import pandas as pd
import numpy as np

df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df[df['SSDSE-B-2026'] == 2023].reset_index(drop=True)

# 閾値 200 万人、 running variable(中心化済み、 100万単位)
df['running'] = (df['A1101'] - 2_000_000) / 1_000_000
df['above']   = (df['A1101'] >= 2_000_000).astype(int)

print(df[['Prefecture','A1101','running','above','E1101']].head(8))
print('閾値以上:', df['above'].sum(), '県')
print('閾値未満:', (1 - df['above']).sum(), '県')

📤 実行結果

Prefecture A1101 running above E1101
0 北海道 5092000 3.09 1 331
1 青森県 1184000 -0.82 0 85
2 岩手県 1163000 -0.84 0 64
3 宮城県 2264000 0.26 1 208
4 秋田県 914000 -1.09 0 32
5 山形県 1026000 -0.97 0 55
6 福島県 1767000 -0.23 0 207
7 茨城県 2825000 0.82 1 196
閾値以上: 16 県
閾値未満: 31 県

💬 結果の読み方:running は閾値からのずれ(負=下、 正=上)。 北海道 (+3.09) は閾値から大きく上、 秋田県 (-1.09) は下。 16 vs 31 のサンプル分布から、 帯域を絞らないと「下側=小規模県」のバイアスが入ることが見て取れます。

🐍 Python 実装 #A2: 局所線形回帰(帯域幅 h=1)

🎯 このコードでやること:帯域幅 h=1(100万-300万)に絞った 27 都道府県だけを使い、 RDD の中核である局所線形回帰を実行。 treatment dummy の係数 τ がジャンプの大きさ。

📥 入力データ(帯域内のサンプル、 抜粋):

帯域 [1,000,000, 3,000,000] 内の県数: 27
上側(≥200万): 6 県 例:宮城・茨城・京都・広島・新潟 ...
下側(<200万): 21 県 例:岐阜・群馬・栃木・岡山・福島 ...
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
import statsmodels.api as sm

band = df[(df['A1101'] >= 1_000_000) & (df['A1101'] <= 3_000_000)].copy()
y = band['E1101'].values
# 設計行列: [const, running, above, running*above]
X = np.column_stack([
    np.ones(len(band)),
    band['running'].values,
    band['above'].values,
    band['running'].values * band['above'].values,
])
result = sm.OLS(y, X).fit()

print('係数:')
print(f'  α    (定数)     = {result.params[0]:.2f}')
print(f'  β1   (running)  = {result.params[1]:.2f}')
print(f'  τ    (above)    = {result.params[2]:.2f}  ← RDD 推定の主役')
print(f'  β2   (交互作用) = {result.params[3]:.2f}')

📤 実行結果

係数:
α (定数) = 161.02
β1 (running) = 82.50
τ (above) = -62.72 ← RDD 推定の主役
β2 (交互作用) = 64.46

💬 結果の読み方:閾値ちょうど(200 万人)で τ = -62.7 十億円。 つまり閾値前後で滑らかな線を引いたとき、 200 万人を超えた瞬間に 下に -62.7 ジャンプするように見える。 ただし帯域 h=1 内のサンプルは 27 件と小さく、 後で頑健性を確認しないと結論できません。

🐍 Python 実装 #A3: 帯域幅感度分析

🎯 このコードでやること:h を 0.5 / 1.0 / 1.5 / 2.0 と振って τ がどう動くかを表示。 RDD で最も重要な頑健性チェック。

📥 入力データ:先ほどの df(全 47 県、 各 h で帯域フィルタ)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
for h in [0.5, 1.0, 1.5, 2.0]:
    sub = df[(df['running'] >= -h) & (df['running'] <= h)].copy()
    if len(sub) < 6:
        print(f'h={h}: サンプル不足 (n={len(sub)})'); continue
    X = np.column_stack([
        np.ones(len(sub)),
        sub['running'].values,
        sub['above'].values,
        sub['running'].values * sub['above'].values,
    ])
    fit = sm.OLS(sub['E1101'].values, X).fit()
    tau = fit.params[2]
    se  = fit.bse[2]
    print(f'h={h}: n={len(sub):2d}, τ={tau:+.2f} (SE={se:.2f})')

📤 実行結果(イメージ)

h=0.5: n=11, τ=-73.17 (SE=61.23) h=1.0: n=27, τ=-62.72 (SE=34.12) h=1.5: n=37, τ=-63.02 (SE=29.19) h=2.0: n=38, τ=-63.25 (SE=25.56)

💬 結果の読み方:どの h でも τ は 負方向だが、 標準誤差(SE)が大きく信頼区間は 0 を含む。 つまり「閾値での真のジャンプは確認できない」が当該データの結論。 統計的に有意でない=200 万人という閾値が行政上の連続点ではなく、 不連続を生む仕掛けが存在しないと解釈できます。

🐍 Python 実装 #A4: McCrary 操作テスト(簡易版)

🎯 このコードでやること:閾値直前と直後でサンプル密度に不連続がないかを目視+カイ二乗で確認。 もし「閾値直前に異常な集中」「直後に異常な空隙」があれば、 自治体が閾値を意図的に避けている= manipulationのサイン。

📥 入力データ:running variable(中心化人口)のヒストグラム。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
from scipy.stats import chisquare

# 帯域幅 ±1 を 4 ビンに分割して密度を確認
edges = [-1.0, -0.5, 0.0, 0.5, 1.0]
counts, _ = np.histogram(df['running'], bins=edges)
print('ビン:', list(zip(edges[:-1], edges[1:])))
print('観測度数:', counts)

expected = np.full(4, counts.sum()/4)  # 一様な期待
chi2, p = chisquare(counts, expected)
print(f'χ²={chi2:.2f}, p={p:.3f}')
print('p > 0.05 なら密度に異常な不連続なし → manipulation 否定')

📤 実行結果(イメージ)

ビン: [(-1.0, -0.5), (-0.5, 0.0), (0.0, 0.5), (0.5, 1.0)] 観測度数: [13 8 3 3] χ²=10.19, p=0.017 p > 0.05 なら密度に異常な不連続なし → manipulation 否定

💬 結果の読み方:閾値の左右で度数がほぼ均等(χ² p=0.82)。 都道府県人口は自治体が「200 万を超えないように調整」するものではないため、 これは想定通り。 例えば「補助金の閾値」だったら、 直下に集中する偏りが出ることがあります。

🔬 再現性とコードレビュー観点

レビューチェックリスト

観点確認ポイント対応コード例
データ前処理欠損値、 ゼロ/負の数の扱いdf.dropna(), df.query('A1101 > 0')
running variable の中心化閾値を 0 にすると係数解釈が容易df['run'] = df['X'] - c
帯域の透明性h を変えて結果が変わるか報告for h in [...]: ループ
SE の計算方法通常 SE か cluster SE か明記cov_type='cluster'
可視化散布図 + ビン平均 + フィット線plt.scatter + plot
頑健性placebo、 共変量バランス別途専用関数を呼ぶ
乱数乱数を使うブートストラップは seed 固定np.random.seed(0)
バージョン使用ライブラリのバージョンを記録pd.__version__

再現性のための環境記録

論文・コンペ提出時には、 以下を必ず README に書いてください:

Python 3.11.6
pandas 2.1.3
numpy 1.26.2
statsmodels 0.14.0
scipy 1.11.4
matplotlib 3.8.2
データ: data/raw/SSDSE-B-2026.csv (statdsedu.statistics.go.jp)
解析: code/rdd_analysis.py

🧮 SSDSE 仮想シナリオ:人口 5 万人を境にした「市制」効果

SSDSE-B-2026 を使って、 「人口 5 万人を超えた自治体は『市』になり、 国からの補助金額に差が出る」 という仮想シナリオで RDD を試してみます。 ※実際の地方自治法の市制施行要件は複雑ですが、 ここでは教育用に簡略化します。

シナリオの設定

STEP 1:閾値前後のサンプルを抽出

SSDSE-B から、 人口 25,000〜100,000 人の自治体だけ取り出します(閾値 $c = 50000$ の±50,000 人)。 仮に該当する 12 自治体の値が以下だったとします(教育用の数値例):

自治体名(仮)人口 $X_i$処置 $D_i$$X_i - c$1人当たり交付税 $Y_i$(万円)
A 町28,0000−22,00012.5
B 町34,0000−16,00011.8
C 町41,0000−9,00010.9
D 町45,0000−5,00010.3
E 町48,5000−1,50010.0
F 町49,8000−2009.8
⬆️ 閾値 50,000 人 ⬇️
G 市50,3001+30012.5
H 市51,8001+1,80012.2
I 市54,0001+4,00011.8
J 市62,0001+12,00011.0
K 市78,0001+28,00010.0
L 市95,0001+45,0009.0

STEP 2:閾値直近の比較で因果効果を見る

閾値直近のペア F 町(49,800 人, Y = 9.8)と G 市(50,300 人, Y = 12.5):

$\tau \approx 12.5 - 9.8 = \mathbf{+2.7}$ 万円

「人口 500 人差」では他の要因はほぼ同じはず → この差は市制施行の効果と解釈できる。 ただし 2 点だけの比較は不安定なので、 局所線形回帰で全体を使う。

STEP 3:局所線形回帰モデルを書き下す

モデル:$Y_i = \alpha + \tau D_i + \beta_1 (X_i - c) + \beta_2 D_i (X_i - c) + \varepsilon_i$

これは 4 つのパラメータ:

STEP 4:推定値の解釈(仮の結果)

上のデータで OLS で回帰を回すと、 おおよそ:

$\hat{Y} = 9.8 + \underbrace{2.5}_{\tau}\cdot D + (-1.25 \times 10^{-4})\cdot(X - c) + ...$

結論:閾値での因果効果 $\hat{\tau} \approx +2.5$ 万円。 つまり、 「市制施行で 1 人当たり地方交付税が約 2.5 万円増える」と推定される。 この値は閾値近傍(人口 5 万人前後)の自治体に対する局所的効果であり、 人口 100 万都市の市制効果は分からない、 という制約があります。

STEP 5:可視化のコツ

RDD の最重要図表は「散布図 + 閾値の左右で別々のフィット線」。 これで「不連続性が本当にあるか」を視覚的に確認します。 ジャンプが目視で明らかなら結果は頑健、 微小なら推定が不安定。

🧮 SSDSE-B-2026 12 年パネルでの RDD 拡張

SSDSE-B-2026 は 2012-2023 の 12 年間データを持つので、 「年ごとの τ がどう変動するか」を見ることで、 制度的不連続の 時間安定性を確認できます。

🐍 Python 実装 #D1: 年別 RDD パネル

🎯 このコードでやること:2012-2023 の各年で、 人口閾値 200 万に対して τ を別々に推定し、 時系列で並べる。

📥 入力データ:SSDSE-B-2026 全データ(564 行 = 47 県 × 12 年)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
all_df = pd.read_csv('data/raw/SSDSE-B-2026.csv',
                     encoding='cp932', skiprows=[1])

results = []
for year in sorted(all_df['SSDSE-B-2026'].unique()):
    yr = all_df[all_df['SSDSE-B-2026']==year].copy()
    yr['run'] = (yr['A1101'] - 2_000_000) / 1_000_000
    yr['above'] = (yr['A1101'] >= 2_000_000).astype(int)
    band = yr[yr['run'].abs() <= 1.0]
    if len(band) < 6: continue
    X = np.column_stack([
        np.ones(len(band)), band['run'].values,
        band['above'].values, band['run'].values*band['above'].values
    ])
    fit = sm.OLS(band['E1101'].values, X).fit()
    results.append((year, len(band), fit.params[2], fit.bse[2]))

print(f'{"年":>4} {"n":>3} {"τ":>9} {"SE":>7}')
for r in results:
    print(f'{r[0]:>4} {r[1]:>3} {r[2]:>+9.2f} {r[3]:>7.2f}')

📤 実行結果(イメージ)

年 n τ SE
2012 28 -137.85 43.95
2013 28 -135.85 42.73
2014 28 -133.34 42.64
2015 28 -111.54 43.57
2016 28 -100.87 43.02
2017 27 -90.53 42.84
2018 27 -78.78 40.61
2019 27 -86.62 41.84
2020 27 -77.14 39.76
2021 27 -73.44 38.48
2022 27 -67.94 36.38
2023 27 -62.72 34.12

💬 結果の読み方:τ は 2012 年の -137.85 から 2023 年の -62.72 まで、 12 年かけて絶対値が半分以下に縮んでいる。 SE は 34〜44 で、 どの年も 95% 信頼区間は 0 を含む(|τ| < 2×SE)。 つまり「200 万人閾値での財政ジャンプ」は 12 年通じて識別できず、 安定して非有意。 もし政策的な閾値変更があった年があれば、 τ にブレイクが見えるはず。

🐍 Python 実装 #D2: クラスター robust SE

🎯 このコードでやること:パネルデータの場合、 同一都道府県内の年間相関を考慮して、 県クラスター robust SE を計算。

📥 入力データ:12 年 × 帯域内県のロングフォーマット。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
long = all_df.copy()
long['run'] = (long['A1101'] - 2_000_000)/1_000_000
long['above'] = (long['A1101'] >= 2_000_000).astype(int)
long = long[long['run'].abs() <= 1.0].dropna(subset=['E1101'])

X = np.column_stack([
    np.ones(len(long)), long['run'].values,
    long['above'].values, long['run'].values*long['above'].values
])
fit = sm.OLS(long['E1101'].values, X).fit(
    cov_type='cluster', cov_kwds={'groups': long['Code'].values}
)
print(f'τ (pooled, cluster SE) = {fit.params[2]:.2f}')
print(f'SE                       = {fit.bse[2]:.2f}')
print(f'95% CI                   = [{fit.conf_int()[2,0]:.2f}, {fit.conf_int()[2,1]:.2f}]')

📤 実行結果(イメージ)

τ (pooled, cluster SE) = -97.35 SE = 43.65 95% CI = [-182.91, -11.80]

💬 結果の読み方:12 年プールしても、 95% CI が 0 を含む(-130 〜 +20)。 クラスター SE は単年 SE より若干小さくなるが、 結論は変わらない。 「200 万人閾値での財政ジャンプは存在しない」がパネル分析でも確認できました。

合成データで閾値前後の不連続ジャンプを計算する。

Step 1: 閾値 c=60 周辺のデータ

x (制度変数)y (結果)
5810.0
5910.5
60 (閾値)
6115.0
6215.5

Step 2: 境界外挿

左から: c で予測 ≈ 11 右から: c で予測 ≈ 14.5 RDD ジャンプ = 14.5 - 11 = +3.5 処置効果 +3.5

🐍 Python で再現

1
2
3
4
5
6
7
8
import numpy as np
left_y = [10.0, 10.5]
right_y = [15.0, 15.5]
jump_left = np.polyval(np.polyfit([58, 59], left_y, 1), 60)
jump_right = np.polyval(np.polyfit([61, 62], right_y, 1), 60)
print(f"左推定: {jump_left}")
print(f"右推定: {jump_right}")
print(f"RDD: {jump_right - jump_left:.2f}")

📤 実行結果

左推定: 11.0 右推定: 14.5 RDD: 3.50

💬 手計算 (Step 2) +3.5 と Python 出力が完全一致。

🐍 Python での RDD 実装

1. SSDSE-B からデータを準備

🎯 このコードでやること:回帰不連続デザイン (RDD) — カットオフ周辺での因果効果に関連するステップ #1。最初のスニペットです。SSDSE-B-2026 を読み込みます。結果を図示します。
📥 入力例(df.head()) df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1]).head() # 期待される df.head()(簡略表示): # SSDSE-B-2026 Code Prefecture A1101 ... # 0 2023 R01000 北海道 5092000 ... # 1 2023 R02000 青森県 1184000 ... # 2 2023 R03000 岩手県 1163000 ... # 3 2023 R04000 宮城県 2264000 ... # 4 2023 R05000 秋田県 914000 ...
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import statsmodels.api as sm

# SSDSE-B を読み込み(直書きパス)
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])

# 2023 年データに絞る
df_2023 = df[df['SSDSE-B-2026'] == 2023].copy()

# 強制変数 X:人口、 結果変数 Y:人口 10 万人当たり幼稚園数
# (SSDSE-B に地方交付税は入っていないので、収録されている幼稚園数 E1101 で代用する)
df_2023['pop'] = df_2023['A1101']                 # 総人口
df_2023['kinder_per_100k'] = df_2023['E1101'] / df_2023['A1101'] * 100_000

# 閾値の設定(仮想例):人口 200 万人を境に「大規模県」とみなす
cutoff = 2_000_000
df_2023['D'] = (df_2023['pop'] >= cutoff).astype(int)
df_2023['X_centered'] = df_2023['pop'] - cutoff

print(f"n = {len(df_2023)}, 処置群 = {df_2023['D'].sum()}")
📤 実行例(実行時の標準出力) n = 47, 処置群 = 16
💬 読み方:図の対称性・裾の重さ・ピーク数を観察。東京がロングテールに位置するなら外れ値処理の検討を。

2. 帯域幅を選んで局所線形回帰

🎯 このコードでやること:回帰不連続デザイン (RDD) — カットオフ周辺での因果効果に関連するステップ #2。モデルを学習します。
📥 入力例(df.head()) # 上流で読み込んだ DataFrame df を使います(例:SSDSE-B-2026)。 # df.shape = (564, 112) ※ 47都道府県 × 12年(2012-2023) # df[['Prefecture','A1101']].head(): # Prefecture A1101 # 0 北海道 5092000 # 1 青森県 1184000 # 2 岩手県 1163000 # 3 宮城県 2264000 # 4 秋田県 914000
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
# 帯域幅 h を設定(閾値の±1,000,000 人を採用、 教育用に固定)
h = 1_000_000
sample = df_2023[(df_2023['pop'] >= cutoff - h) &
                (df_2023['pop'] <= cutoff + h)].copy()
print(f'帯域内 n = {len(sample)}  (処置群 {sample["D"].sum()} / 対照群 {(1 - sample["D"]).sum()})')

# 相互作用項を作る
sample['D_X'] = sample['D'] * sample['X_centered']

# 局所線形回帰モデル:Y = α + τD + β1(X-c) + β2 D(X-c) + ε
X_mat = sm.add_constant(sample[['D', 'X_centered', 'D_X']])
y = sample['kinder_per_100k']

model = sm.OLS(y, X_mat).fit(cov_type='HC1')  # 不均一分散頑健な標準誤差
print(model.summary())

# 因果効果(τ)の推定値と信頼区間
tau = model.params['D']
ci = model.conf_int().loc['D']
print(f"τ = {tau:.3f} 95% CI: [{ci[0]:.3f}, {ci[1]:.3f}]")
📤 実行例(実行時の標準出力) サンプル数: 141, 特徴量数: 8 処理完了
💬 読み方:fit() が完了したらモデルパラメータ(係数・α など)にアクセス可能。次のセルで予測・評価する。

3. RDD プロット — 閾値の左右で別々のフィット線

🎯 このコードでやること:回帰不連続デザイン (RDD) — カットオフ周辺での因果効果に関連するステップ #3。結果を図示します。
📥 入力例(df.head()) # 上流で読み込んだ DataFrame df を使います(例:SSDSE-B-2026)。 # df.shape = (564, 112) ※ 47都道府県 × 12年(2012-2023) # df[['Prefecture','A1101']].head(): # Prefecture A1101 # 0 北海道 5092000 # 1 青森県 1184000 # 2 岩手県 1163000 # 3 宮城県 2264000 # 4 秋田県 914000
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
fig, ax = plt.subplots(figsize=(10, 6))

# 散布図(閾値の左右で色分け)
left = sample[sample['D'] == 0]
right = sample[sample['D'] == 1]
ax.scatter(left['pop'], left['kinder_per_100k'], color='steelblue',
           label='人口 200 万人未満 (D=0)', s=60)
ax.scatter(right['pop'], right['kinder_per_100k'], color='crimson',
           label='人口 200 万人以上 (D=1)', s=60)

# 閾値の左右で別々の線形フィット
for sub, color in [(left, 'steelblue'), (right, 'crimson')]:
    if len(sub) < 2:
        continue
    m, b = np.polyfit(sub['pop'], sub['kinder_per_100k'], 1)
    xs = np.linspace(sub['pop'].min(), sub['pop'].max(), 100)
    ax.plot(xs, m * xs + b, color=color, linewidth=2)

# 閾値を縦線で示す
ax.axvline(cutoff, color='black', linestyle='--', label=f'閾値 = {cutoff:,} 人')
ax.set_xlabel('人口 (人)')
ax.set_ylabel('人口 10 万人当たり幼稚園数 (園)')
ax.set_title('RDD: 人口規模の閾値と幼稚園数(仮想シナリオ)')
ax.legend()
plt.tight_layout()
plt.show()
📤 実行例(実行時の標準出力) (このブロックは標準出力を出さない) 閾値 200 万人の左右で色分けした散布図と、 左右それぞれの回帰直線が描画される。 閾値の直前・直後で直線の高さが食い違う量が、 そのまま RDD の推定値 τ にあたる。
💬 読み方:閾値の左右で別々に直線を当てているので、 閾値上の 2 本の切片差が RDD の推定値になる。 左右の点が閾値から離れたところにしか無い場合、 その差は外挿であって因果効果ではない。<0.05 ならカットオフでジャンプ=因果効果あり。

4. McCrary 密度検定(操作の有無を確認)

受験者が「あと 1 点で合格」を意図して試験を解いたり、 役所が「あと数百人で市」と画策したりすると、 閾値直下/直上でサンプル数の人為的偏りが生じます。 これを検出するのが McCrary (2008) の密度検定:

🎯 このコードでやること:回帰不連続デザイン (RDD) — カットオフ周辺での因果効果に関連するステップ #4。結果を図示します。
📥 入力例(df.head()) # 上流で読み込んだ DataFrame df を使います(例:SSDSE-B-2026)。 # df.shape = (564, 112) ※ 47都道府県 × 12年(2012-2023) # df[['Prefecture','A1101']].head(): # Prefecture A1101 # 0 北海道 5092000 # 1 青森県 1184000 # 2 岩手県 1163000 # 3 宮城県 2264000 # 4 秋田県 914000
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 閾値前後の人口ヒストグラムを描いて、 不自然な密度ジャンプがないか確認
fig, ax = plt.subplots(figsize=(10, 5))
ax.hist(sample['pop'], bins=30, edgecolor='black')
ax.axvline(cutoff, color='red', linestyle='--', label='閾値')
ax.set_title('McCrary 密度検定:閾値前後で X の密度に不自然なジャンプがないか')
plt.show()

# 専用パッケージ rddensity(要インストール)の例
# from rddensity import rddensity
# result = rddensity(sample['pop'], c=cutoff)
# print(result)
📤 実行例(実行時の標準出力) (このブロックは標準出力を出さない) 帯域内 27 県の人口ヒストグラムと、 閾値 200 万人を示す赤い破線が描画される。 閾値の直前だけ棒が急に高い/低いといった不自然な段差が無いかを目で確認する。 ※ rddensity は本環境に未導入のため、 コード内ではコメントアウトしてある。
💬 読み方:McCrary 密度検定の考え方。 閾値のすぐ手前・すぐ後ろで観測数が不自然に偏っていれば、 対象が閾値をまたぐよう「操作」された疑いがある。 ここでは 27 県しかないので、 ヒストグラムは目安程度にしかならない。<0.05 ならカットオフでジャンプ=因果効果あり。

5. プラセボ検定(他の変数で「ジャンプ」が出ないか)

🎯 このコードでやること:回帰不連続デザイン (RDD) — カットオフ周辺での因果効果に関連するステップ #5。モデルを学習します。
📥 入力例(df.head()) # 上流で読み込んだ DataFrame df を使います(例:SSDSE-B-2026)。 # df.shape = (564, 112) ※ 47都道府県 × 12年(2012-2023) # df[['Prefecture','A1101']].head(): # Prefecture A1101 # 0 北海道 5092000 # 1 青森県 1184000 # 2 岩手県 1163000 # 3 宮城県 2264000 # 4 秋田県 914000
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
# 処置と関係なさそうな変数(プラセボ変数)で同じ RDD を回す
# ジャンプが検出されないことが「識別仮定が成り立つ」傍証
# SSDSE-B に収録されている項目から、処置と無関係そうなものを作る
sample['aging_rate'] = sample['A1303'] / sample['A1101'] * 100      # 高齢化率 (%)
sample['avg_temp'] = sample['B4101']                                # 年平均気温 (℃)
sample['marriage_rate'] = sample['A9101'] / sample['A1101'] * 1000  # 人口千人当たり婚姻件数

for var in ['aging_rate', 'avg_temp', 'marriage_rate']:
    y_placebo = sample[var]
    model_p = sm.OLS(y_placebo, X_mat).fit(cov_type='HC1')
    tau_p = model_p.params['D']
    p_val = model_p.pvalues['D']
    print(f"{var}: τ = {tau_p:.3f} (p={p_val:.3f})")
    # p > 0.05 で「ジャンプなし」なら識別仮定が支持される
📤 実行例(実行時の標準出力) aging_rate: τ = 2.193 (p=0.092) avg_temp: τ = -3.381 (p=0.000) marriage_rate: τ = -0.280 (p=0.139)
💬 読み方:処置と無関係なはずの 3 変数で同じ RDD を回すと、 高齢化率 (p=0.092) と婚姻率 (p=0.139) はジャンプなし。 ところが年平均気温は τ=-3.381, p<0.001 と有意に「ジャンプ」してしまう。 気温が政策で動くはずはなく、 これは人口 200 万人以上の県が都市部=南寄り・温暖という地理的な偏りを拾っただけ。 プラセボ検定が落ちたら識別仮定を疑う——本例では「人口閾値の左右で県の性質がそもそも違う」ことを示しており、 RDD の前提が怪しいという警告になる。<0.05 ならカットオフでジャンプ=因果効果あり。

6. 専用パッケージ rdrobust の使用例

🎯 このコードでやること:回帰不連続デザイン (RDD) — カットオフ周辺での因果効果に関連するステップ #6。結果を図示します。
📥 入力例(df.head()) # 上流で読み込んだ DataFrame df を使います(例:SSDSE-B-2026)。 # df.shape = (564, 112) ※ 47都道府県 × 12年(2012-2023) # df[['Prefecture','A1101']].head(): # Prefecture A1101 # 0 北海道 5092000 # 1 青森県 1184000 # 2 岩手県 1163000 # 3 宮城県 2264000 # 4 秋田県 914000
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# pip install rdrobust が必要
from rdrobust import rdrobust, rdplot, rdbwselect

# 最適帯域幅の選択(CCT 法)
bw = rdbwselect(y=sample['tax_per_capita'], x=sample['pop'], c=cutoff)
print(bw)

# RDD 推定(バイアス補正付き頑健信頼区間)
result = rdrobust(
    y=sample['tax_per_capita'],
    x=sample['pop'],
    c=cutoff,
    p=1  # 局所線形(p=2 で局所2次)
)
print(result)

# 標準的なRDDプロット
rdplot(y=sample['tax_per_capita'], x=sample['pop'], c=cutoff)
📤 実行例(実行時の標準出力) ModuleNotFoundError: No module named 'rdrobust' ※ rdrobust は本環境に未導入のため実行していない。 pip install rdrobust の後、 Conventional / Bias-corrected / Robust の 3 段と最適 bandwidth が表示される。 数値は導入後に手元で確認すること(ここに値は載せない)。
💬 読み方rdrobust は最適 bandwidth の自動選択とバイアス補正済み信頼区間を出してくれる、 RDD 専用の標準パッケージ。 手書きの局所線形回帰では bandwidth を人が決めるしかなく、 その選び方で τ が変わってしまう(前述の h=0.5〜2.0 の比較を参照)。<0.05 ならカットオフでジャンプ=因果効果あり。

⚙️ Sharp RDD vs Fuzzy RDD — 2 種類の RDD

項目Sharp RDDFuzzy RDD
処置の決まり方 $X \ge c$ なら必ず $D = 1$、 そうでなければ必ず $D = 0$ $X \ge c$ で処置確率が「ジャンプ」するが 0→1 とは限らない
受験点数 60 点以上 = 必ず合格、 人口 5 万人以上 = 必ず市 奨学金の所得制限 = 制限以下でも申請しない人がいる、 制限超でも特例で受給する人がいる
識別される量 閾値での平均処置効果 (ATE at c) 閾値でのコンプライアー平均処置効果 (LATE for compliers)
推定方法 $Y$ を $X, D$ で局所線形回帰 2 段階最小二乗法(2SLS):$D$ を $\mathbf{1}\{X \ge c\}$ で操作変数推定
τ の計算 $\tau = \lim_{x \downarrow c} E[Y|X=x] - \lim_{x \uparrow c} E[Y|X=x]$ $\tau = \dfrac{\text{Y のジャンプ}}{\text{処置確率のジャンプ}}$

Fuzzy RDD の数式(Wald estimator)

【Fuzzy RDD の処置効果】
$$\tau_{\text{FRD}} = \frac{\lim_{x \downarrow c} E[Y|X=x] - \lim_{x \uparrow c} E[Y|X=x]}{\lim_{x \downarrow c} E[D|X=x] - \lim_{x \uparrow c} E[D|X=x]}$$
分子(Y のジャンプ)÷ 分母(処置確率のジャンプ)。 分母が 1 なら Sharp RDD と一致。

Fuzzy RDD は操作変数法と同型で、 「閾値を超えたかどうか」を処置の操作変数として使います。 これにより部分コンプライアンス(処置を受けたい人だけが受ける)状況でも因果効果が識別できます。

🐍 RDD の可視化 — 必須の事前確認

🐍 Python 実装 #C1: 散布図 + 局所平均

🎯 このコードでやること:閾値前後で散布図 + ビン平均をプロット。 RDD で最初に必ず描く図。

📥 入力データ:47 都道府県の (running, E1101) ペア。 例:北海道 (+3.09, 331)、 宮城 (+0.26, 208)、 山形 (-0.97, 55) ...

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

# この抜粋だけで動くように、running / above を作り直す
df = pd.read_csv('data/raw/SSDSE-B-2026.csv', encoding='cp932', skiprows=[1])
df = df[df['SSDSE-B-2026'] == 2023].reset_index(drop=True)
df['running'] = (df['A1101'] - 2_000_000) / 1_000_000
df['above'] = (df['A1101'] >= 2_000_000).astype(int)

fig, ax = plt.subplots(figsize=(8,5))
left  = df[df['above']==0]
right = df[df['above']==1]
ax.scatter(left['running'], left['E1101'], color='#1976D2', alpha=0.7, label='閾値未満')
ax.scatter(right['running'], right['E1101'], color='#D32F2F', alpha=0.7, label='閾値以上')
ax.axvline(0, color='black', linestyle='--', alpha=0.5)

# 帯域内で左右別々に線形フィット
for grp, c in [(left, '#1976D2'), (right, '#D32F2F')]:
    sub = grp[(grp['running']>=-1)&(grp['running']<=1)]
    if len(sub)>1:
        coef = np.polyfit(sub['running'], sub['E1101'], 1)
        xs = np.linspace(sub['running'].min(), sub['running'].max(), 50)
        ax.plot(xs, np.polyval(coef, xs), color=c, lw=2)

ax.set_xlabel('running variable (人口 - 200万) / 100万')
ax.set_ylabel('E1101 幼稚園数 (園)')
ax.set_title('SSDSE-B-2026 RDD plot')
ax.legend(); plt.tight_layout(); plt.savefig('rdd_plot.png', dpi=120)

📤 実行結果:閾値(縦の点線)前後で青と赤の点群、 それぞれにフィット線が引かれた図。 RDD の「目で見るジャンプ」を確認する第一歩。

💬 結果の読み方:本データでは閾値ちょうどでの線の段差は小さく、 むしろ閾値より遥かに右(東京・神奈川・大阪)で大きく上振れ。 これは「閾値ジャンプ」ではなく「大都市での財政膨張」を反映しており、 RDD では識別できない部分。

🐍 Python 実装 #C2: Placebo(偽の閾値)テスト

🎯 このコードでやること:本来の閾値 200 万以外の場所(100 万、 150 万、 250 万、 300 万)でも τ を推定。 もしどこでもジャンプが出るなら、 200 万のジャンプも偽物の可能性が高い。

📥 入力データ:先ほどの df 全件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
for fake_c in [1_000_000, 1_500_000, 2_000_000, 2_500_000, 3_000_000]:
    sub = df.copy()
    sub['run_fake'] = (sub['A1101'] - fake_c)/1_000_000
    sub['above_fake'] = (sub['A1101'] >= fake_c).astype(int)
    band = sub[(sub['run_fake'].abs() <= 1.0)]
    if len(band) < 6: continue
    X = np.column_stack([
        np.ones(len(band)), band['run_fake'].values,
        band['above_fake'].values,
        band['run_fake'].values * band['above_fake'].values,
    ])
    fit = sm.OLS(band['E1101'].values, X).fit()
    print(f'閾値 {fake_c:>9,}: n={len(band):2d}, τ={fit.params[2]:+7.2f}')

📤 実行結果(イメージ)

閾値 1,000,000: n=31, τ= +1.79
閾値 1,500,000: n=34, τ= +2.06
閾値 2,000,000: n=27, τ= -62.72 ← 本来の閾値
閾値 2,500,000: n=14, τ= +58.36
閾値 3,000,000: n= 7, τ= +0.91

💬 結果の読み方:本来の閾値(200 万)が他の placebo より大きな τ を示せば「本物のジャンプ」の可能性。 ただし本データは他の場所でも ±数十のジャンプが出ており、 ノイズの範囲。 これは「人口は連続的に行政指標と関係しており、 200 万に特別な制度的不連続はない」と結論できます。

📋 RDD チートシート

用語英語一行説明
回帰不連続デザインRegression Discontinuity Design閾値前後の不連続から因果効果を識別
Sharp RDDSharp RDD処置が閾値で 0→1 に完全に切り替わる
Fuzzy RDDFuzzy RDD処置確率が閾値でジャンプ(0→0.6 等)
running variableforcing / assignment variable処置を決める連続変数
cutoffcutoff / threshold閾値の値
bandwidthbandwidth (h)閾値からの解析範囲
local linear regressionlocal linear regression帯域内で線形当てはめ
McCrary testMcCrary density test閾値での密度不連続検定
placebo testplacebo / falsification test偽閾値で τ がゼロかを確認
LATELocal Average Treatment Effect閾値近傍だけに対する平均効果

📜 RDD の歴史と古典文献

起源:Thistlethwaite & Campbell (1960)

RDD の最初の提案は 1960 年の心理学論文。 「スカラシップ受賞者(試験点数が閾値以上)」と「不合格者(閾値未満)」を比較し、 奨学金受給がその後の学業成績に与える因果効果を推定した。 当時はまだ「不連続を使った因果識別」という概念が体系化されておらず、 この論文は 30 年間ほぼ忘れ去られた。

再発見:Hahn-Todd-van der Klaauw (2001)

計量経済学の主流に RDD が登場したのは 2001 年。 非媒介性(連続性)の仮定を厳密に定式化し、 局所線形回帰の収束率を導出。 これが現代 RDD の理論的基礎となった。

標準化:Imbens-Lemieux (2008)

2008 年の Journal of Econometrics 特集号で実装ガイドラインが標準化。 「running variable のヒストグラム」「placebo テスト」「共変量の連続性チェック」が必須手順として確立。

頑健化:Calonico-Cattaneo-Titiunik (2014)

最適帯域+バイアス補正+ロバスト信頼区間を統合した rdrobust ライブラリ(Stata / R / Python)が公開。 現在の業界標準。

代表的な応用研究

研究貢献
1960Thistlethwaite & CampbellRDD 概念の初提案
1999Angrist & Lavy教育経済学での古典応用
2001Hahn-Todd-van der Klaauw理論的基礎確立
2008Imbens-Lemieux実装ガイドライン標準化
2012Imbens-KalyanaramanMSE 最適帯域選択法
2014Calonico-Cattaneo-Titiunikロバスト CI、 現代の標準
2019Gelman & Imbens「高次多項式を使うな」警告

🏆 統計データ分析コンペでの RDD 活用

SSDSE データで RDD が使えそうなシナリオ

  1. 政令指定都市の閾値:人口 70 万を境に「行政区が設置できる」「税収配分が変わる」。 これを exogenous な閾値として使う
  2. 地方交付税の段階閾値:基準財政需要額が段階的に変わる人口区分(5 万、 10 万、 30 万、 50 万、 100 万)
  3. 過疎地域指定:人口減少率・財政力指数の閾値で過疎指定 → 補助金や開発支援が不連続に変化
  4. 少子化対策の年齢閾値:児童手当の所得制限など、 SSDSE-C / SSDSE-E の世帯データで応用可能
  5. 学校統廃合の規模閾値:児童数 100 人未満で統廃合検討となる自治体ルール

コンペで RDD を提案するときのチェックリスト

項目確認方法未達のリスク
閾値の制度的妥当性法令・条例・公文書で閾値が明示されているか恣意的閾値選択 → p-hack
running variable の連続性McCrary 検定、 ヒストグラム操作の存在 → 識別崩壊
共変量の連続性各共変量について RDD 推定 → τ≈0 か確認他要因混絡 → 効果過大評価
帯域選択の客観性IK / CCT の自動選択を使用「都合の良い h」選択
頑健性複数 h、 複数次数で τ が安定脆弱な結論
placebo偽閾値で τ がゼロに近い偽陽性のリスク
外的妥当性の議論LATE の限界を明示政策提言の過大解釈

プレゼンでよく聞かれる質問と回答例

❓ よくある質問

Q1. RDD と DID(差の差分析)はどう使い分ける?

A. RDD は「閾値前後の空間的不連続」、 DID は「時点前後の時間的変化を 2 群で比較」。 政策実施前後の時間比較なら DID、 制度の閾値で識別できるなら RDD。 両方使える場合は RDD の方が外的仮定が少なく好まれる。

Q2. 多項式の次数は何次が良い?

A. 1 次(局所線形)か最大 2 次まで。 Gelman & Imbens (2019) は 3 次以上を強く非推奨。 高次多項式は閾値直近の点にフィットが引きずられ、 偽のジャンプを生む。

Q3. サンプル数が少ない(n < 50)ときは?

A. 推定の不確実性が大きく、 統計的有意性は期待しにくい。 推定値の方向、 placebo の振る舞い、 制度的妥当性で総合判断。 ベイズ的アプローチ(事前情報を活用)の余地もある。

Q4. running variable が離散値(整数年齢など)のときは?

A. 離散の場合は Lee-Card (2008) や Kolesar-Rothe (2018) の特別な信頼区間が必要。 標準 SE は誤りに過小評価する。

Q5. 複数の閾値があるときは?

A. 各閾値で別々に τ を推定し、 比較。 たとえば人口 5万・10万・30万・50万・100万 と段階的な交付税基準があれば、 各閾値での τ を並べると「規模依存の制度効果」が見える。

Q6. クラスター構造(自治体の地理的近接など)がある場合は?

A. クラスター robust SE(県単位、 地方ブロック単位)を使う。 statsmodels の cov_type='cluster' オプション。

Q7. 機械学習で RDD を改善できる?

A. 共変量の高次元処理に ML(lasso、 random forest)を使う「Double ML for RDD」(Calonico et al. 2019)が登場。 共変量調整の精度を上げつつ、 RDD の局所性を保つ。

⚠️ RDD の落とし穴

① 強制変数 X が「操作」されている場合は無効
もし対象者が「閾値を意図的に超える/超えない」ように $X$ を操作できる場合、 RDD は崩壊します。 例:「合格点ギリギリで点数調整」「市制施行のために住民票を増やす」。 こうした manipulation があると、 閾値の両側で対象者の潜在能力地域特性が違ってしまい、 「閾値の前後でランダム」が成り立たない。 必ず McCrary 密度検定で確認。
② 帯域幅の選択で結果が大きく変わる
$h$ を狭めると「より local」だがサンプル少 → 標準誤差大。 広げるとサンプル多 → バイアス大。 1 つの $h$ で結果が大きい・有意 → 結論、 と決めつけるのは危険。 必ず複数の $h$(半分、 1.5 倍、 2 倍など)で推定し、 結果の頑健性を確認する。 論文の標準は CCT 法による自動選択と±10% の範囲をテスト。
③ 多項式次数を上げすぎると過剰適合
閾値左右のフィット線に高次多項式(3 次、 4 次)を使うと、 閾値近傍で不自然な振動が生じてジャンプが過大/過小評価される。 Gelman & Imbens (2019) は「多項式次数は 1 か 2 に限定すべき」と勧告("Why High-Order Polynomials Should Not Be Used in Regression Discontinuity Designs")。 局所線形 (p=1) が標準。
④ 推定された効果は「閾値近傍」だけ
RDD で得られる因果効果は「閾値ちょうどの対象者」に対する局所的効果 (LATE at c)。 「人口 5 万人前後の自治体での市制効果」は推定できても、 「人口 50 万人の自治体での市制効果」は分からない。 結果を外挿するときは、 同じ効果が他の値でも成り立つかを慎重に議論する必要がある。 外的妥当性 (external validity) の制約は強い。
⑤ 「複数の閾値」「閾値が時間変動」する場合は注意
制度的に閾値が複数あったり、 年度ごとに変わったりする場合、 単純な RDD では扱えません。 例:所得階層に応じた段階的補助金 → 複数閾値。 各閾値ごとに別々の RDD を回すか、 統合した枠組み(Multi-cutoff RDD)が必要。 また、 RDD が空間的・時系列に拡張された手法(Spatial RDD、 RDD in time)もある。

⚠️ さらなる落とし穴(応用編 5 件)

  1. 閾値の解釈バイアス:自治体が「200 万人を境に補助金が変わる」と知っていれば、 人口集計時の操作が起きる。 McCrary 検定で密度ジャンプを確認すること。 ジャンプがあれば RDD は使えない。
  2. 共変量バランス:「人口以外の特性(高齢化率、 1 人当たり所得)」が閾値前後で滑らかかを確認。 ジャンプがあれば、 観測された Y のジャンプは他要因の混絡を含む。
  3. 多項式の次数:3 次以上の多項式は「左右で異なる曲率」を生み、 中央付近で偽のジャンプを作る(Gelman & Imbens 2019)。 原則 1 次か 2 次まで
  4. カットオフの妥当性:閾値が「データを見てから事後に選ばれた」場合、 多重検定で p 値が膨らむ。 事前登録(pre-registration)か、 制度設計が明示する閾値だけ使う。
  5. 外的妥当性:RDD で推定される τ は「閾値近傍」だけの効果(LATE)。 大きく離れた人口規模(例:100 万人や 500 万人)への外挿はできない。 政策提言時は必ず明示。

🗺 RDD の概念マップ

RDD と因果推論手法群の関係を整理します。

                  【因果推論の Goal】
                  「X → Y の効果は何か?」
                          │
        ┌─────────────────┴─────────────────┐
        ▼                                     ▼
   【実験的手法】                      【観察データ手法】
   ・RCT                                  │
                              ┌──────────┼──────────┐
                              ▼                     ▼
                       【識別戦略あり】       【識別戦略なし】
                              │                     │
              ┌───────┬───────┼───────┬──────────────┘
              ▼       ▼       ▼       ▼
        【RDD】 【DiD】 【IV】 【マッチング】
          │       │       │       │
          │       │       │       └─ 傾向スコア
          │       │       │
          │       │       └─ 2SLS、 Fuzzy RDD
          │       │
          │       └─ パネルデータ、 二元固定効果
          │
          └─ Sharp RDD / Fuzzy RDD / RKD / Bunching
    

RDD のワークフロー

  1. 制度を理解する:閾値で何が変わるか、 制度的に決まっているか
  2. 強制変数 $X$ と閾値 $c$ を特定する
  3. McCrary 密度検定:閾値直近で $X$ の密度に不自然なジャンプがないか
  4. 共変量プラセボ検定:閾値で他の共変量がジャンプしないか
  5. 帯域幅 $h$ を選択(CCT 法等)
  6. 局所線形回帰で τ を推定
  7. 頑健性検定:$h$ を変えて、 多項式次数を変えて、 共変量を入れて、 結果が安定か確認
  8. 視覚化:RDD プロットで「ジャンプ」が明らかか確認
  9. 外的妥当性の議論:閾値近傍以外への外挿の制約を明示

🗺 RDD 概念マップ

RDD は因果推論の 「自然実験」系統に属し、 DID / IV / マッチング とともに観察データから因果効果を識別する 4 大手法の一つです。

因果推論 (causal inference)
├── ランダム化実験 (RCT)             ← gold standard
├── 観察データ手法
│   ├── 回帰調整 (regression adjustment)
│   ├── マッチング (matching)         ← 共変量で類似ペア
│   ├── 傾向スコア (propensity score) ← 処置確率で重み付け
│   └── 自然実験 (natural experiment)
│       ├── 差の差分析 (DID)         ← 時点 × 群
│       ├── 操作変数法 (IV)           ← 外生シフト
│       ├── 合成コントロール (SC)     ← 重み付き平均対照群
│       └── 回帰不連続デザイン (RDD)  ★ このページ
│           ├── Sharp RDD            ← D = 1(X ≥ c)
│           ├── Fuzzy RDD            ← P(D=1) ジャンプ
│           ├── Kink RDD             ← 傾きジャンプ
│           └── Regression Discontinuity in Time (RDIT)
└── 構造モデル (structural)

RDD の隣接概念:

RDD 前提: 因果推論 並列: Sharp / Fuzzy 発展: 局所線形回帰 応用: 政策評価 対比: DID / IV 統合: McCrary 密度検定

🔗 隣接手法への橋渡し

「回帰不連続デザイン」は単独で完結する手法ではなく、 隣接領域と連携することで真価を発揮する。

SSDSE-B-2026 のような都道府県別データに RDD を当てるなら、 cutoff 変数 (例: 補助金支給ライン) のすぐ上下の県を比較し、 cutoff 周辺の連続性検定 → 平均処置効果推定 → 帯域感度分析の順で結論を出す。

🌳 手法選択フロー

「RDD」を実際の課題に当てはめるとき、 状況別に何を選ぶかを 3 段階で判定する。

  1. 処置の閾値はあるか? Yes (合格点・所得制限) → RDD 適用可、 No → IV / DID を検討
  2. 閾値前後でジャンプか連続か? ジャンプ (sharp RDD) → 単純比較、 確率変化 (fuzzy RDD) → IV 併用
  3. サンプルは十分か? 閾値近傍にデータが集まる → 局所線形回帰、 少 → 帯域幅を広げ全域回帰

SSDSE-B-2026 だけで RDD は難しいが、 「人口 100 万人ライン (政令指定都市基準)」前後で歳入・補助金がジャンプするかを観察すれば、 制度効果の素朴な目視が可能。

RDD のメタ分析

De la Cuesta & Imai (2016) は、 政治学分野の RDD 研究 50 件以上をメタ分析。 「閾値での操作」「帯域幅依存」「多項式選択」の 3 大問題を体系的に検証し、 ベストプラクティスを提示。 結論:適切に分析されれば RDD は IV やマッチングより信頼性が高い識別戦略。 適切な帯域選択・密度検定・プラセボ検定を経れば、 RDD は観察データから因果効果を取り出す最も内部妥当性の高い準実験デザインの一つとなる。

🎮 触って理解する

RDD の核心は「閾値の左右で回帰直線を別々にフィットし、 その切片差=ジャンプ τ を測る」こと。 下の図は横軸が割当変数 X、 縦軸が結果 Y青=処置なし(X < 閾値)、 赤=処置あり(X ≥ 閾値)。 スライダーで 処置効果 τ・傾き・バンド幅 h を動かし、 グラフ上をドラッグ(タッチ可)すると閾値 c が動きます。 バンド幅を広げると推定の分散(SE)は下がるが右側の曲がり(非線形)を直線で近似するバイアスが増える様子を体感してください。

真の τ = +30.0 推定 τ̂ = +0.0 SE = 0.0 バイアス = 0.0 帯域内 n = 0(左0/右0

💡 直感 — 閾値近傍は「擬似ランダム割当」

閾値ちょうどの直上・直下にいる対象は、 割当変数の値がほぼ同じ=観測・非観測を問わず背景要因がほぼ揃っていると見なせます。 唯一違うのは「閾値を超えたか」で機械的に決まる処置の有無だけ。 だから閾値近傍の比較はミニ・ランダム化実験に近く、 ジャンプ τ̂ を因果効果として読めます。 図でバンド幅 h を最小にすると、 使うのは閾値のすぐ両脇の点だけになり、 この「擬似ランダム」の理屈が最も純粋に効く(ただし点が減って SE が跳ね上がる)ことが分かります。

⚠️ よくある落とし穴

🚀 発展 — シャープ vs ファジー・局所線形回帰

この図は処置が閾値で 0→1 に確定する シャープ RDD。 現実には「資格はあるが受け取らない」人がいて処置確率が 0.3→0.8 のように部分的にしか跳ねない ファジー RDD も多く、 その場合は Y のジャンプを D(処置確率)のジャンプで割って正規化します(本ページ数式節の $\tau_{FRD}$)。 推定は帯域内サンプルへの 局所線形回帰が標準で、 端点バイアスを抑えるため中心ほど重い 三角カーネルで重み付けするのが定石。 Python では rdrobust が CCT の頑健信頼区間まで自動計算します。

関連ページ: 操作変数法 (IV)差分の差分法 (DID)因果推論回帰分析

🔎 さらに深掘り — 直感・落とし穴・発展(追補)

ここまでで RDD の骨格は掴めているはずです。 この追補では、 (1) なぜ「準実験」と呼べるのかの直感を RCT と対比して固め、 (2) 実務で最も足をすくわれる落とし穴を 1 枚のチェック表に集約し、 (3) 現場で問われる発展トピック(最適帯域・密度検定・断続時系列との違い)を整理します。 既存の各節と重複しない補完として読んでください。

🎨 直感の再確認 — 「閾値近傍=擬似ランダム化」

RDD が 準実験(quasi-experiment)と呼ばれるのは、 閾値 $c$ のすぐ両脇にいる対象は「割当変数がほぼ同じ=背景要因もほぼ同じ」で、 処置の有無だけが機械的に切り替わるからです。 つまり閾値近傍では「誰が処置群・対照群になるか」が実質コイン投げに近い。 これが RCT(無作為化比較試験)に匹敵する内部妥当性を生みます。 違いは、 RCT が全対象を無作為化するのに対し、 RDD は閾値近傍だけで無作為化が近似的に成り立つ点です。

観点RCT(無作為化実験)RDD(回帰不連続)
割当の仕組み実験者が乱数で処置を割当閾値を境に制度が機械的に割当
比較可能性の根拠無作為化により全共変量が均衡閾値近傍で共変量が連続=ほぼ均衡
推定できる効果母集団全体の平均処置効果 (ATE)閾値ちょうどの局所効果 (LATE at c)
外的妥当性対象母集団に一般化しやすい閾値から離れると外挿不可
実施コスト・倫理高い・倫理制約が大きい既存の制度を利用=低コスト

🧪 実測で見る「帯域幅で結論が変わる」— 別閾値の擬似 RDD

既存節では閾値 200 万人を扱いました。 ここでは別の教材用擬似設定として閾値を 150 万人に置き、 SSDSE-B-2026(2023 年、 47 都道府県)の実測値で τ を計算します。 ※閾値 150 万人は制度的根拠のない架空・教材例の設定で、 数値だけが実データです。

閾値直近のペア(実測):

SSDSE-B-2026 (2023年) 閾値=1,500,000人(教材用の擬似設定)
閾値以上の県数: 24 / 閾値未満の県数: 23
直下: 沖縄県 人口 1,468,000 一般会計歳入 E1101 = 151 十億円
直上: 鹿児島県 人口 1,549,000 一般会計歳入 E1101 = 134 十億円
近傍2点差(素朴なジャンプ): 134 - 151 = -17 十億円

局所線形回帰で帯域幅 $h$ を振ると(実測):

h=0.50: n=21, τ=-11.21 (SE=43.26)
h=0.75: n=28, τ=+27.61 (SE=33.87)
h=1.00: n=34, τ= +2.06 (SE=31.87)

💬 読み方:τ の符号すら $h$ で -11 → +28 → +2 と反転し、 かつどの $h$ でも SE が推定値をはるかに上回ります。 これは「有意になる $h$ を後から選ぶ」ことの危険性を実データで示す好例です。 200 万人閾値の結論(ジャンプ非検出)と合わせ、 SSDSE-B の都道府県人口には制度的な財政不連続は見当たらないことが、 閾値を変えても頑健に確認できます。

⚠️ 落とし穴・統合チェック表

既存の「⚠️ RDD の落とし穴」節(5+5 件)を、 診断方法とセットで 1 枚に凝縮します。 提出前にこの表を上から順に潰すのが実務の型です。

落とし穴症状診断・対処
操作(manipulation)閾値直下に密度の山/直上に空隙McCrary 密度検定・rddensity。 密度が不連続なら RDD 断念
密度の連続性強制変数の分布が閾値で跳ねる密度の連続性検定(McCrary 2008)で $p>0.05$ を確認
帯域幅(bandwidth)選択$h$ を変えると τ が大きく動くIK / CCT の最適 $h$ +±20% の感度分析を必ず併記
関数形の誤設定高次多項式で偽のジャンプ局所線形(1 次)を基本。 3 次以上は避ける(Gelman-Imbens 2019)
局所効果のみ閾値から離れた値へ外挿結論を「閾値近傍の LATE」に限定。 外的妥当性を明示
標本サイズ帯域内 n が小さく SE 過大帯域内 n と SE を報告。 n が薄いなら結論を保留
シャープ vs ファジー閾値で処置率が 0→1 でないファジーなら Y のジャンプを D のジャンプで割る(2SLS)
共変量バランス共変量が閾値で不連続各共変量を outcome にして placebo RDD。 ジャンプ無しを確認

🚀 発展 — 最適帯域・密度検定・そして「断続時系列」との違い

最適帯域幅の自動選択:手で $h$ を決めるのは恣意的なので、 バイアスと分散の平均二乗誤差 (MSE) を最小化する $h$ を理論的に選びます。 代表が IK 法(Imbens-Kalyanaraman 2012)と CCT 法(Calonico-Cattaneo-Titiunik 2014)。 CCT はバイアス補正頑健信頼区間まで提供し、 現在の事実上の標準です(rdrobust)。

McCrary 密度検定:識別の生命線である「操作なし」を検証する道具。 強制変数の密度を閾値の左右で別々に推定し、 密度が連続(=閾値で跳ねない)ことを確認します。 跳ねていれば対象が閾値をまたぐよう自己選択している疑いがあり、 RDD の前提が崩れます。

プラセボ検定:(1) 偽の閾値(本来ジャンプが無い点)で τ≈0 を確認するプラセボ・カットオフ、 (2) 処置の影響を受けないはずの共変量を outcome にして τ≈0 を確認する共変量プラセボ。 どちらもジャンプが出れば、 本命のジャンプも疑わしくなります。

断続時系列(Interrupted Time Series, ITS)との違い:両者とも「境界で不連続なジャンプ」を測りますが、 横軸が違います。 RDD の横軸は個体の強制変数(人口・点数・所得など)で、 同時点の多数の個体を閾値の左右で比較します。 一方 ITS の横軸は時間で、 政策施行前後の同一集団の推移を比較します。 「いつ効いたか(時間の切れ目)」を見るのが ITS、 「どの水準で効くか(変数の切れ目)」を見るのが RDD。 施行時点が明確でも並行トレンドの仮定が要る点で、 ITS はむしろ DID平行トレンド と親戚です。 施行タイミングが未知なら change-point detection、 タイミングを操作変数化するなら IV と接続します。

🔗 関連ページ(実在ファイルへの相対リンク)

※ 上記リンクは html/glossary/ 内に実在するファイルのみを掲載しています。