| Title: | Fast and Unified Synthetic Control Methods |
| Version: | 1.0.1 |
| Description: | A unified 'Formula' interface to the Synthetic Control Method (SCM) and related panel-data causal inference estimators: Synthetic Difference-in-Differences (SDID), Generalized Synthetic Control (GSC), Matrix Completion (MC), Time-Aware Synthetic Control (TASC), and Synthetic Interventions (SI), together with an experimental-design variant. Computational bottlenecks (quadratic programming, singular value decomposition, and Kalman filtering) are implemented in 'C++' via 'RcppArmadillo'. Methods are described in Abadie, Diamond and Hainmueller (2010) <doi:10.1198/jasa.2009.ap08746>, Arkhangelsky, Athey, Hirshberg, Imbens and Wager (2021) <doi:10.1257/aer.20190159>, Xu (2017) <doi:10.1017/pan.2016.2>, Athey, Bayati, Doudchenko, Imbens and Khosravi (2021) <doi:10.1080/01621459.2021.1891924>, and Agarwal, Shah and Shen (2025) <doi:10.1287/opre.2025.1590>. |
| License: | MIT + file LICENSE |
| URL: | https://github.com/yo5uke/coresynth, https://yo5uke.com/coresynth/ |
| BugReports: | https://github.com/yo5uke/coresynth/issues |
| Encoding: | UTF-8 |
| Config/roxygen2/markdown: | TRUE |
| Depends: | R (≥ 4.1.0) |
| LinkingTo: | Rcpp, RcppArmadillo |
| Imports: | Rcpp, Formula, ggplot2, broom, jsonlite |
| Suggests: | testthat (≥ 3.0.0), knitr, rmarkdown |
| VignetteBuilder: | knitr |
| Config/testthat/edition: | 3 |
| Config/roxygen2/version: | 8.1.0 |
| NeedsCompilation: | yes |
| Packaged: | 2026-10-10 14:23:00 UTC; yo5uk |
| Author: | Yosuke Abe [aut, cre] |
| Maintainer: | Yosuke Abe <yosuke.abe0507@gmail.com> |
| Repository: | CRAN |
| Date/Publication: | 2026-10-11 13:40:54 UTC |
coresynth: Fast and Unified Synthetic Control Methods
Description
A unified 'Formula' interface to the Synthetic Control Method (SCM) and related panel-data causal inference estimators: Synthetic Difference-in-Differences (SDID), Generalized Synthetic Control (GSC), Matrix Completion (MC), Time-Aware Synthetic Control (TASC), and Synthetic Interventions (SI), together with an experimental-design variant. Computational bottlenecks (quadratic programming, singular value decomposition, and Kalman filtering) are implemented in 'C++' via 'RcppArmadillo'. Methods are described in Abadie, Diamond and Hainmueller (2010) doi:10.1198/jasa.2009.ap08746, Arkhangelsky, Athey, Hirshberg, Imbens and Wager (2021) doi:10.1257/aer.20190159, Xu (2017) doi:10.1017/pan.2016.2, Athey, Bayati, Doudchenko, Imbens and Khosravi (2021) doi:10.1080/01621459.2021.1891924, and Agarwal, Shah and Shen (2025) doi:10.1287/opre.2025.1590.
Parallel computation
The in-space placebo refits of mspe_ratio_pval() and sdid_inference(),
and the multi-start search of scm_fit(), run in parallel with OpenMP.
Set options(coresynth.threads = n) to cap the number of threads. Left
unset, the OpenMP default applies (all cores, or OMP_NUM_THREADS /
OMP_THREAD_LIMIT when set), except under R CMD check --as-cran, where at
most two threads are used. The results do not depend on the thread count.
Author(s)
Maintainer: Yosuke Abe yosuke.abe0507@gmail.com
Authors:
Yosuke Abe yosuke.abe0507@gmail.com
See Also
Useful links:
Report bugs at https://github.com/yo5uke/coresynth/issues
Augmented Synthetic Control Method (Ridge ASCM)
Description
Applies a ridge-regression-based bias correction to a fitted SCM object, following Ben-Michael, Feller & Rothstein (2021, JASA). The corrected estimator is:
Usage
augment_scm(fit, lambda_ridge = NULL)
Arguments
fit |
A |
lambda_ridge |
Ridge penalty (non-negative). |
Details
tau_aug = tau_SCM + (m_tr_post - sum_j W_j * m_j_post)
where m_i_post = Y_pre_i' beta_hat is the ridge outcome model prediction for unit i's mean post-treatment outcome, and beta_hat is estimated by ridge regression across control units.
Value
A list with:
-
att_aug: Augmented ATT estimate -
delta: Bias correction term (m_tr_post - sum_j W_j m_j_post) -
att_scm: Original SCM ATT for comparison -
lambda_ridge: Ridge penalty used -
beta_hat: Ridge regression coefficients (length T_pre)
Conformal Inference for Synthetic Control Estimators
Description
Implements the permutation-based conformal inference procedure of
Chernozhukov, Wuthrich & Zhu (2021, JASA). The test inverts a sharp null
H_0: \tau = \tau_0 by imputing the treated post-treatment
counterfactual as Y_{1t} - \tau_0, re-estimating the counterfactual
proxy on all T periods (imposing the null), and computing a
moving-block permutation p-value from the estimated residuals. A confidence
interval is obtained by test inversion over a grid of candidate \tau_0.
Usage
conformal_inference(
fit,
tau0 = 0,
q = 1,
alternative = c("two.sided", "greater", "less"),
ci = TRUE,
level = 0.95,
grid = NULL,
n_grid = 200L,
grid_mult = 4,
...
)
Arguments
fit |
A |
tau0 |
Null value of the ATT for the reported p-value (default 0). |
q |
Exponent of the |
alternative |
|
ci |
Logical; construct a confidence interval by test inversion
(default |
level |
Confidence level for the interval (default 0.95). |
grid |
Optional numeric vector of candidate |
n_grid |
Number of grid points when |
grid_mult |
Half-width multiplier when |
... |
Unused. |
Details
Supported for sharp (single-cohort) fits with method in
c("scm", "sdid", "gsc", "mc", "si"). Staggered, multi-arm, and tasc
fits are not supported (use sdid_inference(), gsc_inference(), or
si_inference() instead).
Value
A list of class c("conformal_inference", "coresynth_inference")
with estimate, se (NA; conformal has no SE), p_value (at tau0),
ci_lower, ci_upper, method ("conformal"), n_controls,
alternative, staggered (FALSE), plus tau0, q, grid, and
p_grid (p-values along the grid). Compatible with tidy() / glance().
References
Chernozhukov, V., Wuthrich, K., & Zhu, Y. (2021). An Exact and Robust Conformal Inference Method for Counterfactual and Synthetic Controls. Journal of the American Statistical Association, 116(536), 1849-1864.
Export coresynth Results to JSON
Description
Generates a comprehensive, standardized JSON record covering all six
estimators. Suitable for reproducibility workflows (Xu & Yang 2026) and
downstream tooling. Pass the result of mspe_ratio_pval() or gsc_boot()
via the inference argument to include inference results.
Usage
export_json(x, file = "coresynth_results.json", inference = NULL, digits = 6L)
Arguments
x |
A |
file |
Output file path. Default |
inference |
Optional list from |
digits |
Number of significant digits applied to numeric values (default 6L). |
Value
Invisibly, the R list that was (or would be) serialized.
Glance at an inference result
Description
One-row summary of a coresynth_inference (or sdid_inference) object.
Usage
## S3 method for class 'coresynth_inference'
glance(x, ...)
Arguments
x |
An inference object. |
... |
Unused. |
Value
A one-row data.frame with columns method, n_controls,
staggered, estimate, std.error, p.value, conf.low,
conf.high, alternative, n_boot_valid.
Parametric Bootstrap Inference for GSC (Xu 2017 S.3)
Description
Generates the null distribution of the ATT under H0 (no treatment effect) by parametric resampling from the estimated IFE factor model. Under H0, both the control panel and treated unit are generated from the fitted factor model with homoskedastic noise. When the fit includes covariate adjustment (beta), the covariate contribution is included in the simulated DGP and re-estimated in each bootstrap replicate.
Usage
gsc_boot(fit, B = 499L, alpha = 0.05, seed = NULL)
Arguments
fit |
A |
B |
Bootstrap replications (default 499L). |
alpha |
Significance level for the confidence interval (default 0.05). |
seed |
RNG seed for reproducibility (default NULL). |
Value
A list with:
-
p_value: Two-sided p-value: mean(|ATT*| >= |ATT_obs|) -
ci_lower: Lower bound of the (1-alpha)*100% CI for the ATT,ATT_obs - q(1 - alpha/2)whereqare quantiles ofboot_dist -
ci_upper: Upper bound,ATT_obs - q(alpha/2) -
se: Bootstrap standard error -
boot_dist: Numeric vector of length B (bootstrap ATT* values) -
att_obs: Observed ATT from the original fit
Fast Interactive Fixed Effects (IFE) for Generalized Synthetic Control
Description
Implements Xu (2017) IFE model with optional covariate adjustment. When X_co has p > 0 slices, runs an EM loop alternating between: E-step: truncated SVD of Y_tilde = Y_co - X_co * beta M-step: panel OLS to update beta given current factors When X_co has 0 slices (default), falls back to the plain 3-step estimator.
Usage
gsc_ife_cpp(Y_co, Y_tr_pre, r, X_co, X_tr_pre, max_iter = 50L, tol = 1e-06)
Arguments
Y_co |
Control units outcome matrix (T x N_co) |
Y_tr_pre |
Treated units pre-treatment outcomes (T_pre x N_tr) |
r |
Number of latent factors (must be <= min(T, N_co)) |
X_co |
Time-varying covariate cube (T x N_co x p). Pass an empty cube (0 slices) for the covariate-free estimator. |
X_tr_pre |
Time-varying covariate cube for treated units in the pre-treatment window (T_pre x N_tr x p). Required for correct Step 2 loading estimation per Xu (2017): lambda_hat is estimated from Y_tr_pre - X_tr_pre * beta (covariate- demeaned). Pass an empty cube (0 slices) to skip demeaning (backward-compatible, but biased when beta != 0). |
max_iter |
Maximum EM iterations (default 50) |
tol |
Convergence tolerance on relative beta change (default 1e-6) |
Value
A list with components:
-
F: estimated time factors (T x r). -
L_co: control-unit factor loadings (N_co x r). -
L_tr: treated-unit factor loadings (N_tr x r). -
Y_tr_hat: estimated treated-unit counterfactual outcomes (T x N_tr). -
singular_values: singular values from the final truncated SVD. -
beta: estimated covariate coefficients (p x 1), empty when no covariates are supplied.
Non-parametric Inference for GSC (Xu 2017)
Description
Estimates SE and confidence intervals for the ATT via non-parametric cluster bootstrap or jackknife over control units. Works for both sharp and staggered GSC fits. For staggered fits, bootstrap resamples each cohort's control pool independently, and jackknife uses a per-cohort LOO with delta-method variance aggregation.
Usage
gsc_inference(
fit,
method = c("bootstrap", "jackknife", "jackknife_global"),
n_boot = 499L,
level = 0.95,
alternative = c("two.sided", "greater", "less"),
seed = NULL
)
Arguments
fit |
A |
method |
|
n_boot |
Number of bootstrap replications (default 499L; ignored for jackknife). |
level |
Confidence level (default 0.95). |
alternative |
|
seed |
RNG seed for reproducibility (default NULL). |
Details
Note: gsc_boot() performs a parametric bootstrap under H0 (hypothesis
testing). gsc_inference() provides non-parametric SE and CIs suitable
for inference about the ATT magnitude.
Value
A list of class coresynth_inference.
Kalman Filter and RTS Smoother (TASC)
Description
Implements the Kalman filter (forward pass) and Rauch-Tung-Striebel smoother (backward pass) for the state-space model in Rho et al. (2026):
Usage
kalman_smoother_cpp(Y, W, A, C, Q, R, z0, P0)
Arguments
Y |
Observed data matrix (N x T). Use NA for unobserved entries. |
W |
Observation / loading matrix (N x r) |
A |
State transition matrix (r x r). Pass diag(r) for random-walk dynamics. |
C |
State drift vector (r x 1) |
Q |
State noise covariance (r x r) |
R |
Observation noise covariance (N x N, diagonal in practice) |
z0 |
Initial state mean (r x 1) |
P0 |
Initial state covariance (r x r) |
Details
State: z(t+1) = A z(t) + C + eta(t), eta(t) ~ N(0, Q) Observation: y_t = W z_t + eps_t, eps_t ~ N(0, R)
Observation rows with NA (treated post-intervention) are automatically dropped at each time step so only control-unit rows update the filter.
The P update uses the numerically stable Joseph form: P(t|t) = (I - K W_obs) P(t|t-1) (I - K W_obs)^T + K R_obs K^T
Value
A list with z_smooth, P_smooth, P_cross, z_pred, z_upd. P_cross is an r x r x (T-1) cube. Slice t (C++ 0-indexed, t=0,...,T-2) stores P(t+1, t | T) (0-indexed), i.e. P(t+2, t+1 | T) in 1-indexed Shumway-Stoffer notation. Formula: P(t+1|T) * J_t^T (eq. 6.68-6.69).
Leave-One-Out Donor Robustness for SCM
Description
Iteratively re-estimates the synthetic control excluding one contributing donor at a time, holding the predictor weights V fixed at their baseline values (Abadie, Diamond & Hainmueller 2015, footnote 20). The spread of the leave-one-out ATT estimates shows how much the result hinges on any single donor.
Usage
loo_donors(fit, weight_threshold = 1e-06)
Arguments
fit |
A sharp |
weight_threshold |
Only donors whose baseline weight exceeds this
value are dropped (removing a zero-weight donor cannot change the fit).
Default |
Details
For penalised fits (lambda_pen used), the same penalty is re-applied in
each leave-one-out QP.
Value
A list with:
-
att_original: baseline ATT -
results: data.frame with one row per excluded donor (donor,weight,att_loo) -
att_range: range of the leave-one-out ATTs
See Also
placebo_in_time(), mspe_ratio_pval()
Permutation Inference via MSPE Ratio for SCM
Description
Computes the Abadie et al. (2010) / Abadie (2021) permutation p-value. For each control unit, a leave-one-out synthetic control is fitted.
Usage
mspe_ratio_pval(
fit,
mspe_threshold = 0,
max_iter = 100L,
tol = 1e-04,
use_covariates = NULL,
alternative = c("two.sided", "greater", "less"),
mspe_prune = Inf,
statistic = c("ratio", "post_mspe")
)
Arguments
fit |
A |
mspe_threshold |
Minimum pre-treatment MSPE for including a control unit in the two-sided test. Ignored for one-sided tests. Default: 0 (no filtering). |
max_iter |
Passed to |
tol |
Passed to |
use_covariates |
Controls which predictor specification the placebo
refits use. Default |
alternative |
Direction of the alternative hypothesis:
|
mspe_prune |
Discard placebo runs whose pre-treatment MSPE exceeds
this multiple of the treated unit's, following Abadie, Diamond &
Hainmueller (2010, Figures 5-7), who use 20, 5 and 2. A run the synthetic
control could not fit before treatment carries no information about how
rare a large post-treatment gap is, so leaving it in the permutation
distribution only adds noise. |
statistic |
Which quantity to rank, for two-sided tests.
|
Details
When alternative = "two.sided" (default), the test statistic is the
post/pre MSPE ratio, following Abadie et al. (2010). When
alternative = "greater" or "less", the test statistic is the signed
average post-treatment gap (ATT), giving a one-sided permutation test as
recommended by Abadie (2021) S.3.5 for improved power when the direction
of the treatment effect is known.
The placebo refits mirror the treated fit's outer optimiser and evaluation
window: a fit estimated with the multi-start outer search (v_optim = "multistart", or the "auto" default with predictors) or with a
v_window runs every placebo unit through the same configuration, keeping
the permutation statistic exchangeable across units.
Value
An object of class scm_placebo (a list) with:
-
p_value: Permutation p-value between 0 and 1 -
mspe_ratio_treated: MSPE_post / MSPE_pre for the treated unit (two.sided only) -
mspe_ratios_all: Named numeric vector (treated first, then controls); two.sided only -
placebo_effects: Named N_co-vector of placebo ATT estimates -
treated_effect: ATT estimate for the treated unit -
n_placebo_used: Number of control units used -
n_placebo_pruned: Number discarded bymspe_prune -
mspe_prune,statistic: the settings the p-value was computed under -
gaps: T x N_co matrix of placebo gap paths (unit minus its synthetic control over all periods), for the Abadie et al. (2010) Figure 4-7 plot -
treated_gap: T-vector of the treated unit's gap path -
mspe_pre_treated,mspe_pre_placebo: Pre-treatment MSPEs used for the relative pruning rule inplot.scm_placebo() -
times,T_pre: Time axis metadata for plotting
See Also
plot.scm_placebo() for the placebo gap and MSPE ratio plots.
In-Time Placebo (Backdating) Test for SCM
Description
Re-estimates the synthetic control after artificially backdating the
treatment to a pre-treatment period, following Abadie, Diamond &
Hainmueller (2015) and Abadie & Vives-i-Bastida (2022, principle 7:
"out-of-sample validation is key"). Only pre-treatment data enter the
exercise, so the placebo gap after t0_placebo is uncontaminated by the
actual intervention. A credible design shows no sizable divergence at the
backdated treatment time.
Usage
placebo_in_time(fit, t0_placebo = NULL)
Arguments
fit |
A sharp |
t0_placebo |
Backdated treatment period as a 1-based position in
|
Details
The refit uses the outcomes of periods 1..t0_placebo as predictors
(the predictors = NULL default), regardless of how the original fit was
specified, because user-supplied pred() windows cannot be lagged
automatically (ADH 2015 lag their predictors by hand).
Value
A list with:
-
t0_placebo: the backdated treatment period used -
times: time values of the pre-treatment window -
unit_weights: placebo donor weights -
Y_treat,Y_synth,gap: series over the pre-treatment window -
placebo_att: mean placebo gap over(t0_placebo, T_pre] -
fit_rmspe: RMSPE over the placebo fitting window1..t0_placebo -
eval_rmspe: RMSPE over the placebo post window(t0_placebo, T_pre]
See Also
mspe_ratio_pval() for in-space placebos, loo_donors() for
donor-robustness checks.
Plot a coresynth model
Description
Plot a coresynth model
Usage
## S3 method for class 'coresynth'
plot(
x,
type = c("trend", "gap", "weights", "pred_weights"),
colors = NULL,
labels = NULL,
linetypes = NULL,
vline = list(),
vline_offset = .VLINE_OFFSET_DEFAULT,
hline = list(),
fill = NULL,
top_n = Inf,
align = FALSE,
show_donors = 0,
...
)
Arguments
x |
A |
type |
One of |
colors |
For |
labels |
For |
linetypes |
For |
vline |
Aesthetic overrides for the vertical treatment-time line, as a
list passed to |
vline_offset |
For |
hline |
Aesthetic overrides for the horizontal zero line in |
fill |
For |
top_n |
For |
align |
For |
show_donors |
For |
... |
Ignored. |
Value
A ggplot2 plot object.
Examples
set.seed(1)
panel <- expand.grid(unit = 1:10, year = 1:20)
panel$treated <- as.integer(panel$unit == 5 & panel$year > 15)
panel$gdp <- panel$unit + 0.5 * panel$year +
rnorm(nrow(panel)) + 3 * panel$treated
fit <- scm_fit(gdp ~ treated | unit + year, data = panel, method = "scm")
plot(fit, type = "trend")
plot(fit, type = "gap")
plot(fit, type = "weights")
plot(fit, type = "weights", top_n = 5)
# Predictor (V) weights: which predictors the fit leans on
plot(fit, type = "pred_weights")
# Overlay the five largest donors behind the treated/synthetic series
plot(fit, type = "trend", show_donors = 5)
# SDID: align the synthetic series on the lambda-weighted pre-period level
fit_sdid <- scm_fit(gdp ~ treated | unit + year, data = panel, method = "sdid")
plot(fit_sdid, type = "trend", align = TRUE)
plot(fit_sdid, type = "gap", align = TRUE)
# Customize series colors, legend text, line types, and reference lines
plot(fit, type = "trend",
colors = c(treated = "black"),
labels = c(treated = "Unit 5"),
vline = list(color = "red", linetype = "dashed"))
# Draw both series solid, or restyle the gap line
plot(fit, type = "trend", linetypes = c(synthetic = "solid"))
plot(fit, type = "gap", linetypes = "dashed")
# Move the treatment line onto the first post-treatment period,
# pin it to an absolute time, or drop it entirely
plot(fit, type = "trend", vline_offset = 0)
plot(fit, type = "trend", vline = list(xintercept = 15.5))
plot(fit, type = "trend", vline = FALSE)
Plot an scm_design object
Description
Plot an scm_design object
Usage
## S3 method for class 'scm_design'
plot(x, type = c("outcome", "gap"), ...)
Arguments
x |
An |
type |
|
... |
Currently ignored. |
Value
A ggplot object: for type = "outcome", the synthetic treated and
synthetic control outcome series; for type = "gap", the estimated
treatment effect over the experimental periods with split-conformal
confidence intervals. The object is returned for printing or further
customisation.
Plot a Balance Possibility Frontier
Description
Draws the trade-off traced by scm_balance_frontier(): the imbalance of the
treated average, which the aggregate ATT rests on, against the root mean
square of the per-block imbalances, which the block-level effects rest on.
Separate SCM sits at one end of the curve and fully pooled SCM at the other,
with the heuristic the fit used marked along it.
Usage
## S3 method for class 'scm_frontier'
plot(x, relative = TRUE, label_nu = TRUE, colors = NULL, ...)
Arguments
x |
An |
relative |
Plot the imbalances normalised by the separate solution (default), which puts both axes on comparable scales, rather than in the outcome's own units. |
label_nu |
Annotate the drawn points with their |
colors |
Optional named overrides for the |
... |
Ignored. |
Details
The curve is normally strongly convex, which is the point of looking at it:
a small move away from either end usually buys a large reduction in the
other imbalance for very little, so the interesting values of nu are the
ones near the bend.
Value
A ggplot2 plot object.
See Also
Examples
set.seed(1)
panel <- expand.grid(unit = 1:15, year = 1:20)
adopt <- c(`1` = 12, `2` = 15, `3` = 17)
panel$treated <- as.integer(!is.na(adopt[as.character(panel$unit)]) &
panel$year >= adopt[as.character(panel$unit)])
panel$treated[is.na(panel$treated)] <- 0L
panel$gdp <- panel$unit + 0.4 * panel$year +
rnorm(nrow(panel)) + 2 * panel$treated
fit <- scm_fit(gdp ~ treated | unit + year, data = panel, method = "scm")
plot(scm_balance_frontier(fit, nu_grid = c(0, 0.25, 0.5, 0.75, 1)))
Plot SCM In-Space Placebo Results
Description
Visualizes the placebo study returned by mspe_ratio_pval(), following
Abadie, Diamond & Hainmueller (2010, Section 3.4).
Usage
## S3 method for class 'scm_placebo'
plot(
x,
type = c("gaps", "ratios"),
mspe_prune = NULL,
colors = NULL,
labels = NULL,
linetypes = NULL,
vline = list(),
vline_offset = .VLINE_OFFSET_DEFAULT,
hline = list(),
...
)
Arguments
x |
A |
type |
One of |
mspe_prune |
Exclude placebo units whose
pre-treatment MSPE exceeds |
colors |
A named vector overriding series colors, e.g.
|
labels |
A named vector overriding the legend text of individual
series, e.g. |
linetypes |
Only for |
vline |
Only for |
vline_offset |
Only for |
hline |
Only for |
... |
Ignored. |
Details
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 unit (ADH 2010, Figure 4). Placebo units whose
synthetic control fits poorly before treatment carry no information about
the rarity of a large post-treatment gap, so ADH exclude units whose
pre-treatment MSPE exceeds a multiple of the treated unit's: 20, 5, and 2
in their Figures 5-7 (mspe_prune).
type = "ratios" shows the statistic behind the two-sided permutation
p-value, one point per unit (ADH 2010, Figure 8): the post/pre-treatment
MSPE ratio, or the post-treatment MSPE where the test was run with
statistic = "post_mspe". The ratio needs no pruning cutoff by
construction, which is why mspe_ratio_pval() leaves mspe_prune at
Inf; where the test did prune, the same runs are dropped here so that the
points and the p-value in the subtitle describe one set of placebo runs.
Value
A ggplot2 plot object.
See Also
Examples
set.seed(1)
panel <- expand.grid(unit = 1:10, year = 1:20)
panel$treated <- as.integer(panel$unit == 5 & panel$year > 15)
panel$gdp <- panel$unit + 0.5 * panel$year +
rnorm(nrow(panel)) + 3 * panel$treated
fit <- scm_fit(gdp ~ treated | unit + year, data = panel, method = "scm")
placebo <- mspe_ratio_pval(fit)
# Treated gap overlaid on the donor-pool placebo gaps (ADH 2010, Fig. 4)
plot(placebo, type = "gaps")
# Prune poorly fitting placebos and relabel the legend
plot(placebo, type = "gaps", mspe_prune = 5,
labels = c(treated = "Unit 5"))
# Set the line type of the placebo paths off against the treated one
plot(placebo, type = "gaps", linetypes = c(placebo = "dotted"))
# Move the treatment line onto the first post-treatment period
plot(placebo, type = "gaps", vline_offset = 0)
# Post/pre-treatment MSPE ratios (ADH 2010, Fig. 8)
plot(placebo, type = "ratios")
Extract the tidy data behind a coresynth plot
Description
Returns the tidy data.frame that plot() draws for a given type, so the
underlying series, weights, or placebo paths can be inspected, joined into a
table, or re-plotted directly. plot() stays the quick path; plot_data()
is the handle for anyone who wants to relabel simplified series names, feed
the numbers into their own figure, or postprocess them further.
Usage
plot_data(x, ...)
## Default S3 method:
plot_data(x, ...)
## S3 method for class 'coresynth'
plot_data(
x,
type = c("trend", "gap", "weights", "pred_weights"),
align = FALSE,
top_n = Inf,
show_donors = 0,
...
)
## S3 method for class 'scm_placebo'
plot_data(x, type = c("gaps", "ratios"), mspe_prune = NULL, ...)
Arguments
x |
A |
... |
Passed to methods (unused by the current methods). |
type |
For a |
align |
For |
top_n |
For |
show_donors |
For |
mspe_prune |
For a |
Details
The frame mirrors what the matching plot(x, type = ...) call shows, with
two deliberate departures that make it a better data source:
Plain column names (
time,value,series,weight, ...) are used instead of the dotted convention ofaugment(), since this is data to manipulate rather than model-augmented observations.The cosmetic "drop donors with weight below 1e-4" filter that
plot(type = "weights")applies is not used here: every donor is returned (usetop_nto subset), so the frame is the complete set of weights.
Only the arguments that change which rows or values appear are accepted
(align, top_n, show_donors, mspe_prune); purely cosmetic arguments
of plot() (colors, labels, linetypes, vline, fill, ...) have no
data counterpart and are not part of this interface.
Value
A tidy data.frame. Columns by type:
"trend"time,value,series("Treated"/"Synthetic Control"); withshow_donors > 0, also"Donors"rows and aunitcolumn."gap"time,gap(treated minus synthetic control)."weights"unit,weight; SDID fits add apanelcolumn ("omega"unit weights,"lambda"time weights), withunitholding the pre-period label for"lambda"rows."pred_weights"predictor,weight(sharp SCM only)."gaps"time,gap,unit(NAfor the treated series),series("Treated"/"Placebo (donor pool)")."ratios"unit,ratio,post_mspe,series– both candidate statistics, so the frame answers eithermspe_ratio_pval()statistic.
See Also
plot.coresynth(), plot.scm_placebo()
Examples
set.seed(1)
panel <- expand.grid(unit = 1:10, year = 1:20)
panel$treated <- as.integer(panel$unit == 5 & panel$year > 15)
panel$gdp <- panel$unit + 0.5 * panel$year +
rnorm(nrow(panel)) + 3 * panel$treated
fit <- scm_fit(gdp ~ treated | unit + year, data = panel, method = "scm")
head(plot_data(fit, type = "trend"))
plot_data(fit, type = "gap")
plot_data(fit, type = "weights")
# Relabel the simplified series names, then plot it yourself
df <- plot_data(fit, type = "trend")
df$series <- sub("Synthetic Control", "Synthetic Unit 5", df$series)
ggplot2::ggplot(df, ggplot2::aes(time, value, color = series)) +
ggplot2::geom_line()
Predictor Specification for SCM
Description
Creates a single predictor specification for use in scm_fit() with
method = "scm". Pass a list() of pred() calls as the predictors
argument to define the full covariate matrix.
Usage
pred(vars, times, op = "mean")
Arguments
vars |
Character vector of variable names. All variables share the
same |
times |
Numeric/integer vector of time values to aggregate over.
Values are matched against the time index of the panel passed to
|
op |
Aggregation operator applied to each variable over |
Value
A pred_spec object (a named list with class "pred_spec").
See Also
scm_fit() for the predictors argument that consumes a list()
of pred_spec objects.
Examples
# Three variables averaged over the same window
pred(c("lnincome", "retprice", "age15to24"), 1980:1988)
# Single variable at a specific year
pred("cigsale", 1975)
# Single variable averaged over a range
pred("beer", 1984:1988)
# Abadie, Diamond & Hainmueller (2010) California Prop 99 style: combine
# several covariates aggregated over different windows plus three outcome
# lags at specific years. The resulting list is passed to
# scm_fit(..., predictors = predictors).
predictors <- list(
pred(c("lnincome", "retprice", "age15to24"), 1980:1988),
pred("beer", 1984:1988),
pred("cigsale", 1988),
pred("cigsale", 1980),
pred("cigsale", 1975)
)
predictors
Balance Possibility Frontier for Staggered SCM
Description
Traces how the two pre-treatment imbalances of partially pooled SCM trade
off as the pooling parameter nu varies: q_pool, the imbalance of the
average of the treated units, which the aggregate ATT rests on, and
q_sep, the root mean square of the per-block imbalances, which the
block-level effects rest on. Both are the cohort-level measures of
Ben-Michael, Feller & Rothstein (2022, Appendix A.2), in which a block
enters through the sum over its treated units, so a block of n_g
units counts n_g times towards q_pool and n_g^2 times towards
q_sep. Supplying weights to scm_fit() replaces the unit count with
the mass it gives them, and the frontier is traced on that same objective.
Usage
scm_balance_frontier(fit, nu_grid = seq(0, 1, length.out = 11L))
Arguments
fit |
A staggered |
nu_grid |
Values of |
Details
Ben-Michael, Feller & Rothstein (2022) call this plot "an important tool for
using partially pooled SCM in practice" (S.4.2): the curve is strongly
convex, so a little pooling usually buys a large reduction in one imbalance
for a small rise in the other, and where that bargain stops being worth it
is a judgement the data can inform but not settle. Their nu heuristic,
which scm_fit() uses by default, is one point on this curve.
A fit made with cov_method = "balance" is traced on the objective that
includes its covariate term, and the frontier reports that term's two
measures in q_cov_sep and q_cov_pool. The plot still draws the outcome
imbalances, which are then only part of what the fit trades off.
Value
A data.frame of class scm_frontier, one row per nu, with
nu, q_sep, q_pool, their values normalised by the separate solution
(q_sep_rel, q_pool_rel), estimate, and heuristic marking the row
closest to the fit's own nu. A fit made with cov_method = "balance"
trades off a covariate imbalance as well, so its frontier carries
q_cov_sep and q_cov_pool too, and the two normalisation constants are
then the combined measure of the paper's footnote 7 rather than the
outcome imbalance alone – which is why q_sep_rel and q_pool_rel are
not 1 at nu = 0 there. Plot it with plot().
References
Ben-Michael, E., Feller, A., & Rothstein, J. (2022). Synthetic controls with staggered adoption. JRSS-B, 84(2), 351-381.
Examples
set.seed(1)
panel <- expand.grid(unit = 1:15, year = 1:20)
adopt <- c(`1` = 12, `2` = 15, `3` = 17)
panel$treated <- as.integer(!is.na(adopt[as.character(panel$unit)]) &
panel$year >= adopt[as.character(panel$unit)])
panel$treated[is.na(panel$treated)] <- 0L
panel$gdp <- panel$unit + 0.4 * panel$year +
rnorm(nrow(panel)) + 2 * panel$treated
fit <- scm_fit(gdp ~ treated | unit + year, data = panel, method = "scm")
fr <- scm_balance_frontier(fit, nu_grid = c(0, 0.5, 1))
fr
Experimental Synthetic Control Design
Description
Selects which units to assign to the treatment arm (and which to the control
arm) in a planned experiment, following Abadie and Zhao (2026). Both sets
of units are chosen by minimising the distance between their weighted-average
predictor vectors and the population-average predictor vector \bar{X},
so the resulting estimates are less susceptible to post-randomisation bias
than pure random assignment.
Usage
scm_design(
data,
outcome,
unit,
time,
T0,
T_fit = NULL,
m_min = 1L,
m_max = 1L,
f = NULL,
predictors = NULL,
design = c("base", "weakly_targeted", "unit_level"),
beta = 1,
xi = 1,
alpha = 0.05,
normalize = TRUE,
max_subsets = 100000L
)
Arguments
data |
Long-format data frame (one row per unit–time). |
outcome |
Name of the outcome column. |
unit |
Name of the unit identifier column. |
time |
Name of the time identifier column. |
T0 |
Last pre-experimental period (a value present in the time
column). Periods after |
T_fit |
Number of fitting periods, counted from the start of
the pre-experimental phase. Defaults to |
m_min |
Minimum number of units assigned to treatment (default 1). |
m_max |
Maximum number of units assigned to treatment (default 1). |
f |
Named numeric vector of population weights |
predictors |
A |
design |
Design formulation: |
beta |
Trade-off parameter |
xi |
Trade-off parameter |
alpha |
Significance level for confidence intervals (default 0.05). |
normalize |
If |
max_subsets |
Maximum number of treatment-set candidates to evaluate before switching to random sampling (default 100 000). |
Details
Three design formulations are available:
-
"base"(eq. 7): both the synthetic treated and the synthetic control independently target the population average\bar{X}. -
"weakly_targeted"(eq. 9): the treated and control weights jointly minimise the distance of the synthetic treated to\bar{X}plusbetatimes the distance between the synthetic control and the synthetic treated. -
"unit_level"(eq. 10): each treated unit gets its own synthetic control; the treated weights trade off matching\bar{X}againstxitimes each treated unit's own synthetic-control fit, and the aggregate control weight is thew-weighted combination of the per-unit controls (eq. 11).
Inference uses "blank periods" — pre-experimental periods whose outcomes were
not used to estimate the weights. Set T_fit strictly smaller than the
number of pre-experimental periods to enable the permutation test and split-
conformal confidence intervals from Section 3 of Abadie and Zhao (2026).
Value
An object of class "scm_design" with components:
-
treated_units: unit identifiers selected for treatment -
control_units: unit identifiers in the control pool -
w: J-length weight vector for the synthetic treated unit (sums to 1) -
v: J-length weight vector for the synthetic control unit (sums to 1) -
tau_hat: estimated treatment effects for each experimental period -
p_value: permutation p-value (NA when blank periods are unavailable) -
ci_lower,ci_upper: per-period split-conformal confidence interval -
Y_synth_tr,Y_synth_co: synthetic treated/control series (all periods) -
estimate: ATT (mean oftau_hat)
References
Abadie, A. and Zhao, J. (2026). "Synthetic Controls for Experimental Design." MIT Working Paper.
Fit a Synthetic Control Method Model
Description
Unified formula interface for Synthetic Control and related causal inference methods. The formula syntax is:
Usage
scm_fit(
formula,
data,
method = c("scm", "sdid", "gsc", "mc", "tasc", "si"),
...,
predictors = NULL,
covariates = NULL,
v_selection = c("insample", "oos"),
donor_mspe_threshold = Inf,
lambda_pen = NULL,
v_optim = c("auto", "coord_descent", "bfgs", "multistart"),
qp_solver = c("auto", "active_set", "wolfe"),
v_window = NULL,
nu = NULL,
fixedeff = NULL,
pool_cohorts = TRUE,
weights = NULL
)
Arguments
formula |
A |
data |
A |
method |
One of |
... |
Estimator-specific arguments forwarded to the chosen method:
Three tuning parameters that govern model complexity are selected from
the data when left at their |
predictors |
A |
covariates |
An optional named |
v_selection |
V matrix selection method for |
donor_mspe_threshold |
Donor pool filtering threshold (Abadie 2021 S.4).
For |
lambda_pen |
Penalised SCM parameter (Abadie & L'Hour 2021, JASA).
For |
v_optim |
Outer V-optimisation method for |
qp_solver |
Inner-QP solver for |
v_window |
Optional vector of pre-treatment time values (matching the
time index in |
nu |
How strongly the adoption cohorts are pooled in staggered
SCM fits (Ben-Michael, Feller & Rothstein 2022, JRSS-B). Cohort weight
vectors are chosen jointly to minimise
Staggered panels only. Under a single adoption date there are no cohorts to pool across, and several units sharing one date are already fully pooled into that cohort: S.4.1 of the paper shows the across-unit variation that would justify doing otherwise is zero when the adoption date is shared. |
fixedeff |
Unpenalised additive level terms. For |
pool_cohorts |
For staggered |
weights |
Optional named numeric vector over the treated units giving
the mass each carries in the aggregate ATT, for |
Details
outcome ~ treatment | unit_id + time_id
Value
An object of classes c("coresynth_<method>", "coresynth").
Fits with staggered adoption additionally inherit from
"coresynth_staggered", and multi-arm SI fits from
"coresynth_multiarm"; S3 methods such as tidy() and augment()
dispatch on these subclasses.
All methods return at minimum:
-
method: estimator name -
estimate: average treatment effect (ATT) -
times: time index vector -
T_pre: number of pre-treatment periods -
Y_treat: treated unit outcome series -
gap: treatment effect series (Y_treat - counterfactual)
Staggered method = "scm" fits add cohort_estimates, cohort_fits,
and the settings the fit resolved: nu (the number the partially pooled
solver used, or "separate" for the per-cohort path), fixedeff,
pool_cohorts and qp_solver.
Examples
# Synthetic balanced panel: 10 units over 20 periods, unit 1 treated
# after period 15.
set.seed(1)
panel <- expand.grid(unit = 1:10, year = 1:20)
panel$treated <- as.integer(panel$unit == 1 & panel$year > 15)
panel$gdp <- panel$unit + 0.5 * panel$year +
rnorm(nrow(panel)) + 3 * panel$treated
fit <- scm_fit(gdp ~ treated | unit + year, data = panel, method = "sdid")
summary(fit)
# Visualise the estimated gap (requires ggplot2)
plot(fit, type = "gap")
Inference for Staggered SCM
Description
Standard errors, confidence intervals and p-values for the aggregate ATT of a staggered SCM fit, by the two procedures of Ben-Michael, Feller & Rothstein (2022).
Usage
scm_inference(
fit,
method = c("jackknife", "wild_bootstrap"),
n_boot = 1000L,
level = 0.95,
alternative = c("two.sided", "greater", "less"),
seed = NULL
)
Arguments
fit |
A staggered SCM fit from |
method |
|
n_boot |
Number of bootstrap draws ( |
level |
Confidence level. Default 0.95. |
alternative |
Direction of the alternative hypothesis for the
p-value: |
seed |
Optional RNG seed ( |
Details
method = "jackknife" (default) is the leave-one-unit-out jackknife of
their Appendix A.1: every unit in the panel – treated units and donors
alike – is dropped in turn, the weights (and intercepts, for fixedeff
fits) are refitted with the pooling parameter nu held at the fit's value,
and the aggregate ATT is recomputed. The variance is
(n-1)/n \sum_i (\widehat{ATT}^{(-i)} - \overline{ATT})^2, and the
interval and p-value use the normal approximation. Because the donors are
resampled too, the interval reflects the noise in the synthetic controls,
which every treated unit of a cohort shares.
method = "wild_bootstrap" is the multiplier bootstrap of their Section
5.3, which adapts Otsu & Rai (2017). The ATT is written as a sum over every
unit of the panel, and each unit's term is perturbed by its own
golden-ratio two-point multiplier while the weights stay fixed: a treated
unit's term is its effect's deviation from the ATT plus its residual, a
donor's is its residual times the weight the synthetic controls put on it.
Residuals are against a two-way (unit and period) model of the untreated
outcome. Following Otsu & Rai's Equation (9), only the treated units'
heterogeneity term is centred at the ATT; centring every unit, as the
paper's Equation (13) reads, would make the interval grow with the size of
the effect. The draws cost no refitting, so this is the choice for panels
too large for the jackknife. It is conservative where the outcome has
structure the two-way model does not absorb (unit-specific trends): in
simulation a nominal 95% interval covered 96-99.5% across donor pools of
8 to 40 and 3 to 24 treated units, and 93% with 2 treated units.
The jackknife refits the model once per unit, which grows quickly with the panel: about 0.2 s at 50 units, 0.7 s at 100 and 3 s at 200, but over a minute at 400. Donors that carry no weight in any block are not refitted (dropping one leaves the solution unchanged), and each refit starts from the full fit's weights.
Both work with both staggered SCM paths (the partially pooled default and
nu = "separate") and honour the aggregation weights N_treated x T_post,
or the weights given to scm_fit().
The jackknife treats the outcome as fixed: a fit with covariates
partialled out keeps the partialled-out outcome, and one with
cov_method = "balance" keeps its scaled covariate matrix, rather than
re-estimating either without the dropped unit. A dropped unit that was the
only member of its cohort removes that cohort from the estimate, as in the
paper's J^{(-i)}.
Value
A coresynth_inference object with the standard fields
(estimate, se, p_value, ci_lower, ci_upper, method,
staggered, n_controls, alternative), compatible with
tidy.coresynth_inference() and glance.coresynth_inference().
n_treated records the number of treated units. The wild bootstrap adds
the draws in boot_ests, centred on the estimate; the jackknife adds the leave-one-out estimates
in jack_ests (named by the dropped unit, NA where a refit was not
possible).
References
Ben-Michael, E., Feller, A., & Rothstein, J. (2022). Synthetic controls with staggered adoption. JRSS-B, 84(2), 351-381.
Otsu, T., & Rai, Y. (2017). Bootstrap inference of matching estimators for average treatment effects. JASA, 112(520), 1720-1732.
Examples
set.seed(1)
dat <- expand.grid(time = 1:20, id = paste0("u", 1:12))
dat$y <- rnorm(nrow(dat)) + as.numeric(factor(dat$id))
dat$d <- as.integer(
(dat$id %in% c("u1", "u2") & dat$time > 10) |
(dat$id %in% c("u3", "u4") & dat$time > 14)
)
fit <- scm_fit(y ~ d | id + time, data = dat, method = "scm")
scm_inference(fit)
SCM Inner Weights (QP Given V)
Description
Solves the inner-loop QP for SCM: given a fixed diagonal metric matrix V, finds donor weights W on the simplex minimising the V-weighted covariate loss. The returned weights are a KKT-verified exact optimum whenever the active-set solver converges, with accelerated projected gradient as a fallback.
Usage
scm_inner_weights_cpp(X0, X1, V_diag, wolfe = FALSE)
Arguments
X0 |
Covariate matrix for control units (k x N_co) |
X1 |
Covariate vector for the treated unit (k x 1) |
V_diag |
Diagonal of the metric matrix V (k x 1, non-negative, need not sum to 1) |
wolfe |
If |
Value
Donor weight vector W (N_co x 1) on the unit simplex
Fast Leave-One-Out Placebo Test for SCM (Abadie et al. 2010)
Description
For each control unit, treats it as pseudo-treated and fits SCM weights from the remaining N_co-1 donors. Returns MSPE components for constructing MSPE-ratio permutation p-values in R.
Usage
scm_placebo_cpp(
Y_pre,
Y_post,
max_iter = 100L,
tol = 1e-04,
z_rows = NULL,
wolfe = FALSE
)
Arguments
Y_pre |
Control pre-treatment outcomes (T_pre x N_co) |
Y_post |
Control post-treatment outcomes (T_post x N_co) |
max_iter |
Outer coordinate-descent iterations (default 100) |
tol |
Convergence tolerance for V updates (default 1e-4) |
z_rows |
Optional 1-based pre-period row indices of the outer
evaluation window (the |
wolfe |
If |
Value
A list with:
-
mspe_pre: N_co-vector of pre-treatment MSPE per placebo unit -
mspe_post: N_co-vector of post-treatment MSPE per placebo unit -
effects: N_co-vector of mean post-period gap per placebo unit -
gaps: (T_pre + T_post) x N_co matrix of placebo gap paths
Fast Leave-One-Out Placebo Test for SCM with a Predictor Specification
Description
Covariate-spec counterpart of scm_placebo_cpp(): for each control unit,
treats it as pseudo-treated with its own predictor column X0[, i] and
fits the nested V/W optimisation against the remaining donors' predictors
X0[, -i], evaluating the prediction loss on pre-treatment outcomes.
Each leave-one-out problem is identical to a scm_weights_cpp() call on
the same submatrices; iterations are independent and run in parallel
under OpenMP.
Usage
scm_placebo_x_cpp(
X0,
Y_pre,
Y_post,
max_iter = 100L,
tol = 1e-04,
z_rows = NULL,
multistart = FALSE,
wolfe = FALSE
)
Arguments
X0 |
Predictor matrix for control units (k x N_co), on the same
scale as the treated fit (SD-scaled when |
Y_pre |
Control pre-treatment outcomes (T_pre x N_co) |
Y_post |
Control post-treatment outcomes (T_post x N_co) |
max_iter |
Outer coordinate-descent iterations (default 100) |
tol |
Convergence tolerance for V updates (default 1e-4) |
z_rows |
Optional 1-based pre-period row indices of the outer
evaluation window (the |
multistart |
If |
wolfe |
If |
Value
A list with:
-
mspe_pre: N_co-vector of pre-treatment MSPE per placebo unit -
mspe_post: N_co-vector of post-treatment MSPE per placebo unit -
effects: N_co-vector of mean post-period gap per placebo unit -
gaps: (T_pre + T_post) x N_co matrix of placebo gap paths A placebo unit whose solver fails yields NaN entries.
Rank-Based Permutation Test for Several Treated Units
Description
Permutation inference on the aggregate effect when more than one unit is treated, following Abadie & L'Hour (2021, S.3.2). Each treated unit contributes its own test statistic; the identities of the treated units are then reassigned at random among all units, and the sum of the ranks of the real treated units' statistics is compared with its permutation distribution.
Usage
scm_rank_pval(
fit,
B = 500L,
statistic = c("mspe_ratio", "effect", "abs_effect"),
alternative = c("two.sided", "greater", "less"),
seed = 1L
)
Arguments
fit |
A |
B |
Number of treatment reassignments. |
statistic |
Per-unit statistic to rank. |
alternative |
|
seed |
Seed for the reassignment draw, so the p-value is reproducible without consuming the caller's RNG state. |
Details
Ranking is what makes the test usable here. mspe_ratio_pval() compares one
treated unit against placebo runs on single donors, which breaks down as
soon as the treated series is an average: the average is less noisy than any
single donor, so its pre/post MSPE ratio is inflated and the test rejects
far too often. Ranks sidestep that, because every statistic entering the
comparison, real or permuted, is computed on a single unit.
The fit must expose per-unit effects, so a staggered fit has to be made with
pool_cohorts = FALSE. A sharp fit with several treated units pools them by
design, and its per-unit statistics are computed here from the stored
per-unit series.
Value
An object of class c("scm_rank_test", "coresynth_inference") with
p_value, statistic (the observed rank sum), rank_sums (its
permutation distribution), unit_statistics, n_treated, B and
alternative.
References
Abadie, A., & L'Hour, J. (2021). A penalized synthetic control estimator for disaggregated data. JASA, 116(536), 1817-1834.
See Also
mspe_ratio_pval() for the single-treated-unit test.
Examples
set.seed(1)
panel <- expand.grid(unit = 1:12, year = 1:20)
panel$treated <- as.integer(panel$unit <= 3 & panel$year > 12)
panel$gdp <- panel$unit + 0.4 * panel$year +
rnorm(nrow(panel)) + 2 * panel$treated
fit <- scm_fit(gdp ~ treated | unit + year, data = panel, method = "scm")
scm_rank_pval(fit, B = 50)
SCM Outer Weights (Joint Optimization of W and V)
Description
Jointly optimises donor weights W (on the simplex) and the diagonal metric matrix V via coordinate descent on the pre-treatment prediction MSPE, following Abadie, Diamond & Hainmueller (2010).
Usage
scm_weights_cpp(
X0,
X1,
Z0,
Z1,
max_iter = 100L,
tol = 1e-04,
t_train = -1L,
z_rows = NULL,
multistart = FALSE,
cheap_face = FALSE,
wolfe = FALSE
)
Arguments
X0 |
Covariate matrix for control units (k x N_co, typically pre-treatment outcomes) |
X1 |
Covariate vector for the treated unit (k x 1) |
Z0 |
Outcome matrix for control units in the pre-treatment window (T_pre x N_co) |
Z1 |
Outcome vector for the treated unit in the pre-treatment window (T_pre x 1) |
max_iter |
Maximum coordinate-descent iterations (default 100) |
tol |
Convergence tolerance on MSPE improvement (default 1e-4) |
t_train |
Validation-window split for V selection. -1 (default): V selected on the full Z window (in-sample). Positive: rows t_train..(T_pre-1) of Z form the validation window used to select V (W is fitted on the full X throughout); after selecting V*, W is refit and the reported loss uses the full Z window. |
z_rows |
Optional 1-based row indices of Z defining the evaluation
window for the outer V optimisation (the |
multistart |
If |
cheap_face |
If |
wolfe |
If |
Details
When t_train > 0, V is selected by minimising MSPE on a validation
window (rows t_train..T_pre-1 of Z) while W is fitted on the full
predictor matrix X. This is appropriate when X is a fixed predictor
matrix that contains no validation-period outcome information (the
user-supplied predictors case). For the outcomes-only case the proper
Abadie (2021) S.3.2 train/validation split is implemented in R
(.scm_oos_outcomes()): candidate W(V) are fitted on training-half
outcomes only, by passing the training rows as X and the validation
rows as Z to this function with t_train = -1.
Value
A list with:
-
W: Donor weight vector (N_co x 1) on the unit simplex -
V: Optimal metric diagonal (k x 1, normalised to sum to 1) -
loss: Final pre-treatment prediction loss (full pre-treatment window)
Calculate SDID Estimate (tau_sdid)
Description
Given unit weights omega and time weights lambda, computes the SDID estimator as a weighted two-way difference:
Usage
sdid_estimate_cpp(Y_pre_co, Y_post_co, Y_pre_tr, Y_post_tr, omega, lambda)
Arguments
Y_pre_co |
Control pre-treatment outcomes (T_pre x N_co) |
Y_post_co |
Control post-treatment outcomes (T_post x N_co) |
Y_pre_tr |
Treated pre-treatment outcomes (T_pre x 1) |
Y_post_tr |
Treated post-treatment outcomes (T_post x 1) |
omega |
Unit weights (N_co x 1) |
lambda |
Time weights (T_pre x 1) |
Details
tau_sdid = (Y_tr_post_mean - Y_tr_pre_wt) - (Y_co_post_wt - Y_co_pre_wt)
Value
A single numeric value: the SDID treatment-effect estimate
tau_sdid.
Inference for Synthetic Difference-in-Differences
Description
Computes standard errors and p-values for a SDID estimate using one of four methods, following Clarke et al. (2024): permutation placebo test (Algorithm 4), cluster bootstrap (Algorithm 2), leave-one-out jackknife (Algorithm 3), or (staggered fits only) a global jackknife across the unique control units of all cohorts.
Usage
sdid_inference(
fit,
method = c("placebo", "bootstrap", "jackknife", "jackknife_global"),
n_boot = 200L,
level = 0.95,
alternative = c("two.sided", "greater", "less"),
seed = NULL
)
Arguments
fit |
A |
method |
Inference method: |
n_boot |
Number of bootstrap replications (only for |
level |
Confidence level for the interval (all methods). |
alternative |
Direction of the alternative hypothesis: |
seed |
Integer seed for reproducibility (only for |
Details
For method = "placebo", the p-value is the permutation p-value, while
the standard error is the dispersion of the placebo distribution
(Clarke et al. 2024, Algorithm 4) and the confidence interval is the
normal approximation around the estimate with that SE. The placebo SE
assumes the treated unit's noise is comparable to the control units';
interpret it with caution when the donor pool is small.
Value
A list with:
-
estimate: The SDID point estimate. -
se: Standard error (placebo: placebo-distribution SD, Algorithm 4). -
p_value: Permutation or normal-approximation p-value. -
ci_lower,ci_upper: Confidence interval bounds. -
method: The inference method used. -
n_controls: Number of control units. -
alternative: The alternative hypothesis direction. -
placebo_effects: Named vector of LOO placebo effects (placebo only). -
boot_ests: Bootstrap estimate distribution (bootstrap only).
Fast Placebo Test for SDID
Description
For each control unit, treats it as the "pseudo-treated" unit and estimates the leave-one-out SDID effect. The distribution of these placebo effects provides a permutation-based null distribution for inference.
Usage
sdid_placebo_cpp(Y_pre, Y_post, time_weights, zeta2)
Arguments
Y_pre |
Control units pre-treatment outcomes (T_pre x N_co) |
Y_post |
Control units post-treatment outcomes (T_post x N_co) |
time_weights |
Lambda weights for pre-treatment periods (T_pre x 1) |
zeta2 |
Ridge penalty (same as used in the main estimate) |
Value
A numeric vector of length N_co. Each element is the
leave-one-out placebo SDID effect obtained by treating that control unit
as the pseudo-treated unit; the vector serves as a permutation-based null
distribution for inference.
Calculate SDID Time Weights (lambda)
Description
Solves the time-weight QP (with implicit intercept lambda_0 concentrated out):
Usage
sdid_time_weights_cpp(Y_pre_co, Y_post_target, zeta_t)
Arguments
Y_pre_co |
Pre-treatment outcomes for control units, row-demeaned (T_pre x N_co) |
Y_post_target |
Post-treatment mean per control unit, demeaned (N_co x 1) |
zeta_t |
Ridge penalty for time weights (paper: 1e-6 * sigma_hat) |
Details
min over lambda in Delta_pre: ||Y_post_target - Y_pre_co^T lambda||^2 + zeta_t^2 * N_co * ||lambda||^2
The caller is responsible for pre-demeaning Y_pre_co (row-wise) and Y_post_target (subtract the cross-unit mean) to concentrate out lambda_0, as described in Arkhangelsky et al. (2021) Algorithm 1, Eq. (2.3).
Value
A numeric vector of length T_pre holding the SDID time weights
lambda (non-negative and summing to one).
Calculate SDID Unit Weights (omega)
Description
Solves the regularized QP: min over omega in Delta: sum_t (sum_i omega_i Y_it - Y_tr_t)^2 + zeta^2 * T_pre * ||omega||^2
Usage
sdid_unit_weights_cpp(Y_pre, Y_tr_pre, zeta2)
Arguments
Y_pre |
Pre-treatment outcome matrix for control units (T_pre x N_co) |
Y_tr_pre |
Pre-treatment outcome vector for treated unit (T_pre x 1), averaged if multiple |
zeta2 |
Ridge penalty parameter (zeta^2). The code internally multiplies by T_pre per the paper. |
Details
This corresponds to equation (5) in Arkhangelsky et al. (2021).
Value
A numeric vector of length N_co holding the SDID unit weights
omega (non-negative and summing to one).
Non-parametric Inference for SI (Agarwal et al. 2025)
Description
Estimates SE and confidence intervals for the ATT via non-parametric cluster bootstrap or jackknife over control units. Works for both sharp and staggered SI fits. For staggered fits, bootstrap resamples each cohort's control pool independently, and jackknife uses a per-cohort LOO with delta-method variance aggregation.
Usage
si_inference(
fit,
method = c("bootstrap", "jackknife", "jackknife_global"),
n_boot = 499L,
level = 0.95,
alternative = c("two.sided", "greater", "less"),
seed = NULL
)
Arguments
fit |
A |
method |
|
n_boot |
Number of bootstrap replications (default 499L; ignored for jackknife). |
level |
Confidence level (default 0.95). |
alternative |
|
seed |
RNG seed for reproducibility (default NULL). |
Value
A list of class coresynth_inference.
SI-PCR: Synthetic Interventions via Principal Component Regression
Description
Implements the SI-PCR estimator of Agarwal et al. (2025). Uses the top-k SVD of pre-treatment control outcomes to find donor weights that predict each treated unit's pre-treatment trajectory, then applies those weights to post-treatment control outcomes.
Usage
si_pcr_cpp(Y_pre_co, Y_post_co, Y_pre_tr, k)
Arguments
Y_pre_co |
Pre-treatment control outcomes (T_pre x N_co) |
Y_post_co |
Post-treatment control outcomes (T_post x N_co) |
Y_pre_tr |
Pre-treatment treated outcomes (T_pre x N_tr) |
k |
Number of SVD components to retain |
Value
A list with:
-
W: Donor weight matrix (N_co x N_tr) -
Y_hat: Counterfactual post-treatment outcomes (T_post x N_tr)
Fast Matrix Completion using Soft-Impute Algorithm
Description
Solves: min_L (1/2) ||O o (Y - L)||F^2 + lambda * ||L||* via iterative SVD soft-thresholding (Mazumder, Hastie, Tibshirani 2010). Note: lambda is NOT normalized by |O|.
Usage
soft_impute_cpp(Y, O, lambda, max_iter = 1000L, tol = 1e-05)
Arguments
Y |
Observed outcome matrix (T x N). Unobserved entries should be 0. |
O |
Binary mask matrix (T x N): 1 = observed, 0 = missing (treated post). |
lambda |
Nuclear norm penalty (soft-threshold on singular values). |
max_iter |
Maximum iterations. |
tol |
Convergence tolerance (relative Frobenius norm change). |
Details
This is soft_impute_fe_cpp() with the fixed effects switched off.
Value
A numeric matrix of the same dimension as Y (T x N): the
completed low-rank matrix L that minimises the soft-thresholded
nuclear-norm objective.
Matrix Completion with Optional Two-Way Fixed Effects
Description
Solves
min over L, a, b of (1/2) * squaredFrobenius(O o (Y - L - a1' - 1b')) + lambda * nuclearNorm(L)
by alternating a back-fitting update of the fixed effects with one
soft-thresholded SVD step for L (Mazumder, Hastie & Tibshirani 2010).
Usage
soft_impute_fe_cpp(
Y,
O,
lambda,
estimate_fe = TRUE,
max_iter = 1000L,
tol = 1e-05,
fe_sweeps = 5L
)
Arguments
Y |
Observed outcome matrix (T x N). Unobserved entries should be 0. |
O |
Binary mask matrix (T x N): 1 = observed, 0 = missing (treated post). |
lambda |
Nuclear norm penalty (soft-threshold on singular values). |
estimate_fe |
Estimate unpenalised row and column effects. |
max_iter |
Maximum iterations. |
tol |
Convergence tolerance (relative Frobenius norm change). |
fe_sweeps |
Back-fitting sweeps per iteration. The mask makes the two effects non-orthogonal, so one closed-form pass does not solve for both. |
Details
The row and column effects are the unpenalised fixed effects of Athey et
al. (2021, S.8.1): "we regularize the estimates of L*, but do not wish to
regularize the estimates of the fixed effects". Leaving them inside L
shrinks the panel's level towards zero along with its factor structure,
which biases the imputed counterfactual whenever the level is far from
zero. estimate_fe = false reproduces the S.4 baseline model Y = L + e.
Both effects absorb an additive constant, so a and b are identified
only up to a shift between them; their sum, which is what enters the
completed matrix, is unaffected.
Value
A list with L (the penalised low-rank part), a (row effects),
b (column effects), and M, the completed matrix L + a 1' + 1 b'.
Tensor Unfolding (Matricization) for Synthetic Interventions
Description
Tensor Unfolding (Matricization) for Synthetic Interventions
Usage
tensor_unfold_cpp(T_cube, mode)
Arguments
T_cube |
A 3D array (cube) of dimensions (n1, n2, n3) |
mode |
The mode to unfold along (1, 2, or 3) |
Value
A numeric matrix: the mode-mode unfolding (matricization) of
T_cube, with dimensions n1 x (n2 * n3), n2 x (n1 * n3), or
n3 x (n1 * n2) for mode 1, 2, or 3 respectively.
Tidy an inference result
Description
Coerces a coresynth_inference or sdid_inference object to a one-row
tidy data.frame with broom-style column names so it can be combined
with regression output for paper tables.
Usage
## S3 method for class 'coresynth_inference'
tidy(x, conf.int = TRUE, ...)
Arguments
x |
A |
conf.int |
Logical. Include |
... |
Unused. |
Value
A one-row data.frame with columns term, estimate, std.error,
statistic, p.value, conf.low, conf.high, method, alternative,
n_controls, staggered.
Extract Outcome Series from a coresynth Fit
Description
Accessor generics that return the outcome series stored in a fitted
coresynth object under a uniform interface, regardless of the estimation
method:
Usage
treated_outcomes(x, ...)
## S3 method for class 'coresynth'
treated_outcomes(x, na.rm = FALSE, ...)
synthetic_outcomes(x, ...)
## S3 method for class 'coresynth'
synthetic_outcomes(x, na.rm = FALSE, ...)
## S3 method for class 'coresynth_staggered'
synthetic_outcomes(x, na.rm = FALSE, ...)
## S3 method for class 'coresynth_tasc'
synthetic_outcomes(x, na.rm = FALSE, ...)
donor_outcomes(x, ...)
## S3 method for class 'coresynth_scm'
donor_outcomes(x, ...)
## S3 method for class 'coresynth_sdid'
donor_outcomes(x, ...)
## S3 method for class 'coresynth_si'
donor_outcomes(x, ...)
## S3 method for class 'coresynth_gsc'
donor_outcomes(x, ...)
## S3 method for class 'coresynth_mc'
donor_outcomes(x, ...)
## S3 method for class 'coresynth_tasc'
donor_outcomes(x, ...)
## S3 method for class 'coresynth'
donor_outcomes(x, ...)
Arguments
x |
A |
... |
Passed to methods. |
na.rm |
Logical; passed to the per-period averaging over multiple
treated units (default |
Details
-
treated_outcomes(): the treated unit's observed outcome series (lengthT). When several units are treated it is their per-period average – weighted byweightswhere a sharp fit was given them, and unweighted across cohorts for a staggered fit, whose treated units do not share a treatment date. -
synthetic_outcomes(): the estimated counterfactual series (lengthT), i.e. the synthetic control or model-fitted outcome. -
donor_outcomes(): theT \times N_{co}matrix of observed donor (control unit) outcomes over all periods.
Each accessor returns NULL when the requested series is not stored in
the fit. Staggered-adoption fits keep their counterfactual per cohort (in
fit$cohort_fits), because collapsing it to one series would average
cohorts whose treatment starts at different times, so
synthetic_outcomes() returns NULL for them; use augment() for the
per-cohort series.
Value
For treated_outcomes() and synthetic_outcomes(), a numeric
vector of length T, or NULL. For donor_outcomes(), a
T \times N_{co} numeric matrix (donors in columns, named when unit
names are available), or NULL.
Examples
set.seed(1)
panel <- expand.grid(unit = 1:10, year = 1:20)
panel$treated <- as.integer(panel$unit == 1 & panel$year > 15)
panel$gdp <- panel$unit + 0.5 * panel$year +
rnorm(nrow(panel)) + 3 * panel$treated
fit <- scm_fit(gdp ~ treated | unit + year, data = panel, method = "scm")
y1 <- treated_outcomes(fit) # observed treated series
y1_0 <- synthetic_outcomes(fit) # synthetic counterfactual
Yco <- donor_outcomes(fit) # donor outcome matrix
all.equal(y1 - y1_0, unname(fit$gap))