scDblFinder method combines the strengths of various doublet detection approaches,
training an iterative classifier on the neighborhood of real cells and artificial doublets.
scDblFinder() has two main modes of operation: cluster-based or not.
Both perform quite well (see Germain et al., 2021).
In general, we recommend the cluster-based approach in datasets with a very clear cluster structure, and the random approach in more complex datasets.
if (!requireNamespace("BiocManager", quietly = TRUE)) install.packages("BiocManager") BiocManager::install("scDblFinder") # or, to get that latest developments: BiocManager::install("plger/scDblFinder")
The input of
scDblFinder is an object
sce of class SingleCellExperiment (empty drops having already been removed) containing at least the counts (assay ‘counts’). Alternatively, a simple count matrix can also be provided.
Given an SCE object,
scDblFinder (using the random approach) can be launched as follows :
set.seed(123) library(scDblFinder) # we create a dummy dataset; since it's small we set a higher doublet rate sce <- mockDoubletSCE(dbl.rate=0.1, ngenes=300 ) # we run scDblFinder (providing the unusually high doublet rate) sce <- scDblFinder(sce, dbr=0.1)
## Creating ~5000 artificial doublets...
## Dimensional reduction
## Evaluating kNN...
## Training model...
## iter=0, 26 cells excluded from training.
## iter=1, 25 cells excluded from training.
## iter=2, 26 cells excluded from training.
## Threshold found:0.606
## 27 (5.2%) doublets called
For 10x data, it is usually safe to leave the
dbr empty, and it will be automatically estimated.
scDblFinder will add a number of columns to the colData of
sce prefixed with ‘scDblFinder’, the most important of which are:
sce$scDblFinder.score: the final doublet score
sce$scDblFinder.class: the classification (doublet or singlet)
We can compare the calls with the truth in this toy example:
## call ## truth singlet doublet ## singlet 497 3 ## doublet 0 24
To use the cluster-based approach, one simply needs to additionally provide the
sce <- scDblFinder(sce, clusters="cluster")
## 2 clusters
## Creating ~5000 artificial doublets...
## as(<dgeMatrix>, "dgCMatrix") is deprecated since Matrix 1.5-0; do as(., "CsparseMatrix") instead
## Dimensional reduction
## Evaluating kNN...
## Training model...
## iter=0, 24 cells excluded from training.
## iter=1, 24 cells excluded from training.
## iter=2, 21 cells excluded from training.
## Threshold found:0.764
## 21 (4%) doublets called
## call ## truth singlet doublet ## singlet 500 0 ## doublet 3 21
clusters argument can be either a vector of cluster labels for each column of
sce, a colData column of
sce containing such labels, or
clusters=TRUE, the fast clustering approach (see
?fastcluster) will be employed.
If normalized expression (assay ‘logcounts’) and/or PCA (reducedDim ‘PCA’) are already present in the object, these will be used for the clustering step.
If you have multiple samples (understood as different cell captures), then it is
preferable to look for doublets separately for each sample (for multiplexed samples with cell hashes,
this means for each batch). You can do this by simply providing a vector of the sample ids to the
samples parameter of
scDblFinder or, if these are stored in a column of
colData, the name of the
column. In this case, you might also consider multithreading it using the
BPPARAM parameter (assuming you’ve got enough RAM!).
library(BiocParallel) sce <- scDblFinder(sce, samples="sample_id", BPPARAM=MulticoreParam(3)) table(sce$scDblFinder.class)
Note that if you are running multiple samples using the cluster-based approach (see below), clustering will be performed sample-wise.
While this is typically not an issue for doublet identification, it means that the cluster labels (and putative origins of doublets) won’t match between samples. If you are interested in these, it is preferable to first cluster (for example using
sce$cluster <- fastcluster(sce)) and then provide the clusters to
scDblFinder, which will ensure concordant labels across samples.
Of note, if you have very large differences in number of cells between samples the scores will not be directly comparable. We are working on improving this, but in the meantime it would be preferable to stratify similar samples and threshold the sets separately.
Wrapped in the
scDblFinder function are the following steps:
Doublets can only arise within a given sample or capture, and for this reason are better sought independently for each sample, which also speeds up the analysis. If the
samples argument is given,
scDblFinder will use it to split the cells into samples/captures, and process each of them in parallel if the
BPPARAM argument is given. The classifier will be trained globally, but thresholds will be optimized on a per-sample basis. If your samples are multiplexed, i.e. the different samples are mixed in different batches, then the batches should be what you provide to this argument.
The analysis can be considerably sped up, at little if any cost in accuracy, by reducing the dataset to only the top expressed genes (controlled by the
Then, depending on the
clusters argument, an eventual PCA and clustering (using the internal
fastcluster function) will be performed.
The rationale for the cluster-based approach is that homotypic doublets are nearly impossible to distinguish on the basis of their transcriptome, and therefore that creating that kind of doublets is a waste of computational resources that can moreover mislead the classifier into flagging singlets.
An alternative approach, however, is to generate doublets randomly (setting
clusters to FALSE or NULL), and use the iterative approach (see below) to exclude also unidentifiable artificial doublets from the training.
Depending on the
propRandom arguments, artificial doublets will be generated by combining random cells and/or pairs of non-identical clusters (this can be performed manually using the
getArtificialDoublets function). A proportion of the doublets will simply use the sum of counts of the composing cells, while the rest will undergo a library size adjustment and poisson resampling.
A new PCA is performed on the combination of real and artificial cells, from which a kNN network is generated. Using this kNN, a number of parameters are gathered for each cell, such as the proportion of doublets (i.e. artificial doublets or known doublets provided through the
knownDoublets argument, if given) among the KNN, ratio of the distances to the nearest doublet and nearest non-doublet, etc. Several of this features are reported in the output with the ‘scDblFinder.’ prefix, e.g.:
distanceToNearest: distance to the nearest cell (real or artificial)
ratio: the proportion of the KNN that are doublets. (If more than one value of
kis given, the various ratios will be used during classification and will be reported)
weighted: the proportion of the KNN that are doublets, weighted by their distance (useful for isolated cells)
score argument is set to ‘weighted’ or ‘ratio’ (in which case the aforementioned ratio is directly used as a doublet score),
scDblFinder then uses gradient boosted trees trained on the kNN-derived properties along with a few additional features (e.g. library size, number of non-zero features, and an estimate of the difficultly of detecting artificial doublets in the cell’s neighborhood, a variant of the
cxds score from the scds, etc.) to distinguish doublets (either artificial or given) from other cells, and assigns a score on this basis.
One problem of using a classifier for this task is that some of the real cells (the actual doublets) are mislabeled as singlet, so to speak.
scDblFinder therefore iteratively retrains the classifier, each time excluding from the training the (real) cells called as doublets in the previous step (as well as unidentifiable artificial doublets). The number of steps being controlled by the
iter parameter (in our experience, 2 or 3 is optimal).
This score is available in the output, in the
scDblFinder.score colData column, and can be interpreted as a probability. If the data is multi-sample, a single model is trained for all samples.
Rather than thresholding on some arbitrary cutoff of the score,
scDblFinder uses the expected number of doublets in combination to the misclassification rate to establish a threshold. Unless it is manually given through the
dbr argument, the expected doublet rate is first estimated using the empirical rule of thumb applicable to 10X data, namely roughly 1% per 1000 cells captures (so with 5000 cells, (0.01*5)*5000 = 250 doublets, and the more cells you capture the higher the chance of creating a doublet). If samples were specified, and if the
dbr is automatically calculated, thresholding is performed separately across samples.
Thresholding then tries to simultaneously minimize: 1) the classification error (in terms of the proportion of known doublets below the threshold) and 2) the deviation from the expected number of doublets among real cells (as a ratio of the total number of expected doublets within the range determined by
dbr.sd, and adjusted for homotypic doublets). This means that, if you have no idea about the doublet rate, setting
dbr.sd=1 will make the threshold depend entirely on the misclassification rate.
If artificial doublets are generated between clusters, it is sometimes possible to call the most likely origin (in terms of the combination of clusters) of a given putative real doublet.
We observed that at least one of the two composing cell is typically recognized, but that both are seldom correctly recognized, owing to the sometimes small relative contribution of one of the two original cells.
This information is provided through the
scDblFinder.mostLikelyOrigin column of the output (and the
scDblFinder.originAmbiguous column indicates whether this origin is ambiguous or rather clear).
This, in turn, allows us to identify enrichment over expectation for specific kinds of doublets.
Some statistics on each combination of clusters are saved in
metadata(sce)$scDblFinder.stats, and the
plotDoubletMap function can be used to visualize enrichments.
In addition, two frameworks are offered for testing the significance of enrichments:
clusterStickinessfunction tests whether each cluster forms more doublet than would be expected given its abundance, by default using a single quasi-binomial model fitted across all doublet types.
doubletPairwiseEnrichmentfunction separately tests whether each specific doublet type (i.e. combination of clusters) is more abundant than expected, by default using a poisson model.
scDblFinder has a fair number of parameters governing the preprocessing, generation of doublets, classification, etc. (see
?scDblFinder). Here we describe just a few of the most important ones.
The expected proportion of doublets has no impact on the density of artificial doublets in the neighborhood, but impacts the classifier’s score and, especially, where the cutoff will be placed. It is specified through the
dbr parameter and the
dbr.sd parameter (the latter specifies a +/- range around
dbr within which the deviation from
dbr will be considered null). For 10x data, the more cells you capture the higher the chance of creating a doublet, and Chromium documentation indicates a doublet rate of roughly 1% per 1000 cells captures (so with 5000 cells, (0.01*5)*5000 = 250 doublets), and the default expected doublet rate will be set to this value (with a default standard deviation of 0.015). Note however that different protocols may create considerably more doublets, and that this should be updated accordingly. If you have unsure about the doublet rate, you might consider increasing
dbr.sd, so that it is estimated mostl/purely from the misclassification error.
Then you most likely have a wrong doublet rate. If you did not provide it (
dbr argument), the doublet rate will be calculated automatically using expected doublet rates from 10x, meaning that the more cells captured, the higher the doublet rates. If you have reasons to think that this is not applicable to your data, set the
The most common cause for an unexpectedly large proportion of doublets is if you have a multi-sample dataset and did not split by samples.
scDblFinder will think that the data is a single capture with loads of cells, and hence with a very high doublet rate. Splitting by sample should solve the issue.
The thresholding tries to minimize both the deviation from the expected number of doublets and the misclassification (i.e. of artificial doublets), meaning that the effective (i.e. final) doublet rate will differ from the given one.
scDblFinder also considers false positives to be less problematic than false negatives. You can reduce to some degree the deviation from the input doublet rate by setting
Both approaches perform very similarly overall in benchmarks.
If your data is very clearly segregated into clusters, or if you are interested in the origin of the doublets, the cluster-based approach is preferable. This will also enable a more accurate accounting of homotypic doublets, and therefore a slightly better thresholding.
Otherwise, and especially if your data does not segregate very clearly into clusters, the random approach (e.g.
clusters=FALSE, the default) is preferable.
If you ran
scDblFinder on a multi-sample dataset and did not provide the cluster labels, then the labels are sample-specific (meaning that label ‘1’ in one sample might have nothing to do with label ‘1’ in another), and plotting them on a tSNE will look like they do not make sense. For this reason, when running multiple samples we recommend to first cluster all samples together (for example using
sce$cluster <- fastcluster(sce)) and then provide the clusters to
You will get this error if you have some cells that have zero reads (or a very low read count, leading to zero after feature selection). After filtering out these cells the error should go away.
Because it relies on the partly random generation of artificial doublets, running scDblFinder multiple times on the same data will yield slightly different results.
You can ensure reproducibility using
set.seed(), however this will not be sufficient when multithreading.
In such case, the seed needs to be passed to the BPPARAMs:
bp <- MulticoreParam(3, RNGseed=1234) sce <- scDblFinder(sce, clusters="cluster", samples="sample", BPPARAM=bp)
(Note that in
BiocParallel versions <1.28, one had in addition to explicitly start the cluster before the run using
bpstart(bp), and then
If the input SCE already contains a
logcounts assay or a
reducedDim slot named ‘PCA’, scDblFinder will use them for the clustering step. In addition, a clustering can be manually given using the
clusters argument of
scDblFinder(). In this way, seurat clustering could for instance be used to create the artificial doublets (see
?Seurat::as.SingleCellExperiment.Seurat for conversion to SCE).
After artificial doublets generation, the counts of real and artificial cells must then be reprocessed (i.e. normalization and PCA) together, which is performed internally using scater. If you wish this step to be performed differently, you may provide your own function for doing so (see the
processing argument in
?scDblFinder). We note, however, that the impact of variations of this step on doublet detection is rather mild. In fact, not performing any normalization at all for instance decreases doublet identification accuracy, but by rather little.
Yes, see the scATAC vignette specifically on this topic.
The input to
scDblFinder should not include empty droplets, and it might be necessary to remove cells with a very low coverage (e.g. <200 reads) to avoid errors.
Further quality filtering should be performed downstream of doublet detection, for two reasons:
1. the default expected doublet rate is calculated on the basis of the cells given, and if you excluded a lot of cells as low quality,
scDblFinder might think that the doublet rate should be lower than it is.
2. kicking out all low quality cells first might hamper our ability to detect doublets that are formed by the combination of a good quality cell with a low-quality one.
This being said, these are mostly theoretical grounds, and unless your QC filtering is very stringent (and it shouldn’t be!), it’s unlikely to make a big difference.
## R version 4.2.1 (2022-06-23) ## Platform: x86_64-pc-linux-gnu (64-bit) ## Running under: Ubuntu 20.04.5 LTS ## ## Matrix products: default ## BLAS: /home/biocbuild/bbs-3.16-bioc/R/lib/libRblas.so ## LAPACK: /home/biocbuild/bbs-3.16-bioc/R/lib/libRlapack.so ## ## locale: ##  LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C ##  LC_TIME=en_GB LC_COLLATE=C ##  LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8 ##  LC_PAPER=en_US.UTF-8 LC_NAME=C ##  LC_ADDRESS=C LC_TELEPHONE=C ##  LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C ## ## attached base packages: ##  stats4 stats graphics grDevices utils datasets methods ##  base ## ## other attached packages: ##  bluster_1.8.0 scDblFinder_1.12.0 ##  scater_1.26.0 ggplot2_3.3.6 ##  scran_1.26.0 scuttle_1.8.0 ##  ensembldb_2.22.0 AnnotationFilter_1.22.0 ##  GenomicFeatures_1.50.0 AnnotationDbi_1.60.0 ##  scRNAseq_2.11.0 SingleCellExperiment_1.20.0 ##  SummarizedExperiment_1.28.0 Biobase_2.58.0 ##  GenomicRanges_1.50.0 GenomeInfoDb_1.34.0 ##  IRanges_2.32.0 S4Vectors_0.36.0 ##  BiocGenerics_0.44.0 MatrixGenerics_1.10.0 ##  matrixStats_0.62.0 BiocStyle_2.26.0 ## ## loaded via a namespace (and not attached): ##  AnnotationHub_3.6.0 BiocFileCache_2.6.0 ##  igraph_1.3.5 lazyeval_0.2.2 ##  BiocParallel_1.32.0 digest_0.6.30 ##  htmltools_0.5.3 magick_2.7.3 ##  viridis_0.6.2 fansi_1.0.3 ##  magrittr_2.0.3 memoise_2.0.1 ##  ScaledMatrix_1.6.0 cluster_2.1.4 ##  limma_3.54.0 Biostrings_2.66.0 ##  prettyunits_1.1.1 colorspace_2.0-3 ##  ggrepel_0.9.1 blob_1.2.3 ##  rappdirs_0.3.3 xfun_0.34 ##  dplyr_1.0.10 crayon_1.5.2 ##  RCurl_1.98-1.9 jsonlite_1.8.3 ##  glue_1.6.2 gtable_0.3.1 ##  zlibbioc_1.44.0 XVector_0.38.0 ##  DelayedArray_0.24.0 BiocSingular_1.14.0 ##  scales_1.2.1 DBI_1.1.3 ##  edgeR_3.40.0 Rcpp_1.0.9 ##  viridisLite_0.4.1 xtable_1.8-4 ##  progress_1.2.2 dqrng_0.3.0 ##  bit_4.0.4 rsvd_1.0.5 ##  metapod_1.6.0 httr_1.4.4 ##  ellipsis_0.3.2 farver_2.1.1 ##  pkgconfig_2.0.3 XML_3.99-0.12 ##  sass_0.4.2 dbplyr_2.2.1 ##  locfit_1.5-9.6 utf8_1.2.2 ##  labeling_0.4.2 tidyselect_1.2.0 ##  rlang_1.0.6 later_1.3.0 ##  munsell_0.5.0 BiocVersion_3.16.0 ##  tools_4.2.1 cachem_1.0.6 ##  xgboost_188.8.131.52 cli_3.4.1 ##  generics_0.1.3 RSQLite_2.2.18 ##  ExperimentHub_2.6.0 evaluate_0.17 ##  stringr_1.4.1 fastmap_1.1.0 ##  yaml_2.3.6 knitr_1.40 ##  bit64_4.0.5 purrr_0.3.5 ##  KEGGREST_1.38.0 sparseMatrixStats_1.10.0 ##  mime_0.12 xml2_1.3.3 ##  biomaRt_2.54.0 compiler_4.2.1 ##  beeswarm_0.4.0 filelock_1.0.2 ##  curl_4.3.3 png_0.1-7 ##  interactiveDisplayBase_1.36.0 tibble_3.1.8 ##  statmod_1.4.37 bslib_0.4.0 ##  stringi_1.7.8 highr_0.9 ##  lattice_0.20-45 ProtGenerics_1.30.0 ##  Matrix_1.5-1 vctrs_0.5.0 ##  pillar_1.8.1 lifecycle_1.0.3 ##  BiocManager_1.30.19 jquerylib_0.1.4 ##  BiocNeighbors_1.16.0 cowplot_1.1.1 ##  data.table_1.14.4 bitops_1.0-7 ##  irlba_184.108.40.206 httpuv_1.6.6 ##  rtracklayer_1.58.0 R6_2.5.1 ##  BiocIO_1.8.0 bookdown_0.29 ##  promises_220.127.116.11 gridExtra_2.3 ##  vipor_0.4.5 codetools_0.2-18 ##  MASS_7.3-58.1 assertthat_0.2.1 ##  rjson_0.2.21 withr_2.5.0 ##  GenomicAlignments_1.34.0 Rsamtools_2.14.0 ##  GenomeInfoDbData_1.2.9 parallel_4.2.1 ##  hms_1.1.2 grid_4.2.1 ##  beachmat_2.14.0 rmarkdown_2.17 ##  DelayedMatrixStats_1.20.0 Rtsne_0.16 ##  shiny_1.7.3 ggbeeswarm_0.6.0 ##  restfulr_0.0.15