box_plotters/
quartiles.rs1use itertools::Itertools;
2
3#[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 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 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 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 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 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 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}