--- title: "Doublet detection with doubletrs" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Doublet detection with doubletrs} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") has_seurat <- requireNamespace("Seurat", quietly = TRUE) # CRAN allows at most 2 cores; by default doubletrs uses all of them. options(doubletrs.threads = 2) ``` doubletrs detects doublets (droplets containing two cells) in single-cell RNA sequencing data with the DoubletFinder method of McGinnis, Murrow and Gartner (2019). The memory-heavy steps (artificial doublet simulation, nearest-neighbour search and pANN scoring) are written in Rust. DoubletFinder builds a dense cells x cells distance matrix; doubletrs never does. doubletrs has the same functions and arguments as DoubletFinder, so an existing workflow switches by replacing `library(DoubletFinder)` with `library(doubletrs)`. Parent cells for artificial doublets are drawn with the same R random number calls, so after the same `set.seed()` the results are identical to DoubletFinder's. ```{r} library(doubletrs) ``` ## A DoubletFinder workflow The workflow functions need Seurat. This example uses `pbmc_small`, an 80-cell dataset from SeuratObject that has already been normalised, scaled and run through PCA. ```{r, eval = has_seurat} seu <- SeuratObject::pbmc_small ``` ### 1. Choose pK `paramSweep()` computes pANN over a grid of pN and pK values. `summarizeSweep()` measures how bimodal each pANN distribution is, and `find.pK()` combines those into BCmvn. The pK with the highest BCmvn is DoubletFinder's recommended choice. ```{r, eval = has_seurat, fig.width = 6, fig.height = 3.5} set.seed(1) sweep <- paramSweep(seu, PCs = 1:5) stats <- summarizeSweep(sweep) bcmvn <- find.pK(stats) pK <- as.numeric(as.character(bcmvn$pK[which.max(bcmvn$BCmetric)])) pK ``` ### 2. Estimate the number of doublets The expected number of doublets comes from the loading density of the experiment; here we assume 7.5%. Doublets of two cells of the same type (homotypic) are hard to detect, so the estimate is scaled down by the expected homotypic fraction from cluster labels. ```{r, eval = has_seurat} homotypic <- modelHomotypic(seu$RNA_snn_res.1) nExp <- round(0.075 * ncol(seu) * (1 - homotypic)) nExp ``` ### 3. Call doublets ```{r, eval = has_seurat} set.seed(1) seu <- doubletFinder(seu, PCs = 1:5, pN = 0.25, pK = pK, nExp = nExp) calls <- grep("^DF.classifications", colnames(seu@meta.data), value = TRUE) table(seu@meta.data[[calls]]) ``` The metadata columns are named as DoubletFinder names them: `pANN___` and `DF.classifications___`. ## Lower-level functions The steps are also available without Seurat, for other preprocessing pipelines. `simulate_doublets()` averages random pairs of cells from a count matrix. After merging real cells and doublets, and normalising and running PCA on the merged matrix in the pipeline of your choice, `compute_pann()` scores each real cell. ```{r} set.seed(1) counts <- abs(Matrix::rsparsematrix(200, 100, density = 0.2)) doublets <- simulate_doublets(counts, n = 33) dim(doublets) # Stand-in for PCs of the merged matrix: real cells first, then doublets. pcs <- matrix(rnorm(133 * 5), ncol = 5) pann <- compute_pann(pcs, n_real = 100, k = 10) summary(pann) ``` `find_neighbors()` returns the neighbour indices themselves. ## Threads Every function takes a `threads` argument. The default, `doubletrs_threads()`, uses all CPU cores unless `options(doubletrs.threads = n)` is set. Results do not depend on the number of threads. ## Reference McGinnis CS, Murrow LM, Gartner ZJ (2019). DoubletFinder: Doublet detection in single-cell RNA sequencing data using artificial nearest neighbors. *Cell Systems* 8(4), 329-337.