Skip to main content

use_z_score/
lib.rs

1//! Z-score helpers for `f64` slices.
2//!
3//! Normalization uses the population standard deviation of the provided sample.
4//!
5//! # Examples
6//!
7//! ```rust
8//! use use_z_score::{normalize, z_score};
9//!
10//! assert_eq!(z_score(80.0, 70.0, 5.0).unwrap(), 2.0);
11//! assert_eq!(normalize(&[1.0, 2.0, 3.0]).unwrap().len(), 3);
12//! ```
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub enum ZScoreError {
16    EmptyInput,
17    ZeroStandardDeviation,
18}
19
20pub fn z_score(value: f64, mean: f64, standard_deviation: f64) -> Result<f64, ZScoreError> {
21    if standard_deviation == 0.0 {
22        return Err(ZScoreError::ZeroStandardDeviation);
23    }
24
25    Ok((value - mean) / standard_deviation)
26}
27
28pub fn normalize(values: &[f64]) -> Result<Vec<f64>, ZScoreError> {
29    if values.is_empty() {
30        return Err(ZScoreError::EmptyInput);
31    }
32
33    let mean = values.iter().sum::<f64>() / values.len() as f64;
34    let variance = values
35        .iter()
36        .map(|value| (value - mean).powi(2))
37        .sum::<f64>()
38        / values.len() as f64;
39    let standard_deviation = variance.sqrt();
40
41    if standard_deviation == 0.0 {
42        return Err(ZScoreError::ZeroStandardDeviation);
43    }
44
45    values
46        .iter()
47        .map(|value| z_score(*value, mean, standard_deviation))
48        .collect::<Result<Vec<_>, _>>()
49}
50
51#[cfg(test)]
52mod tests {
53    use super::{normalize, z_score, ZScoreError};
54
55    fn approx_eq(left: f64, right: f64) {
56        assert!((left - right).abs() < 1.0e-10, "left={left}, right={right}");
57    }
58
59    #[test]
60    fn computes_single_z_score() {
61        approx_eq(z_score(80.0, 70.0, 5.0).unwrap(), 2.0);
62    }
63
64    #[test]
65    fn normalizes_known_values() {
66        let normalized = normalize(&[2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0]).unwrap();
67        let expected = [-1.5, -0.5, -0.5, -0.5, 0.0, 0.0, 1.0, 2.0];
68
69        for (left, right) in normalized.iter().zip(expected) {
70            approx_eq(*left, right);
71        }
72    }
73
74    #[test]
75    fn rejects_invalid_inputs() {
76        assert_eq!(normalize(&[]), Err(ZScoreError::EmptyInput));
77        assert_eq!(
78            normalize(&[3.0, 3.0, 3.0]),
79            Err(ZScoreError::ZeroStandardDeviation)
80        );
81        assert_eq!(
82            z_score(10.0, 10.0, 0.0),
83            Err(ZScoreError::ZeroStandardDeviation)
84        );
85    }
86
87    #[test]
88    fn rejects_single_value_normalization() {
89        assert_eq!(normalize(&[5.0]), Err(ZScoreError::ZeroStandardDeviation));
90    }
91}