use crate::foundation::{AlgoError, Result};
use statrs::statistics::Statistics;
pub fn mean(data: &[f64]) -> Result<f64> {
non_empty(data)?;
Ok(data.iter().mean())
}
pub fn variance(data: &[f64]) -> Result<f64> {
non_empty(data)?;
if data.len() == 1 {
return Ok(0.0);
}
Ok(data.iter().variance())
}
pub fn std_dev(data: &[f64]) -> Result<f64> {
Ok(variance(data)?.sqrt())
}
pub fn percentile(data: &[f64], p: f64) -> Result<f64> {
non_empty(data)?;
if !(0.0..=100.0).contains(&p) {
return Err(AlgoError::InvalidArgument(
"percentile: p must be in [0, 100]".to_string(),
));
}
let mut sorted = data.to_vec();
sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let n = sorted.len();
if n == 1 {
return Ok(sorted[0]);
}
let rank = (p / 100.0) * (n - 1) as f64;
let lo = rank.floor() as usize;
if lo + 1 >= n {
return Ok(sorted[n - 1]); }
let frac = rank - lo as f64;
Ok(sorted[lo] + frac * (sorted[lo + 1] - sorted[lo]))
}
pub fn median(data: &[f64]) -> Result<f64> {
percentile(data, 50.0)
}
fn non_empty(data: &[f64]) -> Result<()> {
if data.is_empty() {
return Err(AlgoError::EmptyInput("stats: no data"));
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use approx::assert_relative_eq;
#[test]
fn empty_errors() {
assert!(matches!(mean(&[]), Err(AlgoError::EmptyInput(_))));
assert!(matches!(variance(&[]), Err(AlgoError::EmptyInput(_))));
assert!(matches!(
percentile(&[], 50.0),
Err(AlgoError::EmptyInput(_))
));
}
#[test]
fn mean_variance_std_known_values() {
let d = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
assert_relative_eq!(mean(&d).unwrap(), 5.0, epsilon = 1e-12);
assert_relative_eq!(variance(&d).unwrap(), 32.0 / 7.0, epsilon = 1e-12);
assert_relative_eq!(
std_dev(&d).unwrap(),
(32.0f64 / 7.0).sqrt(),
epsilon = 1e-12
);
}
#[test]
fn single_value_has_zero_spread() {
assert_eq!(variance(&[3.0]).unwrap(), 0.0);
assert_eq!(std_dev(&[3.0]).unwrap(), 0.0);
assert_eq!(mean(&[3.0]).unwrap(), 3.0);
}
#[test]
fn percentile_endpoints_and_median() {
let d = [1.0, 2.0, 3.0, 4.0, 5.0];
assert_relative_eq!(percentile(&d, 0.0).unwrap(), 1.0, epsilon = 1e-12);
assert_relative_eq!(percentile(&d, 100.0).unwrap(), 5.0, epsilon = 1e-12);
assert_relative_eq!(median(&d).unwrap(), 3.0, epsilon = 1e-12);
}
#[test]
fn percentile_is_true_type7_excel_parity() {
let d = [1.0, 2.0, 3.0, 4.0, 5.0];
assert_relative_eq!(percentile(&d, 25.0).unwrap(), 2.0, epsilon = 1e-12);
assert_relative_eq!(percentile(&d, 50.0).unwrap(), 3.0, epsilon = 1e-12);
assert_relative_eq!(percentile(&d, 75.0).unwrap(), 4.0, epsilon = 1e-12);
let e = [1.0, 2.0, 3.0, 4.0];
assert_relative_eq!(percentile(&e, 25.0).unwrap(), 1.75, epsilon = 1e-12);
let f = [5.0, 1.0, 3.0, 2.0, 4.0];
assert_relative_eq!(percentile(&f, 25.0).unwrap(), 2.0, epsilon = 1e-12);
}
#[test]
fn percentile_rejects_out_of_range() {
assert!(percentile(&[1.0, 2.0], -1.0).is_err());
assert!(percentile(&[1.0, 2.0], 101.0).is_err());
}
#[test]
fn percentile_is_monotone_in_p() {
let d = [10.0, 3.0, 7.0, 1.0, 9.0, 4.0];
let mut prev = f64::NEG_INFINITY;
for p in [0.0, 10.0, 25.0, 50.0, 75.0, 90.0, 100.0] {
let q = percentile(&d, p).unwrap();
assert!(q >= prev - 1e-12, "not monotone at p={p}: {q} < {prev}");
prev = q;
}
}
}