use itertools::Itertools;
#[derive(Clone, Copy, Debug)]
pub struct Quartiles {
pub lower_whisker: f64,
pub lower_quartile: f64,
pub median: f64,
pub upper_quartile: f64,
pub upper_whisker: f64,
}
impl Quartiles {
pub fn new(
lower_whisker: f64,
lower_quartile: f64,
median: f64,
upper_quartile: f64,
upper_whisker: f64,
) -> Self {
Self {
lower_whisker,
lower_quartile,
median,
upper_quartile,
upper_whisker
}
}
pub fn new_min_max(values: &[f64]) -> Self {
let sorted_values = values
.iter()
.cloned()
.sorted_by(f64::total_cmp)
.collect::<Vec<_>>();
Self::new_min_max_from_sorted(&sorted_values)
}
pub fn new_min_max_from_sorted(values: &[f64]) -> Self {
let lower_whisker = quartile_from_sorted(values, 0.0);
let lower_quartile = quartile_from_sorted(values, 1.0 / 4.0);
let median = quartile_from_sorted(values, 1.0 / 2.0);
let upper_quartile = quartile_from_sorted(values, 3.0 / 4.0);
let upper_whisker = quartile_from_sorted(values, 1.0);
Self::new(lower_whisker, lower_quartile, median, upper_quartile, upper_whisker)
}
pub fn new_iqr(values: &[f64]) -> Self {
let sorted_values = values
.iter()
.cloned()
.sorted_by(f64::total_cmp)
.collect::<Vec<_>>();
Self::new_iqr_from_sorted(&sorted_values)
}
pub fn new_iqr_from_sorted(values: &[f64]) -> Self {
let min = quartile_from_sorted(values, 0.0);
let lower_quartile = quartile_from_sorted(values, 1.0 / 4.0);
let median = quartile_from_sorted(values, 1.0 / 2.0);
let upper_quartile = quartile_from_sorted(values, 3.0 / 4.0);
let max = quartile_from_sorted(values, 1.0);
let iqr = upper_quartile - lower_quartile;
let lower_whisker = (lower_quartile - 1.5 * iqr).max(min);
let upper_whisker = (upper_quartile + 1.5 * iqr).min(max);
Self::new(lower_whisker, lower_quartile, median, upper_quartile, upper_whisker)
}
pub fn values(&self) -> [f64; 5] {
[
self.lower_whisker,
self.lower_quartile,
self.median,
self.upper_quartile,
self.upper_whisker
]
}
}
fn quartile_from_sorted(values: &[f64], t: f64) -> f64 {
let p = t * (values.len() - 1) as f64;
let a = values[p.floor() as usize];
let b = values[p.ceil() as usize];
lerp(a, b, p - p.floor())
}
fn lerp(a: f64, b: f64, t: f64) -> f64 {
a + (b - a) * t
}
#[cfg(test)]
mod tests {
use approx::assert_abs_diff_eq;
use super::*;
#[test]
#[should_panic]
fn test_min_max_panic() {
Quartiles::new_min_max(&[]);
}
#[test]
#[should_panic]
fn test_iqr_panic() {
Quartiles::new_iqr(&[]);
}
#[test]
fn test_0_and_1_min_max() {
let quartiles = Quartiles::new_min_max_from_sorted(&[0.0, 1.0]);
assert_abs_diff_eq!(quartiles.lower_whisker, 0.0);
assert_abs_diff_eq!(quartiles.lower_quartile, 0.25);
assert_abs_diff_eq!(quartiles.median, 0.5);
assert_abs_diff_eq!(quartiles.upper_quartile, 0.75);
assert_abs_diff_eq!(quartiles.upper_whisker, 1.0);
}
#[test]
fn test_0_and_1_iqr() {
let quartiles = Quartiles::new_iqr_from_sorted(&[0.0, 1.0]);
assert_abs_diff_eq!(quartiles.lower_whisker, 0.0);
assert_abs_diff_eq!(quartiles.lower_quartile, 0.25);
assert_abs_diff_eq!(quartiles.median, 0.5);
assert_abs_diff_eq!(quartiles.upper_quartile, 0.75);
assert_abs_diff_eq!(quartiles.upper_whisker, 1.0);
}
#[test]
fn test_iqr_5_points() {
let quartiles = Quartiles::new_iqr_from_sorted(&[0.0, 0.4, 0.5, 0.6, 1.0]);
assert_abs_diff_eq!(quartiles.lower_whisker, 0.1);
assert_abs_diff_eq!(quartiles.lower_quartile, 0.4);
assert_abs_diff_eq!(quartiles.median, 0.5);
assert_abs_diff_eq!(quartiles.upper_quartile, 0.6);
assert_abs_diff_eq!(quartiles.upper_whisker, 0.9);
}
}