Skip to main content

tsorder_core/
arma.rs

1//! Hannan–Rissanen (1982) recursive ARMA order selection and initial
2//! coefficient estimation (their stages 1–2; the efficient third stage is
3//! left to the bindings/estimation layer).
4//!
5//! Stage 1 fits a long Yule–Walker autoregression (order chosen by AIC, their
6//! practical recommendation) and extracts innovation estimates ε̂. Stage 2
7//! regresses `x_t` on `p` lags of `x` and `q` lags of `ε̂` for every
8//! candidate `(p, q)` on a common sample and minimises
9//! `log σ̃²_{p,q} + (p + q) log T / T` (their BIC choice, eq. 1.11).
10//!
11//! Their 1983 Biometrika correction (70, p. 303) reports that Theorem 3's
12//! proof is in error and the stage-2 selection as published can converge to
13//! values *larger* than `(p₀, q₀)`, typically when the MA polynomial has
14//! zeros near the unit circle; the fix they give — repeating stage 2 with the
15//! residuals `ε̃(t)` of eq. (1.12) in place of ε̂ — restores convergence and
16//! is applied here by default (`erratum_pass`). The common-factor degeneracy
17//! makes the stage-2 design matrix singular exactly when `p > p₀` and
18//! `q > q₀` — such candidates are skipped when the Cholesky fails, which is
19//! itself the paper's monitoring advice.
20
21use crate::criteria::{ic_order, Criterion};
22use crate::levinson::levinson_from_series;
23use crate::linalg::ols;
24use crate::{check_input, Result};
25
26/// Configuration of [`hannan_rissanen`].
27#[derive(Debug, Clone, Copy)]
28pub struct HrConfig {
29    /// Candidate grid `0..=max_p × 0..=max_q` (defaults 5 × 5).
30    pub max_p: usize,
31    pub max_q: usize,
32    /// Long-AR order of stage 1; `None` → AIC choice (clamped to ≥ 1).
33    pub long_ar: Option<usize>,
34    /// Repeat stage 2 with the eq. (1.12) residuals of the first-pass model
35    /// (the 1983 erratum's consistency fix). Default true.
36    pub erratum_pass: bool,
37}
38
39impl Default for HrConfig {
40    fn default() -> Self {
41        HrConfig {
42            max_p: 5,
43            max_q: 5,
44            long_ar: None,
45            erratum_pass: true,
46        }
47    }
48}
49
50/// Output of [`hannan_rissanen`].
51#[derive(Debug, Clone)]
52pub struct HrResult {
53    pub p: usize,
54    pub q: usize,
55    /// AR coefficients in the `x_t = Σ φ_j x_{t−j} + ε_t + Σ θ_j ε_{t−j}` convention.
56    pub phi: Vec<f64>,
57    pub theta: Vec<f64>,
58    /// Stage-2 residual variance at the selected order.
59    pub sigma2: f64,
60    /// Long-AR order used in stage 1.
61    pub long_ar: usize,
62}
63
64/// Hannan–Rissanen stages 1–2: select `(p, q)` and return the (consistent,
65/// not efficient) stage-2 coefficient estimates.
66pub fn hannan_rissanen(x: &[f64], config: HrConfig) -> Result<HrResult> {
67    let n = x.len();
68    let h = match config.long_ar {
69        Some(h) => h.max(1),
70        None => ic_order(x, Criterion::Aic, None)?.order.max(1),
71    };
72    check_input(x, 2 * (h + config.max_p.max(config.max_q)) + 2)?;
73    let mean = x.iter().sum::<f64>() / n as f64;
74    let xc: Vec<f64> = x.iter().map(|v| v - mean).collect();
75    // Stage 1: Yule–Walker long AR, innovations ε̂(t) for t = h..n.
76    let l = levinson_from_series(&xc, h)?;
77    let eps: Vec<f64> = (h..n)
78        .map(|t| {
79            let mut e = xc[t];
80            for (j, a) in l.phi_last.iter().enumerate() {
81                e -= a * xc[t - j - 1];
82            }
83            e
84        })
85        .collect();
86    // Stage 2 on the common sample t = m..n with m = h + max(max_p, max_q):
87    // eps index offset: ε̂(t) lives at eps[t - h].
88    let m = h + config.max_p.max(config.max_q);
89    let rows = n - m;
90    if rows < 10 * (config.max_p + config.max_q + 1) {
91        return Err(crate::Error::TooShort {
92            n,
93            needed: m + 10 * (config.max_p + config.max_q + 1),
94        });
95    }
96    let (p, q, beta, sigma2) = stage2(&xc, &eps, h, m, config)?;
97    let (p, q, beta, sigma2) = if config.erratum_pass {
98        // 1983 erratum fix: recompute residuals through the eq. (1.12)
99        // recursion of the first-pass model and repeat stage 2.
100        let (phi, theta) = (&beta[..p], &beta[p..]);
101        let start = p.max(q);
102        let mut eps2 = vec![0.0; n];
103        for t in start..n {
104            let mut e = xc[t];
105            for (j, ph) in phi.iter().enumerate() {
106                e -= ph * xc[t - j - 1];
107            }
108            for (j, th) in theta.iter().enumerate() {
109                e -= th * eps2[t - j - 1];
110            }
111            eps2[t] = e;
112        }
113        stage2(&xc, &eps2, 0, m, config)?
114    } else {
115        (p, q, beta, sigma2)
116    };
117    Ok(HrResult {
118        p,
119        q,
120        phi: beta[..p].to_vec(),
121        theta: beta[p..].to_vec(),
122        sigma2,
123        long_ar: h,
124    })
125}
126
127/// The stage-2 grid: regress `xc[t]` on `p` lags of `xc` and `q` lags of the
128/// innovation proxy (`eps[t - offset]` holds ε(t)), over the common sample
129/// `t = m..n`, and minimise `log σ̃² + (p+q) log T / T`.
130fn stage2(
131    xc: &[f64],
132    eps: &[f64],
133    offset: usize,
134    m: usize,
135    config: HrConfig,
136) -> Result<(usize, usize, Vec<f64>, f64)> {
137    let n = xc.len();
138    let rows = n - m;
139    let tn = rows as f64;
140    let mut best: Option<(f64, usize, usize, Vec<f64>, f64)> = None;
141    for p in 0..=config.max_p {
142        for q in 0..=config.max_q {
143            let cols = p + q;
144            let (beta, sigma2) = if cols == 0 {
145                let s2 = (0..rows).map(|t| xc[m + t].powi(2)).sum::<f64>() / tn;
146                (Vec::new(), s2)
147            } else {
148                let mut design = vec![0.0; rows * cols];
149                let mut y = vec![0.0; rows];
150                for t in 0..rows {
151                    y[t] = xc[m + t];
152                    for j in 0..p {
153                        design[t * cols + j] = xc[m + t - j - 1];
154                    }
155                    for j in 0..q {
156                        design[t * cols + p + j] = eps[m + t - j - 1 - offset];
157                    }
158                }
159                match ols(&design, rows, cols, &y) {
160                    Ok((beta, _s2, _se)) => {
161                        // σ̃² with the paper's 1/T normalisation on the common sample.
162                        let mut rss = 0.0;
163                        for t in 0..rows {
164                            let fit: f64 = (0..cols).map(|c| design[t * cols + c] * beta[c]).sum();
165                            rss += (y[t] - fit).powi(2);
166                        }
167                        (beta, rss / tn)
168                    }
169                    // Common-factor singularity (p > p0 and q > q0): skip.
170                    Err(_) => continue,
171                }
172            };
173            let crit = sigma2.max(1e-300).ln() + (p + q) as f64 * tn.ln() / tn;
174            if best.as_ref().map_or(true, |(b, ..)| crit < *b) {
175                best = Some((crit, p, q, beta, sigma2));
176            }
177        }
178    }
179    let (_, p, q, beta, sigma2) = best.ok_or(crate::Error::Singular)?;
180    Ok((p, q, beta, sigma2))
181}