gamlss_transform/transforms/
standardize.rs1use crate::{TargetTransform, TransformError, validate_non_empty_finite};
2
3#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
5pub struct Standardize;
6
7#[derive(Debug, Clone, Copy, PartialEq)]
9pub struct StandardizeState {
10 pub center: f64,
12 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}