1use crate::errors::EdgeErrors;
17use crate::numeric::stats::p_adjust_bh;
18
19#[derive(Clone, Copy, Debug, PartialEq, Eq)]
25pub enum SortBy {
26 PValue,
29 LogFc,
31 None,
33}
34
35#[derive(Clone, Debug)]
40pub struct TopTags {
41 pub index: Vec<usize>,
43 pub log_fc: Vec<f64>,
45 pub log_cpm: Vec<f64>,
47 pub statistic: Vec<f64>,
50 pub p_value: Vec<f64>,
52 pub fdr: Vec<f64>,
55}
56
57fn check_length(name: &'static str, values: &[f64], n_genes: usize) -> Result<(), EdgeErrors> {
73 if values.len() != n_genes {
74 return Err(EdgeErrors::LengthMismatch {
75 name,
76 expected: n_genes,
77 got: values.len(),
78 });
79 }
80 Ok(())
81}
82
83fn check_cutoff(name: &str, value: f64) -> Result<(), EdgeErrors> {
95 if !(value.is_finite() && (0.0..=1.0).contains(&value)) {
96 return Err(EdgeErrors::InvalidArgument(format!(
97 "'{name}' must be a probability in [0, 1]; got {value}."
98 )));
99 }
100 Ok(())
101}
102
103pub fn top_tags(
133 log_fc: &[f64],
134 log_cpm: &[f64],
135 statistic: Option<&[f64]>,
136 p_value: &[f64],
137 n: usize,
138 sort_by: SortBy,
139 p_cutoff: f64,
140) -> Result<TopTags, EdgeErrors> {
141 let n_genes = log_fc.len();
142 if n_genes == 0 {
143 return Err(EdgeErrors::EmptyCounts {
144 n_genes: 0,
145 n_samples: 0,
146 });
147 }
148 if n == 0 {
149 return Err(EdgeErrors::MustBePositive("n".to_string()));
150 }
151 check_length("log_cpm", log_cpm, n_genes)?;
152 check_length("p_value", p_value, n_genes)?;
153 if let Some(s) = statistic {
154 check_length("statistic", s, n_genes)?;
155 }
156 check_cutoff("p_cutoff", p_cutoff)?;
157
158 let fdr = p_adjust_bh(p_value);
159
160 let mut order: Vec<usize> = (0..n_genes).collect();
161 match sort_by {
162 SortBy::PValue => order.sort_by(|&a, &b| {
163 p_value[a]
164 .total_cmp(&p_value[b])
165 .then_with(|| log_fc[b].abs().total_cmp(&log_fc[a].abs()))
166 }),
167 SortBy::LogFc => order.sort_by(|&a, &b| log_fc[b].abs().total_cmp(&log_fc[a].abs())),
168 SortBy::None => {}
169 }
170
171 if p_cutoff < 1.0 {
172 order.retain(|&g| fdr[g] <= p_cutoff);
173 }
174 order.truncate(n);
175
176 Ok(TopTags {
177 log_fc: order.iter().map(|&g| log_fc[g]).collect(),
178 log_cpm: order.iter().map(|&g| log_cpm[g]).collect(),
179 statistic: match statistic {
180 Some(s) => order.iter().map(|&g| s[g]).collect(),
181 None => Vec::new(),
182 },
183 p_value: order.iter().map(|&g| p_value[g]).collect(),
184 fdr: order.iter().map(|&g| fdr[g]).collect(),
185 index: order,
186 })
187}
188
189pub fn decide_tests(
216 p_value: &[f64],
217 log_fc: &[f64],
218 p_cutoff: f64,
219 lfc: f64,
220) -> Result<Vec<i8>, EdgeErrors> {
221 check_length("log_fc", log_fc, p_value.len())?;
222 check_cutoff("p_cutoff", p_cutoff)?;
223 if !lfc.is_finite() || lfc < 0.0 {
224 return Err(EdgeErrors::InvalidArgument(format!(
225 "'lfc' must be non-negative and finite; got {lfc}."
226 )));
227 }
228
229 let fdr = p_adjust_bh(p_value);
230 Ok(fdr
231 .iter()
232 .zip(log_fc.iter())
233 .map(|(q, fc)| {
234 if *q >= p_cutoff || fc.abs() < lfc {
235 0
236 } else if *fc < 0.0 {
237 -1
238 } else {
239 1
240 }
241 })
242 .collect())
243}
244
245#[cfg(test)]
250mod tests {
251 #![allow(clippy::excessive_precision)]
254
255 use super::*;
256 use approx::assert_relative_eq;
257
258 const P_VALUE: [f64; 6] = [
270 3.4349237412116053e-06,
271 7.6299000470704081e-01,
272 4.6743523769487572e-01,
273 1.4972774802797630e-05,
274 1.0337909917591625e-02,
275 8.9951524706410912e-01,
276 ];
277
278 const LOG_FC: [f64; 6] = [
279 1.76837964949810367,
280 -0.13190914581416660,
281 -0.89031602850510549,
282 5.98706174338887909,
283 0.86708062515865070,
284 -0.12249104113230144,
285 ];
286
287 const LOG_CPM: [f64; 6] = [
288 4.6828570480490503,
289 5.6094058355452656,
290 1.8369026364109835,
291 2.4631896748603697,
292 10.7199768972231873,
293 2.8848509178259678,
294 ];
295
296 const FDR: [f64; 6] = [
298 2.0609542447269630e-05,
299 8.9951524706410912e-01,
300 7.0115285654231352e-01,
301 4.4918324408392889e-05,
302 2.0675819835183250e-02,
303 8.9951524706410912e-01,
304 ];
305
306 #[test]
313 fn test_top_tags_sorts_by_p_value() {
314 let got = top_tags(
315 &LOG_FC,
316 &LOG_CPM,
317 None,
318 &P_VALUE,
319 usize::MAX,
320 SortBy::PValue,
321 1.0,
322 )
323 .unwrap();
324 assert_eq!(got.index, vec![0, 3, 4, 2, 1, 5]);
325
326 let want_fdr = [
327 2.0609542447269630e-05,
328 4.4918324408392889e-05,
329 2.0675819835183250e-02,
330 7.0115285654231352e-01,
331 8.9951524706410912e-01,
332 8.9951524706410912e-01,
333 ];
334 for (g, w) in got.fdr.iter().zip(want_fdr.iter()) {
335 assert_relative_eq!(g, w, max_relative = 1e-12);
336 }
337
338 let want_fc = [
339 1.76837964949810367,
340 5.98706174338887909,
341 0.86708062515865070,
342 -0.89031602850510549,
343 -0.13190914581416660,
344 -0.12249104113230144,
345 ];
346 for (g, w) in got.log_fc.iter().zip(want_fc.iter()) {
347 assert_relative_eq!(g, w, max_relative = 1e-12);
348 }
349 assert!(got.statistic.is_empty());
350 }
351
352 #[test]
354 fn test_top_tags_sorts_by_absolute_fold_change() {
355 let got = top_tags(
356 &LOG_FC,
357 &LOG_CPM,
358 None,
359 &P_VALUE,
360 usize::MAX,
361 SortBy::LogFc,
362 1.0,
363 )
364 .unwrap();
365 assert_eq!(got.index, vec![3, 0, 2, 4, 1, 5]);
366
367 let want_fdr = [
368 4.4918324408392889e-05,
369 2.0609542447269630e-05,
370 7.0115285654231352e-01,
371 2.0675819835183250e-02,
372 8.9951524706410912e-01,
373 8.9951524706410912e-01,
374 ];
375 for (g, w) in got.fdr.iter().zip(want_fdr.iter()) {
376 assert_relative_eq!(g, w, max_relative = 1e-12);
377 }
378 }
379
380 #[test]
382 fn test_top_tags_leaves_the_order_alone_when_asked() {
383 let got = top_tags(
384 &LOG_FC,
385 &LOG_CPM,
386 None,
387 &P_VALUE,
388 usize::MAX,
389 SortBy::None,
390 1.0,
391 )
392 .unwrap();
393 assert_eq!(got.index, vec![0, 1, 2, 3, 4, 5]);
394 for (g, w) in got.fdr.iter().zip(FDR.iter()) {
395 assert_relative_eq!(g, w, max_relative = 1e-12);
396 }
397 }
398
399 #[test]
403 fn test_top_tags_filters_on_the_adjusted_p_value() {
404 let got = top_tags(
405 &LOG_FC,
406 &LOG_CPM,
407 None,
408 &P_VALUE,
409 usize::MAX,
410 SortBy::PValue,
411 0.05,
412 )
413 .unwrap();
414 assert_eq!(got.index, vec![0, 3, 4]);
415 assert_eq!(got.p_value.len(), 3);
416 assert!(got.fdr.iter().all(|q| *q <= 0.05));
417 }
418
419 #[test]
421 fn test_top_tags_truncates_to_n() {
422 let got = top_tags(&LOG_FC, &LOG_CPM, None, &P_VALUE, 3, SortBy::PValue, 1.0).unwrap();
423 assert_eq!(got.index, vec![0, 3, 4]);
424 }
425
426 #[test]
429 fn test_top_tags_can_return_nothing() {
430 let got = top_tags(
431 &LOG_FC,
432 &LOG_CPM,
433 None,
434 &P_VALUE,
435 usize::MAX,
436 SortBy::PValue,
437 1e-9,
438 )
439 .unwrap();
440 assert!(got.index.is_empty());
441 assert!(got.log_fc.is_empty());
442 assert!(got.fdr.is_empty());
443 }
444
445 #[test]
447 fn test_top_tags_carries_a_statistic_column() {
448 let stat = [10.0, 1.0, 2.0, 9.0, 5.0, 0.5];
449 let got = top_tags(
450 &LOG_FC,
451 &LOG_CPM,
452 Some(&stat),
453 &P_VALUE,
454 usize::MAX,
455 SortBy::PValue,
456 1.0,
457 )
458 .unwrap();
459 assert_eq!(got.statistic, vec![10.0, 9.0, 5.0, 2.0, 1.0, 0.5]);
460 }
461
462 #[test]
465 fn test_top_tags_breaks_ties_the_way_r_does() {
466 let p = [0.1, 0.1, 0.1, 0.05];
467 let fc = [1.0, -3.0, 1.0, 0.5];
468 let got = top_tags(&fc, &fc, None, &p, usize::MAX, SortBy::PValue, 1.0).unwrap();
469 assert_eq!(got.index, vec![3, 1, 0, 2]);
470
471 let got = top_tags(&fc, &fc, None, &p, usize::MAX, SortBy::LogFc, 1.0).unwrap();
473 assert_eq!(got.index, vec![1, 0, 2, 3]);
474 }
475
476 #[test]
477 fn test_top_tags_rejects_bad_input() {
478 assert!(matches!(
479 top_tags(&[], &[], None, &[], 5, SortBy::PValue, 1.0),
480 Err(EdgeErrors::EmptyCounts { .. })
481 ));
482 assert!(matches!(
483 top_tags(&LOG_FC, &LOG_CPM, None, &P_VALUE, 0, SortBy::PValue, 1.0),
484 Err(EdgeErrors::MustBePositive(_))
485 ));
486 assert!(matches!(
487 top_tags(
488 &LOG_FC,
489 &LOG_CPM[..3],
490 None,
491 &P_VALUE,
492 5,
493 SortBy::PValue,
494 1.0
495 ),
496 Err(EdgeErrors::LengthMismatch {
497 name: "log_cpm",
498 ..
499 })
500 ));
501 assert!(matches!(
502 top_tags(
503 &LOG_FC,
504 &LOG_CPM,
505 None,
506 &P_VALUE[..3],
507 5,
508 SortBy::PValue,
509 1.0
510 ),
511 Err(EdgeErrors::LengthMismatch {
512 name: "p_value",
513 ..
514 })
515 ));
516 assert!(matches!(
517 top_tags(
518 &LOG_FC,
519 &LOG_CPM,
520 Some(&[1.0, 2.0]),
521 &P_VALUE,
522 5,
523 SortBy::PValue,
524 1.0
525 ),
526 Err(EdgeErrors::LengthMismatch {
527 name: "statistic",
528 ..
529 })
530 ));
531 assert!(matches!(
532 top_tags(&LOG_FC, &LOG_CPM, None, &P_VALUE, 5, SortBy::PValue, 1.5),
533 Err(EdgeErrors::InvalidArgument(_))
534 ));
535 assert!(matches!(
536 top_tags(
537 &LOG_FC,
538 &LOG_CPM,
539 None,
540 &P_VALUE,
541 5,
542 SortBy::PValue,
543 f64::NAN
544 ),
545 Err(EdgeErrors::InvalidArgument(_))
546 ));
547 }
548
549 #[test]
555 fn test_decide_tests_matches_edger_without_a_fold_change_threshold() {
556 let got = decide_tests(&P_VALUE, &LOG_FC, 0.05, 0.0).unwrap();
557 assert_eq!(got, vec![1, 0, 0, 1, 1, 0]);
558 }
559
560 #[test]
563 fn test_decide_tests_applies_the_fold_change_threshold() {
564 let got = decide_tests(&P_VALUE, &LOG_FC, 0.05, 1.0).unwrap();
565 assert_eq!(got, vec![1, 0, 0, 1, 0, 0]);
566 }
567
568 #[test]
570 fn test_decide_tests_at_a_higher_fold_change_threshold() {
571 let got = decide_tests(&P_VALUE, &LOG_FC, 0.05, 2.0).unwrap();
572 assert_eq!(got, vec![0, 0, 0, 1, 0, 0]);
573 }
574
575 #[test]
578 fn test_decide_tests_calls_the_down_direction() {
579 let got = decide_tests(&P_VALUE, &LOG_FC, 1.0, 0.0).unwrap();
580 assert_eq!(got, vec![1, -1, -1, 1, 1, -1]);
581 }
582
583 #[test]
586 fn test_decide_tests_is_strict_at_the_cutoff() {
587 assert_eq!(decide_tests(&[0.05], &[3.0], 0.05, 0.0).unwrap(), vec![0]);
589 assert_eq!(decide_tests(&[0.05], &[3.0], 0.06, 0.0).unwrap(), vec![1]);
590 }
591
592 #[test]
595 fn test_decide_tests_at_the_extremes_of_the_cutoff() {
596 assert_eq!(
597 decide_tests(&P_VALUE, &LOG_FC, 0.0, 0.0).unwrap(),
598 vec![0, 0, 0, 0, 0, 0]
599 );
600 }
601
602 #[test]
603 fn test_decide_tests_rejects_bad_input() {
604 assert!(matches!(
605 decide_tests(&P_VALUE, &LOG_FC[..3], 0.05, 0.0),
606 Err(EdgeErrors::LengthMismatch { name: "log_fc", .. })
607 ));
608 assert!(matches!(
609 decide_tests(&P_VALUE, &LOG_FC, -0.1, 0.0),
610 Err(EdgeErrors::InvalidArgument(_))
611 ));
612 assert!(matches!(
613 decide_tests(&P_VALUE, &LOG_FC, 1.1, 0.0),
614 Err(EdgeErrors::InvalidArgument(_))
615 ));
616 assert!(matches!(
617 decide_tests(&P_VALUE, &LOG_FC, f64::NAN, 0.0),
618 Err(EdgeErrors::InvalidArgument(_))
619 ));
620 assert!(matches!(
621 decide_tests(&P_VALUE, &LOG_FC, 0.05, -1.0),
622 Err(EdgeErrors::InvalidArgument(_))
623 ));
624 assert!(matches!(
625 decide_tests(&P_VALUE, &LOG_FC, 0.05, f64::INFINITY),
626 Err(EdgeErrors::InvalidArgument(_))
627 ));
628 }
629
630 #[test]
632 fn test_decide_tests_on_an_empty_table() {
633 assert!(decide_tests(&[], &[], 0.05, 0.0).unwrap().is_empty());
634 }
635}