coresynth ships method-appropriate inference for each estimator, plus a cross-cutting conformal procedure. This article surveys them on a single synthetic panel.
set.seed(123)
N <- 12; TT <- 20; T_pre <- 12
f <- cumsum(rnorm(TT, 0, 0.5))
lam <- rnorm(N, 1, 0.3)
dat <- expand.grid(time = seq_len(TT), id = paste0("u", seq_len(N)))
dat$y <- as.vector(outer(f, lam)) + rnorm(nrow(dat), 0, 0.3)
dat$d <- as.integer(dat$id == "u1" & dat$time > T_pre)
dat$y[dat$d == 1] <- dat$y[dat$d == 1] + 2.0 # true ATT = 2.0
fit_scm <- scm_fit(y ~ d | id + time, data = dat, method = "scm")
fit_sdid <- scm_fit(y ~ d | id + time, data = dat, method = "sdid")
fit_gsc <- scm_fit(y ~ d | id + time, data = dat, method = "gsc", r = 2)
fit_si <- scm_fit(y ~ d | id + time, data = dat, method = "si")SCM — MSPE ratio permutation test
Abadie et al. (2010). mspe_ratio_pval()
compares the treated unit’s post/pre MSPE ratio against placebo ratios
from every donor. It returns a plain list.
mspe <- mspe_ratio_pval(fit_scm, alternative = "two.sided")
c(p_value = mspe$p_value, ratio_obs = mspe$ratio_obs)
#> p_value
#> 0.08333333The returned scm_placebo object has a
plot() method that reproduces the two companion figures of
the paper. type = "gaps" overlays the treated unit’s gap
path (treated minus synthetic control) on the placebo gap paths obtained
by reassigning the intervention to each donor (ADH 2010, Figure 4):
plot(mspe, type = "gaps")
Placebo units whose synthetic control fits poorly before treatment
carry no information about the rarity of a large post-treatment gap; ADH
exclude them with a relative pre-treatment MSPE cutoff (20x/5x/2x in
their Figures 5–7), available here as mspe_prune:
plot(mspe, type = "gaps", mspe_prune = 2)
type = "ratios" shows the post/pre-treatment MSPE ratio
of every unit (ADH 2010, Figure 8) — the statistic behind the
permutation p-value, which needs no pruning cutoff by construction:
plot(mspe, type = "ratios")
For staggered SCM fits, use
scm_inference() — the wild bootstrap of Ben-Michael, Feller
& Rothstein (2022) — described in the Staggered adoption article.
Series colors, legend text, and the reference lines are customizable
via colors, labels
(e.g. labels = c(treated = "California")),
vline, and hline — see
?plot.scm_placebo.
SDID — four inference methods
Arkhangelsky et al. (2021); Clarke et al. (2023).
sdid_inference() supports "placebo",
"bootstrap", "jackknife", and
"jackknife_global". The result is
broom-friendly.
inf_plac <- sdid_inference(fit_sdid, method = "placebo")
inf_boot <- sdid_inference(fit_sdid, method = "bootstrap", n_boot = 100, seed = 1)
tidy(inf_plac)
#> term estimate std.error statistic p.value conf.low conf.high method
#> 1 ATT 1.821899 0.09624075 18.93064 0.08333333 1.633271 2.010528 placebo
#> alternative n_controls staggered
#> 1 two.sided 11 FALSE
tidy(inf_boot)
#> term estimate std.error statistic p.value conf.low conf.high method
#> 1 ATT 1.821899 0.1287232 14.15362 1.773922e-45 1.60038 2.027952 bootstrap
#> alternative n_controls staggered
#> 1 two.sided 11 FALSEtidy() returns a one-row data frame with
estimate, std.error, p.value, and
a CI — ready to rbind() into a results table.
glance() gives a compact summary:
glance(inf_boot)
#> method n_controls staggered estimate std.error p.value conf.low
#> 1 bootstrap 11 FALSE 1.821899 0.1287232 1.773922e-45 1.60038
#> conf.high alternative n_boot_valid
#> 1 2.027952 two.sided 100GSC — parametric and non-parametric
Xu (2017). gsc_boot() is a parametric bootstrap
under H0 (sharp fits only); gsc_inference() offers
non-parametric bootstrap / jackknife.
gb <- gsc_boot(fit_gsc, B = 100, alpha = 0.05)
c(ci_lower = gb$ci_lower, ci_upper = gb$ci_upper, p_value = gb$p_value)
#> ci_lower ci_upper p_value
#> -0.3934672 0.2557330 0.0000000
tidy(gsc_inference(fit_gsc, method = "jackknife"))
#> term estimate std.error statistic p.value conf.low conf.high method
#> 1 ATT 1.966522 0.04177117 47.07845 0 1.884652 2.048392 jackknife
#> alternative n_controls staggered
#> 1 two.sided 11 FALSESI — bootstrap / jackknife
Agarwal et al. (2025). si_inference() mirrors
the GSC non-parametric API and also handles staggered and multi-arm
fits.
tidy(si_inference(fit_si, method = "bootstrap", n_boot = 100, seed = 1))
#> term estimate std.error statistic p.value conf.low conf.high method
#> 1 ATT 2.009438 0.05357169 37.50932 6.49154e-308 1.881553 2.080903 bootstrap
#> alternative n_controls staggered
#> 1 two.sided 11 FALSEConformal inference (any sharp fit)
Chernozhukov, Wüthrich & Zhu (2021).
conformal_inference() works across scm /
sdid / gsc / mc / si
sharp fits. Under H0: τ = τ0 it re-imputes the treated post-period as
Y1 - τ0, re-estimates the counterfactual on all T
periods, and inverts a moving-block permutation test of the
residuals to get a p-value and a confidence interval.
conf <- conformal_inference(fit_scm, tau0 = 0, level = 0.95)
tidy(conf)
#> term estimate std.error statistic p.value conf.low conf.high method
#> 1 ATT 1.698439 NA NA 0.1 -0.1072756 3.162243 conformal
#> alternative n_controls staggered
#> 1 two.sided 11 FALSEThe p-value tests the sharp null τ0 = 0; the CI is obtained by test inversion over a grid. Because the permutation uses |Π| = T cyclic shifts, the smallest attainable p-value is 1/T.
Assembling a results table
Since every inference object tidies to the same columns, comparing
methods is a single rbind():
do.call(rbind, list(
cbind(method = "sdid (placebo)", tidy(inf_plac)[c("estimate","p.value","conf.low","conf.high")]),
cbind(method = "sdid (bootstrap)", tidy(inf_boot)[c("estimate","p.value","conf.low","conf.high")]),
cbind(method = "scm (conformal)", tidy(conf)[c("estimate","p.value","conf.low","conf.high")])
))
#> method estimate p.value conf.low conf.high
#> 1 sdid (placebo) 1.821899 8.333333e-02 1.6332710 2.010528
#> 2 sdid (bootstrap) 1.821899 1.773922e-45 1.6003801 2.027952
#> 3 scm (conformal) 1.698439 1.000000e-01 -0.1072756 3.162243