use ndarray::ArrayView1;
use super::common::CriticalValues;
use super::common::DeterministicTerm;
use super::common::adf_critical_values;
use super::common::fit_adf;
use super::common::newey_west_long_run_variance;
use super::common::schwert_max_lags;
use super::common::validate_series;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PPTestType {
Tau,
Rho,
}
#[derive(Debug, Clone, Copy)]
pub struct PhillipsPerronConfig {
pub deterministic: DeterministicTerm,
pub test_type: PPTestType,
pub lags: Option<usize>,
pub alpha: f64,
}
impl Default for PhillipsPerronConfig {
fn default() -> Self {
Self {
deterministic: DeterministicTerm::Constant,
test_type: PPTestType::Tau,
lags: None,
alpha: 0.05,
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct PhillipsPerronResult {
pub statistic: f64,
pub used_lags: usize,
pub test_type: PPTestType,
pub critical_values: Option<CriticalValues>,
pub reject_unit_root: Option<bool>,
}
impl crate::traits::HypothesisTest for PhillipsPerronResult {
fn statistic(&self) -> f64 {
self.statistic
}
fn null_rejected(&self) -> Option<bool> {
self.reject_unit_root
}
}
pub fn phillips_perron_test(y: ArrayView1<f64>, cfg: PhillipsPerronConfig) -> PhillipsPerronResult {
let y = y
.as_slice()
.expect("phillips_perron_test requires a contiguous ArrayView1");
validate_series(y, 20);
assert!(
cfg.alpha > 0.0 && cfg.alpha < 1.0,
"alpha must be in (0, 1)"
);
let fit = fit_adf(y, 0, cfg.deterministic);
let u = fit.residuals;
let nobs = u.len();
let n_f = nobs as f64;
let s2 = u.iter().map(|v| v * v).sum::<f64>() / n_f;
let s = s2.max(1e-12).sqrt();
let gamma0 = s2;
let used_lags = cfg.lags.unwrap_or_else(|| schwert_max_lags(y.len()));
let lam2 = newey_west_long_run_variance(&u, used_lags).max(1e-12);
let lam = lam2.sqrt();
let rho = 1.0 + fit.gamma;
let statistic = match cfg.test_type {
PPTestType::Rho => {
n_f * (rho - 1.0) - 0.5 * ((n_f * n_f * fit.sigma2 / s2.max(1e-12)) * (lam2 - s2))
}
PPTestType::Tau => {
let se = fit.std_err_gamma.max(1e-12);
(gamma0 / lam2).sqrt() * ((rho - 1.0) / se) - 0.5 * ((lam2 - s2) / lam) * (n_f * se / s)
}
};
let (critical_values, reject_unit_root) = match cfg.test_type {
PPTestType::Tau => {
let cvals = adf_critical_values(cfg.deterministic);
(Some(cvals), Some(statistic < cvals.value_at(cfg.alpha)))
}
PPTestType::Rho => (None, None),
};
PhillipsPerronResult {
statistic,
used_lags,
test_type: cfg.test_type,
critical_values,
reject_unit_root,
}
}
#[cfg(test)]
mod tests {
use rand::SeedableRng;
use rand::rngs::StdRng;
use rand_distr::Distribution;
use rand_distr::Normal;
use super::PPTestType;
use super::PhillipsPerronConfig;
use super::phillips_perron_test;
use crate::stationarity::common::DeterministicTerm;
fn simulate_ar1(phi: f64, n: usize, seed: u64) -> Vec<f64> {
let innovations = {
let dist = Normal::new(0.0, 1.0).unwrap();
let mut rng = StdRng::seed_from_u64(seed);
(0..n).map(|_| dist.sample(&mut rng)).collect::<Vec<_>>()
};
let mut x = vec![0.0; n];
for t in 1..n {
x[t] = phi * x[t - 1] + innovations[t];
}
x
}
fn simulate_random_walk(n: usize, seed: u64) -> Vec<f64> {
let innovations = {
let dist = Normal::new(0.0, 1.0).unwrap();
let mut rng = StdRng::seed_from_u64(seed);
(0..n).map(|_| dist.sample(&mut rng)).collect::<Vec<_>>()
};
let mut x = vec![0.0; n];
for t in 1..n {
x[t] = x[t - 1] + innovations[t];
}
x
}
#[test]
fn pp_tau_rejects_stationary_ar1() {
let x = simulate_ar1(0.75, 2500, 0xA11CE);
let cfg = PhillipsPerronConfig {
deterministic: DeterministicTerm::Constant,
test_type: PPTestType::Tau,
lags: Some(12),
..PhillipsPerronConfig::default()
};
let res = phillips_perron_test(ndarray::ArrayView1::from(&x), cfg);
assert_eq!(
res.reject_unit_root,
Some(true),
"expected rejection, got {res:?}"
);
}
#[test]
fn pp_tau_random_walk_is_not_rejected_at_one_percent() {
let x = simulate_random_walk(2500, 0xBADC0DE);
let cfg = PhillipsPerronConfig {
deterministic: DeterministicTerm::Constant,
test_type: PPTestType::Tau,
lags: Some(12),
alpha: 0.01,
..PhillipsPerronConfig::default()
};
let res = phillips_perron_test(ndarray::ArrayView1::from(&x), cfg);
assert_eq!(
res.reject_unit_root,
Some(false),
"expected no rejection, got {res:?}"
);
}
}