rill_ml/preprocessing/
missing_indicator.rs1use crate::error::{RillError, checked_increment, ensure_finite};
10use crate::traits::Transformer;
11
12#[derive(Debug, Clone)]
18#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
19pub struct MissingIndicator {
20 feature_count: usize,
21 samples_seen: u64,
22}
23
24impl MissingIndicator {
25 pub fn new(feature_count: usize) -> Result<Self, RillError> {
30 if feature_count == 0 {
31 return Err(RillError::EmptyFeatures);
32 }
33 Ok(Self {
34 feature_count,
35 samples_seen: 0,
36 })
37 }
38
39 fn check_dimension(&self, features: &[f64]) -> Result<(), RillError> {
41 if features.is_empty() {
42 return Err(RillError::EmptyFeatures);
43 }
44 if features.len() != self.feature_count {
45 return Err(RillError::DimensionMismatch {
46 expected: self.feature_count,
47 actual: features.len(),
48 });
49 }
50 Ok(())
51 }
52}
53
54impl Transformer for MissingIndicator {
55 fn input_dim(&self) -> usize {
56 self.feature_count
57 }
58
59 fn output_dim(&self) -> usize {
60 self.feature_count * 2
61 }
62
63 fn transform(&self, features: &[f64]) -> Result<Vec<f64>, RillError> {
64 self.check_dimension(features)?;
65 let mut out = Vec::with_capacity(features.len() * 2);
66 for &v in features {
67 if !v.is_nan() {
68 ensure_finite("feature", v)?;
69 }
70 out.push(v);
71 out.push(if v.is_nan() { 1.0 } else { 0.0 });
72 }
73 Ok(out)
74 }
75
76 fn update(&mut self, features: &[f64]) -> Result<(), RillError> {
77 self.check_dimension(features)?;
78 for &v in features {
79 if !v.is_nan() {
80 ensure_finite("feature", v)?;
81 }
82 }
83 self.samples_seen = checked_increment(self.samples_seen, "samples_seen")?;
84 Ok(())
85 }
86
87 fn samples_seen(&self) -> u64 {
88 self.samples_seen
89 }
90
91 fn reset(&mut self) {
92 self.samples_seen = 0;
93 }
94}
95
96#[cfg(test)]
97mod tests {
98 use super::*;
99
100 #[test]
101 fn no_missing_values() {
102 let mi = MissingIndicator::new(3).unwrap();
103 let out = mi.transform(&[1.0, 2.0, 3.0]).unwrap();
104 assert_eq!(out, vec![1.0, 0.0, 2.0, 0.0, 3.0, 0.0]);
105 }
106
107 #[test]
108 fn all_missing_values() {
109 let mi = MissingIndicator::new(2).unwrap();
110 let out = mi.transform(&[f64::NAN, f64::NAN]).unwrap();
111 assert!(out[0].is_nan());
112 assert_eq!(out[1], 1.0);
113 assert!(out[2].is_nan());
114 assert_eq!(out[3], 1.0);
115 }
116
117 #[test]
118 fn mixed_missing_and_present() {
119 let mi = MissingIndicator::new(3).unwrap();
120 let out = mi.transform(&[1.0, f64::NAN, 3.0]).unwrap();
121 assert_eq!(out[0], 1.0);
122 assert_eq!(out[1], 0.0);
123 assert!(out[2].is_nan());
124 assert_eq!(out[3], 1.0);
125 assert_eq!(out[4], 3.0);
126 assert_eq!(out[5], 0.0);
127 }
128
129 #[test]
130 fn dimension_mismatch_rejected() {
131 let mi = MissingIndicator::new(3).unwrap();
132 assert!(matches!(
133 mi.transform(&[1.0, 2.0]),
134 Err(RillError::DimensionMismatch { .. })
135 ));
136 let mut mi = mi;
137 assert!(matches!(
138 mi.update(&[1.0, 2.0, 3.0, 4.0]),
139 Err(RillError::DimensionMismatch { .. })
140 ));
141 }
142
143 #[test]
144 fn reset_clears_state() {
145 let mut mi = MissingIndicator::new(2).unwrap();
146 mi.update(&[1.0, 2.0]).unwrap();
147 mi.update(&[3.0, 4.0]).unwrap();
148 assert_eq!(mi.samples_seen(), 2);
149 mi.reset();
150 assert_eq!(mi.samples_seen(), 0);
151 }
152
153 #[test]
154 #[cfg(feature = "serde")]
155 fn serde_roundtrip() {
156 let mut mi = MissingIndicator::new(3).unwrap();
157 mi.update(&[1.0, 2.0, 3.0]).unwrap();
158 let json = serde_json::to_string(&mi).unwrap();
159 let restored: MissingIndicator = serde_json::from_str(&json).unwrap();
160 assert_eq!(restored.input_dim(), mi.input_dim());
161 assert_eq!(restored.output_dim(), mi.output_dim());
162 assert_eq!(restored.samples_seen(), mi.samples_seen());
163 }
164}