use crate::errors::QlResult;
use crate::math::comparison::close_enough;
use crate::math::statistics::{IncrementalStatistics, MeanStdDev, Statistics};
use crate::types::{Real, Size};
use crate::{fail, require};
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum HistogramAlgorithm {
Sturges,
FreedmanDiaconis,
Scott,
}
#[derive(Clone, Debug)]
pub struct Histogram {
algorithm: Option<HistogramAlgorithm>,
breaks: Vec<Real>,
counts: Vec<Size>,
frequencies: Vec<Real>,
}
impl Histogram {
pub fn with_break_count(data: &[Real], breaks: Size) -> QlResult<Self> {
require!(breaks < Size::MAX, "break count ({breaks}) is too large");
Histogram::build(data, breaks + 1, None, Vec::new(), None)
}
pub fn with_algorithm(data: &[Real], algorithm: HistogramAlgorithm) -> QlResult<Self> {
let n = data.len() as Real;
require!(!data.is_empty(), "no data given");
let (min, max) = min_max(data);
let bins = match algorithm {
HistogramAlgorithm::Sturges => (n.log2() + 1.0).ceil() as Size,
HistogramAlgorithm::FreedmanDiaconis => {
let mut sorted = data.to_vec();
sorted.sort_by(Real::total_cmp);
let r1 = quantile(&sorted, 0.25)?;
let r2 = quantile(&sorted, 0.75)?;
let h = 2.0 * (r2 - r1) * n.powf(-1.0 / 3.0);
width_bin_count(max - min, h)?
}
HistogramAlgorithm::Scott => {
let mut summary = IncrementalStatistics::new();
summary.add_sequence(data.iter().copied())?;
let h = 3.5 * summary.standard_deviation()? * n.powf(-1.0 / 3.0);
width_bin_count(max - min, h)?
}
};
Histogram::build(
data,
bins.max(1),
Some(algorithm),
Vec::new(),
Some((min, max)),
)
}
pub fn with_breaks(data: &[Real], mut breaks: Vec<Real>) -> QlResult<Self> {
let bins = breaks.len() + 1;
breaks.sort_by(Real::total_cmp);
breaks.dedup_by(|a, b| close_enough(*a, *b));
Histogram::build(data, bins, None, breaks, None)
}
pub fn bins(&self) -> Size {
self.counts.len()
}
pub fn breaks(&self) -> &[Real] {
&self.breaks
}
pub fn algorithm(&self) -> Option<HistogramAlgorithm> {
self.algorithm
}
pub fn counts(&self) -> &[Size] {
&self.counts
}
pub fn frequencies(&self) -> &[Real] {
&self.frequencies
}
fn build(
data: &[Real],
bins: Size,
algorithm: Option<HistogramAlgorithm>,
mut breaks: Vec<Real>,
extrema: Option<(Real, Real)>,
) -> QlResult<Self> {
require!(!data.is_empty(), "no data given");
if breaks.is_empty() {
let (min, max) = extrema.unwrap_or_else(|| min_max(data));
let h = (max - min) / bins as Real;
breaks = (1..bins).map(|i| min + i as Real * h).collect();
}
let mut counts: Vec<Size> = vec![0; bins];
for &point in data {
let bin = breaks.iter().position(|&b| point < b).unwrap_or(bins - 1);
counts[bin] += 1;
}
let total = data.len() as Real;
let frequencies = counts.iter().map(|&c| c as Real / total).collect();
Ok(Histogram {
algorithm,
breaks,
counts,
frequencies,
})
}
}
fn min_max(data: &[Real]) -> (Real, Real) {
data.iter()
.fold((Real::INFINITY, Real::NEG_INFINITY), |(min, max), &x| {
(min.min(x), max.max(x))
})
}
fn width_bin_count(range: Real, width: Real) -> QlResult<Size> {
if range < Real::MIN_POSITIVE {
return Ok(1);
}
let width_is_positive = width >= Real::MIN_POSITIVE;
require!(
width_is_positive,
"bin width ({width}) is not positive over the data range ({range})"
);
Ok((range / width).ceil() as Size)
}
fn quantile(sorted: &[Real], prob: Real) -> QlResult<Real> {
let n = sorted.len();
if !(0.0..=1.0).contains(&prob) {
fail!("probability ({prob}) has to be in [0, 1]");
}
require!(n > 0, "the sample size has to be positive");
if n == 1 {
return Ok(sorted[0]);
}
let a = 1.0 / 3.0;
let b = 2.0 * a / (n as Real + a);
if prob <= b {
return Ok(sorted[0]);
}
if prob >= 1.0 - b {
return Ok(sorted[n - 1]);
}
let index = ((n as Real + a) * prob + a).floor() as Size;
let weight = n as Real * prob + a - index as Real;
Ok((1.0 - weight) * sorted[index - 1] + weight * sorted[index])
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn counts_data_into_even_bins() {
let data = [1.0, 2.0, 2.0, 3.0, 3.0, 3.0, 4.0, 4.0, 4.0, 4.0];
let histogram = Histogram::with_break_count(&data, 2).unwrap();
assert_eq!(histogram.bins(), 3);
assert_eq!(histogram.breaks(), &[2.0, 3.0]);
assert_eq!(histogram.counts(), &[1, 2, 7]);
assert_eq!(histogram.frequencies(), &[0.1, 0.2, 0.7]);
assert_eq!(histogram.algorithm(), None);
}
#[test]
fn sorts_and_deduplicates_given_breaks() {
let data = [0.5, 1.5, 2.5, 3.5];
let histogram = Histogram::with_breaks(&data, vec![3.0, 1.0, 1.0, 2.0]).unwrap();
assert_eq!(histogram.breaks(), &[1.0, 2.0, 3.0]);
assert_eq!(histogram.bins(), 5);
assert_eq!(histogram.counts(), &[1, 1, 1, 0, 1]);
}
#[test]
fn bin_count_algorithms_match_hand_calculations() {
let data: Vec<Real> = (1..=100).map(|i| i as Real).collect();
let sturges = Histogram::with_algorithm(&data, HistogramAlgorithm::Sturges).unwrap();
assert_eq!(sturges.bins(), 8);
assert_eq!(sturges.algorithm(), Some(HistogramAlgorithm::Sturges));
let fd = Histogram::with_algorithm(&data, HistogramAlgorithm::FreedmanDiaconis).unwrap();
assert_eq!(fd.bins(), 5);
let scott = Histogram::with_algorithm(&data, HistogramAlgorithm::Scott).unwrap();
assert_eq!(scott.bins(), 5);
let total: Size = sturges.counts().iter().sum();
assert_eq!(total, data.len());
let frequency_sum: Real = sturges.frequencies().iter().sum();
assert!((frequency_sum - 1.0).abs() < 1e-15);
}
#[test]
fn quantile_interpolates_and_clamps_at_boundaries() {
let data: Vec<Real> = (1..=100).map(|i| i as Real).collect();
assert!((quantile(&data, 0.25).unwrap() - (76.0 / 3.0)).abs() < 1e-12);
assert!((quantile(&data, 0.75).unwrap() - (226.0 / 3.0)).abs() < 1e-12);
assert_eq!(quantile(&data, 0.001).unwrap(), 1.0);
assert_eq!(quantile(&data, 0.999).unwrap(), 100.0);
assert_eq!(quantile(&[42.0], 0.5).unwrap(), 42.0);
assert!(quantile(&data, -0.1).is_err());
assert!(quantile(&[], 0.5).is_err());
}
#[test]
fn quantile_clamps_at_exact_boundary_probabilities() {
let sorted = [1.0, 2.0];
let a = 1.0 / 3.0;
let b = 2.0 * a / (2.0 + a);
assert_eq!(quantile(&sorted, b).unwrap(), 1.0);
assert_eq!(quantile(&sorted, 1.0 - b).unwrap(), 2.0);
}
#[test]
fn zero_iqr_over_nonzero_range_is_an_error() {
let data = [0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 2.0];
assert!(Histogram::with_algorithm(&data, HistogramAlgorithm::FreedmanDiaconis).is_err());
}
#[test]
fn all_equal_data_yields_a_single_bin() {
let data = [5.0; 8];
let fd = Histogram::with_algorithm(&data, HistogramAlgorithm::FreedmanDiaconis).unwrap();
assert_eq!(fd.bins(), 1);
assert_eq!(fd.counts(), &[8]);
let scott = Histogram::with_algorithm(&data, HistogramAlgorithm::Scott).unwrap();
assert_eq!(scott.bins(), 1);
}
#[test]
fn single_sample_builds_a_single_bin() {
let sturges = Histogram::with_algorithm(&[7.0], HistogramAlgorithm::Sturges).unwrap();
assert_eq!(sturges.bins(), 1);
let fd = Histogram::with_algorithm(&[7.0], HistogramAlgorithm::FreedmanDiaconis).unwrap();
assert_eq!(fd.bins(), 1);
assert_eq!(fd.counts(), &[1]);
}
#[test]
fn nan_samples_count_into_the_last_bin() {
let data = [0.5, 1.5, Real::NAN];
let histogram = Histogram::with_breaks(&data, vec![1.0, 2.0]).unwrap();
assert_eq!(histogram.counts(), &[1, 1, 1]);
}
#[test]
fn rejects_empty_data_and_maximal_break_count() {
assert!(Histogram::with_break_count(&[], 3).is_err());
assert!(Histogram::with_algorithm(&[], HistogramAlgorithm::Sturges).is_err());
assert!(Histogram::with_breaks(&[], vec![1.0]).is_err());
assert!(Histogram::with_break_count(&[1.0, 2.0], Size::MAX).is_err());
}
}