Skip to main content

gam_math/
quantile.rs

1/// Linear-interpolation quantile matching numpy.quantile default (method='linear').
2pub fn quantile_from_sorted(sorted: &[f64], q: f64) -> f64 {
3    let n = sorted.len();
4    if n == 0 {
5        return f64::NAN;
6    }
7    if n == 1 {
8        return sorted[0];
9    }
10    let pos = q.clamp(0.0, 1.0) * (n - 1) as f64;
11    let lo = pos.floor() as usize;
12    let hi = (lo + 1).min(n - 1);
13    let frac = pos - lo as f64;
14    sorted[lo] * (1.0 - frac) + sorted[hi] * frac
15}
16
17/// Exact 1-based order statistic from an already sorted slice.
18///
19/// Returns `NaN` when the slice is empty, `rank` is zero, or `rank` exceeds
20/// the number of observations. This is intentionally not an interpolating
21/// quantile: split-conformal calibration needs the observed `k`-th value to
22/// preserve the finite-sample coverage proof.
23pub fn order_statistic_from_sorted(sorted: &[f64], rank: usize) -> f64 {
24    if sorted.is_empty() || rank == 0 || rank > sorted.len() {
25        return f64::NAN;
26    }
27    sorted[rank - 1]
28}
29
30/// Exact 1-based order statistic from an unsorted slice.
31///
32/// This centralizes the sort+select path for code that needs an observed
33/// sample value rather than `quantile_from_sorted`'s linear interpolation.
34pub fn order_statistic(values: &[f64], rank: usize) -> f64 {
35    let mut sorted = values.to_vec();
36    sorted.sort_by(f64::total_cmp);
37    order_statistic_from_sorted(&sorted, rank)
38}
39
40#[cfg(test)]
41mod tests {
42    use super::*;
43
44    #[test]
45    fn order_statistic_returns_nan_on_empty() {
46        assert!(order_statistic(&[], 1).is_nan());
47        assert!(order_statistic_from_sorted(&[], 1).is_nan());
48    }
49
50    #[test]
51    fn order_statistic_returns_nan_on_zero_rank() {
52        let v = [3.0, 1.0, 2.0];
53        assert!(order_statistic(&v, 0).is_nan());
54        let sorted = [1.0, 2.0, 3.0];
55        assert!(order_statistic_from_sorted(&sorted, 0).is_nan());
56    }
57
58    #[test]
59    fn order_statistic_returns_nan_when_rank_exceeds_len() {
60        let v = [3.0, 1.0, 2.0];
61        assert!(order_statistic(&v, 4).is_nan());
62        let sorted = [1.0, 2.0, 3.0];
63        assert!(order_statistic_from_sorted(&sorted, 4).is_nan());
64    }
65
66    #[test]
67    fn order_statistic_hits_mid_rank() {
68        // Unsorted input; the 1-based 3rd-smallest of {1,2,3,4,5} is 3.
69        let v = [5.0, 1.0, 4.0, 2.0, 3.0];
70        assert_eq!(order_statistic(&v, 3), 3.0);
71        let sorted = [1.0, 2.0, 3.0, 4.0, 5.0];
72        assert_eq!(order_statistic_from_sorted(&sorted, 3), 3.0);
73        // Boundary ranks: first and last.
74        assert_eq!(order_statistic(&v, 1), 1.0);
75        assert_eq!(order_statistic(&v, 5), 5.0);
76    }
77}