pub(super) fn count_to_f64(n: usize) -> f64 {
let wide = u64::try_from(n).unwrap_or(u64::MAX);
let hi = u32::try_from(wide >> 32).unwrap_or(0);
let lo = u32::try_from(wide & 0xFFFF_FFFF).unwrap_or(0);
f64::from(hi).mul_add(4_294_967_296.0, f64::from(lo))
}
pub(super) fn mean(values: &[f64]) -> f64 {
let n = count_to_f64(values.len());
if n == 0.0 {
return 0.0;
}
values.iter().sum::<f64>() / n
}
pub(super) fn population_std(values: &[f64]) -> f64 {
let n = count_to_f64(values.len());
if n == 0.0 {
return 0.0;
}
let m = mean(values);
let ss: f64 = values
.iter()
.map(|&v| {
let d = v - m;
d * d
})
.sum();
(ss / n).sqrt()
}
pub(super) fn percentile_linear(sorted: &[f64], q: f64) -> f64 {
let n = sorted.len();
if n == 0 {
return 0.0;
}
if n == 1 {
return sorted.first().copied().unwrap_or(0.0);
}
let h = q * (count_to_f64(n) - 1.0);
let lo_idx = floor_to_usize(h);
let frac = h - count_to_f64(lo_idx);
let lo = sorted.get(lo_idx).copied().unwrap_or(0.0);
let hi = sorted.get(lo_idx + 1).copied().unwrap_or(lo);
(hi - lo).mul_add(frac, lo)
}
fn floor_to_usize(x: f64) -> usize {
if x <= 0.0 {
return 0;
}
let mut acc: u32 = 0;
let mut bit: u32 = 1 << 31;
while bit > 0 {
let candidate = acc | bit;
if f64::from(candidate) <= x {
acc = candidate;
}
bit >>= 1;
}
usize::try_from(acc).unwrap_or(0)
}
pub(super) fn median(values: &[f64]) -> f64 {
let mut sorted = values.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
percentile_linear(&sorted, 0.5)
}
pub(super) fn median_absolute_deviation(values: &[f64]) -> f64 {
let med = median(values);
let deviations: Vec<f64> = values.iter().map(|&v| (v - med).abs()).collect();
median(&deviations)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn count_widens_exactly() {
assert!((count_to_f64(4096) - 4096.0).abs() < 1e-12, "widening");
}
#[test]
fn mean_matches_hand_value() {
let m = mean(&[2.0, 4.0, 6.0]);
assert!((m - 4.0).abs() < 1e-12, "mean was {m}");
}
#[test]
fn population_std_uses_divisor_n() {
let s = population_std(&[2.0, 4.0, 6.0]);
assert!((s - 1.632_993_161_855_452_2).abs() < 1e-12, "std was {s}");
}
#[test]
fn percentile_matches_numpy_linear() {
let sorted = [1.0, 2.0, 3.0, 4.0, 100.0];
let q1 = percentile_linear(&sorted, 0.25);
let q3 = percentile_linear(&sorted, 0.75);
assert!((q1 - 2.0).abs() < 1e-12, "q1 was {q1}");
assert!((q3 - 4.0).abs() < 1e-12, "q3 was {q3}");
}
#[test]
fn percentile_interpolates_between_points() {
let sorted = [1.0, 2.0, 3.0, 4.0];
let q1 = percentile_linear(&sorted, 0.25);
assert!((q1 - 1.75).abs() < 1e-12, "q1 was {q1}");
}
#[test]
fn median_of_even_sample() {
let m = median(&[1.0, 2.0, 3.0, 4.0]);
assert!((m - 2.5).abs() < 1e-12, "median was {m}");
}
#[test]
fn mad_matches_hand_value() {
let mad = median_absolute_deviation(&[1.0, 2.0, 3.0, 4.0, 5.0]);
assert!((mad - 1.0).abs() < 1e-12, "mad was {mad}");
}
}