Benchmarks#
Every estimator in mlsynth ships with at least one durable benchmark: a
self-contained case under benchmarks/cases/ that re-runs a published result
(or a reference implementation) and asserts the headline numbers against a fixed
tolerance. Where the Replications page tells the story of each
validation in prose, this page documents the machinery – the runnable cases
that guard against regressions as the library changes.
Each case is a small module exposing run() (which returns a dict of metrics,
driving everything through mlsynth’s public API) and EXPECTED (a map from
metric to a (value, tolerance) pair). The driver compares the two and a case
that cannot find its data or an optional reference dependency raises
BenchmarkSkipped instead of failing.
Running them#
python benchmarks/run_benchmarks.py --all # every pure-Python case
python benchmarks/run_benchmarks.py --case cwz_ttest # one case
python benchmarks/run_benchmarks.py --with-reference # also R / external cross-checks
The registry of cases lives in benchmarks/registry.py (the source of truth);
the catalogue below is grouped by validation path.
Validation paths#
Path A – reproduce the source paper’s empirical result on the original authors’ data.
Path B – reproduce the paper’s Monte Carlo / simulation table.
Cross-validation – match an authoritative reference implementation (an R/MATLAB package or the authors’ own code); these skip themselves when the optional dependency is absent.
Path A — empirical replications#
Case |
Validates |
|---|---|
|
de Brabander et al. (2025) Table 1: seven estimators on the Brexit referendum (SC, DSC, SDID under three panel conventions, MASC, ASCM) at two dates, all fourteen cells |
|
de Brabander et al. (2025) Table 7: the in-sample placebo across twenty pre-Brexit quarters that ranks those seven, all twenty-one cells |
|
RPCA-SC West Germany |
|
CWZ 2025 Table 5 carbon-tax debiased t-test |
|
DSC distributional SC on Dube minimum-wage (Gunsilius/DiSCo vignette) |
|
Okano-Kurisu (2026) functional SC, all three applications from the authors’ data: pre-treatment fits for the plain and ridge-augmented estimator in each (fertility curves 0.1259/0.0687, age-at-death distributions 0.2092/0.0634, trade covariance matrices 39.3429/20.0639) plus every weight of Tables 1-3, reproduced exactly by a port of the authors’ R code |
|
Song et al. (2023) clean winter heating in China, the ridge-ASCM half of their ML-ASCM: 30 stratified cells of their 1024-fit design, carried against two reference bases – a live augsynth 0.2.0 run (tight) and the authors’ published |
|
DSCAR Beijing PM2.5 alerts (Zheng-Chen) |
|
HK GDP empirical |
|
forward-selected SC (Prop 99) |
|
HSC HK handover |
|
VanillaSC vs mharoruiz/ibex scinference/lsei SC: Iberian exception day-ahead price, weights value-for-value (Haro Ruiz-Schult-Wunder 2024) |
|
Walmart placebo design |
|
dense L-inf vs sparse SC (Prop 99) |
|
MAREX Walmart placebo design vs live SCDesign (Abadie-Zhao, full 45-store panel + covariates, open quadprog, no Gurobi) |
|
MASC Basque/ETA (KMPT Sec 5) |
|
Shi-Wang Brexit multi-treated-units L2-relaxation |
|
PDA methods on HK CEPA (Shi-Wang App E.1) |
|
Shi-Huang China luxury-watch fsPDA (prewhitened-NW) |
|
Shi-Wang China PPI L2-relaxation (real-estate policy) |
|
SCM-relaxation Brexit/UK GDP (2016Q3) |
|
SCM-relaxation Brexit robustness (2020Q1) |
|
Lee-Wooldridge Prop99 + castle |
|
SBC German reunification |
|
Tian et al. West Germany balance |
|
scpi staggered point estimates, Germany (Cattaneo et al. 2025) |
|
scpi staggered TSUA prediction intervals, Germany (Cattaneo et al. 2025) |
|
scpi covariate (multi-feature) staggered illustration, Germany (Cattaneo et al. 2025) |
|
scpi single-unit CFT-2021 prediction intervals, German reunification: levels + cointegrated bands + weights |
|
VanillaSC reproduces Schulte et al. (2026) lost-autonomy SCM: post-trigger secessionist surge, Catalonia 2010 / Faroe 1994 (tracks authors’ SyntheticControlMethods synthetic) |
|
L1 predictor selection (Prop 99) |
|
SPCD design vs random/SC on Prop 99 (Lu et al. 2022) |
|
grossi direct+spillover German reunification (Grossi et al.) |
|
inclusive SCM German reunification (Di Stefano-Mellace) |
|
iterative waterfall SCM German reunification (Melnychuk) |
|
SPOTSYNTH donor-spillover screening: Germany/California/Basque (Fig 6) + detection (Fig 2) + debias (Fig 4) |
|
Brooklyn showroom (Li-Shankar) |
|
canonical ADH 2010 Prop 99 |
Path B — Monte Carlo / simulation#
Case |
Validates |
|---|---|
|
de Brabander et al. (2025) Section 5 Monte Carlo: per-replication cross-validation against synthdid on 48 panels drawn by the authors’ own DGP, plus the Table 9 finding that demeaning cuts the factor-driven bias about fivefold |
|
ASCM near-nominal coverage + bias reduction (BMR 2021 Sec 7) |
|
ClusterSC vs RSC |
|
fGRC subspace separation recovers cluster structure invisible to k-means (Yamamoto-Hwang |
|
CTSC vs two-way FE bias (Powell 2022 Table 1) |
|
CWZ 2025 Table 3 application-based Monte Carlo |
|
DR/PIPW recovery + double-robustness (Qiu et al. normal DGP) |
|
VanillaSC recovers the factor structure as J, T0 grow (Ferman 2021 JASA Table 1): weight on the treated factor group → 1, se(α) shrinks while OLS’s grows; mlsynth == R |
|
Ferman-Pinto 2021 QE Table 1 MC (CPS-calibrated factor model): VanillaSC (SC) and TSSC MSCa (demeaned SC) reproduce the Panel A/B bias + the efficiency-over-DID and break-panel findings; SC/demeaned == the authors’ |
|
simulation |
|
FMA asymptotic-CI coverage robust to variance (Li-Sonnier) |
|
HSC regime adaptation |
|
Abadie-Zhao design sim |
|
L-inf vs SC (Wang-Xing-Ye Table 4) |
|
MSQRT unbiasedness + RMSE noise-floor (Shen-Song-Abadie Sec 6) |
|
nonlinear coverage + error-shrinks-with-J |
|
PANGEO trajectory match vs scalar (Chen et al.) |
|
Shi-Wang Table 2 L2-relaxation size/power |
|
Li-Bell Table 2 LASSO-PDA OOS prediction (N>T1) |
|
Jiang et al. 2025 prediction-interval coverage (Tables 2-5) |
|
Shi-Huang Table 1 fs-vs-LASSO size/power geometry |
|
PI/PIS/PIPost vs SC under trending factor (Liu et al.) |
|
latent-group MC, relaxations beat SCM |
|
RSC train≈gen error |
|
Shi-Xi-Xie MSE ratios |
|
Sun averaged regime geometry |
|
Tian Table 1 / Sun Sim1 |
|
SSDiD vs DiD coverage/RMSE |
|
SHC latent-confounder recovery (Chen-Yang-Yang Sec 3.1) |
|
SIV vs 2SLS-TWFE bias (Gulek-Vives Table 1) |
|
ORTHSC carbon-tax ATT/p/K/CI (Fry; Andersson 2019 data, vs live R) |
|
VanillaSC malo + mscmt reproduce Andersson (2019) carbon-tax ATT/2005-gap (paper predictor spec) |
|
STACKEDSC on Wiltshire (2023) Section 4.2: 566 Walmart counties in six cohorts against 39 never-treated donors. Geometry, not cells – the paper’s prose claims (excellent pre-fit, no effect at entry, decline from the following year, large negative at five years), the base-period indexing identity at 6e-16, and the per-cohort batching. Its Table 4 magnitudes are not claimed, and STACKEDSC – Walmart Supercenters and local employment (Wiltshire 2023) says why |
|
Hirshberg (2021) error-in-variables SC prediction-interval coverage on a low-rank DGP |
|
ORTHSC fixed-smoothing t-test size control + power (Fry Tables 1-2) |
|
SAR spillover recovery + SCM nesting (Sakaguchi-Tagawa) |
|
SAR Bayesian spatial SC vs Mendez California Prop 99 tutorial (bare rho 4dp + ATT + Nevada spillover; full rho weakly identified) |
|
SPSC IFEM recovery + DT-vs-NoDT coverage (Park-Tchetgen) |
|
Doudchenko et al. 2021 Monte Carlo (BLS unemployment) |
|
TASC vs SC state-space ablation (Rho et al.) |
|
Figure 2 MSE-ratio grid |
Cross-validation against reference implementations#
Case |
Validates |
|---|---|
|
vs augsynth: Kansas ridge-ASCM ladder (SCM/ridge/covariate/residualized) |
|
vs the |
|
vs augsynth |
|
DROSC vs authors’ R |
|
vs LIVE propsdid (Rscript): Bogatyrev-Stoetzer Table 2 common-weights SDID on party vote shares (skips if absent) |
|
vs Synth (uniform custom.v) and tidysynth (ADH spec) on Prop 99: placebo rank and p-value agree exactly (1/39); also records where mlsynth attains a lower value of Synth’s own objective and where ipop fails outright |
|
Path A: Boussim (2026) Pennsylvania AEPS from public EIA data – Table 1 donor weights and both RMSPEs, all eleven years of Table 2, and the section 6.4 placebo (p = 0.111); also pins the clr-vs-alr geometry correction |
|
vs authors’ repo |
|
vs the authors’ |
|
vs LIVE reference R (He-Li-Shi-Miao 2026): RRSC large-N and fixed-N regimes value-for-value on a synthetic interference panel (skips if R absent) |
|
vs LIVE pensynth wsoll1 (Rscript+LowRankQP): penalized SC weights/ATT on Prop 99 (skips if absent) |
|
LINF vs LinfinitySC (skips if absent) |
|
vs authors’ MCPanel R (mcnnm_cv; ATT + California counterfactual path) |
|
vs R microsynth panel method (Seattle DMI) |
|
vs Bottmer’s mlSC_estimator (skips if absent) |
|
vs authors’ bsynth R package (rstan): posterior counterfactual + credible bands + ATT, West Germany reunification (Martinez & Vives-i-Bastida) |
|
vs Tian’s NSC.R (Prop 99 Table 2) |
|
vs augsynth::multisynth (jackknife + bootstrap SEs) |
|
vs live R (authors’ analysis.Rmd, commit 3bcb5ec): over-identified DR-OID, Brazil vaccine/pneumonia |
|
vs live R (same script): standard SC (VanillaSC) vs proximal (DR-OID h/DR) contrast, Brazil vaccine/pneumonia |
|
vs freshtaste/proximal (Panic 1907 Table 3) |
|
vs scmrelax (skips if absent) |
|
Shen CIs + coverage |
|
TSSC MSCa == Ferman-Pinto (2021) demeaned SC, value-for-value vs their R quadprog (live Rscript), Basque/ETA 1975 |
|
vs authors’ synthdid R (synthdid_estimate; SDID/DiD/SC on Prop 99) |
|
vs Agarwal-Shah-Shen 2026 authors’ code (Prop 99) |
|
vs deshen24/syntheticNN (Prop 99) |
|
vs Melnychuk-Andrii/Spillover-SCM (inclusive SCM German) |
|
vs jcao0/synthetic-control-spillover (Cao-Dowd Prop 99) |
|
vs authors’ repo |
|
vs jcao0/staggered_synthetic_control (criminality Sec 4) |
|
vs cvxpy (CLARABEL) on the MSCMT inner simplex program: the batched active set never finishes above the interior-point optimum across the Basque candidate weightings, and its work bounds are pinned as iteration counts, which are machine independent where wall-clock is not. Also pins what the default |
The captured reference corpus#
For many of the cross-validation cases above, the reference is not a number
transcribed from a paper or a package that has to be re-installed every time the
suite runs. It is a captured artifact: the original authors’ code, the exact
command that ran it, the verbatim output, and a record of the environment that
produced it, all committed under benchmarks/reference/<case>/. mlsynth’s
result is then pinned to that captured output, so the comparison is reproducible
offline and the reference value cannot silently drift from what the authors’
code actually produces.
This section documents that machinery in detail.
Anatomy of a bundle#
A captured bundle is a directory benchmarks/reference/<case>/ containing:
manifest.json– the bundle’s contract. It records thecasename, a humantitle, thepaperbeing validated, areference_implstring naming the exact code that was run, thepath_type(Path A / Path B / cross-validation), thecommandthat regenerates the bundle, and the list of inputdatafiles. Thecommandis run verbatim, so it can be anRscriptinvocation, apythonscript, or anything else that prints the expected output block.reference.Rorreference.py– the runnable reference. It drives the authors’ code on the case’s data at the matched settings and prints two blocks: a== REFERENCE VALUES ==block ofkey<TAB>valuelines (andweight<TAB>label<TAB>valuerows for weight vectors), and a== SESSION INFO ==block of tool and package versions.The authors’ code itself, vendored alongside (for example
Fun_FDID.R,scm.corner.R, or avendor/subdirectory of the minimal modules needed), together with any small input data the run requires (for exampleGDP.csv). ANOTICEfile records provenance and licensing – and where an upstream repository ships no license, only the minimal subset needed to run the reference is vendored, for provenance, not redistribution.reference.out– the verbatim captured stdout of the run, kept as the human-readable evidence of what the authors’ code printed.reference.json– the parsed result, a mapping{"values": {...}}that the test harness reads.provenance.json– a record of the run: a UTCgenerated_attimestamp, thegit_shaof the repository at capture time, theplatform, thecommand, the inputdatawith SHA-256 checksums, and the interpreter and package versions (for exampler_versionand the loadedpackages).comparison.csv– the side-by-side table of mlsynth against the reference, one row per quantity, with the absolute difference (described under Comparison tables).
How a live cross-validation is built#
Each live cross-validation follows the same recipe, designed to isolate the one thing being tested – whether mlsynth and the authors’ code compute the same quantity – from everything that would otherwise confound it.
Run the authors’ code, not a paraphrase of it. The reference fetches or vendors the upstream implementation and calls it directly, on the same input data the mlsynth case uses.
Match the settings that are free to differ. Estimators expose tuning choices (a penalty level, a number of retained singular values, a transformation window, an EM initialisation). The reference and the mlsynth call are driven at the same values so that any remaining difference is attributable to the implementation, not the configuration. Where a method’s own tuning differs from a paper’s by construction – for example a time-respecting cross-validation against a future-leaking K-fold – the cross-validation pins the solve at a single fixed setting (where the program is a unique optimisation), not the tuned end-to-end number, and the tuned number is kept as a separate, clearly labelled pin.
Capture the output with provenance.
benchmarks/reference/generate.pyruns the manifestcommand, parses the== REFERENCE VALUES ==block intoreference.json, stores the verbatimreference.out, and writesprovenance.jsonwith the checksums and versions above.Pin mlsynth to the captured values. The case reads the captured numbers with
benchmarks.reference.reference_value()(orload_reference) and uses them as theEXPECTEDtargets, so the constant in the test and the captured run are the same object – they cannot diverge without the bundle being regenerated.
Comparison tables#
Every bundle with a comparison() writes a comparison.csv: a metadata
header (the case title, the reference implementation, the generation timestamp
and versions) followed by one row per quantity with columns quantity,
mlsynth, reference and abs_diff. The public, web-native rollup of
the whole corpus is the Validation dashboard dashboard, generated from these CSVs.
Regenerate both with
python benchmarks/reference/export_comparison.py --all
python benchmarks/reference/build_validation.py
Regenerating a bundle#
A bundle is rebuilt from its manifest with
python benchmarks/reference/generate.py <case>
which re-runs the captured command, refreshes reference.out /
reference.json / provenance.json, and so re-stamps the environment and
checksums. Regeneration requires whatever the reference needs (an R toolchain
and the named packages, or the relevant Python dependency); when that toolchain
is absent the corresponding case raises BenchmarkSkipped at suite time
instead of failing, and the committed bundle remains the offline record.