Difference-in-Differences and Instrumental Variables are powerful tools, but both require assumptions about unobservables — parallel trends, exclusion restrictions — that can be difficult to defend. This week we study a design where identification comes from something much more concrete: a cutoff.
San Diego is a large, prosperous US city. As you drive south through it, incomes decline a bit — maybe from \$85,000 to \$55,000 over 16 miles to the border district of San Ysidro. It’s a smooth, gradual gradient. Then you take one step over the US-Mexico border into Tijuana and household incomes crash to \$20,000. Nothing about geography or the people changes in that single step. What changes is the policy regime. That abrupt break — smooth trend, sudden jump — is the logic of regression discontinuity.
Whenever treatment is assigned based on whether a running variable crosses a threshold, individuals just on either side of that threshold are effectively randomly assigned. This is the core insight of Regression Discontinuity Design (RDD).
This lecture covers:
- The intuition behind regression discontinuity
- Key terminology: running variable, cutoff, bandwidth
- Sharp vs fuzzy RDD
- Implementing RDD by hand with OLS
- The “zoom in and go simple” approach: bandwidth and kernel weighting
- Using the
rdrobustpackage for local polynomial estimation
We follow Chapter 20 of The Effect closely and use its example data from Manacorda, Miguel, and Vigorito (2011) which is called gov_transfers.dta and is available on the list of datasets page.
The idea behind regression discontinuity
Imagine a government poverty program that gives cash transfers to households whose predicted income falls below a threshold. Households just barely below the cutoff receive the transfer; households just barely above it do not. These two groups are, in every meaningful sense, nearly identical — except one group received cash and the other did not.
This is the logic of RDD: compare observations just on either side of a cutoff in a running variable. If assignment to treatment is determined (or strongly influenced) by crossing that cutoff, and we believe that nothing else jumps at the same point, then any discontinuity in the outcome at the cutoff can be attributed to the treatment.
Connecting RDD to the identification toolkit
Recall from Week 8 that we identify causal effects by blocking back-door paths. In RDD, we don’t need to find and close every back door individually. Instead, by focusing on variation only at the cutoff, we isolate the front-door path:
RunningVariable → AboveCutoff → Treatment → Outcome
Everything else — all the back doors through unobservables — washes out because nothing else jumps at the cutoff. This is conceptually similar to what IV does (isolating a front-door path), but the source of identifying variation is the cutoff rather than an external instrument.
Key terminology
- Running variable (also called the forcing variable): the variable that determines treatment. For the poverty program, this is predicted income.
- Cutoff: the threshold value. Below the cutoff you get the transfer, above it you do not (or vice versa).
- Bandwidth: how far from the cutoff we are willing to look. Observations close to the cutoff are more comparable, but a narrow bandwidth means fewer observations and less precision. This is the fundamental bias-variance tradeoff in RDD.
Sharp vs fuzzy RDD
In a sharp RDD, the cutoff perfectly determines treatment: everyone below gets treated, everyone above does not. Treatment rates jump from 0% to 100% at the cutoff.
In a fuzzy RDD, crossing the cutoff changes the probability of treatment but does not determine it completely. For example, reaching pension eligibility age sharply increases the retirement rate, but not everyone retires the moment they become eligible. In this case, the cutoff is used as an instrument for treatment, and we scale our estimate using IV — exactly as we did in Week 13.
Implementing sharp RDD by hand
We use data from Manacorda, Miguel, and Vigorito (2011), which studies whether receiving government cash transfers in Uruguay increased political support for the incumbent government. The running variable (income_centered) is a predicted income score, already centered so the cutoff is at zero. The outcome (support) measures government approval. Treatment (participation) indicates receipt of the transfer.
Visualizing the discontinuity
Before estimating anything, we should graph the data. A plot of binned means is the standard RDD visual diagnostic — and it’s nearly compulsory (Chapter 20 emphasizes this). We slice the running variable into bins, compute the mean outcome in each bin, and look for a jump at the cutoff.
* load government transfers data
use "$data/gov_transfers.dta", clear
preserve
* create bins. 15 bins on either side of the cutoff from
* -.02 to .02, plus 0, means we want "steps" of...
local step = (.02)/15
egen bins = cut(income_centered), at(-.02(`step').02)
* means within bins
collapse (mean) support, by(bins)
* and graph with a cutoff line
twoway (line support bins) || ///
(function y = 0, horiz range(support)), ///
xti("Centered Income") yti("Support")
restore
You should see a visible jump at the cutoff: observations to the left (lower income, eligible for transfers) show higher government support than observations to the right. This is the visual signature of RDD — a smooth trend with an abrupt break.
Alternatively, there is a dedicated package for RDD analysis called rdrobust that includes a convenient command for generating these plots without having to collapse the data manually: rdplot. By default, rdplot will attempt to choose the optimal number of bins, but you can customize the plot to match our manual approach using options. You can use an if condition (or the subset() option) to restrict the range of the running variable, and the nbins() option to set the number of bins on each side of the cutoff.
We will explore the rest of the rdrobust package later in this lecture, but you can use rdplot now as a faster, more flexible alternative to the manual twoway approach:
* use rdplot to generate the binned-means graph, matching our manual plot
* by restricting to a bandwidth of .02 and using 15 bins per side
rdplot support income_centered if abs(income_centered) <= .02, c(0) nbins(15 15)
The OLS approach
The simplest way to estimate an RDD is to run a regression with the running variable, a treatment indicator, and their interaction. Crucially, we must center the running variable so that the intercept shift at zero gives us the treatment effect directly.
The linear model is:
\[Y_i = \beta_0 + \beta_1 (Running_i - Cutoff) + \beta_2 Treated_i + \beta_3 (Running_i - Cutoff) \times Treated_i + \varepsilon_i\]Since our running variable is already centered ($Cutoff = 0$), $\beta_2$ is the RDD estimate: the jump in the outcome at the cutoff. We allow the slope to differ on each side via the interaction $\beta_3$.
Notice the lack of control variables. This is intentional. The whole idea of RDD is that treatment assignment is effectively random at the cutoff. If that’s true, there are no open back doors to close — adding controls implies you don’t believe the design works. Controls are more common in fuzzy RDD (where there are other determinants of treatment) and can help with precision, but they should not be necessary for identification.
We can also use a second-order polynomial to allow for curvature:
* include the running variable, its square (by interaction with itself)
* and interactions of both with treatment, and robust standard errors
reg support i.participation##c.income_centered##c.income_centered, ///
robust
The coefficient on 1.participation is the RDD estimate: approximately 9.3 percentage points increase in government support.
A warning about polynomials: Gelman and Imbens (2019) show that high-order polynomials (third-order and above) should not be used in RDD. More flexible shapes sound appealing in principle, but they make increasingly wild predictions near the edges of the data — exactly where the cutoff sits. A fourth- or sixth-order polynomial can veer off dramatically at the cutoff, giving you a “precise” estimate that is entirely an artifact of the polynomial’s bad behavior. The recommended approach: never go above a second-order polynomial. If the shape is complex, zoom in with a bandwidth and use a linear model instead.
Adding a kernel weight and bandwidth
Rather than fitting a global polynomial, we can restrict our attention to observations close to the cutoff using a bandwidth and weight observations by their distance from the cutoff using a triangular kernel. This is the “zoom in and go simple” approach — use a linear model, but only on data near the cutoff. A triangular kernel gives full weight to observations right at the cutoff and linearly decreasing weight as you move away, reaching zero at the bandwidth boundary.
* create the triangular kernel weight. to start at a weight of 0 at x = 0,
* and impose a bandwidth of .01, we need a "slope" of -1/.01 = 100
* and to go in either direction use the absolute value
gen w = 1 - 100*abs(income_centered)
* if further away than .01, the weight is 0, not negative
replace w = 0 if w < 0
reg support i.participation##c.income_centered ///
[aw = w], robust
The kernel-weighted estimate here is smaller and less precise than the polynomial version. That’s the bias-variance tradeoff at work: by zooming in close, we reduce bias (observations far from the cutoff can’t distort our estimate), but we also lose observations. The data was already limited to a narrow range around the cutoff, so further restricting the bandwidth is aggressive here.
Using rdrobust
Manual RDD estimation requires many decisions: polynomial order, bandwidth, kernel shape, standard error correction. The rdrobust package (Calonico, Cattaneo, and Titiunik) handles all of these with data-driven defaults:
- Optimal bandwidth selection — automatically chooses the bandwidth that balances bias and variance
- Local polynomial regression — fits a local linear (or polynomial) regression with a triangular kernel
- Robust standard errors — uses a heteroskedasticity-robust estimator tailored to the RDD structure
- Bias correction — adjusts for the bias introduced by the optimal bandwidth procedure
Install rdrobust by adding it to your project.do package loop:
* install rdrobust
ssc install rdrobust, replace
* reload the data
causaldata gov_transfers.dta, use clear download
* run the RDD model and plot it. note, by default,
* rdrobust and rdplot use different
* numbers of polynomial terms.
* you can set the p() option to standardize them.
rdrobust support income_centered, c(0)
rdplot support income_centered, c(0)
rdrobust reports the conventional estimate, the bias-corrected estimate, and a robust confidence interval.
. rdrobust support income_centered, c(0)
Mass points detected in the running variable.
Sharp RD estimates using local polynomial regression.
Cutoff c = 0 | Left of c Right of c Number of obs = 1948
-------------------+---------------------- BW type = mserd
Number of obs | 1127 821 Kernel = Triangular
Eff. Number of obs | 291 194 VCE method = NN
Order est. (p) | 1 1
Order bias (q) | 2 2
BW est. (h) | 0.005 0.005
BW bias (b) | 0.010 0.010
rho (h/b) | 0.509 0.509
Unique obs | 841 639
Outcome: support. Running variable: income_centered.
-------------------------------------------------------------------------------
| Point | Robust Inference
| Estimate | z-stat P>|z| [95% Conf. Interval]
------------------+------------------------------------------------------------
RD Effect | .0247 | 0.6238 0.533 -.097391 .188325
-------------------------------------------------------------------------------
Estimates adjusted for mass points in the running variable.
There’s a lot going on here compared to standard OLS output. Let’s break it down:
- Data-driven Bandwidth:
BW type = mserdmeansrdrobustchose an optimal bandwidth using an MSE-optimal selector. The chosen bandwidthBW est. (h)is 0.005, which is quite narrow! - Effective Observations: Because the bandwidth is narrow, we’re throwing away a lot of data. The full sample
Number of obsis 1,948, but theEff. Number of obs(observations actually within the bandwidth on either side of the cutoff) is only 291 on the left and 194 on the right. - Kernel Weighting:
Kernel = Triangularmeans it’s applying a triangular kernel weight (giving more weight to observations closer to the cutoff), just like we did by hand. - Local Polynomial:
Order est. (p) = 1indicates it’s fitting a local linear regression (an order 1 polynomial). - The Estimate: The actual jump at the cutoff is the Point Estimate, which is
0.0247(or about 2.5 percentage points). This is much smaller than our manual OLS estimate. - Robust Inference: Because we threw away so much data by zooming in, our estimate is noisier. The z-statistic uses robust bias-corrected standard errors. Under these optimal settings, the effect has a p-value of
0.533— it is no longer statistically significant!
The rdplot command also produces a publication-quality binned-means graph with fitted polynomials on each side of the cutoff.
The output from rdrobust may differ substantially from our by-hand estimates — possibly even in sign — because it selects a potentially narrower bandwidth. This illustrates an important lesson: you should not blindly accept the defaults. Just because we have a pre-packaged command does not mean we can let it make all the decisions. Explore the sensitivity of your results to different bandwidths and polynomial orders.
Bandwidth sensitivity
In practice, researchers report how the RDD estimate changes across a range of bandwidths. This is exactly what you would do in a specification chart (Week 10) — vary an analytic choice and show whether the result is stable:
* check sensitivity to different bandwidths
foreach bw in .005 .008 .01 .015 .02 {
di as text "Bandwidth = `bw'"
rdrobust support income_centered, c(0) h(`bw')
}
If the estimate is fairly stable across bandwidths, that’s reassuring. If it swings wildly, you should think carefully about which bandwidth range is most credible given the context.
Summary
- Regression Discontinuity exploits a cutoff in a running variable to identify causal effects, by comparing observations just on either side of the threshold where treatment is effectively randomly assigned
- Use
rdrobustfor data-driven bandwidth selection and bias-corrected local polynomial estimation, but always check sensitivity to bandwidth choice and never use polynomials above second order