qualia_core_db/solvers/learning/preprocessing/
scaling.rs1use crate::solvers::statistics::descriptive::{mean, std_dev};
5
6#[derive(Debug, Clone)]
10pub struct StandardScaler {
11 means: Vec<f64>,
12 stds: Vec<f64>,
13}
14
15impl StandardScaler {
16 pub fn fit(x: &[f64], n_rows: usize, n_cols: usize) -> Option<Self> {
19 if n_rows == 0 || n_cols == 0 || x.len() != n_rows * n_cols {
20 return None;
21 }
22 let mut means = vec![0.0; n_cols];
23 let mut stds = vec![0.0; n_cols];
24 let mut col = vec![0.0; n_rows];
25 for j in 0..n_cols {
26 for i in 0..n_rows {
27 col[i] = x[i * n_cols + j];
28 }
29 means[j] = mean(&col)?;
30 let s = std_dev(&col, true).unwrap_or(0.0);
31 stds[j] = if s > 0.0 { s } else { 1.0 };
32 }
33 Some(Self { means, stds })
34 }
35
36 pub fn means(&self) -> &[f64] {
37 &self.means
38 }
39 pub fn stds(&self) -> &[f64] {
40 &self.stds
41 }
42
43 pub fn transform_inplace(&self, x: &mut [f64], n_rows: usize, n_cols: usize) -> Option<()> {
46 if n_cols != self.means.len() || x.len() != n_rows * n_cols {
47 return None;
48 }
49 for i in 0..n_rows {
50 for j in 0..n_cols {
51 x[i * n_cols + j] = (x[i * n_cols + j] - self.means[j]) / self.stds[j];
52 }
53 }
54 Some(())
55 }
56
57 pub fn fit_transform(x: &[f64], n_rows: usize, n_cols: usize) -> Option<(Self, Vec<f64>)> {
59 let scaler = Self::fit(x, n_rows, n_cols)?;
60 let mut out = x.to_vec();
61 scaler.transform_inplace(&mut out, n_rows, n_cols)?;
62 Some((scaler, out))
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use crate::solvers::statistics::descriptive::{mean, std_dev};
70
71 #[test]
72 fn standardizes_to_zero_mean_unit_var() {
73 let x = [1.0, 10.0, 2.0, 20.0, 3.0, 30.0, 4.0, 40.0];
75 let (_, z) = StandardScaler::fit_transform(&x, 4, 2).unwrap();
76 for j in 0..2 {
77 let col: Vec<f64> = (0..4).map(|i| z[i * 2 + j]).collect();
78 assert!(mean(&col).unwrap().abs() < 1e-12, "col {j} mean");
79 assert!(
80 (std_dev(&col, true).unwrap() - 1.0).abs() < 1e-9,
81 "col {j} std"
82 );
83 }
84 }
85
86 #[test]
87 fn constant_column_is_centered_not_nan() {
88 let x = [5.0, 1.0, 5.0, 2.0, 5.0, 3.0]; let (_, z) = StandardScaler::fit_transform(&x, 3, 2).unwrap();
90 for i in 0..3 {
91 assert_eq!(z[i * 2], 0.0); }
93 }
94
95 #[test]
96 fn guards_shape() {
97 assert!(StandardScaler::fit(&[1.0, 2.0], 2, 2).is_none());
98 }
99}