1#[cfg(not(feature = "std"))]
7use crate::math::FloatExt;
8use crate::math::{f64_usize, u64_f64, usize_f64};
9
10#[inline]
18#[must_use]
19pub fn percentile_rank(sorted_data: &[f64], value: f64) -> f64 {
20 if sorted_data.is_empty() {
21 return 0.0;
22 }
23 let mut count_below = 0usize;
24 let mut count_equal = 0usize;
25 for &v in sorted_data {
26 if v < value {
27 count_below += 1;
28 } else if (v - value).abs() < f64::EPSILON {
29 count_equal += 1;
30 }
31 }
32 let n = usize_f64(sorted_data.len());
34 0.5f64.mul_add(usize_f64(count_equal), usize_f64(count_below)) / n * 100.0
35}
36
37#[derive(Clone, Copy, Debug)]
43pub struct IqrResult {
44 pub q1: f64,
46 pub median: f64,
48 pub q3: f64,
50 pub iqr: f64,
52 pub lower_fence: f64,
54 pub upper_fence: f64,
56}
57
58#[must_use]
62pub fn iqr(sorted_data: &[f64]) -> Option<IqrResult> {
63 let n = sorted_data.len();
64 if n < 4 {
65 return None;
66 }
67
68 let q1 = quantile_sorted(sorted_data, 0.25);
69 let median = quantile_sorted(sorted_data, 0.50);
70 let q3 = quantile_sorted(sorted_data, 0.75);
71 let iqr_val = q3 - q1;
72
73 Some(IqrResult {
74 q1,
75 median,
76 q3,
77 iqr: iqr_val,
78 lower_fence: 1.5f64.mul_add(-iqr_val, q1),
79 upper_fence: 1.5f64.mul_add(iqr_val, q3),
80 })
81}
82
83#[inline]
85#[must_use]
86pub fn quantile_sorted(sorted_data: &[f64], q: f64) -> f64 {
87 let n = sorted_data.len();
88 if n == 0 {
89 return 0.0;
90 }
91 if n == 1 {
92 return sorted_data[0];
93 }
94 let q = q.clamp(0.0, 1.0);
95 let pos = q * usize_f64(n - 1);
96 let lower = f64_usize(pos.floor());
97 let upper = f64_usize(pos.ceil());
98 if lower == upper {
99 return sorted_data[lower];
100 }
101 let frac = pos - usize_f64(lower);
102 sorted_data[lower].mul_add(1.0 - frac, sorted_data[upper] * frac)
103}
104
105pub struct CovarianceMatrix<const D: usize> {
113 count: u64,
115 means: [f64; D],
117 co_moments: [[f64; D]; D],
119}
120
121impl<const D: usize> CovarianceMatrix<D> {
122 #[must_use]
124 pub const fn new() -> Self {
125 Self {
126 count: 0,
127 means: [0.0; D],
128 co_moments: [[0.0; D]; D],
129 }
130 }
131
132 pub fn observe(&mut self, values: &[f64; D]) {
134 self.count += 1;
135 let n = u64_f64(self.count);
136
137 let mut dx = [0.0; D];
138 for (i, d) in dx.iter_mut().enumerate() {
139 *d = values[i] - self.means[i];
140 }
141
142 for (i, mean) in self.means.iter_mut().enumerate() {
144 *mean += dx[i] / n;
145 }
146
147 for (i, (&val, &mean_i)) in values.iter().zip(self.means.iter()).enumerate() {
149 let dx2_i = val - mean_i;
150 for (j, dx_j) in dx.iter().enumerate().skip(i) {
151 self.co_moments[i][j] += dx_j * dx2_i;
152 }
153 }
154 }
155
156 #[must_use]
158 pub fn covariance(&self, i: usize, j: usize) -> f64 {
159 if self.count < 2 || i >= D || j >= D {
160 return 0.0;
161 }
162 let (r, c) = if i <= j { (i, j) } else { (j, i) };
163 self.co_moments[r][c] / u64_f64(self.count - 1)
164 }
165
166 #[must_use]
168 pub fn correlation(&self, i: usize, j: usize) -> f64 {
169 if i == j {
170 return 1.0;
171 }
172 let cov = self.covariance(i, j);
173 let var_i = self.covariance(i, i);
174 let var_j = self.covariance(j, j);
175 let denom = (var_i * var_j).sqrt();
176 if denom < f64::EPSILON {
177 return 0.0;
178 }
179 (cov / denom).clamp(-1.0, 1.0)
180 }
181
182 #[inline]
184 #[must_use]
185 pub fn variance(&self, i: usize) -> f64 {
186 self.covariance(i, i)
187 }
188
189 #[inline]
191 #[must_use]
192 pub const fn mean(&self, i: usize) -> f64 {
193 if i < D {
194 self.means[i]
195 } else {
196 0.0
197 }
198 }
199
200 #[inline]
202 #[must_use]
203 pub const fn count(&self) -> u64 {
204 self.count
205 }
206}
207
208impl<const D: usize> Default for CovarianceMatrix<D> {
209 fn default() -> Self {
210 Self::new()
211 }
212}
213
214#[derive(Clone, Debug)]
222pub struct StreamingStats {
223 count: u64,
224 mean: f64,
225 m2: f64,
226 m3: f64,
227 m4: f64,
228 min: f64,
229 max: f64,
230}
231
232impl StreamingStats {
233 #[must_use]
235 pub const fn new() -> Self {
236 Self {
237 count: 0,
238 mean: 0.0,
239 m2: 0.0,
240 m3: 0.0,
241 m4: 0.0,
242 min: f64::INFINITY,
243 max: f64::NEG_INFINITY,
244 }
245 }
246
247 pub fn observe(&mut self, value: f64) {
249 let n1 = self.count;
250 self.count += 1;
251 let n = u64_f64(self.count);
252
253 let delta = value - self.mean;
254 let delta_n = delta / n;
255 let delta_n2 = delta_n * delta_n;
256 let term1 = delta * delta_n * u64_f64(n1);
257
258 self.mean += delta_n;
259
260 self.m4 += (4.0 * delta_n).mul_add(
262 -self.m3,
263 (term1 * delta_n2).mul_add(n.mul_add(n, -(3.0 * n)) + 3.0, 6.0 * delta_n2 * self.m2),
264 );
265 self.m3 += (term1 * delta_n).mul_add(n - 2.0, -(3.0 * delta_n * self.m2));
266 self.m2 += term1;
267
268 if value < self.min {
269 self.min = value;
270 }
271 if value > self.max {
272 self.max = value;
273 }
274 }
275
276 #[inline]
278 #[must_use]
279 pub const fn count(&self) -> u64 {
280 self.count
281 }
282
283 #[inline]
285 #[must_use]
286 pub const fn mean(&self) -> f64 {
287 self.mean
288 }
289
290 #[must_use]
292 pub fn variance(&self) -> f64 {
293 if self.count < 2 {
294 return 0.0;
295 }
296 self.m2 / u64_f64(self.count)
297 }
298
299 #[must_use]
301 pub fn sample_variance(&self) -> f64 {
302 if self.count < 2 {
303 return 0.0;
304 }
305 self.m2 / u64_f64(self.count - 1)
306 }
307
308 #[inline]
310 #[must_use]
311 pub fn std_dev(&self) -> f64 {
312 self.variance().sqrt()
313 }
314
315 #[must_use]
317 pub fn skewness(&self) -> f64 {
318 if self.count < 3 || self.m2 < f64::EPSILON {
319 return 0.0;
320 }
321 let n = u64_f64(self.count);
322 n.sqrt() * self.m3 / (self.m2 * self.m2.sqrt())
324 }
325
326 #[must_use]
328 pub fn kurtosis(&self) -> f64 {
329 if self.count < 4 || self.m2 < f64::EPSILON {
330 return 0.0;
331 }
332 let n = u64_f64(self.count);
333 n * self.m4 / (self.m2 * self.m2) - 3.0
334 }
335
336 #[inline]
338 #[must_use]
339 pub const fn min(&self) -> f64 {
340 self.min
341 }
342
343 #[inline]
345 #[must_use]
346 pub const fn max(&self) -> f64 {
347 self.max
348 }
349
350 pub const fn reset(&mut self) {
352 self.count = 0;
353 self.mean = 0.0;
354 self.m2 = 0.0;
355 self.m3 = 0.0;
356 self.m4 = 0.0;
357 self.min = f64::INFINITY;
358 self.max = f64::NEG_INFINITY;
359 }
360}
361
362impl Default for StreamingStats {
363 fn default() -> Self {
364 Self::new()
365 }
366}
367
368#[cfg(test)]
373#[allow(clippy::float_cmp)]
374mod tests {
375 use super::*;
376
377 #[test]
378 fn percentile_rank_basic() {
379 let data = [1.0, 2.0, 3.0, 4.0, 5.0];
380 let rank = percentile_rank(&data, 3.0);
381 assert!((rank - 50.0).abs() < 1e-6);
383 }
384
385 #[test]
386 fn percentile_rank_empty() {
387 assert!((percentile_rank(&[], 5.0) - 0.0).abs() < 1e-6);
388 }
389
390 #[test]
391 fn percentile_rank_below_all() {
392 let data = [10.0, 20.0, 30.0];
393 let rank = percentile_rank(&data, 1.0);
394 assert!((rank - 0.0).abs() < 1e-6);
395 }
396
397 #[test]
398 fn percentile_rank_above_all() {
399 let data = [10.0, 20.0, 30.0];
400 let rank = percentile_rank(&data, 50.0);
401 assert!((rank - 100.0).abs() < 1e-6);
402 }
403
404 #[test]
405 fn iqr_basic() {
406 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
408 let result = iqr(&data).unwrap();
409 assert!((result.q1 - 2.75).abs() < 1e-6);
411 assert!((result.median - 4.5).abs() < 1e-6);
412 assert!((result.q3 - 6.25).abs() < 1e-6);
413 assert!((result.iqr - 3.5).abs() < 1e-6);
414 }
415
416 #[test]
417 fn iqr_too_small() {
418 let data = [1.0, 2.0, 3.0];
419 assert!(iqr(&data).is_none());
420 }
421
422 #[test]
423 fn iqr_fences() {
424 let data = [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0];
425 let result = iqr(&data).unwrap();
426 assert!(result.lower_fence < result.q1);
427 assert!(result.upper_fence > result.q3);
428 }
429
430 #[test]
431 fn quantile_sorted_edges() {
432 let data = [10.0, 20.0, 30.0];
433 assert!((quantile_sorted(&data, 0.0) - 10.0).abs() < 1e-6);
434 assert!((quantile_sorted(&data, 1.0) - 30.0).abs() < 1e-6);
435 assert!((quantile_sorted(&data, 0.5) - 20.0).abs() < 1e-6);
436 }
437
438 #[test]
439 fn covariance_matrix_basic() {
440 let mut cov = CovarianceMatrix::<2>::new();
441 for i in 0..100 {
443 let v = f64::from(i);
444 cov.observe(&[v, v]);
445 }
446 let r = cov.correlation(0, 1);
447 assert!((r - 1.0).abs() < 1e-6);
448 }
449
450 #[test]
451 fn covariance_matrix_negative_correlation() {
452 let mut cov = CovarianceMatrix::<2>::new();
453 for i in 0..100 {
455 let v = f64::from(i);
456 cov.observe(&[v, -v]);
457 }
458 let r = cov.correlation(0, 1);
459 assert!((r - (-1.0)).abs() < 1e-6);
460 }
461
462 #[test]
463 fn covariance_matrix_self_correlation() {
464 let cov = CovarianceMatrix::<3>::new();
465 assert!((cov.correlation(0, 0) - 1.0).abs() < 1e-6);
467 }
468
469 #[test]
470 fn covariance_matrix_variance() {
471 let mut cov = CovarianceMatrix::<2>::new();
472 for i in 0..100 {
474 cov.observe(&[f64::from(i), 0.0]);
475 }
476 let var = cov.variance(0);
477 assert!(var > 800.0 && var < 870.0, "var = {var}");
480 }
481
482 #[test]
483 fn streaming_stats_basic() {
484 let mut s = StreamingStats::new();
485 for v in [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0] {
486 s.observe(v);
487 }
488 assert_eq!(s.count(), 8);
489 assert!((s.mean() - 5.0).abs() < 1e-6);
490 assert!((s.min() - 2.0).abs() < 1e-6);
491 assert!((s.max() - 9.0).abs() < 1e-6);
492 }
493
494 #[test]
495 fn streaming_stats_variance() {
496 let mut s = StreamingStats::new();
497 for v in [2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0] {
498 s.observe(v);
499 }
500 assert!((s.variance() - 4.0).abs() < 1e-6);
502 }
503
504 #[test]
505 fn streaming_stats_empty() {
506 let s = StreamingStats::new();
507 assert_eq!(s.count(), 0);
508 assert!((s.mean() - 0.0).abs() < 1e-6);
509 assert!((s.variance() - 0.0).abs() < 1e-6);
510 assert!((s.skewness() - 0.0).abs() < 1e-6);
511 }
512
513 #[test]
514 fn streaming_stats_reset() {
515 let mut s = StreamingStats::new();
516 s.observe(10.0);
517 s.observe(20.0);
518 s.reset();
519 assert_eq!(s.count(), 0);
520 }
521
522 #[test]
523 fn streaming_stats_skewness_symmetric() {
524 let mut s = StreamingStats::new();
525 for v in [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0] {
527 s.observe(v);
528 }
529 assert!(s.skewness().abs() < 0.1);
530 }
531
532 #[test]
533 fn streaming_stats_kurtosis_uniform() {
534 let mut s = StreamingStats::new();
535 for i in 0..1000 {
537 s.observe(f64::from(i));
538 }
539 assert!(s.kurtosis() < 0.0);
540 }
541
542 #[test]
543 fn streaming_stats_default() {
544 let s = StreamingStats::default();
545 assert_eq!(s.count(), 0);
546 }
547
548 #[test]
549 fn covariance_matrix_default() {
550 let cov = CovarianceMatrix::<4>::default();
551 assert_eq!(cov.count(), 0);
552 }
553}