rsomics_quantile_transform/
quantile.rs1use crate::rng::{Mt19937, shuffle_arange};
15
16#[cfg(test)]
18fn quantile_type7(xs: &mut [f64], p: f64) -> f64 {
19 let n = xs.len();
20 if n == 1 {
21 return xs[0];
22 }
23 let h = (n as f64 - 1.0) * p;
24 let lo = h.floor() as usize;
25 let frac = h - lo as f64;
26 if lo + 1 >= n {
27 return *xs.select_nth_unstable_by(n - 1, f64::total_cmp).1;
28 }
29 let (_, &mut kth, right) = xs.select_nth_unstable_by(lo, f64::total_cmp);
30 let next = right.iter().copied().fold(f64::INFINITY, f64::min);
31 kth + frac * (next - kth)
32}
33
34#[cfg(test)]
37fn nanpercentile(col: &[f64], q: f64) -> f64 {
38 let mut v: Vec<f64> = col.iter().copied().filter(|x| !x.is_nan()).collect();
39 if v.is_empty() {
40 return f64::NAN;
41 }
42 quantile_type7(&mut v, q / 100.0)
43}
44
45fn nanpercentile_all(col: &[f64], references: &[f64]) -> Vec<f64> {
54 let mut v: Vec<f64> = col.iter().copied().filter(|x| !x.is_nan()).collect();
55 if v.is_empty() {
56 return vec![f64::NAN; references.len()];
57 }
58 v.sort_unstable_by(f64::total_cmp);
59 let n = v.len();
60 let nm1 = (n - 1) as f64;
61 references
62 .iter()
63 .map(|&r| {
64 let q = r * 100.0;
68 let h = nm1 * (q / 100.0);
69 let lo = h.floor() as usize;
70 let frac = h - lo as f64;
71 if lo + 1 >= n {
72 return v[n - 1];
73 }
74 v[lo] + frac * (v[lo + 1] - v[lo])
75 })
76 .collect()
77}
78
79fn fit_column(col: &[f64], references: &[f64]) -> Vec<f64> {
82 let mut q = nanpercentile_all(col, references);
83
84 let mut running_max = f64::NEG_INFINITY;
86 for v in &mut q {
87 if !v.is_nan() {
88 if *v < running_max {
89 *v = running_max;
90 } else {
91 running_max = *v;
92 }
93 }
94 }
95
96 q
97}
98
99pub fn fit_quantiles(
104 data: &[f64],
105 n_rows: usize,
106 n_cols: usize,
107 n_quantiles_req: usize,
108 subsample: Option<usize>,
109 seed: u64,
110) -> (Vec<f64>, Vec<Vec<f64>>) {
111 let n_quantiles = n_quantiles_req.min(n_rows).max(1);
112 let step = if n_quantiles > 1 {
115 1.0 / (n_quantiles - 1) as f64
116 } else {
117 0.0
118 };
119 let references: Vec<f64> = (0..n_quantiles)
120 .map(|i| {
121 if i == n_quantiles - 1 {
122 1.0
123 } else {
124 i as f64 * step
125 }
126 })
127 .collect();
128
129 let shared_indices: Option<Vec<usize>> = subsample.and_then(|k| {
131 if n_rows > k {
132 let mut rng = Mt19937::seed(seed as u32);
133 let all = shuffle_arange(&mut rng, n_rows);
134 Some(all[..k].to_vec())
135 } else {
136 None
137 }
138 });
139
140 let quantiles: Vec<Vec<f64>> = (0..n_cols)
141 .map(|j| {
142 let col: Vec<f64> = match &shared_indices {
143 Some(idxs) => idxs.iter().map(|&i| data[i * n_cols + j]).collect(),
144 None => (0..n_rows).map(|i| data[i * n_cols + j]).collect(),
145 };
146 fit_column(&col, &references)
147 })
148 .collect();
149
150 (references, quantiles)
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 fn close(a: f64, b: f64) {
158 assert!((a - b).abs() < 1e-12, "{a} != {b}");
159 }
160
161 #[test]
162 fn nanpercentile_type7() {
163 let c = [1.0, 2.0, 3.0, 4.0, 5.0];
164 close(nanpercentile(&c, 25.0), 2.0);
165 close(nanpercentile(&c, 50.0), 3.0);
166 close(nanpercentile(&c, 75.0), 4.0);
167 close(nanpercentile(&c, 10.0), 1.4);
168 close(nanpercentile(&c, 0.0), 1.0);
169 close(nanpercentile(&c, 100.0), 5.0);
170 }
171
172 #[test]
173 fn nanpercentile_unsorted() {
174 close(nanpercentile(&[7.0, 3.0, 9.0, 1.0, 5.0], 10.0), 1.8);
175 close(nanpercentile(&[7.0, 3.0, 9.0, 1.0, 5.0], 75.0), 7.0);
176 }
177
178 #[test]
179 fn fit_column_basic() {
180 let refs: Vec<f64> = (0..5).map(|i| i as f64 / 4.0).collect();
183 let q = fit_column(&[1.0, 2.0, 3.0, 4.0, 5.0], &refs);
184 for (got, want) in q.iter().zip(&[1.0, 2.0, 3.0, 4.0, 5.0]) {
185 close(*got, *want);
186 }
187 }
188
189 #[test]
190 fn monotonic_accumulate() {
191 let refs = vec![0.0, 0.5, 1.0];
192 let q = fit_column(&[5.0, 5.0, 5.0], &refs);
193 assert!(q.windows(2).all(|w| w[0] <= w[1]));
194 }
195}