use crate::error::{RillError, ensure_finite, validate_features};
use crate::traits::Transformer;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct MinMaxScaler {
feature_count: usize,
mins: Vec<Option<f64>>,
maxs: Vec<Option<f64>>,
target_min: f64,
target_max: f64,
}
impl MinMaxScaler {
pub fn new(feature_count: usize) -> Result<Self, RillError> {
Self::with_range(feature_count, 0.0, 1.0)
}
pub fn with_range(
feature_count: usize,
target_min: f64,
target_max: f64,
) -> Result<Self, RillError> {
if feature_count == 0 {
return Err(RillError::EmptyFeatures);
}
ensure_finite("target_min", target_min)?;
ensure_finite("target_max", target_max)?;
if target_min >= target_max {
return Err(RillError::InvalidParameter {
name: "target_min",
value: target_min,
});
}
Ok(Self {
feature_count,
mins: vec![None; feature_count],
maxs: vec![None; feature_count],
target_min,
target_max,
})
}
pub fn mins(&self) -> Vec<Option<f64>> {
self.mins.clone()
}
pub fn maxs(&self) -> Vec<Option<f64>> {
self.maxs.clone()
}
}
impl Transformer for MinMaxScaler {
fn input_dim(&self) -> usize {
self.feature_count
}
fn output_dim(&self) -> usize {
self.feature_count
}
fn transform(&self, features: &[f64]) -> Result<Vec<f64>, RillError> {
validate_features(self.feature_count, features)?;
let range = self.target_max - self.target_min;
Ok(features
.iter()
.enumerate()
.map(|(i, &x)| {
match (self.mins[i], self.maxs[i]) {
(Some(min), Some(max)) => {
if (max - min).abs() < f64::EPSILON {
self.target_min + range / 2.0
} else {
self.target_min + (x - min) / (max - min) * range
}
}
_ => x,
}
})
.collect())
}
fn update(&mut self, features: &[f64]) -> Result<(), RillError> {
validate_features(self.feature_count, features)?;
for (i, &x) in features.iter().enumerate() {
ensure_finite("feature", x)?;
self.mins[i] = Some(match self.mins[i] {
None => x,
Some(m) => m.min(x),
});
self.maxs[i] = Some(match self.maxs[i] {
None => x,
Some(m) => m.max(x),
});
}
Ok(())
}
fn samples_seen(&self) -> u64 {
self.mins.iter().filter(|m| m.is_some()).count() as u64
}
fn reset(&mut self) {
for m in &mut self.mins {
*m = None;
}
for m in &mut self.maxs {
*m = None;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn minmax_scales_to_zero_one() {
let mut s = MinMaxScaler::new(1).unwrap();
s.update(&[0.0]).unwrap();
s.update(&[10.0]).unwrap();
let out = s.transform(&[5.0]).unwrap();
assert!((out[0] - 0.5).abs() < 1e-12);
}
#[test]
fn constant_feature_returns_midpoint() {
let mut s = MinMaxScaler::new(1).unwrap();
s.update(&[5.0]).unwrap();
s.update(&[5.0]).unwrap();
let out = s.transform(&[5.0]).unwrap();
assert!((out[0] - 0.5).abs() < 1e-12);
}
#[test]
fn custom_range() {
let mut s = MinMaxScaler::with_range(1, -1.0, 1.0).unwrap();
s.update(&[0.0]).unwrap();
s.update(&[10.0]).unwrap();
let out = s.transform(&[5.0]).unwrap();
assert!((out[0] - 0.0).abs() < 1e-12);
}
#[test]
fn invalid_range_rejected() {
assert!(MinMaxScaler::with_range(1, 1.0, 1.0).is_err());
assert!(MinMaxScaler::with_range(1, 2.0, 1.0).is_err());
}
#[test]
fn unobserved_feature_returns_original() {
let s = MinMaxScaler::new(1).unwrap();
let out = s.transform(&[7.0]).unwrap();
assert!((out[0] - 7.0).abs() < 1e-12);
}
}