Speed dashboard¶
Accuracy before speed. This page is generated from one run of the
benchmark harness, whose rule is that an estimate must
match an independent reference (statsmodels, arch, SciPy, scikit-learn) at a
stated tolerance before it is timed. The parity matrix therefore comes
first and is the deliverable; the timings below it are indicative,
single-machine numbers and are labelled with the build that produced them.
| Parity | 65/65 metrics across 25 operations: ALL PASS |
| Faster than the reference | 22/25 timed operations (RELEASE build) |
| Slower than the reference | 3/25 — published below, not dropped |
| Measured | 2026-09-12, tsecon 0.10.0, Intel(R) Xeon(R) Processor @ 2.10GHz (4 cores) |
1. Parity matrix — the deliverable¶
max abs diff is the largest absolute disagreement between tsecon and the
reference on that metric; tol is the tolerance the harness asserts, set
from the known source of numerical difference (machine precision for
closed-form linear algebra, optimiser tolerance for iterative QMLE fits) and
never widened to pass. This table is machine-independent: a debug build
produces the identical matrix.
| Operation | Metric | max abs diff | tol | Result |
|---|---|---|---|---|
| ADF test (regression='c', fixed lag=4) vs statsmodels.tsa.stattools.adfuller |
statistic | 2.22e-16 | 1e-06 | PASS |
| p_value | 1.11e-16 | 1e-06 | PASS | |
| crit values | 0.00e+00 | 1e-06 | PASS | |
| VAR(2) coefficients (2 vars, trend='c') vs statsmodels.tsa.api.VAR |
coef matrix (5x2) | 6.11e-16 | 1e-08 | PASS |
| log-likelihood | 1.14e-13 | 1e-06 | PASS | |
| OLS + HAC (Newey-West) SEs (maxlags=4, corrected) vs statsmodels OLS cov_type='HAC' |
params | 3.11e-15 | 1e-08 | PASS |
| HAC bse | 2.08e-16 | 1e-08 | PASS | |
| GARCH(1,1) QMLE (constant mean, normal) vs arch.arch_model |
log-likelihood (rtol 1e-5) | 1.46e-07 | 5e-02 | PASS |
| params (atol 1e-3) | 7.84e-06 | 1e-03 | PASS | |
| GJR-GARCH(1,1,1) QMLE (constant mean, normal) vs arch.arch_model (o=1) |
log-likelihood (rtol 1e-5) | 2.46e-09 | 4e-02 | PASS |
| params (atol 1e-3) | 1.53e-06 | 1e-03 | PASS | |
| EGARCH(1,1,1) QMLE (constant mean, normal) vs arch.arch_model (vol='EGARCH') |
log-likelihood (rtol 1e-5) | 3.03e-08 | 2e-02 | PASS |
| params (atol 1e-3) | 1.49e-05 | 1e-03 | PASS | |
| KPSS test (regression='c', auto lags) vs statsmodels.tsa.stattools.kpss |
statistic | 5.55e-17 | 1e-10 | PASS |
| p_value (clipped) | 0.00e+00 | 1e-10 | PASS | |
| auto lags | 0.00e+00 | exact (0) | PASS | |
| ACF (20 lags) + Bartlett SEs vs statsmodels.tsa.stattools.acf |
acf (adjusted=False) | 4.44e-16 | 1e-12 | PASS |
| Bartlett SE | 4.16e-17 | 1e-12 | PASS | |
| acf (adjusted=True) | 3.33e-16 | 1e-12 | PASS | |
| PACF (15 lags, Yule-Walker + OLS) vs statsmodels.tsa.stattools.pacf |
pacf (yw / ywm) | 1.09e-15 | 1e-10 | PASS |
| pacf (ols) | 2.60e-15 | 1e-10 | PASS | |
| Ljung-Box + Box-Pierce (lags 1..10) vs statsmodels.stats.diagnostic.acorr_ljungbox |
lb_stat | 6.22e-15 | 1e-10 | PASS |
| lb_pvalue | 4.25e-15 | 1e-12 | PASS | |
| bp_stat | 3.55e-15 | 1e-10 | PASS | |
| bp_pvalue | 4.30e-15 | 1e-12 | PASS | |
| Jarque-Bera normality test vs statsmodels.stats.stattools.jarque_bera |
statistic | 4.26e-14 | 1e-10 | PASS |
| p_value | 2.11e-29 | 1e-12 | PASS | |
| skewness | 8.33e-16 | 1e-12 | PASS | |
| kurtosis | 8.88e-16 | 1e-12 | PASS | |
| Engle ARCH-LM test (4 lags) vs statsmodels.stats.diagnostic.het_arch |
LM statistic | 7.07e-13 | 1e-09 | PASS |
| LM p_value | 1.20e-13 | 1e-12 | PASS | |
| White heteroskedasticity test vs statsmodels.stats.diagnostic.het_white |
LM statistic | 3.55e-13 | 1e-09 | PASS |
| LM p_value | 7.26e-15 | 1e-12 | PASS | |
| F statistic | 7.11e-14 | 1e-09 | PASS | |
| F p_value | 5.52e-15 | 1e-12 | PASS | |
| Breusch-Pagan test (Koenker studentised) vs statsmodels.stats.diagnostic.het_breuschpagan |
LM statistic | 0.00e+00 | 1e-09 | PASS |
| LM p_value | 1.39e-17 | 1e-12 | PASS | |
| F statistic | 2.66e-15 | 1e-09 | PASS | |
| F p_value | 6.73e-15 | 1e-12 | PASS | |
| Ramsey RESET (powers of yhat up to 3) vs statsmodels.stats.diagnostic.linear_reset |
F statistic | 1.81e-13 | 1e-09 | PASS |
| p_value | 2.04e-13 | 1e-10 | PASS | |
| df (num, den) | 0.00e+00 | exact (0) | PASS | |
| Johansen cointegration (3 vars, k_ar_diff=1) vs statsmodels.tsa.vector_ar.vecm.coint_johansen |
eigenvalues | 9.89e-14 | 1e-10 | PASS |
| trace stat | 6.37e-11 | 1e-08 | PASS | |
| max-eig stat | 6.37e-11 | 1e-08 | PASS | |
| trace crit (90/95/99) | 0.00e+00 | 1e-12 | PASS | |
| max-eig crit | 0.00e+00 | 1e-12 | PASS | |
| VAR(2) orthogonalised IRF + FEVD (h=10) vs statsmodels VARResults.irf/.fevd |
orth IRF (11x2x2) | 1.67e-16 | 1e-10 | PASS |
| FEVD (10x2x2) | 4.44e-16 | 1e-10 | PASS | |
| VAR(2) Granger causality F-test vs statsmodels VARResults.test_causality(kind='f') |
F statistic | 9.77e-15 | 1e-10 | PASS |
| p_value | 3.83e-17 | 1e-12 | PASS | |
| df (num, den) | 0.00e+00 | exact (0) | PASS | |
| HP filter (lambda=1600, two-sided) vs statsmodels.tsa.filters.hp_filter.hpfilter |
trend | 5.41e-12 | 1e-08 | PASS |
| cycle | 5.41e-12 | 1e-08 | PASS | |
| Baxter-King band-pass (low=6, high=32, k=12) vs statsmodels.tsa.filters.bk_filter.bkfilter |
first_index (== K) | 0.00e+00 | exact (0) | PASS |
| cycle | 1.22e-15 | 1e-12 | PASS | |
| Christiano-Fitzgerald band-pass (low=6, high=32) vs statsmodels.tsa.filters.cf_filter.cffilter |
cycle | 2.55e-15 | 1e-12 | PASS |
| trend | 3.55e-15 | 1e-12 | PASS | |
| Periodogram PSD (boxcar, n=4096) vs scipy.signal.periodogram |
freqs | 0.00e+00 | 1e-15 | PASS |
| psd | 4.26e-14 | 1e-12 | PASS | |
| Welch PSD (Hann, nperseg=256, 50% overlap) vs scipy.signal.welch |
freqs | 0.00e+00 | 1e-15 | PASS |
| psd | 1.07e-14 | 1e-12 | PASS | |
| Ridge regression (alpha=1.0, no intercept) vs sklearn.linear_model.Ridge |
coef | 2.66e-15 | 1e-10 | PASS |
| Elastic net / lasso (coordinate descent) vs sklearn.linear_model.ElasticNet |
coef (l1_ratio=1.0) | 3.09e-13 | 1e-08 | PASS |
| coef (l1_ratio=0.5) | 2.19e-12 | 1e-08 | PASS |
Result: 65/65 parity checks passed — the harness exited 0.
Tolerance notes recorded by the harness:
- GARCH(1,1) QMLE (constant mean, normal) — QMLE optimisers differ; parity is asserted at optimiser tolerance (loglik rtol 1e-5, params atol 1e-3), not machine precision.
- GJR-GARCH(1,1,1) QMLE (constant mean, normal) — Leverage-term QMLE; two different optimisers, so parity is at optimiser tolerance (loglik rtol 1e-5, params atol 1e-3), not machine precision.
- EGARCH(1,1,1) QMLE (constant mean, normal) — Same parameterisation (mu, omega, alpha, gamma, beta) on both sides; parity at optimiser tolerance (loglik rtol 1e-5, params atol 1e-3).
- KPSS test (regression='c', auto lags) — p-value is interpolated and clipped to [0.01, 0.10] by BOTH sides (Kwiatkowski table); parity is on the clipped value.
- PACF (15 lags, Yule-Walker + OLS) — tsecon method='yw' is statsmodels method='ywm' (Yule-Walker, no mean adjustment).
- Baxter-King band-pass (low=6, high=32, k=12) — bkfilter drops k observations at each end; tsecon returns the same trimmed series plus
first_index= k, so the two align element-wise. - Elastic net / lasso (coordinate descent) — Both minimise (1/2n)||y-Xb||^2 + al1||b||_1 + (a/2)(1-l1)||b||^2. Tolerance reflects coordinate-descent stopping rules, not a formula difference.
2. Timings — indicative, subordinate to parity¶
Read the caveats before the numbers:
- Build:
RELEASE— detected via<repo>/bindings/python/python/tsecon/_core.abi3.so (19.4 MB) == <target>/release/lib_core.so. - Machine: Intel(R) Xeon(R) Processor @ 2.10GHz, 4 cores, Linux-6.18.44-fc-v24-x86_64-with-glibc2.39 (x86_64); Python 3.11.15 (CPython).
- Versions: tsecon 0.10.0, numpy 2.4.6, scipy 1.17.1, statsmodels 0.15.0, arch 8.0.0, scikit-learn 1.9.0.
- Method: best (minimum) of 20 wall-clock runs after a warm-up, on the harness's small synthetic inputs; the three QMLE volatility fits use at most 3 repeats.
ratiois reference time / tsecon time, so values above 1 mean tsecon was faster. Order-of-magnitude information only. - Date: 2026-09-12T12:49:58+0000.
- Per-call overhead: the public
tsecon.kpsstook 0.009 ms against 0.006 ms for the raw extension entry pointtsecon._core.kpsson the same array — a fixed ~0.003 ms of Python-side argument validation per call. It is included in every tsecon timing below and dominates the rows whose compute is far below a millisecond; the ratios on those rows measure that wrapper, not the Rust core.
| Operation | Reference | tsecon (ms) | reference (ms) | ratio | |
|---|---|---|---|---|---|
| ADF test (regression='c', fixed lag=4) | statsmodels.tsa.stattools.adfuller |
0.026 | 0.449 | 17.15x | faster |
| VAR(2) coefficients (2 vars, trend='c') | statsmodels.tsa.api.VAR |
0.205 | 1.361 | 6.65x | faster |
| OLS + HAC (Newey-West) SEs (maxlags=4, corrected) | statsmodels OLS cov_type='HAC' |
0.035 | 0.175 | 4.96x | faster |
| GARCH(1,1) QMLE (constant mean, normal) | arch.arch_model |
49.460 | 16.765 | 0.34x | SLOWER |
| GJR-GARCH(1,1,1) QMLE (constant mean, normal) | arch.arch_model (o=1) |
45.351 | 20.955 | 0.46x | SLOWER |
| EGARCH(1,1,1) QMLE (constant mean, normal) | arch.arch_model (vol='EGARCH') |
101.518 | 11.789 | 0.12x | SLOWER |
| KPSS test (regression='c', auto lags) | statsmodels.tsa.stattools.kpss |
0.009 | 0.051 | 5.93x | faster |
| ACF (20 lags) + Bartlett SEs | statsmodels.tsa.stattools.acf |
0.030 | 0.056 | 1.86x | faster |
| PACF (15 lags, Yule-Walker + OLS) | statsmodels.tsa.stattools.pacf |
0.023 | 0.794 | 34.05x | faster |
| Ljung-Box + Box-Pierce (lags 1..10) | statsmodels.stats.diagnostic.acorr_ljungbox |
0.011 | 0.264 | 23.00x | faster |
| Jarque-Bera normality test | statsmodels.stats.stattools.jarque_bera |
0.005 | 0.618 | 132.96x | faster |
| Engle ARCH-LM test (4 lags) | statsmodels.stats.diagnostic.het_arch |
0.018 | 0.414 | 23.18x | faster |
| White heteroskedasticity test | statsmodels.stats.diagnostic.het_white |
0.028 | 0.548 | 19.44x | faster |
| Breusch-Pagan test (Koenker studentised) | statsmodels.stats.diagnostic.het_breuschpagan |
0.018 | 0.423 | 22.97x | faster |
| Ramsey RESET (powers of yhat up to 3) | statsmodels.stats.diagnostic.linear_reset |
0.024 | 0.583 | 23.83x | faster |
| Johansen cointegration (3 vars, k_ar_diff=1) | statsmodels.tsa.vector_ar.vecm.coint_johansen |
0.057 | 0.770 | 13.49x | faster |
| VAR(2) orthogonalised IRF + FEVD (h=10) | statsmodels VARResults.irf/.fevd |
0.065 | 1.716 | 26.23x | faster |
| VAR(2) Granger causality F-test | statsmodels VARResults.test_causality(kind='f') |
0.052 | 2.153 | 41.60x | faster |
| HP filter (lambda=1600, two-sided) | statsmodels.tsa.filters.hp_filter.hpfilter |
0.034 | 0.945 | 28.04x | faster |
| Baxter-King band-pass (low=6, high=32, k=12) | statsmodels.tsa.filters.bk_filter.bkfilter |
0.018 | 0.087 | 4.94x | faster |
| Christiano-Fitzgerald band-pass (low=6, high=32) | statsmodels.tsa.filters.cf_filter.cffilter |
0.417 | 6.495 | 15.57x | faster |
| Periodogram PSD (boxcar, n=4096) | scipy.signal.periodogram |
0.079 | 0.442 | 5.58x | faster |
| Welch PSD (Hann, nperseg=256, 50% overlap) | scipy.signal.welch |
0.038 | 0.691 | 18.26x | faster |
| Ridge regression (alpha=1.0, no intercept) | sklearn.linear_model.Ridge |
0.062 | 0.520 | 8.38x | faster |
| Elastic net / lasso (coordinate descent) | sklearn.linear_model.ElasticNet |
0.050 | 0.408 | 8.18x | faster |
tsecon was faster on 22 of 25 timed operations in this run. The losses are published with the wins: GARCH(1,1) QMLE (constant mean, normal) at 0.34x, GJR-GARCH(1,1,1) QMLE (constant mean, normal) at 0.46x, EGARCH(1,1,1) QMLE (constant mean, normal) at 0.12x. The harness README explains what was measured and deliberately not done about each of them.
3. How to reproduce¶
Build a release extension first — maturin develop without --release
installs a debug build whose timings the harness refuses to label as speed
claims (the parity matrix is identical either way):
pip install numpy scipy statsmodels arch scikit-learn maturin
cd bindings/python && maturin develop --release && cd ../..
python benchmarks/bench.py --json benchmarks/results/latest.json # exits 0 iff every parity check passes
python benchmarks/render_dashboard.py # regenerates this page
python benchmarks/render_dashboard.py --check # exits 1 if the page is stale
The committed benchmarks/results/latest.json is the exact input
this page was rendered from; the harness's provenance banner (CPU, versions,
build mode) is stored in it, so any number quoted from this page can be traced
to the machine and build that produced it. Different hardware will move every
timing; it must not move a single parity row.