Probabilistic Outlier Identification for RNA Sequencing Generalized Linear Models

Introduction

Relative transcript abundance has proven to be a valuable tool for understanding the function of genes in biological systems. For the differential analysis of transcript abundance using RNA sequencing data, the negative binomial model is by far the most frequently adopted. However, common methods that are based on a negative binomial model are not robust to extreme outliers, which we found to be abundant in public datasets. So far, no rigorous and probabilistic methods for detection of outliers have been developed for RNA sequencing data, leaving the identification mostly to visual inspection. Recent advances in Bayesian computation allow large-scale comparison of observed data against its theoretical distribution given in a statistical model. Here we propose ppcseq, a key quality-control tool for identifying transcripts that include outlier data points in differential expression analysis, which do not follow a negative binomial distribution. Applying ppcseq to analyse several publicly available datasets using popular tools, we show that from 3 to 10 percent of differentially abundant transcripts across algorithms and datasets had statistics inflated by the presence of outliers.

Installation and use

The input data set is a tidy representation of a differential gene transcript abundance analysis

library(dplyr)
library(ppcseq)

To install:

Before install, for linux systems, in order to exploit multi-threading, from R write:

fileConn<-file("~/.R/Makevars")
writeLines(c( "CXX14FLAGS += -O3","CXX14FLAGS += -DSTAN_THREADS", "CXX14FLAGS += -pthread"), fileConn)
close(fileConn)

Multi-threading allows the sampling or variational bayes to share the computation on multiple cores.

Then, install with

if(!requireNamespace("BiocManager", quietly = TRUE))
  install.packages("BiocManager")
BiocManager::install("ppcseq")

You can get the test dataset with

data("counts")
counts 
## # A tibble: 394,821 × 9
##    sample  symbol   logCPM    LR   PValue        FDR value       W Label      
##    <chr>   <chr>     <dbl> <dbl>    <dbl>      <dbl> <int>   <dbl> <chr>      
##  1 10922PP SLC16A12   1.39  41.1 1.46e-10 0.00000274   160 -0.129  High       
##  2 10935PP SLC16A12   1.39  41.1 1.46e-10 0.00000274   150 -0.127  High       
##  3 10973PP SLC16A12   1.39  41.1 1.46e-10 0.00000274   146 -0.426  High       
##  4 10976PP SLC16A12   1.39  41.1 1.46e-10 0.00000274   347 -0.0164 High       
##  5 10985PP SLC16A12   1.39  41.1 1.46e-10 0.00000274   175 -0.135  High       
##  6 11026PP SLC16A12   1.39  41.1 1.46e-10 0.00000274   244  0.125  High       
##  7 11045PP SLC16A12   1.39  41.1 1.46e-10 0.00000274   399 -0.0892 High       
##  8 11082PP SLC16A12   1.39  41.1 1.46e-10 0.00000274   100  0.261  Neoadjuvant
##  9 11086PP SLC16A12   1.39  41.1 1.46e-10 0.00000274    37 -0.132  Neoadjuvant
## 10 11103PP SLC16A12   1.39  41.1 1.46e-10 0.00000274    73  0.146  Neoadjuvant
## # ℹ 394,811 more rows

You can identify anrtefactual calls from your differential transcribt anundance analysis, due to outliers.

# Import libraries

if(Sys.info()[['sysname']] == "Linux")
counts.ppc = 
    counts %>%
    mutate(is_significant = FDR < 0.0001) %>%
    identify_outliers(
        formula = ~ Label,
        .sample = sample, 
        .transcript = symbol,
        .abundance = value,
        .significance = PValue,
        .do_check = is_significant,
        percent_false_positive_genes = 5, 
        approximate_posterior_inference = FALSE,
        cores = 1, 
        
        # This is ONLY for speeding up the Vignette execution
        draws_after_tail = 1
    )

The new posterior predictive check has been added to the original data frame

if(Sys.info()[['sysname']] == "Linux")
counts.ppc 
## # A tibble: 1 × 4
##   symbol   sample_wise_data   ppc_samples_failed tot_deleterious_outliers
##   <chr>    <list>                          <int>                    <int>
## 1 SLC16A12 <tibble [21 × 13]>                  0                        0

