--- title: Elaborating on cell-based quality control in single-cell RNA-seq data author: - name: Aaron T. L. Lun affiliation: &CRUK Cancer Research UK Cambridge Institute, Li Ka Shing Centre, Robinson Way, Cambridge CB2 0RE, United Kingdom - name: Davis J. McCarthy affiliation: - &EMBL EMBL European Bioinformatics Institute, Wellcome Genome Campus, Hinxton, Cambridge CB10 1SD, United Kingdom - St Vincent's Institute of Medical Research, 41 Victoria Parade, Fitzroy, Victoria 3065, Australia - name: John C. Marioni affiliation: - *CRUK - *EMBL - Wellcome Trust Sanger Institute, Wellcome Genome Campus, Hinxton, Cambridge CB10 1SA, United Kingdom date: "`r Sys.Date()`" vignette: > %\VignetteIndexEntry{06. Quality control details} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} output: BiocStyle::html_document: titlecaps: false toc_float: true bibliography: ref.bib --- ```{r style, echo=FALSE, results='hide', message=FALSE, cache=FALSE} library(BiocStyle) library(knitr) opts_chunk$set(error=FALSE, message=FALSE, warning=FALSE, cache=TRUE) opts_chunk$set(fig.asp=1) ``` ```{r, cache=FALSE, echo=FALSE, results="hide"} simpleSingleCell:::.compile("reads") # 416B simpleSingleCell:::.compile("tenx") # PBMC ``` # Overview Low-quality cells can often yield misleading results in downstream analyses, by: - forming their own distinct cluster(s), complicating interpretation of the results. This can be most obviously driven by increased mitochondrial proportions or enrichment for nuclear RNAs after cell damage. However, very small libraries can also form their own clusters due to shifts in the mean upon log-transformation. - containing genes that appear to be strongly "upregulated" due to the presence of very small size factors. This is most problematic if some transcripts are present at constant levels in the ambient solution for all cells (i.e., wells or droplets). Small counts will then be greatly inflated upon normalization with these size factors. - containing genes that appear to be strongly "downregulated" due to the loss of RNA upon cell damage. This seems most pronounced with ribosomal protein genes, though other cytoplasmic transcripts are likely to be affected. - distorting the characterization of population heterogeneity during variance estimation or principal components analysis. The first few principal components will capture differences in quality rather than biology, reducing the effectiveness of dimensionality reduction. Similarly, genes with the largest variances will be driven by differences between low- and high-quality cells. As such, we need to remove these cells at the start of the analysis. Recall that we were defining low-quality cells as those with outlier values for various quality control (QC) metrics, using the `isOutlier()` and `calculateQCMetrics()` functions from the `r Biocpkg("scater")` package [@mccarthy2017scater]. Here, we will examine some of the reasoning behind the outlier-based QC in more detail. # Assumptions of outlier identification An outlier-based definition for low-quality cells assumes that most cells are of high quality. This is usually reasonable and can be experimentally supported in some situations by visually checking that the cells are intact, e.g., on the microwell plate. Another assumption is that the QC metrics are independent on the biological state of each cell. This ensures that any outlier values for these metrics are driven by technical factors rather than biological processes. Thus, removing cells based on the metrics will not misrepresent the biology in downstream analyses. The second assumption is most likely to be violated in highly heterogeneous cell populations. For example, some cell types may naturally have less RNA or express fewer genes than other cell types. Such cell types are more likely to be considered outliers and removed, even if they are of high quality. The use of the MAD mitigates this problem by accounting for biological variability in the QC metrics. A heterogeneous population should have higher variability in the metrics among high-quality cells, increasing the MAD and reducing the chance of incorrectly removing particular cell types (at the cost of reducing power to remove low-quality cells). Nonetheless, filtering based on outliers may not be appropriate in extreme cases where one cell type is very different from the others. Systematic differences in the QC metrics can be handled to some extent using the `batch=` argument in the `isOutlier()` function. For example, setting `batch` to the plate of origin will identify outliers within each level of `batch`, using plate-specific median and MAD estimates. This is obviously useful for accommodating known differences in experimental processing, e.g., sequencing at different depth or different amounts of added spike-in RNA. We can also include biological factors in `batch`, if those factors could result in systematically fewer expressed genes or lower RNA content. However, this is not applicable in experiments where the factors are not known in advance. # Checking for discarded cell types ## In the 416B data set We can diagnose loss of distinct cell types during QC by looking for differences in gene expression between the discarded and retained cells. To demonstrate, we compute the average count across the discarded and retained pools in the 416B data set. ```{r} library(SingleCellExperiment) sce.full.416b <- readRDS("416B_preQC.rds") library(scater) lost <- calcAverage(counts(sce.full.416b)[,!sce.full.416b$PassQC]) kept <- calcAverage(counts(sce.full.416b)[,sce.full.416b$PassQC]) ``` If the discarded pool is enriched for a certain cell type, we should observe increased expression of the corresponding marker genes. No systematic upregulation of genes is apparent in the discarded pool in Figure \@ref(fig:discardplot416b), indicating that the QC step did not inadvertently filter out a cell type in the 416B dataset. ```{r discardplot416b, fig.cap="Average counts across all discarded and retained cells in the 416B dataset. Each point represents a gene, with spike-in and mitochondrial transcripts in red and blue respectively."} # Avoid loss of points where either average is zero. capped.lost <- pmax(lost, min(lost[lost>0])) capped.kept <- pmax(kept, min(kept[kept>0])) plot(capped.lost, capped.kept, xlab="Average count (discarded)", ylab="Average count (retained)", log="xy", pch=16) is.spike <- isSpike(sce.full.416b) points(capped.lost[is.spike], capped.kept[is.spike], col="red", pch=16) is.mito <- rowData(sce.full.416b)$is_feature_control_Mt points(capped.lost[is.mito], capped.kept[is.mito], col="dodgerblue", pch=16) ``` We examine this more closely by computing log-fold changes between the average counts of the two pools. The `predFC` function stabilizes the log-fold change estimates by adding a prior count to the average of each pool. We only examine the log-fold changes rather than formally testing for differential expression, as we are not interested in penalizing intra-pool heterogeneity. ```{r} library(edgeR) coefs <- predFC(cbind(lost, kept), design=cbind(1, c(1, 0)))[,2] info <- data.frame(logFC=coefs, Lost=lost, Kept=kept, row.names=rownames(sce.full.416b)) head(info[order(info$logFC, decreasing=TRUE),], 20) ``` Again, no obvious cell type markers are present in the top set of genes upregulated in the discarded pool. The magnitude of the log-fold changes is less important, attributable to imprecision with few cells in the discarded pool. Large log-fold changes can also be driven by enrichment or depletion of mitochondrial, ribosomal protein or nuclear genes upon cell damage. ## In the PBMC data set For comparison, we consider the PBMC data set in which we previously identified a platelet population (see the `r simpleSingleCell:::.link("tenx", "Marker gene detection", "previous workflow")`). Recall that we relied on the use of the `emptyDrops()` method from the `r Biocpkg("DropletUtils")` package to retain the platelets. In contrast, if we had used a naive threshold on the total unique molecular identifier (UMI) count, we would have removed this population during the cell calling step. ```{r} sce.pbmc <- readRDS("pbmc_data.rds") wrong.keep <- sce.pbmc$total_counts >= 1000 lost <- calcAverage(counts(sce.pbmc)[,!wrong.keep]) kept <- calcAverage(counts(sce.pbmc)[,wrong.keep]) ``` The presence of a distinct population in the discarded pool manifests in Figure \@ref(fig:discardplotpbmc) as a shift to the bottom-right for a number of genes. This includes _PF4_, _PPBP_ and _SDPR_ that are strongly upregulated in the platelets. ```{r discardplotpbmc, fig.cap="Average counts across all discarded and retained cells in the PBMC dataset, after using a more stringent filter on the total UMI count. Each point represents a gene, with platelet-related genes highlighted in orange."} # Avoid loss of points where either average is zero. capped.lost <- pmax(lost, min(lost[lost>0])) capped.kept <- pmax(kept, min(kept[kept>0])) plot(capped.lost, capped.kept, xlab="Average count (discarded)", ylab="Average count (retained)", log="xy", pch=16) platelet <- c("PF4", "PPBP", "SDPR") points(capped.lost[platelet], capped.kept[platelet], col="orange", pch=16) ``` These platelet-specific genes are also present among the top set of positive log-fold changes. ```{r} coefs <- predFC(cbind(lost, kept), design=cbind(1, c(1, 0)))[,2] info <- data.frame(logFC=coefs, Lost=lost, Kept=kept, row.names=rownames(sce.pbmc)) head(info[order(info$logFC, decreasing=TRUE),], 20) ``` ## Avoiding loss of cell types If cell types are being incorrectly discarded, the most direct solution is to relax the QC filters by increasing `nmads=` in the `isOutlier()` calls. We can also avoid filtering on metrics that are associated with genuine biological differences between cell types. The most extreme approach would be to not perform any QC filtering at all, thus guaranteeing that all cell types in the data are retained. However, this obviously comes with an increased risk of retaining more low-quality damaged cells. Such cells will cause problems in downstream analyses as discussed above, which motivates the use of a more strict filter (at least on the first pass) in our workflows. As an aside, it is worth mentioning that the true technical quality of a cell may be correlated with its type. (This differs from a correlation between the cell type and the QC metrics, as the latter are our imperfect proxies for quality.) This can arise if some cell types are not amenable to dissociation or microfluidics handling during the scRNA-seq protocol. In such cases, it is possible to correctly discard an entire cell type during QC if all of its members are damaged. Indeed, concerns over the computational removal of cell types during QC are probably minor compared to losses in the experimental protocol. # Alternative approaches to quality control ## Using fixed thresholds One alternative strategy is to set pre-defined thresholds on each QC metric. For example, we might remove all cells with library sizes below 100000 and numbers of expressed genes below 4000. This avoids any assumptions associated with the use of outliers to identify low-quality cells. However, it generally requires considerable experience to determine appropriate thresholds for each experimental protocol and biological system. For example, thresholds for read count-based data are simply not applicable for UMI-based data, and vice versa. Indeed, even with the same protocol and system, the appropriate threshold can vary from run to run due to the vagaries of RNA capture and sequencing. ## Using PCA-based outliers Another strategy is to perform a principal components analysis (PCA) based on the quality metrics for each cell, e.g., the total number of reads, the total number of features and the proportion of mitochondrial or spike-in reads. Outliers on a PCA plot may be indicative of low-quality cells that have aberrant technical properties compared to the (presumed) majority of high-quality cells. This is demonstrated below on a brain cell dataset from @tasic2016adult, using functions from `r Biocpkg("scater")`. ```{r} # Obtaining the dataset. library(scRNAseq) data(allen) # Setting up the data. sce.allen <- as(allen, "SingleCellExperiment") assayNames(sce.allen) <- "counts" isSpike(sce.allen, "ERCC") <- grep("ERCC", rownames(sce.allen)) # Computing the QC metrics and running PCA. library(scater) sce.allen <- calculateQCMetrics(sce.allen) sce.allen <- runPCA(sce.allen, use_coldata=TRUE, detect_outliers=TRUE) table(sce.allen$outlier) ``` Methods like PCA-based outlier detection and support vector machines can provide more power to distinguish low-quality cells from high-quality counterparts [@ilicic2016classification]. This is because they are able to detect subtle patterns across many quality metrics simultaneously. However, this comes at some cost to interpretability, as the reason for removing a given cell may not always be obvious. Users interested in the more sophisticated approaches are referred to the `r Biocpkg("scater")` and `r Biocpkg("cellity")` packages. ## Using the gene expression profiles For completeness, we note that outliers can also be identified from the gene expression profiles, rather than QC metrics. We consider this to be a risky strategy as it can remove high-quality cells in rare populations. Even if subpopulations are explicitly captured with a mixture model, removal of outlier cells will simply reinforce the existing model. This may be misleading if it understates the biological heterogeneity in each population. # Concluding remarks All software packages used in this workflow are publicly available from the Comprehensive R Archive Network (https://cran.r-project.org) or the Bioconductor project (http://bioconductor.org). The specific version numbers of the packages used are shown below, along with the version of the R installation. ```{r} sessionInfo() ``` # References