Causal Inference IV(b) — RD Validity & Falsification (R companion)¶

rdrobust and rddensity for the credibility suite¶

The same falsification and robustness checks with the CCT authors' own R packages: rdrobust for covariate-continuity tests, placebo cutoffs, donut-hole and bandwidth estimation, and rddensity for the manipulation test. On a valid vs a deliberately manipulated simulated design and the real Lee (2008) data, R reproduces the Python results — the covariate-continuity and density tests catch sorting, the incumbency jump appears only at the true cutoff, and the estimate survives the donut-hole.

1. Covariate continuity and the manipulation test¶

Run rdrobust with a predetermined covariate as the outcome: a jump signals sorting. In the valid design there is none; in the manipulated design (high-covariate units sorting just above the cutoff) both the covariate-continuity RD and the rddensity manipulation test fire.

In [1]:
suppressMessages({library(rdrobust); library(rddensity)})
set.seed(0); n<-5000; tau<-5
X<-runif(n,-1,1); W<-1.5*X+rnorm(n); Y<-2*X+tau*(X>=0)+0.5*W+rnorm(n)
Xm<-X; med<-median(W[X>-0.2 & X<0]); mask<-(X>-0.2)&(X<0)&(W>med); Xm[mask]<-Xm[mask]+0.22
rv<-rdrobust(W,X,c=0); rm_<-rdrobust(W,Xm,c=0)
cat(sprintf("covariate-continuity (W as outcome):\n  VALID       W jump %+.3f (robust p=%.3f) -> PASS\n  MANIPULATED W jump %+.3f (robust p=%.4f) -> FAIL\n",
            rv$coef[1], rv$pv[3], rm_$coef[1], rm_$pv[3]))
cat(sprintf("\nmanipulation (rddensity) p:  VALID %.3f (pass)   MANIPULATED %.4f (fail)\n",
            rddensity(X,c=0)$test$p_jk, rddensity(Xm,c=0)$test$p_jk))
Warning message:
"package 'rdrobust' was built under R version 4.6.1"
Warning message:
"package 'rddensity' was built under R version 4.6.1"
covariate-continuity (W as outcome):
  VALID       W jump -0.006 (robust p=0.964) -> PASS
  MANIPULATED W jump +1.178 (robust p=0.0000) -> FAIL
manipulation (rddensity) p:  VALID 0.213 (pass)   MANIPULATED 0.0013 (fail)

2. Placebo cutoffs on the Lee (2008) data¶

Re-run rdrobust at fake cutoffs. A valid RD shows a jump only at the true threshold (margin = 0); the incumbency effect is absent at ±0.15 and ±0.30.

In [2]:
options(repr.plot.width=8.5, repr.plot.height=4.2)
d<-read.csv("lee2008.csv"); x<-d$margin; y<-d$dem_voteshare_next
cutoffs<-c(-0.30,-0.15,0,0.15,0.30); est<-c(); pv<-c()
for(cc in cutoffs){ sub<-if(cc==0) rep(TRUE,length(x)) else (x>cc-0.35 & x<cc+0.35)
  r<-rdrobust(y[sub],x[sub],c=cc); est<-c(est,r$coef[1]); pv<-c(pv,r$pv[3]) }
print(data.frame(cutoff=cutoffs, RD.jump=round(est,3), robust.p=round(pv,3)))
bp<-barplot(est, names.arg=cutoffs, col=ifelse(cutoffs==0,"#c53030","#a0aec0"),
            xlab="cutoff (0 = true threshold)", ylab="RD jump", main="Placebo cutoffs: significant jump only at the true threshold")
text(bp, est+0.003, sprintf("p=%.2f",pv), cex=.8)
Warning message in rdrobust(y[sub], x[sub], c = cc):
"Mass points detected in the running variable."
Warning message in rdrobust(y[sub], x[sub], c = cc):
"Mass points detected in the running variable."
Warning message in rdrobust(y[sub], x[sub], c = cc):
"Mass points detected in the running variable."
Warning message in rdrobust(y[sub], x[sub], c = cc):
"Mass points detected in the running variable."
Warning message in rdrobust(y[sub], x[sub], c = cc):
"Mass points detected in the running variable."
  cutoff RD.jump robust.p
