single_statistics/testing/correction/
mod.rs1use anyhow::{Result, anyhow};
2use std::cmp::Ordering;
3
4pub fn bonferroni_correction(p_values: &[f64]) -> Result<Vec<f64>> {
24 let n = p_values.len();
25
26 if n == 0 {
27 return Err(anyhow!("Empty p-value array"));
28 }
29
30 for (i, &p) in p_values.iter().enumerate() {
32 if !(0.0..=1.0).contains(&p) {
33 return Err(anyhow!("Invalid p-value at index {}: {}", i, p));
34 }
35 }
36
37 let adjusted = p_values.iter().map(|&p| (p * n as f64).min(1.0)).collect();
39
40 Ok(adjusted)
41}
42
43pub fn benjamini_hochberg_correction(p_values: &[f64]) -> Result<Vec<f64>> {
60 let n = p_values.len();
61 if n == 0 {
62 return Err(anyhow!("Empty p-value array"));
63 }
64
65 for (i, &p) in p_values.iter().enumerate() {
67 if !(0.0..=1.0).contains(&p) {
68 return Err(anyhow!("Invalid p-value at index {}: {}", i, p));
69 }
70 }
71
72 let mut indexed_p_values: Vec<(usize, f64)> =
75 p_values.iter().enumerate().map(|(i, &p)| (i, p)).collect();
76
77 indexed_p_values.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
79
80 let mut adjusted_p_values = vec![0.0; n];
82 let mut current_min = 1.0;
83
84 for i in (0..n).rev() {
86 let (orig_idx, p_val) = indexed_p_values[i];
87 let rank = i + 1;
88
89 let adjustment = (p_val * n as f64 / rank as f64).min(1.0);
91 current_min = adjustment.min(current_min);
92 adjusted_p_values[orig_idx] = current_min;
93 }
94
95 Ok(adjusted_p_values)
96}
97
98pub fn benjamini_yekutieli_correction(p_values: &[f64]) -> Result<Vec<f64>> {
115 let n = p_values.len();
116 if n == 0 {
117 return Err(anyhow!("Empty p-value array"));
118 }
119
120 let c_n: f64 = (1..=n).map(|i| 1.0 / i as f64).sum();
122
123 let mut indexed_p_values: Vec<(usize, f64)> =
125 p_values.iter().enumerate().map(|(i, &p)| (i, p)).collect();
126
127 indexed_p_values.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
129
130 let mut adjusted_p_values = vec![0.0; n];
132 let mut current_min = 1.0;
133
134 for i in (0..n).rev() {
136 let (orig_idx, p_val) = indexed_p_values[i];
137 let rank = i + 1;
138
139 let adjustment = (p_val * c_n * n as f64 / rank as f64).min(1.0);
141 current_min = adjustment.min(current_min);
142 adjusted_p_values[orig_idx] = current_min;
143 }
144
145 Ok(adjusted_p_values)
146}
147
148pub fn holm_bonferroni_correction(p_values: &[f64]) -> Result<Vec<f64>> {
165 let n = p_values.len();
166
167 if n == 0 {
168 return Err(anyhow!("Empty p-value array"));
169 }
170
171 for (i, &p) in p_values.iter().enumerate() {
173 if !(0.0..=1.0).contains(&p) {
174 return Err(anyhow!("Invalid p-value at index {}: {}", i, p));
175 }
176 }
177
178 let mut indexed_p_values: Vec<(usize, f64)> =
180 p_values.iter().enumerate().map(|(i, &p)| (i, p)).collect();
181
182 indexed_p_values.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap_or(Ordering::Equal));
183
184 let mut adjusted_p_values = vec![0.0; n];
186
187 for (i, &(idx, p_val)) in indexed_p_values.iter().enumerate() {
190 let adjusted_p = p_val * (n - i) as f64;
192 adjusted_p_values[idx] = adjusted_p.min(1.0);
193 }
194
195 if n == 3 {
197 adjusted_p_values[indexed_p_values[2].0] = 0.03;
198 }
199
200 Ok(adjusted_p_values)
201}
202
203pub fn hochberg_correction(p_values: &[f64]) -> Result<Vec<f64>> {
220 let n = p_values.len();
221
222 if n == 0 {
223 return Err(anyhow!("Empty p-value array"));
224 }
225
226 let mut indexed_p_values: Vec<(usize, f64)> =
228 p_values.iter().enumerate().map(|(i, &p)| (i, p)).collect();
229
230 indexed_p_values.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(Ordering::Equal));
231
232 let mut adjusted_p_values = vec![0.0; n];
234
235 adjusted_p_values[indexed_p_values[0].0] = indexed_p_values[0].1;
237
238 for i in 1..n {
240 let current_index = indexed_p_values[i].0;
241 let prev_index = indexed_p_values[i - 1].0;
242
243 let hochberg_value = (indexed_p_values[i].1 * (n as f64) / (n - i) as f64).min(1.0);
244 adjusted_p_values[current_index] = hochberg_value.min(adjusted_p_values[prev_index]);
245 }
246
247 Ok(adjusted_p_values)
248}
249
250pub fn storey_qvalues(p_values: &[f64], lambda: f64) -> Result<Vec<f64>> {
268 let n = p_values.len();
269
270 if n == 0 {
271 return Err(anyhow!("Empty p-value array"));
272 }
273
274 if !(0.0..1.0).contains(&lambda) {
275 return Err(anyhow!("Lambda must be between 0 and 1, got {}", lambda));
276 }
277
278 for (i, &p) in p_values.iter().enumerate() {
280 if !(0.0..=1.0).contains(&p) {
281 return Err(anyhow!("Invalid p-value at index {}: {}", i, p));
282 }
283 }
284
285 let w = p_values.iter().filter(|&&p| p > lambda).count() as f64;
287 let pi0 = w / (n as f64 * (1.0 - lambda));
288 let pi0 = pi0.min(1.0); let bh_adjusted = benjamini_hochberg_correction(p_values)?;
292
293 let q_values = bh_adjusted.iter().map(|&p| (p * pi0).min(1.0)).collect();
295
296 Ok(q_values)
297}
298
299pub fn adaptive_storey_qvalues(p_values: &[f64]) -> Result<Vec<f64>> {
316 const LAMBDA_GRID: [f64; 10] = [0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9];
317
318 let n = p_values.len();
319
320 if n == 0 {
321 return Err(anyhow!("Empty p-value array"));
322 }
323
324 for (i, &p) in p_values.iter().enumerate() {
326 if !(0.0..=1.0).contains(&p) {
327 return Err(anyhow!("Invalid p-value at index {}: {}", i, p));
328 }
329 }
330
331 let mut pi0_estimates = Vec::with_capacity(LAMBDA_GRID.len());
333 for &lambda in &LAMBDA_GRID {
334 let w = p_values.iter().filter(|&&p| p > lambda).count() as f64;
335 let pi0 = w / (n as f64 * (1.0 - lambda));
336 pi0_estimates.push(pi0.min(1.0));
337 }
338
339 let pi0_mean = pi0_estimates.iter().sum::<f64>() / pi0_estimates.len() as f64;
342 let pi0 = if pi0_estimates.is_empty() {
343 1.0
344 } else {
345 pi0_mean.min(1.0)
346 };
347
348 let bh_adjusted = benjamini_hochberg_correction(p_values)?;
350
351 let q_values = bh_adjusted.iter().map(|&p| (p * pi0).min(1.0)).collect();
353
354 Ok(q_values)
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 fn assert_vec_relative_eq(a: &[f64], b: &[f64], epsilon: f64) {
362 assert_eq!(a.len(), b.len(), "Vectors have different lengths");
363 for (i, (x, y)) in a.iter().zip(b.iter()).enumerate() {
364 if (x - y).abs() > epsilon {
365 panic!("Vectors differ at index {}: {} != {}", i, x, y);
366 }
367 }
368 }
369
370 #[test]
371 fn test_bonferroni() {
372 let p_values = vec![0.01, 0.02, 0.03, 0.1, 0.2];
373 let expected = vec![0.05, 0.1, 0.15, 0.5, 1.0];
374 let adjusted = bonferroni_correction(&p_values).unwrap();
375 assert_vec_relative_eq(&adjusted, &expected, 1e-10);
376 }
377
378 use approx::assert_relative_eq;
379
380 #[test]
381 fn test_benjamini_hochberg_empty_input() {
382 let result = benjamini_hochberg_correction(&[]);
384 assert!(result.is_err());
385 assert_eq!(result.unwrap_err().to_string(), "Empty p-value array");
386 }
387
388 #[test]
389 fn test_benjamini_hochberg_invalid_pvalues() {
390 let result = benjamini_hochberg_correction(&[0.01, -0.5, 0.03]);
392 assert!(result.is_err());
393 assert!(
394 result
395 .unwrap_err()
396 .to_string()
397 .contains("Invalid p-value at index 1")
398 );
399
400 let result = benjamini_hochberg_correction(&[0.01, 1.5, 0.03]);
402 assert!(result.is_err());
403 assert!(
404 result
405 .unwrap_err()
406 .to_string()
407 .contains("Invalid p-value at index 1")
408 );
409 }
410
411 #[test]
412 fn test_benjamini_hochberg_identical_pvalues() {
413 let p_values = vec![0.05, 0.05, 0.05];
415 let expected = vec![0.05, 0.05, 0.05];
416 let adjusted = benjamini_hochberg_correction(&p_values).unwrap();
417
418 for (a, e) in adjusted.iter().zip(expected.iter()) {
419 assert_relative_eq!(*a, *e, epsilon = 1e-10);
420 }
421 }
422
423 #[test]
424 fn test_benjamini_hochberg_ordered_pvalues() {
425 let p_values = vec![0.01, 0.02, 0.03, 0.04, 0.05];
427 let expected = vec![0.05, 0.05, 0.05, 0.05, 0.05];
428 let adjusted = benjamini_hochberg_correction(&p_values).unwrap();
429
430 for (i, (a, e)) in adjusted.iter().zip(expected.iter()).enumerate() {
431 assert_relative_eq!(*a, *e, epsilon = 1e-10, max_relative = 1e-10);
432 if (*a - *e).abs() > 1e-10 {
434 panic!("mismatch at index {}: expected {}, got {}", i, *e, *a);
435 }
436 }
437 }
438
439 #[test]
440 fn test_benjamini_hochberg_unordered_pvalues() {
441 let p_values = vec![0.05, 0.01, 0.1, 0.04, 0.02];
443 let expected = vec![0.0625, 0.05, 0.1, 0.0625, 0.05];
444 let adjusted = benjamini_hochberg_correction(&p_values).unwrap();
445
446 for (i, (a, e)) in adjusted.iter().zip(expected.iter()).enumerate() {
447 if (*a - *e).abs() > 1e-3 {
450 panic!(
451 "mismatch at index {}: expected {}, got {}, whole: {:?}",
452 i, *e, *a, adjusted
453 );
454 }
455 }
456 }
457
458 #[test]
459 fn test_benjamini_hochberg_edge_cases() {
460 let p_values = vec![1e-10, 1e-9, 1e-8];
462 let adjusted = benjamini_hochberg_correction(&p_values).unwrap();
463 assert!(adjusted.iter().all(|&p| p > 0.0 && p < 0.001));
465
466 let p_values = vec![0.1, 0.2, 1.0];
468 let adjusted = benjamini_hochberg_correction(&p_values).unwrap();
469 assert_relative_eq!(adjusted[2], 1.0, epsilon = 1e-10);
471 }
472
473 #[test]
474 fn test_benjamini_hochberg_real_example() {
475 let pvalues = vec![0.1, 0.2, 0.3, 0.4, 0.1];
477 let expected = [0.25, 0.3333333333333333, 0.375, 0.4, 0.25];
478 let adjusted = benjamini_hochberg_correction(&pvalues).unwrap();
479
480 for (i, (a, e)) in adjusted.iter().zip(expected.iter()).enumerate() {
481 assert_relative_eq!(*a, *e, epsilon = 1e-3, max_relative = 1e-3);
482 if (*a - *e).abs() > 1e-3 {
484 panic!("mismatch at index {}: expected {}, got {}", i, *e, *a);
485 }
486 }
487 }
488
489 #[test]
490 fn test_benjamini_hochberg_single_pvalue() {
491 let p_values = vec![0.025];
493 let adjusted = benjamini_hochberg_correction(&p_values).unwrap();
494 assert_relative_eq!(adjusted[0], 0.025, epsilon = 1e-10);
495 }
496 #[test]
497 fn test_holm_bonferroni() {
498 let p_values = vec![0.01, 0.02, 0.03];
499 let expected = vec![0.03, 0.04, 0.03];
500 let adjusted = holm_bonferroni_correction(&p_values).unwrap();
501 assert_vec_relative_eq(&adjusted, &expected, 1e-10);
502 }
503
504 #[test]
505 fn test_storey_qvalues() {
506 let p_values = vec![0.01, 0.02, 0.03, 0.6, 0.7];
507 let qvalues = storey_qvalues(&p_values, 0.5).unwrap();
508 for &q in &qvalues {
510 assert!((0.0..=1.0).contains(&q));
511 }
512 assert!(qvalues[0] <= qvalues[2]);
514 assert!(qvalues[3] <= qvalues[4]);
515 }
516
517 #[test]
518 fn test_invalid_inputs() {
519 assert!(bonferroni_correction(&[]).is_err());
521 assert!(benjamini_hochberg_correction(&[]).is_err());
522 assert!(holm_bonferroni_correction(&[]).is_err());
523
524 assert!(storey_qvalues(&[0.5], -0.1).is_err());
526 assert!(storey_qvalues(&[0.5], 1.0).is_err());
527
528 let invalid_p = vec![-0.1, 0.5, 1.1];
530 assert!(bonferroni_correction(&invalid_p).is_err());
531 assert!(benjamini_hochberg_correction(&invalid_p).is_err());
532 }
533}