この連載について
因果推論を「本を読んだ」で終わらせず、自分の言葉で説明でき、コードで再現できる状態まで落とす30日連載です。直前のDay 19では、処置された州が1つしかない状況で、他の州の凸結合から「合成テキサス」という対照群を作り、プラセボ置換検定で効果の珍しさを語る合成コントロール法をやりました。今日は、比較相手を空間にではなく制度の設計そのものの中に見つけます。実データはウルグアイの現金給付プログラム gov_transfers です。
TL;DR(3行)
- 閾値で機械的に割付が決まる制度の「際」は擬似ランダム。 ウルグアイの現金給付は、所得スコアが閾値未満の世帯だけに受給資格。スコアは政府が非公開の算式で計算するので世帯は微調整できない ← 際ではRCTが自然発生します。
- 局所線形回帰+三角カーネルでカットオフのジャンプを推定。全幅のあてはめで政府支持は +0.10(0〜1の支持指標で1割の上昇)、ただし際のビン平均の生の差は +0.038。
- 点推定はバンド幅次第で −0.03 から +0.10 まで動きます。バンド幅感度と密度検査(操作の痕跡)まで示して初めてRDDの報告です。効果を語れるのはカットオフ近傍だけで、遠くへの外挿は識別の外です。
今日の問い
所得スコアが基準以下なら給付、超えたら対象外。補助金も奨学金も進級も、行政と制度はこういう「線引き」だらけだ。
線引きのすぐ両側にいる人たちは、ほとんど同じ人たちなのに扱いだけが違う。この「際」から、給付が政府支持を動かしたかという因果効果を取り出せるか?
今日のゴールは、この「際の比較」を**回帰不連続デザイン(RDD, regression discontinuity design)**として定式化し、実データで、ビン平均の目視、局所線形回帰によるジャンプ推定、バンド幅感度、密度検査までを一気にコード化することです。
概念① ランニング変数とカットオフ:際ではRCTが自然発生する
RDDの登場人物は3人です。割付を決める連続変数をランニング変数 $X$、線引きの値をカットオフ $c$ と呼びます。今日のデータでは処置 $D$ が
$$
D_i = \mathbf{1}[X_i < c]
$$
と、ランニング変数だけで機械的に決まります。$\mathbf{1}[\cdot]$ はカッコの中が成り立てば1、成り立たなければ0を返す記号なので、この式は「スコアが閾値未満なら処置1、そうでなければ0」と読みます。閾値未満なら全員が処置、以上なら全員が対象外。このタイプをシャープRDDと呼びます(割付が確率的にしか動かないファジーRDDは次回Day 21の親戚です)。
データの背景を先に置きます。ウルグアイ政府は2005年から2007年、貧困世帯向けの現金給付プログラム PANES を実施しました。受給資格は、事前調査の世帯属性から政府が予測所得スコアを計算し、閾値未満の世帯だけに与えられました。スコアの算式は非公開です。Manacorda, Miguel & Vigorito (2011) は、その後の追跡調査で「現政権を支持するか」(不支持0/中立0.5/支持1)を尋ね、給付が政府支持を動かしたかをRDDで検証しました。causaldata 収載版は、閾値を0に中心化したスコア ±0.02 の近傍にいる1,948世帯へ事前に絞られています。
なぜ「際」ではランダム扱いできるのでしょうか。ここを精密に言います。スコアには、世帯が制御できない要素(調査時点の測定、算式の重み、集計の誤差)が必ず混ざります。だから閾値ぎりぎりの世帯にとって、自分が線のどちら側に落ちるかは本人には選べない偶然です。閾値の下に落ちた世帯と上に落ちた世帯は、際に近づく極限では同じ分布から来た2群、つまりコイン投げで分けた2群と見なせます。Day 4 で見たRCTが、制度の隙間に局所的に自然発生しているわけです。この議論の本質は「自分でスコアを精密に操作できない」の一点です。もし世帯がスコアを微調整できるなら(給付が欲しい世帯が申告を「調整」して閾値の下へ駆け込むなら)、際の両側はもはや同質の2群ではなく、比較は壊れます。だから後で密度検査をします。
識別(データと仮定から因果効果を一意に決めること)の正式な仮定は、潜在的結果の条件付き期待値 $E[Y(1) \mid X=x]$ と $E[Y(0) \mid X=x]$ がカットオフで連続であること。平たく言えば「給付という制度がなければ、閾値をまたいでも支持は急に変わらなかったはず」です。このとき観測される支持率の段差がそのまま因果効果になります。
$$
\tau_{\mathrm{SRD}} = \lim_{x \uparrow 0} E[Y \mid X = x] ; - ; \lim_{x \downarrow 0} E[Y \mid X = x] ; = ; E[Y(1) - Y(0) \mid X = 0]
$$
真ん中の2項は、カットオフに左(受給側)から近づいたときの支持の平均と、右(対象外の側)から近づいたときの平均です。その差、つまり際でできる段差が、右辺のカットオフ上の処置効果に等しい、と読みます。
注意が2つ。第一に、教科書の多くは「閾値以上が処置」ですが、今日は「閾値未満が処置(貧しい側が給付対象)」で向きが逆です。第二に、右辺を見てください。推定できるのは $X = 0$、つまりカットオフ上の世帯に限った局所効果です。閾値から遠い世帯の効果は、この式のどこにも出てきません。
概念② 局所線形回帰とバンド幅:際に近いほど重く
段差をどう測るか。いちばん素朴なのは「際のすぐ両側の平均差」ですが、ぎりぎりの世帯だけではデータが少なすぎます。窓を広げれば数は増えますが、今度は所得と支持のトレンド(貧しい世帯ほど支持が高い、など)が混入します。この綱引きを扱う標準装備が局所線形回帰です。カットオフから距離 $h$(バンド幅)以内のデータだけを使い、両側で別々の直線をあてはめます。
$$
Y_i = \alpha + \tau D_i + \beta_1 X_i + \beta_2 D_i X_i + \varepsilon_i \qquad (|X_i| < h)
$$
$D_i X_i$ の交互作用で左右の傾きを変え、切片の差 $\tau$ がカットオフでのジャンプです。さらに、窓の中でも際に近い観測を重くするため、三角カーネル $K_i = 1 - |X_i|/h$ を重みにした重み付き最小二乗(WLS)にします。「際が主役、遠くは脇役」を重みで表現するわけです。
バンド幅 $h$ は、バイアスと分散のトレードオフを1つの数字に集約したものです。$h$ を小さくすると直線近似は正確になりますが(バイアス小)、データが減って推定が暴れます(分散大)。$h$ を大きくするとその逆です。実務では最適バンド幅と頑健な信頼区間を出してくれる rdrobust パッケージ(Calonico, Cattaneo & Titiunik)が定番ですが、今日は中身を理解するために statsmodels で素朴版を手組みします。式は上の1本だけなので、コードは10行で書けます。
手を動かす① 散布図のビン平均で不連続を見る
まずデータを読み、割付が本当にシャープかを確かめ、際の段差を目視します。
import numpy as np
from causaldata import gov_transfers
d = gov_transfers.load_pandas().data
x = d["Income_Centered"].to_numpy() # ランニング変数(閾値を0に中心化済み)
y = d["Support"].to_numpy() # 政府支持(0=不支持, 0.5=中立, 1=支持)
t = (x < 0).astype(float) # 閾値未満なら受給資格あり(処置)
print("閾値未満 %d 世帯 / 閾値以上 %d 世帯" % (int(t.sum()), int((1 - t).sum())))
print("受給率: 閾値未満 %.2f / 閾値以上 %.2f"
% (d.loc[x < 0, "Participation"].mean(), d.loc[x >= 0, "Participation"].mean()))
print("窓内全体の素朴な差 = %.3f" % (y[t == 1].mean() - y[t == 0].mean()))
jb = y[(x >= -0.002) & (x < 0)].mean() # 際のすぐ下(幅0.002のビン)
ja = y[(x >= 0) & (x < 0.002)].mean() # 際のすぐ上
print("際のビン平均: すぐ下 %.3f / すぐ上 %.3f (差 %.3f)" % (jb, ja, jb - ja))
閾値未満 1127 世帯 / 閾値以上 821 世帯
受給率: 閾値未満 1.00 / 閾値以上 0.00
窓内全体の素朴な差 = 0.118
際のビン平均: すぐ下 0.846 / すぐ上 0.808 (差 0.038)
受給率は閾値未満で 1.00、以上で 0.00。割付は完全に線どおりで、教科書的なシャープRDDです。窓内全体の素朴な差は 0.118 ですが、これは閾値から遠い世帯まで丸ごと混ぜた比較で、所得と支持のトレンドを含んでいます。いっぽう際のすぐ両側のビン平均の差は +0.038。全体の差の3分の1です。どちらもまだ答えではありません。前者はトレンド混入、後者は片側90世帯前後しか使わないノイズまみれの比較だからです。RDDの仕事は、この間を規律を持って埋めることです。
手を動かす② 局所線形回帰でジャンプを推定する
概念②の式をそのまま組みます。比較のため、まずカーネルなしで全幅(±0.02)に直線をあてはめ、次に三角カーネル付きの局所線形をバンド幅 h=0.01 で回します。
import statsmodels.api as sm
# 全幅(±0.02)に両側の直線をあてはめる。tの係数がカットオフでのジャンプ
X = sm.add_constant(np.column_stack([t, x, t * x]))
# HC1=観測ごとに誤差のばらつきが違っても壊れない、頑健な標準誤差の計算法
ols = sm.OLS(y, X).fit(cov_type="HC1")
print("全幅の直線あてはめ: ジャンプ = %+.3f (SE %.3f)" % (ols.params[1], ols.bse[1]))
def rdd_local_linear(h):
m = np.abs(x) < h # バンド幅 h の窓に絞る
k = 1 - np.abs(x[m]) / h # 三角カーネル:際に近いほど重い
X = sm.add_constant(np.column_stack([t[m], x[m], t[m] * x[m]]))
res = sm.WLS(y[m], X, weights=k).fit(cov_type="HC1")
return res.params[1], res.bse[1], int(m.sum())
tau, se, n = rdd_local_linear(0.01)
print("局所線形 (h=0.010): ジャンプ = %+.3f (SE %.3f, N=%d)" % (tau, se, n))
全幅の直線あてはめ: ジャンプ = +0.100 (SE 0.030)
局所線形 (h=0.010): ジャンプ = +0.033 (SE 0.044, N=937)
全幅の直線あてはめでは、ジャンプは +0.100。0〜1の支持指標で1割の上昇なので、効果としてはかなり大きい数字です。下の図がこの推定の絵解きで、幅0.002のビン平均(灰色の点)に両側の直線を重ねています。カットオフの左(受給資格あり側)が全体に高く、際で赤い矢印ぶんの段差が見えます。
ところが、際を重くした局所線形(h=0.01、三角カーネル)に切り替えると、ジャンプは +0.033(SE 0.044)まで縮み、統計的にはゼロと区別がつきません。同じデータ、同じ式で、使う窓と重みを変えただけで答えが3倍違う。ここで私は、記事の冒頭にどちらの数字を書くかで手が止まりました。片方だけ載せれば話はきれいになりますが、それは選ばなかったほうを黙って捨てることでもあります。この不一致をそのまま次のステップで直視します。
手を動かす③ バンド幅を振り、密度の偏りを確かめる
RDDの報告の作法は「1本のバンド幅で言い切らない」です。h を 0.005 から 0.02 まで振って、推定がどう動くかを全部見せます。あわせて、概念①の約束どおり操作(manipulation)の検査もします。世帯が閾値の下へ駆け込んでいるなら、ランニング変数の密度がカットオフのすぐ下だけ盛り上がるはずです。
for h in [0.005, 0.0075, 0.010, 0.0125, 0.015, 0.0175, 0.020]:
tau, se, n = rdd_local_linear(h)
print("h=%.4f: ジャンプ = %+.3f (95%%CI [%+.3f, %+.3f], N=%d)"
% (h, tau, tau - 1.96 * se, tau + 1.96 * se, n))
n_below = int(((x >= -0.002) & (x < 0)).sum()) # 際のすぐ下の世帯数
n_above = int(((x >= 0) & (x < 0.002)).sum()) # 際のすぐ上の世帯数
print("際の世帯数: すぐ下 %d / すぐ上 %d (比 %.2f)" % (n_below, n_above, n_below / n_above))
print("窓全体の比: 閾値未満/以上 = %.2f" % (t.sum() / (1 - t).sum()))
h=0.0050: ジャンプ = -0.027 (95%CI [-0.168, +0.114], N=462)
h=0.0075: ジャンプ = +0.006 (95%CI [-0.096, +0.108], N=698)
h=0.0100: ジャンプ = +0.033 (95%CI [-0.053, +0.120], N=937)
h=0.0125: ジャンプ = +0.062 (95%CI [-0.015, +0.139], N=1178)
h=0.0150: ジャンプ = +0.080 (95%CI [+0.009, +0.150], N=1425)
h=0.0175: ジャンプ = +0.089 (95%CI [+0.023, +0.154], N=1683)
h=0.0200: ジャンプ = +0.096 (95%CI [+0.034, +0.157], N=1948)
際の世帯数: すぐ下 94 / すぐ上 73 (比 1.29)
窓全体の比: 閾値未満/以上 = 1.37
点推定は −0.027 から +0.096 まで単調に動きます。狭いバンド幅では信頼区間が ±0.14 と広く(際の462世帯しか使わないので当然です)、h=0.015 以上でようやく区間がゼロを離れます。カットオフのすぐ上のビン(0.004〜0.008)に支持の凹みがあり、狭い窓ではこれが「上側の切片」を押し上げてジャンプを小さく見せています。正直にまとめると、この抽出データと素朴な実装から言えるのは「効果は正で+0.1弱を示唆するが、際に絞ると統計的にははっきりしない」までです。原論文はより大きな標本と追加の設計でこの正の効果を頑健に報告していますが、1,948世帯の教材抽出では点推定がバンド幅にこれだけ敏感になる。この感度自体が、RDDを報告するとき必ず添えるべき情報です。
次に密度です。際のすぐ下は94世帯、すぐ上は73世帯で、たしかに下側が厚い(比1.29)。これだけ見ると駆け込みを疑いたくなります。でも窓全体の比も1.37で、下側はどこでも同じくらい厚いのです。カットオフに向かって密度がせり上がる形、つまり駆け込みの形にはなっていません。この一様な段差は、調査が受給世帯を多めに抽出した設計の産物です。そして制度側の事実がこれを裏打ちします。スコアは政府が非公開の算式で計算するので、世帯が「あと0.001だけ下」を狙うことはそもそも不可能でした。密度検査の実務版としては McCrary 検定(両側の密度を局所推定して段差を検定)がありますが、検定より先に、この目視と制度の理解が本体です。
つまづき・誤解しやすい点
- RDDが推定するのはカットオフの際の局所効果だけです。 今日の +0.10 は「閾値ぎりぎりの世帯」への効果で、もっと貧しい世帯やもっと豊かな世帯への効果はデータのどこにも書かれていません。外挿は識別の外です。裏返すと、際の内側に限れば観察研究として最強クラスの説得力が出ます。狭く深く、がRDDの性格です。
- 高次多項式で全体をなめらかにフィットしてはいけません。 4次や5次の多項式は端点で暴れ、カットオフに偽のジャンプを作ります(Gelman & Imbens 2019 が明確に非推奨)。作法は局所線形(せいぜい2次)+バンド幅感度の提示です。今日の fig2 のように、hを振った絵を必ず添えます。
- 密度検査は合否判定の儀式ではありません。 今日のデータは際の密度に段差がありますが、正体は抽出設計でした。逆に、閾値が公表されスコアが自己申告なら、密度がなめらかに見えても歪みは残りえます。検定のp値ではなく「誰がスコアを、いつ、どこまで動かせたか」という制度の理解が判断の本体です。共変量(年齢や学歴)が際で連続かのチェックも同じ精神です。
GISデータ実務での使い方
- 制度の線引きは全部RDDの候補です。 水道分野なら、給水人口や料金回収率など指標の閾値で交付金や補助率が変わる制度があります。閾値ぎりぎりの事業体同士を比べれば、「補助を受けた事業体は投資が増えたか」を自己選択の交絡なしで検証できます。導入済みと未導入を丸ごと比べたDay 6 の反省の、制度版の答えです。
- 地理的境界もカットオフになります。 給水区域や自治体の境界線の両側は、地形も気候もほぼ同じなのに料金体系や管理主体だけが違います。境界の際で漏水率や更新投資を比べる境界不連続デザインは、衛星データと相性のよい設計です。ランニング変数が2次元(緯度経度)になったときの扱いは、Day 28 の空間×因果でまとめて掘ります。
- 自分たちで閾値を作るときは、際をランダム化しておく手があります。 漏水リスクスコアの上位から工事する運用は、まさにスコアとカットオフの制度です。閾値が既知だと現場が指標を「調整」する誘因も生まれます。設計段階で、閾値ぎりぎりの帯だけ順序をランダムにしておけば、将来の効果検証(工事は本当に漏水を減らしたか)が際のRCTとしてただで手に入ります。
参考(本棚)
- 伊藤公一朗『データ分析の力 因果関係に迫る思考法』。RDDを一般向けに最も丁寧に説いた入門。「際の比較」の直感はこの本の言葉が下敷き
- Nick Huntington-Klein『The Effect』第20章。gov_transfers を使ったRDD実装の本家。局所線形・カーネル・バンド幅の実務手順
- Jeffrey M. Wooldridge『Introductory Econometrics: A Modern Approach』。政策評価の計量経済学の標準教科書。RDDと処置効果の位置づけ
- Lee & Lemieux (2010) "Regression Discontinuity Designs in Economics" (Journal of Economic Literature)。RDDサーベイの定番。密度検査・共変量連続性チェックの作法
- データ: gov_transfers(
causaldataパッケージ経由、MITライセンス、取得日2026-08-15)。Manacorda, Miguel & Vigorito (2011) "Government Transfers and Political Support" (AEJ: Applied 3(3)) のウルグアイ PANES 調査から、閾値近傍の1,948世帯を抽出した教材データ
次回予告(Day 21)
今日のRDDはシャープでした。閾値未満は受給率 1.00、以上は 0.00 という完璧な段差です。でも現実の制度と実験の多くは、こうはいきません。無料クーポンを配っても受け取らない人がいて、対象外なのに制度に潜り込む人がいる。割付(assignment)と受療(take-up)がズレるのです。次回 Day 21 は ITT と LATE。「配った効果」と「受けた効果」はどう違うのか、割付を操作変数(Day 17)に使うと何が推定できるのか、そしてその効果は誰の効果なのか(コンプライヤー)まで降ろします。今日ちらりと名前の出たファジーRDDも、実はこのズレの親戚です。