The new data frame contains plots for each gene

We can visualise the top five differentially transcribed genes

if(Sys.info()[['sysname']] == "Linux")
counts.ppc_plots = 
    counts.ppc %>% 
    plot_credible_intervals() 
if(Sys.info()[['sysname']] == "Linux")
counts.ppc_plots %>%
    pull(plot) %>% 
    .[seq_len(1)]
## [[1]]

sessionInfo()
## R version 4.4.1 (2024-06-14)
## Platform: x86_64-pc-linux-gnu
## Running under: Ubuntu 24.04.1 LTS
## 
## Matrix products: default
## BLAS:   /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3 
## LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.26.so;  LAPACK version 3.12.0
## 
## locale:
##  [1] LC_CTYPE=en_US.UTF-8       LC_NUMERIC=C              
##  [3] LC_TIME=en_US.UTF-8        LC_COLLATE=C              
##  [5] LC_MONETARY=en_US.UTF-8    LC_MESSAGES=en_US.UTF-8   
##  [7] LC_PAPER=en_US.UTF-8       LC_NAME=C                 
##  [9] LC_ADDRESS=C               LC_TELEPHONE=C            
## [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C       
## 
## time zone: Etc/UTC
## tzcode source: system (glibc)
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] ppcseq_1.15.0       rstan_2.32.6        StanHeaders_2.32.10
## [4] dplyr_1.1.4         knitr_1.48          BiocStyle_2.35.0   
## 
## loaded via a namespace (and not attached):
##  [1] benchmarkmeData_1.0.4 gtable_0.3.6          tensorA_0.36.2.1     
##  [4] xfun_0.48             bslib_0.8.0           ggplot2_3.5.1        
##  [7] QuickJSR_1.4.0        inline_0.3.19         lattice_0.22-6       
## [10] vctrs_0.6.5           tools_4.4.1           generics_0.1.3       
## [13] stats4_4.4.1          curl_5.2.3            parallel_4.4.1       
## [16] tibble_3.2.1          fansi_1.0.6           highr_0.11           
## [19] pkgconfig_2.0.3       Matrix_1.7-1          checkmate_2.3.2      
## [22] distributional_0.5.0  RcppParallel_5.1.9    lifecycle_1.0.4      
## [25] farver_2.1.2          stringr_1.5.1         compiler_4.4.1       
## [28] statmod_1.5.0         munsell_0.5.1         codetools_0.2-20     
## [31] htmltools_0.5.8.1     sys_3.4.3             buildtools_1.0.0     
## [34] sass_0.4.9            yaml_2.3.10           pillar_1.9.0         
## [37] jquerylib_0.1.4       tidyr_1.3.1           arrayhelpers_1.1-0   
## [40] cachem_1.1.0          limma_3.63.0          iterators_1.0.14     
## [43] foreach_1.5.2         abind_1.4-8           posterior_1.6.0      
## [46] tidybayes_3.0.7       tidyselect_1.2.1      locfit_1.5-9.10      
## [49] digest_0.6.37         svUnit_1.0.6          stringi_1.8.4        
## [52] purrr_1.0.2           labeling_0.4.3        maketools_1.3.1      
## [55] fastmap_1.2.0         grid_4.4.1            colorspace_2.1-1     
## [58] cli_3.6.3             magrittr_2.0.3        loo_2.8.0            
## [61] pkgbuild_1.4.5        utf8_1.2.4            withr_3.0.2          
## [64] edgeR_4.4.0           scales_1.3.0          backports_1.5.0      
## [67] httr_1.4.7            rmarkdown_2.28        matrixStats_1.4.1    
## [70] benchmarkme_1.0.8     gridExtra_2.3         coda_0.19-4.1        
## [73] evaluate_1.0.1        doParallel_1.0.17     V8_6.0.0             
## [76] ggdist_3.3.2          rstantools_2.4.0      rlang_1.1.4          
## [79] Rcpp_1.0.13           glue_1.8.0            BiocManager_1.30.25  
## [82] jsonlite_1.8.9        R6_2.5.1