use super::{Accumulator, Mergeable};
use crate::error::StatError;
#[derive(Debug, Clone, PartialEq)]
pub struct HistResult {
pub counts: Vec<u64>,
pub edges: Vec<f64>,
pub underflow: u64,
pub overflow: u64,
}
#[derive(Debug, Clone)]
pub struct Histogram {
lo: f64,
hi: f64,
n_bins: usize, counts: Vec<u64>,
underflow: u64,
overflow: u64,
}
impl Histogram {
pub fn new(lo: f64, hi: f64, n_bins: usize) -> Result<Self, StatError> {
if lo.is_nan() || hi.is_nan() || lo >= hi {
return Err(StatError::DomainError("histogram: need lo < hi"));
}
if n_bins == 0 {
return Err(StatError::DomainError("histogram: need n_bins >= 1"));
}
Ok(Self { lo, hi, n_bins, counts: vec![0; n_bins], underflow: 0, overflow: 0 })
}
#[inline]
fn bin_width(&self) -> f64 {
(self.hi - self.lo) / self.n_bins as f64
}
pub fn edges(&self) -> Vec<f64> {
let w = self.bin_width();
(0..=self.n_bins).map(|i| self.lo + i as f64 * w).collect()
}
pub fn counts(&self) -> &[u64] {
&self.counts
}
pub fn underflow(&self) -> u64 {
self.underflow
}
pub fn overflow(&self) -> u64 {
self.overflow
}
pub fn ecdf(&self) -> Vec<f64> {
let total: u64 = self.counts.iter().sum();
if total == 0 {
return vec![f64::NAN; self.n_bins];
}
let total = total as f64;
let mut acc = 0u64;
self.counts
.iter()
.map(|&c| {
acc += c;
acc as f64 / total
})
.collect()
}
}
impl Mergeable for Histogram {
fn merge(&mut self, o: &Self) {
if self.n_bins == 0 {
*self = o.clone();
return;
}
debug_assert_eq!(
(self.lo, self.hi, self.n_bins),
(o.lo, o.hi, o.n_bins),
"merging histograms with mismatched bins"
);
for (c, &oc) in self.counts.iter_mut().zip(o.counts.iter()) {
*c += oc;
}
self.underflow += o.underflow;
self.overflow += o.overflow;
}
}
impl Accumulator for Histogram {
type Item = f64;
type Output = HistResult;
fn empty() -> Self {
Self { lo: 0.0, hi: 0.0, n_bins: 0, counts: Vec::new(), underflow: 0, overflow: 0 }
}
fn update(&mut self, x: f64) {
if x.is_nan() {
return; }
if x < self.lo {
self.underflow += 1;
} else if x > self.hi {
self.overflow += 1;
} else {
let b = ((x - self.lo) / self.bin_width()) as usize;
self.counts[b.min(self.n_bins - 1)] += 1;
}
}
fn finalize(&self) -> HistResult {
HistResult {
counts: self.counts.clone(),
edges: self.edges(),
underflow: self.underflow,
overflow: self.overflow,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_rejects_bad_args() {
assert!(Histogram::new(1.0, 1.0, 4).is_err());
assert!(Histogram::new(2.0, 1.0, 4).is_err());
assert!(Histogram::new(0.0, 1.0, 0).is_err());
}
#[test]
fn matches_numpy_histogram() {
let mut h = Histogram::new(0.0, 2.0, 4).unwrap();
for x in [-1.0, 0.0, 0.5, 1.0, 1.0, 1.5, 2.0, 3.0] {
h.update(x);
}
assert_eq!(h.counts(), &[1, 1, 2, 2]);
assert_eq!(h.underflow(), 1);
assert_eq!(h.overflow(), 1);
let r = h.finalize();
assert_eq!(r.edges, vec![0.0, 0.5, 1.0, 1.5, 2.0]);
assert_eq!(r.counts, vec![1, 1, 2, 2]);
}
#[test]
fn nan_is_omitted() {
let mut h = Histogram::new(0.0, 1.0, 2).unwrap();
h.update(f64::NAN);
h.update(0.5);
assert_eq!(h.counts(), &[0, 1]);
assert_eq!(h.underflow() + h.overflow(), 0);
}
#[test]
fn ecdf_is_cumulative_normalized() {
let mut h = Histogram::new(0.0, 2.0, 4).unwrap();
for x in [0.0, 0.5, 1.0, 1.0, 1.5, 2.0] {
h.update(x); }
let e = h.ecdf();
assert!((e[0] - 1.0 / 6.0).abs() < 1e-15);
assert!((e[1] - 2.0 / 6.0).abs() < 1e-15);
assert!((e[2] - 4.0 / 6.0).abs() < 1e-15);
assert!((e[3] - 1.0).abs() < 1e-15);
}
#[test]
fn ecdf_empty_is_nan() {
let h = Histogram::new(0.0, 1.0, 3).unwrap();
assert!(h.ecdf().iter().all(|x| x.is_nan()));
}
#[test]
fn merge_adds_counts() {
let mk = |xs: &[f64]| {
let mut h = Histogram::new(0.0, 2.0, 4).unwrap();
for &x in xs {
h.update(x);
}
h
};
let whole = mk(&[0.0, 0.5, 1.0, 1.0, 1.5, 2.0, -1.0, 3.0]);
let mut a = mk(&[0.0, 0.5, 1.0, -1.0]);
let b = mk(&[1.0, 1.5, 2.0, 3.0]);
a.merge(&b);
assert_eq!(a.counts(), whole.counts());
assert_eq!(a.underflow(), whole.underflow());
assert_eq!(a.overflow(), whole.overflow());
}
#[test]
fn empty_merge_is_identity() {
let mut acc = Histogram::empty();
let mut h = Histogram::new(0.0, 1.0, 2).unwrap();
h.update(0.25);
h.update(0.75);
acc.merge(&h);
assert_eq!(acc.counts(), h.counts());
}
#[test]
fn bins_partition_the_range() {
let mut h = Histogram::new(0.0, 10.0, 5).unwrap();
for x in [1.0, 3.0, 3.0, 7.0, 9.5] {
h.update(x);
}
assert_eq!(h.counts(), &[1, 2, 0, 1, 1]);
}
}