Skip to main content

rill_ml/preprocessing/
standard_scaler.rs

1//! Online standard scaler.
2//!
3//! Maintains per-feature Welford variance and mean. Time complexity per
4//! update/transform: `O(d)`. Space complexity: `O(d)`.
5
6use crate::error::{RillError, checked_increment, ensure_finite, validate_features};
7use crate::traits::Transformer;
8
9/// Configuration for [`StandardScaler`].
10#[derive(Debug, Clone)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct StandardScalerConfig {
13    /// Whether to subtract the running mean. Default: `true`.
14    pub with_mean: bool,
15    /// Whether to divide by the running standard deviation. Default: `true`.
16    pub with_std: bool,
17    /// Variance threshold below which the scale is treated as `1.0` to avoid
18    /// division by zero. Default: `1e-12`.
19    pub epsilon: f64,
20}
21
22impl Default for StandardScalerConfig {
23    fn default() -> Self {
24        Self {
25            with_mean: true,
26            with_std: true,
27            epsilon: 1e-12,
28        }
29    }
30}
31
32/// Online standard scaler that standardizes features to approximately zero
33/// mean and unit variance.
34///
35/// - When `with_mean = false`, the mean subtraction is skipped.
36/// - When `with_std = false`, the scaling is skipped.
37/// - When a feature has seen zero samples, its mean is `0` and scale is `1`,
38///   so the original value is returned unchanged.
39/// - When a feature's variance is below `epsilon`, the scale is `1` to avoid
40///   NaN or Infinity.
41///
42/// `transform` does not update state; only `update` does.
43#[derive(Debug, Clone)]
44#[cfg_attr(feature = "serde", derive(serde::Serialize))]
45pub struct StandardScaler {
46    feature_count: usize,
47    config: StandardScalerConfig,
48    counts: Vec<u64>,
49    means: Vec<f64>,
50    m2s: Vec<f64>,
51}
52
53impl StandardScaler {
54    /// Create a new scaler for `feature_count` features with default config.
55    pub fn new(feature_count: usize) -> Result<Self, RillError> {
56        Self::with_config(feature_count, StandardScalerConfig::default())
57    }
58
59    /// Create a new scaler with a custom configuration.
60    pub fn with_config(
61        feature_count: usize,
62        config: StandardScalerConfig,
63    ) -> Result<Self, RillError> {
64        if feature_count == 0 {
65            return Err(RillError::EmptyFeatures);
66        }
67        ensure_finite("epsilon", config.epsilon)?;
68        if config.epsilon < 0.0 {
69            return Err(RillError::InvalidParameter {
70                name: "epsilon",
71                value: config.epsilon,
72            });
73        }
74        Ok(Self {
75            feature_count,
76            config,
77            counts: vec![0; feature_count],
78            means: vec![0.0; feature_count],
79            m2s: vec![0.0; feature_count],
80        })
81    }
82
83    /// The per-feature means.
84    pub fn means(&self) -> &[f64] {
85        &self.means
86    }
87
88    /// The per-feature variances (population).
89    pub fn variances(&self) -> Vec<f64> {
90        self.m2s
91            .iter()
92            .zip(&self.counts)
93            .map(|(&m2, &n)| if n == 0 { 0.0 } else { m2 / n as f64 })
94            .collect()
95    }
96
97    /// The per-feature standard deviations.
98    pub fn std_devs(&self) -> Vec<f64> {
99        self.variances().iter().map(|v| v.sqrt()).collect()
100    }
101
102    /// The per-feature scales used during transformation.
103    pub fn scales(&self) -> Vec<f64> {
104        self.variances()
105            .iter()
106            .map(|&var| {
107                if var < self.config.epsilon {
108                    1.0
109                } else {
110                    var.sqrt()
111                }
112            })
113            .collect()
114    }
115
116    /// Validate all configuration and persisted-state invariants.
117    ///
118    /// This is run automatically during deserialization and before operations
119    /// that index per-feature state.
120    pub fn validate(&self) -> Result<(), RillError> {
121        if self.feature_count == 0 {
122            return Err(RillError::EmptyFeatures);
123        }
124        ensure_finite("epsilon", self.config.epsilon)?;
125        if self.config.epsilon < 0.0 {
126            return Err(RillError::InvalidParameter {
127                name: "epsilon",
128                value: self.config.epsilon,
129            });
130        }
131        if self.counts.len() != self.feature_count
132            || self.means.len() != self.feature_count
133            || self.m2s.len() != self.feature_count
134        {
135            return Err(RillError::InvalidState(
136                "standard scaler feature_count does not match state lengths".to_owned(),
137            ));
138        }
139        if self.means.iter().any(|value| !value.is_finite())
140            || self.m2s.iter().any(|value| !value.is_finite())
141        {
142            return Err(RillError::InvalidState(
143                "standard scaler state must contain only finite values".to_owned(),
144            ));
145        }
146        if self.counts.windows(2).any(|pair| pair[0] != pair[1]) {
147            return Err(RillError::InvalidState(
148                "standard scaler feature counts must stay synchronized".to_owned(),
149            ));
150        }
151        Ok(())
152    }
153}
154
155impl Transformer for StandardScaler {
156    fn input_dim(&self) -> usize {
157        self.feature_count
158    }
159
160    fn output_dim(&self) -> usize {
161        self.feature_count
162    }
163
164    fn transform(&self, features: &[f64]) -> Result<Vec<f64>, RillError> {
165        self.validate()?;
166        validate_features(self.feature_count, features)?;
167        let scales = self.scales();
168        features
169            .iter()
170            .enumerate()
171            .map(|(i, &x)| {
172                let mean = if self.config.with_mean {
173                    self.means[i]
174                } else {
175                    0.0
176                };
177                let scale = if self.config.with_std { scales[i] } else { 1.0 };
178                let transformed = (x - mean) / scale;
179                ensure_finite("transformed feature", transformed)?;
180                Ok(transformed)
181            })
182            .collect()
183    }
184
185    fn update(&mut self, features: &[f64]) -> Result<(), RillError> {
186        self.validate()?;
187        validate_features(self.feature_count, features)?;
188        let mut next_counts = self.counts.clone();
189        let mut next_means = self.means.clone();
190        let mut next_m2s = self.m2s.clone();
191        for (i, &x) in features.iter().enumerate() {
192            let count = checked_increment(self.counts[i], "standard scaler sample")?;
193            let delta = x - self.means[i];
194            ensure_finite("standard scaler delta", delta)?;
195            let mean = self.means[i] + delta / count as f64;
196            ensure_finite("standard scaler mean", mean)?;
197            let delta2 = x - mean;
198            ensure_finite("standard scaler delta", delta2)?;
199            let m2 = self.m2s[i] + delta * delta2;
200            ensure_finite("standard scaler M2", m2)?;
201            next_counts[i] = count;
202            next_means[i] = mean;
203            next_m2s[i] = m2;
204        }
205        self.counts = next_counts;
206        self.means = next_means;
207        self.m2s = next_m2s;
208        Ok(())
209    }
210
211    fn samples_seen(&self) -> u64 {
212        self.counts.iter().copied().max().unwrap_or(0)
213    }
214
215    fn reset(&mut self) {
216        for c in &mut self.counts {
217            *c = 0;
218        }
219        for m in &mut self.means {
220            *m = 0.0;
221        }
222        for m2 in &mut self.m2s {
223            *m2 = 0.0;
224        }
225    }
226}
227
228#[cfg(feature = "serde")]
229#[derive(serde::Deserialize)]
230struct StandardScalerState {
231    feature_count: usize,
232    config: StandardScalerConfig,
233    counts: Vec<u64>,
234    means: Vec<f64>,
235    m2s: Vec<f64>,
236}
237
238#[cfg(feature = "serde")]
239impl<'de> serde::Deserialize<'de> for StandardScaler {
240    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
241    where
242        D: serde::Deserializer<'de>,
243    {
244        let state = StandardScalerState::deserialize(deserializer)?;
245        let scaler = Self {
246            feature_count: state.feature_count,
247            config: state.config,
248            counts: state.counts,
249            means: state.means,
250            m2s: state.m2s,
251        };
252        scaler.validate().map_err(serde::de::Error::custom)?;
253        Ok(scaler)
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260
261    #[test]
262    fn scaler_zero_state_returns_original() {
263        let s = StandardScaler::new(3).unwrap();
264        let out = s.transform(&[1.0, 2.0, 3.0]).unwrap();
265        // count == 0 -> mean=0, scale=1 -> original
266        assert!((out[0] - 1.0).abs() < 1e-12);
267        assert!((out[1] - 2.0).abs() < 1e-12);
268        assert!((out[2] - 3.0).abs() < 1e-12);
269    }
270
271    #[test]
272    fn scaler_standardizes_after_updates() {
273        let mut s = StandardScaler::new(2).unwrap();
274        // feature 0: values [1, 3] -> mean 2, var 1, std 1
275        // feature 1: values [10, 20] -> mean 15, var 25, std 5
276        s.update(&[1.0, 10.0]).unwrap();
277        s.update(&[3.0, 20.0]).unwrap();
278        let out = s.transform(&[3.0, 20.0]).unwrap();
279        // (3-2)/1 = 1, (20-15)/5 = 1
280        assert!((out[0] - 1.0).abs() < 1e-9);
281        assert!((out[1] - 1.0).abs() < 1e-9);
282    }
283
284    #[test]
285    fn transform_does_not_update_state() {
286        let mut s = StandardScaler::new(1).unwrap();
287        s.update(&[10.0]).unwrap();
288        let mean_before = s.means()[0];
289        let _ = s.transform(&[5.0]).unwrap();
290        assert_eq!(s.means()[0], mean_before);
291        assert_eq!(s.counts[0], 1);
292    }
293
294    #[test]
295    fn update_rejects_overflow_without_mutating_state() {
296        let mut scaler = StandardScaler::new(1).unwrap();
297        scaler.update(&[f64::MAX]).unwrap();
298        let before = scaler.clone();
299        assert!(scaler.update(&[-f64::MAX]).is_err());
300        assert_eq!(scaler.counts, before.counts);
301        assert_eq!(scaler.means, before.means);
302        assert_eq!(scaler.m2s, before.m2s);
303    }
304
305    #[cfg(feature = "serde")]
306    #[test]
307    fn serde_rejects_malformed_state() {
308        let malformed = r#"{
309            "feature_count":2,
310            "config":{"with_mean":true,"with_std":true,"epsilon":1e-12},
311            "counts":[1],
312            "means":[0.0],
313            "m2s":[0.0]
314        }"#;
315        assert!(serde_json::from_str::<StandardScaler>(malformed).is_err());
316    }
317
318    #[test]
319    fn constant_feature_uses_scale_one() {
320        let mut s = StandardScaler::new(1).unwrap();
321        for _ in 0..10 {
322            s.update(&[5.0]).unwrap();
323        }
324        // var = 0 < epsilon -> scale = 1, mean = 5 -> (5-5)/1 = 0
325        let out = s.transform(&[5.0]).unwrap();
326        assert!(out[0].abs() < 1e-12);
327        assert!(!out[0].is_nan());
328    }
329
330    #[test]
331    fn with_mean_false_keeps_offset() {
332        let mut s = StandardScaler::with_config(
333            1,
334            StandardScalerConfig {
335                with_mean: false,
336                with_std: true,
337                epsilon: 1e-12,
338            },
339        )
340        .unwrap();
341        s.update(&[1.0]).unwrap();
342        s.update(&[3.0]).unwrap();
343        // mean=2, var=1, std=1, but with_mean=false so x/1 = x
344        let out = s.transform(&[3.0]).unwrap();
345        assert!((out[0] - 3.0).abs() < 1e-9);
346    }
347
348    #[test]
349    fn dimension_mismatch_rejected() {
350        let mut s = StandardScaler::new(3).unwrap();
351        assert!(s.transform(&[1.0, 2.0]).is_err());
352        assert!(s.update(&[1.0, 2.0]).is_err());
353    }
354
355    #[test]
356    fn zero_features_rejected() {
357        assert!(matches!(
358            StandardScaler::new(0),
359            Err(RillError::EmptyFeatures)
360        ));
361    }
362
363    #[test]
364    fn non_finite_rejected() {
365        let mut s = StandardScaler::new(2).unwrap();
366        assert!(s.update(&[1.0, f64::NAN]).is_err());
367    }
368
369    #[test]
370    fn reset_clears_state() {
371        let mut s = StandardScaler::new(1).unwrap();
372        s.update(&[1.0]).unwrap();
373        s.update(&[2.0]).unwrap();
374        s.reset();
375        assert_eq!(s.counts[0], 0);
376        assert_eq!(s.means()[0], 0.0);
377    }
378}