◆ Rust · time series

The order of a model, kept only where noise can't explain it.

Classical information criteria next to Fine–Hwang threshold estimators: grow the memory with the sample, studentize every statistic, clip what white noise could produce, report the last surviving lag.

CI crates.io License MIT No dependencies Platforms

Two families, one honest comparison

Information criteria score every candidate order; threshold estimators keep the last lag whose statistic survives a vanishing cut-off. tsorder computes both from one pass over the data, so they can be compared on equal footing — in accuracy and in time.

🧮

One data path

Sample autocovariance (by FFT for large lags) → Levinson–Durbin recursion. AIC, AICc, BIC, HQ, FPE, CAT and MDL, the PACF and the long AR fit all come from the same O(K²) pass — with Burg and conditional-OLS variants a flag away.

📏

Studentized by construction

The raw Fine–Hwang threshold is not scale-invariant and degrades with n. Every statistic here returns its null standard error, and every threshold lives on the studentized scale.

🔌

Pluggable statistics

PACF for AR, Bartlett-studentized ACF for MA, long-AR coefficients by Yule–Walker or OLS. The statistic and the cut-off rule are orthogonal axes of the API.

🎚️

Calibrated thresholds

BIC-like, iterated-logarithm, max-of-K and Gumbel rules. The Gumbel threshold controls the family-wise over-selection level: α = 5% means ≈ 5% false positives on white noise, verified in the tests.

🧪

Benchmarked, defaults included

Every default was decided by a reproducible benchmark shipped in the repository — including the negative results: the mode-by-consensus ensemble never beat its best member and was dropped.

🦀

No dependencies

Pure Rust on the standard library, deterministic, MSRV 1.81. Errors are values (Result), never a panic on user data — ready for thin R and Python bindings.

Statistics
PacfAcfBartlettInversePacfJirakBandPacfRobustLongArYwLongArOlsAcf
Thresholds & memory
BicLike{c}Lil{c}MaxOfK{c}Gumbel{α}Fixedp_n = ⌈2·log n⌉ (default)p_n = ⌊n^0.4⌋

An AR(2), read straight off the PACF

Simulated AR(2) with φ = (1.2, −0.6) and n = 400. The studentized partial autocorrelations at lags 1 and 2 tower over the threshold τₙ = 3.0; everything after them is what white noise produces. The last crossing gives the order — the same answer BIC reaches by scoring all eleven candidate models.

Studentized PACF and the Fine–Hwang threshold

|PACF(k)|·√n for k = 1…p_n, with the default memory p_n = ⌈2 log 400⌉ = 12 and the default threshold τₙ = √(1.5 log n).

Kept (above τₙ) Clipped Threshold τₙ = 3.0
Values as a table
Lag|studentized PACF|Decision

Selected order: 2 (last lag above the threshold). BIC on the same series: 2.

Reproducible: cargo run --release --example pacf_path prints these numbers.

How it works

Fine & Hwang (1979) proved the first consistent order estimator: no likelihood, no Gaussianity, no maximum order to guess. tsorder is that idea with the one repair it needed — studentization.

01

Grow the memory

Fit a long model of order p_n that increases with the sample size n.

02

One recursion

Autocovariances and one Levinson–Durbin pass give every statistic and every criterion.

03

Studentize

Each statistic is divided by its null standard error, so the threshold is scale-free.

04

Clip and report

Zero everything below τₙ; the last surviving lag is the order. The full path is returned for plotting.

Install & run

Add the crate and pass any &[f64]. Rust 1.81 or later; changes between versions in the changelog. R (extendr) and Python (PyO3) bindings are planned on top of this same core.

$ cargo add tsorder-core
// classical criteria and the default Fine–Hwang
use tsorder_core::*;

let bic = ic_order(&x, Criterion::Bic, None)?;
let fh  = fh_order(&x, FhConfig::default())?;
println!("BIC {} / FH {}", bic.order, fh.order);

// the whole studentized path, for plots
for (k, t) in fh.studentized.iter().enumerate() {
    println!("lag {} |t| {:.2}", k + 1, t.abs());
}
// MA order, level-calibrated threshold, ensemble
let ma = fh_order(&x, FhConfig {
    statistic: Statistic::AcfBartlett,
    threshold: Threshold::Gumbel { alpha: 0.05 },
    ..Default::default()
})?;

let e = ensemble_order(&x, &[
    FhConfig::default(),
    FhConfig { statistic: Statistic::LongArOls,
               ..Default::default() },
], EnsembleRule::Majority)?;

Built on the literature, tested against it

Each piece implements a published result and says so. Where a convention mattered, it follows the R reference implementation, so numbers are comparable across ecosystems.

ComponentFoundation
Threshold-and-clip order estimationFine & Hwang (1979); Hwang (1978)
PACF null distribution (self-studentized)Quenouille (1949)
ACF standard errors for MABartlett (1946)
Inverse autocorrelations from a long AR fitCleveland (1972)
Bands over a growing Yule–Walker fit, Gumbel calibrationJirak (2012, Ann. Statist.)
Q_n scale and robust autocorrelationsRousseeuw & Croux (1993); Croux & Rousseeuw (1992); Ma & Genton (2000)
Innovation variances & criteria conventionsR's stats::ar.yw; AICc as in forecast
Ensemble (model by consensus)García-Hiernaux, Jerez & Casals (2012)
McMillan degree: SVC via canonical correlations (Table 1 reproduced), Hankel SVT, sequential rank testBauer (2001); Zheng, Jedra & Proutière (2025); Baram & Porat (1988); Robin & Smith (2000)
ARMA (p, q): EACF table (matches R's TSA::eacf) and recursive regressionTsay & Tiao (1984); Hannan & Rissanen (1982)
✅

Tested properties

Order recovery on long simulated AR, MA and subset-AR series; affine invariance of every statistic and criterion; empirical level of the Gumbel rule on white noise; errors instead of silent NaN.

🖥️

CI on every platform

fmt · clippy -D warnings · tests and docs on Linux, macOS and Windows, plus the MSRV build (Rust 1.81).

⚖️

MIT, nothing attached

No dependencies, no license to reconcile, no supply chain to watch. Pre-alpha: the API is still settling.