1#[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}