use crate::error::Result;
use crate::forecast::spa_common::{
compute_means, compute_spa_pvalues, compute_standardized, compute_variances, find_best_model,
validate_model_data,
};
#[derive(Debug, Clone)]
pub struct SPAResult {
pub statistic: f64,
pub p_value_consistent: f64,
pub p_value_upper: f64,
pub n_bootstrap: usize,
pub best_model_idx: Option<usize>,
}
pub fn spa_test(
benchmark_losses: &[f64],
model_losses: &[Vec<f64>],
n_bootstrap: usize,
block_length: f64,
seed: Option<u64>,
) -> Result<SPAResult> {
let t = benchmark_losses.len();
validate_model_data(t, model_losses, "competing model")?;
let d: Vec<Vec<f64>> = compute_loss_differentials(benchmark_losses, model_losses);
let d_bar = compute_means(&d);
let variances = compute_variances(&d, &d_bar);
let standardized = compute_standardized(&d_bar, &variances, t);
let observed = standardized
.iter()
.cloned()
.fold(f64::NEG_INFINITY, f64::max);
let best_model_idx = find_best_model(&standardized);
let (p_value_consistent, p_value_upper) =
compute_spa_pvalues(&d, &d_bar, observed, n_bootstrap, block_length, seed);
Ok(SPAResult {
statistic: observed,
p_value_consistent,
p_value_upper,
n_bootstrap,
best_model_idx,
})
}
fn compute_loss_differentials(benchmark: &[f64], models: &[Vec<f64>]) -> Vec<Vec<f64>> {
models
.iter()
.map(|model_loss| {
benchmark
.iter()
.zip(model_loss.iter())
.map(|(b, m)| b - m)
.collect()
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_spa_clearly_better_model() {
let benchmark: Vec<f64> = vec![10.0; 100];
let model: Vec<Vec<f64>> = vec![vec![1.0; 100]];
let result = spa_test(&benchmark, &model, 499, 5.0, Some(42)).unwrap();
assert!(
result.p_value_consistent < 0.05,
"p_value {} should be < 0.05",
result.p_value_consistent
);
assert!(result.best_model_idx == Some(0));
}
#[test]
fn test_spa_no_better_model() {
let benchmark: Vec<f64> = (0..100).map(|i| (i as f64 * 0.1).sin()).collect();
let model1: Vec<f64> = benchmark
.iter()
.enumerate()
.map(|(i, &x)| x + 0.01 * (i as f64).cos())
.collect();
let model2: Vec<f64> = benchmark
.iter()
.enumerate()
.map(|(i, &x)| x - 0.01 * (i as f64).sin())
.collect();
let models = vec![model1, model2];
let result = spa_test(&benchmark, &models, 499, 5.0, Some(42)).unwrap();
assert!(
result.p_value_upper > 0.01,
"p_value_upper {} should be > 0.01",
result.p_value_upper
);
}
#[test]
fn test_spa_multiple_models() {
let benchmark: Vec<f64> = vec![5.0; 50];
let models = vec![
vec![4.0; 50], vec![2.0; 50], vec![6.0; 50], vec![4.5; 50], ];
let result = spa_test(&benchmark, &models, 499, 3.0, Some(42)).unwrap();
assert_eq!(result.best_model_idx, Some(1));
}
#[test]
fn test_spa_empty_benchmark() {
let benchmark: Vec<f64> = vec![];
let models = vec![vec![1.0, 2.0, 3.0]];
assert!(spa_test(&benchmark, &models, 100, 3.0, None).is_err());
}
#[test]
fn test_spa_no_models() {
let benchmark: Vec<f64> = vec![1.0, 2.0, 3.0];
let models: Vec<Vec<f64>> = vec![];
assert!(spa_test(&benchmark, &models, 100, 3.0, None).is_err());
}
#[test]
fn test_spa_length_mismatch() {
let benchmark: Vec<f64> = vec![1.0, 2.0, 3.0];
let models = vec![vec![1.0, 2.0]];
assert!(spa_test(&benchmark, &models, 100, 3.0, None).is_err());
}
}