rill_ml/preprocessing/
mean_imputer.rs1use crate::error::{RillError, checked_finite_add, checked_increment, ensure_finite};
8use crate::traits::Transformer;
9
10#[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 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 pub fn means(&self) -> &[f64] {
42 &self.means
43 }
44
45 pub fn counts(&self) -> &[u64] {
47 &self.counts
48 }
49
50 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 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 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 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}