use crate::error::{RillError, checked_increment, ensure_finite, validate_features};
use crate::traits::Transformer;
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct StandardScalerConfig {
pub with_mean: bool,
pub with_std: bool,
pub epsilon: f64,
}
impl Default for StandardScalerConfig {
fn default() -> Self {
Self {
with_mean: true,
with_std: true,
epsilon: 1e-12,
}
}
}
#[derive(Debug, Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
pub struct StandardScaler {
feature_count: usize,
config: StandardScalerConfig,
counts: Vec<u64>,
means: Vec<f64>,
m2s: Vec<f64>,
}
impl StandardScaler {
pub fn new(feature_count: usize) -> Result<Self, RillError> {
Self::with_config(feature_count, StandardScalerConfig::default())
}
pub fn with_config(
feature_count: usize,
config: StandardScalerConfig,
) -> Result<Self, RillError> {
if feature_count == 0 {
return Err(RillError::EmptyFeatures);
}
ensure_finite("epsilon", config.epsilon)?;
if config.epsilon < 0.0 {
return Err(RillError::InvalidParameter {
name: "epsilon",
value: config.epsilon,
});
}
Ok(Self {
feature_count,
config,
counts: vec![0; feature_count],
means: vec![0.0; feature_count],
m2s: vec![0.0; feature_count],
})
}
pub fn means(&self) -> &[f64] {
&self.means
}
pub fn variances(&self) -> Vec<f64> {
self.m2s
.iter()
.zip(&self.counts)
.map(|(&m2, &n)| if n == 0 { 0.0 } else { m2 / n as f64 })
.collect()
}
pub fn std_devs(&self) -> Vec<f64> {
self.variances().iter().map(|v| v.sqrt()).collect()
}
pub fn scales(&self) -> Vec<f64> {
self.variances()
.iter()
.map(|&var| {
if var < self.config.epsilon {
1.0
} else {
var.sqrt()
}
})
.collect()
}
pub fn validate(&self) -> Result<(), RillError> {
if self.feature_count == 0 {
return Err(RillError::EmptyFeatures);
}
ensure_finite("epsilon", self.config.epsilon)?;
if self.config.epsilon < 0.0 {
return Err(RillError::InvalidParameter {
name: "epsilon",
value: self.config.epsilon,
});
}
if self.counts.len() != self.feature_count
|| self.means.len() != self.feature_count
|| self.m2s.len() != self.feature_count
{
return Err(RillError::InvalidState(
"standard scaler feature_count does not match state lengths".to_owned(),
));
}
if self.means.iter().any(|value| !value.is_finite())
|| self.m2s.iter().any(|value| !value.is_finite())
{
return Err(RillError::InvalidState(
"standard scaler state must contain only finite values".to_owned(),
));
}
if self.counts.windows(2).any(|pair| pair[0] != pair[1]) {
return Err(RillError::InvalidState(
"standard scaler feature counts must stay synchronized".to_owned(),
));
}
Ok(())
}
}
impl Transformer for StandardScaler {
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> {
self.validate()?;
validate_features(self.feature_count, features)?;
let scales = self.scales();
features
.iter()
.enumerate()
.map(|(i, &x)| {
let mean = if self.config.with_mean {
self.means[i]
} else {
0.0
};
let scale = if self.config.with_std { scales[i] } else { 1.0 };
let transformed = (x - mean) / scale;
ensure_finite("transformed feature", transformed)?;
Ok(transformed)
})
.collect()
}
fn update(&mut self, features: &[f64]) -> Result<(), RillError> {
self.validate()?;
validate_features(self.feature_count, features)?;
let mut next_counts = self.counts.clone();
let mut next_means = self.means.clone();
let mut next_m2s = self.m2s.clone();
for (i, &x) in features.iter().enumerate() {
let count = checked_increment(self.counts[i], "standard scaler sample")?;
let delta = x - self.means[i];
ensure_finite("standard scaler delta", delta)?;
let mean = self.means[i] + delta / count as f64;
ensure_finite("standard scaler mean", mean)?;
let delta2 = x - mean;
ensure_finite("standard scaler delta", delta2)?;
let m2 = self.m2s[i] + delta * delta2;
ensure_finite("standard scaler M2", m2)?;
next_counts[i] = count;
next_means[i] = mean;
next_m2s[i] = m2;
}
self.counts = next_counts;
self.means = next_means;
self.m2s = next_m2s;
Ok(())
}
fn samples_seen(&self) -> u64 {
self.counts.iter().copied().max().unwrap_or(0)
}
fn reset(&mut self) {
for c in &mut self.counts {
*c = 0;
}
for m in &mut self.means {
*m = 0.0;
}
for m2 in &mut self.m2s {
*m2 = 0.0;
}
}
}
#[cfg(feature = "serde")]
#[derive(serde::Deserialize)]
struct StandardScalerState {
feature_count: usize,
config: StandardScalerConfig,
counts: Vec<u64>,
means: Vec<f64>,
m2s: Vec<f64>,
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for StandardScaler {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let state = StandardScalerState::deserialize(deserializer)?;
let scaler = Self {
feature_count: state.feature_count,
config: state.config,
counts: state.counts,
means: state.means,
m2s: state.m2s,
};
scaler.validate().map_err(serde::de::Error::custom)?;
Ok(scaler)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn scaler_zero_state_returns_original() {
let s = StandardScaler::new(3).unwrap();
let out = s.transform(&[1.0, 2.0, 3.0]).unwrap();
assert!((out[0] - 1.0).abs() < 1e-12);
assert!((out[1] - 2.0).abs() < 1e-12);
assert!((out[2] - 3.0).abs() < 1e-12);
}
#[test]
fn scaler_standardizes_after_updates() {
let mut s = StandardScaler::new(2).unwrap();
s.update(&[1.0, 10.0]).unwrap();
s.update(&[3.0, 20.0]).unwrap();
let out = s.transform(&[3.0, 20.0]).unwrap();
assert!((out[0] - 1.0).abs() < 1e-9);
assert!((out[1] - 1.0).abs() < 1e-9);
}
#[test]
fn transform_does_not_update_state() {
let mut s = StandardScaler::new(1).unwrap();
s.update(&[10.0]).unwrap();
let mean_before = s.means()[0];
let _ = s.transform(&[5.0]).unwrap();
assert_eq!(s.means()[0], mean_before);
assert_eq!(s.counts[0], 1);
}
#[test]
fn update_rejects_overflow_without_mutating_state() {
let mut scaler = StandardScaler::new(1).unwrap();
scaler.update(&[f64::MAX]).unwrap();
let before = scaler.clone();
assert!(scaler.update(&[-f64::MAX]).is_err());
assert_eq!(scaler.counts, before.counts);
assert_eq!(scaler.means, before.means);
assert_eq!(scaler.m2s, before.m2s);
}
#[cfg(feature = "serde")]
#[test]
fn serde_rejects_malformed_state() {
let malformed = r#"{
"feature_count":2,
"config":{"with_mean":true,"with_std":true,"epsilon":1e-12},
"counts":[1],
"means":[0.0],
"m2s":[0.0]
}"#;
assert!(serde_json::from_str::<StandardScaler>(malformed).is_err());
}
#[test]
fn constant_feature_uses_scale_one() {
let mut s = StandardScaler::new(1).unwrap();
for _ in 0..10 {
s.update(&[5.0]).unwrap();
}
let out = s.transform(&[5.0]).unwrap();
assert!(out[0].abs() < 1e-12);
assert!(!out[0].is_nan());
}
#[test]
fn with_mean_false_keeps_offset() {
let mut s = StandardScaler::with_config(
1,
StandardScalerConfig {
with_mean: false,
with_std: true,
epsilon: 1e-12,
},
)
.unwrap();
s.update(&[1.0]).unwrap();
s.update(&[3.0]).unwrap();
let out = s.transform(&[3.0]).unwrap();
assert!((out[0] - 3.0).abs() < 1e-9);
}
#[test]
fn dimension_mismatch_rejected() {
let mut s = StandardScaler::new(3).unwrap();
assert!(s.transform(&[1.0, 2.0]).is_err());
assert!(s.update(&[1.0, 2.0]).is_err());
}
#[test]
fn zero_features_rejected() {
assert!(matches!(
StandardScaler::new(0),
Err(RillError::EmptyFeatures)
));
}
#[test]
fn non_finite_rejected() {
let mut s = StandardScaler::new(2).unwrap();
assert!(s.update(&[1.0, f64::NAN]).is_err());
}
#[test]
fn reset_clears_state() {
let mut s = StandardScaler::new(1).unwrap();
s.update(&[1.0]).unwrap();
s.update(&[2.0]).unwrap();
s.reset();
assert_eq!(s.counts[0], 0);
assert_eq!(s.means()[0], 0.0);
}
}