rill_ml/preprocessing/
mean_imputer.rs1use crate::error::{RillError, 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) {
66 let n = self.counts[idx] + 1;
67 self.counts[idx] = n;
68 let delta = value - self.means[idx];
69 self.means[idx] += delta / n as f64;
70 }
71}
72
73impl Transformer for MeanImputer {
74 fn input_dim(&self) -> usize {
75 self.feature_count
76 }
77
78 fn output_dim(&self) -> usize {
79 self.feature_count
80 }
81
82 fn transform(&self, features: &[f64]) -> Result<Vec<f64>, RillError> {
83 self.check_dimension(features)?;
84 Ok(features
85 .iter()
86 .enumerate()
87 .map(|(i, &x)| {
88 if x.is_nan() {
89 if self.counts[i] == 0 {
90 0.0
91 } else {
92 self.means[i]
93 }
94 } else {
95 x
96 }
97 })
98 .collect())
99 }
100
101 fn update(&mut self, features: &[f64]) -> Result<(), RillError> {
102 self.check_dimension(features)?;
103 for (i, &x) in features.iter().enumerate() {
104 if x.is_nan() {
105 continue;
106 }
107 ensure_finite("feature", x)?;
108 self.update_mean(i, x);
109 }
110 self.samples_seen += 1;
111 Ok(())
112 }
113
114 fn samples_seen(&self) -> u64 {
115 self.samples_seen
116 }
117
118 fn reset(&mut self) {
119 for c in &mut self.counts {
120 *c = 0;
121 }
122 for m in &mut self.means {
123 *m = 0.0;
124 }
125 self.samples_seen = 0;
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn nan_replaced_with_mean_after_update() {
135 let mut imp = MeanImputer::new(2).unwrap();
136 imp.update(&[2.0, 10.0]).unwrap();
139 imp.update(&[4.0, f64::NAN]).unwrap();
140 let out = imp.transform(&[f64::NAN, f64::NAN]).unwrap();
141 assert!((out[0] - 3.0).abs() < 1e-12);
142 assert!((out[1] - 10.0).abs() < 1e-12);
143 }
144
145 #[test]
146 fn nan_replaced_with_zero_when_no_data() {
147 let imp = MeanImputer::new(2).unwrap();
148 let out = imp.transform(&[f64::NAN, f64::NAN]).unwrap();
149 assert_eq!(out, vec![0.0, 0.0]);
150 }
151
152 #[test]
153 fn non_nan_passed_through() {
154 let mut imp = MeanImputer::new(2).unwrap();
155 imp.update(&[5.0, 6.0]).unwrap();
156 let out = imp.transform(&[1.5, -2.0]).unwrap();
157 assert_eq!(out, vec![1.5, -2.0]);
158 }
159
160 #[test]
161 fn mean_updates_correctly() {
162 let mut imp = MeanImputer::new(1).unwrap();
163 imp.update(&[1.0]).unwrap();
164 imp.update(&[2.0]).unwrap();
165 imp.update(&[3.0]).unwrap();
166 assert!((imp.means()[0] - 2.0).abs() < 1e-12);
167 assert_eq!(imp.counts()[0], 3);
168 }
169
170 #[test]
171 fn nan_skipped_in_update() {
172 let mut imp = MeanImputer::new(2).unwrap();
173 imp.update(&[1.0, f64::NAN]).unwrap();
176 imp.update(&[f64::NAN, f64::NAN]).unwrap();
177 imp.update(&[3.0, f64::NAN]).unwrap();
178 assert!((imp.means()[0] - 2.0).abs() < 1e-12);
179 assert_eq!(imp.counts()[0], 2);
180 assert_eq!(imp.counts()[1], 0);
181 assert!((imp.means()[1] - 0.0).abs() < 1e-12);
182 }
183
184 #[test]
185 fn dimension_mismatch_rejected() {
186 let imp = MeanImputer::new(3).unwrap();
187 assert!(matches!(
188 imp.transform(&[1.0, 2.0]),
189 Err(RillError::DimensionMismatch { .. })
190 ));
191 let mut imp = imp;
192 assert!(matches!(
193 imp.update(&[1.0, 2.0, 3.0, 4.0]),
194 Err(RillError::DimensionMismatch { .. })
195 ));
196 }
197
198 #[test]
199 fn reset_clears_state() {
200 let mut imp = MeanImputer::new(2).unwrap();
201 imp.update(&[1.0, 2.0]).unwrap();
202 imp.update(&[3.0, 4.0]).unwrap();
203 assert_eq!(imp.samples_seen(), 2);
204 assert_eq!(imp.counts()[0], 2);
205 imp.reset();
206 assert_eq!(imp.samples_seen(), 0);
207 assert_eq!(imp.counts()[0], 0);
208 assert!((imp.means()[0] - 0.0).abs() < 1e-12);
209 }
210
211 #[test]
212 #[cfg(feature = "serde")]
213 fn serde_roundtrip() {
214 let mut imp = MeanImputer::new(2).unwrap();
215 imp.update(&[1.0, f64::NAN]).unwrap();
216 imp.update(&[3.0, 5.0]).unwrap();
217 let json = serde_json::to_string(&imp).unwrap();
218 let restored: MeanImputer = serde_json::from_str(&json).unwrap();
219 assert_eq!(restored.input_dim(), imp.input_dim());
220 assert_eq!(restored.output_dim(), imp.output_dim());
221 assert_eq!(restored.samples_seen(), imp.samples_seen());
222 assert_eq!(restored.counts(), imp.counts());
223 assert!((restored.means()[0] - imp.means()[0]).abs() < 1e-12);
224 }
225}