1  -0.30  -0.001    0.951
2  -0.15  -0.007    0.747
3   0.00   0.064    0.000
4   0.15   0.012    0.539
5   0.30  -0.027    0.180
No description has been provided for this image

3. Donut-hole and bandwidth robustness¶

Exclude observations within $\pm\delta$ of the cutoff (donut-hole) and vary the bandwidth. The Lee incumbency estimate stays positive and significant throughout — robust to the points nearest the boundary and to the bandwidth choice.

In [3]:
options(repr.plot.width=13, repr.plot.height=4.2); par(mfrow=c(1,2))
deltas<-c(0,0.005,0.01,0.02,0.05); de<-c(); dp<-c()
for(dl in deltas){ keep<-abs(x)>=dl; r<-rdrobust(y[keep],x[keep],c=0); de<-c(de,r$coef[1]); dp<-c(dp,r$pv[3]) }
plot(deltas, de, type="b", pch=19, col="#2b6cb0", lwd=2, xlab="donut radius delta", ylab="RD estimate",
     main="Donut-hole: estimate survives dropping near-cutoff points", ylim=c(0,max(de)*1.2)); abline(h=0,col="grey")
text(deltas, de, sprintf("p=%.2f",dp), pos=3, cex=.7)
hs<-seq(0.05,0.4,length.out=12); be<-sapply(hs, function(h) rdrobust(y,x,c=0,h=h)$coef[1])
plot(hs, be, type="b", pch=15, col="#2f855a", lwd=2, xlab="bandwidth h", ylab="RD estimate", main="Bandwidth sensitivity"); abline(h=0,col="grey")
par(mfrow=c(1,1))
cat(sprintf("donut delta 0->0.05: RD %.3f -> %.3f (all p<0.05); bandwidth-stable and positive.\n", de[1], de[length(de)]))
Warning message in rdrobust(y[keep], x[keep], c = 0):
"Mass points detected in the running variable."
Warning message in rdrobust(y[keep], x[keep], c = 0):
"Mass points detected in the running variable."
Warning message in rdrobust(y[keep], x[keep], c = 0):
"Mass points detected in the running variable."
Warning message in rdrobust(y[keep], x[keep], c = 0):
"Mass points detected in the running variable."
Warning message in rdrobust(y[keep], x[keep], c = 0):
"Mass points detected in the running variable."
Warning message in rdrobust(y, x, c = 0, h = h):
"Mass points detected in the running variable."
Warning message in rdrobust(y, x, c = 0, h = h):
"Mass points detected in the running variable."
Warning message in rdrobust(y, x, c = 0, h = h):
"Mass points detected in the running variable."
Warning message in rdrobust(y, x, c = 0, h = h):
"Mass points detected in the running variable."
Warning message in rdrobust(y, x, c = 0, h = h):
"Mass points detected in the running variable."
Warning message in rdrobust(y, x, c = 0, h = h):
"Mass points detected in the running variable."
Warning message in rdrobust(y, x, c = 0, h = h):
"Mass points detected in the running variable."
Warning message in rdrobust(y, x, c = 0, h = h):
"Mass points detected in the running variable."
Warning message in rdrobust(y, x, c = 0, h = h):
"Mass points detected in the running variable."
Warning message in rdrobust(y, x, c = 0, h = h):
"Mass points detected in the running variable."
Warning message in rdrobust(y, x, c = 0, h = h):
"Mass points detected in the running variable."
Warning message in rdrobust(y, x, c = 0, h = h):
"Mass points detected in the running variable."
donut delta 0->0.05: RD 0.064 -> 0.093 (all p<0.05); bandwidth-stable and positive.
No description has been provided for this image

4. Summary¶

rdrobust and rddensity reproduced the full RD credibility suite: the covariate-continuity RD and the manipulation test caught the sorted (manipulated) design and passed the valid one; placebo cutoffs confirmed the Lee incumbency jump is specific to the true threshold; and donut-hole and bandwidth checks showed the estimate is robust. The rule to carry: report all of these alongside any RD point estimate — a discontinuity is causal evidence only after it survives the falsification suite. Cross-links: covariate-continuity ↔ matching balance (subsection 2); placebo cutoffs ↔ placebo/permutation inference (experiments, synthetic control). This completes the depth of the RD subsection.