qualia_core_db/solvers/statistics/
robust.rs1use super::descriptive::{mean, median_in_place, quantile_in_place};
6
7pub fn trimmed_mean(values: &[f64], proportion: f64) -> Option<f64> {
11 let n = values.len();
12 if n == 0 || !(0.0..0.5).contains(&proportion) {
13 return None;
14 }
15 let mut v = values.to_vec();
16 v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
17 let cut = (n as f64 * proportion).floor() as usize;
18 if 2 * cut >= n {
19 return median_in_place(&mut v); }
21 mean(&v[cut..n - cut])
22}
23
24pub fn winsorized_mean(values: &[f64], proportion: f64) -> Option<f64> {
28 let n = values.len();
29 if n == 0 || !(0.0..0.5).contains(&proportion) {
30 return None;
31 }
32 let mut v = values.to_vec();
33 v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
34 let cut = (n as f64 * proportion).floor() as usize;
35 if 2 * cut >= n {
36 return median_in_place(&mut v);
37 }
38 let lo = v[cut];
39 let hi = v[n - 1 - cut];
40 for x in v.iter_mut() {
41 if *x < lo {
42 *x = lo;
43 } else if *x > hi {
44 *x = hi;
45 }
46 }
47 mean(&v)
48}
49
50pub fn median_abs_deviation(values: &[f64], scaled: bool) -> Option<f64> {
54 if values.is_empty() {
55 return None;
56 }
57 let mut v = values.to_vec();
58 let med = median_in_place(&mut v)?;
59 let mut dev: Vec<f64> = values.iter().map(|&x| (x - med).abs()).collect();
60 let mad = median_in_place(&mut dev)?;
61 Some(if scaled {
62 1.482_602_218_505_602 * mad
63 } else {
64 mad
65 })
66}
67
68pub fn iqr(values: &[f64]) -> Option<f64> {
70 if values.is_empty() {
71 return None;
72 }
73 let mut v = values.to_vec();
74 let q3 = quantile_in_place(&mut v, 0.75)?;
75 let q1 = quantile_in_place(&mut v, 0.25)?;
76 Some(q3 - q1)
77}
78
79#[cfg(test)]
80mod tests {
81 use super::*;
82 const EPS: f64 = 1e-9;
83
84 #[test]
85 fn trimmed_mean_ignores_outliers() {
86 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 1000.0];
88 assert!(mean(&data).unwrap() > 100.0);
89 let tm = trimmed_mean(&data, 0.2).unwrap();
90 assert!((tm - 5.5).abs() < 1.0, "trimmed mean {tm}"); assert!((trimmed_mean(&[1.0, 2.0, 3.0], 0.0).unwrap() - 2.0).abs() < EPS);
93 }
94
95 #[test]
96 fn winsorized_mean_pulls_in_tails() {
97 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 1000.0];
98 let wm = winsorized_mean(&data, 0.1).unwrap();
99 assert!(wm < 10.0, "winsorized mean {wm}");
101 }
102
103 #[test]
104 fn mad_and_iqr_measure_robust_spread() {
105 let data: Vec<f64> = (0..=8).map(|i| i as f64).collect();
107 assert!((median_abs_deviation(&data, false).unwrap() - 2.0).abs() < EPS);
108 assert!((median_abs_deviation(&data, true).unwrap() - 2.965204).abs() < 1e-5);
110 assert!((iqr(&data).unwrap() - 4.0).abs() < EPS);
112 }
113
114 #[test]
115 fn robust_resists_a_single_contaminant() {
116 let clean: Vec<f64> = (1..=20).map(|i| i as f64).collect();
117 let mut dirty = clean.clone();
118 dirty[0] = 1e6;
119 let m_clean = median_abs_deviation(&clean, false).unwrap();
121 let m_dirty = median_abs_deviation(&dirty, false).unwrap();
122 assert!((m_clean - m_dirty).abs() < 2.0);
123 }
124
125 #[test]
126 fn guards() {
127 assert_eq!(trimmed_mean(&[], 0.1), None);
128 assert_eq!(trimmed_mean(&[1.0], 0.6), None);
129 assert_eq!(iqr(&[]), None);
130 assert_eq!(median_abs_deviation(&[], false), None);
131 }
132}