rill_ml/preprocessing/
constant_imputer.rs1use crate::error::{RillError, checked_increment, ensure_finite};
7use crate::traits::Transformer;
8
9#[derive(Debug, Clone)]
11#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
12pub struct ConstantImputerConfig {
13 pub fill_value: f64,
15}
16
17impl Default for ConstantImputerConfig {
18 fn default() -> Self {
19 Self { fill_value: 0.0 }
20 }
21}
22
23#[derive(Debug, Clone)]
28#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
29pub struct ConstantImputer {
30 feature_count: usize,
31 config: ConstantImputerConfig,
32 samples_seen: u64,
33}
34
35impl ConstantImputer {
36 pub fn new(feature_count: usize) -> Result<Self, RillError> {
41 Self::with_config(feature_count, ConstantImputerConfig::default())
42 }
43
44 pub fn with_config(
50 feature_count: usize,
51 config: ConstantImputerConfig,
52 ) -> Result<Self, RillError> {
53 if feature_count == 0 {
54 return Err(RillError::EmptyFeatures);
55 }
56 ensure_finite("fill_value", config.fill_value)?;
57 Ok(Self {
58 feature_count,
59 config,
60 samples_seen: 0,
61 })
62 }
63
64 pub fn fill_value(&self) -> f64 {
66 self.config.fill_value
67 }
68
69 fn check_dimension(&self, features: &[f64]) -> Result<(), RillError> {
71 if features.is_empty() {
72 return Err(RillError::EmptyFeatures);
73 }
74 if features.len() != self.feature_count {
75 return Err(RillError::DimensionMismatch {
76 expected: self.feature_count,
77 actual: features.len(),
78 });
79 }
80 Ok(())
81 }
82}
83
84impl Transformer for ConstantImputer {
85 fn input_dim(&self) -> usize {
86 self.feature_count
87 }
88
89 fn output_dim(&self) -> usize {
90 self.feature_count
91 }
92
93 fn transform(&self, features: &[f64]) -> Result<Vec<f64>, RillError> {
94 self.check_dimension(features)?;
95 let mut out = Vec::with_capacity(features.len());
96 for &x in features {
97 if x.is_nan() {
98 out.push(self.config.fill_value);
99 } else {
100 ensure_finite("feature", x)?;
101 out.push(x);
102 }
103 }
104 Ok(out)
105 }
106
107 fn update(&mut self, features: &[f64]) -> Result<(), RillError> {
108 self.check_dimension(features)?;
109 for &x in features {
110 if !x.is_nan() {
111 ensure_finite("feature", x)?;
112 }
113 }
114 self.samples_seen = checked_increment(self.samples_seen, "samples_seen")?;
115 Ok(())
116 }
117
118 fn samples_seen(&self) -> u64 {
119 self.samples_seen
120 }
121
122 fn reset(&mut self) {
123 self.samples_seen = 0;
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130
131 #[test]
132 fn nan_replaced_with_fill_value() {
133 let imp = ConstantImputer::new(3).unwrap();
134 let out = imp.transform(&[1.0, f64::NAN, 3.0]).unwrap();
135 assert_eq!(out, vec![1.0, 0.0, 3.0]);
136 }
137
138 #[test]
139 fn non_nan_passed_through() {
140 let imp = ConstantImputer::new(3).unwrap();
141 let out = imp.transform(&[1.5, -2.0, 3.0]).unwrap();
142 assert_eq!(out, vec![1.5, -2.0, 3.0]);
143 }
144
145 #[test]
146 fn custom_fill_value() {
147 let imp =
148 ConstantImputer::with_config(2, ConstantImputerConfig { fill_value: -1.0 }).unwrap();
149 let out = imp.transform(&[f64::NAN, 5.0]).unwrap();
150 assert_eq!(out, vec![-1.0, 5.0]);
151 }
152
153 #[test]
154 fn dimension_mismatch_rejected() {
155 let imp = ConstantImputer::new(3).unwrap();
156 assert!(matches!(
157 imp.transform(&[1.0, 2.0]),
158 Err(RillError::DimensionMismatch { .. })
159 ));
160 let mut imp = imp;
161 assert!(matches!(
162 imp.update(&[1.0, 2.0, 3.0, 4.0]),
163 Err(RillError::DimensionMismatch { .. })
164 ));
165 }
166
167 #[test]
168 fn reset_clears_state() {
169 let mut imp = ConstantImputer::new(2).unwrap();
170 imp.update(&[1.0, f64::NAN]).unwrap();
171 imp.update(&[f64::NAN, 2.0]).unwrap();
172 assert_eq!(imp.samples_seen(), 2);
173 imp.reset();
174 assert_eq!(imp.samples_seen(), 0);
175 }
176
177 #[test]
178 #[cfg(feature = "serde")]
179 fn serde_roundtrip() {
180 let mut imp =
181 ConstantImputer::with_config(3, ConstantImputerConfig { fill_value: 7.0 }).unwrap();
182 imp.update(&[1.0, f64::NAN, 3.0]).unwrap();
183 let json = serde_json::to_string(&imp).unwrap();
184 let restored: ConstantImputer = serde_json::from_str(&json).unwrap();
185 assert_eq!(restored.input_dim(), imp.input_dim());
186 assert_eq!(restored.output_dim(), imp.output_dim());
187 assert_eq!(restored.samples_seen(), imp.samples_seen());
188 assert_eq!(restored.fill_value(), imp.fill_value());
189 }
190}