#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Reducer {
Count,
Sum,
Mean,
Median,
Min,
Max,
Percentile(f64),
Deviation,
Variance,
StdErr,
First,
Last,
}
impl Reducer {
pub fn reduce(&self, values: &[f64]) -> f64 {
let mut state = ReducerState::new(*self);
if let ReducerState::Quantile { values: finite, .. } = &mut state {
finite.reserve(values.len());
}
for &value in values {
state.add(value);
}
state.finish()
}
}
#[derive(Debug, Clone)]
pub(crate) enum ReducerState {
Count(usize),
Sum(f64),
Mean {
count: usize,
mean: f64,
},
Min(Option<f64>),
Max(Option<f64>),
Quantile {
position: f64,
values: Vec<f64>,
},
Spread {
kind: Spread,
count: usize,
mean: f64,
m2: f64,
},
First(Option<f64>),
Last(Option<f64>),
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum Spread {
Deviation,
Variance,
StdErr,
}
impl ReducerState {
pub(crate) fn new(reducer: Reducer) -> ReducerState {
match reducer {
Reducer::Count => ReducerState::Count(0),
Reducer::Sum => ReducerState::Sum(0.0),
Reducer::Mean => ReducerState::Mean {
count: 0,
mean: 0.0,
},
Reducer::Min => ReducerState::Min(None),
Reducer::Max => ReducerState::Max(None),
Reducer::Median => ReducerState::Quantile {
position: 0.5,
values: Vec::new(),
},
Reducer::Percentile(position) => {
assert!(
(0.0..=1.0).contains(&position),
"Reducer::Percentile requires a position in [0, 1]"
);
ReducerState::Quantile {
position,
values: Vec::new(),
}
}
Reducer::Deviation | Reducer::Variance | Reducer::StdErr => ReducerState::Spread {
kind: match reducer {
Reducer::Variance => Spread::Variance,
Reducer::StdErr => Spread::StdErr,
_ => Spread::Deviation,
},
count: 0,
mean: 0.0,
m2: 0.0,
},
Reducer::First => ReducerState::First(None),
Reducer::Last => ReducerState::Last(None),
}
}
pub(crate) fn add(&mut self, value: f64) {
if !value.is_finite() {
return;
}
match self {
ReducerState::Count(count) => *count += 1,
ReducerState::Sum(sum) => *sum += value,
ReducerState::Mean { count, mean } => {
*count += 1;
*mean = crate::numeric::lerp(*mean, value, 1.0 / *count as f64);
}
ReducerState::Min(minimum) => {
*minimum = Some(minimum.map_or(value, |current| current.min(value)));
}
ReducerState::Max(maximum) => {
*maximum = Some(maximum.map_or(value, |current| current.max(value)));
}
ReducerState::Quantile { values, .. } => values.push(value),
ReducerState::Spread {
count, mean, m2, ..
} => {
*count += 1;
let delta = value - *mean;
*mean += delta / *count as f64;
*m2 += delta * (value - *mean);
}
ReducerState::First(first) => {
first.get_or_insert(value);
}
ReducerState::Last(last) => *last = Some(value),
}
}
pub(crate) fn finish(self) -> f64 {
match self {
ReducerState::Count(count) => count as f64,
ReducerState::Sum(sum) => sum,
ReducerState::Mean { count: 0, .. } => f64::NAN,
ReducerState::Mean { mean, .. } => mean,
ReducerState::Min(minimum) => minimum.unwrap_or(f64::NAN),
ReducerState::Max(maximum) => maximum.unwrap_or(f64::NAN),
ReducerState::Quantile { values, .. } if values.is_empty() => f64::NAN,
ReducerState::Quantile {
position,
mut values,
} => {
values.sort_by(f64::total_cmp);
quantile_sorted(&values, position)
}
ReducerState::Spread { count, .. } if count < 2 => f64::NAN,
ReducerState::Spread {
kind, count, m2, ..
} => {
let variance = m2 / (count - 1) as f64;
match kind {
Spread::Variance => variance,
Spread::Deviation => variance.sqrt(),
Spread::StdErr => (variance / count as f64).sqrt(),
}
}
ReducerState::First(first) => first.unwrap_or(f64::NAN),
ReducerState::Last(last) => last.unwrap_or(f64::NAN),
}
}
}
pub fn quantiles(values: &[f64], positions: &[f64]) -> Vec<f64> {
assert!(
positions.iter().all(|p| (0.0..=1.0).contains(p)),
"quantiles requires positions in [0, 1]"
);
let mut finite: Vec<f64> = values.iter().copied().filter(|v| v.is_finite()).collect();
if finite.is_empty() {
return vec![f64::NAN; positions.len()];
}
finite.sort_by(f64::total_cmp);
positions
.iter()
.map(|&position| quantile_sorted(&finite, position))
.collect()
}
pub(crate) fn quantile_select(values: &mut [f64], p: f64) -> f64 {
let position = (values.len() - 1) as f64 * p;
let index = position.floor() as usize;
let fraction = position - index as f64;
let (_, &mut at, above) = values.select_nth_unstable_by(index, f64::total_cmp);
if fraction > 0.0 && !above.is_empty() {
let next = above.iter().copied().fold(f64::INFINITY, f64::min);
crate::numeric::lerp(at, next, fraction)
} else {
at
}
}
pub(crate) fn quantile_sorted(sorted: &[f64], p: f64) -> f64 {
let position = (sorted.len() - 1) as f64 * p;
let index = position.floor() as usize;
let fraction = position - index as f64;
if index + 1 < sorted.len() {
crate::numeric::lerp(sorted[index], sorted[index + 1], fraction)
} else {
sorted[index]
}
}
#[cfg(test)]
#[path = "tests/reducer_tests.rs"]
mod tests;