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    #[inline(always)]
44    fn transform(state: &Self::State, y: f64) -> f64 {
45        (y - state.center) / state.scale
46    }
47
48    #[inline(always)]
49    fn inverse(state: &Self::State, value: f64) -> f64 {
50        value.mul_add(state.scale, state.center)
51    }
52}
53
54#[cfg(test)]
55mod tests {
56    use approx::assert_relative_eq;
57
58    use crate::{Standardize, TargetTransform, TransformError};
59
60    #[test]
61    fn round_trips_values() {
62        let y = [1.0, 2.0, 4.0];
63        let (state, transformed) = Standardize::fit_transform(&y).unwrap();
64        let restored = Standardize::inverse_slice(&state, &transformed).unwrap();
65
66        for (actual, expected) in restored.iter().zip(y) {
67            assert_relative_eq!(*actual, expected);
68        }
69    }
70
71    #[test]
72    fn rejects_empty_and_non_finite_values() {
73        assert_eq!(
74            Standardize::fit(&[]).unwrap_err(),
75            TransformError::EmptyInput
76        );
77        assert_eq!(
78            Standardize::fit(&[1.0, f64::NAN]).unwrap_err(),
79            TransformError::NonFiniteValue
80        );
81    }
82
83    #[test]
84    fn rejects_zero_scale() {
85        assert_eq!(
86            Standardize::fit(&[2.0, 2.0]).unwrap_err(),
87            TransformError::ZeroScale
88        );
89    }
90
91    #[test]
92    fn handles_large_offset_values_stably() {
93        let y = [1.0e12, 1.0e12 + 2.0, 1.0e12 + 4.0];
94        let (state, transformed) = Standardize::fit_transform(&y).unwrap();
95        let restored = Standardize::inverse_slice(&state, &transformed).unwrap();
96
97        assert!(state.center.is_finite());
98        assert!(state.scale.is_finite());
99        assert!(state.scale > 0.0);
100        for (actual, expected) in restored.iter().zip(y) {
101            assert_relative_eq!(*actual, expected, epsilon = 1.0e-6);
102        }
103    }
104}