Sharp Regression Discontinuity Design —————————————————–

Author

Metin Bulus

1 Install and load packages

install.packages(c("rdrobust",
                   "rddensity",
                   "lmtest",
                   "sandwich"))
library(rdrobust) 
library(rddensity)
library(lmtest)
library(sandwich)

packageVersion("rdrobust")
#> [1] '4.0.0'
packageVersion("rddensity")
#> [1] '3.0'
packageVersion("lmtest")
#> [1] '0.9.40'
packageVersion("sandwich")
#> [1] '3.1.1'

2 Load and check the data

sharp_data <- read.csv("C:/Users/.../sharp_data.csv")
head(sharp_data)
#>       AGNO    AGNO_c D      YKS
#> 1 44.64775 -25.35225 0 318.1068
#> 2 47.31709 -22.68291 0 323.1706
#> 3 51.43038 -18.56962 0 325.9956
#> 4 53.78935 -16.21065 0 345.2440
#> 5 57.87032 -12.12968 0 339.0280
#> 6 59.29873 -10.70127 0 342.5197

tail(sharp_data)
#>         AGNO   AGNO_c D      YKS
#> 95  85.29534 15.29534 1 428.4752
#> 96  86.53061 16.53061 1 422.0859
#> 97  86.91692 16.91692 1 434.5603
#> 98  87.67141 17.67141 1 429.4159
#> 99  90.51579 20.51579 1 447.7176
#> 100 91.61646 21.61646 1 427.7910

YKS: Öğrencinin Yüksek Öğretim Sınavı Puanı
AGNO: Öğrencinin Ağırlıklı Genel Not Ortalaması
AGNO_c: Öğrencinin eşik değerine (70) göre ortalanmış AGNO’su
D: Öğrencinin vakıftan burs alıp almadığı

3 Check manipulation of the running variable

Code
density_test <- rddensity(X = sharp_data$AGNO, # running variable 
                          c = 70)              # cutoff

summary(density_test)
#> 
#> Manipulation testing using local polynomial density estimation.
#> 
#> Number of obs =       100
#> Model =               unrestricted
#> Kernel =              triangular
#> BW method =           estimated
#> VCE method =          jackknife
#> 
#> c = 70                Left of c           Right of c          
#> Number of obs         51                  49                  
#> Eff. Number of obs    39                  23                  
#> Order est. (p)        2                   2                   
#> Order bias (q)        3                   3                   
#> BW est. (h)           7.921               6.287               
#> 
#> Method                T                   P > |T|             
#> Robust                0.3411              0.733               
#> 
#> 
#> P-values of binomial tests (H0: p=0.5).
#> 
#> Window Length              <c     >=c    P>|T|
#> 6.087     + 6.087          33      20    0.0984
#> 6.290     + 6.109          34      20    0.0759
#> 6.494     + 6.131          35      20    0.0581
#> 6.698     + 6.153          37      22    0.0674
#> 6.902     + 6.176          37      22    0.0674
#> 7.106     + 6.198          37      23    0.0925
#> 7.309     + 6.220          38      23    0.0722
#> 7.513     + 6.242          38      23    0.0722
#> 7.717     + 6.264          38      23    0.0722
#> 7.921     + 6.287          39      23    0.0559

rdplotdensity(rdd = density_test,  # rddenisty() result
              X = sharp_data$AGNO, 
              noPlot = TRUE) # running variable
#> $Estl
#> Call: lpdensity
#> 
#> Sample size                                      51
#> Polynomial order for point estimation    (p=)    2
#> Order of derivative estimated            (v=)    1
#> Polynomial order for confidence interval (q=)    3
#> Kernel function                                  triangular
#> Scaling factor                                   0.515151515151515
#> Bandwidth method                                 user provided
#> 
#> Use summary(...) to show estimates.
#> 
#> $Estr
#> Call: lpdensity
#> 
#> Sample size                                      49
#> Polynomial order for point estimation    (p=)    2
#> Order of derivative estimated            (v=)    1
#> Polynomial order for confidence interval (q=)    3
#> Kernel function                                  triangular
#> Scaling factor                                   0.494949494949495
#> Bandwidth method                                 user provided
#> 
#> Use summary(...) to show estimates.
#> 
#> $Estplot

4 Simple mean difference

4.1 Regression equation

\[ YKS_i = \beta_0 + \beta_1 D_i + \epsilon_i \]

4.2 Estimation


rd_mean <- lm(YKS ~ D,           # model
              data = sharp_data) # data 

coef(summary(rd_mean))
#>              Estimate Std. Error   t value      Pr(>|t|)
#> (Intercept) 358.64499   1.747540 205.22847 6.715047e-131
#> D            62.90158   2.496486  25.19605  1.319258e-44

4.3 Plot

5 Linear functional form - same regression line on both sides

