use sketches_ddsketch::{Config, DDSketch};
#[derive(Clone)]
pub struct Summary {
negative: DDSketch,
positive: DDSketch,
min_value: f64,
zeroes: usize,
min: f64,
max: f64,
}
impl Summary {
pub fn new(alpha: f64, max_buckets: u32, min_value: f64) -> Summary {
let config = Config::new(alpha, max_buckets, min_value.abs());
Summary {
negative: DDSketch::new(config),
positive: DDSketch::new(config),
min_value: min_value.abs(),
zeroes: 0,
min: f64::INFINITY,
max: f64::NEG_INFINITY,
}
}
pub fn with_defaults() -> Summary {
Summary::new(0.0001, 32_768, 1.0e-9)
}
pub fn add(&mut self, value: f64) {
if value.is_infinite() {
return;
}
if value < self.min {
self.min = value;
}
if value > self.max {
self.max = value;
}
if value > self.min_value {
self.positive.add(value);
} else if value < -self.min_value {
self.negative.add(-value);
} else {
self.zeroes += 1;
}
}
pub fn quantile(&self, q: f64) -> Option<f64> {
if q < 0.0 || q > 1.0 || self.count() == 0 {
return None;
}
let ncount = self.negative.count();
let pcount = self.positive.count();
let zcount = self.zeroes;
let total = ncount + pcount + zcount;
let rank = (q * (total - 1) as f64) as usize;
if rank < ncount {
let nq = 1.0 - (rank as f64 / ncount as f64);
self.negative
.quantile(nq)
.expect("quantile should be valid at this point")
.map(|v| -v)
} else if rank >= ncount && rank < (ncount + zcount) {
Some(0.0)
} else {
let pq = (rank - (ncount + zcount)) as f64 / pcount as f64;
self.positive
.quantile(pq)
.expect("quantile should be valid at this point")
}
}
pub fn min(&self) -> f64 {
self.min
}
pub fn max(&self) -> f64 {
self.max
}
pub fn is_empty(&self) -> bool {
self.count() == 0
}
pub fn count(&self) -> usize {
self.negative.count() + self.positive.count() + self.zeroes
}
pub fn detailed_count(&self) -> (usize, usize, usize) {
(self.zeroes, self.negative.count(), self.positive.count())
}
pub fn estimated_size(&self) -> usize {
std::mem::size_of::<Self>() + ((self.positive.length() + self.negative.length()) * 8)
}
}
#[cfg(test)]
mod tests {
use super::Summary;
use quickcheck_macros::quickcheck;
#[allow(unused_imports)]
use approx::{abs_diff_eq, assert_abs_diff_eq, assert_relative_eq, relative_eq};
use ndarray::{Array1, Axis};
use ndarray_stats::{interpolate::Linear, QuantileExt};
use noisy_float::types::n64;
use ordered_float::NotNan;
use rand::{distributions::Distribution, thread_rng};
use rand_distr::Uniform;
#[test]
fn test_basics() {
let mut summary = Summary::with_defaults();
assert!(summary.is_empty());
summary.add(-420.42);
assert_eq!(summary.count(), 1);
assert_relative_eq!(summary.min(), -420.42);
assert_relative_eq!(summary.max(), -420.42);
assert_abs_diff_eq!(summary.quantile(0.1).expect("value should exist"), -420.42);
assert_abs_diff_eq!(summary.quantile(0.5).expect("value should exist"), -420.42);
assert_abs_diff_eq!(summary.quantile(0.99).expect("value should exist"), -420.42);
summary.add(420.42);
assert_eq!(summary.count(), 2);
assert_relative_eq!(summary.min(), -420.42);
assert_relative_eq!(summary.max(), 420.42);
assert_abs_diff_eq!(summary.quantile(0.49).expect("value should exist"), -420.42);
summary.add(42.42);
assert_eq!(summary.count(), 3);
assert_relative_eq!(summary.min(), -420.42);
assert_relative_eq!(summary.max(), 420.42);
assert_abs_diff_eq!(
summary.quantile(0.4999999999).expect("value should exist"),
-420.42
);
assert_abs_diff_eq!(summary.quantile(0.5).expect("value should exist"), 42.42);
assert_abs_diff_eq!(
summary.quantile(0.9999999999).expect("value should exist"),
42.42
);
}
#[test]
fn test_positive_uniform() {
let alpha = 0.0001;
let max_bins = 32_768;
let min_value = 1.0e-9;
let mut rng = thread_rng();
let dist = Uniform::new(0.0, 100.0);
let mut summary = Summary::new(alpha, max_bins, min_value);
let mut uniform = Vec::new();
for _ in 0..100_000 {
let value = dist.sample(&mut rng);
uniform.push(NotNan::new(value).unwrap());
summary.add(value);
}
uniform.sort();
let mut true_histogram = Array1::from(uniform);
let quantiles = &[0.25, 0.5, 0.75, 0.99];
for quantile in quantiles {
let aval_raw = true_histogram
.quantile_axis_mut(Axis(0), n64(*quantile), &Linear)
.expect("quantile should be in range");
let aval = aval_raw
.get(())
.expect("quantile value should be present")
.into_inner();
let sval = summary
.quantile(*quantile)
.expect("quantile value should be present");
let distance = (aval * alpha) * 2.0;
assert_relative_eq!(aval, sval, max_relative = distance);
}
}
#[test]
fn test_negative_positive_uniform() {
let alpha = 0.0001;
let max_bins = 65_536;
let min_value = 1.0e-9;
let mut rng = thread_rng();
let dist = Uniform::new(-100.0, 100.0);
let mut summary = Summary::new(alpha, max_bins, min_value);
let mut uniform = Vec::new();
for _ in 0..100_000 {
let value = dist.sample(&mut rng);
uniform.push(NotNan::new(value).unwrap());
summary.add(value);
}
uniform.sort();
let mut true_histogram = Array1::from(uniform);
let quantiles = &[0.25, 0.47, 0.75, 0.99];
for quantile in quantiles {
let aval_raw = true_histogram
.quantile_axis_mut(Axis(0), n64(*quantile), &Linear)
.expect("quantile should be in range");
let aval = aval_raw
.get(())
.expect("quantile value should be present")
.into_inner();
let sval = summary
.quantile(*quantile)
.expect("quantile value should be present");
let distance = (aval.abs() * alpha) * 2.0;
assert_relative_eq!(aval, sval, max_relative = distance);
}
}
#[test]
fn test_zeroes() {
let mut summary = Summary::with_defaults();
summary.add(0.0);
assert_eq!(summary.quantile(0.5), Some(0.0));
}
#[test]
fn test_infinities() {
let mut summary = Summary::with_defaults();
summary.add(f64::INFINITY);
assert_eq!(summary.quantile(0.5), None);
summary.add(f64::NEG_INFINITY);
assert_eq!(summary.quantile(0.5), None);
}
#[quickcheck]
fn quantile_validity(inputs: Vec<f64>) -> bool {
let mut had_non_inf = false;
let mut summary = Summary::with_defaults();
for input in &inputs {
if !input.is_infinite() {
had_non_inf = true;
}
summary.add(*input);
}
let qs = &[0.0, 0.5, 0.9, 0.95, 0.99, 0.999, 1.0];
for q in qs {
let result = summary.quantile(*q);
if had_non_inf {
assert!(result.is_some());
} else {
assert!(result.is_none());
}
}
true
}
}