use linreg_core::diagnostics::{cooks_distance_test, dfbetas_test, dffits_test, DiagnosticTestResult};
use linreg_core::error::Error;
fn approx_eq(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() < tol
}
#[test]
fn test_cooks_distance_insufficient_data() {
let y = vec![1.0, 2.0];
let x = vec![1.0, 2.0];
let result = cooks_distance_test(&y, &[x]);
match result {
Err(Error::InsufficientData { .. }) => (),
_ => panic!("Expected InsufficientData error"),
}
}
#[test]
fn test_cooks_distance_perfect_fit() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = cooks_distance_test(&y, &[x]).unwrap();
for &d in &result.distances {
assert!(approx_eq(d, 0.0, 1e-10));
}
assert_eq!(result.influential_4_over_n.len(), 0);
assert_eq!(result.test_name, "Cook's Distance");
}
#[test]
fn test_cooks_distance_with_outlier() {
let y = vec![1.0, 2.0, 3.0, 4.0, 100.0];
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = cooks_distance_test(&y, &[x]).unwrap();
let max_idx = result
.distances
.iter()
.enumerate()
.max_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
.map(|(i, _)| i)
.unwrap();
assert_eq!(max_idx, 4); assert!(result.distances[max_idx] > 0.1); }
#[test]
fn test_cooks_distance_threshold() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0, 10.0];
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let result = cooks_distance_test(&y, &[x]).unwrap();
let expected_threshold = 4.0 / 6.0;
assert!((result.threshold_4_over_n - expected_threshold).abs() < 1e-10);
assert_eq!(result.influential_4_over_n, vec![6]);
}
#[test]
fn test_cooks_distance_multiple_predictors() {
let y = vec![
10.0, 15.0, 20.0, 25.0, 30.0, 35.0, 40.0, 45.0, 50.0, 55.0,
];
let x1: Vec<f64> = (1..=10).map(|i| i as f64).collect();
let x2: Vec<f64> = (1..=10).map(|i| (i as f64) * 2.0 + ((i % 2) as f64)).collect();
let result = cooks_distance_test(&y, &[x1, x2]).unwrap();
assert_eq!(result.distances.len(), 10);
assert_eq!(result.p, 3);
for &d in &result.distances {
assert!(d >= 0.0);
}
}
#[test]
fn test_cooks_distance_output_structure() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = cooks_distance_test(&y, &[x]).unwrap();
assert!(!result.test_name.is_empty());
assert!(!result.distances.is_empty());
assert!(result.p > 0);
assert!(!result.interpretation.is_empty());
assert!(!result.guidance.is_empty());
}
#[test]
fn test_dfbetas_insufficient_data() {
let y = vec![1.0, 2.0];
let x = vec![1.0, 2.0];
let result = dfbetas_test(&y, &[x]);
match result {
Err(Error::InsufficientData { .. }) => (),
_ => panic!("Expected InsufficientData error"),
}
}
#[test]
fn test_dfbetas_perfect_fit() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = dfbetas_test(&y, &[x]).unwrap();
let influential_count: usize = result.influential_observations.values().map(|v| v.len()).sum();
assert!(influential_count <= 3, "Expected at most 3 influential observations for near-perfect fit, got {}", influential_count);
}
#[test]
fn test_dfbetas_structure() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = dfbetas_test(&y, &[x]).unwrap();
assert_eq!(result.dfbetas.len(), 5);
for obs_dfbetas in &result.dfbetas {
assert_eq!(obs_dfbetas.len(), 2); }
}
#[test]
fn test_dfbetas_with_outlier() {
let y = vec![1.0, 2.0, 3.0, 4.0, 100.0];
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = dfbetas_test(&y, &[x]).unwrap();
let total_influential: usize = result.influential_observations
.values()
.map(|v| v.len())
.sum();
assert!(total_influential > 0, "Expected some influential observations with extreme outlier");
}
#[test]
fn test_dfbetas_threshold() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0];
let result = dfbetas_test(&y, &[x]).unwrap();
let expected_threshold = 2.0 / (6.0_f64.sqrt());
assert!((result.threshold - expected_threshold).abs() < 1e-10);
}
#[test]
fn test_dfbetas_output_structure() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = dfbetas_test(&y, &[x]).unwrap();
assert!(!result.test_name.is_empty());
assert_eq!(result.n, 5);
assert!(result.p > 0);
assert!(result.threshold.is_finite());
assert!(!result.interpretation.is_empty());
assert!(!result.guidance.is_empty());
}
#[test]
fn test_dffits_insufficient_data() {
let y = vec![1.0, 2.0];
let x = vec![1.0, 2.0];
let result = dffits_test(&y, &[x]);
match result {
Err(Error::InsufficientData { .. }) => (),
_ => panic!("Expected InsufficientData error"),
}
}
#[test]
fn test_dffits_perfect_fit() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = dffits_test(&y, &[x]).unwrap();
let max_abs = result.dffits.iter().map(|&v| v.abs()).fold(0.0_f64, |a, b| a.max(b));
assert!(max_abs < 2.0, "Max DFFITS should be small for perfect fit, got {}", max_abs);
}
#[test]
fn test_dffits_with_outlier() {
let y = vec![1.0, 2.0, 3.0, 4.0, 100.0];
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = dffits_test(&y, &[x]).unwrap();
let max_abs = result.dffits.iter().map(|&v| v.abs()).fold(0.0_f64, |a, b| a.max(b));
assert!(max_abs > 0.5, "Expected some DFFITS to be affected by outlier, got max {}", max_abs);
}
#[test]
fn test_dffits_threshold() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = dffits_test(&y, &[x]).unwrap();
let expected_threshold = 2.0 * (2.0_f64 / 5.0_f64).sqrt();
assert!((result.threshold - expected_threshold).abs() < 1e-10);
}
#[test]
fn test_dffits_n_and_p() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let x1 = vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
let x2 = vec![2.1, 4.0, 5.9, 8.1, 10.0, 11.9, 14.1, 16.0];
let result = dffits_test(&y, &[x1, x2]).unwrap();
assert_eq!(result.n, 8);
assert_eq!(result.p, 3); assert_eq!(result.dffits.len(), 8);
}
#[test]
fn test_dffits_output_structure() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let result = dffits_test(&y, &[x]).unwrap();
assert!(!result.test_name.is_empty());
assert_eq!(result.dffits.len(), 5);
assert!(result.n > 0);
assert!(result.p > 0);
assert!(result.threshold.is_finite());
assert!(!result.interpretation.is_empty());
assert!(!result.guidance.is_empty());
}
#[test]
fn test_influence_tests_agree_on_outlier() {
let y = vec![1.0, 2.0, 3.0, 4.0, 100.0];
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let cooks = cooks_distance_test(&y, &[x.clone()]).unwrap();
let dfbetas = dfbetas_test(&y, &[x.clone()]).unwrap();
let dffits = dffits_test(&y, &[x]).unwrap();
assert!(cooks.influential_4_over_n.contains(&5));
let dfbetas_count: usize = dfbetas.influential_observations.values().map(|v| v.len()).sum();
let dffits_count = dffits.influential_observations.len();
assert!(dfbetas_count > 0 || dffits_count > 0 || cooks.influential_4_over_n.len() > 0,
"Expected some influential observations to be detected");
}
#[test]
fn test_influence_tests_agree_on_clean_data() {
let y = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let cooks = cooks_distance_test(&y, &[x.clone()]).unwrap();
let dfbetas = dfbetas_test(&y, &[x.clone()]).unwrap();
let dffits = dffits_test(&y, &[x]).unwrap();
assert!(cooks.influential_4_over_n.is_empty());
let dfbetas_count: usize = dfbetas.influential_observations.values().map(|v| v.len()).sum();
let dffits_count = dffits.influential_observations.len();
assert!(dfbetas_count <= 3, "DFBETAS flagged {} observations, expected <= 3", dfbetas_count);
assert!(dffits_count <= 3, "DFFITS flagged {} observations, expected <= 3", dffits_count);
}