use crate::error::{StatsError, StatsResult};
use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
#[derive(Debug, Clone)]
pub struct AdfResult {
pub statistic: f64,
pub p_value: f64,
pub used_lags: usize,
pub n_obs: usize,
pub critical_values: CriticalValues,
pub regression: AdfRegression,
}
#[derive(Debug, Clone)]
pub struct KpssResult {
pub statistic: f64,
pub p_value: f64,
pub used_lags: usize,
pub critical_values: CriticalValues,
pub trend: KpssTrend,
}
#[derive(Debug, Clone)]
pub struct PhillipsPerronResult {
pub z_alpha: f64,
pub z_tau: f64,
pub p_value: f64,
pub used_lags: usize,
pub n_obs: usize,
pub critical_values: CriticalValues,
}
#[derive(Debug, Clone)]
pub struct ZivotAndrewsResult {
pub statistic: f64,
pub p_value: f64,
pub break_point: usize,
pub used_lags: usize,
pub critical_values: CriticalValues,
pub break_type: ZivotAndrewsBreak,
}
#[derive(Debug, Clone, Copy)]
pub struct CriticalValues {
pub one_pct: f64,
pub five_pct: f64,
pub ten_pct: f64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AdfRegression {
None,
Constant,
ConstantTrend,
ConstantTrendSquared,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KpssTrend {
Constant,
Trend,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ZivotAndrewsBreak {
Intercept,
Trend,
Both,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LagCriterion {
Aic,
Bic,
Hqic,
TStatistic,
}
pub(crate) struct OlsResult {
pub(crate) coefficients: Array1<f64>,
pub(crate) residuals: Array1<f64>,
pub(crate) sigma_sq: f64,
pub(crate) se: Array1<f64>,
}
pub(crate) fn ols_regression(y: &ArrayView1<f64>, x: &Array2<f64>) -> StatsResult<OlsResult> {
let n = y.len();
let k = x.ncols();
if n != x.nrows() {
return Err(StatsError::DimensionMismatch(format!(
"y length {} != X rows {}",
n,
x.nrows()
)));
}
if n <= k {
return Err(StatsError::InsufficientData(format!(
"need n > k for OLS (n={}, k={})",
n, k
)));
}
let xtx = x.t().dot(x);
let xty = x.t().dot(y);
let beta = solve_symmetric(&xtx, &xty)?;
let fitted = x.dot(&beta);
let mut residuals = Array1::zeros(n);
for i in 0..n {
residuals[i] = y[i] - fitted[i];
}
let df = (n - k) as f64;
let rss: f64 = residuals.iter().map(|r| r * r).sum();
let sigma_sq = rss / df;
let xtx_inv = invert_symmetric(&xtx)?;
let mut se = Array1::zeros(k);
for j in 0..k {
let var_j = sigma_sq * xtx_inv[[j, j]];
se[j] = if var_j > 0.0 { var_j.sqrt() } else { 0.0 };
}
Ok(OlsResult {
coefficients: beta,
residuals,
sigma_sq,
se,
})
}
fn solve_symmetric(a: &Array2<f64>, b: &Array1<f64>) -> StatsResult<Array1<f64>> {
let n = a.nrows();
if n != a.ncols() || n != b.len() {
return Err(StatsError::DimensionMismatch(
"solve_symmetric: dimension mismatch".into(),
));
}
let mut l = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..=i {
let mut sum = 0.0;
for kk in 0..j {
sum += l[[i, kk]] * l[[j, kk]];
}
if i == j {
let diag = a[[i, i]] - sum;
if diag <= 0.0 {
return solve_with_regularization(a, b);
}
l[[i, j]] = diag.sqrt();
} else {
let denom = l[[j, j]];
if denom.abs() < 1e-15 {
return solve_with_regularization(a, b);
}
l[[i, j]] = (a[[i, j]] - sum) / denom;
}
}
}
let mut z = Array1::<f64>::zeros(n);
for i in 0..n {
let mut sum = 0.0;
for j in 0..i {
sum += l[[i, j]] * z[j];
}
let denom = l[[i, i]];
if denom.abs() < 1e-15 {
return solve_with_regularization(a, b);
}
z[i] = (b[i] - sum) / denom;
}
let mut x = Array1::<f64>::zeros(n);
for i in (0..n).rev() {
let mut sum = 0.0;
for j in (i + 1)..n {
sum += l[[j, i]] * x[j];
}
let denom = l[[i, i]];
if denom.abs() < 1e-15 {
return solve_with_regularization(a, b);
}
x[i] = (z[i] - sum) / denom;
}
Ok(x)
}
fn solve_with_regularization(a: &Array2<f64>, b: &Array1<f64>) -> StatsResult<Array1<f64>> {
let n = a.nrows();
let mut a_reg = a.clone();
let ridge = 1e-10
* (0..n)
.map(|i| a[[i, i]].abs())
.fold(0.0_f64, f64::max)
.max(1e-10);
for i in 0..n {
a_reg[[i, i]] += ridge;
}
let mut l = Array2::<f64>::zeros((n, n));
for i in 0..n {
for j in 0..=i {
let mut sum = 0.0;
for kk in 0..j {
sum += l[[i, kk]] * l[[j, kk]];
}
if i == j {
let diag = a_reg[[i, i]] - sum;
l[[i, j]] = if diag > 0.0 { diag.sqrt() } else { 1e-15 };
} else {
let denom = l[[j, j]];
l[[i, j]] = if denom.abs() > 1e-15 {
(a_reg[[i, j]] - sum) / denom
} else {
0.0
};
}
}
}
let mut z = Array1::<f64>::zeros(n);
for i in 0..n {
let mut sum = 0.0;
for j in 0..i {
sum += l[[i, j]] * z[j];
}
let denom = l[[i, i]];
z[i] = if denom.abs() > 1e-15 {
(b[i] - sum) / denom
} else {
0.0
};
}
let mut x = Array1::<f64>::zeros(n);
for i in (0..n).rev() {
let mut sum = 0.0;
for j in (i + 1)..n {
sum += l[[j, i]] * x[j];
}
let denom = l[[i, i]];
x[i] = if denom.abs() > 1e-15 {
(z[i] - sum) / denom
} else {
0.0
};
}
Ok(x)
}
fn invert_symmetric(a: &Array2<f64>) -> StatsResult<Array2<f64>> {
let n = a.nrows();
let mut inv = Array2::<f64>::zeros((n, n));
for j in 0..n {
let mut e = Array1::<f64>::zeros(n);
e[j] = 1.0;
let col = solve_symmetric(a, &e)?;
for i in 0..n {
inv[[i, j]] = col[i];
}
}
Ok(inv)
}
fn diff(y: &ArrayView1<f64>) -> Array1<f64> {
let n = y.len();
if n < 2 {
return Array1::zeros(0);
}
let mut d = Array1::zeros(n - 1);
for i in 0..(n - 1) {
d[i] = y[i + 1] - y[i];
}
d
}
fn build_adf_design(
y: &ArrayView1<f64>,
lags: usize,
regression: AdfRegression,
) -> StatsResult<(Array1<f64>, Array2<f64>, usize)> {
let dy = diff(y);
let n_dy = dy.len();
if n_dy <= lags + 1 {
return Err(StatsError::InsufficientData(format!(
"series too short for {} lags (n_dy={})",
lags, n_dy
)));
}
let n_eff = n_dy - lags;
let dep = Array1::from_vec((lags..n_dy).map(|i| dy[i]).collect());
let mut n_reg: usize = 1 + lags; match regression {
AdfRegression::None => {}
AdfRegression::Constant => n_reg += 1,
AdfRegression::ConstantTrend => n_reg += 2,
AdfRegression::ConstantTrendSquared => n_reg += 3,
}
let mut design = Array2::<f64>::zeros((n_eff, n_reg));
for i in 0..n_eff {
let t_idx = lags + i; let mut col = 0;
match regression {
AdfRegression::None => {}
AdfRegression::Constant => {
design[[i, col]] = 1.0;
col += 1;
}
AdfRegression::ConstantTrend => {
design[[i, col]] = 1.0;
col += 1;
design[[i, col]] = (t_idx + 1) as f64;
col += 1;
}
AdfRegression::ConstantTrendSquared => {
design[[i, col]] = 1.0;
col += 1;
let t_val = (t_idx + 1) as f64;
design[[i, col]] = t_val;
col += 1;
design[[i, col]] = t_val * t_val;
col += 1;
}
}
design[[i, col]] = y[t_idx];
col += 1;
for lag in 1..=lags {
design[[i, col]] = dy[t_idx - lag];
col += 1;
}
}
Ok((dep, design, n_eff))
}
pub fn select_lag(
y: &ArrayView1<f64>,
max_lags: Option<usize>,
criterion: LagCriterion,
regression: AdfRegression,
) -> StatsResult<usize> {
let n = y.len();
if n < 10 {
return Err(StatsError::InsufficientData(
"need at least 10 observations for lag selection".into(),
));
}
let default_max = ((12.0 * ((n as f64) / 100.0).powf(0.25)) as usize).min(n / 3);
let max_p = max_lags.unwrap_or(default_max).min(n / 3);
match criterion {
LagCriterion::TStatistic => select_lag_tstat(y, max_p, regression),
_ => select_lag_ic(y, max_p, criterion, regression),
}
}
fn select_lag_ic(
y: &ArrayView1<f64>,
max_p: usize,
criterion: LagCriterion,
regression: AdfRegression,
) -> StatsResult<usize> {
let n = y.len() as f64;
let mut best_lag = 0;
let mut best_ic = f64::INFINITY;
for p in 0..=max_p {
let result = build_adf_design(y, p, regression);
let (dep, design, n_eff) = match result {
Ok(v) => v,
Err(_) => continue,
};
let ols = match ols_regression(&dep.view(), &design) {
Ok(v) => v,
Err(_) => continue,
};
let nf = n_eff as f64;
let k = design.ncols() as f64;
let rss: f64 = ols.residuals.iter().map(|r| r * r).sum();
let log_sigma = (rss / nf).ln();
let ic = match criterion {
LagCriterion::Aic => log_sigma + 2.0 * k / nf,
LagCriterion::Bic => log_sigma + k * n.ln() / nf,
LagCriterion::Hqic => log_sigma + 2.0 * k * n.ln().ln() / nf,
LagCriterion::TStatistic => log_sigma, };
if ic < best_ic {
best_ic = ic;
best_lag = p;
}
}
Ok(best_lag)
}
fn select_lag_tstat(
y: &ArrayView1<f64>,
max_p: usize,
regression: AdfRegression,
) -> StatsResult<usize> {
for p in (1..=max_p).rev() {
let result = build_adf_design(y, p, regression);
let (dep, design, _n_eff) = match result {
Ok(v) => v,
Err(_) => continue,
};
let ols = match ols_regression(&dep.view(), &design) {
Ok(v) => v,
Err(_) => continue,
};
let last_col = design.ncols() - 1;
let beta_last = ols.coefficients[last_col];
let se_last = ols.se[last_col];
if se_last > 1e-15 {
let t_stat = (beta_last / se_last).abs();
if t_stat > 1.645 {
return Ok(p);
}
}
}
Ok(0)
}
fn adf_critical_values(n: usize, regression: AdfRegression) -> CriticalValues {
let nf = n as f64;
match regression {
AdfRegression::None => CriticalValues {
one_pct: -2.5658 - 1.960 / nf - 10.04 / (nf * nf),
five_pct: -1.9393 - 0.398 / nf,
ten_pct: -1.6156 - 0.181 / nf,
},
AdfRegression::Constant => CriticalValues {
one_pct: -3.4336 - 5.999 / nf - 29.25 / (nf * nf),
five_pct: -2.8621 - 2.738 / nf - 8.36 / (nf * nf),
ten_pct: -2.5671 - 1.438 / nf - 4.48 / (nf * nf),
},
AdfRegression::ConstantTrend | AdfRegression::ConstantTrendSquared => CriticalValues {
one_pct: -3.9638 - 8.353 / nf - 47.44 / (nf * nf),
five_pct: -3.4126 - 4.039 / nf - 17.83 / (nf * nf),
ten_pct: -3.1279 - 2.418 / nf - 7.58 / (nf * nf),
},
}
}
fn adf_p_value(stat: f64, n: usize, regression: AdfRegression) -> f64 {
let cv = adf_critical_values(n, regression);
if stat <= cv.one_pct {
let slope = (0.05 - 0.01) / (cv.five_pct - cv.one_pct);
let p = 0.01 + slope * (stat - cv.one_pct);
p.max(0.0001)
} else if stat <= cv.five_pct {
let frac = (stat - cv.one_pct) / (cv.five_pct - cv.one_pct);
0.01 + frac * (0.05 - 0.01)
} else if stat <= cv.ten_pct {
let frac = (stat - cv.five_pct) / (cv.ten_pct - cv.five_pct);
0.05 + frac * (0.10 - 0.05)
} else {
let frac = (stat - cv.ten_pct) / (cv.ten_pct.abs()).max(1.0);
(0.10 + frac * 0.4).min(0.999)
}
}
pub fn adf_test(
y: &ArrayView1<f64>,
max_lags: Option<usize>,
regression: AdfRegression,
criterion: LagCriterion,
) -> StatsResult<AdfResult> {
let n = y.len();
if n < 12 {
return Err(StatsError::InsufficientData(
"ADF test requires at least 12 observations".into(),
));
}
let p = select_lag(y, max_lags, criterion, regression)?;
let (dep, design, n_eff) = build_adf_design(y, p, regression)?;
let ols = ols_regression(&dep.view(), &design)?;
let gamma_col = match regression {
AdfRegression::None => 0,
AdfRegression::Constant => 1,
AdfRegression::ConstantTrend => 2,
AdfRegression::ConstantTrendSquared => 3,
};
let gamma = ols.coefficients[gamma_col];
let se_gamma = ols.se[gamma_col];
let t_stat = if se_gamma > 1e-15 {
gamma / se_gamma
} else {
f64::NEG_INFINITY
};
let cv = adf_critical_values(n_eff, regression);
let p_value = adf_p_value(t_stat, n_eff, regression);
Ok(AdfResult {
statistic: t_stat,
p_value,
used_lags: p,
n_obs: n_eff,
critical_values: cv,
regression,
})
}
fn kpss_critical_values(trend: KpssTrend) -> CriticalValues {
match trend {
KpssTrend::Constant => CriticalValues {
one_pct: 0.739,
five_pct: 0.463,
ten_pct: 0.347,
},
KpssTrend::Trend => CriticalValues {
one_pct: 0.216,
five_pct: 0.146,
ten_pct: 0.119,
},
}
}
fn kpss_p_value(stat: f64, trend: KpssTrend) -> f64 {
let cv = kpss_critical_values(trend);
if stat >= cv.one_pct {
let overshoot = (stat - cv.one_pct) / cv.one_pct.max(0.001);
(0.01 - overshoot * 0.005).max(0.001)
} else if stat >= cv.five_pct {
let frac = (stat - cv.five_pct) / (cv.one_pct - cv.five_pct);
0.05 - frac * (0.05 - 0.01)
} else if stat >= cv.ten_pct {
let frac = (stat - cv.ten_pct) / (cv.five_pct - cv.ten_pct);
0.10 - frac * (0.10 - 0.05)
} else {
let frac = stat / cv.ten_pct.max(0.001);
(0.10 + (1.0 - frac) * 0.40).min(0.999)
}
}
pub fn kpss_test(
y: &ArrayView1<f64>,
trend: KpssTrend,
n_lags: Option<usize>,
) -> StatsResult<KpssResult> {
let n = y.len();
if n < 6 {
return Err(StatsError::InsufficientData(
"KPSS test requires at least 6 observations".into(),
));
}
let n_det = match trend {
KpssTrend::Constant => 1,
KpssTrend::Trend => 2,
};
let mut det = Array2::<f64>::zeros((n, n_det));
for i in 0..n {
det[[i, 0]] = 1.0;
if n_det == 2 {
det[[i, 1]] = (i + 1) as f64;
}
}
let ols = ols_regression(&y.into(), &det)?;
let resid = &ols.residuals;
let mut s = Array1::<f64>::zeros(n);
s[0] = resid[0];
for i in 1..n {
s[i] = s[i - 1] + resid[i];
}
let lags = n_lags.unwrap_or(((n as f64).sqrt() * 0.75) as usize);
let nf = n as f64;
let gamma0: f64 = resid.iter().map(|r| r * r).sum::<f64>() / nf;
let mut long_run_var = gamma0;
for lag in 1..=lags {
let w = 1.0 - (lag as f64) / ((lags + 1) as f64); let mut gamma_l = 0.0;
for i in lag..n {
gamma_l += resid[i] * resid[i - lag];
}
gamma_l /= nf;
long_run_var += 2.0 * w * gamma_l;
}
if long_run_var <= 0.0 {
return Err(StatsError::ComputationError(
"KPSS: estimated long-run variance is non-positive".into(),
));
}
let ss: f64 = s.iter().map(|si| si * si).sum();
let eta = ss / (nf * nf * long_run_var);
let cv = kpss_critical_values(trend);
let p_value = kpss_p_value(eta, trend);
Ok(KpssResult {
statistic: eta,
p_value,
used_lags: lags,
critical_values: cv,
trend,
})
}
fn pp_critical_values(n: usize) -> CriticalValues {
adf_critical_values(n, AdfRegression::Constant)
}
pub fn phillips_perron_test(
y: &ArrayView1<f64>,
n_lags: Option<usize>,
) -> StatsResult<PhillipsPerronResult> {
let n = y.len();
if n < 12 {
return Err(StatsError::InsufficientData(
"Phillips-Perron test requires at least 12 observations".into(),
));
}
let (dep, design, n_eff) = build_adf_design(y, 0, AdfRegression::Constant)?;
let ols = ols_regression(&dep.view(), &design)?;
let gamma = ols.coefficients[1]; let se_gamma = ols.se[1];
let t_stat = if se_gamma > 1e-15 {
gamma / se_gamma
} else {
f64::NEG_INFINITY
};
let nf = n_eff as f64;
let resid = &ols.residuals;
let lags = n_lags.unwrap_or(((nf).powf(1.0 / 3.0) * 1.5) as usize);
let gamma0: f64 = resid.iter().map(|r| r * r).sum::<f64>() / nf;
let mut lambda_sq = gamma0;
for lag in 1..=lags {
let w = 1.0 - (lag as f64) / ((lags + 1) as f64);
let mut g = 0.0;
for i in lag..n_eff {
g += resid[i] * resid[i - lag];
}
g /= nf;
lambda_sq += 2.0 * w * g;
}
lambda_sq = lambda_sq.max(1e-15);
let y_lag: Vec<f64> = (0..n_eff)
.map(|i| {
let idx = i; design[[i, 1]]
})
.collect();
let y_lag_mean: f64 = y_lag.iter().sum::<f64>() / nf;
let y_lag_var: f64 = y_lag.iter().map(|v| (v - y_lag_mean).powi(2)).sum::<f64>();
let s_sq = gamma0; let correction_factor = (lambda_sq - s_sq) * nf / (2.0 * y_lag_var.max(1e-15));
let z_alpha = nf * gamma - correction_factor;
let ratio = (s_sq / lambda_sq).sqrt();
let z_tau = t_stat * ratio
- (lambda_sq - s_sq) * nf.sqrt() / (2.0 * lambda_sq.sqrt() * y_lag_var.max(1e-15).sqrt());
let cv = pp_critical_values(n_eff);
let p_value = adf_p_value(z_tau, n_eff, AdfRegression::Constant);
Ok(PhillipsPerronResult {
z_alpha,
z_tau,
p_value,
used_lags: lags,
n_obs: n_eff,
critical_values: cv,
})
}
fn za_critical_values(break_type: ZivotAndrewsBreak) -> CriticalValues {
match break_type {
ZivotAndrewsBreak::Intercept => CriticalValues {
one_pct: -5.34,
five_pct: -4.80,
ten_pct: -4.58,
},
ZivotAndrewsBreak::Trend => CriticalValues {
one_pct: -4.93,
five_pct: -4.42,
ten_pct: -4.11,
},
ZivotAndrewsBreak::Both => CriticalValues {
one_pct: -5.57,
five_pct: -5.08,
ten_pct: -4.82,
},
}
}
pub fn zivot_andrews_test(
y: &ArrayView1<f64>,
max_lags: Option<usize>,
break_type: ZivotAndrewsBreak,
trim: Option<f64>,
) -> StatsResult<ZivotAndrewsResult> {
let n = y.len();
if n < 20 {
return Err(StatsError::InsufficientData(
"Zivot-Andrews test requires at least 20 observations".into(),
));
}
let trim_frac = trim.unwrap_or(0.15);
let start = (n as f64 * trim_frac).ceil() as usize;
let end = n - start;
if start >= end {
return Err(StatsError::InvalidArgument(
"trim fraction too large for the data".into(),
));
}
let p = select_lag(y, max_lags, LagCriterion::Aic, AdfRegression::ConstantTrend)?;
let dy = diff(y);
let n_dy = dy.len();
let mut min_t_stat = f64::INFINITY;
let mut best_break = start;
let mut best_lags = p;
for tb in start..end {
let eff_start = p;
if n_dy <= eff_start {
continue;
}
let n_eff = n_dy - eff_start;
let dep = Array1::from_vec((eff_start..n_dy).map(|i| dy[i]).collect());
let n_break_cols = match break_type {
ZivotAndrewsBreak::Intercept => 1,
ZivotAndrewsBreak::Trend => 1,
ZivotAndrewsBreak::Both => 2,
};
let n_reg = 1 + 1 + 1 + n_break_cols + p; let mut design = Array2::<f64>::zeros((n_eff, n_reg));
for i in 0..n_eff {
let t_idx = eff_start + i;
let orig_t = t_idx + 1; let mut col = 0;
design[[i, col]] = 1.0;
col += 1;
design[[i, col]] = orig_t as f64;
col += 1;
match break_type {
ZivotAndrewsBreak::Intercept => {
design[[i, col]] = if orig_t > tb { 1.0 } else { 0.0 };
col += 1;
}
ZivotAndrewsBreak::Trend => {
design[[i, col]] = if orig_t > tb {
(orig_t - tb) as f64
} else {
0.0
};
col += 1;
}
ZivotAndrewsBreak::Both => {
design[[i, col]] = if orig_t > tb { 1.0 } else { 0.0 };
col += 1;
design[[i, col]] = if orig_t > tb {
(orig_t - tb) as f64
} else {
0.0
};
col += 1;
}
}
design[[i, col]] = y[t_idx];
col += 1;
for lag in 1..=p {
if t_idx >= lag {
design[[i, col]] = dy[t_idx - lag];
}
col += 1;
}
}
let ols = match ols_regression(&dep.view(), &design) {
Ok(v) => v,
Err(_) => continue,
};
let gamma_col = 2 + n_break_cols;
let gamma = ols.coefficients[gamma_col];
let se_gamma = ols.se[gamma_col];
let t_stat = if se_gamma > 1e-15 {
gamma / se_gamma
} else {
f64::NEG_INFINITY
};
if t_stat < min_t_stat {
min_t_stat = t_stat;
best_break = tb;
best_lags = p;
}
}
let cv = za_critical_values(break_type);
let p_value = if min_t_stat <= cv.one_pct {
0.005
} else if min_t_stat <= cv.five_pct {
let frac = (min_t_stat - cv.one_pct) / (cv.five_pct - cv.one_pct);
0.01 + frac * 0.04
} else if min_t_stat <= cv.ten_pct {
let frac = (min_t_stat - cv.five_pct) / (cv.ten_pct - cv.five_pct);
0.05 + frac * 0.05
} else {
let frac = (min_t_stat - cv.ten_pct) / cv.ten_pct.abs().max(1.0);
(0.10 + frac * 0.4).min(0.999)
};
Ok(ZivotAndrewsResult {
statistic: min_t_stat,
p_value,
break_point: best_break,
used_lags: best_lags,
critical_values: cv,
break_type,
})
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array1;
fn make_stationary_series(n: usize) -> Array1<f64> {
let mut y = vec![0.0_f64; n];
let noise: Vec<f64> = (0..n)
.map(|i| ((i as f64 * 1.7 + 0.3).sin()) * 0.5)
.collect();
for i in 1..n {
y[i] = 0.5 * y[i - 1] + noise[i];
}
Array1::from_vec(y)
}
fn make_random_walk(n: usize) -> Array1<f64> {
let mut y = vec![0.0_f64; n];
let noise: Vec<f64> = (0..n)
.map(|i| ((i as f64 * 2.3 + 0.7).sin()) * 0.3)
.collect();
for i in 1..n {
y[i] = y[i - 1] + noise[i];
}
Array1::from_vec(y)
}
#[test]
fn test_select_lag_aic() {
let y = make_stationary_series(100);
let p = select_lag(
&y.view(),
Some(10),
LagCriterion::Aic,
AdfRegression::Constant,
);
assert!(p.is_ok());
let p = p.expect("lag selection should succeed");
assert!(p <= 10);
}
#[test]
fn test_select_lag_bic() {
let y = make_stationary_series(100);
let p = select_lag(
&y.view(),
Some(10),
LagCriterion::Bic,
AdfRegression::Constant,
);
assert!(p.is_ok());
}
#[test]
fn test_select_lag_tstat() {
let y = make_stationary_series(100);
let p = select_lag(
&y.view(),
Some(10),
LagCriterion::TStatistic,
AdfRegression::Constant,
);
assert!(p.is_ok());
}
#[test]
fn test_adf_stationary() {
let y = make_stationary_series(200);
let result = adf_test(&y.view(), None, AdfRegression::Constant, LagCriterion::Aic);
assert!(result.is_ok());
let r = result.expect("ADF should succeed");
assert!(r.statistic.is_finite());
assert!(r.p_value >= 0.0);
assert!(r.n_obs > 0);
}
#[test]
fn test_adf_random_walk() {
let y = make_random_walk(200);
let result = adf_test(&y.view(), None, AdfRegression::Constant, LagCriterion::Aic);
assert!(result.is_ok());
let r = result.expect("ADF should succeed");
assert!(r.statistic.is_finite());
}
#[test]
fn test_adf_no_constant() {
let y = make_stationary_series(100);
let result = adf_test(&y.view(), Some(2), AdfRegression::None, LagCriterion::Bic);
assert!(result.is_ok());
}
#[test]
fn test_adf_trend() {
let y = make_stationary_series(100);
let result = adf_test(
&y.view(),
Some(3),
AdfRegression::ConstantTrend,
LagCriterion::Aic,
);
assert!(result.is_ok());
}
#[test]
fn test_kpss_stationary() {
let y = make_stationary_series(100);
let result = kpss_test(&y.view(), KpssTrend::Constant, None);
assert!(result.is_ok());
let r = result.expect("KPSS should succeed");
assert!(r.statistic.is_finite());
assert!(r.statistic >= 0.0);
}
#[test]
fn test_kpss_trend() {
let y = make_stationary_series(100);
let result = kpss_test(&y.view(), KpssTrend::Trend, Some(5));
assert!(result.is_ok());
}
#[test]
fn test_kpss_random_walk() {
let y = make_random_walk(200);
let result = kpss_test(&y.view(), KpssTrend::Constant, None);
assert!(result.is_ok());
let r = result.expect("KPSS should succeed");
assert!(r.statistic.is_finite());
}
#[test]
fn test_phillips_perron() {
let y = make_stationary_series(200);
let result = phillips_perron_test(&y.view(), None);
assert!(result.is_ok());
let r = result.expect("PP test should succeed");
assert!(r.z_tau.is_finite());
assert!(r.z_alpha.is_finite());
}
#[test]
fn test_phillips_perron_random_walk() {
let y = make_random_walk(200);
let result = phillips_perron_test(&y.view(), None);
assert!(result.is_ok());
let r = result.expect("PP test should succeed");
assert!(r.z_tau.is_finite());
}
#[test]
fn test_zivot_andrews_intercept() {
let mut y_vec = vec![0.0_f64; 80];
for i in 0..40 {
y_vec[i] = ((i as f64) * 1.3).sin() * 0.3;
}
for i in 40..80 {
y_vec[i] = 3.0 + ((i as f64) * 1.3).sin() * 0.3;
}
let y = Array1::from_vec(y_vec);
let result = zivot_andrews_test(&y.view(), Some(4), ZivotAndrewsBreak::Intercept, None);
assert!(result.is_ok());
let r = result.expect("ZA test should succeed");
assert!(r.statistic.is_finite());
assert!(r.break_point > 0);
}
#[test]
fn test_zivot_andrews_trend() {
let y_vec: Vec<f64> = (0..80)
.map(|i| {
let t = i as f64;
if i < 40 {
0.1 * t + (t * 0.7).sin() * 0.2
} else {
0.5 * t + (t * 0.7).sin() * 0.2
}
})
.collect();
let y = Array1::from_vec(y_vec);
let result = zivot_andrews_test(&y.view(), Some(3), ZivotAndrewsBreak::Trend, None);
assert!(result.is_ok());
}
#[test]
fn test_zivot_andrews_both() {
let y_vec: Vec<f64> = (0..80)
.map(|i| {
let t = i as f64;
if i < 40 {
0.1 * t + (t * 0.5).sin() * 0.2
} else {
5.0 + 0.5 * t + (t * 0.5).sin() * 0.2
}
})
.collect();
let y = Array1::from_vec(y_vec);
let result = zivot_andrews_test(&y.view(), Some(2), ZivotAndrewsBreak::Both, None);
assert!(result.is_ok());
}
#[test]
fn test_adf_insufficient_data() {
let y = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let result = adf_test(&y.view(), None, AdfRegression::Constant, LagCriterion::Aic);
assert!(result.is_err());
}
#[test]
fn test_kpss_insufficient_data() {
let y = Array1::from_vec(vec![1.0, 2.0]);
let result = kpss_test(&y.view(), KpssTrend::Constant, None);
assert!(result.is_err());
}
#[test]
fn test_pp_insufficient_data() {
let y = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let result = phillips_perron_test(&y.view(), None);
assert!(result.is_err());
}
#[test]
fn test_za_insufficient_data() {
let y = Array1::from_vec(vec![1.0; 5]);
let result = zivot_andrews_test(&y.view(), None, ZivotAndrewsBreak::Intercept, None);
assert!(result.is_err());
}
#[test]
fn test_critical_values_ordering() {
let cv = adf_critical_values(100, AdfRegression::Constant);
assert!(cv.one_pct < cv.five_pct);
assert!(cv.five_pct < cv.ten_pct);
}
#[test]
fn test_kpss_critical_values_ordering() {
let cv = kpss_critical_values(KpssTrend::Constant);
assert!(cv.one_pct > cv.five_pct);
assert!(cv.five_pct > cv.ten_pct);
}
#[test]
fn test_select_lag_auto_max() {
let y = make_stationary_series(200);
let p = select_lag(&y.view(), None, LagCriterion::Aic, AdfRegression::Constant);
assert!(p.is_ok());
let p = p.expect("auto lag selection should succeed");
assert!(p < 200 / 3);
}
}