Skip to contents

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.0

SCM — 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.040515

Out-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.073836

The 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.118604

SDID — 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.969398

The 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.01877909

r 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.961371

MC — 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.

fit_mc_auto <- scm_fit(y ~ d | id + time, data = dat, method = "mc")
fit_mc_man  <- scm_fit(y ~ d | id + time, data = dat, method = "mc", lambda = 1.0)
c(auto = fit_mc_auto$estimate, manual = fit_mc_man$estimate)
#>     auto   manual 
#> 2.083119 2.079777

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.

fit_tasc      <- scm_fit(y ~ d | id + time, data = dat, method = "tasc", r = 2)
fit_tasc_fixA <- scm_fit(y ~ d | id + time, data = dat, method = "tasc",
                         r = 2, fix_A = TRUE)
c(free_A = fit_tasc$estimate, fixed_A = fit_tasc_fixA$estimate)
#>   free_A  fixed_A 
#> 1.891103 1.973811

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.923958

Multi-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.04574832

SCM-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)