5.1 Regression equation

\[ YKS_i = \beta_0 + \beta_1 D_i + \beta_2 AGNO_i + \epsilon_i \]

5.2 Estimation

rd_parallel <- lm(YKS ~ D + AGNO, 
                  data = sharp_data)

coef(summary(rd_parallel))
#>               Estimate Std. Error   t value     Pr(>|t|)
#> (Intercept) 254.407587 10.9705711 23.190004 2.365823e-41
#> D            40.134864  2.9842957 13.448689 6.771265e-24
#> AGNO          1.624911  0.1698836  9.564851 1.167380e-15

5.3 Plot

6 Linear functional form - different regression lines on both sides

6.1 Regression equation

\[ YKS_i = \beta_0 + \beta_1 D_i + \beta_2 AGNO_{c,i} + \beta_3 D_i * AGNO_{c,i} + \epsilon_i \]

6.2 Estimation

rd_lin <- lm(YKS ~ D + AGNO_c + I(D*AGNO_c), 
                  data = sharp_data)

coef(summary(rd_lin))
#>                 Estimate Std. Error    t value      Pr(>|t|)
#> (Intercept)   371.524724  1.7707948 209.806762 1.315444e-129
#> D              41.603947  2.8390879  14.653983  3.176609e-26
#> AGNO_c          2.201514  0.2246551   9.799531  4.009082e-16
#> I(D * AGNO_c)  -1.169992  0.3200142  -3.656064  4.179514e-04

6.3 Plot

7 Quadratic functional form - different regression lines on both sides

7.1 Regression equation

\[ YKS_i = \beta_0 + \beta_1 D_i + \beta_2 AGNO_{c,i} + \beta_3 AGNO_{c,i}^2 + \beta_4 D_i * AGNO_{c,i} + \beta_5 D_i * AGNO_{c,i}^2 + \epsilon_i \]

7.2 Estimation

rd_quad <- lm(YKS ~ D + AGNO_c + I(AGNO_c^2) + I(D*AGNO_c) +  I(D*AGNO_c^2), 
         data = sharp_data)

coef(summary(rd_quad))
#>                     Estimate Std. Error     t value      Pr(>|t|)
#> (Intercept)     372.81671614 2.58779043 144.0675844 4.515711e-112
#> D                39.86339303 4.17595749   9.5459288  1.697315e-15
#> AGNO_c            2.63252821 0.66488251   3.9593886  1.460167e-04
#> I(AGNO_c^2)       0.01979083 0.02870461   0.6894651  4.922292e-01
#> I(D * AGNO_c)    -1.46493163 1.01144536  -1.4483547  1.508454e-01
#> I(D * AGNO_c^2)  -0.02677161 0.04705384  -0.5689570  5.707423e-01

7.3 Plot

8 {rdrobust} bandwidth, estimation and plotting

8.1 Linear functional form

8.1.1 Bandwidth

bw_lin <- rdbwselect(y = sharp_data$YKS, # outcome
                 x = sharp_data$AGNO,   # running variables
                 c = 70,                # cutoff
                 vce = "hc0",           # robust standard error estimator
                 p = 1,                 # polynomial form (1 = linear)
                 kernel = "triangular", # triangular kernel weighting
                 bwselect = "mserd")$bws[1,1] 

# mserd: one common MSE-optimal bandwidth on two sides
# msetwo: two separate MSE-optimal bandwidth on two sides

bw_lin
#> [1] 5.372379

8.1.2 Estimation

rdrobust_lin <- rdrobust(y = sharp_data$YKS,  # outcome
                         x = sharp_data$AGNO, # running variable
                         c = 70,              # cutoff
                         p = 1,               # polynomial form (1 = linear)
                         vce = "hc0",         # robust standard error estimator
                         h = bw_lin)      # bandwidth 

summary(rdrobust_lin)
#> Call: rdrobust
#> 
#> Sharp RD estimates using local polynomial regression.
#> 
#> Number of Obs.                  100
#> BW type                      Manual
#> Kernel                   Triangular
#> VCE method                      HC0
#> 
#>                                Left        Right
#> Number of Obs.                   51           49
#> Eff. Number of Obs.              27           17
#> Order est. (p)                    1            1
#> Order bias (q)                    2            2
#> BW est. (h)                   5.372        5.372
#> BW bias (b)                   5.372        5.372
#> rho (h/b)                     1.000        1.000
#> Unique Obs.                      51           49
#> 
#> =====================================================================
#>                    Point    Robust Inference
#>                 Estimate         z     P>|z|      [ 95% C.I. ]       
#> ---------------------------------------------------------------------
#>      RD Effect    39.431     5.718     0.000    [26.783 , 54.720]    
#> =====================================================================

# estimate and standard error
c("tau" = rdrobust_lin$coef[1],
  "se" = rdrobust_lin$se[1])
