use crate::error::Error;
use crate::math::reduction::det_reduce;
use crate::parallel_gates::{
cheap_map_f64_parallel_threshold, scan_f64_parallel_min_elems, sum_f64_parallel_min_elems,
};
use ndarray::{Array, ArrayBase, ArrayViewMut1, Axis, Data, Dimension};
use rayon::iter::ParallelIterator;
use rayon::prelude::IntoParallelRefMutIterator;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StandardizationAxis {
Row,
Column,
Global,
}
impl StandardizationAxis {
fn apply<D>(&self, data: &mut Array<f64, D>) -> Result<(), Error>
where
D: Dimension,
{
match self {
StandardizationAxis::Global => standardize_global(data),
StandardizationAxis::Row => standardize_lanes(data, 1, "Row standardization"),
StandardizationAxis::Column => standardize_lanes(data, 2, "Column standardization"),
}
}
}
pub fn standardize<S, D>(
data: &ArrayBase<S, D>,
axis: StandardizationAxis,
) -> Result<Array<f64, D>, Error>
where
S: Data<Elem = f64>,
D: Dimension,
{
if data.is_empty() {
return Err(Error::empty_input("Cannot standardize empty array"));
}
if data.iter().any(|&x| !x.is_finite()) {
return Err(Error::non_finite("input data"));
}
let mut result = data.to_owned();
axis.apply(&mut result)?;
Ok(result)
}
type WelfordState = (f64, f64, f64);
#[inline]
fn welford_step((count, mean, m2): WelfordState, x: f64) -> WelfordState {
let count = count + 1.0;
let delta = x - mean;
let mean = mean + delta / count;
let m2 = m2 + delta * (x - mean);
(count, mean, m2)
}
#[inline]
fn welford_merge(a: WelfordState, b: WelfordState) -> WelfordState {
let (na, ma, m2a) = a;
let (nb, mb, m2b) = b;
if na == 0.0 {
return b;
}
if nb == 0.0 {
return a;
}
let n = na + nb;
let delta = mb - ma;
let mean = ma + delta * nb / n;
let m2 = m2a + m2b + delta * delta * na * nb / n;
(n, mean, m2)
}
#[inline]
fn is_constant_feature(variance: f64, mean: f64, n: f64) -> bool {
let eps = f64::EPSILON;
let upper_bound = n * eps * variance + (n * mean * eps).powi(2);
variance <= upper_bound
}
#[inline]
fn scale_from_variance(variance: f64, mean: f64, n: f64) -> f64 {
if is_constant_feature(variance, mean, n) {
1.0
} else {
variance.sqrt()
}
}
fn standardize_global<D>(data: &mut Array<f64, D>) -> Result<(), Error>
where
D: Dimension,
{
let n = data.len() as f64;
if n == 0.0 {
return Err(Error::computation("No values to standardize"));
}
let (_, mean, m2) = match data.as_slice() {
Some(slice) => det_reduce(
slice,
slice.len() >= sum_f64_parallel_min_elems(),
|block| {
block
.iter()
.fold((0.0, 0.0, 0.0), |acc, &x| welford_step(acc, x))
},
welford_merge,
(0.0, 0.0, 0.0),
),
_ => data
.iter()
.fold((0.0, 0.0, 0.0), |acc, &x| welford_step(acc, x)),
};
let scale = scale_from_variance(m2 / n, mean, n);
if data.len() >= cheap_map_f64_parallel_threshold() {
data.par_mapv_inplace(|x| (x - mean) / scale);
} else {
data.mapv_inplace(|x| (x - mean) / scale);
}
Ok(())
}
fn lane_mean_and_std(lane: &ArrayViewMut1<f64>) -> (f64, f64) {
let n = lane.len() as f64;
let (_, mean, m2) = lane
.iter()
.fold((0.0, 0.0, 0.0), |acc, &x| welford_step(acc, x));
(mean, scale_from_variance(m2 / n, mean, n))
}
fn standardize_lanes<D>(
data: &mut Array<f64, D>,
axis_from_end: usize,
operation_name: &str,
) -> Result<(), Error>
where
D: Dimension,
{
let ndim = data.ndim();
if ndim < 2 {
return Err(Error::invalid_input(format!(
"{} requires at least 2 dimensions",
operation_name
)));
}
let axis = Axis(ndim - axis_from_end);
let data_len = data.len();
let mut lanes: Vec<ArrayViewMut1<f64>> = data.lanes_mut(axis).into_iter().collect();
let process = |lane: &mut ArrayViewMut1<f64>| {
let (mean, scale) = lane_mean_and_std(lane);
lane.mapv_inplace(|x| (x - mean) / scale);
};
if data_len >= scan_f64_parallel_min_elems() {
lanes.par_iter_mut().for_each(process);
} else {
lanes.iter_mut().for_each(process);
}
Ok(())
}