use crate::error::{NumRs2Error, Result};
use scirs2_core::ndarray::{Array1, Array2, ArrayView1};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TrendType {
None,
Constant,
ConstantTrend,
}
impl std::str::FromStr for TrendType {
type Err = NumRs2Error;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
match s {
"nc" | "none" | "n" => Ok(TrendType::None),
"c" | "constant" => Ok(TrendType::Constant),
"ct" | "trend" | "ctt" => Ok(TrendType::ConstantTrend),
_ => Err(NumRs2Error::ValueError(format!(
"Invalid trend type '{}'. Use 'nc', 'c', or 'ct'.",
s
))),
}
}
}
impl TrendType {
fn n_deterministic(&self) -> usize {
match self {
TrendType::None => 0,
TrendType::Constant => 1,
TrendType::ConstantTrend => 2,
}
}
fn code(&self) -> &'static str {
match self {
TrendType::None => "nc",
TrendType::Constant => "c",
TrendType::ConstantTrend => "ct",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LagSelection {
AIC,
BIC,
Fixed(usize),
}
#[derive(Debug, Clone)]
pub struct AdfTestResult {
pub statistic: f64,
pub p_value: f64,
pub lags_used: usize,
pub n_obs: usize,
pub critical_values: CriticalValues,
pub trend: TrendType,
pub ic_best: Option<f64>,
}
#[derive(Debug, Clone)]
pub struct KpssTestResult {
pub statistic: f64,
pub p_value: f64,
pub bandwidth: usize,
pub n_obs: usize,
pub critical_values: CriticalValues,
pub trend: TrendType,
}
#[derive(Debug, Clone)]
pub struct PhillipsPerronResult {
pub z_tau: f64,
pub z_alpha: f64,
pub p_value: f64,
pub bandwidth: usize,
pub n_obs: usize,
pub critical_values: CriticalValues,
pub trend: TrendType,
}
#[derive(Debug, Clone)]
pub struct IntegrationOrderResult {
pub order: usize,
pub adf_results: Vec<AdfTestResult>,
pub significance: f64,
}
#[derive(Debug, Clone)]
pub struct CriticalValues {
pub one_pct: f64,
pub five_pct: f64,
pub ten_pct: f64,
}
struct OlsResult {
beta: Array1<f64>,
residuals: Array1<f64>,
rss: f64,
se: Array1<f64>,
n_obs: usize,
n_vars: usize,
}
fn ols_regression(x: &Array2<f64>, y: &Array1<f64>) -> Result<OlsResult> {
let n_obs = y.len();
let n_vars = x.ncols();
if n_obs <= n_vars {
return Err(NumRs2Error::ValueError(format!(
"Insufficient observations ({}) for {} regressors",
n_obs, n_vars
)));
}
let xtx = x.t().dot(x);
let xty = x.t().dot(y);
let beta = scirs2_linalg::solve(&xtx.view(), &xty.view(), None)
.map_err(|e| NumRs2Error::ComputationError(format!("OLS solve failed: {}", e)))?;
let y_hat = x.dot(&beta);
let residuals = y - &y_hat;
let rss: f64 = residuals.iter().map(|&r| r * r).sum();
let sigma2 = rss / (n_obs - n_vars) as f64;
let identity = Array2::<f64>::eye(n_vars);
let xtx_inv = scirs2_linalg::solve_multiple(&xtx.view(), &identity.view(), None)
.map_err(|e| NumRs2Error::ComputationError(format!("OLS inverse failed: {}", e)))?;
let mut se = Array1::zeros(n_vars);
for j in 0..n_vars {
let var_j = sigma2 * xtx_inv[[j, j]];
se[j] = if var_j > 0.0 { var_j.sqrt() } else { 0.0 };
}
Ok(OlsResult {
beta,
residuals,
rss,
se,
n_obs,
n_vars,
})
}
pub fn adf_test_full(
data: &ArrayView1<f64>,
lag_selection: LagSelection,
trend: TrendType,
max_lags: Option<usize>,
) -> Result<AdfTestResult> {
let n = data.len();
if n < 8 {
return Err(NumRs2Error::ValueError(format!(
"Need at least 8 observations for ADF test, got {}",
n
)));
}
let diff = difference(data, 1)?;
let auto_max = ((12.0 * (n as f64 / 100.0).powf(0.25)) as usize).max(1);
let max_lag = max_lags.unwrap_or(auto_max).min(n / 3);
let (best_lag, ic_best) = match lag_selection {
LagSelection::Fixed(lag) => {
if lag > max_lag {
return Err(NumRs2Error::ValueError(format!(
"Requested lag {} exceeds maximum {}",
lag, max_lag
)));
}
(lag, None)
}
LagSelection::AIC | LagSelection::BIC => {
select_lag_ic(data, &diff, trend, max_lag, lag_selection)?
}
};
let result = adf_regression(data, &diff, best_lag, trend)?;
let n_obs = result.n_obs;
let gamma_idx = trend.n_deterministic();
let gamma = result.beta[gamma_idx];
let se_gamma = result.se[gamma_idx];
if se_gamma < 1e-15 {
return Err(NumRs2Error::ComputationError(
"Standard error of gamma is effectively zero".to_string(),
));
}
let adf_stat = gamma / se_gamma;
let p_value = mackinnon_p_value(adf_stat, n_obs, trend);
let critical_values = adf_critical_values(n_obs, trend);
Ok(AdfTestResult {
statistic: adf_stat,
p_value,
lags_used: best_lag,
n_obs,
critical_values,
trend,
ic_best,
})
}
fn adf_regression(
data: &ArrayView1<f64>,
diff: &Array1<f64>,
lags: usize,
trend: TrendType,
) -> Result<OlsResult> {
let n = data.len();
let n_obs = diff.len() - lags;
if n_obs < 3 {
return Err(NumRs2Error::ValueError(
"Too few observations after accounting for lags".to_string(),
));
}
let n_vars = trend.n_deterministic() + 1 + lags;
let mut x = Array2::zeros((n_obs, n_vars));
let mut y = Array1::zeros(n_obs);
for i in 0..n_obs {
let t = i + lags; y[i] = diff[t];
let mut col = 0;
if trend == TrendType::Constant || trend == TrendType::ConstantTrend {
x[[i, col]] = 1.0;
col += 1;
}
if trend == TrendType::ConstantTrend {
x[[i, col]] = (t + 1) as f64;
col += 1;
}
x[[i, col]] = data[t];
col += 1;
for lag in 1..=lags {
if t >= lag {
x[[i, col]] = diff[t - lag];
}
col += 1;
}
}
ols_regression(&x, &y)
}
fn select_lag_ic(
data: &ArrayView1<f64>,
diff: &Array1<f64>,
trend: TrendType,
max_lag: usize,
method: LagSelection,
) -> Result<(usize, Option<f64>)> {
let mut best_lag = 0;
let mut best_ic = f64::INFINITY;
for lag in 0..=max_lag {
let result = match adf_regression(data, diff, lag, trend) {
Ok(r) => r,
Err(_) => continue,
};
let n = result.n_obs as f64;
let k = result.n_vars as f64;
let sigma2 = result.rss / n;
if sigma2 <= 0.0 {
continue;
}
let log_lik = -0.5 * n * (1.0 + (2.0 * std::f64::consts::PI).ln() + sigma2.ln());
let ic = match method {
LagSelection::AIC => -2.0 * log_lik + 2.0 * k,
LagSelection::BIC => -2.0 * log_lik + k * n.ln(),
LagSelection::Fixed(_) => 0.0, };
if ic < best_ic {
best_ic = ic;
best_lag = lag;
}
}
Ok((best_lag, Some(best_ic)))
}
fn adf_critical_values(n: usize, trend: TrendType) -> CriticalValues {
let t = n as f64;
let t_inv = 1.0 / t;
let t_inv2 = t_inv * t_inv;
match trend {
TrendType::None => {
let one_pct = -2.5658 + (-1.960) * t_inv + (-10.04) * t_inv2;
let five_pct = -1.9393 + (-0.398) * t_inv + 0.0 * t_inv2;
let ten_pct = -1.6156 + (-0.181) * t_inv + 0.0 * t_inv2;
CriticalValues {
one_pct,
five_pct,
ten_pct,
}
}
TrendType::Constant => {
let one_pct = -3.4336 + (-5.999) * t_inv + (-29.25) * t_inv2;
let five_pct = -2.8621 + (-2.738) * t_inv + (-8.36) * t_inv2;
let ten_pct = -2.5671 + (-1.438) * t_inv + (-4.48) * t_inv2;
CriticalValues {
one_pct,
five_pct,
ten_pct,
}
}
TrendType::ConstantTrend => {
let one_pct = -3.9638 + (-8.353) * t_inv + (-47.44) * t_inv2;
let five_pct = -3.4126 + (-4.039) * t_inv + (-17.83) * t_inv2;
let ten_pct = -3.1279 + (-2.418) * t_inv + (-7.58) * t_inv2;
CriticalValues {
one_pct,
five_pct,
ten_pct,
}
}
}
}
fn mackinnon_p_value(stat: f64, n: usize, trend: TrendType) -> f64 {
let cv = adf_critical_values(n, trend);
let levels: [(f64, f64); 4] = [
(0.01, cv.one_pct),
(0.05, cv.five_pct),
(0.10, cv.ten_pct),
(
0.50,
match trend {
TrendType::None => -0.44,
TrendType::Constant => -0.80,
TrendType::ConstantTrend => -1.50,
},
),
];
if stat <= levels[0].1 {
let ratio = (stat - levels[0].1) / (levels[0].1 - levels[1].1).abs();
return (0.01 * (-ratio.abs()).exp()).max(1e-6);
}
if stat >= levels[3].1 {
let z = stat - levels[3].1;
return (0.50 + 0.40 * (1.0 - (-0.5 * z).exp())).min(0.999);
}
for i in 0..levels.len() - 1 {
if stat <= levels[i + 1].1 && stat >= levels[i].1 {
let frac = (stat - levels[i].1) / (levels[i + 1].1 - levels[i].1);
return levels[i].0 + frac * (levels[i + 1].0 - levels[i].0);
}
}
for i in 0..levels.len() - 1 {
if stat >= levels[i].1 && stat <= levels[i + 1].1 {
let frac = (stat - levels[i].1) / (levels[i + 1].1 - levels[i].1);
return levels[i].0 + frac * (levels[i + 1].0 - levels[i].0);
}
}
0.50
}
pub fn kpss_test_full(
data: &ArrayView1<f64>,
trend: TrendType,
bandwidth: Option<usize>,
) -> Result<KpssTestResult> {
let n = data.len();
if n < 10 {
return Err(NumRs2Error::ValueError(format!(
"Need at least 10 observations for KPSS test, got {}",
n
)));
}
if trend == TrendType::None {
return Err(NumRs2Error::ValueError(
"KPSS test requires 'c' (level) or 'ct' (trend) specification".to_string(),
));
}
let residuals = detrend_for_kpss(data, trend)?;
let bw = bandwidth.unwrap_or_else(|| newey_west_bandwidth(n));
let bw = bw.min(n - 1);
let mut partial_sums = Array1::zeros(n);
partial_sums[0] = residuals[0];
for i in 1..n {
partial_sums[i] = partial_sums[i - 1] + residuals[i];
}
let numerator: f64 = partial_sums.iter().map(|&s| s * s).sum::<f64>() / (n as f64 * n as f64);
let s2 = newey_west_variance(&residuals, bw)?;
if s2 <= 0.0 {
return Err(NumRs2Error::ComputationError(
"Long-run variance estimate is non-positive; series may be constant".to_string(),
));
}
let kpss_stat = numerator / s2;
let critical_values = kpss_critical_values(trend);
let p_value = kpss_p_value_approx(kpss_stat, trend);
Ok(KpssTestResult {
statistic: kpss_stat,
p_value,
bandwidth: bw,
n_obs: n,
critical_values,
trend,
})
}
fn detrend_for_kpss(data: &ArrayView1<f64>, trend: TrendType) -> Result<Array1<f64>> {
let n = data.len();
let y = Array1::from_iter(data.iter().copied());
match trend {
TrendType::Constant => {
let mean: f64 = y.iter().sum::<f64>() / n as f64;
Ok(y.mapv(|v| v - mean))
}
TrendType::ConstantTrend => {
let mut x = Array2::zeros((n, 2));
for i in 0..n {
x[[i, 0]] = 1.0;
x[[i, 1]] = (i + 1) as f64;
}
let result = ols_regression(&x, &y)?;
Ok(result.residuals)
}
TrendType::None => Err(NumRs2Error::ValueError(
"KPSS requires 'c' or 'ct' trend specification".to_string(),
)),
}
}
fn newey_west_variance(residuals: &Array1<f64>, bandwidth: usize) -> Result<f64> {
let n = residuals.len();
let gamma0: f64 = residuals.iter().map(|&r| r * r).sum::<f64>() / n as f64;
let mut s2 = gamma0;
for j in 1..=bandwidth.min(n - 1) {
let weight = 1.0 - j as f64 / (bandwidth as f64 + 1.0);
let mut gamma_j = 0.0;
for t in j..n {
gamma_j += residuals[t] * residuals[t - j];
}
gamma_j /= n as f64;
s2 += 2.0 * weight * gamma_j;
}
Ok(s2)
}
fn newey_west_bandwidth(n: usize) -> usize {
let bw = (4.0 * (n as f64 / 100.0).powf(2.0 / 9.0)).floor() as usize;
bw.max(1)
}
fn kpss_critical_values(trend: TrendType) -> CriticalValues {
match trend {
TrendType::Constant => CriticalValues {
one_pct: 0.739,
five_pct: 0.463,
ten_pct: 0.347,
},
TrendType::ConstantTrend => CriticalValues {
one_pct: 0.216,
five_pct: 0.146,
ten_pct: 0.119,
},
TrendType::None => CriticalValues {
one_pct: 0.739,
five_pct: 0.463,
ten_pct: 0.347,
},
}
}
fn kpss_p_value_approx(stat: f64, trend: TrendType) -> f64 {
let cv = kpss_critical_values(trend);
let levels: [(f64, f64); 4] = [
(0.01, cv.one_pct),
(0.05, cv.five_pct),
(0.10, cv.ten_pct),
(
0.50,
match trend {
TrendType::Constant => 0.119,
TrendType::ConstantTrend => 0.045,
TrendType::None => 0.119,
},
),
];
if stat > levels[0].1 {
let ratio = (stat - levels[0].1) / levels[0].1;
return (0.01 * (-ratio).exp()).max(1e-6);
}
for i in 0..levels.len() - 1 {
if stat <= levels[i].1 && stat >= levels[i + 1].1 {
let frac = (levels[i].1 - stat) / (levels[i].1 - levels[i + 1].1);
return levels[i].0 + frac * (levels[i + 1].0 - levels[i].0);
}
}
if stat < levels[levels.len() - 1].1 {
return (0.50 + 0.40 * (1.0 - stat / levels[levels.len() - 1].1).max(0.0)).min(0.999);
}
0.50
}
pub fn phillips_perron_test(
data: &ArrayView1<f64>,
trend: TrendType,
bandwidth: Option<usize>,
) -> Result<PhillipsPerronResult> {
let n = data.len();
if n < 10 {
return Err(NumRs2Error::ValueError(format!(
"Need at least 10 observations for PP test, got {}",
n
)));
}
let n_obs = n - 1;
let n_det = trend.n_deterministic();
let n_vars = n_det + 1;
let mut x = Array2::zeros((n_obs, n_vars));
let mut y = Array1::zeros(n_obs);
for i in 0..n_obs {
y[i] = data[i + 1];
let mut col = 0;
if trend == TrendType::Constant || trend == TrendType::ConstantTrend {
x[[i, col]] = 1.0;
col += 1;
}
if trend == TrendType::ConstantTrend {
x[[i, col]] = (i + 2) as f64;
col += 1;
}
x[[i, col]] = data[i];
}
let ols = ols_regression(&x, &y)?;
let rho_idx = n_det; let rho_hat = ols.beta[rho_idx];
let se_rho = ols.se[rho_idx];
let residuals = &ols.residuals;
let sigma2 = ols.rss / n_obs as f64;
let bw = bandwidth.unwrap_or_else(|| newey_west_bandwidth(n));
let bw = bw.min(n_obs - 1);
let s2 = newey_west_variance(residuals, bw)?;
if s2 <= 0.0 || sigma2 <= 0.0 {
return Err(NumRs2Error::ComputationError(
"Variance estimate is non-positive in PP test".to_string(),
));
}
let lambda2 = (s2 - sigma2).max(0.0);
let xtx = x.t().dot(&x);
let identity = Array2::<f64>::eye(n_vars);
let xtx_inv =
scirs2_linalg::solve_multiple(&xtx.view(), &identity.view(), None).map_err(|e| {
NumRs2Error::ComputationError(format!("PP test matrix inverse failed: {}", e))
})?;
let mxx_rho = xtx_inv[[rho_idx, rho_idx]]; let sum_y2: f64 = (0..n_obs).map(|i| data[i] * data[i]).sum();
let t_rho = (rho_hat - 1.0) / se_rho;
let z_tau =
(sigma2 / s2).sqrt() * t_rho - 0.5 * lambda2 * (n_obs as f64 * mxx_rho).sqrt() / s2.sqrt();
let z_alpha = n_obs as f64 * (rho_hat - 1.0)
- 0.5 * (n_obs as f64).powi(2) * se_rho.powi(2) * lambda2 / (sigma2 * n_obs as f64);
let p_value = mackinnon_p_value(z_tau, n_obs, trend);
let critical_values = adf_critical_values(n_obs, trend);
Ok(PhillipsPerronResult {
z_tau,
z_alpha,
p_value,
bandwidth: bw,
n_obs,
critical_values,
trend,
})
}
pub fn difference(data: &ArrayView1<f64>, d: usize) -> Result<Array1<f64>> {
if d == 0 {
return Ok(Array1::from_iter(data.iter().copied()));
}
let n = data.len();
if n <= d {
return Err(NumRs2Error::ValueError(format!(
"Cannot take {}-order difference of series with {} observations",
d, n
)));
}
let mut result = Array1::zeros(n - 1);
for i in 0..(n - 1) {
result[i] = data[i + 1] - data[i];
}
for _ in 1..d {
let len = result.len();
if len < 2 {
return Err(NumRs2Error::ValueError(
"Series too short for additional differencing".to_string(),
));
}
let mut next = Array1::zeros(len - 1);
for i in 0..(len - 1) {
next[i] = result[i + 1] - result[i];
}
result = next;
}
Ok(result)
}
pub fn seasonal_difference(data: &ArrayView1<f64>, period: usize) -> Result<Array1<f64>> {
let n = data.len();
if period == 0 {
return Err(NumRs2Error::ValueError(
"Seasonal period must be positive".to_string(),
));
}
if n <= period {
return Err(NumRs2Error::ValueError(format!(
"Series length ({}) must exceed seasonal period ({})",
n, period
)));
}
let mut result = Array1::zeros(n - period);
for i in 0..(n - period) {
result[i] = data[i + period] - data[i];
}
Ok(result)
}
pub fn detrend_linear(data: &ArrayView1<f64>) -> Result<Array1<f64>> {
let n = data.len();
if n < 3 {
return Err(NumRs2Error::ValueError(format!(
"Need at least 3 observations for linear detrending, got {}",
n
)));
}
let y = Array1::from_iter(data.iter().copied());
let mut x = Array2::zeros((n, 2));
for i in 0..n {
x[[i, 0]] = 1.0;
x[[i, 1]] = i as f64;
}
let result = ols_regression(&x, &y)?;
Ok(result.residuals)
}
pub fn detrend_polynomial(data: &ArrayView1<f64>, degree: usize) -> Result<Array1<f64>> {
let n = data.len();
if degree == 0 {
let mean: f64 = data.iter().sum::<f64>() / n as f64;
return Ok(Array1::from_iter(data.iter().map(|&v| v - mean)));
}
if n <= degree + 1 {
return Err(NumRs2Error::ValueError(format!(
"Need at least {} observations for degree-{} polynomial, got {}",
degree + 2,
degree,
n
)));
}
if degree > 10 {
return Err(NumRs2Error::ValueError(
"Polynomial degree too high (max 10); consider using differencing instead".to_string(),
));
}
let y = Array1::from_iter(data.iter().copied());
let n_vars = degree + 1;
let mut x = Array2::zeros((n, n_vars));
for i in 0..n {
let t = i as f64 / n as f64; let mut power = 1.0;
for j in 0..n_vars {
x[[i, j]] = power;
power *= t;
}
}
let result = ols_regression(&x, &y)?;
Ok(result.residuals)
}
pub fn integration_order(
data: &ArrayView1<f64>,
max_order: Option<usize>,
significance: Option<f64>,
trend: TrendType,
) -> Result<IntegrationOrderResult> {
let max_d = max_order.unwrap_or(5);
let alpha = significance.unwrap_or(0.05);
let mut results = Vec::new();
if alpha <= 0.0 || alpha >= 1.0 {
return Err(NumRs2Error::ValueError(format!(
"Significance level must be in (0, 1), got {}",
alpha
)));
}
let mut current = Array1::from_iter(data.iter().copied());
for d in 0..=max_d {
if current.len() < 8 {
return Ok(IntegrationOrderResult {
order: d,
adf_results: results,
significance: alpha,
});
}
let adf = adf_test_full(¤t.view(), LagSelection::BIC, trend, None)?;
let reject = adf.p_value < alpha;
results.push(adf);
if reject {
return Ok(IntegrationOrderResult {
order: d,
adf_results: results,
significance: alpha,
});
}
if d < max_d {
current = difference(¤t.view(), 1)?;
}
}
Ok(IntegrationOrderResult {
order: max_d + 1,
adf_results: results,
significance: alpha,
})
}
#[cfg(test)]
mod tests {
use super::*;
use scirs2_core::ndarray::Array1;
use scirs2_core::random::{Rng, SeedableRng, StdRng};
use std::str::FromStr;
fn white_noise(n: usize, seed: u64) -> Array1<f64> {
let mut rng = StdRng::seed_from_u64(seed);
Array1::from_iter((0..n).map(|_| rng.gen_range(-1.0..1.0)))
}
fn random_walk(n: usize, seed: u64) -> Array1<f64> {
let mut rng = StdRng::seed_from_u64(seed);
let mut data = Array1::zeros(n);
for i in 1..n {
data[i] = data[i - 1] + rng.gen_range(-1.0..1.0);
}
data
}
fn trend_stationary(n: usize, seed: u64) -> Array1<f64> {
let mut rng = StdRng::seed_from_u64(seed);
Array1::from_iter((0..n).map(|i| 0.5 + 0.1 * i as f64 + rng.gen_range(-0.5..0.5)))
}
#[test]
fn test_adf_stationary_white_noise() {
let data = white_noise(200, 42);
let result = adf_test_full(&data.view(), LagSelection::BIC, TrendType::Constant, None);
assert!(result.is_ok());
let r = result.expect("ADF should succeed");
assert!(
r.p_value < 0.10,
"White noise should be detected as stationary, p={:.4}",
r.p_value
);
}
#[test]
fn test_adf_nonstationary_random_walk() {
let data = random_walk(200, 42);
let result = adf_test_full(&data.view(), LagSelection::BIC, TrendType::Constant, None);
assert!(result.is_ok());
let r = result.expect("ADF should succeed");
assert!(
r.p_value > 0.05,
"Random walk should be detected as non-stationary, p={:.4}",
r.p_value
);
}
#[test]
fn test_adf_with_trend() {
let data = trend_stationary(200, 42);
let result = adf_test_full(
&data.view(),
LagSelection::BIC,
TrendType::ConstantTrend,
None,
);
assert!(result.is_ok());
let r = result.expect("ADF should succeed");
assert!(r.statistic.is_finite());
assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
}
#[test]
fn test_adf_lag_selection_aic() {
let data = white_noise(100, 123);
let result = adf_test_full(
&data.view(),
LagSelection::AIC,
TrendType::Constant,
Some(10),
);
assert!(result.is_ok());
let r = result.expect("ADF AIC should succeed");
assert!(r.lags_used <= 10);
assert!(r.ic_best.is_some());
}
#[test]
fn test_adf_lag_selection_bic() {
let data = white_noise(100, 456);
let result = adf_test_full(
&data.view(),
LagSelection::BIC,
TrendType::Constant,
Some(10),
);
assert!(result.is_ok());
let r = result.expect("ADF BIC should succeed");
assert!(r.lags_used <= 10);
assert!(r.ic_best.is_some());
}
#[test]
fn test_adf_fixed_lags() {
let data = white_noise(100, 789);
let result = adf_test_full(
&data.view(),
LagSelection::Fixed(3),
TrendType::Constant,
None,
);
assert!(result.is_ok());
let r = result.expect("ADF fixed lag should succeed");
assert_eq!(r.lags_used, 3);
assert!(r.ic_best.is_none());
}
#[test]
fn test_adf_critical_values_structure() {
let data = white_noise(100, 111);
let result = adf_test_full(
&data.view(),
LagSelection::Fixed(2),
TrendType::Constant,
None,
)
.expect("ADF should succeed");
assert!(result.critical_values.one_pct < result.critical_values.five_pct);
assert!(result.critical_values.five_pct < result.critical_values.ten_pct);
}
#[test]
fn test_adf_no_constant() {
let data = white_noise(100, 222);
let result = adf_test_full(&data.view(), LagSelection::Fixed(1), TrendType::None, None);
assert!(result.is_ok());
}
#[test]
fn test_adf_short_series() {
let data = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let result = adf_test_full(
&data.view(),
LagSelection::Fixed(0),
TrendType::Constant,
None,
);
assert!(result.is_err());
}
#[test]
fn test_kpss_stationary() {
let data = white_noise(200, 42);
let result = kpss_test_full(&data.view(), TrendType::Constant, None);
assert!(result.is_ok());
let r = result.expect("KPSS should succeed");
assert!(
r.p_value > 0.05,
"White noise should be level-stationary, p={:.4}",
r.p_value
);
}
#[test]
fn test_kpss_nonstationary() {
let data = random_walk(200, 42);
let result = kpss_test_full(&data.view(), TrendType::Constant, None);
assert!(result.is_ok());
let r = result.expect("KPSS should succeed");
assert!(
r.p_value < 0.10,
"Random walk should not be level-stationary, p={:.4}, stat={:.4}",
r.p_value,
r.statistic
);
}
#[test]
fn test_kpss_trend_stationarity() {
let data = trend_stationary(200, 42);
let result = kpss_test_full(&data.view(), TrendType::ConstantTrend, None);
assert!(result.is_ok());
let r = result.expect("KPSS trend should succeed");
assert!(r.statistic >= 0.0);
assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
}
#[test]
fn test_kpss_bandwidth_selection() {
let data = white_noise(100, 333);
let result = kpss_test_full(&data.view(), TrendType::Constant, None);
assert!(result.is_ok());
let r = result.expect("KPSS should succeed");
assert!(r.bandwidth > 0);
assert!(r.bandwidth < r.n_obs);
}
#[test]
fn test_kpss_manual_bandwidth() {
let data = white_noise(100, 444);
let result = kpss_test_full(&data.view(), TrendType::Constant, Some(5));
assert!(result.is_ok());
let r = result.expect("KPSS should succeed");
assert_eq!(r.bandwidth, 5);
}
#[test]
fn test_kpss_invalid_trend() {
let data = white_noise(50, 555);
let result = kpss_test_full(&data.view(), TrendType::None, None);
assert!(result.is_err());
}
#[test]
fn test_pp_stationary() {
let data = white_noise(200, 42);
let result = phillips_perron_test(&data.view(), TrendType::Constant, None);
assert!(result.is_ok());
let r = result.expect("PP should succeed");
assert!(r.z_tau.is_finite());
assert!(r.z_alpha.is_finite());
assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
}
#[test]
fn test_pp_nonstationary() {
let data = random_walk(200, 42);
let result = phillips_perron_test(&data.view(), TrendType::Constant, None);
assert!(result.is_ok());
let r = result.expect("PP should succeed");
assert!(
r.p_value > 0.05,
"Random walk should have unit root, p={:.4}",
r.p_value
);
}
#[test]
fn test_pp_with_trend() {
let data = trend_stationary(200, 42);
let result = phillips_perron_test(&data.view(), TrendType::ConstantTrend, None);
assert!(result.is_ok());
let r = result.expect("PP trend should succeed");
assert!(r.z_tau.is_finite());
assert!(r.p_value >= 0.0 && r.p_value <= 1.0);
}
#[test]
fn test_difference_first_order() {
let data = Array1::from_vec(vec![1.0, 3.0, 6.0, 10.0, 15.0]);
let diff = difference(&data.view(), 1).expect("difference should succeed");
assert_eq!(diff.len(), 4);
let expected = [2.0, 3.0, 4.0, 5.0];
for (i, &e) in expected.iter().enumerate() {
assert!(
(diff[i] - e).abs() < 1e-10,
"diff[{}] = {}, expected {}",
i,
diff[i],
e
);
}
}
#[test]
fn test_difference_second_order() {
let data = Array1::from_vec(vec![1.0, 3.0, 6.0, 10.0, 15.0]);
let diff2 = difference(&data.view(), 2).expect("second difference should succeed");
assert_eq!(diff2.len(), 3);
for i in 0..diff2.len() {
assert!((diff2[i] - 1.0).abs() < 1e-10);
}
}
#[test]
fn test_seasonal_difference() {
let data = Array1::from_vec(vec![10.0, 20.0, 30.0, 40.0, 11.0, 22.0, 33.0, 44.0]);
let sdiff = seasonal_difference(&data.view(), 4).expect("seasonal diff should succeed");
assert_eq!(sdiff.len(), 4);
let expected = [1.0, 2.0, 3.0, 4.0];
for (i, &e) in expected.iter().enumerate() {
assert!((sdiff[i] - e).abs() < 1e-10);
}
}
#[test]
fn test_difference_zero_order() {
let data = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let diff0 = difference(&data.view(), 0).expect("zero difference should succeed");
assert_eq!(diff0.len(), 3);
assert!((diff0[0] - 1.0).abs() < 1e-10);
}
#[test]
fn test_detrend_linear() {
let n = 50;
let mut rng = StdRng::seed_from_u64(42);
let data =
Array1::from_iter((0..n).map(|i| 2.0 + 3.0 * i as f64 + rng.gen_range(-0.1..0.1)));
let detrended = detrend_linear(&data.view()).expect("detrend should succeed");
assert_eq!(detrended.len(), n);
let mean: f64 = detrended.iter().sum::<f64>() / n as f64;
assert!(
mean.abs() < 0.5,
"Detrended mean should be near zero, got {}",
mean
);
}
#[test]
fn test_detrend_polynomial() {
let n = 100;
let mut rng = StdRng::seed_from_u64(42);
let data = Array1::from_iter((0..n).map(|i| {
let t = i as f64 / n as f64;
1.0 + 2.0 * t + 3.0 * t * t + rng.gen_range(-0.01..0.01)
}));
let detrended = detrend_polynomial(&data.view(), 2).expect("poly detrend should succeed");
assert_eq!(detrended.len(), n);
let max_abs: f64 = detrended.iter().map(|&r| r.abs()).fold(0.0, f64::max);
assert!(
max_abs < 0.5,
"Polynomial detrend residuals too large: max={}",
max_abs
);
}
#[test]
fn test_integration_order_stationary() {
let data = white_noise(200, 42);
let result = integration_order(&data.view(), Some(3), Some(0.05), TrendType::Constant);
assert!(result.is_ok());
let r = result.expect("integration order should succeed");
assert_eq!(r.order, 0, "White noise should be I(0)");
}
#[test]
fn test_integration_order_random_walk() {
let data = random_walk(200, 42);
let result = integration_order(&data.view(), Some(3), Some(0.05), TrendType::Constant);
assert!(result.is_ok());
let r = result.expect("integration order should succeed");
assert!(
r.order <= 2,
"Random walk should be I(1) or I(2), got I({})",
r.order
);
}
#[test]
fn test_integration_order_i2() {
let rw = random_walk(200, 42);
let mut i2 = Array1::zeros(200);
i2[0] = rw[0];
for i in 1..200 {
i2[i] = i2[i - 1] + rw[i];
}
let result = integration_order(&i2.view(), Some(4), Some(0.05), TrendType::Constant);
assert!(result.is_ok());
let r = result.expect("integration order should succeed");
assert!(
r.order >= 1,
"I(2) process should have order >= 1, got I({})",
r.order
);
}
#[test]
fn test_constant_series_adf() {
let data = Array1::from_vec(vec![5.0; 50]);
let result = adf_test_full(
&data.view(),
LagSelection::Fixed(1),
TrendType::Constant,
None,
);
assert!(result.is_err() || result.is_ok());
}
#[test]
fn test_short_series_kpss() {
let data = Array1::from_vec(vec![1.0, 2.0, 3.0]);
let result = kpss_test_full(&data.view(), TrendType::Constant, None);
assert!(result.is_err());
}
#[test]
fn test_trend_type_from_str() {
assert_eq!(TrendType::from_str("nc").expect("ok"), TrendType::None);
assert_eq!(TrendType::from_str("c").expect("ok"), TrendType::Constant);
assert_eq!(
TrendType::from_str("ct").expect("ok"),
TrendType::ConstantTrend
);
assert!(TrendType::from_str("invalid").is_err());
}
#[test]
fn test_seasonal_difference_invalid_period() {
let data = Array1::from_vec(vec![1.0, 2.0, 3.0]);
assert!(seasonal_difference(&data.view(), 0).is_err());
assert!(seasonal_difference(&data.view(), 5).is_err());
}
#[test]
fn test_adf_and_kpss_complementary() {
let data = white_noise(200, 9999);
let adf = adf_test_full(&data.view(), LagSelection::BIC, TrendType::Constant, None)
.expect("ADF should succeed");
let kpss =
kpss_test_full(&data.view(), TrendType::Constant, None).expect("KPSS should succeed");
let adf_agrees = adf.p_value < 0.15; let kpss_agrees = kpss.p_value > 0.05;
assert!(
adf_agrees || kpss_agrees,
"At least one test should detect stationarity: ADF p={:.4}, KPSS p={:.4}",
adf.p_value,
kpss.p_value
);
}
}