use crate::error::{StatsError, StatsResult};
use num_traits::ToPrimitive;
use std::fmt::Debug;
#[inline]
pub fn variance_population<T>(data: &[T]) -> StatsResult<f64>
where
T: ToPrimitive + Debug,
{
let (n, _, m2) = welford_pass(data, "prob::variance_population")?;
Ok(m2 / n)
}
#[inline]
pub fn variance_sample<T>(data: &[T]) -> StatsResult<f64>
where
T: ToPrimitive + Debug,
{
let (n, _, m2) = welford_pass(data, "prob::variance_sample")?;
if n < 2.0 {
return Err(StatsError::invalid_input(
"prob::variance_sample: need at least 2 observations for sample variance",
));
}
Ok(m2 / (n - 1.0))
}
#[inline]
pub fn variance<T>(data: &[T]) -> StatsResult<f64>
where
T: ToPrimitive + Debug,
{
variance_population(data)
}
#[inline]
fn welford_pass<T>(data: &[T], ctx: &str) -> StatsResult<(f64, f64, f64)>
where
T: ToPrimitive + Debug,
{
if data.is_empty() {
return Err(StatsError::empty_data(format!(
"{ctx}: cannot compute variance of empty dataset"
)));
}
let mut n = 0.0_f64;
let mut mean = 0.0_f64;
let mut m2 = 0.0_f64;
for (i, x) in data.iter().enumerate() {
let value = x.to_f64().ok_or_else(|| {
StatsError::conversion_error(format!(
"{ctx}: failed to convert value at index {i} to f64"
))
})?;
n += 1.0;
let delta = value - mean;
mean += delta / n;
m2 += delta * (value - mean);
}
Ok((n, mean, m2))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_population_matches_legacy_alias() {
let data = [1.0, 2.5, 3.0, 4.5, 5.0];
assert_eq!(
variance(&data).unwrap(),
variance_population(&data).unwrap()
);
}
#[test]
fn test_population_known_value() {
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
assert!((variance_population(&data).unwrap() - 2.0).abs() < 1e-12);
}
#[test]
fn test_sample_known_value() {
let data = [1.0, 2.0, 3.0, 4.0, 5.0];
assert!((variance_sample(&data).unwrap() - 2.5).abs() < 1e-12);
}
#[test]
fn test_population_single_value_is_zero() {
let data = [5];
assert_eq!(variance_population(&data).unwrap(), 0.0);
}
#[test]
fn test_sample_single_value_errors() {
let data = [5];
assert!(matches!(
variance_sample(&data),
Err(StatsError::InvalidInput { .. })
));
}
#[test]
fn test_empty_errors() {
let data: &[f64] = &[];
assert!(matches!(variance(data), Err(StatsError::EmptyData { .. })));
assert!(matches!(
variance_sample(data),
Err(StatsError::EmptyData { .. })
));
}
#[test]
fn test_identical_values_zero_variance() {
let data = [2.0; 10];
assert_eq!(variance_population(&data).unwrap(), 0.0);
assert_eq!(variance_sample(&data).unwrap(), 0.0);
}
}