1use anyhow::{bail, Result};
10use ndarray::Array2;
11
12use crate::classifytestsf::classify_tests_f;
13use crate::fit::MArrayLM;
14use crate::special::{ln_norm_cdf, t_two_sided_pvalue};
15
16#[derive(Clone, Copy, PartialEq, Eq, Debug)]
19pub enum Adjust {
20 None,
21 Bonferroni,
22 Holm,
23 BH,
24 BY,
25}
26
27impl Adjust {
28 pub fn parse(s: &str) -> Result<Self> {
29 Ok(match s.to_ascii_lowercase().as_str() {
30 "none" => Adjust::None,
31 "bonferroni" => Adjust::Bonferroni,
32 "holm" => Adjust::Holm,
33 "bh" | "fdr" => Adjust::BH,
34 "by" => Adjust::BY,
35 other => bail!(
36 "invalid adjust.method '{}' (none|bonferroni|holm|BH|fdr|BY)",
37 other
38 ),
39 })
40 }
41}
42
43#[derive(Clone, Copy, PartialEq, Eq, Debug)]
50pub enum DecideMethod {
51 Separate,
52 Global,
53 Hierarchical,
54 NestedF,
55}
56
57impl DecideMethod {
58 pub fn parse(s: &str) -> Result<Self> {
59 Ok(match s.to_ascii_lowercase().as_str() {
60 "separate" => DecideMethod::Separate,
61 "global" => DecideMethod::Global,
62 "hierarchical" => DecideMethod::Hierarchical,
63 "nestedf" => DecideMethod::NestedF,
64 other => bail!(
65 "decideTests method '{}' not supported (separate|global|hierarchical|nestedF)",
66 other
67 ),
68 })
69 }
70}
71
72#[derive(Clone, Debug)]
76pub struct TestResults {
77 pub data: Array2<i8>, pub gene_names: Vec<String>,
79 pub coef_names: Vec<String>,
80}
81
82impl TestResults {
83 pub fn counts(&self) -> Vec<(usize, usize, usize)> {
85 (0..self.data.ncols())
86 .map(|j| {
87 let mut down = 0;
88 let mut up = 0;
89 let mut notsig = 0;
90 for g in 0..self.data.nrows() {
91 match self.data[[g, j]] {
92 v if v < 0 => down += 1,
93 v if v > 0 => up += 1,
94 _ => notsig += 1,
95 }
96 }
97 (down, notsig, up)
98 })
99 .collect()
100 }
101
102 pub fn summary(&self) -> TestResultsSummary {
107 let mut down = Vec::with_capacity(self.coef_names.len());
108 let mut notsig = Vec::with_capacity(self.coef_names.len());
109 let mut up = Vec::with_capacity(self.coef_names.len());
110 for (d, n, u) in self.counts() {
111 down.push(d);
112 notsig.push(n);
113 up.push(u);
114 }
115 TestResultsSummary {
116 coef_names: self.coef_names.clone(),
117 down,
118 notsig,
119 up,
120 }
121 }
122}
123
124#[derive(Clone, Debug)]
128pub struct TestResultsSummary {
129 pub coef_names: Vec<String>,
130 pub down: Vec<usize>,
131 pub notsig: Vec<usize>,
132 pub up: Vec<usize>,
133}
134
135impl TestResultsSummary {
136 pub const LABELS: [&'static str; 3] = ["Down", "NotSig", "Up"];
138 pub const LEVELS: [i8; 3] = [-1, 0, 1];
140}
141
142pub fn p_adjust(p: &[f64], method: Adjust) -> Vec<f64> {
146 let n = p.len();
147 if n == 0 {
148 return Vec::new();
149 }
150 let nf = n as f64;
151 match method {
152 Adjust::None => p.to_vec(),
153 Adjust::Bonferroni => p.iter().map(|&x| (nf * x).min(1.0)).collect(),
154 Adjust::Holm => {
155 let mut o: Vec<usize> = (0..n).collect();
158 o.sort_by(|&a, &b| p[a].partial_cmp(&p[b]).unwrap());
159 let mut out = vec![0.0_f64; n];
160 let mut cummax = f64::NEG_INFINITY;
161 for (rank0, &idx) in o.iter().enumerate() {
162 let i = (rank0 + 1) as f64;
163 let val = (nf + 1.0 - i) * p[idx];
164 cummax = cummax.max(val);
165 out[idx] = cummax.min(1.0);
166 }
167 out
168 }
169 Adjust::BH | Adjust::BY => {
170 let mut o: Vec<usize> = (0..n).collect();
173 o.sort_by(|&a, &b| p[b].partial_cmp(&p[a]).unwrap());
174 let q = if matches!(method, Adjust::BY) {
175 (1..=n).map(|k| 1.0 / k as f64).sum::<f64>()
176 } else {
177 1.0
178 };
179 let mut out = vec![0.0_f64; n];
180 let mut cummin = f64::INFINITY;
181 for (rank0, &idx) in o.iter().enumerate() {
182 let i = (n - rank0) as f64; let val = q * (nf / i) * p[idx];
184 cummin = cummin.min(val);
185 out[idx] = cummin.min(1.0);
186 }
187 out
188 }
189 }
190}
191
192fn sign_i8(x: f64) -> i8 {
194 if x > 0.0 {
195 1
196 } else if x < 0.0 {
197 -1
198 } else {
199 0
200 }
201}
202
203fn cor_from_cov(cov: &Array2<f64>) -> Array2<f64> {
207 let mut c = cov.clone();
208 for i in 0..c.nrows() {
209 if c[[i, i]] == 0.0 {
210 c[[i, i]] = 1.0;
211 }
212 }
213 crate::linalg::cov2cor(&c)
214}
215
216pub fn decide_tests(
221 fit: &MArrayLM,
222 method: DecideMethod,
223 adjust: Adjust,
224 p_value: f64,
225 lfc: f64,
226) -> Result<TestResults> {
227 let coef = &fit.coefficients;
228 let (ng, nc) = (coef.nrows(), coef.ncols());
229 let mut data = Array2::<i8>::zeros((ng, nc));
230
231 match method {
232 DecideMethod::Separate => {
233 let p = fit
234 .p_value
235 .as_ref()
236 .ok_or_else(|| anyhow::anyhow!("need to run eBayes first (p.value missing)"))?;
237 for j in 0..nc {
238 let mut idx = Vec::new();
239 let mut vals = Vec::new();
240 for g in 0..ng {
241 let v = p[[g, j]];
242 if v.is_finite() {
243 idx.push(g);
244 vals.push(v);
245 }
246 }
247 let adj = p_adjust(&vals, adjust);
248 for (k, &g) in idx.iter().enumerate() {
249 if adj[k] < p_value {
250 data[[g, j]] = sign_i8(coef[[g, j]]);
251 }
252 }
253 }
254 }
255 DecideMethod::Global => {
256 let p = fit
257 .p_value
258 .as_ref()
259 .ok_or_else(|| anyhow::anyhow!("need to run eBayes first (p.value missing)"))?;
260 let mut idx = Vec::new();
261 let mut vals = Vec::new();
262 for j in 0..nc {
263 for g in 0..ng {
264 let v = p[[g, j]];
265 if v.is_finite() {
266 idx.push((g, j));
267 vals.push(v);
268 }
269 }
270 }
271 let adj = p_adjust(&vals, adjust);
272 for (k, &(g, j)) in idx.iter().enumerate() {
273 if adj[k] < p_value {
274 data[[g, j]] = sign_i8(coef[[g, j]]);
275 }
276 }
277 }
278 DecideMethod::Hierarchical => {
279 bail!(
280 "decideTests(method=\"hierarchical\") on a fit needs the stepwise \
281 .classifyTestsP classifier, which is not yet ported; use \
282 decide_tests_pvalues for the p-value-matrix hierarchical method"
283 );
284 }
285 DecideMethod::NestedF => {
286 let t = fit
287 .t
288 .as_ref()
289 .ok_or_else(|| anyhow::anyhow!("need to run eBayes first (t missing)"))?;
290 let fp = fit
291 .f_p_value
292 .as_ref()
293 .ok_or_else(|| anyhow::anyhow!("need to run eBayes first (F.p.value missing)"))?;
294 if fp.iter().any(|v| v.is_nan()) {
295 bail!("nestedF method can't handle NA p-values");
296 }
297 let adj = p_adjust(&fp.to_vec(), adjust);
299 let sel: Vec<usize> = (0..ng).filter(|&g| adj[g] < p_value).collect();
300 let i = sel.len();
301 if i > 0 {
302 let n = ng as f64;
305 let a = match adjust {
306 Adjust::None => 1.0,
307 Adjust::Bonferroni => 1.0 / n,
308 Adjust::Holm => 1.0 / (n - i as f64 + 1.0),
309 Adjust::BH => i as f64 / n,
310 Adjust::BY => {
311 let harmonic: f64 = (1..=ng).map(|k| 1.0 / k as f64).sum();
312 i as f64 / n / harmonic
313 }
314 };
315 let cor = cor_from_cov(&fit.cov_coefficients);
316 let mut tsel = Array2::<f64>::zeros((i, nc));
317 let mut dfsel = Vec::with_capacity(i);
318 for (k, &g) in sel.iter().enumerate() {
319 for j in 0..nc {
320 tsel[[k, j]] = t[[g, j]];
321 }
322 let dfg = match &fit.df_prior {
324 Some(dp) if dp.len() == 1 => dp[0] + fit.df_residual[g],
325 Some(dp) => dp[g] + fit.df_residual[g],
326 None => f64::INFINITY,
327 };
328 dfsel.push(dfg);
329 }
330 let rsel = classify_tests_f(&tsel, Some(&cor), &dfsel, p_value * a);
331 for (k, &g) in sel.iter().enumerate() {
332 for j in 0..nc {
333 data[[g, j]] = rsel[[k, j]];
334 }
335 }
336 }
337 }
338 }
339
340 if lfc > 0.0 {
341 for j in 0..nc {
342 for g in 0..ng {
343 if coef[[g, j]].abs() <= lfc {
344 data[[g, j]] = 0;
345 }
346 }
347 }
348 }
349
350 Ok(TestResults {
351 data,
352 gene_names: fit.gene_names.clone(),
353 coef_names: fit.coef_names.clone(),
354 })
355}
356
357pub fn decide_tests_pvalues(
365 p: &Array2<f64>,
366 method: DecideMethod,
367 adjust: Adjust,
368 p_value: f64,
369 lfc: f64,
370 coefficients: Option<&Array2<f64>>,
371) -> Result<TestResults> {
372 if p.iter().any(|v| v.is_nan()) {
373 bail!("NA p-values not supported");
374 }
375 if p.iter().any(|&v| !(0.0..=1.0).contains(&v)) {
376 bail!("object doesn't appear to be a matrix of p-values");
377 }
378 let (ng, nc) = (p.nrows(), p.ncols());
379 if let Some(co) = coefficients {
380 if co.nrows() != ng || co.ncols() != nc {
381 bail!("dim(object) disagrees with dim(coefficients)");
382 }
383 }
384
385 let mut padj = p.clone();
386 let mut cutoff = p_value;
387
388 match method {
389 DecideMethod::Separate => {
390 for j in 0..nc {
391 let col: Vec<f64> = (0..ng).map(|g| p[[g, j]]).collect();
392 let adj = p_adjust(&col, adjust);
393 for g in 0..ng {
394 padj[[g, j]] = adj[g];
395 }
396 }
397 }
398 DecideMethod::Global => {
399 let all: Vec<f64> = p.iter().copied().collect();
400 let adj = p_adjust(&all, adjust);
401 for (v, &a) in padj.iter_mut().zip(adj.iter()) {
402 *v = a;
403 }
404 }
405 DecideMethod::Hierarchical => {
406 let mult: Vec<f64> = (1..=nc).map(|k| nc as f64 / k as f64).collect();
408 let mut genewise = vec![0.0_f64; ng];
409 for g in 0..ng {
410 let mut row: Vec<f64> = (0..nc).map(|j| p[[g, j]]).collect();
411 row.sort_by(|a, b| a.partial_cmp(b).unwrap());
412 genewise[g] = (0..nc)
413 .map(|k| row[k] * mult[k])
414 .fold(f64::INFINITY, f64::min);
415 }
416 let gadj = p_adjust(&genewise, adjust);
417 let de: Vec<bool> = gadj.iter().map(|&v| v <= p_value).collect();
418 let nde = de.iter().filter(|&&b| b).count();
419 for g in 0..ng {
420 if de[g] {
421 let row: Vec<f64> = (0..nc).map(|j| p[[g, j]]).collect();
422 let adj = p_adjust(&row, adjust);
423 for j in 0..nc {
424 padj[[g, j]] = adj[j];
425 }
426 } else {
427 for j in 0..nc {
428 padj[[g, j]] = 1.0;
429 }
430 }
431 }
432 let a = match adjust {
433 Adjust::None => 1.0,
434 Adjust::Bonferroni => 1.0 / ng as f64,
435 Adjust::Holm => 1.0 / (ng - nde + 1) as f64,
436 Adjust::BH => nde as f64 / ng as f64,
437 Adjust::BY => {
438 let harmonic: f64 = (1..=ng).map(|k| 1.0 / k as f64).sum();
439 nde as f64 / ng as f64 / harmonic
440 }
441 };
442 cutoff = a * p_value;
443 }
444 DecideMethod::NestedF => {
445 bail!("nestedF adjust method requires an MArrayLM object");
446 }
447 }
448
449 let mut data = Array2::<i8>::zeros((ng, nc));
450 for g in 0..ng {
451 for j in 0..nc {
452 if padj[[g, j]] <= cutoff {
453 let mut v: i8 = 1;
454 if let Some(co) = coefficients {
455 if co[[g, j]] < 0.0 {
456 v = -1;
457 }
458 if lfc > 0.0 && co[[g, j]].abs() < lfc {
459 v = 0;
460 }
461 }
462 data[[g, j]] = v;
463 }
464 }
465 }
466
467 Ok(TestResults {
468 data,
469 gene_names: vec![String::new(); ng],
470 coef_names: vec![String::new(); nc],
471 })
472}
473
474pub fn classify_tests_p(
485 tstat: &Array2<f64>,
486 df: &[f64],
487 p_value: f64,
488 method: Adjust,
489) -> Array2<i8> {
490 let (ng, nc) = tstat.dim();
491 let mut result = Array2::<i8>::zeros((ng, nc));
492 for i in 0..ng {
493 let df_i = if df.len() == 1 { df[0] } else { df[i] };
494 let prow: Vec<f64> = (0..nc).map(|j| two_sided_p(tstat[[i, j]], df_i)).collect();
495 let adj = p_adjust(&prow, method);
496 for j in 0..nc {
497 if adj[j] < p_value {
498 let t = tstat[[i, j]];
499 result[[i, j]] = if t > 0.0 {
500 1
501 } else if t < 0.0 {
502 -1
503 } else {
504 0
505 };
506 }
507 }
508 }
509 result
510}
511
512fn two_sided_p(t: f64, df: f64) -> f64 {
515 if t.is_nan() {
516 return f64::NAN;
517 }
518 if df.is_infinite() {
519 2.0 * ln_norm_cdf(-t.abs()).exp()
520 } else {
521 t_two_sided_pvalue(t, df)
522 }
523}
524
525#[cfg(test)]
526mod tests {
527 use super::*;
528 use crate::ebayes::ebayes;
529 use crate::fit::lmfit;
530 use crate::toptable::p_adjust_bh;
531 use ndarray::array;
532
533 fn fit_of(y: &Array2<f64>, design: &Array2<f64>) -> MArrayLM {
535 let ng = y.nrows();
536 let mut fit = lmfit(
537 y,
538 design,
539 vec![String::new(); ng],
540 vec![String::new(); design.ncols()],
541 )
542 .unwrap();
543 ebayes(&mut fit, 0.01, (0.1, 4.0), false, false).unwrap();
544 fit
545 }
546
547 fn nestedf(fit: &MArrayLM, adjust: Adjust) -> Array2<i8> {
548 decide_tests(fit, DecideMethod::NestedF, adjust, 0.1, 0.0)
549 .unwrap()
550 .data
551 }
552
553 #[test]
554 fn summary_testresults_matches_r() {
555 let data = array![[-1i8, 1], [-1, 1], [0, 1], [0, 0], [1, -1], [1, 0]];
558 let res = TestResults {
559 data,
560 gene_names: vec![String::new(); 6],
561 coef_names: vec!["C1".into(), "C2".into()],
562 };
563 let s = res.summary();
564 assert_eq!(s.down, vec![2, 1]);
565 assert_eq!(s.notsig, vec![2, 2]);
566 assert_eq!(s.up, vec![2, 3]);
567 assert_eq!(s.coef_names, vec!["C1".to_string(), "C2".to_string()]);
568 assert_eq!(TestResultsSummary::LABELS, ["Down", "NotSig", "Up"]);
569 assert_eq!(TestResultsSummary::LEVELS, [-1, 0, 1]);
570 }
571
572 #[test]
575 fn nestedf_orthogonal_df_inf_matches_r() {
576 let y = array![
577 [
578 -0.326036490515386,
579 0.526448098873642,
580 -0.163755665941423,
581 0.894937200540396,
582 0.482458809858766,
583 -1.15025528126137,
584 -0.259802107839475,
585 1.50989743811878,
586 1.85214757493023
587 ],
588 [
589 0.552461855419139,
590 -0.794844435415055,
591 0.708522104156376,
592 3.27915199641374,
593 3.758213782399,
594 2.72552883818354,
595 -3.41117304365668,
596 -0.380062992060078,
597 -0.888324730462178
598 ],
599 [
600 -0.67494384395583,
601 1.42775554468304,
602 -0.267980546181617,
603 1.00786575031424,
604 -2.31932736896749,
605 0.577901002861171,
606 -0.641357554081644,
607 1.1531581669848,
608 -0.51137532176584
609 ],
610 [
611 0.214359459043425,
612 -1.46681969416347,
613 -1.46392175987102,
614 -2.07310649057379,
615 -0.459504780196151,
616 -1.39690264657703,
617 0.112457509152448,
618 -0.0776035954486494,
619 -0.54388110381726
620 ],
621 [
622 2.3107692173136,
623 1.7633166213971,
624 2.74443582287532,
625 1.18985338378285,
626 -1.10538366697927,
627 0.74905771616478,
628 -1.57739566886144,
629 -3.81893450079797,
630 -3.72892728363495
631 ],
632 [
633 1.1739662875627,
634 -0.1933379649975,
635 -1.4103901810052,
636 -0.724374218369974,
637 0.402928273406297,
638 -1.05118669692296,
639 0.386835291215675,
640 -1.03744458273607,
641 0.470749538951291
642 ],
643 [
644 0.618789855625968,
645 -0.849754740333862,
646 0.467067606015246,
647 0.167983772348879,
648 0.568934917844047,
649 0.165380870625706,
650 -0.687798326082019,
651 0.302492245643734,
652 0.00538712217177903
653 ],
654 [
655 -3.11273431475421,
656 -2.9415345021505,
657 -3.11932010769425,
658 3.92033515858084,
659 2.29391671757039,
660 4.12980912049522,
661 0.148902488663005,
662 -1.27794616724639,
663 1.34804578555275
664 ],
665 [
666 0.917028289512712,
667 -0.817670355875958,
668 0.467238961765515,
669 -1.67160481202292,
670 -0.290090625815567,
671 1.17372246423572,
672 -0.0576497484824952,
673 0.138339047584972,
674 0.724096713299373
675 ],
676 [
677 -0.223259364627263,
678 -2.05030781563963,
679 0.498135564435914,
680 0.448469069614306,
681 -1.48387807160556,
682 -0.427863231850421,
683 -0.0748233649223828,
684 -0.0509841237610558,
685 1.5525491646626
686 ],
687 ];
688 let d = array![
689 [1.0, 0.0, 0.0],
690 [1.0, 0.0, 0.0],
691 [1.0, 0.0, 0.0],
692 [0.0, 1.0, 0.0],
693 [0.0, 1.0, 0.0],
694 [0.0, 1.0, 0.0],
695 [0.0, 0.0, 1.0],
696 [0.0, 0.0, 1.0],
697 [0.0, 0.0, 1.0],
698 ];
699 let fit = fit_of(&y, &d);
700 let none = array![
701 [0i8, 0, 0],
702 [0, 1, -1],
703 [0, 0, 0],
704 [0, -1, 0],
705 [1, 0, -1],
706 [0, 0, 0],
707 [0, 0, 0],
708 [-1, 1, 0],
709 [0, 0, 0],
710 [0, 0, 0],
711 ];
712 let bh = array![
713 [0i8, 0, 0],
714 [0, 1, -1],
715 [0, 0, 0],
716 [0, 0, 0],
717 [1, 0, -1],
718 [0, 0, 0],
719 [0, 0, 0],
720 [-1, 1, 0],
721 [0, 0, 0],
722 [0, 0, 0],
723 ];
724 assert_eq!(nestedf(&fit, Adjust::None), none);
725 assert_eq!(nestedf(&fit, Adjust::BH), bh);
726 assert_eq!(nestedf(&fit, Adjust::Bonferroni), bh);
728 assert_eq!(nestedf(&fit, Adjust::Holm), bh);
729 assert_eq!(nestedf(&fit, Adjust::BY), bh);
730 }
731
732 #[test]
736 fn nestedf_treatment_finite_df_matches_r() {
737 let y = array![
738 [
739 -0.56589213263584,
740 -0.129111406171985,
741 0.883844216372846,
742 0.165824405266157,
743 0.510226918101866,
744 -0.296387565274696,
745 -0.492528835489312,
746 -0.648942826369362,
747 0.112440116314965
748 ],
749 [
750 -0.440493509393273,
751 1.97452284169111,
752 0.943716053263893,
753 1.85296247687763,
754 0.918879051549637,
755 -1.08806877466116,
756 0.6005901507507,
757 2.44390249684897,
758 0.758510828034637
759 ],
760 [
761 -0.672800308611205,
762 -1.12260239699623,
763 -1.571493652712,
764 0.758358865990787,
765 3.16762861462062,
766 -1.71682047480086,
767 0.335444413313772,
768 -3.25946041926619,
769 -4.68105427064873
770 ],
771 [
772 -0.423524520837027,
773 0.478683534928917,
774 -0.705633425785031,
775 0.125863310320817,
776 -0.90874639309216,
777 -1.00280943895942,
778 -0.501117549685322,
779 0.366958043550944,
780 -0.664545331122461
781 ],
782 [
783 -0.14602743813943,
784 1.44508592518769,
785 -0.358918727489447,
786 -0.66242135593651,
787 0.0751840739467855,
788 -0.0948756636231239,
789 0.210626203050984,
790 -0.501720689149703,
791 -0.550236280526646
792 ],
793 [
794 -2.86109866273184,
795 0.678893878924788,
796 0.991825247632336,
797 -1.79353490652056,
798 -0.455885146494197,
799 -0.173438703743574,
800 -1.08355120304816,
801 -1.76993918388511,
802 1.66298603162855
803 ],
804 [
805 -0.388300259051768,
806 0.440476081563293,
807 -0.429356748501314,
808 3.60592978345959,
809 3.11410184476029,
810 3.47987710551828,
811 -2.49264794186162,
812 -4.16894137418337,
813 -3.0883195139039
814 ],
815 [
816 -1.93331260617645,
817 0.903537424251507,
818 0.51244916074171,
819 0.0196320667605606,
820 -0.977619008137272,
821 -0.288571232816508,
822 0.452976738959059,
823 -0.486617267289267,
824 0.617269628542488
825 ],
826 [
827 0.626091910478746,
828 1.49985602027188,
829 -3.49951251341427,
830 4.4610353076871,
831 -0.860897362008685,
832 -0.326803782997329,
833 -0.412492792768815,
834 2.55297326743676,
835 2.42184097256313
836 ],
837 [
838 0.234326050551141,
839 0.17952148545252,
840 -0.36977969915983,
841 0.254021081017535,
842 0.453273832028051,
843 -1.04934374531001,
844 0.609930131193006,
845 0.760402655291936,
846 0.446361110207894
847 ],
848 [
849 -0.138440069780317,
850 0.252561692089675,
851 1.83598463357393,
852 3.70833902421073,
853 3.23737777957835,
854 4.13127266288387,
855 -3.98118598742833,
856 -3.18961650350616,
857 -4.23194940211263
858 ],
859 [
860 2.35976493177311,
861 1.15449268217921,
862 1.19387673975458,
863 0.180160065150074,
864 -1.36989243177605,
865 2.64622305514602,
866 -0.0689664241349938,
867 -0.929549543153579,
868 -1.06316624908704
869 ],
870 [
871 -0.387941487034651,
872 0.209175055219415,
873 -0.0544107817969502,
874 0.0691119664745673,
875 0.0142895174023705,
876 0.0671937469525734,
877 -0.462459018396492,
878 0.417995706160492,
879 -0.31269377190439
880 ],
881 [
882 -0.107005800520593,
883 -0.661074953263737,
884 0.516719009994363,
885 3.46523024323356,
886 2.90939820969627,
887 3.58148939547231,
888 -3.04144579252648,
889 -4.38912509195675,
890 -3.23472505001669
891 ],
892 [
893 3.33934094640944,
894 5.49246633658689,
895 2.17312161159718,
896 -0.214606718731824,
897 -0.198951093031657,
898 -1.06259138423522,
899 -0.670897848735379,
900 -3.35869824696667,
901 0.697941907044954
902 ],
903 ];
904 let d = array![
905 [1.0, 0.0, 0.0],
906 [1.0, 0.0, 0.0],
907 [1.0, 0.0, 0.0],
908 [1.0, 1.0, 0.0],
909 [1.0, 1.0, 0.0],
910 [1.0, 1.0, 0.0],
911 [1.0, 0.0, 1.0],
912 [1.0, 0.0, 1.0],
913 [1.0, 0.0, 1.0],
914 ];
915 let fit = fit_of(&y, &d);
916 let none_bh = array![
917 [0i8, 0, 0],
918 [0, 0, 0],
919 [0, 0, 0],
920 [0, 0, 0],
921 [0, 0, 0],
922 [0, 0, 0],
923 [0, 1, -1],
924 [0, 0, 0],
925 [0, 0, 0],
926 [0, 0, 0],
927 [1, 1, -1],
928 [0, 0, 0],
929 [0, 0, 0],
930 [0, 1, -1],
931 [1, -1, -1],
932 ];
933 let bonf = array![
934 [0i8, 0, 0],
935 [0, 0, 0],
936 [0, 0, 0],
937 [0, 0, 0],
938 [0, 0, 0],
939 [0, 0, 0],
940 [0, 1, -1],
941 [0, 0, 0],
942 [0, 0, 0],
943 [0, 0, 0],
944 [0, 1, -1],
945 [0, 0, 0],
946 [0, 0, 0],
947 [0, 1, -1],
948 [0, 0, 0],
949 ];
950 let holm_by = array![
951 [0i8, 0, 0],
952 [0, 0, 0],
953 [0, 0, 0],
954 [0, 0, 0],
955 [0, 0, 0],
956 [0, 0, 0],
957 [0, 1, -1],
958 [0, 0, 0],
959 [0, 0, 0],
960 [0, 0, 0],
961 [0, 1, -1],
962 [0, 0, 0],
963 [0, 0, 0],
964 [0, 1, -1],
965 [1, 0, 0],
966 ];
967 assert_eq!(nestedf(&fit, Adjust::None), none_bh);
968 assert_eq!(nestedf(&fit, Adjust::BH), none_bh);
969 assert_eq!(nestedf(&fit, Adjust::Bonferroni), bonf);
970 assert_eq!(nestedf(&fit, Adjust::Holm), holm_by);
971 assert_eq!(nestedf(&fit, Adjust::BY), holm_by);
972 }
973
974 fn pdata() -> (Array2<f64>, Array2<f64>) {
975 let p = array![
976 [0.0008, 0.50, 0.30],
977 [0.0001, 0.002, 0.80],
978 [0.20, 0.40, 0.95],
979 [0.030, 0.045, 0.60],
980 [0.70, 0.65, 0.55],
981 [0.004, 0.009, 0.012],
982 [0.50, 0.0006, 0.40],
983 [0.10, 0.11, 0.0009],
984 ];
985 let coef = array![
986 [1.5, -0.3, 0.8],
987 [-2.1, 1.7, -0.2],
988 [0.4, -0.6, 0.1],
989 [1.2, 1.1, -0.5],
990 [-0.2, 0.3, -0.4],
991 [2.0, -1.8, 1.6],
992 [-0.5, -2.4, 0.6],
993 [0.9, 1.0, -2.2],
994 ];
995 (p, coef)
996 }
997
998 #[test]
999 fn decide_tests_pvalues_matches_r() {
1000 let (p, coef) = pdata();
1001 let dt = |m, a| {
1002 decide_tests_pvalues(&p, m, a, 0.05, 0.0, Some(&coef))
1003 .unwrap()
1004 .data
1005 };
1006 let sep_none = array![
1007 [1i8, 0, 0],
1008 [-1, 1, 0],
1009 [0, 0, 0],
1010 [1, 1, 0],
1011 [0, 0, 0],
1012 [1, -1, 1],
1013 [0, -1, 0],
1014 [0, 0, -1],
1015 ];
1016 let sep_bh = array![
1017 [1i8, 0, 0],
1018 [-1, 1, 0],
1019 [0, 0, 0],
1020 [0, 0, 0],
1021 [0, 0, 0],
1022 [1, -1, 1],
1023 [0, -1, 0],
1024 [0, 0, -1],
1025 ];
1026 let sep_bonf = array![
1027 [1i8, 0, 0],
1028 [-1, 1, 0],
1029 [0, 0, 0],
1030 [0, 0, 0],
1031 [0, 0, 0],
1032 [1, 0, 0],
1033 [0, -1, 0],
1034 [0, 0, -1],
1035 ];
1036 let glob_bonf = array![
1037 [1i8, 0, 0],
1038 [-1, 1, 0],
1039 [0, 0, 0],
1040 [0, 0, 0],
1041 [0, 0, 0],
1042 [0, 0, 0],
1043 [0, -1, 0],
1044 [0, 0, -1],
1045 ];
1046 let hier_none = sep_bh.clone(); let hier_bonf = glob_bonf.clone();
1048 let hier_holm = sep_bonf.clone();
1049
1050 assert_eq!(dt(DecideMethod::Separate, Adjust::None), sep_none);
1052 assert_eq!(dt(DecideMethod::Separate, Adjust::BH), sep_bh);
1053 assert_eq!(dt(DecideMethod::Separate, Adjust::Bonferroni), sep_bonf);
1054 assert_eq!(dt(DecideMethod::Separate, Adjust::Holm), sep_bonf);
1055 assert_eq!(dt(DecideMethod::Separate, Adjust::BY), sep_bonf);
1056 assert_eq!(dt(DecideMethod::Global, Adjust::None), sep_none);
1058 assert_eq!(dt(DecideMethod::Global, Adjust::BH), sep_bh);
1059 assert_eq!(dt(DecideMethod::Global, Adjust::Bonferroni), glob_bonf);
1060 assert_eq!(dt(DecideMethod::Global, Adjust::Holm), glob_bonf);
1061 assert_eq!(dt(DecideMethod::Global, Adjust::BY), glob_bonf);
1062 assert_eq!(dt(DecideMethod::Hierarchical, Adjust::None), hier_none);
1064 assert_eq!(dt(DecideMethod::Hierarchical, Adjust::BH), hier_none);
1065 assert_eq!(
1066 dt(DecideMethod::Hierarchical, Adjust::Bonferroni),
1067 hier_bonf
1068 );
1069 assert_eq!(dt(DecideMethod::Hierarchical, Adjust::Holm), hier_holm);
1070 assert_eq!(dt(DecideMethod::Hierarchical, Adjust::BY), hier_bonf);
1071 }
1072
1073 #[test]
1074 fn decide_tests_pvalues_lfc_and_nocoef_match_r() {
1075 let (p, coef) = pdata();
1076 let lfc_res = array![
1078 [1i8, 0, 0],
1079 [-1, 1, 0],
1080 [0, 0, 0],
1081 [0, 0, 0],
1082 [0, 0, 0],
1083 [1, -1, 1],
1084 [0, -1, 0],
1085 [0, 0, -1],
1086 ];
1087 let got = decide_tests_pvalues(
1088 &p,
1089 DecideMethod::Separate,
1090 Adjust::None,
1091 0.05,
1092 1.3,
1093 Some(&coef),
1094 )
1095 .unwrap()
1096 .data;
1097 assert_eq!(got, lfc_res);
1098
1099 let nocoef = array![
1101 [1i8, 0, 0],
1102 [1, 1, 0],
1103 [0, 0, 0],
1104 [0, 0, 0],
1105 [0, 0, 0],
1106 [1, 1, 1],
1107 [0, 1, 0],
1108 [0, 0, 1],
1109 ];
1110 let got = decide_tests_pvalues(&p, DecideMethod::Global, Adjust::BH, 0.05, 0.0, None)
1111 .unwrap()
1112 .data;
1113 assert_eq!(got, nocoef);
1114 }
1115
1116 #[test]
1117 fn nestedf_on_pvalue_matrix_errors() {
1118 let (p, _) = pdata();
1119 assert!(
1120 decide_tests_pvalues(&p, DecideMethod::NestedF, Adjust::BH, 0.05, 0.0, None).is_err()
1121 );
1122 }
1123
1124 #[test]
1125 fn bh_matches_toptable_impl() {
1126 let p = [0.01, 0.04, 0.03, 0.20, 0.005, 0.9, 0.5];
1127 let a = p_adjust(&p, Adjust::BH);
1128 let b = p_adjust_bh(&p);
1129 for (x, y) in a.iter().zip(b.iter()) {
1130 assert!((x - y).abs() < 1e-15, "{x} vs {y}");
1131 }
1132 }
1133
1134 #[test]
1135 fn bonferroni_clamps_at_one() {
1136 let p = [0.1, 0.5, 0.9];
1137 let a = p_adjust(&p, Adjust::Bonferroni);
1138 assert!((a[0] - 0.3).abs() < 1e-15);
1139 assert!((a[1] - 1.0).abs() < 1e-15);
1140 assert!((a[2] - 1.0).abs() < 1e-15);
1141 }
1142
1143 #[test]
1144 fn holm_matches_r() {
1145 let p = [0.01, 0.04, 0.03, 0.005];
1148 let a = p_adjust(&p, Adjust::Holm);
1149 let want = [0.03, 0.06, 0.06, 0.02];
1150 for (x, y) in a.iter().zip(want.iter()) {
1151 assert!((x - y).abs() < 1e-12, "{x} vs {y}");
1152 }
1153 }
1154
1155 #[test]
1156 fn by_is_bh_times_harmonic() {
1157 let p = [0.01, 0.04, 0.03, 0.20, 0.005];
1159 let by = p_adjust(&p, Adjust::BY);
1160 let want = [
1163 0.0570833333,
1164 0.1141666667,
1165 0.1141666667,
1166 0.4566666667,
1167 0.0570833333,
1168 ];
1169 for (x, y) in by.iter().zip(want.iter()) {
1170 assert!((x - y).abs() < 1e-9, "{x} vs {y}");
1171 }
1172 }
1173
1174 #[test]
1175 fn classify_tests_p_matches_r() {
1176 let tstat = array![
1177 [3.2, -1.1, 0.4],
1178 [2.6, 2.9, -2.7],
1179 [-0.3, 0.8, 1.0],
1180 [4.1, -3.8, 2.2],
1181 ];
1182 let holm_inf = array![[1, 0, 0], [1, 1, -1], [0, 0, 0], [1, -1, 1]];
1185 assert_eq!(
1186 classify_tests_p(&tstat, &[f64::INFINITY], 0.05, Adjust::Holm),
1187 holm_inf
1188 );
1189 assert_eq!(
1190 classify_tests_p(&tstat, &[f64::INFINITY], 0.10, Adjust::BH),
1191 holm_inf
1192 );
1193 let df10 = array![[1, 0, 0], [1, 1, -1], [0, 0, 0], [1, -1, 0]];
1194 assert_eq!(classify_tests_p(&tstat, &[10.0], 0.05, Adjust::Holm), df10);
1195 assert_eq!(classify_tests_p(&tstat, &[5.0], 0.05, Adjust::None), df10);
1196 }
1197}