coresynth 0.4.0.9000
Changed
Sharp SCM fits with
predictors = NULLnow emit a one-line message naming the default model (“using the outcome in each of the k pre-treatment periods as predictors”), so the implied specification is visible rather than only documented. The message is suppressed where it would be noise: staggered fits (always outcomes-only, no alternative) andv_selection = "oos"(its own split is described under that argument). Wrap the call insuppressMessages()to silence it.A predictor specification consisting solely of the outcome at each single pre-treatment period is now fitted through the outcomes-only path, returning a fit identical to
predictors = NULL(with a message). Such a spec builds the same X0/X1 as the outcomes-only fit – it is the standard way reference implementations express an outcomes-only SCM (Synthrequires it: with no predictors at all,dataprep()errors) – but previously it entered the predictor path (per-row SD scaling + multi-start V search), which could return slightly different weights for the same model and ran orders of magnitude slower. Verified on Proposition 99:predictors = NULL, the per-period spec, andSynth’s per-periodspecial.predictorsrun now agree (weights within 3e-3 ofSynth, identical between the two coresynth forms). Passv_optim = "multistart"explicitly to force the predictor-path optimiser for such a spec.
Performance
Predictor matrix construction is no longer a bottleneck for predictor-based SCM fits.
build_predictor_matrices()aggregated each predictor row with one full scan of the input data frame per unit (O(rows x units x data size)); with manypred()entries – e.g. one outcome lag per pre-treatment year – this R-level loop dominated the fit time (1.75 s of a 3.9 s fit at N_co = 150, T_pre = 80, 15k rows). Each predictor row is now computed with a single grouped aggregation (0.06 s for the same spec, ~29x). Output is bit-identical to the previous implementation across ops (mean/median/sum), id types, NA handling, and the empty-window error path, so every fit is unchanged. The same per-unit-scan pattern was removed frombuild_covariate_array()(the covariate paths of GSC, SDID, and staggered SCM) and fromscm_design()’s predictor builder, likewise with bit-identical output.Outcomes-only SCM fits are ~2-2.5x faster again. Hardening the inner active-set QP for the predictor multi-start search (0.4.0) replaced the cheap bordered-KKT face solve with a scale-robust null-space projection on every pivot. That robustness is only needed when
predictorsare supplied (rank-deficient V, scale invariance under predictor rescaling); the outcomes-only path – dense V, well-conditioned faces, no rescaling – paid the cost without the benefit, running roughly twice as slow as 0.3.x despite returning the identical fit. The outcomes-only inner QP now uses the cheap bordered-KKT solve again (scm_weights_cpp(cheap_face = TRUE), wired automatically for outcomes-only fits, staggered SCM, the outcomes-only in-space placebo,placebo_in_time(), and OOS V selection). Predictor-based fits and the multi-start search keep the null-space solver, so their results and scale invariance are untouched. Outcomes-only results are bit-identical to the 0.2/0.3 line and differ from 0.4.0 only at round-off (<= 5e-14 in weights), i.e. not observably. Verified againstSynthon Proposition 99 (ATT, synthetic series, pre-RMSPE, donor weights all agree) and across a degeneracy/noise sweep.
coresynth 0.4.0
CRAN release: 2026-07-20
Breaking changes
-
Predictor-based SCM fits now default to a multi-start outer V optimisation (
v_optim = "auto", the new default, resolves to"multistart"whenpredictorsare supplied and to the single-start"coord_descent"for outcomes-only fits). The outer problem – choose V so that the implied donor weights minimise pre-treatment outcome MSPE – is non-convex, and the previous single start from uniform V could settle in a poor basin: on the Proposition 99 predictor specification it reported a pre-treatment SSR of 137.8 where 58.5 is attainable (below the referenceSynth/tidysynthsolution’s 78.1), halving the treated unit’s MSPE ratio. The multi-start search screens a fixed, fully deterministic start set (uniform, one-hot per predictor, 100 fixed-seed draws) with warm-started exact inner QPs, then runs the leaders through a Nelder-Mead / coordinate-descent refinement pipeline; its solution is never worse (in pre-treatment loss) than the single-start path, and no R RNG state is consumed.mspe_ratio_pval()applies the same search to every placebo refit so the permutation test stays symmetric. Numeric results for predictor-based fits therefore change (toward better pre-treatment fits); passv_optim = "coord_descent"to reproduce the previous behaviour. Outcomes-only fits are unchanged. Note that the old"auto"(choose"bfgs"whenk <= 15) is gone;"bfgs"itself remains available.
New features
-
plot(fit, type = "pred_weights")draws the predictor/variable weight matrix of a sharp SCM fit as a horizontal bar chart, the companion to the existing donortype = "weights"chart. Bars are labelled with the predictor names (orV1..V_Tprefor outcomes-only fits) and honour thefillandtop_narguments. Staggered SCM and the other estimators do not estimate aVmatrix and raise an informative error. -
plot_data()returns the tidydata.framebehind anyplot()view, taking the sametypeargument ("trend","gap","weights","pred_weights"for a fit;"gaps","ratios"for anscm_placeboobject). It is the data-side companion toplot(): extract the plotted series, weights, or placebo paths to relabel the simplified series names, feed them into a table, or build a bespoke figure. Columns use plain names (time,value,series, …), and the arguments that change which rows or values appear (align,top_n,show_donors,mspe_prune) are honoured. The cosmetic near-zero-weight filter thattype = "weights"applies to the chart is not used here, so every donor is returned (usetop_nto subset). -
v_windowargument inscm_fit()(sharp SCM fits): a vector of pre-treatment time values over which the outer V optimisation evaluates the pre-treatment fit, e.g.v_window = 1975:1988. The default (NULL) keeps the current behaviour of evaluating on all pre-treatment periods. The window restricts only the outer evaluation loss – predictor matrices and the reported full-windowlossare untouched – andmspe_ratio_pval()mirrors it in every placebo refit. Cannot be combined withv_selection = "oos", which manages its own train/validation split. -
colors/labelsinplot.coresynth()andplot.scm_placebo()are now keyed by one-word series identifiers:treated,synthetic, and (withshow_donors > 0)donorsfor trend plots;treatedandplacebofor the placebo plots. The previous keys were the displayed legend strings (Treated,Synthetic Control,Placebo (donor pool)), which required quoting for the multi-word names and conflated series identity with the legend text thatlabelsitself relabels. Writecolors = c(treated = "black")instead ofc(Treated = "black"); an unknown key errors with the list of valid keys. The displayed legend text is unchanged. -
mspe_ratio_pval()placebo refits now mirror the treated fit’s predictor specification by default:use_covariatesdefaults toNULL(auto) instead ofFALSE. When the fit was estimated with apredictorsspecification, each placebo unit is now refit with that same specification – the Abadie et al. (2010) /Synthconvention, under which the treated and placebo test statistics are computed under one common spec. Previously the default silently switched the placebo refits to the outcomes-only spec, which (a) made the permutation compare statistics that were not exchangeable with the treated one, and (b) was extremely slow for long pre-periods because each placebo re-ran aT_pre-dimensional V optimisation. Outcomes-only fits are unaffected (auto resolves to the outcomes-only placebo path, as before). Passuse_covariates = FALSEexplicitly to reproduce the old behaviour on covariate fits. -
Treatment-line placement in plots:
plot.coresynth()(type = "trend"and"gap") andplot.scm_placebo()(type = "gaps") gain avline_offsetargument controlling where the vertical treatment line is drawn, in periods relative to the first post-treatment period (the previous fixed position).vline_offset = -1moves it to the last pre-treatment period, and fractional values such as-0.5interpolate between adjacent observed times, on numeric,Date, andPOSIXctaxes alike. Thevlinestyle list now also accepts anxinterceptelement for one or more absolute positions (previously an error), e.g.vline = list(xintercept = "1989-01-01")on aDateaxis. Hiding the line still works viavline = FALSE. -
Level-aligned trend and gap plots:
plot.coresynth()gains analignargument fortype = "trend"and"gap".align = TRUEshifts the synthetic series by its pre-treatment level gap to the treated series, so both are drawn on the same level. SDID’s unit weights are estimated with the intercept concentrated out of the QP (Arkhangelsky et al. 2021), so its raw trend plot can show the synthetic control at a different level than the treated unit;align = TRUEuses the time-weight (lambda) weighted pre-period gap, which makes the average post-period gap in the plot equal the SDID estimate exactly. -
Donor paths in trend plots:
plot.coresynth(type = "trend")gains ashow_donorsargument that draws the outcome paths of theshow_donorsdonor units with the largest weights as thin background lines (Infshows all). The new"Donors"series participates incolors/labelsoverrides. -
SDID time weights in the weights plot: for SDID fits,
plot.coresynth(type = "weights")now shows two panels — donor unit weights (omega) and pre-period time weights (lambda) — in the same bar style. Other methods keep the single unit-weight panel. -
Placebo SE and CI for SDID:
sdid_inference(method = "placebo")now also reports the placebo-distribution standard error (Clarke et al. 2023, Algorithm 4) and the corresponding normal-approximation confidence interval, for both sharp and staggered fits. The p-value is unchanged (permutation-based).tidy()/glance()pick the new columns up automatically. -
Partially pooled staggered SCM (Ben-Michael, Feller & Rothstein 2022, JRSS-B):
scm_fit(method = "scm")on a staggered panel gains anuargument.nu = NULL(default) keeps the previous behaviour (per-cohort V-optimised SCM). A numericnuin[0, 1]minimises the convex combinationnu * (pooled pre-treatment imbalance)^2 + (1 - nu) * (per-cohort imbalance)^2over all cohort weight vectors jointly, so that the aggregate ATT is anchored by the pooled fit (nu = 0reproduces separate per-cohort SCM with uniform lag weights,nu = 1is fully pooled).nu = "auto"selects the paper’s heuristic value. The fit stores balance diagnostics infit$pooling(q_sep,q_pool, and their separate-SCM baselines). -
Intercept-shifted staggered SCM: new
fixedeffargument for staggered SCM fits.fixedeff = TRUEdemeans every unit by its own pre-treatment mean within each cohort before fitting (Ben-Michael, Feller & Rothstein 2022, Section 5.1; Doudchenko & Imbens 2017; Ferman & Pinto 2021), turning the estimator into a weighted difference-in-differences. Works with both the default path and the partially pooled path. -
Wild bootstrap inference for staggered SCM: new
scm_inference()function implements the weighted multiplier (wild) bootstrap of Ben-Michael, Feller & Rothstein (2022, Section 5.3): donor weights are kept fixed and per-treated-unit effect contributions are perturbed with golden-ratio two-point multipliers. Returns a standardcoresynth_inferenceobject (works withtidy()/glance()). This is the first inference method available for staggered SCM fits. -
solve_simplex_qp()gains an optionalx0warm-start argument, used by the partially pooled block coordinate descent to restart FISTA from the previous block solution. Together with an objective-based stopping rule this makes the pooled path roughly an order of magnitude faster on larger donor pools (N = 100: ~0.8 s to ~0.06 s per fit). Validated against the reference implementationaugsynth(weights correlate at 1.0, identical heuristicnu, equal pooled imbalance atnu = 1).
Deprecated
-
v_optim = "bfgs"is deprecated and will be removed in a future release. It is a single-start L-BFGS-B outer optimiser with no advantage over the alternatives now that the default is multi-start: usev_optim = "multistart"(or the"auto"default) for predictor-based fits, and"coord_descent"for outcomes-only fits. Passing"bfgs"still works but now emits a deprecation warning."coord_descent"is not deprecated: besides being a selectable optimiser, it is the engine for outcomes-only and staggered fits and the reference against which the multi-start never-worse guarantee is defined.
Performance
-
The multi-start outer search runs at interactive speed. Outer-loss evaluations are KKT-gated warm-started active-set solves; the active set gained Bland’s-rule anti-cycling (engaged only beyond 30 pivots, so previously terminating pivot paths are untouched) and now solves each face subproblem in the null space of the sum constraint via a Cholesky/eigendecomposition hybrid that is robust to the rank-deficient metrics (k < |active set|) the multi-start screen hits constantly – and whose branch decision cannot be flipped by last-bit input perturbations, preserving the scale-invariance of
scale_predictors. On a degenerate face the warm active-set solve can churn to its pivot cap, and the old cold-FISTA fallback (thousands of iterations on ill-conditioned metrics) made a single slow donor dominate placebo wall time; a finite-termination Lawson-Hanson NNLS now seeds the exact face solve on those cases in microseconds, gated by the same KKT check so the returned solution is unchanged. The rescue is confined to the multi-start internals – every other path (outcomes-only,coord_descent,oos, staggered,loo_donors(), conformal) keeps its historical solver bit-for-bit, and the multi-start uniform-start leg stays on the plain path so the never-worse guarantee holds. Candidate refinement pipelines run in parallel, and the placebo battery flattens the (donor x candidate) task list into one schedule – donor-level parallelism alone lets the slowest donor pin a thread while the others go idle. On the Proposition 99 8-predictor spec a full multi-start fit takes ~0.06s and the complete 38-donormspe_ratio_pval()battery ~1.7s (every donor’s fit verified never worse than the single-start path). A side effect of the exact face solves: inner QPs on rank-deficient faces are now solved exactly where the previous code fell back to a loosely-converged FISTA iterate. Outcomes-only sharp, staggered, and placebo results are bit-identical to the previous release;v_selection = "oos"outcomes fits (where the training half has fewer rows than donors) can shift slightly, toward better fits. -
The nested V/W coordinate descent is orders of magnitude faster. The solver behind
scm_weights_cpp()and the outcomes-only placebo loop inscm_placebo_cpp()now solves each inner simplex QP with a warm-started active-set method (KKT-verified exact solve, with FISTA fallback), applies V-coordinate changes to the Gram matrix as implicit rank-1 updates instead of rebuildingX0' V X0per grid point, and computes the Lipschitz constant once per coordinate-descent sweep instead of once per QP. On a monthly panel withT_pre = 139,T_post = 19, and 75 donors, the full 75-unit outcomes-only placebo run drops from roughly an hour to about 2 seconds, and a single outcomes-onlyscm_fit()from ~75 s to ~0.1 s. Per-unit results agree with the previous implementation within the coordinate-descent convergence tolerance: the estimator and its grid/accept/normalisation semantics are unchanged, and the inner QP solutions are now exact rather than first-order-approximate. -
Covariate-spec placebo refits now run in parallel. When the fit was estimated with a
predictorsspecification,mspe_ratio_pval()previously refit each placebo unit in a sequential R loop; the loop now runs in C++ under OpenMP (new low-level routinescm_placebo_x_cpp(), the covariate counterpart ofscm_placebo_cpp()). Each leave-one-out problem is solved by the same coordinate-descent core as before, so per-unit results are identical to machine precision – only the wall time changes. The speedup is bounded by the slowest single placebo unit (iterations are distributed dynamically across cores), so it approaches the core count when per-unit costs are uniform and is smaller when one hard-to-fit donor dominates.
Bug fixes
-
plot.coresynth()no longer embeds non-ASCII characters (Greek letters, the Unicode minus sign) in plot titles and subtitles. Some platforms cannot compute text metrics for these characters outside a UTF-8 locale, which crashedR CMD check --run-dontteston macOS.
coresynth 0.3.0
CRAN release: 2026-07-12
New features
-
Outcome-series accessors: new exported generics
treated_outcomes(),synthetic_outcomes(), anddonor_outcomes()return the treated series, the estimated counterfactual, and the donor outcome matrix from anycoresynthfit under a uniform interface, regardless of the estimation method. They replace the per-method field sniffing previously duplicated acrossaugment(),conformal_inference(), andplot(); each returnsNULLwhen the series is not stored in the fit (e.g. staggered fits, which keep their data per cohort infit$cohort_fits). -
Structural subclasses: fits with staggered adoption now additionally inherit from
"coresynth_staggered", and multi-arm SI fits from"coresynth_multiarm". S3 methods (print(),summary(),tidy(),augment()) dispatch on these subclasses instead of branching on internal flags. Thestaggered/multi_armlist fields are retained, so existing code that reads them keeps working, and all class checks remaininherits()-compatible. -
In-space placebo visualization (Abadie, Diamond & Hainmueller 2010, Section 3.4):
mspe_ratio_pval()now returns anscm_placeboobject (still fully backward compatible with the previous plain-list fields) that additionally carries the placebo gap path for every donor unit. A newplot.scm_placebo()method renders the two companion figures from the paper:type = "gaps"overlays the treated unit’s gap on the donor-pool placebo gaps (Figures 4-7, withmspe_pruneimplementing the paper’s relative pre-treatment MSPE pruning at 20x/5x/2x), andtype = "ratios"plots the post/pre-treatment MSPE ratio for every unit (Figure 8). -
Plot style customization:
plot.coresynth()andplot.scm_placebo()gaincolors,labels,vline,hline, and (fortype = "weights")fillandtop_narguments.top_nrestricts the weights bar chart to thetop_nlargest-weight donors (defaultInfkeeps every donor with a non-negligible weight), useful for large donor pools.vline/hlineaccept a list ofgeom_vline()/geom_hline()aesthetic overrides merged onto the built-in defaults, orNULL/FALSEto suppress the reference line entirely — since a line already added to a returnedggplotobject cannot be removed, only restyled or overplotted, suppression has to happen inside the plot method.colorsaccepts a named vector overriding individual series colors (unmentioned series keep their default).labelsaccepts a named vector overriding the legend text of individual series (e.g.labels = c(Treated = "California")); keys are always the original series names, socolorsandlabelscompose independently, and intype = "ratios"the treated unit’s axis tick follows the relabeled legend entry. All defaults reproduce the previous appearance exactly, so existing calls are unaffected.
Improvements
-
augment(fit, include_donors = TRUE)now returns donor rows formethod = "si"fits too (previously it warned that control outcomes were unavailable and returned treated rows only). Estimates are unaffected. -
plot()on a staggered fit now fails with a clear message explaining that staggered fits store their series per cohort, instead of an internaldata.framelength error. - The gap plot (
plot(fit, type = "gap")) now labels its y-axis simply “Gap” and states the definition (“Treated − synthetic control”) in a new subtitle, replacing the previousY_treated - Y_syntheticaxis label. The placebo gaps plot (plot(<scm_placebo>, type = "gaps")) uses the same “Gap” axis label — there each line is that unit’s gap relative to its own synthetic control, so the old parenthetical was dropped rather than reworded. Cosmetic only; no computed values change.
Bug fixes
- For
method = "tasc",plot(),augment(), and theY_synthseries ofexport_json()reported the average fitted value of all units as the counterfactual: TASC stores its fitted values as a full T x N matrix (Y_hat), unlike the other methods, and the shared extraction code averaged over every column. The treated unit’s columns are now used (synthetic_outcomes()handles this per method), so the plotted/augmented counterfactual, gap, and residuals fortascfits change; the ATT estimate itself was always computed from the correct per-unit gaps and is unaffected. -
scm_design()solved theweakly_targeted(eq. 9) andunit_level(eq. 10) designs by a sequential approximation: the treated weightswwere always chosen to match the population average predictor vector alone, which made thexipenalty of eq. 10 effectively inert and left the eq. 9 objective jointly suboptimal. Both designs are now solved exactly for every candidate treated set — eq. 9 by two-block alternating minimisation of the jointly convex QP in(w, v), eq. 10 by folding the per-unit synthetic-control losses into the treated-weight QP target — and the solutions have been verified against an independent exact QP solver. This changes numerical results forscm_design(design = "weakly_targeted")anddesign = "unit_level"whenm >= 2(the defaultm = 1selects a single treated unit, sowis degenerate and both old and new solvers agree).
Internal
-
conformal_inference()’s counterfactual refit now dispatches on the fit’s class (one S3 method per estimator) rather than anif-chain on the method string. Results are unchanged.
coresynth 0.2.4
CRAN release: 2026-07-04
Bug fixes
-
method = "mc"treated missing(id, time)panel cells andNAoutcomes as observed zeros instead of excluding them from the observation mask used by the Soft-Impute solver. Missing cells are now masked out the same way as treated post-adoption cells. This changes numerical results formcfits on unbalanced panels or panels withNAoutcomes. -
method = "tasc"’s EM loop already handles missing outcome cells (Kalman smoother plus per-unit M-steps), but its initialisation (svd(), the treated-unit loading OLS, andvar()) failed on panels with missing cells. Initial values are now computed from a column-mean-imputed matrix; the EM loop itself still runs on the true, unimputed data.
Input validation
Malformed panels previously produced confusing or misleading errors — for example, a 0-row data frame (from an upstream filtering bug) was misclassified as staggered adoption and failed with an unrelated “predictors not supported” error, and missing (id, time) cells surfaced as a raw eig_sym(): decomposition failed from the C++ layer. panel_to_matrices() (shared by all six estimators) and scm_design() now validate and error clearly on:
- 0-row input data
-
NAunit or time identifiers -
NAor negative treatment indicator values - no treated units, or (for sharp fits only) no control units — staggered fits may legitimately have no never-treated units when future adopters serve as clean controls
- missing
(id, time)cells or non-finite outcomes, for the estimators that require a fully observed panel (scm,sdid,gsc,si—mcandtaschandle missing data by design)
scm_fit() also now rejects non-numeric (factor/character) outcome or treatment columns and non-integer treatment values, instead of silently coercing them (as.integer(factor(...)) returns level codes, not the original values). “All cohort-level fits failed” errors (SCM/SDID/GSC/SI staggered paths) now point back to the preceding per-cohort warnings for diagnosis.
coresynth 0.2.3
Performance
- The inner simplex QP solvers (
solve_simplex_qp()/solve_simplex_qp_lr()), used by everyscm_fit(method = "scm")fit and by themethod = "sdid"unit weights, now use FISTA with adaptive restart (O’Donoghue & Candès 2015, gradient scheme). The first-order solver’s iteration count grows with the condition number ofQ = X0'VX0, which is large for the collinear pre-treatment outcome panels typical of synthetic control; resetting the momentum term when it works against the gradient removes this slowdown. Each inner solve converges to the same optimum (verified against an exact QP solver to within1e-10), so the returned weights are unchanged. - For the common outcomes-only case the fit is effectively identical (differences
~1e-5). Forv_selection = "oos"and penalised (lambda_pen) fits on ill-conditioned panels, the non-convex outer V search may now settle on a different local optimum, shifting results slightly; both the previous and the new solutions are valid SCM fits with the same objective. This can change numerical results forv_selection = "oos"and penalised fits on poorly conditioned data.
coresynth 0.2.2
CRAN release: 2026-06-26
Bug fixes
-
panel_to_matrices()(and therefore everyscm_fit()method) andscm_design()now error on duplicate(id, time)entries instead of silently keeping a single arbitrary row. A balanced panel requires each unit-time cell to be unique; duplicates were previously overwritten by the last matrix-index assignment, dropping data without warning. The error reports the number of offending rows and the first duplicated unit and time. -
plot()now rendersDate/POSIXcttime axes correctly. The time vector was coerced withas.numeric(), so dates appeared as days-since-epoch (e.g. 16000,- on the x-axis.
Date/POSIXctvalues are now passed through unchanged so ggplot2 selects the appropriate date scale; onlycharacter/factortime values are coerced to numeric.
- on the x-axis.
coresynth 0.2.1
Bug fixes
-
v_selection = "oos"(outcomes-only case) previously fit candidateW(V)on the full pre-treatment outcome matrix and restricted only the MSPE evaluation to the validation window, allowing the V optimiser to fit the validation period indirectly (a data leak relative to Abadie (2021) S.3.2). The new.scm_oos_outcomes()implements the correct train/validation split: candidateW(V)are fitted on training-half outcomes only,V*minimises validation-half MSPE, andW*is refit withV*on the outcomes of the lastfloor(T_pre/2)pre-treatment periods. For OOS fits,v_weightsnow hasfloor(T_pre/2)entries and a newv_rowsfield records which periods they refer to. This changes numerical results forv_selection = "oos".
New features
-
scale_predictors(defaultTRUE): predictor rows supplied viapredictors =are now divided by their standard deviation across all units before optimisation, matching the Synth reference implementation (Abadie, Diamond & Hainmueller 2011, JSS).predictor_tablecontinues to report values on the original scale. This changes numerical results for SCM fits with user-suppliedpredictorsunlessscale_predictors = FALSE. -
placebo_in_time(): in-time placebo (backdating) test for sharp SCM fits (Abadie, Diamond & Hainmueller 2015; Abadie & Vives-i-Bastida 2022). -
loo_donors(): leave-one-out donor robustness check with the predictor weights V held fixed (Abadie, Diamond & Hainmueller 2015, footnote 20). -
build_predictor_matrices()now errors with an informative message if apred()time window produces missing or non-finite predictor values.
coresynth 0.2.0
CRAN release: 2026-06-12
New features
-
Conformal inference (
conformal_inference()): permutation-based p-values and confidence intervals following Chernozhukov, Wüthrich & Zhu (2021). Works with sharp fits across all supported estimation methods (scm,sdid,gsc,mc,si). The counterfactual proxy is re-estimated under the null on all T periods (essential for finite-sample validity per CWZ S.2.2), and p-values are obtained via moving-block (cyclic-shift) permutation of the estimated residuals. Confidence intervals are constructed by test inversion over a user-supplied or automatically chosen grid. Returns acoresynth_inferencesubclass compatible withtidy()andglance().
Minor improvements
-
panel_to_matrices(): fill loop replaced by vectorisedmatch()+ matrix-index assignment; removes an O(n × (T + N)) bottleneck in the shared data-prep path. -
tasc.cpp:safe_inv_sympd()helper added so the Kalman filter degrades topinvinstead of aborting when the innovation covariance is not numerically PD. -
%||%null-coalescing helper centralised inutils.R; duplicate definitions inbroom.Randplot.Rremoved. -
check_sharp_adoption()(unused internal function) removed.
coresynth 0.1.0
First public release.
Methods
-
SCM (Abadie, Diamond & Hainmueller 2010): Synthetic Control Method with unified formula interface. Supports predictor variables via
pred(), out-of-sample V selection (v_selection = "oos"), donor filtering (donor_mspe_threshold), penalised SCM (lambda_pen), and staggered adoption. Inference: MSPE ratio permutation test viamspe_ratio_pval(). -
SDID (Arkhangelsky et al. 2021): Synthetic Difference-in-Differences. Supports time-varying covariates (
covariates =), sharp and staggered adoption. Inference:sdid_inference()with placebo / bootstrap / jackknife / jackknife_global. -
GSC (Xu 2017): Generalised Synthetic Control with interactive fixed effects. Supports time-varying covariates via the full EM algorithm, sharp and staggered adoption. Inference: parametric bootstrap (
gsc_boot()) and non-parametric (gsc_inference()). - MC (Athey et al. 2021): Matrix Completion via nuclear-norm regularisation (Soft-Impute). Supports sharp and staggered adoption.
- TASC (Rho et al. 2026): Time-Aware Synthetic Control via Kalman EM. Supports sharp and staggered adoption.
-
SI (Agarwal et al. 2025): Synthetic Interventions via SI-PCR. Supports sharp, staggered, multi-arm (K > 1), and staggered × multi-arm. Inference:
si_inference()with bootstrap / jackknife / jackknife_global. -
SCM-Design (Abadie & Zhao 2026):
scm_design()with base / weakly_targeted / unit_level variants, blank-period permutation test, and split-conformal confidence intervals.
Unified API
- Single
scm_fit(outcome ~ treatment | unit + time, data, method = ...)entry point for all methods. -
panel_to_tensor()for multi-arm SI data preparation. -
broomintegration:tidy(),glance(),augment()for all methods and inference objects. -
plot.coresynth(): trend, gap, and weights plots via ggplot2. -
export_json(): JSON export for reproducibility.
