1#[derive(Debug, Clone, Copy, PartialEq)]
17pub struct Summary {
18 pub mean: f64,
19 pub median: f64,
20 pub trimmed_mean: f64,
21 pub stddev: f64,
22 pub cv: f64,
24 pub mad: f64,
26 pub iqr: f64,
28 pub min: f64,
29 pub max: f64,
30 pub p50: f64,
31 pub p95: f64,
32 pub p99: f64,
33}
34
35impl Default for Summary {
36 fn default() -> Self {
37 Self {
38 mean: 0.0,
39 median: 0.0,
40 trimmed_mean: 0.0,
41 stddev: 0.0,
42 cv: 0.0,
43 mad: 0.0,
44 iqr: 0.0,
45 min: 0.0,
46 max: 0.0,
47 p50: 0.0,
48 p95: 0.0,
49 p99: 0.0,
50 }
51 }
52}
53
54#[derive(Debug, Clone, Copy)]
56pub struct MwResult {
57 pub u: f64,
58 pub z: f64,
59 pub p: f64,
60}
61
62#[derive(Debug, Clone, Copy)]
64pub struct WelchResult {
65 pub t: f64,
66 pub df: f64,
67 pub p: f64,
68}
69
70pub fn mean(xs: &[f64]) -> f64 {
72 if xs.is_empty() {
73 return 0.0;
74 }
75 let sum: f64 = xs.iter().sum();
76 sum / xs.len() as f64
77}
78
79pub fn variance(xs: &[f64], sample_mean: f64) -> f64 {
81 if xs.len() < 2 {
82 return 0.0;
83 }
84 let mut sum_sq = 0.0;
85 for &x in xs {
86 let d = x - sample_mean;
87 sum_sq += d * d;
88 }
89 sum_sq / (xs.len() - 1) as f64
90}
91
92pub fn stddev(xs: &[f64]) -> f64 {
94 variance(xs, mean(xs)).sqrt()
95}
96
97pub fn coefficient_of_variation(xs: &[f64]) -> f64 {
99 let m = mean(xs);
100 if m == 0.0 {
101 0.0
102 } else {
103 variance(xs, m).sqrt() / m
104 }
105}
106
107fn sorted_copy(xs: &[f64]) -> Vec<f64> {
109 let mut v: Vec<f64> = xs.to_vec();
110 v.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
111 v
112}
113
114fn percentile_sorted(sorted: &[f64], p: f64) -> f64 {
116 if sorted.is_empty() {
117 return 0.0;
118 }
119 let rank = (p / 100.0) * (sorted.len() - 1) as f64;
120 let lo = rank.floor() as usize;
121 let hi = rank.ceil() as usize;
122 if lo == hi {
123 return sorted[lo];
124 }
125 let frac = rank - lo as f64;
126 sorted[lo] * (1.0 - frac) + sorted[hi] * frac
127}
128
129pub fn percentile(xs: &[f64], p: f64) -> f64 {
131 if xs.is_empty() {
132 return 0.0;
133 }
134 percentile_sorted(&sorted_copy(xs), p)
135}
136
137pub fn median(xs: &[f64]) -> f64 {
139 percentile(xs, 50.0)
140}
141
142pub fn trimmed_mean(xs: &[f64], trim: f64) -> f64 {
144 if xs.is_empty() {
145 return 0.0;
146 }
147 let sorted = sorted_copy(xs);
148 let cut = (sorted.len() as f64 * trim).floor() as usize;
149 let slice = if cut > 0 {
150 &sorted[cut..sorted.len() - cut]
151 } else {
152 &sorted[..]
153 };
154 mean(slice)
155}
156
157pub fn median_absolute_deviation(xs: &[f64]) -> f64 {
159 if xs.is_empty() {
160 return 0.0;
161 }
162 let sorted = sorted_copy(xs);
163 let med = percentile_sorted(&sorted, 50.0);
164 mad_from_sorted(&sorted, med)
165}
166
167fn mad_from_sorted(sorted: &[f64], med: f64) -> f64 {
171 let n = sorted.len();
172 if n == 0 {
173 return 0.0;
174 }
175
176 let mut left = 0usize;
178 while left < n && sorted[left] < med {
179 left += 1;
180 }
181 let mut right: isize = n as isize - 1;
182 while right >= 0 && sorted[right as usize] > med {
183 right -= 1;
184 }
185 let eq_count = (right as i64 - left as i64 + 1).max(0) as usize;
186
187 let mut merged: Vec<f64> = vec![0.0; eq_count];
188 merged.reserve(n - eq_count);
189
190 let mut li: isize = left as isize - 1;
193 let mut ri: usize = (right + 1).max(0) as usize;
194 while li >= 0 && ri < n {
195 let lv = med - sorted[li as usize];
196 let rv = sorted[ri] - med;
197 if lv <= rv {
198 merged.push(lv);
199 li -= 1;
200 } else {
201 merged.push(rv);
202 ri += 1;
203 }
204 }
205 while li >= 0 {
206 merged.push(med - sorted[li as usize]);
207 li -= 1;
208 }
209 while ri < n {
210 merged.push(sorted[ri] - med);
211 ri += 1;
212 }
213
214 percentile_sorted(&merged, 50.0)
215}
216
217pub fn interquartile_range(xs: &[f64]) -> f64 {
219 if xs.is_empty() {
220 return 0.0;
221 }
222 let sorted = sorted_copy(xs);
223 percentile_sorted(&sorted, 75.0) - percentile_sorted(&sorted, 25.0)
224}
225
226pub fn summarize_samples(samples: &[f64]) -> Summary {
230 let n = samples.len();
231 if n == 0 {
232 return Summary::default();
233 }
234
235 let mut mean_ = 0.0_f64;
237 let mut m2 = 0.0_f64;
238 let mut min = samples[0];
239 let mut max = samples[0];
240 for (i, &x) in samples.iter().enumerate() {
241 if x < min {
242 min = x;
243 }
244 if x > max {
245 max = x;
246 }
247 let delta = x - mean_;
248 mean_ += delta / (i + 1) as f64;
249 let delta2 = x - mean_;
250 m2 += delta * delta2;
251 }
252 let variance_ = if n < 2 { 0.0 } else { m2 / (n - 1) as f64 };
253 let stddev_ = variance_.sqrt();
254 let cv_ = if mean_ == 0.0 { 0.0 } else { stddev_ / mean_ };
255
256 let sorted = sorted_copy(samples);
257 let median_ = percentile_sorted(&sorted, 50.0);
258 let p95_ = percentile_sorted(&sorted, 95.0);
259 let p99_ = percentile_sorted(&sorted, 99.0);
260 let p25_ = percentile_sorted(&sorted, 25.0);
261 let p75_ = percentile_sorted(&sorted, 75.0);
262 let iqr_ = p75_ - p25_;
263
264 let cut = (n as f64 * 0.05).floor() as usize;
266 let trimmed_slice = if cut > 0 && 2 * cut < n {
267 &sorted[cut..n - cut]
268 } else {
269 &sorted[..]
270 };
271 let trimmed_mean_ = mean(trimmed_slice);
272
273 let mad_ = mad_from_sorted(&sorted, median_);
274
275 Summary {
276 mean: mean_,
277 median: median_,
278 trimmed_mean: trimmed_mean_,
279 stddev: stddev_,
280 cv: cv_,
281 mad: mad_,
282 iqr: iqr_,
283 min,
284 max,
285 p50: median_,
286 p95: p95_,
287 p99: p99_,
288 }
289}
290
291pub fn welch_t(a: &[f64], b: &[f64]) -> WelchResult {
297 if a.len() < 2 || b.len() < 2 {
298 return WelchResult {
299 t: 0.0,
300 df: 0.0,
301 p: 1.0,
302 };
303 }
304 let ma = mean(a);
305 let mb = mean(b);
306 let va = variance(a, ma);
307 let vb = variance(b, mb);
308 let na = a.len() as f64;
309 let nb = b.len() as f64;
310 let se_sq = va / na + vb / nb;
311 if se_sq == 0.0 {
312 return WelchResult {
313 t: 0.0,
314 df: na + nb - 2.0,
315 p: 1.0,
316 };
317 }
318 let t = (ma - mb) / se_sq.sqrt();
319 let df =
320 (se_sq * se_sq) / ((va * va) / (na * na * (na - 1.0)) + (vb * vb) / (nb * nb * (nb - 1.0)));
321 let p = 2.0 * (1.0 - student_t_cdf(t.abs(), df));
322 WelchResult {
323 t,
324 df,
325 p: p.clamp(0.0, 1.0),
326 }
327}
328
329pub fn mann_whitney_u(a: &[f64], b: &[f64]) -> MwResult {
337 let n_a = a.len();
338 let n_b = b.len();
339 if n_a == 0 || n_b == 0 {
340 return MwResult {
341 u: 0.0,
342 z: 0.0,
343 p: 1.0,
344 };
345 }
346
347 let sorted_a = sorted_copy(a);
348 let sorted_b = sorted_copy(b);
349
350 let mut i = 0usize;
351 let mut j = 0usize;
352 let mut rank = 1u64;
353 let mut rank_sum_a = 0.0_f64;
354 let mut tie_correction = 0.0_f64;
355
356 while i < n_a || j < n_b {
357 let val = if j >= n_b || (i < n_a && sorted_a[i] <= sorted_b[j]) {
359 sorted_a[i]
360 } else {
361 sorted_b[j]
362 };
363 let start_a = i;
364 let start_b = j;
365 while i < n_a && sorted_a[i] == val {
366 i += 1;
367 }
368 while j < n_b && sorted_b[j] == val {
369 j += 1;
370 }
371 let count_a = (i - start_a) as u64;
372 let count_b = (j - start_b) as u64;
373 let total = count_a + count_b;
374 let end_rank = rank + total - 1;
375 let avg_rank = (rank + end_rank) as f64 / 2.0;
376 rank_sum_a += count_a as f64 * avg_rank;
377 if total > 1 {
378 let t = total as f64;
379 tie_correction += t * t * t - t;
380 }
381 rank = end_rank + 1;
382 }
383
384 let n_a_f = n_a as f64;
385 let n_b_f = n_b as f64;
386 let u_a = rank_sum_a - (n_a_f * (n_a_f + 1.0)) / 2.0;
387 let u_b = n_a_f * n_b_f - u_a;
388 let u = u_a.min(u_b);
389
390 let n_total = n_a_f + n_b_f;
391 let mean_u = (n_a_f * n_b_f) / 2.0;
392 let var_u =
393 ((n_a_f * n_b_f) / 12.0) * (n_total + 1.0 - tie_correction / (n_total * (n_total - 1.0)));
394 if var_u <= 0.0 {
395 return MwResult { u, z: 0.0, p: 1.0 };
396 }
397
398 let diff = ((u - mean_u).abs() - 0.5).max(0.0);
400 let z = diff / var_u.sqrt();
401 let p = 2.0 * standard_normal_cdf(-z);
402 MwResult {
403 u,
404 z,
405 p: p.clamp(0.0, 1.0),
406 }
407}
408
409fn standard_normal_cdf(x: f64) -> f64 {
412 0.5 * (1.0 + erf(x / std::f64::consts::SQRT_2))
413}
414
415fn erf(x: f64) -> f64 {
418 let sign = if x < 0.0 { -1.0 } else { 1.0 };
419 let a = x.abs();
420 let t = 1.0 / (1.0 + 0.3275911 * a);
421 let poly = ((((1.061405429 * t - 1.453152027) * t + 1.421413741) * t - 0.284496736) * t
422 + 0.254829592)
423 * t;
424 sign * (1.0 - poly * (-a * a).exp())
425}
426
427fn student_t_cdf(x: f64, df: f64) -> f64 {
428 if df <= 0.0 {
429 return 0.5;
430 }
431 let xt = df / (df + x * x);
432 let ib = regularized_incomplete_beta(xt, df / 2.0, 0.5);
433 if x >= 0.0 {
434 1.0 - 0.5 * ib
435 } else {
436 0.5 * ib
437 }
438}
439
440fn regularized_incomplete_beta(x: f64, a: f64, b: f64) -> f64 {
441 if x <= 0.0 {
442 return 0.0;
443 }
444 if x >= 1.0 {
445 return 1.0;
446 }
447 let ln_beta = ln_gamma(a) + ln_gamma(b) - ln_gamma(a + b);
448 let front = (x.ln() * a + (1.0 - x).ln() * b - ln_beta).exp() / a;
449 front * beta_continued_fraction(x, a, b)
450}
451
452fn beta_continued_fraction(x: f64, a: f64, b: f64) -> f64 {
453 let max_iter = 200;
454 let epsilon = 1e-15;
455 let fpmin = 1e-300;
456 let qab = a + b;
457 let qap = a + 1.0;
458 let qam = a - 1.0;
459 let mut c = 1.0;
460 let mut d = 1.0 - (qab * x) / qap;
461 if d.abs() < fpmin {
462 d = fpmin;
463 }
464 d = 1.0 / d;
465 let mut h = d;
466 for m in 1..=max_iter {
467 let m_f = m as f64;
468 let m2 = 2.0 * m_f;
469 let mut aa = (m_f * (b - m_f) * x) / ((qam + m2) * (a + m2));
470 d = 1.0 + aa * d;
471 if d.abs() < fpmin {
472 d = fpmin;
473 }
474 c = 1.0 + aa / c;
475 if c.abs() < fpmin {
476 c = fpmin;
477 }
478 d = 1.0 / d;
479 h *= d * c;
480 aa = (-(a + m_f) * (qab + m_f) * x) / ((a + m2) * (qap + m2));
481 d = 1.0 + aa * d;
482 if d.abs() < fpmin {
483 d = fpmin;
484 }
485 c = 1.0 + aa / c;
486 if c.abs() < fpmin {
487 c = fpmin;
488 }
489 d = 1.0 / d;
490 let del = d * c;
491 h *= del;
492 if (del - 1.0).abs() < epsilon {
493 break;
494 }
495 }
496 h
497}
498
499fn ln_gamma(z: f64) -> f64 {
501 const G: usize = 7;
502 const C: [f64; 9] = [
503 0.999_999_999_999_809_9,
504 676.520_368_121_885_1,
505 -1_259.139_216_722_402_8,
506 771.323_428_777_653_1,
507 -176.615_029_162_140_6,
508 12.507_343_278_686_905,
509 -0.138_571_095_265_720_1,
510 9.984_369_578_019_572e-6,
511 1.505_632_735_149_311_6e-7,
512 ];
513 if z < 0.5 {
514 return (std::f64::consts::PI / (std::f64::consts::PI * z).sin()).ln() - ln_gamma(1.0 - z);
515 }
516 let z = z - 1.0;
517 let mut x = C[0];
518 for (i, coef) in C.iter().enumerate().skip(1) {
519 x += coef / (z + i as f64);
520 }
521 let t = z + G as f64 + 0.5;
522 0.5 * (2.0 * std::f64::consts::PI).ln() + (z + 0.5) * t.ln() - t + x.ln()
523}