Skip to contents

Compute Manski–type lower/upper bounds on the causal effect at a Regression Discontinuity Design (sharp treatment assignment) when the running variable can be strategically manipulated to the right of the cutoff. The implementation mirrors Algorithm 1 in Rosenman et al. (2025) and is the foundation for the fuzzy variant bounds_fuzzy().

Usage

bounds_sharp(
  x,
  y,
  cutoff,
  true_counts,
  weights = NULL,
  poly_order = 1L,
  bounds = c("both", "lower", "upper"),
  solver = getOption("rdpartial.solver", "ECOS"),
  ...
)

Arguments

x

Numeric running variable.

y

Numeric outcome variable.

cutoff

Numeric threshold separating control (< cutoff) from treatment (>= cutoff).

true_counts

Data.frame with columns x (support points >= cutoff) and n_true (estimated number of non-manipulated observations at each support point).

weights

Numeric vector of observation weights (optional).

poly_order

Integer polynomial order for local regression (default 1L).

bounds

Character – which bound(s) to return; one of "both" (default), "lower", "upper".

solver

Character – CVXR solver (default taken from getOption("rdpartial.solver"); package default is "ECOS").

...

Further arguments passed on to CVXR::solve(). Use this to override CVXR tolerances or provide a different solver control list.

Value

A numeric vector of length 2 (lower, upper) when bounds = "both"; otherwise a single numeric. The full CVXR solution objects are attached as attributes opt_lwr / opt_upr for diagnostic purposes.

Required Inputs

  • x – numeric running variable.

  • y – outcome measured under realised treatment status (because the design is sharp, no separate z argument is needed).

  • cutoff – numeric threshold separating control (< cutoff) from treatment (≥ cutoff).

  • true_countsdata.frame with columns x (support points ≥ cutoff) and n_true (estimated number of non‑manipulated observations at each support point). Typically produced by .density_estimation().

Examples

set.seed(123)
n  <- 4000
x  <- rpois(n, lambda = 15)
y0 <- 0.1 * x + 0.002 * x^2 + rnorm(n)
y1 <- y0 + 1                      # treatment adds 1
cutoff <- 16
y <- ifelse(x >= cutoff, y1, y0)  # sharp assignment

# Assume 90 % of post‑cutoff mass is genuine
tc <- data.frame(
  x      = cutoff:max(x),
  n_true = round(tabulate(x[x >= cutoff] + 1L) * 0.9)
)

bounds_sharp(x, y, cutoff, true_counts = tc)
#>     lower     upper 
#> -2.057823 -2.057823 
#> attr(,"opt_lwr")
#>  Status: optimal
#>  Objective value: -2.057823
#>  Solver: ECOS
#> → <me>$getValue(x) returns value of variable/constraint x
#> attr(,"opt_upr")
#>  Status: optimal
#>  Objective value: -2.057823
#>  Solver: ECOS
#> → <me>$getValue(x) returns value of variable/constraint x