Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
A direct command skips the review prompt. Inspect the source before running it.
Regression Discontinuity Design (RDD) exploits a known threshold in a continuous assignment
variable ("running variable") that determines treatment. Units just above and just below the
cutoff are locally comparable — providing near-experimental variation for causal identification.
Conceptual Framework
Sharp RDD
Treatment is a deterministic function of the running variable X:
D_i = 1(X_i ≥ c)
The treatment effect τ_SRD is identified as:
τ_SRD = lim_{x↓c} E[Y|X=x] − lim_{x↑c} E[Y|X=x]
Identification requires:
Continuity: E[Y(0)|X=x] and E[Y(1)|X=x] are continuous at c.
No precise manipulation: agents cannot perfectly sort just above the cutoff.
Fuzzy RDD
When the probability of treatment changes discontinuously at c (but not from 0 to 1):
τ_FRD = jump in E[Y|X] at c / jump in E[D|X] at c
This is a local IV estimator; it recovers the LATE for compliers near the threshold.
Bandwidth Selection
The MSE-optimal bandwidth (Calonico, Cattaneo, Titiunik 2014):
h_MSE = C_n · n^{-1/5}
In practice rdrobust computes data-driven bandwidths via IK (Imbens-Kalyanaraman) or
CCT (Calonico-Cattaneo-Titiunik) selectors.
running_var: np.ndarray, cutoff: float,
n_bins: int = 30
dict
"""
McCrary (2008) density continuity test for sorting / manipulation.
H0: density of running variable is continuous at the cutoff.
Rejects if agents can precisely manipulate their score to just exceed c.
Method: estimate local linear density on each side of cutoff using
a bin count approximation; compare slopes.
Parameters
----------
running_var : 1-D array of running variable values
cutoff : threshold value c
n_bins : number of histogram bins per side (default 30)
Returns
-------
dict with t_statistic, p_value, interpretation, and density arrays
"""
# center at cutoff
# separate sides
0
0
# bin width: half the standard bandwidth rule
2
1.06
len
0.2
def
_local_density
side, sign
"""Fit local linear to binned density on one side."""
min
0
if
0
else
0
max
1
1
1
2
len
1
0
# local linear fit weighted by triangular kernel near 0
"""
Estimate sharp RDD treatment effect using local polynomial regression.
Parameters
----------
y : outcome variable
x : running variable
cutoff : threshold c
bandwidth : half-bandwidth h; if None, uses IK-style MSE-optimal selector
poly_order: polynomial order for local regression (1 = local linear)
kernel : 'triangular', 'uniform', or 'epanechnikov'
Returns
-------
dict with tau (LATE), se, t_stat, p_value, ci, bandwidth, and raw results
"""
"""
Fuzzy RDD: local IV using cutoff as instrument for treatment.
τ_FRDD = (jump in E[Y|X] at c) / (jump in E[D|X] at c)
Parameters
----------
y : outcome
x : running variable
z : actual treatment take-up (binary or continuous compliance)
cutoff : threshold
"""
float
float
float
if
is
None
# reduced form: effect of crossing on Y
# first stage: effect of crossing on treatment take-up
if
abs
"tau"
1e-10
raise
"First stage is essentially zero — instrument is weak."
"tau"
"tau"
# delta method SE
"se"
"tau"
2
"tau"
"se"
"tau"
2
2
2
1
abs
return
"tau_fuzzy"
"se"
"t_statistic"
"p_value"
"ci_95"
1.96
1.96
"first_stage_tau"
"tau"
"first_stage_F"
"t_statistic"
2
"reduced_form_tau"
"tau"
"bandwidth"
"interpretation"
f"Fuzzy RDD LATE = {tau_fuzzy:.4f} (SE={se_fuzzy:.4f}, p={p_fuzzy:.4f})"
"""
Test that predetermined covariates are balanced across the cutoff.
Run sharp RDD for each covariate as the outcome — reject H0 indicates imbalance.
Returns a DataFrame with tau, se, p_value, and pass/fail for each covariate.
"""
"""
Estimate sharp RDD across a grid of bandwidths and report stability.
A credible RDD should show τ stable across a reasonable bandwidth range.
Large swings suggest model sensitivity or violation of continuity assumption.
"""
"""
Simplified Imbens-Kalyanaraman (2012) bandwidth selector.
Uses a pilot bandwidth (Silverman) and regularises the optimal formula.
"""
len
# pilot bandwidth
1.84
1
5
0
0
if
sum
5
or
sum
5
return
# curvature estimates via second-order polynomial fits
def
_curvature
mask
if
sum
4
return
0.0
2
return
2
abs
0
# regularisation constants
4
4
3.4375
# triangular kernel constant
2
2
if
0
return
1
5
return
max
0.1
Example A — Education Policy at a Test Score Threshold
A school district assigns remedial tutoring to students scoring below 50 on a diagnostic
exam. We estimate the causal effect on year-end scores.
# example_a_education_rdd.py"""
Sharp RDD: effect of remedial tutoring assigned at test score < 50.
"""import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from rdd_design import (
test_manipulation,
run_sharp_rdd,
plot_rdd,
covariate_balance_rdd,
rdd_sensitivity_bandwidth,
)
rng = np.random.default_rng(1234)
n = 3000# running variable: baseline test score (0–100)
score = rng.beta(5, 5, n) * 100
cutoff = 50.0# treatment: tutoring if score < 50
treated = (score < cutoff).astype(float)
# covariates (pre-determined — should be balanced at cutoff)
age = 10 + rng.normal(0, 0.5, n)
female = rng.binomial(1, 0.5, n).astype(float)
ses = rng.normal(0, 1, n) # socioeconomic status index# outcome: end-of-year test score# true effect of tutoring: +6 points for compliers near cutoff
noise = rng.normal(0, 8, n)
outcome = (
40 + 0.5 * score
- 0.3 * (score - cutoff)**2 * 0.005
+ 6 * treated
- 2 * ses
+ noise
)
df = pd.DataFrame({
"outcome": outcome, "score": score, "treated": treated,
"age": age, "female": female, "ses": ses,
})
# ── 1. Manipulation test ──
manip = test_manipulation(df["score"].values, cutoff)
print("Manipulation test:", manip["interpretation"])
# ── 2. Main RDD estimate ──
result = run_sharp_rdd(df["outcome"].values, df["score"].values, cutoff)
print(f"\nSharp RDD τ = {result['tau']:.3f} SE = {result['se']:.3f} "f"p = {result['p_value']:.4f}")
print(f"95% CI: [{result['ci_95'][0]:.3f}, {result['ci_95'][1]:.3f}]")
print(f"Bandwidth: {result['bandwidth']:.2f} N (left/right): "f"{result['n_left']}/{result['n_right']}")
# ── 3. Covariate balance ──
balance = covariate_balance_rdd(df, "score", cutoff, ["age", "female", "ses"])
print("\nCovariate balance:")
print(balance.to_string(index=False))
# ── 4. Bandwidth sensitivity ──
sensitivity = rdd_sensitivity_bandwidth(
df["outcome"].values, df["score"].values, cutoff
)
print("\nBandwidth sensitivity (τ range):",
f"[{sensitivity['tau'].min():.3f}, {sensitivity['tau'].max():.3f}]")
# ── 5. RDD plot ──
fig = plot_rdd(
df["outcome"].values, df["score"].values, cutoff,
title="Effect of Remedial Tutoring on End-of-Year Score",
ylabel="End-of-year test score",
xlabel="Baseline test score",
)
# overlay bandwidth sensitivity as inset
ax_inset = fig.add_axes([0.62, 0.15, 0.28, 0.30])
ax_inset.fill_between(sensitivity["bandwidth"], sensitivity["ci_lower"],
sensitivity["ci_upper"], alpha=0.3, color="#2c7bb6")
ax_inset.plot(sensitivity["bandwidth"], sensitivity["tau"],
color="#2c7bb6", linewidth=2)
ax_inset.axhline(0, color="black", linestyle="--", linewidth=0.8)
ax_inset.set_xlabel("Bandwidth", fontsize=8)
ax_inset.set_ylabel("τ", fontsize=8)
ax_inset.set_title("BW sensitivity", fontsize=8)
fig.savefig("rdd_education.png", dpi=150, bbox_inches="tight")
print("\nSaved rdd_education.png")
Example B — Incumbency Advantage in Elections
Lee (2008) style: the running variable is the Democratic vote margin in election t;
the outcome is winning in election t+1. The cutoff is 0 (bare majority).
# example_b_incumbency_rdd.py"""
Fuzzy RDD: incumbency advantage.
Running variable: vote share margin = DEM% - 50.
Outcome: probability of winning next election.
Treatment: actually serving as incumbent (compliance < 1 due to death, resignation, etc.)
"""import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from rdd_design import (
test_manipulation,
run_sharp_rdd,
run_fuzzy_rdd,
plot_rdd,
rdd_sensitivity_bandwidth,
)
rng = np.random.default_rng(99)
n = 5000
cutoff = 0.0# margin of 0 = bare majority# running variable: Democratic vote margin (centred, in %)
margin = rng.normal(0, 15, n)
margin = np.clip(margin, -49, 49)
# fuzzy: incumbency take-up is 0.95 on winning side, 0 on losing side# (small non-compliance: some winners vacate seat before next election)
above = margin >= 0
prob_inc = np.where(above, 0.93, 0.0)
incumbent = rng.binomial(1, prob_inc, n).astype(float)
# true incumbency advantage: +15 pp in next election
noise = rng.normal(0, 20, n)
win_next = (
50 + 0.3 * margin + 15 * incumbent
+ noise
).clip(0, 100)
df = pd.DataFrame({
"win_next": win_next, "margin": margin, "incumbent": incumbent,
})
# ── Manipulation test ──
manip = test_manipulation(df["margin"].values, cutoff, n_bins=40)
print("Manipulation test:", manip["interpretation"])
# ── Sharp RDD (treating instrument as treatment) ──
sharp = run_sharp_rdd(df["win_next"].values, df["margin"].values, cutoff)
print(f"\nSharp (reduced-form) τ = {sharp['tau']:.3f} p = {sharp['p_value']:.4f}")
# ── Fuzzy RDD (LATE for compliers) ──
fuzzy = run_fuzzy_rdd(
df["win_next"].values, df["margin"].values,
df["incumbent"].values, cutoff
)
print(f"Fuzzy RDD LATE = {fuzzy['tau_fuzzy']:.3f} SE = {fuzzy['se']:.3f}"f" p = {fuzzy['p_value']:.4f}")
print(f"First-stage F = {fuzzy['first_stage_F']:.1f} "f"(rule of thumb: >10 for strong instrument)")
# ── Bandwidth sensitivity plot ──
sens = rdd_sensitivity_bandwidth(df["win_next"].values, df["margin"].values, cutoff)
fig, axes = plt.subplots(1, 2, figsize=(13, 5))
# RDD scatterfrom rdd_design import plot_rdd as _plot_rdd
fig2 = plot_rdd(
df["win_next"].values, df["margin"].values, cutoff,
title="Incumbency Advantage (Sharp RDD)",
ylabel="Vote share in next election (%)",
xlabel="Vote margin in current election (%)",
)
fig2.savefig("rdd_incumbency.png", dpi=150, bbox_inches="tight")
# bandwidth sensitivity
axes[0].fill_between(sens["bandwidth"], sens["ci_lower"], sens["ci_upper"],
alpha=0.25, color="#2c7bb6", label="95% CI")
axes[0].plot(sens["bandwidth"], sens["tau"], "o-", color="#2c7bb6",
linewidth=2, markersize=4, label="τ estimate")
axes[0].axhline(0, color="black", linestyle="--", linewidth=0.8)
axes[0].set_xlabel("Bandwidth (vote margin %)")
axes[0].set_ylabel("Estimated τ")
axes[0].set_title("Bandwidth sensitivity")
axes[0].legend()
# sample size by bandwidth
axes[1].plot(sens["bandwidth"], sens["n_left"], "s--", color="#d7191c",
label="N (control, left)")
axes[1].plot(sens["bandwidth"], sens["n_right"], "o--", color="#2c7bb6",
label="N (treated, right)")
axes[1].set_xlabel("Bandwidth")
axes[1].set_ylabel("Observations in window")
axes[1].set_title("Sample size vs bandwidth")
axes[1].legend()
plt.tight_layout()
fig.savefig("rdd_sensitivity.png", dpi=150, bbox_inches="tight")
print("\nSaved rdd_incumbency.png and rdd_sensitivity.png")
# ── Summary ──print(f"""
=== INCUMBENCY ADVANTAGE: SUMMARY ===
Sharp reduced-form τ : {sharp['tau']:.2f} pp
Fuzzy LATE : {fuzzy['tau_fuzzy']:.2f} pp
First-stage F : {fuzzy['first_stage_F']:.1f}
Bandwidth used : {sharp['bandwidth']:.2f} pp
Manipulation test : {manip['interpretation']}
""")
Validity Checks Checklist
Check
Method
Pass criterion
No manipulation
McCrary density test
p ≥ 0.05
Covariate balance
RDD on predetermined X
All p ≥ 0.10
Bandwidth stability
τ across h ∈ [0.5h*, 2h*]
τ stable, does not cross 0
Placebo cutoffs
Run RDD at c ± δ
No significant effects
Donut hole
Exclude observations near c
Estimate unchanged
Polynomial order
Compare p=1,2,3
Consistent estimates
When RDD Is and Is Not Valid
RDD is valid when:
The running variable is continuous at the threshold
Agents cannot precisely manipulate their value just above/below c
Only treatment status changes discontinuously at c (no other discontinuous policies)
RDD is not valid when:
Bunching in the density at c (e.g., teachers round borderline exam scores)
Multiple simultaneous treatment discontinuities at the same cutoff
The bandwidth is so narrow that N is too small for reliable local estimates
Extrapolation: the LATE applies only near the cutoff, not in the full population
References
Lee, D. S., & Lemieux, T. (2010). Regression discontinuity designs in economics.
Journal of Economic Literature, 48(2), 281–355.
Calonico, S., Cattaneo, M. D., & Titiunik, R. (2014). Robust nonparametric
confidence intervals for regression-discontinuity designs. Econometrica, 82(6).
Imbens, G., & Kalyanaraman, K. (2012). Optimal bandwidth choice for the regression
discontinuity estimator. Review of Economic Studies, 79(3), 933–959.
McCrary, J. (2008). Manipulation of the running variable in the regression
discontinuity design: A density test. Journal of Econometrics, 142(2), 698–714.