1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
5pub enum MultiTestMethod {
6 Bonferroni,
8 FdrBh,
10 Holm,
12}
13
14#[derive(Debug, Clone)]
17pub struct MultiTestResult {
18 pub reject: Vec<bool>,
20 pub pvals_corrected: Vec<f64>,
22}
23
24pub fn multipletests(pvals: &[f64], alpha: f64, method: MultiTestMethod) -> MultiTestResult {
31 let ntests = pvals.len();
32 if ntests == 0 {
33 return MultiTestResult {
34 reject: vec![],
35 pvals_corrected: vec![],
36 };
37 }
38
39 match method {
40 MultiTestMethod::Bonferroni => {
41 let nt = ntests as f64;
43 let alphac_bonf = alpha / nt;
44 let reject = pvals.iter().map(|&p| p <= alphac_bonf).collect();
45 let pvals_corrected = pvals.iter().map(|&p| (p * nt).min(1.0)).collect();
46 MultiTestResult {
47 reject,
48 pvals_corrected,
49 }
50 }
51 MultiTestMethod::Holm => holm(pvals, alpha),
52 MultiTestMethod::FdrBh => fdr_bh(pvals, alpha),
53 }
54}
55
56fn argsort(pvals: &[f64]) -> Vec<usize> {
59 let mut idx: Vec<usize> = (0..pvals.len()).collect();
60 idx.sort_by(|&a, &b| pvals[a].total_cmp(&pvals[b]));
61 idx
62}
63
64fn unsort(sorted: &[f64], sortind: &[usize]) -> Vec<f64> {
66 let mut out = vec![0.0; sorted.len()];
67 for (k, &orig) in sortind.iter().enumerate() {
68 out[orig] = sorted[k];
69 }
70 out
71}
72
73fn unsort_bool(sorted: &[bool], sortind: &[usize]) -> Vec<bool> {
74 let mut out = vec![false; sorted.len()];
75 for (k, &orig) in sortind.iter().enumerate() {
76 out[orig] = sorted[k];
77 }
78 out
79}
80
81fn holm(pvals: &[f64], alpha: f64) -> MultiTestResult {
82 let ntests = pvals.len();
83 let sortind = argsort(pvals);
84 let sorted: Vec<f64> = sortind.iter().map(|&i| pvals[i]).collect();
85
86 let mut notreject: Vec<bool> = (0..ntests)
88 .map(|i| sorted[i] > alpha / (ntests - i) as f64)
89 .collect();
90 let notrejectmin = notreject.iter().position(|&b| b).unwrap_or(ntests);
92 for nr in notreject.iter_mut().skip(notrejectmin) {
93 *nr = true;
94 }
95 let reject_sorted: Vec<bool> = notreject.iter().map(|&b| !b).collect();
96
97 let mut running = f64::NEG_INFINITY;
99 let mut corrected: Vec<f64> = (0..ntests)
100 .map(|i| {
101 let v = sorted[i] * (ntests - i) as f64;
102 running = running.max(v);
103 running
104 })
105 .collect();
106 for c in corrected.iter_mut() {
107 if *c > 1.0 {
108 *c = 1.0;
109 }
110 }
111
112 MultiTestResult {
113 reject: unsort_bool(&reject_sorted, &sortind),
114 pvals_corrected: unsort(&corrected, &sortind),
115 }
116}
117
118fn fdr_bh(pvals: &[f64], alpha: f64) -> MultiTestResult {
119 let ntests = pvals.len();
120 let sortind = argsort(pvals);
121 let sorted: Vec<f64> = sortind.iter().map(|&i| pvals[i]).collect();
122 let nf = ntests as f64;
123
124 let ecdf: Vec<f64> = (0..ntests).map(|i| (i + 1) as f64 / nf).collect();
126
127 let mut reject_sorted: Vec<bool> = (0..ntests).map(|i| sorted[i] <= ecdf[i] * alpha).collect();
129 if let Some(rejectmax) = reject_sorted.iter().rposition(|&b| b) {
130 for r in reject_sorted.iter_mut().take(rejectmax) {
131 *r = true;
132 }
133 }
134
135 let raw: Vec<f64> = (0..ntests).map(|i| sorted[i] / ecdf[i]).collect();
137 let mut corrected = vec![0.0; ntests];
138 let mut running = f64::INFINITY;
139 for i in (0..ntests).rev() {
140 running = running.min(raw[i]);
141 corrected[i] = running.min(1.0);
142 }
143
144 MultiTestResult {
145 reject: unsort_bool(&reject_sorted, &sortind),
146 pvals_corrected: unsort(&corrected, &sortind),
147 }
148}
149
150#[cfg(test)]
151mod tests {
152 use super::*;
153
154 #[test]
155 fn bonferroni_scales_and_clamps() {
156 let r = multipletests(&[0.01, 0.2, 0.6], 0.05, MultiTestMethod::Bonferroni);
157 assert!((r.pvals_corrected[0] - 0.03).abs() < 1e-12);
158 assert!((r.pvals_corrected[2] - 1.0).abs() < 1e-12); assert_eq!(r.reject, vec![true, false, false]);
160 }
161
162 #[test]
163 fn fdr_bh_monotone() {
164 let r = multipletests(&[0.001, 0.01, 0.03, 0.5], 0.05, MultiTestMethod::FdrBh);
165 for w in r.pvals_corrected.windows(2) {
167 assert!(w[0] <= w[1] + 1e-12);
168 }
169 }
170}