Sanity check · Single-Cell RNA-seq
How to Find and Remove Doublets in Single-Cell RNA-seq
A bridge cluster co-expressing two lineage markers is not a transitional cell state until you've ruled out that it's two cells sharing one droplet.
By Ming "Tommy" Tang, Director of Bioinformatics in Big Pharma · Reviewed September 2026 · 5 min read
You're staring at a UMAP with a cluster wedged between your T cells and your monocytes, and it lights up for CD3E and CD14 at the same time. Your first instinct is excitement: a novel transitional population, maybe worth a figure. Your second instinct, if you've been burned before, is to check whether that cluster is actually two cells that landed in the same droplet.
This matters because doublets don't just add noise, they add a fake cell type. At typical 10x Chromium loading, 5-10% of droplets capture two cells instead of one, and when those two cells come from different lineages, the resulting library reads as a biologically plausible intermediate. If you don't catch it, you'll write a marker gene table for a population that doesn't exist, and a reviewer running scDblFinder on your public data will catch it for you.
In the next hour, you can run scDblFinder or Scrublet on your data, check whether the doublet calls concentrate in your suspect cluster, and decide whether to drop those cells and re-cluster. This page also covers the doublets these tools can't see, and the biological look-alikes you shouldn't remove.
What it looks like when it's happening
- A cluster sits geometrically between two otherwise distinct clusters on the UMAP and won't resolve into either one no matter what resolution you try.
- That cluster's dot plot or feature plot lights up for two lineage markers you'd never expect in the same cell, like CD3E and CD14 together.
- Median nCount_RNA and nFeature_RNA for the suspect cluster run well above the rest of the dataset.
- The scDblFinder or Scrublet doublet score histogram is bimodal, and the suspect cluster is enriched in the upper mode.
- The fraction of cells called doublets barely resembles the expected 10x loading rate for your target cell recovery.
- Doublet calls change substantially depending on whether you ran detection per sample or on the pooled multi-sample object.
- In a species-mixing (barnyard) control, a chunk of barcodes sit in the 70-90% single-species read range instead of clearing 90%+.
Why it happens
10x Chromium loading follows Poisson statistics: to recover enough single cells per run, you have to overload the lane slightly, which means a predictable fraction of droplets pick up two cells instead of one. That fraction scales with how many cells you load, not with your biology, which is why scDblFinder's default expected rate is expressed per 1,000 cells recovered (about 0.8% per 1,000 for standard 10x, roughly half that for the HT chemistry). A lane pushed hard for cell yield will always show a higher doublet rate than a lightly loaded one, independent of the cell types inside it.
When the two co-encapsulated cells come from different lineages, you get a heterotypic doublet: a single barcode carrying transcripts from, say, a T cell and a monocyte at once. Cluster it with everything else and it lands geometrically between the two parent populations, because its expression profile really is a mixture of both. This is the bridge cluster that looks like a transitional cell state, and it's also the case computational tools are good at catching: simulation-based methods (scDblFinder, Scrublet) build artificial doublets by summing random cell pairs, then check whether real cells look more like those synthetic mixtures than like clean singlets.
Homotypic doublets are the opposite problem: two cells of the same or transcriptionally similar type land in one droplet, and the resulting profile is just a scaled-up version of a normal cell of that type. There's no mixture signature to detect, so these are nearly invisible to every current method, sensitivity here is far below the >90% heterotypic detection rate. They still inflate that cell's counts and can distort things like cell-cycle or size-dependent QC metrics, you just can't point at a specific cell and prove it's one of them.
Doublets can only form between two cells captured in the same GEM well, so the base rate is a property of a single 10x lane, not of your whole study. Run scDblFinder or Scrublet on a merged object spanning multiple samples and you corrupt the artificial-doublet simulation and the neighborhood density it's built on, inflating or deflating the estimate. Two adjacent artifacts make this worse in practice: ambient RNA background (3-35% of counts per cell, see [[correcting-ambient-rna]]) raises baseline expression noise that can mimic low-level mixture signal, and intronic reads legitimately push gene and UMI counts up by 40-60% (see [[intronic-reads-10x-scrna]]), so a high-count cluster on its own is not proof of doublets, it's a reason to look closer.
The checks
Run them in order. Each one tells you what healthy looks like and what the problem looks like.
0/7 checked · saved in this browser
Plot nCount_RNA and nFeature_RNA (Seurat) or total_counts/n_genes_by_counts (Scanpy) as violin plots grouped by cluster, and compare medians.
- Healthy
- Distributions overlap across clusters, with modest, biologically explainable variance (e.g., proliferating cells running a bit higher).
- Red flag
- One cluster's median UMI or gene count sits 1.5-2x or more above every other cluster, with no cell-cycle or size explanation.
Dot plot or feature plot two markers from lineages you don't expect to co-occur (e.g., CD3E for T cells and CD14 for monocytes) across all clusters.
- Healthy
- Markers are mutually exclusive: each cluster expresses one lineage's markers, not both.
- Red flag
- A single cluster shows strong expression of two independent lineage marker sets simultaneously.
Run scDblFinder separately for each 10x lane/sample using the samples argument, not on a merged multi-sample object, and inspect the resulting class calls.
rlibrary(scDblFinder) library(BiocParallel) bp <- MulticoreParam(3, RNGseed = 1234) sce <- scDblFinder(sce, samples = "sample_id", BPPARAM = bp) table(sce$scDblFinder.class)- Healthy
- Called doublet fraction roughly matches the expected rate for your loading (commonly 5-10% of cells).
- Red flag
- Called fraction is near 0% (dbr misconfigured, or run accidentally used the pooled object) or far above 15-20%.
Run sc.pp.scrublet on a raw, unnormalized count matrix per sample, then plot the doublet score distribution.
pythonsc.pp.scrublet(adata, expected_doublet_rate=0.06, sim_doublet_ratio=2.0, n_prin_comps=30) adata.obs['predicted_doublet'].value_counts() sc.pl.scrublet_score_distribution(adata)- Healthy
- A bimodal score histogram with a clear valley between singlet and doublet modes, and a threshold that lands in that valley.
- Red flag
- A unimodal histogram with no visible valley, meaning Scrublet is forcing a cut with no real separation to base it on.
Build a contingency table of cluster assignment versus doublet class (table() in R, pd.crosstab in Python) and compute the doublet rate per cluster.
- Healthy
- Doublet calls are spread roughly evenly at the background rate across clusters.
- Red flag
- Your suspect bridge cluster shows a doublet rate several times the dataset-wide background, e.g., 50-80% doublet-called versus 5-10% elsewhere.
If your cell types are clearly separable (e.g., major immune lineages), pass pre-computed cluster labels to scDblFinder's cluster-based mode, which improves heterotypic sensitivity over the default random-pairing approach.
rsce <- scDblFinder(sce, clusters = "cluster")- Healthy
- Doublet calls concentrate more tightly on cells at cluster boundaries, sharpening the separation you saw with the random approach.
- Red flag
- Cluster-based mode barely changes anything, suggesting your clusters aren't cleanly segregated enough for this mode to add power over the default.
If you ran a mixed-species control, compute the fraction of reads per barcode mapping to each species' genome.
- Healthy
- A bimodal distribution with most barcodes above roughly 90% reads to one species (packet notes 70-90% has been used as a cutoff, with kallisto|bustools using 90%).
- Red flag
- A meaningful fraction of barcodes sit in the 70-90% ambiguous zone, indicating a real multiplet problem on that lane independent of any computational call.
What to do about it
Subset to singlets and re-cluster from scratch
When: Marker co-expression and a doublet-detection tool both flag the bridge cluster, and the per-cluster doublet rate check confirms enrichment there.
Keep only cells called singlet by scDblFinder or Scrublet (per sample), then rerun normalization, variable feature selection, PCA, neighbors, and clustering from the beginning rather than just relabeling the existing embedding.
Caveat: Cluster numbers, boundaries, and marker lists will shift after re-embedding; rerun any downstream differential expression against the new cluster labels, don't patch old results onto them.
Apply a homotypic proportion adjustment instead of trusting the raw score
When: You suspect homotypic doublets inside a single annotated cluster (transcriptionally similar cells, no mixture signature to catch).
Use DoubletFinder's homotypic adjustment, the sum of squared per-cluster frequencies, to correct the expected doublet count (nExp) for that cluster before scoring, rather than relying on the raw pANN cutoff alone.
Caveat: This adjusts how many cells you expect to flag, not which specific cells are doublets; individual homotypic doublets stay scientifically unidentifiable, treat the number as a contamination estimate, not per-cell ground truth.
Add HTO-based demultiplexing for multiplexed samples
When: Samples were hashtagged or CITE-seq multiplexed and pooled onto the same lane.
Run HTODemux or MULTIseqDemux to flag cells carrying more than one hashtag as inter-sample doublets, in addition to running scDblFinder or Scrublet within each sample for intra-sample doublets.
Caveat: HTO calls only catch doublets formed between two different samples; two cells from the same sample sharing one hashtag still need the transcriptome-based tool to be caught at all.
Use a species-mapping threshold as ground truth in barnyard designs
When: You have a mixed-species (barnyard) experiment available, even just as a pilot lane.
Threshold cells on percent of reads mapping to each species genome (roughly 70-90%, per the packet's cited practice) to call doublets directly, then use that as a sanity check on how well scDblFinder or Scrublet perform on your data before trusting them on single-species runs.
Caveat: Barnyard doublet rates measure sensitivity for distantly related species pairs; they don't tell you how well the same tool finds doublets between two similar human cell types.
Set the expected doublet rate from your actual loading, not a vignette default
When: Your dbr (scDblFinder) or expected_doublet_rate (Scrublet) parameter was copied from a tutorial rather than computed from your chip.
Recompute the expected rate from the cells loaded and recovered per lane using 10x's published loading table, and pass that value into scDblFinder(dbr=...) or sc.pp.scrublet(expected_doublet_rate=...) instead of the tool's default.
Caveat: If the true loading concentration is unknown (common with core-facility-processed data), treat the default as a rough placeholder and weight the marker co-expression and count-outlier checks more heavily than the absolute score cutoff.
When not to "fix" it
A cluster sitting between two others and co-expressing two markers is not automatically a doublet artifact. Genuine transitional states in a differentiation trajectory (monocyte-to-macrophage, early erythro-myeloid progenitors) form a continuous gradient across pseudotime, not a small tight island, and their per-cell UMI and gene counts stay in the normal range instead of running high like a real doublet cluster. Real cell-cell biology also produces two-lineage transcriptomes in one droplet: platelets adhering to leukocytes, macrophages mid-efferocytosis, or genuine cell fusion. Removing those erases signal you were probably looking for, not noise. Before dropping a cluster, check whether it forms a gradient with normal counts (biology) or a tight population wedged between two others with elevated counts and a strong doublet-tool signal (artifact).
Five things experienced analysts do here
- Always run doublet calling per sample or per GEM well, never on a pooled multi-sample object, since doublets can only form inside a single droplet run.
- Treat the doublet score as continuous evidence to combine with marker co-expression and count outliers, not a single binary gate you accept without a second check.
- Homotypic doublets are a wall you can't compute your way through: clusters of similar cells will always host uncaught doublets, so lean on cluster-level count sanity checks instead of expecting the tool to flag them individually.
- Sanity check that your called doublet fraction tracks the number of cells you actually loaded on the chip, using 10x's own loading-rate table, rather than accepting whatever default rate a vignette happened to use.
- Do doublet removal before locking in your clustering resolution; if a bridge cluster gets baked into your marker calls first, you risk publishing an artifact as a discovery.
Questions people ask
- Should I remove doublets before or after clustering?
Run an initial clustering to get provisional cluster labels, use those labels (optionally) for scDblFinder's cluster-based mode, then subset out the flagged doublets and re-run PCA and clustering from scratch for your final analysis. Don't just relabel the original embedding, the geometry itself is distorted by the doublets you're removing.
- What's a normal doublet rate for 10x scRNA-seq data?
Typically 5-10% of droplets at standard 10x loading, scaling with how many cells you load per lane. scDblFinder's default expected rate is about 0.8% per 1,000 cells recovered for standard 10x chemistry, roughly half that for the HT kits.
- scDblFinder vs Scrublet vs DoubletFinder, which should I use?
Match your ecosystem: scDblFinder if you're in Bioconductor/SingleCellExperiment, Scrublet if you're in Scanpy/AnnData, DoubletFinder if you're in Seurat and want its pANN scoring and homotypic adjustment. There's no head-to-head benchmark in the sources behind this page, so pick based on pipeline fit rather than a claimed accuracy edge.
- Can doublet detection tools catch homotypic doublets?
No, not reliably. Two cells of the same or very similar type produce a transcriptome that's essentially indistinguishable from a single cell of that type, so heterotypic detection sensitivity above 90% does not carry over to homotypic doublets, which remain nearly invisible to every current method.
- Do I still need doublet detection if my samples are hashtagged?
Yes. Hashtag (HTO) demultiplexing only catches doublets formed from two different samples sharing a lane. Two cells from the same sample, and therefore the same hashtag, still look like a single hashed cell and need a transcriptome-based tool like scDblFinder or Scrublet to be caught.
Related pages
- Guide · How to Log-Transform Counts Without Fooling Yourself in Single-Cell RNA-seq
- Guide · How to Choose Cell QC Thresholds in Spatial Transcriptomics
- Guide · How to Detect Batch Effects in Spatial Transcriptomics
- Guide · How to Detect Integration Over-Correction in Spatial Transcriptomics
- Guide · How to Sanity-Check Marker Genes and Cell Type Labels in Spatial Transcriptomics
- Glossary · Spatial transcriptomics
Related reading on the blog
Sources
- Doublet Detection in Advanced Single-Cell Analysis with Bioconductor (OSCA) — Heterotypic vs homotypic doublets, detection strategies, and why there's no fixed threshold
- scDblFinder: Doublet Detection in Single-Cell Data — scDblFinder commands, dbr defaults, per-sample and cluster-based modes
- scanpy.pp.scrublet, Scanpy API Documentation — Scrublet parameters and output fields used in the checks
- DoubletFinder: Doublet Detection in Single-Cell RNA Sequencing Data Using Artificial Nearest Neighbors — pANN scoring and the homotypic proportion adjustment used in the fixes
- Mixing Mouse and Human 10x Single-Cell RNA-seq Data — Species-mapping ratio thresholds used in the barnyard check and fix
Part of the Doublets series.