Single Cells with edgeR

This vignette demonstrates usage of Glimma on a single cell dataset. The data here comes from brain cells from the Zeisel A et al. (2015) study on mouse brain single cells. We will use the MDS plot to perform unsupervised clustering of the cells. A pseudo-bulk single cell aggregation approach with edgeR will be used to test for differential expression, and two styles of MA plots will be used to investigate the results.

This is a simplified workflow not intended to represent best practice, but to produce reasonable looking plots in the minimal amount of code. Please refer to a resource such Orchestrating Single-Cell Analysis with Bioconductor (OSCA) for appropriate workflows for analysis.

We start by loading in the data using the scRNAseq package.

library(scRNAseq)
library(scater)
library(scran)
library(Glimma)
library(edgeR)

sce <- ZeiselBrainData(ensembl=TRUE)
#> downloading 1 resources
#> retrieving 1 resource
#> loading from cache
#> require("ensembldb")
#> Warning: Unable to map 1565 of 20006 requested IDs.

Once the data is loaded in we follow the OSCA procedure for identifying highly variable genes for creating a multi-dimensional scaling (MDS) plot. We use the functions provided by scran to identify the most highly variable genes rather than the algorithm within glimmaMDS, as scran is tailored towards single cells.

sce <- logNormCounts(sce)

var_mod <- modelGeneVar(sce)
hvg_genes <- getTopHVGs(var_mod, n=500)
hvg_sce <- sce[hvg_genes, ]

hvg_sce <- logNormCounts(hvg_sce)

Choosing to colour the MDS plot using level1class reveals separation between cell types.

glimmaMDS(
    exprs(hvg_sce),
    groups = colData(hvg_sce)
)

To demonstrate the MA plot we will perform a differential expression analysis using the pseudo-bulk approach. This involves creating pseudo-bulk samples by aggregating single cells as an analogue of biological replicates. Here the pseudo-bulk samples will be generated from combinations of level1class and level2class, the cells belonging to unique combinations of the two factors will be aggregated into samples.

colData(sce)$pb_group <-
    paste0(colData(sce)$level1class,
           "_",
           colData(sce)$level2class)

sce_counts <- counts(sce)
pb_counts <- t(rowsum(t(sce_counts), colData(sce)$pb_group))

pb_samples <- colnames(pb_counts)
pb_samples <- gsub("astrocytes_ependymal", "astrocytes-ependymal", pb_samples)
pb_split <- do.call(rbind, strsplit(pb_samples, "_"))
pb_sample_anno <- data.frame(
    sample = pb_samples,
    cell_type = pb_split[, 1],
    sample_group = pb_split[, 2]
)

With the pseudo-bulk annotations and counts we can construct a DGEList object.

pb_dge <- DGEList(
    counts = pb_counts,
    samples = pb_sample_anno,
    group = pb_sample_anno$cell_type
)

pb_dge <- calcNormFactors(pb_dge)

With this we perform differential expression analysis between “pyramidal SS” and “pyramidal CA1” samples using edgeR’s generalised linear models.

design <- model.matrix(~0 + cell_type, data = pb_dge$samples)
colnames(design) <- make.names(gsub("cell_type", "", colnames(design)))

pb_dge <- estimateDisp(pb_dge, design)

contr <- makeContrasts("pyramidal.SS - pyramidal.CA1", levels = design)

pb_fit <- glmFit(pb_dge, design)
pb_lrt <- glmLRT(pb_fit, contrast = contr)

The results of this analysis can be visualised using glimmaMA() as it would be for bulk RNA-seq.

glimmaMA(pb_lrt, dge = pb_dge)

An alternative view of the data can be constructed using the single cells in the expression plot rather than the pseudo-bulk samples. Since the MA plot is related to the expressions by only the genes in the rows, another expression matrix containing the same genes can be substituted in as below.

We construct a new DGE list from the raw single cell counts, then filter it down to just the cells used in our comparison and further down-sampled to 100 cells. This is done because Glimma does not handle a large number of cells well, the limit being a few hundred for most computers. Sampling still provides an approximate representation of the data without computation strain.

The code is not evaluated here to keep the vignette compact.

sc_dge <- DGEList(
    counts = sce_counts,
    group = colData(sce)$level1class
)

sc_dge <- sc_dge[, colData(sce)$level1class %in% c("pyramidal CA1", "pyramidal SS")]

glimmaMA(
    pb_lrt,
    dge = sc_dge[, sample(1:ncol(sc_dge), 100)]
)

Session Info

