Skip to main content

rill_ml/preprocessing/
mean_imputer.rs

1//! Mean imputer for missing data.
2//!
3//! Replaces `NaN` values with the running mean of observed non-NaN
4//! values for each feature. Uses Welford's algorithm for numerical
5//! stability.
6
7use crate::error::{RillError, checked_finite_add, checked_increment, ensure_finite};
8use crate::traits::Transformer;
9
10/// Replaces `NaN` values with the per-feature running mean.
11///
12/// When a feature has seen zero non-NaN values, `NaN` is replaced
13/// with `0.0`. This transformer accepts `NaN` in its input.
14#[derive(Debug, Clone)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct MeanImputer {
17    feature_count: usize,
18    counts: Vec<u64>,
19    means: Vec<f64>,
20    samples_seen: u64,
21}
22
23impl MeanImputer {
24    /// Create a new imputer for `feature_count` features.
25    ///
26    /// # Errors
27    /// Returns [`RillError::EmptyFeatures`] if `feature_count` is `0`.
28    pub fn new(feature_count: usize) -> Result<Self, RillError> {
29        if feature_count == 0 {
30            return Err(RillError::EmptyFeatures);
31        }
32        Ok(Self {
33            feature_count,
34            counts: vec![0; feature_count],
35            means: vec![0.0; feature_count],
36            samples_seen: 0,
37        })
38    }
39
40    /// The per-feature running means of observed non-NaN values.
41    pub fn means(&self) -> &[f64] {
42        &self.means
43    }
44
45    /// The per-feature counts of observed non-NaN values.
46    pub fn counts(&self) -> &[u64] {
47        &self.counts
48    }
49
50    /// Validate only the dimension, allowing NaN values.
51    fn check_dimension(&self, features: &[f64]) -> Result<(), RillError> {
52        if features.is_empty() {
53            return Err(RillError::EmptyFeatures);
54        }
55        if features.len() != self.feature_count {
56            return Err(RillError::DimensionMismatch {
57                expected: self.feature_count,
58                actual: features.len(),
59            });
60        }
61        Ok(())
62    }
63
64    /// Update the running mean for feature `idx` using Welford's algorithm.
65    fn update_mean(&mut self, idx: usize, value: f64) -> Result<(), RillError> {
66        let n = checked_increment(self.counts[idx], "feature count")?;
67        self.counts[idx] = n;
68        let delta = value - self.means[idx];
69        ensure_finite("mean delta", delta)?;
70        self.means[idx] = checked_finite_add(self.means[idx], delta / n as f64, "mean")?;
71        Ok(())
72    }
73}
74
75impl Transformer for MeanImputer {
76    fn input_dim(&self) -> usize {
77        self.feature_count
78    }
79
80    fn output_dim(&self) -> usize {
81        self.feature_count
82    }
83
84    fn transform(&self, features: &[f64]) -> Result<Vec<f64>, RillError> {
85        self.check_dimension(features)?;
86        let mut out = Vec::with_capacity(features.len());
87        for (i, &x) in features.iter().enumerate() {
88            if x.is_nan() {
89                out.push(if self.counts[i] == 0 {
90                    0.0
91                } else {
92                    self.means[i]
93                });
94            } else {
95                ensure_finite("feature", x)?;
96                out.push(x);
97            }
98        }
99        Ok(out)
100    }
101
102    fn update(&mut self, features: &[f64]) -> Result<(), RillError> {
103        self.check_dimension(features)?;
104        for (i, &x) in features.iter().enumerate() {
105            if x.is_nan() {
106                continue;
107            }
108            ensure_finite("feature", x)?;
109            self.update_mean(i, x)?;
110        }
111        self.samples_seen = checked_increment(self.samples_seen, "samples_seen")?;
112        Ok(())
113    }
114
115    fn samples_seen(&self) -> u64 {
116        self.samples_seen
117    }
118
119    fn reset(&mut self) {
120        for c in &mut self.counts {
121            *c = 0;
122        }
123        for m in &mut self.means {
124            *m = 0.0;
125        }
126        self.samples_seen = 0;
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn nan_replaced_with_mean_after_update() {
136        let mut imp = MeanImputer::new(2).unwrap();
137        // feature 0: observed [2.0, 4.0] -> mean 3.0
138        // feature 1: observed [10.0] -> mean 10.0
139        imp.update(&[2.0, 10.0]).unwrap();
140        imp.update(&[4.0, f64::NAN]).unwrap();
141        let out = imp.transform(&[f64::NAN, f64::NAN]).unwrap();
142        assert!((out[0] - 3.0).abs() < 1e-12);
143        assert!((out[1] - 10.0).abs() < 1e-12);
144    }
145
146    #[test]
147    fn nan_replaced_with_zero_when_no_data() {
148        let imp = MeanImputer::new(2).unwrap();
149        let out = imp.transform(&[f64::NAN, f64::NAN]).unwrap();
150        assert_eq!(out, vec![0.0, 0.0]);
151    }
152
153    #[test]
154    fn non_nan_passed_through() {
155        let mut imp = MeanImputer::new(2).unwrap();
156        imp.update(&[5.0, 6.0]).unwrap();
157        let out = imp.transform(&[1.5, -2.0]).unwrap();
158        assert_eq!(out, vec![1.5, -2.0]);
159    }
160
161    #[test]
162    fn mean_updates_correctly() {
163        let mut imp = MeanImputer::new(1).unwrap();
164        imp.update(&[1.0]).unwrap();
165        imp.update(&[2.0]).unwrap();
166        imp.update(&[3.0]).unwrap();
167        assert!((imp.means()[0] - 2.0).abs() < 1e-12);
168        assert_eq!(imp.counts()[0], 3);
169    }
170
171    #[test]
172    fn nan_skipped_in_update() {
173        let mut imp = MeanImputer::new(2).unwrap();
174        // feature 0: [1.0, NaN, 3.0] -> mean 2.0 (NaN skipped)
175        // feature 1: [NaN, NaN, NaN] -> count 0, mean 0.0
176        imp.update(&[1.0, f64::NAN]).unwrap();
177        imp.update(&[f64::NAN, f64::NAN]).unwrap();
178        imp.update(&[3.0, f64::NAN]).unwrap();
179        assert!((imp.means()[0] - 2.0).abs() < 1e-12);
180        assert_eq!(imp.counts()[0], 2);
181        assert_eq!(imp.counts()[1], 0);
182        assert!((imp.means()[1] - 0.0).abs() < 1e-12);
183    }
184
185    #[test]
186    fn dimension_mismatch_rejected() {
187        let imp = MeanImputer::new(3).unwrap();
188        assert!(matches!(
189            imp.transform(&[1.0, 2.0]),
190            Err(RillError::DimensionMismatch { .. })
191        ));
192        let mut imp = imp;
193        assert!(matches!(
194            imp.update(&[1.0, 2.0, 3.0, 4.0]),
195            Err(RillError::DimensionMismatch { .. })
196        ));
197    }
198
199    #[test]
200    fn reset_clears_state() {
201        let mut imp = MeanImputer::new(2).unwrap();
202        imp.update(&[1.0, 2.0]).unwrap();
203        imp.update(&[3.0, 4.0]).unwrap();
204        assert_eq!(imp.samples_seen(), 2);
205        assert_eq!(imp.counts()[0], 2);
206        imp.reset();
207        assert_eq!(imp.samples_seen(), 0);
208        assert_eq!(imp.counts()[0], 0);
209        assert!((imp.means()[0] - 0.0).abs() < 1e-12);
210    }
211
212    #[test]
213    #[cfg(feature = "serde")]
214    fn serde_roundtrip() {
215        let mut imp = MeanImputer::new(2).unwrap();
216        imp.update(&[1.0, f64::NAN]).unwrap();
217        imp.update(&[3.0, 5.0]).unwrap();
218        let json = serde_json::to_string(&imp).unwrap();
219        let restored: MeanImputer = serde_json::from_str(&json).unwrap();
220        assert_eq!(restored.input_dim(), imp.input_dim());
221        assert_eq!(restored.output_dim(), imp.output_dim());
222        assert_eq!(restored.samples_seen(), imp.samples_seen());
223        assert_eq!(restored.counts(), imp.counts());
224        assert!((restored.means()[0] - imp.means()[0]).abs() < 1e-12);
225    }
226}