rill_ml/preprocessing/
min_max_scaler.rs1use crate::error::{RillError, ensure_finite, validate_features};
7use crate::traits::Transformer;
8
9#[derive(Debug, Clone)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
16pub struct MinMaxScaler {
17 feature_count: usize,
18 mins: Vec<Option<f64>>,
19 maxs: Vec<Option<f64>>,
20 target_min: f64,
21 target_max: f64,
22}
23
24impl MinMaxScaler {
25 pub fn new(feature_count: usize) -> Result<Self, RillError> {
27 Self::with_range(feature_count, 0.0, 1.0)
28 }
29
30 pub fn with_range(
32 feature_count: usize,
33 target_min: f64,
34 target_max: f64,
35 ) -> Result<Self, RillError> {
36 if feature_count == 0 {
37 return Err(RillError::EmptyFeatures);
38 }
39 ensure_finite("target_min", target_min)?;
40 ensure_finite("target_max", target_max)?;
41 if target_min >= target_max {
42 return Err(RillError::InvalidParameter {
43 name: "target_min",
44 value: target_min,
45 });
46 }
47 Ok(Self {
48 feature_count,
49 mins: vec![None; feature_count],
50 maxs: vec![None; feature_count],
51 target_min,
52 target_max,
53 })
54 }
55
56 pub fn mins(&self) -> Vec<Option<f64>> {
58 self.mins.clone()
59 }
60
61 pub fn maxs(&self) -> Vec<Option<f64>> {
63 self.maxs.clone()
64 }
65}
66
67impl Transformer for MinMaxScaler {
68 fn input_dim(&self) -> usize {
69 self.feature_count
70 }
71
72 fn output_dim(&self) -> usize {
73 self.feature_count
74 }
75
76 fn transform(&self, features: &[f64]) -> Result<Vec<f64>, RillError> {
77 validate_features(self.feature_count, features)?;
78 let range = self.target_max - self.target_min;
79 Ok(features
80 .iter()
81 .enumerate()
82 .map(|(i, &x)| {
83 match (self.mins[i], self.maxs[i]) {
84 (Some(min), Some(max)) => {
85 if (max - min).abs() < f64::EPSILON {
86 self.target_min + range / 2.0
88 } else {
89 self.target_min + (x - min) / (max - min) * range
90 }
91 }
92 _ => x,
93 }
94 })
95 .collect())
96 }
97
98 fn update(&mut self, features: &[f64]) -> Result<(), RillError> {
99 validate_features(self.feature_count, features)?;
100 for (i, &x) in features.iter().enumerate() {
101 ensure_finite("feature", x)?;
102 self.mins[i] = Some(match self.mins[i] {
103 None => x,
104 Some(m) => m.min(x),
105 });
106 self.maxs[i] = Some(match self.maxs[i] {
107 None => x,
108 Some(m) => m.max(x),
109 });
110 }
111 Ok(())
112 }
113
114 fn samples_seen(&self) -> u64 {
115 self.mins.iter().filter(|m| m.is_some()).count() as u64
116 }
117
118 fn reset(&mut self) {
119 for m in &mut self.mins {
120 *m = None;
121 }
122 for m in &mut self.maxs {
123 *m = None;
124 }
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn minmax_scales_to_zero_one() {
134 let mut s = MinMaxScaler::new(1).unwrap();
135 s.update(&[0.0]).unwrap();
136 s.update(&[10.0]).unwrap();
137 let out = s.transform(&[5.0]).unwrap();
138 assert!((out[0] - 0.5).abs() < 1e-12);
139 }
140
141 #[test]
142 fn constant_feature_returns_midpoint() {
143 let mut s = MinMaxScaler::new(1).unwrap();
144 s.update(&[5.0]).unwrap();
145 s.update(&[5.0]).unwrap();
146 let out = s.transform(&[5.0]).unwrap();
147 assert!((out[0] - 0.5).abs() < 1e-12);
148 }
149
150 #[test]
151 fn custom_range() {
152 let mut s = MinMaxScaler::with_range(1, -1.0, 1.0).unwrap();
153 s.update(&[0.0]).unwrap();
154 s.update(&[10.0]).unwrap();
155 let out = s.transform(&[5.0]).unwrap();
156 assert!((out[0] - 0.0).abs() < 1e-12);
157 }
158
159 #[test]
160 fn invalid_range_rejected() {
161 assert!(MinMaxScaler::with_range(1, 1.0, 1.0).is_err());
162 assert!(MinMaxScaler::with_range(1, 2.0, 1.0).is_err());
163 }
164
165 #[test]
166 fn unobserved_feature_returns_original() {
167 let s = MinMaxScaler::new(1).unwrap();
168 let out = s.transform(&[7.0]).unwrap();
169 assert!((out[0] - 7.0).abs() < 1e-12);
170 }
171}