Skip to content

🇬🇧 English · 🇪🇸 Español

Validation

Available from puremacro 0.92.0 onwards.

puremacro reimplements many estimators in pure numpy/scipy so they run in the browser. To show those reimplementations are correct, the package ships a validation gallery: a declarative registry of cases, each comparing a puremacro estimator to an independent reference. Run it yourself:

from puremacro.validation import run_all, scorecard

scorecard()                 # one row per case: puremacro vs reference, pass/fail, margin
assert all(r.passed for r in run_all())

scorecard() and run_all() are pyodide-safe — they need only the four core dependencies — so the gallery runs unchanged in the browser playground (notebook 12_validation_gallery, also available in Spanish as 12_validation_gallery_es).

Pyodide run

On 13 October 2026 the 110-case gallery of 5.0.0a1 was executed inside real Pyodide, headless under Node.js v26.11.1, with tools/pyodide_gallery.py: the wheel installed through micropip with every dependency resolved from the Pyodide distribution (nothing from PyPI, as in the playground) and the cases ran one at a time. It passed 110 of 110 on both Pyodide versions: 314.0.5 (Python 3.14.2, the version pinned by the playground's jupyterlite-pyodide-kernel 0.8.4) and 0.28.3 (Python 3.13.2, the Gate 6 harness pin), and 110 of 110 on the desktop (CPython 3.13) from a fresh install of the same wheel. An earlier run, on 3 October 2026, had found that puremacro.narrative imported sqlite3 and ssl at module level, which Pyodide 0.28 ships as unvendored standard-library modules; those imports go through puremacro._optional_stdlib since 4.6.0. This is Pyodide under Node, not a browser tab, and it measures the gallery, not heavy workloads. The commands are documented in tools/pyodide/README.md.

How a case is validated

Each case declares a mechanism (how its reference is sourced) and a tolerance tier. The mechanism is the heart of the trust argument — the reference must be genuinely independent of the puremacro code under test.

Mechanism Reference Runs live in the browser?
package statsmodels / linearmodels / arch, captured once as a frozen golden value yes — compares to the golden (no heavy dependency at run time)
scipy scipy / numpy (a runtime dependency), recomputed live yes
analytical a closed-form solution yes
published a number from a paper, with citation yes (frozen constant)
internal a cross-method identity or simulate-then-recover check yes

The package mechanism keeps the shipped code pure: statsmodels/linearmodels/arch are never imported by puremacro. Their outputs are frozen as golden JSON, and a continuous-integration drift-guard (pytest -m reference) recomputes the live package and asserts the golden is still faithful. So run_all() reads goldens for package cases and recomputes everything else live — and never needs the heavy packages.

Tolerance tiers: EXACT (rtol 1e-10) · TIGHT (1e-6) · NUMERIC (1e-2) · COARSE (10%) · QUALITATIVE (sign / ordering / a lower-bound threshold).

Coverage

110 cases across 15 subsystems — all passing. By mechanism: internal 59, analytical 31, package 13, scipy 5, published 2.

Subsystem Cases Reference(s)
var 15 Cholesky IRF vs statsmodels orth_irfs; FEVD-sums-to-1 and stability ⇔ companion spectral radius < 1 (identities); narrative sign restrictions; Minnesota BVAR posterior means vs hand-built Theil (λ₂ = 0.5) and Bańbura–Giannone–Reichlin eq. (5) Normal-inverse-Wishart closed forms; GVAR identities — zero trade weights decouple the country blocks, the solved global system reproduces them, the persistence profile is 1 at impact, normalised GFEVD rows sum to 1, and a one-country GVAR's GIRFs equal the var generalised IRFs
lp 6 Jordà LP coefficients/HAC SE vs statsmodels OLS-HAC; LP-IV vs linearmodels IV2SLS; two-way FE vs PanelOLS; IV-reduces-to-OLS identity
garch 7 GARCH(1,1) params/vols vs arch; simulate-then-recover; GARCH-MIDAS variance decomposition identity
inference 9 Newey–West / OLS-HAC SE vs statsmodels HAC; Stock–Yogo (2002, NBER TWP 284) Tables 1–2 TSLS bias and size critical values (published); Kiefer–Vogelsang (2005) Table I Bartlett fixed-b critical values (published); sup-t plug-in critical value vs its i.i.d. closed form; sup-t monotonicity over pointwise critical values
state_space 6 Kalman filter/smoother states + log-likelihood vs statsmodels state space; smoother-variance identities
dynpanel 7 Arellano–Bond / Blundell–Bond GMM recover known ρ; exact-identification J = 0; Windmeijer WC-robust SEs equal an independent finite-difference evaluation (uncollapsed lags(2) layout)
did 11 Callaway–Sant'Anna recovers 2x2 DiD analytically; Sun–Abraham equals Callaway–Sant'Anna on 2x2; on a staggered design with unequal cohort sizes the Callaway–Sant'Anna event study and overall ATTs equal CS (2021) eqs. 3.4/3.7/3.10–3.12 and the Sun–Abraham IW event study equals SA eq. 27; Borusyak–Jaravel–Spiess imputation recovers 2x2; Synthetic DiD recovers treatment effect; spatial DiD with every unit beyond the outermost ring reduces exactly to two-way FE, and the ring adjustment beats the contaminated naive estimate on a planted-spillover DGP
unit_root 3 ERS GLS detrending recovers deterministic trend & constant mean analytically; Ng–Perron MZt = MZa · MSB cross-statistic identity
spectral 6 Welch PSD / cross-spectrum / coherence vs scipy.signal; band-power partition-of-unity; coherence ∈ [0,1]
forecast 6 Gaussian CRPS closed form (Gneiting–Raftery); fair-ensemble convergence; PIT calibration; Diebold–Mariano sign/tie; MCS retention
vfi 5 Tauchen/Rouwenhorst reproduce AR(1) moments; Brock–Mirman closed-form policy; Markov stationary vs scipy left eigenvector; EGM = VFI
dsge 6 Klein = gensys on a known model; closed-form forward-looking solution; Kalman log-likelihood vs the AR(1) analytical likelihood
narrative 7 Known-value lexicon scoring on crafted text; index monotonicity / standardization identities
cointegration 4 FM-OLS and DOLS recover a planted cointegrating β; the two agree; DOLS mitigates endogeneity bias
spatial 12 Moran's I / Geary's C with their Cliff-Ord moments vs esda (PySAL); Conley HAC at cutoff 0 = HC0 and = an explicit Bartlett double loop; flat-kernel space-time HAC = Driscoll-Kraay; the SAR/SEM concentrated log-likelihood at ρ=0 = the OLS Gaussian log-likelihood; SDM = SAR on the augmented design and SLX = OLS on it; LeSage-Pace impacts = a brute-force dense (I−ρW)⁻¹(Iβ+Wθ); the spatial panel at ρ=0 = two-way FE; the Lee-Yu correction rescales σ² by exactly T/(T−1); spatial_lp without a spillover = panel_lp

Each case carries its full citation in the code (ValidationCase.citation), shown in the citation column of scorecard(). Key references include Lütkepohl (2005), Newey & West (1987), Stock & Yogo (2005), Gneiting & Raftery (2007), Diebold & Mariano (1995), Brock & Mirman (1972), Rouwenhorst (1995), Tauchen (1986), Engle (2002), Arellano & Bond (1991), Blundell & Bond (1998), Windmeijer (2005), Kiefer & Vogelsang (2005), Callaway & Sant'Anna (2021), Sun & Abraham (2021) and Bańbura, Giannone & Reichlin (2010).

Native X-11/ARIMA vs the real X-13ARIMA-SEATS binary

puremacro.sa.x11 (native, pure-Python, Pyodide-safe) is validated against the genuine Census X-13ARIMA-SEATS binary (v1.1.57) via frozen goldens: tools/gen_validation_goldens_sa.py runs the binary with the spec pinned to the native v1 configuration (log/level by AICC, airline model, no outlier regressors, 60-period ARIMA extension, X-11 with MSR-selected seasonal filter) on nine real series — six county LAUS unemployment levels spanning large to tiny (Keweenaw MI, ~2k population), NSA industrial production, and two quarterly aggregates — and tests/test_sa_x11_native.py asserts agreement within tolerances that were measured, then frozen with headroom (re-measured 2026-07-22 after the genuine B17/B20 weight cascade landed — extreme weights from each stage's irregular forming weight-modified originals that feed the next stage's trends, the structure the v1 chain had condensed away): interior medians 0.08–0.83%, interior maxima under 0.7% on smooth macro series and up to ~7% at extreme-replaced outlier months of wild small-count series (roughly HALF the v1 gaps), seasonal-filter selection matching the binary on every monthly series. The remaining divergence sources are documented rather than hidden: the binary applies asymmetric filters at the series start (it does not use backcasts inside X-11) where the native engine uses symmetric-over-backcasts, and residual replacement-value details at flagged outliers. CI never runs the binary — only the frozen goldens. This is what the naming "X-11/ARIMA-style, X-13-validated" promises: exactly what the frozen tolerances measured, no more.

Honest scope

Where no sound independent reference exists, a case is skipped with a stated reason rather than rubber-stamped with a circular check. Documented examples: inference.kleibergen_paap_f (the rk Wald F has no independent closed form for two or more endogenous regressors; the k = 1 case, where it equals the HC0-robust first-stage F, is pinned in tests/test_inference/test_weak_iv_values.py rather than in the gallery), the heavy dsge estimation routines, the external Arellano–Bond check against Stata xtabond Examples 1, 2 and 4 (it lives in tests/test_dynpanel/ and runs only when a copy of abdata is available, through PUREMACRO_ABDATA or as tests/fixtures/abdata.dta or .csv; the file is third-party data and not bundled), and the narrative LLM and live-fetch paths. The gallery validates what can be validated independently, and says so when it cannot.

Re-verifying

# fast, pure: puremacro vs the contracted references
python -m pytest tests/validation/ -q

# CI drift-guard: recompute the LIVE statsmodels/linearmodels/arch references
# and assert the frozen goldens are still faithful (needs the dev extra)
python -m pytest -m reference -q

To extend the gallery, drop a cases_<subsystem>.py module exposing CASES: list[ValidationCase] into puremacro/validation/ — run_all() discovers it automatically.