#>       tau        se 
#> 39.431198  5.055529

8.1.3 Plot

8.2 Quadratic functional form

8.2.1 Bandwidth

bw_quad <- rdbwselect(y = sharp_data$YKS,    # outcome
                    x = sharp_data$AGNO,   # running variables
                    c = 70,                # cutoff
                    vce = "hc0",           # robust standard error estimator
                    p = 2,                 # polynomial form (2 = quadratic)
                    kernel = "triangular", # triangular kernel weighting
                    bwselect = "mserd")$bws[1,1] 

# mserd: one common MSE-optimal bandwidth on two sides
# msetwo: two separate MSE-optimal bandwidth on two sides

bw_quad
#> [1] 7.402737

8.2.2 Estimation

rdrobust_quad <- rdrobust(y = sharp_data$YKS,  # outcome
                          x = sharp_data$AGNO, # running variable
                          c = 70,              # cutoff
                          p = 2,               # polynomial form (1 = quadratic)
                          vce = "hc0",         # robust standard error estimator
                          h = bw_quad)      # bandwidth 

summary(rdrobust_quad)
#> Call: rdrobust
#> 
#> Sharp RD estimates using local polynomial regression.
#> 
#> Number of Obs.                  100
#> BW type                      Manual
#> Kernel                   Triangular
#> VCE method                      HC0
#> 
#>                                Left        Right
#> Number of Obs.                   51           49
#> Eff. Number of Obs.              38           27
#> Order est. (p)                    2            2
#> Order bias (q)                    3            3
#> BW est. (h)                   7.403        7.403
#> BW bias (b)                   7.403        7.403
#> rho (h/b)                     1.000        1.000
#> Unique Obs.                      51           49
#> 
#> =====================================================================
#>                    Point    Robust Inference
#>                 Estimate         z     P>|z|      [ 95% C.I. ]       
#> ---------------------------------------------------------------------
#>      RD Effect    41.205     5.055     0.000    [24.996 , 56.654]    
#> =====================================================================

# estimate and standard error
c("tau" = rdrobust_quad$coef[1],
  "se" = rdrobust_quad$se[1])
#>       tau        se 
#> 41.204934  6.103458

8.2.3 Plot

9 Back to lm() but add bandwidth

9.1 Bandwidth and kernel weights

# function to calculate kernel weights
triangular_kernel_weights <- function(x, cutoff = 0, bandwidth) {
  
  u <- (x - cutoff) / bandwidth           # standardized distance u
  K <- ifelse(abs(u) <= 1, 1 - abs(u), 0) # triangular kernel evaluation K(u)
  weights <- K / sum(K)                   # normalized weights (sum to 1)
  return(weights)
  
} # triangular_kernel_weights

Calculate the optimal bandwidth.

sharp_data_bw_lin <- subset(x = sharp_data, 
                           subset = abs(AGNO_c) <= bw_lin)

# calculate triangular kernel weights 

tkweights_bw_lin <- triangular_kernel_weights(x = sharp_data_bw_lin$AGNO_c,
                                       cutoff = 0, 
                                       bandwidth = bw_lin)

9.2 Plot kernel weights

9.3 Estimation

rd_lin <- lm(YKS ~ D + AGNO_c + I(D*AGNO_c), 
             weights = tkweights_bw_lin,
             data = sharp_data_bw_lin)

print(coeftest(rd_lin, vcov = vcovHC(rd_lin, type = "HC0")))
#> 
#> t test of coefficients:
#> 
#>                Estimate Std. Error  t value  Pr(>|t|)    
#> (Intercept)   374.81275    1.87841 199.5376 < 2.2e-16 ***
#> D              39.43120    5.05553   7.7996 1.481e-09 ***
#> AGNO_c          3.66923    0.84634   4.3354 9.557e-05 ***
#> I(D * AGNO_c)  -3.90765    1.86415  -2.0962   0.04244 *  
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

# compare to {rdronbust}

c("tau" = rdrobust_lin$coef[1],
  "se" = rdrobust_lin$se[1])
#>       tau        se 
#> 39.431198  5.055529

9.4 Plot

10 Bandwidth sensitivity analysis

10.1 Linear functional form with interaction and half optimal bandwidth

10.1.1 Re-estimate kernel weights

bw_lin_half <- bw_lin / 2

bw_lin_half
#> [1] 2.686189

sharp_data_bw_lin_half <- subset(x = sharp_data, 
                           subset = abs(AGNO_c) <= bw_lin_half)

# calculate triangular kernel weights 

tkweights_bw_lin_half  <- triangular_kernel_weights(x = sharp_data_bw_lin_half$AGNO_c,
                                                    cutoff = 0, 
                                                    bandwidth = bw_lin_half)

10.1.2 Estimation

