#![allow(non_snake_case)]
use crate::linalg;
use ndarray::{s, Array1, Array2};
pub struct AdfTestResult {
pub stat: f64,
pub p_value: f64,
pub used_lags: usize,
}
pub fn adf_test(series: &Array1<f64>, max_lags: Option<usize>) -> AdfTestResult {
let n = series.len();
if n < 4 {
return AdfTestResult {
stat: 0.0,
p_value: 1.0,
used_lags: 0,
};
}
let std_dev = series.std(0.0);
if std_dev.abs() < f64::EPSILON || std_dev.is_nan() {
return AdfTestResult {
stat: 0.0,
p_value: 0.0, used_lags: 0,
};
}
let lags = max_lags.unwrap_or_else(|| ((n as f64 - 1.0).powf(1.0 / 3.0)) as usize);
let dy = &series.slice(s![1..]) - &series.slice(s![..-1]);
let dy_len = dy.len();
if dy_len <= lags {
return AdfTestResult {
stat: 0.0,
p_value: 1.0,
used_lags: lags,
};
}
let effective_n = dy_len - lags;
let cols = 2 + lags;
if effective_n <= cols {
return AdfTestResult {
stat: 0.0,
p_value: 1.0,
used_lags: lags,
};
}
let y_dep = dy.slice(s![lags..dy_len]).to_owned();
let mut x = Array2::<f64>::zeros((effective_n, cols));
for i in 0..effective_n {
let idx = i + lags;
x[[i, 0]] = series[idx]; x[[i, 1]] = 1.0;
for j in 0..lags {
x[[i, 2 + j]] = dy[idx - 1 - j]; }
}
let beta = match linalg::lstsq(&x, &y_dep) {
Ok(b) => b,
Err(_) => {
return AdfTestResult {
stat: 0.0,
p_value: 1.0,
used_lags: lags,
};
}
};
let residuals = &y_dep - &x.dot(&beta);
let sse = residuals.iter().map(|r| r.powi(2)).sum::<f64>();
let df = effective_n - cols;
let mse = sse / (df as f64);
let xtx_inv = match linalg::inv(&x.t().dot(&x)) {
Ok(m) => m,
Err(_) => {
return AdfTestResult {
stat: 0.0,
p_value: 1.0,
used_lags: lags,
};
}
};
let se_gamma = (mse * xtx_inv[[0, 0]]).sqrt();
if se_gamma <= 0.0 || se_gamma.is_nan() {
return AdfTestResult {
stat: 0.0,
p_value: 0.0,
used_lags: lags,
};
}
let t_stat = beta[0] / se_gamma;
let p_value = dickey_fuller_pvalue(t_stat);
AdfTestResult {
stat: t_stat,
p_value,
used_lags: lags,
}
}
fn dickey_fuller_pvalue(tau: f64) -> f64 {
const TABLE: [(f64, f64); 8] = [
(-3.43, 0.01),
(-3.12, 0.025),
(-2.86, 0.05),
(-2.57, 0.10),
(-0.44, 0.90),
(-0.07, 0.95),
(0.23, 0.975),
(0.60, 0.99),
];
if tau <= TABLE[0].0 {
return 0.01;
}
let last = TABLE[TABLE.len() - 1];
if tau >= last.0 {
return 0.99;
}
for w in TABLE.windows(2) {
let (t0, p0) = w[0];
let (t1, p1) = w[1];
if tau >= t0 && tau <= t1 {
let frac = (tau - t0) / (t1 - t0);
return (p0 + frac * (p1 - p0)).clamp(0.0, 1.0);
}
}
1.0
}
pub fn estimate_d(series: &Array1<f64>, max_d: usize, alpha: f64) -> usize {
let mut current = series.clone();
let mut d = 0;
while d < max_d {
let res = adf_test(¤t, None);
if res.p_value < alpha {
break;
}
if current.len() <= 2 {
break;
}
current = ¤t.slice(s![1..]) - ¤t.slice(s![..-1]);
d += 1;
}
d
}
pub fn estimate_D(series: &Array1<f64>, m: usize, max_D: usize) -> usize {
if m <= 1 || series.len() < 2 * m {
return 0;
}
let mut current = series.clone();
let mut D = 0;
while D < max_D {
let n = current.len();
if n <= 2 * m {
break;
}
let m_neg = -(m as isize);
let seasonal_diff = ¤t.slice(s![m..]) - ¤t.slice(s![..m_neg]);
let var_orig = crate::utils::variance(¤t);
let var_sdiff = crate::utils::variance(&seasonal_diff);
let seasonal_strength = (1.0 - (var_sdiff / var_orig)).max(0.0);
if seasonal_strength < 0.64 {
break;
}
current = seasonal_diff;
D += 1;
}
D
}