use crate::error::{Result, StatError};
use statrs::distribution::{ContinuousCDF, Normal};
#[derive(Debug, Clone)]
pub struct CWResult {
pub statistic: f64,
pub p_value: f64,
pub p_value_two_sided: f64,
}
pub fn clark_west(e1: &[f64], e2: &[f64], h: usize) -> Result<CWResult> {
let n = e1.len();
if n == 0 {
return Err(StatError::EmptyData);
}
if e1.len() != e2.len() {
return Err(StatError::InvalidParameter(
"Forecast error vectors must have equal length".to_string(),
));
}
if n < 3 {
return Err(StatError::InsufficientData { needed: 3, got: n });
}
let d: Vec<f64> = e1
.iter()
.zip(e2.iter())
.map(|(e1_t, e2_t)| {
let adjustment = (e1_t - e2_t).powi(2);
e1_t.powi(2) - e2_t.powi(2) + adjustment
})
.collect();
let n_f = n as f64;
let d_bar: f64 = d.iter().sum::<f64>() / n_f;
let var_d_bar = cw_variance(&d, h);
let statistic = if var_d_bar > 1e-30 {
d_bar / var_d_bar.sqrt()
} else {
0.0
};
let normal = Normal::new(0.0, 1.0).unwrap();
let p_value = normal.sf(statistic);
let p_value_two_sided = 2.0 * normal.sf(statistic.abs());
Ok(CWResult {
statistic,
p_value,
p_value_two_sided,
})
}
fn autocovariance(centered: &[f64], lag: usize) -> f64 {
let n_f = centered.len() as f64;
centered
.iter()
.skip(lag)
.zip(centered.iter())
.map(|(d_t, d_t_k)| d_t * d_t_k)
.sum::<f64>()
/ n_f
}
fn cw_variance(d: &[f64], h: usize) -> f64 {
let n = d.len();
let n_f = n as f64;
let d_bar: f64 = d.iter().sum::<f64>() / n_f;
let d_centered: Vec<f64> = d.iter().map(|x| x - d_bar).collect();
let max_lag = h.saturating_sub(1);
let gamma_0 = autocovariance(&d_centered, 0);
let lagged_sum: f64 = (1..=max_lag)
.take_while(|&k| k < n)
.map(|k| 2.0 * autocovariance(&d_centered, k))
.sum();
(gamma_0 + lagged_sum) / n_f
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cw_identical_errors() {
let e1 = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let e2 = e1.clone();
let result = clark_west(&e1, &e2, 1).unwrap();
assert!(result.statistic.abs() < 1e-10);
}
#[test]
fn test_cw_unrestricted_better() {
let e1: Vec<f64> = (0..50)
.map(|i| 2.0 + (i as f64 * 0.1).sin() * 0.1)
.collect();
let e2: Vec<f64> = (0..50)
.map(|i| 1.0 + (i as f64 * 0.1).cos() * 0.1)
.collect();
let result = clark_west(&e1, &e2, 1).unwrap();
assert!(result.statistic > 0.0);
assert!(result.p_value < 0.05);
}
#[test]
fn test_cw_restricted_better() {
let e1: Vec<f64> = (0..50)
.map(|i| 1.0 + (i as f64 * 0.1).sin() * 0.1)
.collect();
let e2: Vec<f64> = (0..50)
.map(|i| 2.0 + (i as f64 * 0.1).cos() * 0.1)
.collect();
let result = clark_west(&e1, &e2, 1).unwrap();
assert!(result.statistic < 0.0);
assert!(result.p_value > 0.5);
}
#[test]
fn test_cw_empty_error() {
let e1: Vec<f64> = vec![];
let e2: Vec<f64> = vec![];
assert!(clark_west(&e1, &e2, 1).is_err());
}
#[test]
fn test_cw_unequal_length_error() {
let e1 = vec![1.0, 2.0, 3.0];
let e2 = vec![1.0, 2.0];
assert!(clark_west(&e1, &e2, 1).is_err());
}
#[test]
fn test_cw_insufficient_data_error() {
let e1 = vec![1.0, 2.0];
let e2 = vec![1.0, 2.0];
assert!(clark_west(&e1, &e2, 1).is_err());
}
#[test]
fn test_cw_horizon_adjustment() {
let e1: Vec<f64> = (0..100).map(|i| (i as f64 * 0.1).sin() + 1.0).collect();
let e2: Vec<f64> = (0..100).map(|i| (i as f64 * 0.1).cos() + 0.5).collect();
let result_h1 = clark_west(&e1, &e2, 1).unwrap();
let result_h3 = clark_west(&e1, &e2, 3).unwrap();
assert!((result_h1.statistic - result_h3.statistic).abs() > 1e-10);
}
}