1use crate::error::{StatsError, StatsResult};
17
18#[derive(Debug, Clone)]
20pub struct MultipleCorrectionResult {
21 pub pvalues_corrected: Vec<f64>,
23 pub reject: Vec<bool>,
25 pub method: String,
27 pub alpha: f64,
29}
30
31pub fn bonferroni(pvalues: &[f64], alpha: f64) -> StatsResult<MultipleCorrectionResult> {
62 validate_inputs(pvalues, alpha)?;
63
64 let m = pvalues.len() as f64;
65 let corrected: Vec<f64> = pvalues.iter().map(|&p| (p * m).min(1.0)).collect();
66 let reject: Vec<bool> = corrected.iter().map(|&p| p <= alpha).collect();
67
68 Ok(MultipleCorrectionResult {
69 pvalues_corrected: corrected,
70 reject,
71 method: "Bonferroni".to_string(),
72 alpha,
73 })
74}
75
76pub fn holm_bonferroni(pvalues: &[f64], alpha: f64) -> StatsResult<MultipleCorrectionResult> {
106 validate_inputs(pvalues, alpha)?;
107
108 let m = pvalues.len();
109
110 let mut order: Vec<usize> = (0..m).collect();
112 order.sort_by(|&a, &b| {
113 pvalues[a]
114 .partial_cmp(&pvalues[b])
115 .unwrap_or(std::cmp::Ordering::Equal)
116 });
117
118 let mut corrected = vec![0.0_f64; m];
119
120 let mut max_so_far = 0.0_f64;
122 for (rank, &idx) in order.iter().enumerate() {
123 let multiplier = (m - rank) as f64;
124 let adjusted = (pvalues[idx] * multiplier).min(1.0);
125 max_so_far = max_so_far.max(adjusted);
126 corrected[idx] = max_so_far;
127 }
128
129 let reject: Vec<bool> = corrected.iter().map(|&p| p <= alpha).collect();
130
131 Ok(MultipleCorrectionResult {
132 pvalues_corrected: corrected,
133 reject,
134 method: "Holm-Bonferroni".to_string(),
135 alpha,
136 })
137}
138
139pub fn hochberg(pvalues: &[f64], alpha: f64) -> StatsResult<MultipleCorrectionResult> {
168 validate_inputs(pvalues, alpha)?;
169
170 let m = pvalues.len();
171
172 let mut order: Vec<usize> = (0..m).collect();
174 order.sort_by(|&a, &b| {
175 pvalues[a]
176 .partial_cmp(&pvalues[b])
177 .unwrap_or(std::cmp::Ordering::Equal)
178 });
179
180 let mut corrected = vec![0.0_f64; m];
181
182 let mut min_so_far = 1.0_f64;
184 for rank in (0..m).rev() {
185 let idx = order[rank];
186 let multiplier = (m - rank) as f64;
187 let adjusted = (pvalues[idx] * multiplier).min(1.0);
188 min_so_far = min_so_far.min(adjusted);
189 corrected[idx] = min_so_far;
190 }
191
192 let reject: Vec<bool> = corrected.iter().map(|&p| p <= alpha).collect();
193
194 Ok(MultipleCorrectionResult {
195 pvalues_corrected: corrected,
196 reject,
197 method: "Hochberg".to_string(),
198 alpha,
199 })
200}
201
202pub fn benjamini_hochberg(pvalues: &[f64], alpha: f64) -> StatsResult<MultipleCorrectionResult> {
233 validate_inputs(pvalues, alpha)?;
234
235 let m = pvalues.len();
236 let mf = m as f64;
237
238 let mut order: Vec<usize> = (0..m).collect();
240 order.sort_by(|&a, &b| {
241 pvalues[a]
242 .partial_cmp(&pvalues[b])
243 .unwrap_or(std::cmp::Ordering::Equal)
244 });
245
246 let mut corrected = vec![0.0_f64; m];
247
248 let mut min_so_far = 1.0_f64;
250 for rank in (0..m).rev() {
251 let idx = order[rank];
252 let rank_1based = (rank + 1) as f64;
253 let adjusted = (pvalues[idx] * mf / rank_1based).min(1.0);
254 min_so_far = min_so_far.min(adjusted);
255 corrected[idx] = min_so_far;
256 }
257
258 let reject: Vec<bool> = corrected.iter().map(|&p| p <= alpha).collect();
259
260 Ok(MultipleCorrectionResult {
261 pvalues_corrected: corrected,
262 reject,
263 method: "Benjamini-Hochberg".to_string(),
264 alpha,
265 })
266}
267
268pub fn benjamini_yekutieli(pvalues: &[f64], alpha: f64) -> StatsResult<MultipleCorrectionResult> {
297 validate_inputs(pvalues, alpha)?;
298
299 let m = pvalues.len();
300 let mf = m as f64;
301
302 let cm: f64 = (1..=m).map(|i| 1.0 / i as f64).sum();
304
305 let mut order: Vec<usize> = (0..m).collect();
307 order.sort_by(|&a, &b| {
308 pvalues[a]
309 .partial_cmp(&pvalues[b])
310 .unwrap_or(std::cmp::Ordering::Equal)
311 });
312
313 let mut corrected = vec![0.0_f64; m];
314
315 let mut min_so_far = 1.0_f64;
317 for rank in (0..m).rev() {
318 let idx = order[rank];
319 let rank_1based = (rank + 1) as f64;
320 let adjusted = (pvalues[idx] * mf * cm / rank_1based).min(1.0);
321 min_so_far = min_so_far.min(adjusted);
322 corrected[idx] = min_so_far;
323 }
324
325 let reject: Vec<bool> = corrected.iter().map(|&p| p <= alpha).collect();
326
327 Ok(MultipleCorrectionResult {
328 pvalues_corrected: corrected,
329 reject,
330 method: "Benjamini-Yekutieli".to_string(),
331 alpha,
332 })
333}
334
335pub fn sidak(pvalues: &[f64], alpha: f64) -> StatsResult<MultipleCorrectionResult> {
364 validate_inputs(pvalues, alpha)?;
365
366 let m = pvalues.len() as f64;
367 let corrected: Vec<f64> = pvalues
368 .iter()
369 .map(|&p| {
370 if p >= 1.0 {
372 1.0
373 } else if p <= 0.0 {
374 0.0
375 } else {
376 (1.0 - (1.0 - p).powf(m)).min(1.0)
377 }
378 })
379 .collect();
380
381 let reject: Vec<bool> = corrected.iter().map(|&p| p <= alpha).collect();
382
383 Ok(MultipleCorrectionResult {
384 pvalues_corrected: corrected,
385 reject,
386 method: "Sidak".to_string(),
387 alpha,
388 })
389}
390
391pub fn multipletests(
410 pvalues: &[f64],
411 alpha: f64,
412 method: &str,
413) -> StatsResult<MultipleCorrectionResult> {
414 match method {
415 "bonferroni" => bonferroni(pvalues, alpha),
416 "holm" | "holm-bonferroni" => holm_bonferroni(pvalues, alpha),
417 "hochberg" => hochberg(pvalues, alpha),
418 "fdr_bh" | "benjamini-hochberg" | "bh" => benjamini_hochberg(pvalues, alpha),
419 "fdr_by" | "benjamini-yekutieli" | "by" => benjamini_yekutieli(pvalues, alpha),
420 "sidak" => sidak(pvalues, alpha),
421 _ => Err(StatsError::InvalidArgument(format!(
422 "Unknown correction method '{}'. Valid methods: bonferroni, holm, hochberg, fdr_bh, fdr_by, sidak",
423 method
424 ))),
425 }
426}
427
428fn validate_inputs(pvalues: &[f64], alpha: f64) -> StatsResult<()> {
433 if pvalues.is_empty() {
434 return Err(StatsError::InvalidArgument(
435 "p-values array cannot be empty".to_string(),
436 ));
437 }
438 if alpha <= 0.0 || alpha >= 1.0 {
439 return Err(StatsError::InvalidArgument(format!(
440 "alpha must be in (0, 1), got {}",
441 alpha
442 )));
443 }
444 for (i, &p) in pvalues.iter().enumerate() {
445 if p < 0.0 || p > 1.0 {
446 return Err(StatsError::InvalidArgument(format!(
447 "p-value at index {} is {}, must be in [0, 1]",
448 i, p
449 )));
450 }
451 }
452 Ok(())
453}
454
455#[cfg(test)]
460mod tests {
461 use super::*;
462
463 #[test]
464 fn test_bonferroni_basic() {
465 let pvals = vec![0.01, 0.04, 0.03, 0.005];
466 let result = bonferroni(&pvals, 0.05).expect("Bonferroni failed");
467
468 assert_eq!(result.pvalues_corrected.len(), 4);
469 assert!((result.pvalues_corrected[0] - 0.04).abs() < 1e-10);
470 assert!((result.pvalues_corrected[1] - 0.16).abs() < 1e-10);
471 assert!((result.pvalues_corrected[2] - 0.12).abs() < 1e-10);
472 assert!((result.pvalues_corrected[3] - 0.02).abs() < 1e-10);
473
474 assert!(result.reject[0]); assert!(!result.reject[1]); assert!(!result.reject[2]); assert!(result.reject[3]); }
479
480 #[test]
481 fn test_bonferroni_clamped() {
482 let pvals = vec![0.5, 0.6, 0.7];
483 let result = bonferroni(&pvals, 0.05).expect("Bonferroni failed");
484 assert!((result.pvalues_corrected[0] - 1.0).abs() < 1e-10);
486 }
487
488 #[test]
489 fn test_holm_bonferroni_basic() {
490 let pvals = vec![0.01, 0.04, 0.03, 0.005];
491 let result = holm_bonferroni(&pvals, 0.05).expect("Holm failed");
492
493 assert_eq!(result.pvalues_corrected.len(), 4);
494 assert!((result.pvalues_corrected[3] - 0.02).abs() < 1e-10);
500 assert!((result.pvalues_corrected[0] - 0.03).abs() < 1e-10);
501 assert!(result.reject[3]); assert!(result.reject[0]); }
504
505 #[test]
506 fn test_hochberg_basic() {
507 let pvals = vec![0.01, 0.04, 0.03, 0.005];
508 let result = hochberg(&pvals, 0.05).expect("Hochberg failed");
509
510 assert_eq!(result.pvalues_corrected.len(), 4);
511 let bonf_result = bonferroni(&pvals, 0.05).expect("Bonferroni failed");
513 for i in 0..pvals.len() {
514 assert!(result.pvalues_corrected[i] <= bonf_result.pvalues_corrected[i] + 1e-10);
515 }
516 }
517
518 #[test]
519 fn test_benjamini_hochberg_basic() {
520 let pvals = vec![
521 0.001, 0.008, 0.039, 0.041, 0.042, 0.06, 0.074, 0.205, 0.212, 0.216,
522 ];
523 let result = benjamini_hochberg(&pvals, 0.05).expect("BH failed");
524
525 assert_eq!(result.pvalues_corrected.len(), 10);
526 for &p in &result.pvalues_corrected {
528 assert!(p >= 0.0 && p <= 1.0);
529 }
530 assert!(result.reject[0]);
532 }
533
534 #[test]
535 fn test_benjamini_hochberg_monotonicity() {
536 let pvals = vec![0.01, 0.02, 0.03, 0.04, 0.05];
537 let result = benjamini_hochberg(&pvals, 0.05).expect("BH failed");
538
539 let mut order: Vec<usize> = (0..pvals.len()).collect();
541 order.sort_by(|&a, &b| {
542 pvals[a]
543 .partial_cmp(&pvals[b])
544 .unwrap_or(std::cmp::Ordering::Equal)
545 });
546
547 for i in 1..order.len() {
548 assert!(
549 result.pvalues_corrected[order[i]]
550 >= result.pvalues_corrected[order[i - 1]] - 1e-10,
551 "Monotonicity violation at rank {}",
552 i
553 );
554 }
555 }
556
557 #[test]
558 fn test_benjamini_yekutieli_basic() {
559 let pvals = vec![0.001, 0.008, 0.039, 0.041, 0.06];
560 let result = benjamini_yekutieli(&pvals, 0.05).expect("BY failed");
561
562 let bh_result = benjamini_hochberg(&pvals, 0.05).expect("BH failed");
564 for i in 0..pvals.len() {
565 assert!(result.pvalues_corrected[i] >= bh_result.pvalues_corrected[i] - 1e-10);
566 }
567 }
568
569 #[test]
570 fn test_sidak_basic() {
571 let pvals = vec![0.01, 0.04, 0.03, 0.005];
572 let result = sidak(&pvals, 0.05).expect("Sidak failed");
573
574 let bonf_result = bonferroni(&pvals, 0.05).expect("Bonferroni failed");
576 for i in 0..pvals.len() {
577 assert!(result.pvalues_corrected[i] <= bonf_result.pvalues_corrected[i] + 1e-10);
578 }
579 }
580
581 #[test]
582 fn test_sidak_values() {
583 let pvals = vec![0.005];
584 let result = sidak(&pvals, 0.05).expect("Sidak failed");
585 assert!((result.pvalues_corrected[0] - 0.005).abs() < 1e-10);
587
588 let pvals2 = vec![0.01, 0.01];
589 let result2 = sidak(&pvals2, 0.05).expect("Sidak failed");
590 assert!((result2.pvalues_corrected[0] - 0.0199).abs() < 1e-4);
592 }
593
594 #[test]
595 fn test_multipletests_dispatch() {
596 let pvals = vec![0.01, 0.04, 0.03, 0.005];
597
598 let r1 = multipletests(&pvals, 0.05, "bonferroni").expect("dispatch failed");
599 assert_eq!(r1.method, "Bonferroni");
600
601 let r2 = multipletests(&pvals, 0.05, "holm").expect("dispatch failed");
602 assert_eq!(r2.method, "Holm-Bonferroni");
603
604 let r3 = multipletests(&pvals, 0.05, "hochberg").expect("dispatch failed");
605 assert_eq!(r3.method, "Hochberg");
606
607 let r4 = multipletests(&pvals, 0.05, "fdr_bh").expect("dispatch failed");
608 assert_eq!(r4.method, "Benjamini-Hochberg");
609
610 let r5 = multipletests(&pvals, 0.05, "fdr_by").expect("dispatch failed");
611 assert_eq!(r5.method, "Benjamini-Yekutieli");
612
613 let r6 = multipletests(&pvals, 0.05, "sidak").expect("dispatch failed");
614 assert_eq!(r6.method, "Sidak");
615
616 let r7 = multipletests(&pvals, 0.05, "unknown_method");
617 assert!(r7.is_err());
618 }
619
620 #[test]
621 fn test_empty_input() {
622 let empty: Vec<f64> = vec![];
623 assert!(bonferroni(&empty, 0.05).is_err());
624 assert!(holm_bonferroni(&empty, 0.05).is_err());
625 assert!(hochberg(&empty, 0.05).is_err());
626 assert!(benjamini_hochberg(&empty, 0.05).is_err());
627 assert!(benjamini_yekutieli(&empty, 0.05).is_err());
628 assert!(sidak(&empty, 0.05).is_err());
629 }
630
631 #[test]
632 fn test_invalid_alpha() {
633 let pvals = vec![0.01, 0.05];
634 assert!(bonferroni(&pvals, 0.0).is_err());
635 assert!(bonferroni(&pvals, 1.0).is_err());
636 assert!(bonferroni(&pvals, -0.1).is_err());
637 }
638
639 #[test]
640 fn test_invalid_pvalues() {
641 let pvals = vec![0.01, 1.5];
642 assert!(bonferroni(&pvals, 0.05).is_err());
643
644 let pvals2 = vec![-0.01, 0.05];
645 assert!(bonferroni(&pvals2, 0.05).is_err());
646 }
647
648 #[test]
649 fn test_single_pvalue() {
650 let pvals = vec![0.03];
651 let result = bonferroni(&pvals, 0.05).expect("single pval failed");
652 assert!((result.pvalues_corrected[0] - 0.03).abs() < 1e-10);
653 assert!(result.reject[0]);
654 }
655
656 #[test]
657 fn test_all_significant() {
658 let pvals = vec![0.001, 0.002, 0.003];
659 let result = bonferroni(&pvals, 0.05).expect("all sig failed");
660 assert!(result.reject.iter().all(|&r| r));
662 }
663
664 #[test]
665 fn test_none_significant() {
666 let pvals = vec![0.5, 0.6, 0.7, 0.8, 0.9];
667 let result = bonferroni(&pvals, 0.05).expect("none sig failed");
668 assert!(result.reject.iter().all(|&r| !r));
669 }
670
671 #[test]
672 fn test_holm_vs_bonferroni_power() {
673 let pvals = vec![0.001, 0.01, 0.04, 0.06, 0.10];
675 let bonf = bonferroni(&pvals, 0.05).expect("Bonferroni failed");
676 let holm = holm_bonferroni(&pvals, 0.05).expect("Holm failed");
677
678 let bonf_count: usize = bonf.reject.iter().filter(|&&r| r).count();
679 let holm_count: usize = holm.reject.iter().filter(|&&r| r).count();
680 assert!(holm_count >= bonf_count);
681 }
682
683 #[test]
684 fn test_bh_less_conservative_than_bonferroni() {
685 let pvals = vec![0.001, 0.01, 0.04, 0.06, 0.10];
686 let bonf = bonferroni(&pvals, 0.05).expect("Bonferroni failed");
687 let bh = benjamini_hochberg(&pvals, 0.05).expect("BH failed");
688
689 let bonf_count: usize = bonf.reject.iter().filter(|&&r| r).count();
690 let bh_count: usize = bh.reject.iter().filter(|&&r| r).count();
691 assert!(bh_count >= bonf_count);
693 }
694}