Skip to main content

box_plotters/
quartiles.rs

1use itertools::Itertools;
2
3/// Stores the whisker positions, quartiles and median.
4#[derive(Clone, Copy, Debug)]
5pub struct Quartiles {
6    pub lower_whisker: f64,
7    pub lower_quartile: f64,
8    pub median: f64,
9    pub upper_quartile: f64,
10    pub upper_whisker: f64,
11}
12
13impl Quartiles {
14    /// Constructs a new [Quartiles] instance.
15    pub fn new(
16        lower_whisker: f64,
17        lower_quartile: f64,
18        median: f64,
19        upper_quartile: f64,
20        upper_whisker: f64,
21    ) -> Self {
22        Self { 
23            lower_whisker, 
24            lower_quartile, 
25            median, 
26            upper_quartile, 
27            upper_whisker 
28        }
29    }
30
31    /// Constructs a new [Quartiles] instance 
32    /// where whiskers are calculated as the minimum and maximum value.
33    /// Use [Quartiles::new_min_max_from_sorted] if the data is already sorted.
34    pub fn new_min_max(values: &[f64]) -> Self {
35        let sorted_values = values
36            .iter()
37            .cloned()
38            .sorted_by(f64::total_cmp)
39            .collect::<Vec<_>>();
40        Self::new_min_max_from_sorted(&sorted_values)
41    }
42
43    /// Constructs a new [Quartiles] instance
44    /// where whiskers are calculated as the minimum and maximum value.
45    pub fn new_min_max_from_sorted(values: &[f64]) -> Self {
46        let lower_whisker = quartile_from_sorted(values, 0.0);
47        let lower_quartile = quartile_from_sorted(values, 1.0 / 4.0);
48        let median = quartile_from_sorted(values, 1.0 / 2.0);
49        let upper_quartile = quartile_from_sorted(values, 3.0 / 4.0);
50        let upper_whisker = quartile_from_sorted(values, 1.0);
51        Self::new(lower_whisker, lower_quartile, median, upper_quartile, upper_whisker)
52    }
53
54    /// Constructs a new [Quartiles] instance
55    /// where whiskers are calculated using interquartile range
56    /// (https://en.wikipedia.org/wiki/Interquartile_range).
57    /// Use [Quartiles::new_iqr_from_sorted] if the data is already sorted.
58    pub fn new_iqr(values: &[f64]) -> Self {
59        let sorted_values = values
60            .iter()
61            .cloned()
62            .sorted_by(f64::total_cmp)
63            .collect::<Vec<_>>();
64        Self::new_iqr_from_sorted(&sorted_values)
65    }
66
67    /// Constructs a new [Quartiles] instance from sorted data.
68    /// where whiskers are calculated using interquartile range
69    /// (https://en.wikipedia.org/wiki/Interquartile_range).
70    pub fn new_iqr_from_sorted(values: &[f64]) -> Self {
71        let min = quartile_from_sorted(values, 0.0);
72        let lower_quartile = quartile_from_sorted(values, 1.0 / 4.0);
73        let median = quartile_from_sorted(values, 1.0 / 2.0);
74        let upper_quartile = quartile_from_sorted(values, 3.0 / 4.0);
75        let max = quartile_from_sorted(values, 1.0);
76        let iqr = upper_quartile - lower_quartile;
77        let lower_whisker = (lower_quartile - 1.5 * iqr).max(min);
78        let upper_whisker = (upper_quartile + 1.5 * iqr).min(max);
79        Self::new(lower_whisker, lower_quartile, median, upper_quartile, upper_whisker)
80    }
81
82    /// Returns the fields as an array.
83    pub fn values(&self) -> [f64; 5] {
84        [
85            self.lower_whisker,
86            self.lower_quartile,
87            self.median,
88            self.upper_quartile,
89            self.upper_whisker
90        ]
91    }
92}
93
94fn quartile_from_sorted(values: &[f64], t: f64) -> f64 {
95    let p = t * (values.len() - 1) as f64;
96    let a = values[p.floor() as usize];
97    let b = values[p.ceil() as usize];
98    lerp(a, b, p - p.floor())
99}
100
101fn lerp(a: f64, b: f64, t: f64) -> f64 {
102    a + (b - a) * t
103}
104
105#[cfg(test)]
106mod tests {
107    use approx::assert_abs_diff_eq;
108
109    use super::*;
110
111    #[test]
112    #[should_panic]
113    fn test_min_max_panic() {
114        Quartiles::new_min_max(&[]);
115    }
116
117    #[test]
118    #[should_panic]
119    fn test_iqr_panic() {
120        Quartiles::new_iqr(&[]);
121    }
122
123    #[test]
124    fn test_0_and_1_min_max() {
125        let quartiles = Quartiles::new_min_max_from_sorted(&[0.0, 1.0]);
126        assert_abs_diff_eq!(quartiles.lower_whisker, 0.0);
127        assert_abs_diff_eq!(quartiles.lower_quartile, 0.25);
128        assert_abs_diff_eq!(quartiles.median, 0.5);
129        assert_abs_diff_eq!(quartiles.upper_quartile, 0.75);
130        assert_abs_diff_eq!(quartiles.upper_whisker, 1.0);
131    }
132
133    #[test]
134    fn test_0_and_1_iqr() {
135        let quartiles = Quartiles::new_iqr_from_sorted(&[0.0, 1.0]);
136        assert_abs_diff_eq!(quartiles.lower_whisker, 0.0);
137        assert_abs_diff_eq!(quartiles.lower_quartile, 0.25);
138        assert_abs_diff_eq!(quartiles.median, 0.5);
139        assert_abs_diff_eq!(quartiles.upper_quartile, 0.75);
140        assert_abs_diff_eq!(quartiles.upper_whisker, 1.0);
141    }
142
143    #[test]
144    fn test_iqr_5_points() {
145        let quartiles = Quartiles::new_iqr_from_sorted(&[0.0, 0.4, 0.5, 0.6, 1.0]);
146        assert_abs_diff_eq!(quartiles.lower_whisker, 0.1);
147        assert_abs_diff_eq!(quartiles.lower_quartile, 0.4);
148        assert_abs_diff_eq!(quartiles.median, 0.5);
149        assert_abs_diff_eq!(quartiles.upper_quartile, 0.6);
150        assert_abs_diff_eq!(quartiles.upper_whisker, 0.9);
151    }
152}