sessionInfo()
#> R version 4.4.2 (2024-10-31)
#> 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] stats4    stats     graphics  grDevices utils     datasets  methods  
#> [8] base     
#> 
#> other attached packages:
#>  [1] ensembldb_2.31.0            AnnotationFilter_1.31.0    
#>  [3] GenomicFeatures_1.59.1      AnnotationDbi_1.69.0       
#>  [5] AnnotationHub_3.15.0        BiocFileCache_2.15.0       
#>  [7] dbplyr_2.5.0                scran_1.35.0               
#>  [9] scater_1.35.0               ggplot2_3.5.1              
#> [11] scuttle_1.17.0              scRNAseq_2.20.0            
#> [13] SingleCellExperiment_1.29.1 DESeq2_1.47.1              
#> [15] SummarizedExperiment_1.37.0 Biobase_2.67.0             
#> [17] MatrixGenerics_1.19.0       matrixStats_1.4.1          
#> [19] GenomicRanges_1.59.1        GenomeInfoDb_1.43.2        
#> [21] IRanges_2.41.1              S4Vectors_0.45.2           
#> [23] BiocGenerics_0.53.3         generics_0.1.3             
#> [25] edgeR_4.5.0                 limma_3.63.2               
#> [27] Glimma_2.17.0               rmarkdown_2.29             
#> 
#> loaded via a namespace (and not attached):
#>   [1] sys_3.4.3                jsonlite_1.8.9           magrittr_2.0.3          
#>   [4] ggbeeswarm_0.7.2         gypsum_1.3.0             BiocIO_1.17.1           
#>   [7] zlibbioc_1.52.0          vctrs_0.6.5              memoise_2.0.1           
#>  [10] Rsamtools_2.23.1         RCurl_1.98-1.16          htmltools_0.5.8.1       
#>  [13] S4Arrays_1.7.1           curl_6.0.1               BiocNeighbors_2.1.1     
#>  [16] Rhdf5lib_1.29.0          SparseArray_1.7.2        rhdf5_2.51.0            
#>  [19] sass_0.4.9               alabaster.base_1.7.2     bslib_0.8.0             
#>  [22] htmlwidgets_1.6.4        alabaster.sce_1.7.0      httr2_1.0.7             
#>  [25] cachem_1.1.0             buildtools_1.0.0         GenomicAlignments_1.43.0
#>  [28] igraph_2.1.1             mime_0.12                lifecycle_1.0.4         
#>  [31] pkgconfig_2.0.3          rsvd_1.0.5               Matrix_1.7-1            
#>  [34] R6_2.5.1                 fastmap_1.2.0            GenomeInfoDbData_1.2.13 
#>  [37] digest_0.6.37            colorspace_2.1-1         dqrng_0.4.1             
#>  [40] irlba_2.3.5.1            ExperimentHub_2.15.0     RSQLite_2.3.8           
#>  [43] beachmat_2.23.2          filelock_1.0.3           fansi_1.0.6             
#>  [46] httr_1.4.7               abind_1.4-8              compiler_4.4.2          
#>  [49] bit64_4.5.2              withr_3.0.2              BiocParallel_1.41.0     
#>  [52] viridis_0.6.5            DBI_1.2.3                HDF5Array_1.35.1        
#>  [55] alabaster.ranges_1.7.0   alabaster.schemas_1.7.0  rappdirs_0.3.3          
#>  [58] DelayedArray_0.33.2      bluster_1.17.0           rjson_0.2.23            
#>  [61] tools_4.4.2              vipor_0.4.7              beeswarm_0.4.0          
#>  [64] glue_1.8.0               restfulr_0.0.15          rhdf5filters_1.19.0     
#>  [67] grid_4.4.2               cluster_2.1.6            gtable_0.3.6            
#>  [70] metapod_1.15.0           ScaledMatrix_1.15.0      BiocSingular_1.23.0     
#>  [73] utf8_1.2.4               XVector_0.47.0           ggrepel_0.9.6           
#>  [76] BiocVersion_3.21.1       pillar_1.9.0             dplyr_1.1.4             
#>  [79] lattice_0.22-6           rtracklayer_1.67.0       bit_4.5.0               
#>  [82] tidyselect_1.2.1         locfit_1.5-9.10          maketools_1.3.1         
#>  [85] Biostrings_2.75.1        knitr_1.49               gridExtra_2.3           
#>  [88] ProtGenerics_1.39.0      xfun_0.49                statmod_1.5.0           
#>  [91] UCSC.utils_1.3.0         lazyeval_0.2.2           yaml_2.3.10             
#>  [94] evaluate_1.0.1           codetools_0.2-20         tibble_3.2.1            
#>  [97] alabaster.matrix_1.7.2   BiocManager_1.30.25      cli_3.6.3               
#> [100] munsell_0.5.1            jquerylib_0.1.4          Rcpp_1.0.13-1           
#> [103] png_0.1-8                XML_3.99-0.17            parallel_4.4.2          
#> [106] blob_1.2.4               bitops_1.0-9             viridisLite_0.4.2       
#> [109] alabaster.se_1.7.0       scales_1.3.0             purrr_1.0.2             
#> [112] crayon_1.5.3             rlang_1.1.4              KEGGREST_1.47.0