Skip to main content

gamlss_transform/transforms/
standardize.rs

1use crate::{TargetTransform, TransformError, validate_non_empty_finite};
2
3/// Standardization transform: `(y - center) / scale`.
4#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
5pub struct Standardize;
6
7/// State for [`Standardize`].
8#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct StandardizeState {
10    /// Training target mean.
11    pub center: f64,
12    /// Training target root-mean-square deviation.
13    pub scale: f64,
14}
15
16impl TargetTransform for Standardize {
17    type State = StandardizeState;
18
19    #[allow(clippy::cast_precision_loss)]
20    fn fit(y: &[f64]) -> Result<Self::State, TransformError> {
21        validate_non_empty_finite(y)?;
22
23        let mut count = 0.0;
24        let mut center = 0.0;
25        let mut sum_squares = 0.0;
26        for value in y.iter().copied() {
27            count += 1.0;
28            let delta = value - center;
29            center += delta / count;
30            let centered = value - center;
31            sum_squares += delta * centered;
32        }
33
34        let variance = sum_squares / count;
35        let scale = variance.sqrt();
36        if !scale.is_finite() || scale <= 0.0 {
37            return Err(TransformError::ZeroScale);
38        }
39
40        Ok(StandardizeState { center, scale })
41    }
42
43    fn transform(state: &Self::State, y: f64) -> f64 {
44        (y - state.center) / state.scale
45    }
46
47    fn inverse(state: &Self::State, value: f64) -> f64 {
48        value.mul_add(state.scale, state.center)
49    }
50}
51
52#[cfg(test)]
53mod tests {
54    use approx::assert_relative_eq;
55
56    use crate::{Standardize, TargetTransform, TransformError};
57
58    #[test]
59    fn round_trips_values() {
60        let y = [1.0, 2.0, 4.0];
61        let (state, transformed) = Standardize::fit_transform(&y).unwrap();
62        let restored = Standardize::inverse_slice(&state, &transformed).unwrap();
63
64        for (actual, expected) in restored.iter().zip(y) {
65            assert_relative_eq!(*actual, expected);
66        }
67    }
68
69    #[test]
70    fn rejects_empty_and_non_finite_values() {
71        assert_eq!(
72            Standardize::fit(&[]).unwrap_err(),
73            TransformError::EmptyInput
74        );
75        assert_eq!(
76            Standardize::fit(&[1.0, f64::NAN]).unwrap_err(),
77            TransformError::NonFiniteValue
78        );
79    }
80
81    #[test]
82    fn rejects_zero_scale() {
83        assert_eq!(
84            Standardize::fit(&[2.0, 2.0]).unwrap_err(),
85            TransformError::ZeroScale
86        );
87    }
88
89    #[test]
90    fn handles_large_offset_values_stably() {
91        let y = [1.0e12, 1.0e12 + 2.0, 1.0e12 + 4.0];
92        let (state, transformed) = Standardize::fit_transform(&y).unwrap();
93        let restored = Standardize::inverse_slice(&state, &transformed).unwrap();
94
95        assert!(state.center.is_finite());
96        assert!(state.scale.is_finite());
97        assert!(state.scale > 0.0);
98        for (actual, expected) in restored.iter().zip(y) {
99            assert_relative_eq!(*actual, expected, epsilon = 1.0e-6);
100        }
101    }
102}