synth_inference() and generate_placebos() previously classified
any plot-horizon year not in time.optimize.ssr as post-treatment.
This silently mixed pre-treatment plot years before the SSR window
(e.g. 1955-1959 in the Basque example with
time.optimize.ssr = 1960:1969) into the post-period denominator,
inflating post_mspe, mspe_ratio, and the mspe_test() p-value.
Both now classify post as time.plot >= treatment_time, with the
default treatment_time = max(time.optimize.ssr) + 1. A new
treatment_time argument lets users override.
synth_inference(method = "conformal") now uses the order statistic
at rank k = ceiling((n + 1) * (1 - alpha)) instead of
quantile(.., 1 - alpha, type = 1). The former is the rank that
delivers exact finite-sample (1 - alpha) coverage under
exchangeability; the latter selects rank ceiling(n * (1 - alpha))
and slightly under-covers. When n is too small for the requested
coverage, the function now warns and returns Inf for
conformal_q instead of silently using the maximum residual.
synth() now accepts cvxr_pars and torch_pars lists for tuning
the quadopt = "cvxr" and quadopt = "torch" backends (e.g.
torch_pars = list(device = "mps") for Apple Silicon GPU). These
were supported internally by .solve_w() but were not previously
exposed at the public API.
generate_placebos() now exposes genoud, cvxr_pars,
torch_pars, and treatment_time. Match these to the configuration
that produced the real fit so placebos use the same optimizer and
the same post-period definition.
synth_inference() returns a prediction band around the synthetic
counterfactual. Two methods are available:
method = "conformal" (default) — split-conformal intervals
(Chernozhukov, Wuthrich, Zhu 2021), finite-sample valid under
exchangeability of pre-period residuals. Half-width is the
(1 - alpha)-quantile of |gap_pre|.method = "parametric" — Gaussian-residual intervals.
Half-width is qnorm(1 - alpha/2) * sd(gap_pre).Output is an S3 object of class c("synth_<method>", "synth_inference")
with print() and plot() methods. The plot() method overlays the
band on the treated and synthetic series.
generate_placebos(), mspe_test(), mspe_plot(), plot_placebos()
implement the in-space placebo workflow from Abadie, Diamond, and
Hainmueller (2010). generate_placebos() swaps each donor into the
treated slot, refits synth(), and returns a synth_placebos object.
mspe_test() returns a one-sided p-value via the empirical rank of
the treated unit's post/pre MSPE ratio. The function names match
those in the SCtools package by design — migration is a verbatim
rename and you can namespace-qualify (e.g., Synth::generate_placebos)
if both packages are loaded.
No new package dependencies. Optional parallel = TRUE in
generate_placebos() uses parallel::mclapply on non-Windows.
scpi
package (Cattaneo, Feng, Palomba, Titiunik).synth() and fn.V() gain two new opt-in values for quadopt:
quadopt = "cvxr" solves the W-step via the CVXR package
(default solver: OSQP, a hard Imports of CVXR and therefore
always available; alternative solvers "SCS", "ECOS", or
"MOSEK" can be selected via cvxr_pars). Adds no required
dependency; CVXR lives in Suggests: and is loaded only when
requested.quadopt = "torch" solves the W-step via Frank-Wolfe simplex
least squares using the torch package, with optional GPU/MPS
support (torch_pars = list(device = "cuda") or "mps"). Also
in Suggests:. The first use of torch in a session may
require torch::install_torch() to download libtorch.quadopt = "ipop" remains the default and produces output identical
to <= 1.1-10. The new backends agree with ipop on the canonical
examples to within solver tolerance and exist for users with larger
panels who prefer modern convex-optimization solvers (CVXR) or
autodiff/GPU machinery (torch). See the ?synth Details section
and the inference vignette for guidance on choosing a backend.
New quadopt_inner and quadopt_outer arguments on synth() and
generate_placebos() let users pick different backends for the two
QP stages. The V-search calls fn.V() hundreds of times via
optimx; running CVXR or torch on every call is much slower than
ipop. Setting quadopt_outer = "cvxr" (or "torch") with quadopt
left at "ipop" gives ipop's speed for the V-search and uses the
modern solver only for the single final W solve. Existing scripts
that pass quadopt = ... are unchanged: it now sets both
quadopt_inner and quadopt_outer.
print, plot) for the
new synth_inference and synth_placebos classes. Existing
functions (synth, dataprep, path.plot, gaps.plot,
synth.tab, etc.) are unchanged in behavior and signature.synth() and fn.V(): quadopt = "LowRankQP" no longer falls
through to undefined-variable code. The deprecation message is now
raised via stop() rather than cat(), which matches the user's
expectation that the function fails fast on an unsupported option.
dataprep(): the missing-data warning loop in the X0 (control
predictors) section now iterates over nrow(X0) instead of
nrow(X1). Previously it covered only the first
length(time.predictors.prior) rows, missing roughly
(n_controls - 1) / n_controls of the cells.
synth(): the error message for ncol(Z0) < 2 no longer mentions
"specify only one treated unit" (it is checking for at least two
control units).
path.plot(): Ylim is now padded by a fraction of the data
range, not by 0.3 * Y.min. The old formula gave
0.7 * Y.min for negative Y.min, which moved the lower bound
toward zero and cropped the bottom of negative-valued series.
synth(): messages such as "X1, X0, Z1, Z0 all come directly from
dataprep object", "searching for synthetic control unit", and the
final summary block printing MSPE (LOSS V) and the solution
vectors are now gated by verbose = TRUE. With the default
verbose = FALSE, synth() runs silently. Set verbose = TRUE
to restore the previous chatter.synth.R.is.vector(X0.scaled) defensive branch in
synth.R (ncol(X0) < 2 errors out earlier)."\ Please..." → "\n Please...",
"specificy" → "specify", "on time period" → "one time period",
"at least on predictor" → "at least one predictor",
"variabale" → "variable", "mispecified" → "misspecified",
"supoorted" → "supported", "synthtic" → "synthetic".synth() and fn.V(): cleaned up the unsupported-quadopt
branch (also clarifies error messages).