legume_numeric/matrix/
running_quantile.rs1#[derive(Clone, Debug)]
16pub struct P2Quantile {
17 p: f64,
18 init: Vec<f64>,
20 q: [f64; 5], n: [f64; 5], np: [f64; 5], dn: [f64; 5], count: usize,
25}
26
27impl P2Quantile {
28 #[must_use]
29 pub fn new(p: f64) -> Self {
30 let p = p.clamp(0.0, 1.0);
31 Self {
32 p,
33 init: Vec::with_capacity(5),
34 q: [0.0; 5],
35 n: [1.0, 2.0, 3.0, 4.0, 5.0],
36 np: [1.0, 1.0 + 2.0 * p, 1.0 + 4.0 * p, 3.0 + 2.0 * p, 5.0],
37 dn: [0.0, p / 2.0, p, (1.0 + p) / 2.0, 1.0],
38 count: 0,
39 }
40 }
41
42 pub fn add(&mut self, x: f64) {
43 self.count += 1;
44 if self.count <= 5 {
45 self.init.push(x);
46 if self.count == 5 {
47 self.init.sort_by(f64::total_cmp);
48 self.q.copy_from_slice(&self.init);
49 }
50 return;
51 }
52 let k = if x < self.q[0] {
54 self.q[0] = x;
55 0
56 } else if x >= self.q[4] {
57 self.q[4] = x;
58 3
59 } else {
60 (0..4)
61 .find(|&i| self.q[i] <= x && x < self.q[i + 1])
62 .unwrap_or(3)
63 };
64 for i in (k + 1)..5 {
66 self.n[i] += 1.0;
67 }
68 for i in 0..5 {
70 self.np[i] += self.dn[i];
71 }
72 for i in 1..4 {
75 let d = self.np[i] - self.n[i];
76 let go_up = d >= 1.0 && (self.n[i + 1] - self.n[i]) > 1.0;
77 let go_dn = d <= -1.0 && (self.n[i - 1] - self.n[i]) < -1.0;
78 if go_up || go_dn {
79 let ds = if d >= 0.0 { 1.0 } else { -1.0 };
80 let qp = self.parabolic(i, ds);
81 self.q[i] = if self.q[i - 1] < qp && qp < self.q[i + 1] {
82 qp
83 } else {
84 self.linear(i, ds)
85 };
86 self.n[i] += ds;
87 }
88 }
89 }
90
91 fn parabolic(&self, i: usize, d: f64) -> f64 {
92 let (q, n) = (&self.q, &self.n);
93 q[i] + d / (n[i + 1] - n[i - 1])
94 * ((n[i] - n[i - 1] + d) * (q[i + 1] - q[i]) / (n[i + 1] - n[i])
95 + (n[i + 1] - n[i] - d) * (q[i] - q[i - 1]) / (n[i] - n[i - 1]))
96 }
97
98 fn linear(&self, i: usize, d: f64) -> f64 {
99 let j = if d > 0.0 { i + 1 } else { i - 1 };
100 self.q[i] + d * (self.q[j] - self.q[i]) / (self.n[j] - self.n[i])
101 }
102
103 #[must_use]
105 pub fn quantile(&self) -> f64 {
106 if self.count == 0 {
107 return f64::NAN;
108 }
109 if self.count < 5 {
110 let mut v = self.init.clone();
111 v.sort_by(f64::total_cmp);
112 let idx = (((v.len() - 1) as f64) * self.p).round() as usize;
113 return v[idx.min(v.len() - 1)];
114 }
115 self.q[2]
116 }
117
118 #[must_use]
119 pub fn count(&self) -> usize {
120 self.count
121 }
122}
123
124pub struct RunningQuantiles {
129 nrows: usize,
130 quantiles: Vec<f64>,
131 est: Vec<P2Quantile>, }
133
134impl RunningQuantiles {
135 #[must_use]
136 pub fn new(nrows: usize, quantiles: &[f64]) -> Self {
137 let est = (0..nrows)
138 .flat_map(|_| quantiles.iter().map(|&p| P2Quantile::new(p)))
139 .collect();
140 Self {
141 nrows,
142 quantiles: quantiles.to_vec(),
143 est,
144 }
145 }
146
147 pub fn add_dense_column(&mut self, values: &[f32]) {
149 assert_eq!(values.len(), self.nrows, "column length != nrows");
150 let nq = self.quantiles.len();
151 for (row, &v) in values.iter().enumerate() {
152 for qi in 0..nq {
153 self.est[row * nq + qi].add(f64::from(v));
154 }
155 }
156 }
157
158 #[must_use]
160 pub fn quantile(&self, qi: usize) -> Vec<f32> {
161 let nq = self.quantiles.len();
162 (0..self.nrows)
163 .map(|row| self.est[row * nq + qi].quantile() as f32)
164 .collect()
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 fn perm(m: u64) -> impl Iterator<Item = f64> {
175 (0..m).map(move |i| ((i.wrapping_mul(7919)) % m) as f64)
176 }
177
178 #[test]
179 fn recovers_quantiles_within_tolerance() {
180 let m = 10_007u64;
181 for &p in &[0.05, 0.25, 0.5, 0.75, 0.95] {
182 let mut est = P2Quantile::new(p);
183 for x in perm(m) {
184 est.add(x);
185 }
186 let want = p * m as f64;
187 let got = est.quantile();
188 assert!(
189 (got - want).abs() < 0.02 * m as f64,
190 "p={p}: got {got:.1}, want ≈ {want:.1}"
191 );
192 }
193 }
194
195 #[test]
196 fn running_quantiles_per_row() {
197 let m = 5_003u64;
199 let mut rq = RunningQuantiles::new(2, &[0.5]);
200 for x in perm(m) {
201 rq.add_dense_column(&[x as f32, x as f32 + m as f32]);
202 }
203 let med = rq.quantile(0);
204 assert!((med[0] - 0.5 * m as f32).abs() < 0.02 * m as f32);
205 assert!((med[1] - 1.5 * m as f32).abs() < 0.02 * m as f32);
206 }
207
208 #[test]
209 fn handles_fewer_than_five() {
210 let mut est = P2Quantile::new(0.5);
211 est.add(3.0);
212 est.add(1.0);
213 est.add(2.0);
214 assert!((est.quantile() - 2.0).abs() < 1e-9);
215 }
216}