rd_lin_bw_half <- lm(YKS ~ D + AGNO_c + I(D*AGNO_c), 
                     weights = tkweights_bw_lin_half,
                     data = sharp_data_bw_lin_half)

print(coeftest(rd_lin_bw_half, vcov = vcovHC(rd_lin_bw_half, type = "HC0")))
#> 
#> t test of coefficients:
#> 
#>               Estimate Std. Error  t value  Pr(>|t|)    
#> (Intercept)   375.1250     1.9502 192.3488 < 2.2e-16 ***
#> D              41.6082     7.6175   5.4622 2.862e-05 ***
#> AGNO_c          3.6316     1.5014   2.4187   0.02578 *  
#> I(D * AGNO_c)  -7.2880     6.7937  -1.0728   0.29682    
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

# compare

rdrobust_lin_bw_half <- rdrobust(y = sharp_data$YKS,  
                                 x = sharp_data$AGNO, 
                                 c = 70,              
                                 p = 1,               
                                 vce = "hc0",         
                                 h =  bw_lin_half)  

c("tau" = rdrobust_lin_bw_half$coef[1],
  "se" = rdrobust_lin_bw_half$se[1])
#>       tau        se 
#> 41.608209  7.617504

10.1.3 Plot

10.2 Linear functional form with interaction and twice optimal bandwidth

10.2.1 Re-estimate kernel weights

bw_lin_twice <- 2 * bw_lin

bw_lin_twice
#> [1] 10.74476

sharp_data_bw_lin_twice <- subset(x = sharp_data, 
                           subset = abs(AGNO_c) <=  bw_lin_twice)

# calculate triangular kernel weights 

tkweights_bw_lin_twice <- triangular_kernel_weights(x = sharp_data_bw_lin_twice$AGNO_c,
                                                    cutoff = 0, 
                                                    bandwidth =  bw_lin_twice)

10.2.2 Estimation

rd_lin_bw_twice <- lm(YKS ~ D + AGNO_c + I(D*AGNO_c), 
                      weights = tkweights_bw_lin_twice,
                      data = sharp_data_bw_lin_twice)

print(coeftest(rd_lin_bw_twice, vcov = vcovHC(rd_lin_bw_twice, type = "HC0")))
#> 
#> t test of coefficients:
#> 
#>               Estimate Std. Error  t value  Pr(>|t|)    
#> (Intercept)   373.0194     1.6875 221.0465 < 2.2e-16 ***
#> D              38.7804     3.9550   9.8055 4.439e-15 ***
#> AGNO_c          2.6038     0.4849   5.3699 8.498e-07 ***
#> I(D * AGNO_c)  -1.2346     0.8324  -1.4831    0.1422    
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

# compare 

rdrobust_lin_bw_twice <- rdrobust(y = sharp_data$YKS,  
                                  x = sharp_data$AGNO, 
                                  c = 70,              
                                  p = 1,               
                                  vce = "hc0",         
                                  h =  bw_lin_twice)  

c("tau" = rdrobust_lin_bw_twice$coef[1],
  "se" = rdrobust_lin_bw_twice$se[1])
#>       tau        se 
#> 38.780363  3.954964

10.2.3 Plot

11 Quadratic functional form with interaction and optimal bandwidth

11.1 Bandwidth and kernel weights

sharp_data_bw_quad <- subset(x = sharp_data, 
                           subset = abs(AGNO_c) <= bw_quad)

# calculate triangular kernel weights 

tkweights_bw_quad <- triangular_kernel_weights(x = sharp_data_bw_quad$AGNO_c,
                                               cutoff = 0, 
                                               bandwidth = bw_quad)

11.2 Estimation

rd_quad <- lm(YKS ~ D + AGNO_c + I(AGNO_c^2) + I(D*AGNO_c) + I(D*AGNO_c^2), 
              weights = tkweights_bw_quad,
              data = sharp_data_bw_quad)

print(coeftest(rd_quad, vcov = vcovHC(rd_quad, type = "HC0")))
#> 
#> t test of coefficients:
#> 
#>                  Estimate Std. Error  t value  Pr(>|t|)    
#> (Intercept)     375.19104    2.10566 178.1823 < 2.2e-16 ***
#> D                41.20493    6.10346   6.7511 7.128e-09 ***
#> AGNO_c            4.55342    1.78487   2.5511   0.01335 *  
#> I(AGNO_c^2)       0.28904    0.30626   0.9438   0.34913    
#> I(D * AGNO_c)    -8.51092    4.29929  -1.9796   0.05242 .  
#> I(D * AGNO_c^2)   0.55130    0.61159   0.9014   0.37103    
#> ---
#> Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

# compare to {rdronbust}

c("tau" = rdrobust_quad$coef[1],
  "se" = rdrobust_quad$se[1])
#>       tau        se 
#> 41.204934  6.103458

11.3 Plot