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 references: Vec<f64> = if n_quantiles == 1 {
117 vec![0.0]
118 } else {
119 let step = 1.0 / (n_quantiles - 1) as f64;
120 (0..n_quantiles)
121 .map(|i| {
122 if i == n_quantiles - 1 {
123 1.0
124 } else {
125 i as f64 * step
126 }
127 })
128 .collect()
129 };
130
131 let shared_indices: Option<Vec<usize>> = subsample.and_then(|k| {
133 if n_rows > k {
134 let mut rng = Mt19937::seed(seed as u32);
135 let all = shuffle_arange(&mut rng, n_rows);
136 Some(all[..k].to_vec())
137 } else {
138 None
139 }
140 });
141
142 let quantiles: Vec<Vec<f64>> = (0..n_cols)
143 .map(|j| {
144 let col: Vec<f64> = match &shared_indices {
145 Some(idxs) => idxs.iter().map(|&i| data[i * n_cols + j]).collect(),
146 None => (0..n_rows).map(|i| data[i * n_cols + j]).collect(),
147 };
148 fit_column(&col, &references)
149 })
150 .collect();
151
152 (references, quantiles)
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 fn close(a: f64, b: f64) {
160 assert!((a - b).abs() < 1e-12, "{a} != {b}");
161 }
162
163 #[test]
164 fn nanpercentile_type7() {
165 let c = [1.0, 2.0, 3.0, 4.0, 5.0];
166 close(nanpercentile(&c, 25.0), 2.0);
167 close(nanpercentile(&c, 50.0), 3.0);
168 close(nanpercentile(&c, 75.0), 4.0);
169 close(nanpercentile(&c, 10.0), 1.4);
170 close(nanpercentile(&c, 0.0), 1.0);
171 close(nanpercentile(&c, 100.0), 5.0);
172 }
173
174 #[test]
175 fn nanpercentile_unsorted() {
176 close(nanpercentile(&[7.0, 3.0, 9.0, 1.0, 5.0], 10.0), 1.8);
177 close(nanpercentile(&[7.0, 3.0, 9.0, 1.0, 5.0], 75.0), 7.0);
178 }
179
180 #[test]
181 fn fit_column_basic() {
182 let refs: Vec<f64> = (0..5).map(|i| i as f64 / 4.0).collect();
185 let q = fit_column(&[1.0, 2.0, 3.0, 4.0, 5.0], &refs);
186 for (got, want) in q.iter().zip(&[1.0, 2.0, 3.0, 4.0, 5.0]) {
187 close(*got, *want);
188 }
189 }
190
191 #[test]
192 fn monotonic_accumulate() {
193 let refs = vec![0.0, 0.5, 1.0];
194 let q = fit_column(&[5.0, 5.0, 5.0], &refs);
195 assert!(q.windows(2).all(|w| w[0] <= w[1]));
196 }
197}