#[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),
}
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> },
}
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(),
}
}
}
}
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),
}
}
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)
}
}
}
}
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_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;