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 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}