qualia_core_db/solvers/statistics/
descriptive.rs1#[inline]
16pub fn sum(values: &[f64]) -> f64 {
17 let mut acc = 0.0;
18 let mut i = 0;
19 while i < values.len() {
20 acc += values[i];
21 i += 1;
22 }
23 acc
24}
25
26#[inline]
28pub fn mean(values: &[f64]) -> Option<f64> {
29 if values.is_empty() {
30 return None;
31 }
32 Some(sum(values) / values.len() as f64)
33}
34
35#[inline]
41pub fn variance(values: &[f64], sample: bool) -> Option<f64> {
42 let m = mean(values)?;
43 let mut ss = 0.0;
44 let mut i = 0;
45 while i < values.len() {
46 let d = values[i] - m;
47 ss += d * d;
48 i += 1;
49 }
50 let denom = if sample {
51 (values.len() - 1) as f64
52 } else {
53 values.len() as f64
54 };
55 Some(ss / denom)
56}
57
58#[inline]
60pub fn std_dev(values: &[f64], sample: bool) -> Option<f64> {
61 variance(values, sample).map(|v| v.sqrt())
62}
63
64#[inline]
67pub fn median_sorted(sorted: &[f64]) -> Option<f64> {
68 let n = sorted.len();
69 if n == 0 {
70 return None;
71 }
72 if n % 2 == 0 {
73 Some((sorted[n / 2 - 1] + sorted[n / 2]) / 2.0)
74 } else {
75 Some(sorted[n / 2])
76 }
77}
78
79#[inline]
81pub fn median_in_place(values: &mut [f64]) -> Option<f64> {
82 if values.is_empty() {
83 return None;
84 }
85 values.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
86 median_sorted(values)
87}
88
89#[inline]
91pub fn min(values: &[f64]) -> Option<f64> {
92 values
93 .iter()
94 .copied()
95 .reduce(|a, b| if b < a { b } else { a })
96}
97
98#[inline]
100pub fn max(values: &[f64]) -> Option<f64> {
101 values
102 .iter()
103 .copied()
104 .reduce(|a, b| if b > a { b } else { a })
105}
106
107pub fn argmax(values: &[f64]) -> Option<usize> {
111 if values.is_empty() {
112 return None;
113 }
114 let mut best = 0;
115 for i in 1..values.len() {
116 if values[i] > values[best] {
117 best = i;
118 }
119 }
120 Some(best)
121}
122
123pub fn covariance(x: &[f64], y: &[f64], sample: bool) -> Option<f64> {
126 let n = x.len();
127 if n != y.len() || n == 0 {
128 return None;
129 }
130 let mx = mean(x)?;
131 let my = mean(y)?;
132 let mut acc = 0.0;
133 let mut i = 0;
134 while i < n {
135 acc += (x[i] - mx) * (y[i] - my);
136 i += 1;
137 }
138 let denom = if sample { (n - 1) as f64 } else { n as f64 };
139 Some(acc / denom)
140}
141
142#[inline]
144fn central_moment(values: &[f64], k: i32) -> Option<f64> {
145 let m = mean(values)?;
146 let mut acc = 0.0;
147 for &v in values {
148 acc += (v - m).powi(k);
149 }
150 Some(acc / values.len() as f64)
151}
152
153pub fn skewness(values: &[f64]) -> Option<f64> {
156 let m2 = central_moment(values, 2)?;
157 let m3 = central_moment(values, 3)?;
158 if m2 <= 0.0 {
159 return Some(0.0);
160 }
161 Some(m3 / m2.powf(1.5))
162}
163
164pub fn kurtosis(values: &[f64]) -> Option<f64> {
167 let m2 = central_moment(values, 2)?;
168 let m4 = central_moment(values, 4)?;
169 if m2 <= 0.0 {
170 return Some(0.0);
171 }
172 Some(m4 / (m2 * m2) - 3.0)
173}
174
175pub fn quantile_sorted(sorted: &[f64], q: f64) -> Option<f64> {
178 let n = sorted.len();
179 if n == 0 {
180 return None;
181 }
182 if n == 1 {
183 return Some(sorted[0]);
184 }
185 let q = q.clamp(0.0, 1.0);
186 let pos = q * (n - 1) as f64;
187 let lo = pos.floor() as usize;
188 let hi = pos.ceil() as usize;
189 let frac = pos - lo as f64;
190 Some(sorted[lo] + (sorted[hi] - sorted[lo]) * frac)
191}
192
193pub fn quantile_in_place(values: &mut [f64], q: f64) -> Option<f64> {
195 if values.is_empty() {
196 return None;
197 }
198 values.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
199 quantile_sorted(values, q)
200}
201
202pub fn mode_in_place(values: &mut [f64]) -> Option<(f64, usize)> {
214 if values.is_empty() {
215 return None;
216 }
217 values.sort_unstable_by(|a, b| a.partial_cmp(b).unwrap_or(core::cmp::Ordering::Equal));
218 let mut best_val = values[0];
219 let mut best_count = 1usize;
220 let mut cur_val = values[0];
221 let mut cur_count = 1usize;
222 let mut i = 1;
223 while i < values.len() {
224 if values[i] == cur_val {
225 cur_count += 1;
226 } else {
227 cur_val = values[i];
228 cur_count = 1;
229 }
230 if cur_count > best_count {
231 best_count = cur_count;
232 best_val = cur_val;
233 }
234 i += 1;
235 }
236 Some((best_val, best_count))
237}
238
239#[cfg(test)]
240mod tests {
241 use super::*;
242
243 const EPS: f64 = 1e-12;
244
245 #[test]
246 fn mode_picks_most_frequent() {
247 let mut v = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0];
248 assert_eq!(mode_in_place(&mut v), Some((3.0, 3)));
249 }
250
251 #[test]
252 fn mode_tie_picks_smallest() {
253 let mut v = [5.0, 5.0, 1.0, 1.0, 9.0];
254 assert_eq!(mode_in_place(&mut v), Some((1.0, 2)));
255 }
256
257 #[test]
258 fn mode_empty_none() {
259 let mut v: [f64; 0] = [];
260 assert_eq!(mode_in_place(&mut v), None);
261 }
262
263 #[test]
264 fn empty_returns_none() {
265 let empty: [f64; 0] = [];
266 assert_eq!(mean(&empty), None);
267 assert_eq!(variance(&empty, true), None);
268 assert_eq!(std_dev(&empty, false), None);
269 assert_eq!(median_sorted(&empty), None);
270 assert_eq!(min(&empty), None);
271 assert_eq!(max(&empty), None);
272 assert_eq!(argmax(&empty), None);
273 }
274
275 #[test]
276 fn argmax_selects_highest_index() {
277 assert_eq!(argmax(&[0.1, 0.7, 0.2]), Some(1));
278 assert_eq!(argmax(&[3.0, 3.0, 1.0]), Some(0)); assert_eq!(argmax(&[-5.0, -2.0, -9.0]), Some(1));
280 let _ = EPS;
281 }
282
283 #[test]
284 fn mean_matches_inline_formula() {
285 let v = [1.0, 2.0, 3.0, 4.0];
286 assert!((mean(&v).unwrap() - 2.5).abs() < EPS);
287 assert!((sum(&v) - 10.0).abs() < EPS);
288 }
289
290 #[test]
291 fn variance_sample_and_population() {
292 let v = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
293 assert!((variance(&v, false).unwrap() - 4.0).abs() < EPS);
295 assert!((variance(&v, true).unwrap() - (32.0 / 7.0)).abs() < EPS);
296 assert!((std_dev(&v, false).unwrap() - 2.0).abs() < EPS);
297 }
298
299 #[test]
300 fn sample_variance_of_one_is_nan_like_legacy() {
301 let v = [42.0];
302 assert!(variance(&v, true).unwrap().is_nan());
303 assert!((variance(&v, false).unwrap() - 0.0).abs() < EPS);
304 }
305
306 #[test]
307 fn median_odd_even_and_in_place() {
308 assert!((median_sorted(&[1.0, 2.0, 3.0]).unwrap() - 2.0).abs() < EPS);
309 assert!((median_sorted(&[1.0, 2.0, 3.0, 4.0]).unwrap() - 2.5).abs() < EPS);
310 let mut unsorted = [3.0, 1.0, 4.0, 1.0, 5.0];
311 assert!((median_in_place(&mut unsorted).unwrap() - 3.0).abs() < EPS);
312 }
313
314 #[test]
315 fn min_max() {
316 let v = [3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0];
317 assert!((min(&v).unwrap() - 1.0).abs() < EPS);
318 assert!((max(&v).unwrap() - 9.0).abs() < EPS);
319 }
320
321 #[test]
322 fn covariance_matches_definition() {
323 let x = [1.0, 2.0, 3.0, 4.0];
324 let y = [2.0, 4.0, 6.0, 8.0]; let cov = covariance(&x, &y, true).unwrap();
326 let vx = variance(&x, true).unwrap();
327 assert!((cov - 2.0 * vx).abs() < 1e-9);
328 assert!((covariance(&x, &x, true).unwrap() - vx).abs() < 1e-9);
330 assert_eq!(covariance(&x, &[1.0], true), None);
331 }
332
333 #[test]
334 fn skewness_sign_and_symmetry() {
335 assert!(skewness(&[1.0, 2.0, 3.0, 4.0, 5.0]).unwrap().abs() < 1e-9);
337 assert!(skewness(&[1.0, 1.0, 1.0, 2.0, 10.0]).unwrap() > 0.0);
339 assert_eq!(skewness(&[7.0, 7.0, 7.0]), Some(0.0));
341 }
342
343 #[test]
344 fn kurtosis_excess() {
345 assert!(kurtosis(&[1.0, 2.0, 3.0, 4.0, 5.0]).unwrap() < 0.0);
347 assert_eq!(kurtosis(&[3.0, 3.0]), Some(0.0));
348 }
349
350 #[test]
351 fn quantile_interpolates() {
352 let sorted = [1.0, 2.0, 3.0, 4.0]; assert!((quantile_sorted(&sorted, 0.0).unwrap() - 1.0).abs() < EPS);
354 assert!((quantile_sorted(&sorted, 1.0).unwrap() - 4.0).abs() < EPS);
355 assert!((quantile_sorted(&sorted, 0.5).unwrap() - 2.5).abs() < EPS);
357 assert!((quantile_sorted(&sorted, 0.25).unwrap() - 1.75).abs() < EPS);
359 let mut unsorted = [4.0, 1.0, 3.0, 2.0];
360 assert!((quantile_in_place(&mut unsorted, 0.5).unwrap() - 2.5).abs() < EPS);
361 }
362}