rsomics_quantile_transform/
transform.rs1use crate::ndtri::{CLIP_MAX, CLIP_MIN, ndtri};
17
18const BOUNDS_THRESHOLD: f64 = 1e-7;
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum OutputDistribution {
23 Uniform,
24 Normal,
25}
26
27fn np_interp(x: f64, xp: &[f64], fp: &[f64]) -> f64 {
34 debug_assert_eq!(xp.len(), fp.len());
35 let n = xp.len();
36 if x <= xp[0] {
37 return fp[0];
38 }
39 if x >= xp[n - 1] {
40 return fp[n - 1];
41 }
42 let idx = xp.partition_point(|&v| v <= x);
43 let i = idx - 1;
44 let slope = (fp[i + 1] - fp[i]) / (xp[i + 1] - xp[i]);
45 slope.mul_add(x - xp[i], fp[i])
47}
48
49pub fn transform_col(
51 col: &mut [f64],
52 quantiles: &[f64],
53 references: &[f64],
54 dist: OutputDistribution,
55) {
56 let lower_bound_x = quantiles[0];
57 let upper_bound_x = quantiles[quantiles.len() - 1];
58
59 let q_rev: Vec<f64> = quantiles.iter().rev().map(|&v| -v).collect();
61 let r_rev: Vec<f64> = references.iter().rev().map(|&v| -v).collect();
62
63 for v in col.iter_mut() {
64 if v.is_nan() {
65 continue;
66 }
67 let x = *v;
68
69 let (at_lower, at_upper) = match dist {
70 OutputDistribution::Uniform => (x == lower_bound_x, x == upper_bound_x),
71 OutputDistribution::Normal => (
72 x - BOUNDS_THRESHOLD < lower_bound_x,
73 x + BOUNDS_THRESHOLD > upper_bound_x,
74 ),
75 };
76
77 let fwd = np_interp(x, quantiles, references);
79 let rev = -np_interp(-x, &q_rev, &r_rev);
80 let mut y = 0.5 * (fwd + rev);
81
82 if at_upper {
84 y = 1.0;
85 }
86 if at_lower {
87 y = 0.0;
88 }
89
90 if dist == OutputDistribution::Normal {
91 y = ndtri(y).clamp(CLIP_MIN, CLIP_MAX);
92 }
93
94 *v = y;
95 }
96}
97
98pub fn transform_matrix(
100 data: &mut [f64],
101 n_rows: usize,
102 n_cols: usize,
103 quantiles_per_col: &[Vec<f64>],
104 references: &[f64],
105 dist: OutputDistribution,
106) {
107 for j in 0..n_cols {
109 let mut col: Vec<f64> = (0..n_rows).map(|i| data[i * n_cols + j]).collect();
110 transform_col(&mut col, &quantiles_per_col[j], references, dist);
111 for (i, v) in col.into_iter().enumerate() {
112 data[i * n_cols + j] = v;
113 }
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 fn close(a: f64, b: f64) {
122 assert!(
123 (a - b).abs() < 1e-12,
124 "got={a} want={b} diff={}",
125 (a - b).abs()
126 );
127 }
128
129 #[test]
130 fn np_interp_basic() {
131 let xp = [0.0, 1.0, 2.0];
132 let fp = [0.0, 0.5, 1.0];
133 close(np_interp(0.5, &xp, &fp), 0.25);
134 close(np_interp(0.0, &xp, &fp), 0.0);
135 close(np_interp(2.0, &xp, &fp), 1.0);
136 close(np_interp(-1.0, &xp, &fp), 0.0); close(np_interp(3.0, &xp, &fp), 1.0); }
139
140 #[test]
141 fn uniform_ties_average() {
142 let quantiles = [1.0, 2.0, 2.0, 2.0, 3.0];
150 let refs = [0.0, 0.25, 0.5, 0.75, 1.0];
151 let mut col = [2.0];
152 transform_col(&mut col, &quantiles, &refs, OutputDistribution::Uniform);
153 close(col[0], 0.5);
154 }
155
156 #[test]
157 fn boundary_forced_to_exact() {
158 let quantiles = [1.0, 2.0, 3.0];
159 let refs = [0.0, 0.5, 1.0];
160 let mut col = [1.0, 3.0];
161 transform_col(&mut col, &quantiles, &refs, OutputDistribution::Uniform);
162 assert_eq!(col[0], 0.0);
163 assert_eq!(col[1], 1.0);
164 }
165}