Regression Discontinuity in R: rdrobust Package for Causal Inference
Regression discontinuity (RD) is a causal inference method that compares units just above and just below a cutoff to estimate a treatment effect. The rdrobust R package by Calonico, Cattaneo, Farrell, and Titiunik gives you robust local-polynomial estimates, automatic bandwidth selection, and publication-ready plots for sharp and fuzzy designs in a handful of lines of code.
When can you use regression discontinuity in R?
Many causal questions share the same shape: a policy or program kicks in when a score crosses a threshold. Students above 70 get tutoring, firms below a size limit pay a lower tax, babies just under 1500 g get extra care. If the cutoff is strictly enforced and people cannot finely game their score, you can compare units just above and below it to estimate the effect. Let's simulate data where crossing a score of 50 causes a 5-unit jump, then plot it.
The plot shows binned means on each side of the cutoff with fitted polynomials. The visible vertical gap at x = 50 is the treatment effect we will estimate. If you cannot see a gap in the plot, an RD model will not rescue the design, so always plot first.
rdrobust is WebR-compatible. Every code block on this page runs directly in the browser, so you can change parameters and see results without installing anything locally.Try it: Change the cutoff to 60 in the simulation and rerun rdplot(). The jump location should move with it.
Click to reveal solution
Explanation: The running variable is unchanged, but the jump lives wherever the cutoff is. Moving c slides the treatment boundary along the x-axis.
How do you run a sharp RD with rdrobust()?
A sharp RD design is one where the treatment flips deterministically at the cutoff: everyone above gets it, everyone below does not. The full workflow has three steps: pick a bandwidth around the cutoff, fit a local polynomial on each side, and read the gap as the treatment effect. rdrobust handles all three, but seeing each step helps you trust the output.
Step one is bandwidth selection. A narrow bandwidth uses only observations very close to the cutoff (low bias, high variance). A wide bandwidth uses more data (lower variance, higher bias). The rdbwselect() function picks the mean-squared-error-optimal bandwidth automatically using the Calonico, Cattaneo, and Titiunik (2014) method.
The optimal bandwidth h is roughly 9.3 units on each side of the cutoff. That is the window rdrobust() will use by default when you fit the estimator.
Step two is the estimate itself. rdrobust() runs a local linear regression on each side of the cutoff within the chosen bandwidth, using a triangular kernel that gives more weight to observations closer to 50.
The Conventional coefficient of 5.01 is the estimated jump in the outcome at the cutoff, which matches the true 5 we simulated. The Eff. Number of Obs. row shows that only 187 and 183 observations on each side actually enter the weighted regression once the kernel and bandwidth are applied, even though the full dataset has 2000 rows.
Try it: Rerun rdrobust() with a quadratic polynomial by setting p = 2. The point estimate should move slightly, and the standard error should grow.
Click to reveal solution
Explanation: A quadratic fit uses more flexible curves on each side of the cutoff, so it can absorb real non-linearity but also overfit noise. That trade-off is why linear (p = 1) is the safer default for reporting.
How do you read rdrobust output?
The rdrobust() summary has three things you should read in order: the effective sample size, the bandwidth, and the three estimator rows. The effective sample size tells you how much data is actually driving the estimate. If it is under 50 observations per side, your confidence intervals will be wide and you should think hard before publishing.
The bandwidth (BW est. (h)) tells you the window used. If it is a huge fraction of the running variable's range, your design is basically a global regression, which defeats the point of RD. If it is tiny, variance explodes. The MSE-optimal choice tries to balance these.
Now the estimator rows. rdrobust() reports three:
- Conventional, standard local linear estimate and its CI. Point estimate is unbiased asymptotically, but the CI assumes the bandwidth was picked without any bias correction.
- Bias-Corrected, same point estimate after subtracting an estimated bias term from the polynomial fit.
- Robust, bias-corrected CI that also accounts for the variance of the bias correction itself.
The Calonico, Cattaneo, Titiunik papers recommend reporting the Conventional point estimate and the Robust confidence interval. That is the combination most journal reviewers expect. You can pull those values out of the fitted object directly for tables and reports.
The object stores matrices you can index directly, so you can pull point estimates and CIs into a results table without parsing text. This matters because every RD paper reports at least two model specifications, so scripting beats copy-pasting.
Try it: Compute the ratio of the Robust CI width to the Conventional CI width from rd_sharp. It should be greater than 1.
Click to reveal solution
Explanation: A ratio of about 1.2 means the robust CI is 20% wider. That extra width is the honesty premium for bias-corrected inference.
How is a fuzzy RD different, and how do you fit one?
In a sharp RD design, everyone above the cutoff gets treatment and no one below does. In practice, programs rarely work that cleanly. Some eligible people skip the program; some ineligible people sneak in. This is a fuzzy RD design: treatment probability jumps at the cutoff, but not from 0 to 1.
Fuzzy designs are handled by passing the actual treatment indicator (not assignment) to rdrobust() via the fuzzy= argument. The cutoff becomes an instrument for the probability of treatment, and the estimate is a local average treatment effect (LATE) for compliers, meaning units whose treatment status is decided by the cutoff.
Let's simulate a realistic fuzzy design where 80% of units above the cutoff take treatment and 15% of units below do.
The output has two blocks. The first-stage shows that crossing the cutoff increases the probability of treatment by about 0.65, matching the 80% − 15% compliance gap. The treatment effect block shows the LATE of about 5.1 on the outcome, very close to the true effect of 5 we injected for takers.
rdrobust output has two blocks, not one.* The first-stage block is the jump in treatment probability at the cutoff. The treatment effect block is the jump in outcome* divided by that first stage, which gives you the LATE for compliers.Try it: Change the compliance rate above the cutoff from 0.80 to 0.50 in the simulation and refit. The first-stage shrinks, so the LATE estimate gets noisier.
Click to reveal solution
Explanation: Weaker compliance means the cutoff is a weaker instrument, so the denominator in the LATE (the first stage) gets smaller and the standard error gets larger. Always report the first-stage F-statistic or coefficient so readers can judge instrument strength.
How do you validate RD assumptions?
An RD estimate is only credible if three things are true: the running variable is not manipulated around the cutoff, no other policy changes at the same threshold, and the outcome trend is smooth enough that a polynomial captures it. You cannot prove any of these, but you can run tests that would reveal the worst violations.
The most common check is the density test, which asks whether the running variable has a suspicious spike just on one side of the cutoff. If people can game their score to get into the treated group, you will see bunching. The rddensity package implements the Cattaneo, Jansson, and Ma (2020) test, which is the modern successor to the McCrary (2008) test.
A p-value of 0.68 (well above 0.05) means we cannot reject the null that the density is continuous at the cutoff. That is exactly what we want. If this p-value were below 0.05, it would suggest units are sorting around the threshold, and the RD estimate would be biased.
A second check is the placebo cutoff test. The treatment effect should only appear at the real cutoff. Pick a fake threshold where no treatment exists (say 40), and refit. If you get a significant jump, something is generating discontinuities that are not the treatment.
A point estimate of 0.25 with a p-value of 0.75 is a clean null. No jump where no jump should be, which strengthens the credibility of the real estimate at 50.
The third check is bandwidth sensitivity. Rerun rdrobust() with halved and doubled bandwidths. If the point estimate wobbles wildly, the MSE-optimal choice is a lucky sweet spot, not a signal.
All three coefficients land in the 4.9 to 5.2 range, so the estimate is stable across bandwidths. The standard error shrinks as the bandwidth grows (more data, less noise) but at the cost of more bias, which is why the MSE-optimal choice exists.
Try it: Run a placebo at c = 60 on sharp_df and verify the estimate is close to zero with a non-significant p-value.
Click to reveal solution
Explanation: At a fake cutoff far from the real one, the local polynomial on each side is just fitting noise. The expected coefficient is zero, and a significant jump would mean your smoothing assumption is wrong.
Practice Exercises
Exercise 1: Full sharp RD workflow
Simulate 1500 observations with a running variable uniform on [0, 100], cutoff = 55, and a true jump of 3 units in the outcome. Run rdplot(), pick the MSE-optimal bandwidth with rdbwselect(), and fit the estimator with rdrobust(). Report the Robust 95% CI.
Click to reveal solution
Explanation: The Robust CI contains the true effect of 3. The point estimate comes from the Conventional row; the CI to report is the Robust row.
Exercise 2: Fuzzy vs intent-to-treat
Using fuzzy_df from earlier, fit two models: (a) a fuzzy RD with fuzzy = fuzzy_df$take_up, and (b) a sharp RD ignoring take-up (the intent-to-treat effect). Compare the two point estimates. The fuzzy estimate should be larger because it scales by the first stage.
Click to reveal solution
Explanation: The intent-to-treat (ITT) effect is the jump in outcome for crossing the eligibility line, regardless of whether people actually took the treatment. Dividing ITT by the first-stage jump in treatment probability (about 0.65) recovers the LATE of about 5.1. This is the Wald estimator interpretation of fuzzy RD.
Complete Example
Here is a full workflow on a simulated policy evaluation. A job-training program is offered to counties whose median income falls below \$50k, and we want to know whether it raised the employment rate. The running variable is median income, the cutoff is 50, and the outcome is a 12-month change in employment rate.
The running variable runs from \$20k to \$80k. Eligible counties (below \$50k) show a take-up of about 72%; ineligible counties show around 8%. Now visualise and fit the fuzzy RD.
The first stage is a 0.64 jump in take-up. The LATE is 1.79 percentage points of employment change per compliant county, almost exactly the 1.8 we injected. The robust 95% CI excludes zero, so we can report that the training program raised employment for counties on the margin.
Always close an RD analysis with the validation suite:
Both checks pass: no sorting around \$50k, and no spurious jump at a fake cutoff. The estimated 1.8-point gain is credible within the modelling assumptions.
Summary
| Step | Function | What it does |
|---|---|---|
| Visualise | rdplot() |
Binned-mean plot with fitted polynomials on each side |
| Bandwidth | rdbwselect() |
MSE-optimal window around the cutoff |
| Estimate | rdrobust() |
Local linear regression with triangular kernel |
| Validate | rddensity() + placebo + sensitivity |
Check continuity, placebo cutoffs, bandwidth stability |
Key takeaways:
- Report the Conventional point estimate and the Robust 95% CI.
- Use
fuzzy=when compliance is imperfect; read the two-block output carefully. - Always plot first and always validate. A density discontinuity or unstable point estimate is a red flag.
- The RD effect is a local average treatment effect at the cutoff, not the population-average effect. Policy generalisation needs a separate argument.
References
- Calonico, S., Cattaneo, M. D., Farrell, M. H., & Titiunik, R., rdrobust: Software for Regression-Discontinuity Designs. R Journal (2015). Link
- Cattaneo, M. D., Idrobo, N., & Titiunik, R., A Practical Introduction to Regression Discontinuity Designs: Foundations. arXiv:1911.09511 (2019). Link
- Calonico, S., Cattaneo, M. D., & Titiunik, R., Robust Nonparametric Confidence Intervals for Regression-Discontinuity Designs. Econometrica (2014). Link
- rdrobust CRAN page. Link
- RD Packages project. Link
- Lee, D. S., & Lemieux, T., Regression Discontinuity Designs in Economics. Journal of Economic Literature (2010). Link
- Cattaneo, M. D., Jansson, M., & Ma, X., Simple Local Polynomial Density Estimators. Journal of the American Statistical Association (2020). Link
Continue Learning
- Multiple Regression in R, the base linear model that RD fits locally on each side of the cutoff.
- Interpreting Regression Output Completely, how to read coefficients, standard errors, and p-values from any R regression summary.
- Logistic Regression in R, when your outcome is binary rather than continuous, and you still want a clean causal comparison.