use super::{validate_matrix, validate_transform_matrix};
use crate::error::Error;
use crate::utils::normalize::{NormalizationAxis, NormalizationOrder, normalize};
use crate::{Deserialize, Serialize};
use ndarray::{Array2, ArrayBase, Data, Ix2};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Normalizer {
order: NormalizationOrder,
n_features: Option<usize>,
}
impl Default for Normalizer {
fn default() -> Self {
Self {
order: NormalizationOrder::L2,
n_features: None,
}
}
}
impl Normalizer {
pub fn new(order: NormalizationOrder) -> Result<Self, Error> {
if matches!(order, NormalizationOrder::Lp(p) if p <= 0.0 || !p.is_finite()) {
return Err(Error::invalid_parameter(
"p",
"Lp norm parameter must be positive and finite",
));
}
Ok(Self {
order,
n_features: None,
})
}
get_field!(get_order, order, NormalizationOrder);
get_field!(get_n_features, n_features, Option<usize>);
pub fn fit<S>(&mut self, x: &ArrayBase<S, Ix2>) -> Result<&mut Self, Error>
where
S: Data<Elem = f64>,
{
validate_matrix(x)?;
self.n_features = Some(x.ncols());
Ok(self)
}
pub fn transform<S>(&self, x: &ArrayBase<S, Ix2>) -> Result<Array2<f64>, Error>
where
S: Data<Elem = f64>,
{
let n_features = self
.n_features
.ok_or_else(|| Error::not_fitted("Normalizer"))?;
validate_transform_matrix(x, n_features)?;
normalize(x, NormalizationAxis::Row, self.order)
}
pub fn fit_transform<S>(&mut self, x: &ArrayBase<S, Ix2>) -> Result<Array2<f64>, Error>
where
S: Data<Elem = f64>,
{
self.fit(x)?;
self.transform(x)
}
model_save_and_load_methods!(Normalizer);
}
#[cfg(test)]
mod tests {
use super::*;
use ndarray::array;
#[test]
fn scales_rows_to_unit_l2_norm() {
let x = array![[3.0, 4.0], [1.0, 0.0]];
let mut normalizer = Normalizer::default();
let z = normalizer.fit_transform(&x).unwrap();
assert_eq!(z, array![[0.6, 0.8], [1.0, 0.0]]);
assert_eq!(normalizer.get_n_features(), Some(2));
}
#[test]
fn honours_the_configured_order() {
let x = array![[3.0, 4.0]];
let l1 = Normalizer::new(NormalizationOrder::L1)
.unwrap()
.fit_transform(&x)
.unwrap();
assert!((l1[[0, 0]] - 3.0 / 7.0).abs() < 1e-12);
let max = Normalizer::new(NormalizationOrder::Max)
.unwrap()
.fit_transform(&x)
.unwrap();
assert_eq!(max, array![[0.75, 1.0]]);
}
#[test]
fn invalid_lp_order_is_rejected() {
let err = Normalizer::new(NormalizationOrder::Lp(0.0)).unwrap_err();
match err {
Error::InvalidParameter { name, .. } => assert_eq!(name, "p"),
other => panic!("expected InvalidParameter, got {:?}", other),
}
}
#[test]
fn transform_is_independent_of_the_batch() {
let x = array![[3.0, 4.0], [1.0, 1.0], [0.0, 5.0]];
let mut normalizer = Normalizer::default();
normalizer.fit(&x.slice(ndarray::s![0..2, ..])).unwrap();
let whole = normalizer.transform(&x).unwrap();
let single = normalizer.transform(&array![[0.0, 5.0]]).unwrap();
assert_eq!(whole.row(2).to_owned(), single.row(0).to_owned());
}
#[test]
fn zero_row_is_left_untouched() {
let x = array![[3.0, 4.0], [0.0, 0.0]];
let z = Normalizer::default().fit_transform(&x).unwrap();
assert_eq!(z.row(1).to_owned(), array![0.0, 0.0]);
}
#[test]
fn transform_before_fit_gives_not_fitted() {
let err = Normalizer::default().transform(&array![[1.0]]).unwrap_err();
match err {
Error::NotFitted(model) => assert_eq!(model, "Normalizer"),
other => panic!("expected NotFitted, got {:?}", other),
}
}
}