Skip to main content

use_variance/
lib.rs

1//! Variance helpers for `f64` slices.
2//!
3//! This crate exposes population and sample variance calculations together with a
4//! reusable sum-of-squared-deviations helper.
5//!
6//! # Examples
7//!
8//! ```rust
9//! use use_variance::{population_variance, sample_variance};
10//!
11//! let values = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
12//! assert_eq!(population_variance(&values).unwrap(), 4.0);
13//! assert!((sample_variance(&values).unwrap() - (32.0 / 7.0)).abs() < 1.0e-10);
14//! ```
15
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum VarianceError {
18    EmptyInput,
19    InsufficientData,
20}
21
22pub fn sum_squared_deviations(values: &[f64]) -> Result<f64, VarianceError> {
23    if values.is_empty() {
24        return Err(VarianceError::EmptyInput);
25    }
26
27    let mean = mean(values);
28    Ok(values.iter().map(|value| (value - mean).powi(2)).sum())
29}
30
31pub fn population_variance(values: &[f64]) -> Result<f64, VarianceError> {
32    let squared_deviation_sum = sum_squared_deviations(values)?;
33    Ok(squared_deviation_sum / values.len() as f64)
34}
35
36pub fn sample_variance(values: &[f64]) -> Result<f64, VarianceError> {
37    if values.len() < 2 {
38        return Err(VarianceError::InsufficientData);
39    }
40
41    let squared_deviation_sum = sum_squared_deviations(values)?;
42    Ok(squared_deviation_sum / (values.len() as f64 - 1.0))
43}
44
45fn mean(values: &[f64]) -> f64 {
46    values.iter().sum::<f64>() / values.len() as f64
47}
48
49#[cfg(test)]
50mod tests {
51    use super::{population_variance, sample_variance, sum_squared_deviations, VarianceError};
52
53    fn approx_eq(left: f64, right: f64) {
54        assert!((left - right).abs() < 1.0e-10, "left={left}, right={right}");
55    }
56
57    #[test]
58    fn computes_population_and_sample_variance() {
59        let values = [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
60
61        approx_eq(sum_squared_deviations(&values).unwrap(), 32.0);
62        approx_eq(population_variance(&values).unwrap(), 4.0);
63        approx_eq(sample_variance(&values).unwrap(), 32.0 / 7.0);
64    }
65
66    #[test]
67    fn rejects_empty_input() {
68        assert_eq!(sum_squared_deviations(&[]), Err(VarianceError::EmptyInput));
69        assert_eq!(population_variance(&[]), Err(VarianceError::EmptyInput));
70    }
71
72    #[test]
73    fn handles_single_value_population_variance() {
74        approx_eq(population_variance(&[5.0]).unwrap(), 0.0);
75        assert_eq!(
76            sample_variance(&[5.0]),
77            Err(VarianceError::InsufficientData)
78        );
79    }
80}