coresynth exposes six estimators through scm_fit() plus
an experimental-design variant (scm_design()). This article
covers the method-specific options for each. For the basics of the
shared interface, see Get
started. For inference, see Inference;
for multi-period adoption, see Staggered
adoption.
We use one synthetic panel throughout, with two auxiliary covariates so we can demonstrate predictor- and covariate-based matching.
set.seed(1)
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$income <- as.vector(outer(f * 0.5, lam)) + rnorm(nrow(dat), 0, 0.3) # predictor
dat$x <- rnorm(nrow(dat)) # time-varying cov
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.0SCM — Synthetic Control Method
Abadie, Diamond & Hainmueller (2010). Donor weights on
the simplex match the treated unit’s pre-treatment trajectory. Several
units adopting at the same date are one cohort, and are matched through
their average: Ben-Michael, Feller & Rothstein (2022, S.4.1) show
the across-unit variation that would justify fitting them separately is
zero when the adoption date is shared. The fit keeps the raw per-unit
series in Y_treat_mat and reports
n_treated.
Predictors via pred()
By default (predictors = NULL) all pre-treatment
outcomes form the predictor matrix. To match on covariates and specific
outcome lags instead, build a list of pred(vars, times, op)
specs:
fit_scm <- scm_fit(
y ~ d | id + time, data = dat, method = "scm",
predictors = list(
pred("income", 1:T_pre), # mean income over the pre-period
pred("y", T_pre), # outcome in the last pre-period
pred("y", 1:4, op = "mean") # outcome averaged over early pre-period
)
)
fit_scm$estimate
#> [1] 2.040515Out-of-sample V selection, donor filtering, penalisation
# Abadie (2021) S.3.2: split the pre-period to choose V out of sample
fit_oos <- scm_fit(y ~ d | id + time, data = dat, method = "scm",
v_selection = "oos")
# Abadie (2021) S.4: drop poorly-fitting donors by individual MSPE ratio
fit_filt <- scm_fit(y ~ d | id + time, data = dat, method = "scm",
donor_mspe_threshold = 5)
# Abadie & L'Hour (2021): penalised SCM (auto-selects the penalty out of sample)
fit_pen <- scm_fit(y ~ d | id + time, data = dat, method = "scm",
lambda_pen = "auto")
c(oos = fit_oos$estimate, filtered = fit_filt$estimate, penalised = fit_pen$estimate)
#> oos filtered penalised
#> 1.994665 2.073842 2.073836The inner QP projects the treated unit onto the convex hull of the
donor columns in predictor space, so by Caratheodory’s theorem an
optimum supported on at most k + 1 donors exists.
qp_solver = "auto" (the default) uses the Wolfe (1976)
min-norm-point method, which returns such a solution, wherever it is
wired, and the active-set method on the paths that run their own
solver.
Augmented SCM
augment_scm() applies the ridge bias-correction of
Ben-Michael, Feller & Rothstein (2021):
aug <- augment_scm(fit_scm)
c(scm = aug$att_scm, augmented = aug$att_aug)
#> scm augmented
#> 2.040515 2.118604SDID — Synthetic Difference-in-Differences
Arkhangelsky et al. (2021). Combines unit and time weights with a DiD-style double differencing. Time-varying covariates are partialled out first (Clarke et al. 2024):
fit_sdid <- scm_fit(y ~ d | id + time, data = dat, method = "sdid",
covariates = "x")
fit_sdid$estimate
#> [1] 1.969398The double differencing also means the estimate is not the average
gap between the treated series and the synthetic one. SDID subtracts the
pre-treatment level under its own time weights, so
augment()’s .resid over the post-treatment
periods averages to something else, and the two differ by however much
those weights depart from a flat average. For the other five estimators
the counterfactual is the whole story and the two agree; read the SDID
series as a picture of the fit, and fit$estimate as the
effect.
GSC — Generalised Synthetic Control
Xu (2017). Interactive fixed effects estimated by SVD;
r sets the number of latent factors, and
r=NULL (the default) chooses it by the leave-one-period-out
cross-validation of the paper’s Algorithm 1, keeping the scoring table
in fit$r_cv. With covariates the factors and the
coefficients are estimated by the iteration of Bai (2009), run to
convergence, and fit$beta holds the covariate
coefficients.
By default the model fitted is
Y_it(0) = x_it'beta + lambda_i'f_t + e_it, the paper’s
Equation (1), with no additive unit or time effects beside the factors.
A unit or time effect is a factor with a constant loading, so the factor
structure absorbs it once r is large enough.
force adds them explicitly instead – "unit",
"time" or "two-way", the extended model of the
paper’s Remark 3 – in which case r=0 (the additive effects
alone) is among the candidates the cross-validation considers. Estimates
move with r and force on panels where the
factor structure is weak, which is what fit$r_cv is there
to show.
fit_gsc <- scm_fit(y ~ d | id + time, data = dat, method = "gsc",
r = 2, covariates = "x")
c(estimate = fit_gsc$estimate, beta = unname(fit_gsc$beta))
#> estimate beta
#> 1.89883001 0.01877909r and force sensitivity is easy to
sweep:
sapply(1:3, function(rr)
scm_fit(y ~ d | id + time, data = dat, method = "gsc", r = rr)$estimate)
#> [1] 1.912170 1.905871 1.875384
fit_gsc_fe <- scm_fit(y ~ d | id + time, data = dat, method = "gsc",
force = "two-way")
c(r = fit_gsc_fe$r, estimate = fit_gsc_fe$estimate)
#> r estimate
#> 3.000000 1.961371MC — Matrix Completion
Athey et al. (2021). Nuclear-norm-regularised completion
(Soft-Impute). lambda = NULL (the default) selects the
penalty by the paper’s K-fold cross-validation; pass a number to
override. Unit and time fixed effects are estimated outside the nuclear
norm, as in their S.8.1, so the penalty shrinks the factor structure
without shrinking the panel’s level; fixedeff = FALSE
recovers the S.4 baseline.
TASC — Time-Aware Synthetic Control
Rho et al. (2026). A state-space model fitted by the paper’s
EM algorithm (Kalman filter and smoother, then the exact M-step), run
until the fitted values settle: em_tol sets the tolerance
and em_iter caps the iterations. r sets the
latent state dimension and fix_A = TRUE fixes the
transition matrix at the identity. Each unit has its own intercept in
the observation equation (intercept = TRUE); without one
the states have to carry every unit’s level, and on panels far from zero
the EM wanders along directions the likelihood barely tells apart, so
the estimate depends on when it stops.
SI — Synthetic Interventions
Agarwal et al. (2025). SI-PCR uses a truncated SVD of the
donor pre-period. k sets the rank; k = NULL
(the default) applies the optimal hard threshold of Gavish & Donoho
(2014) to the pre-treatment donor block, one of the data-driven rules
the paper names.
fit_si <- scm_fit(y ~ d | id + time, data = dat, method = "si")
fit_si$estimate
#> [1] 1.923958Multi-arm SI
SI uniquely supports multiple treatment arms (K > 1).
When the treatment column takes values 0, 1, ..., K,
scm_fit() automatically routes to the multi-arm path: the
control-arm SVD basis is shared across all arms (the unit factors are
arm-invariant). Use panel_to_tensor() to prepare/inspect
the tensor structure.
set.seed(7)
Nc <- 10; T2 <- 16; Tp <- 10
U <- matrix(rnorm(T2 * 2), T2, 2)
V <- matrix(rnorm((Nc + 4) * 2), Nc + 4, 2)
Lam <- rbind(c(1, 1), c(1.4, .6), c(.6, 1.4)) # arm 0, 1, 2
arm_unit <- c(rep(0L, Nc), 1L, 1L, 2L, 2L)
g <- expand.grid(time = seq_len(T2), id = seq_len(Nc + 4))
g$arm <- arm_unit[g$id]
g$d <- ifelse(g$arm == 0L, 0L, ifelse(g$time > Tp, g$arm, 0L))
g$y <- mapply(function(t, i, a)
sum(U[t, ] * Lam[a + 1L, ] * V[i, ]) + 50 + rnorm(1, sd = .3),
g$time, g$id, g$arm)
fit_multi <- scm_fit(y ~ d | id + time, data = g, method = "si")
fit_multi$arm_estimates # per-arm ATTs
#> 1 2
#> -0.2139400 0.1224433
fit_multi$estimate # weighted aggregate
#> [1] -0.04574832SCM-Design — experimental design
Abadie & Zhao (2026). scm_design() has a
different, design-oriented interface (it selects which units to expose
to a planned intervention). It offers "base",
"weakly_targeted", and "unit_level" variants
with blank-period permutation tests and split-conformal CIs.
des <- scm_design(
data = dat, outcome = "y", unit = "id", time = "time",
T0 = T_pre, design = "base"
)
des
#> === scm_design (Abadie & Zhao 2026) ===
#> Design variant : base
#> Treated units : u3
#> ATT estimate : 0.0082
#> p-value : NA (no blank periods)