Skip to content

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. ratio is 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.kpss took 0.009 ms against 0.006 ms for the raw extension entry point tsecon._core.kpss on 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.