Chatomics Field GuideWhat They Don't Teach You

Sanity check · Single-Nucleus RNA-seq

How to Find and Remove Doublets in Single-Nucleus RNA-seq

A cluster that co-expresses two lineage markers isn't always a transitional cell state, and the fix depends on telling homotypic from heterotypic doublets before you delete anything.

By Ming "Tommy" Tang, Director of Bioinformatics in Big Pharma · Reviewed September 2026 · 5 min read

You clustered your snRNA-seq data and found a group sitting between two known lineages, expressing markers of both. Before you name it a transitional state, ask whether it's two nuclei in one droplet. At typical 10x loading, 5-10% of droplets contain a doublet, and nuclei from frozen or hard-to-dissociate tissue clump more than whole cells, so the real rate in your run is often higher than the loading table promised.

Get this wrong in either direction and you pay for it downstream. Call real biology a doublet and you erase the exact transitional or rare population your experiment was designed to find. Miss real doublets and you get spurious clusters, inflated marker gene lists, and differential expression results that don't replicate because half the "cells" driving the signal are actually two.

This page gets you to a defensible doublet call in the next hour: how to estimate your expected rate before you look at a single cluster, which detector catches which kind of doublet, why ambient RNA and intronic reads specific to nuclei data change the calculation, and when the flagged cluster is multinucleate biology you should leave alone.

What it looks like when it's happening

  • A cluster sits between two known cell types on UMAP and co-expresses canonical markers of both lineages
  • Median UMI count or gene count in one cluster is noticeably higher than in other clusters of the same apparent cell type
  • DoubletFinder or scDblFinder flags a small, clean fraction of cells but a suspicious cluster is barely touched
  • nCount_RNA / nFeature_RNA colored on UMAP shows a gradient that lines up with a cluster boundary, not just within-cluster noise
  • The observed doublet-like fraction is far above what the 10x loading table predicts for your loaded cell count
  • A 'doublet' cluster persists after removing cells flagged by one detector, but disappears when you also filter on per-cluster count inflation
  • Marker co-expression looks weak and diffuse rather than clearly bimodal, more consistent with ambient contamination than two full transcriptomes stacked

Why it happens

Droplet loading is a Poisson process: load more nuclei per droplet-generation run and the chance of catching two in one droplet rises. Standard 10x guidance puts the expected doublet fraction at roughly 4.6e-06 times the number of cells loaded, which lands most standard runs in the 5-10% range. Nuclei behave worse than intact cells here. They clump, especially after freeze-thaw and mechanical or detergent-based extraction from tissue, so observed doublet rates in snRNA-seq frequently exceed what the loading table predicts. Isolation method matters too: column-based nuclei prep runs 3.2-5.7% doublets, centrifugation and machine-assisted methods 4.5-5.5%.

Not all doublets look the same to a detector. A heterotypic doublet pairs two different cell types in one droplet, so the resulting transcriptome expresses markers of both lineages, which is exactly the signal that lands it as a fake "intermediate" cluster on UMAP. A homotypic doublet pairs two nuclei of the same type, so there's no new marker combination to detect, only inflated library size within a cluster that already looks correct. Tools built around artificial-doublet nearest-neighbor logic, including DoubletFinder, are good at catching heterotypic pairs but detect fewer than 20% of homotypic ones, because there's no expression-space signature to distinguish a homotypic doublet from a genuinely high-depth singlet.

Two nuclei-specific artifacts make this harder than in whole-cell data. First, ambient RNA from lysed cytoplasm during tissue and nuclei prep makes up a larger fraction of each droplet's counts in snRNA-seq than in scRNA-seq, and column-based isolation runs roughly double the ambient burden of centrifugation methods. Weak, diffuse co-expression of two markers can come from ambient contamination alone, which erodes the reliability of marker-co-expression as doublet evidence unless you've corrected for background first. Second, 34-58% of UMIs in nuclei data map to introns from unspliced pre-mRNA. That's real signal you want to keep, it adds genes and total counts per nucleus without distorting clustering, but if you strip it out before doublet calling you starve the detector of the library complexity it needs to separate a doublet's PC-distance profile from a singlet's.

The checks

Run them in order. Each one tells you what healthy looks like and what the problem looks like.

0/8 checked · saved in this browser

  1. Before looking at any cluster, multiply the number of cells/nuclei loaded by 4.6e-06 to get the ballpark expected doublet fraction, and compare it against your specific loading table entry.

    python
    cells_loaded = 10000
    expected_doublet_rate = cells_loaded * 4.6e-06
    print(expected_doublet_rate)
    Healthy
    Your computed rate lands close to the loading table's stated rate for that cell count, typically in the 5-10% range for standard loading.
    Red flag
    Any downstream detector flags a much larger fraction than this ceiling, which means the caller is miscalibrated (wrong pK, wrong nExp), not that your sample really has that many doublets.
  2. Note whether your prep used column-based isolation or centrifugation/machine-assisted isolation, and use the matching expected range instead of a generic scRNA-seq default.

    Healthy
    Column-based prep: 3.2-5.7% doublets. Centrifugation/machine-assisted: 4.5-5.5%. Your observed rate after correction should fall in this band, not the lower rates typical of whole-cell scRNA-seq.
    Red flag
    You used a generic 10x scRNA-seq doublet-rate assumption for a snRNA-seq column-prep sample and it's noticeably lower than 3.2-5.7%, meaning you likely underestimated your true doublet burden going into the analysis.
  3. Plot nCount_RNA/nFeature_RNA (Seurat) or n_counts/n_genes_by_counts (scanpy) on the UMAP before calling any doublets, to see whether the suspicious cluster is really a depth artifact rather than a doublet.

    r
    FeaturePlot(seu, features = c("nCount_RNA", "nFeature_RNA"))
    Healthy
    Smooth, low-magnitude variation in counts/genes across clusters, with no cluster boundary that lines up cleanly with a depth gradient.
    Red flag
    A clear gradient in UMIs or genes per nucleus that aligns with the same boundary separating your suspicious cluster from its neighbors, that's a sequencing-depth artifact riding along with, or instead of, a doublet signal.
  4. After removing empty droplets and nuclei with under ~200-500 reads, run scDblFinder on a SingleCellExperiment built from unfiltered (exon+intron) counts.

    r
    library(scDblFinder)
    sce <- scDblFinder(sce)
    table(sce$scDblFinder.class)
    Healthy
    A doublet-called fraction close to your computed expected rate from check 1, concentrated in cells with elevated counts or in clusters expressing mixed markers.
    Red flag
    scDblFinder calls a doublet fraction far above your expected ceiling, or calls almost nothing in a cluster you already suspect on marker-co-expression grounds, the second case usually means homotypic doublets it isn't built to catch.
  5. Sweep pK per dataset with paramSweep, pick the value from find.pK, then run doubletFinder with pN=0.25 and nExp set from your expected doublet rate.

    r
    sweep.res <- paramSweep(seu, PCs = 1:30, sct = FALSE)
    sweep.stats <- summarizeSweep(sweep.res, GT = FALSE)
    bcmvn <- find.pK(sweep.stats)
    
    seu <- doubletFinder(seu, PCs = 1:10, pN = 0.25,
                          pK = optimal_pK, nExp = expected_doublets)
    Healthy
    DoubletFinder's calls substantially overlap scDblFinder's calls, and both concentrate on the suspicious mixed-marker cluster.
    Red flag
    DoubletFinder and scDblFinder disagree on most flagged cells, or DoubletFinder flags almost nothing in a cluster with visibly inflated median counts, a sign you're looking at homotypic doublets neither default run will catch.
  6. Compute median nCount_RNA/nFeature_RNA per cluster and compare each cluster to others of the same apparent cell type; look for one cluster running noticeably higher without a matching increase in detected genes per UMI.

    Healthy
    Clusters of the same cell type have comparable median counts, scaled roughly by expected sequencing depth differences.
    Red flag
    One cluster has a median UMI count well above its same-type neighbors with no proportional gene diversity increase, classic homotypic doublet signature that heterotypic-tuned detectors miss.
  7. Check whether CellBender, SoupX, or DecontX was already run on the raw unfiltered matrix. If not, run one before trusting marker-co-expression as doublet evidence.

    Healthy
    Ambient contamination estimate in the 3-35% range typical of background noise, ideally on the lower end after correction; marker co-expression in flagged doublet cells stays sharp and bimodal after correction.
    Red flag
    No ambient correction was run, and the 'doublet' cluster's marker co-expression is diffuse and low-level rather than two clear expression peaks, that pattern is consistent with ambient contamination, not real doublets, especially if you used column-based nuclei isolation.
  8. Intersect the cell sets flagged by scDblFinder and DoubletFinder; treat the intersection as high-confidence doublets and manually inspect the disagreement set for single-lineage marker purity before deciding.

    Healthy
    Most flagged cells from each tool overlap, and the discordant set is small.
    Red flag
    Large discordant sets between tools, or the flagged set for a suspected doublet cluster barely moves the cluster's size or marker profile after removal, signals you need a second look (ambient correction, homotypic check) before trusting either tool alone.

What to do about it

Remove scDblFinder-flagged doublets

When: scDblFinder's doublet score clearly separates two populations and its calls agree with a second tool or with the marker-co-expression evidence.

Filter nuclei where sce$scDblFinder.class == 'doublet' (or the equivalent Seurat metadata column) using the unfiltered exon+intron count matrix, then re-cluster and confirm the suspicious mixed-marker cluster shrinks or collapses.

Caveat: scDblFinder is tuned for heterotypic pairs. A clean pass does not mean your data is doublet-free; homotypic doublets pass straight through.

Run DoubletFinder with a dataset-specific pK and a homotypic-adjusted nExp

When: You need more control over the expected-rate threshold, or you want to explicitly account for the homotypic fraction using cluster labels.

Sweep pK with paramSweep and find.pK per dataset (no default carries over), estimate the homotypic contribution with modelHomotypic against your clustering, adjust nExp accordingly, then run doubletFinder and remove the flagged cells.

Caveat: paramSweep is computationally heavy on large nuclei datasets and slows down noticeably above tens of thousands of nuclei; the pK you pick will not transfer to your next dataset.

Correct ambient RNA before or alongside doublet calling

When: Marker co-expression is your main doublet evidence and you're working with a nuclei prep known for higher ambient burden (especially column-based isolation).

Run CellBender (or SoupX/DecontX) on the raw, unfiltered droplet matrix first, then run doublet detection on the corrected counts so lineage-marker co-expression reflects real doublets rather than background.

Caveat: CellBender needs the raw unfiltered matrix and meaningful compute time; correcting counts first changes the distribution your doublet detector's defaults were tuned against, so re-check pK and thresholds afterward.

Filter to the consensus of two independent tools

When: You need a defensible call for a public dataset, or false-positive removal is costly because you're protecting a rare population.

Run scDblFinder and DoubletFinder independently, remove only nuclei both tools flag, and manually review the discordant set for single-lineage marker purity before deciding on those cells.

Caveat: This is conservative and slower, and it still misses homotypic doublets since neither tool is built to catch them.

Use multi-omic doublet detection when a second modality is available

When: You ran CITE-seq, hashing, or VDJ-seq alongside RNA and homotypic doublets specifically matter for your question, for example dosage-sensitive gene analysis.

Use a method like MLtiplet that incorporates the second modality; homotypic doublets invisible in RNA space often show a distinct signal in hashing counts, ATAC fragments, or antibody counts.

Caveat: Only usable if the second assay was captured at generation time, this is not retrofittable onto RNA-only data you already have.

When not to "fix" it

If your tissue is cardiac, hepatic, skeletal muscle, or another source with naturally binucleate or multinucleate cells, two-nuclei signal from a single physical cell is expected biology, not a droplet doublet, and removing it will bias your dataset toward mononuclear cell types. Check the literature for that cell type's expected multi-nucleation rate before filtering anything flagged in that population. Similarly, if the flagged cluster sits on a known differentiation trajectory and shows a smooth, ordered marker gradient rather than an abrupt bimodal jump between two unrelated lineages, you're probably looking at a real transitional state; run the count-inflation check (per-cluster median UMI/gene counts) before deleting it, since true intermediate cells don't carry the library-size inflation that homotypic or heterotypic doublets do.

Five things experienced analysts do here

  1. Compute the expected doublet rate from your loading concentration before you look at a single cluster, so you know the ceiling on what a caller should flag; if 30% of a cluster gets called doublet, the caller is miscalibrated, not the biology.
  2. For tissue with naturally binucleate or multinucleate cells (cardiomyocytes, hepatocytes, osteoclasts), check that cell type's expected multi-nucleation rate in the literature before removing anything flagged in that population.
  3. Never lean on marker co-expression as doublet evidence before you've corrected ambient RNA; snRNA-seq's higher ambient burden from lysed cytoplasm can fake weak heterotypic signal on its own.
  4. Keep intronic reads in the count matrix used for doublet detection. Restricting to exon-only counts throws away 34-58% of UMIs in nuclei data and starves the detector of the library complexity it needs.
  5. Write 'homotypic doublets not assessed' into your QC notes if you only ran a heterotypic-tuned tool like DoubletFinder or scDblFinder; don't claim a clean doublet rate from a single detector that can't see that failure mode.

Questions people ask

What's a normal doublet rate for snRNA-seq?

Roughly 5-10% at typical 10x loading, using the linear relationship of about 4.6e-06 times cells loaded. Nuclei clumping during frozen-tissue prep tends to push real rates above what the loading table predicts, and the exact range depends on isolation method: 3.2-5.7% for column-based prep, 4.5-5.5% for centrifugation or machine-assisted prep.

Does DoubletFinder work on snRNA-seq the same way as scRNA-seq?

Mechanically yes, the same paramSweep/pK/pN/nExp workflow applies. But it's tuned for heterotypic doublets and catches fewer than 20% of homotypic ones, so pair it with a per-cluster count-inflation check or a second detector before trusting a low doublet count.

Should I remove doublets before or after ambient RNA correction?

Correct ambient RNA first with CellBender, SoupX, or DecontX. snRNA-seq carries a higher ambient fraction than scRNA-seq from lysed cytoplasm during nuclei prep, and that contamination alone can produce weak marker co-expression that looks like a heterotypic doublet signal.

Can scDblFinder be used directly on snRNA-seq data?

Yes. It takes a SingleCellExperiment and is recommended inside optimized snRNA-seq preprocessing pipelines (CellBender, then scDblFinder, then QC filtering and normalization). Remove empty droplets and nuclei with fewer than roughly 200-500 reads first.

How do I catch homotypic doublets that DoubletFinder misses?

Check whether a cluster's median UMI or gene count is inflated relative to what its library size alone predicts, since homotypic doublets don't produce a new marker combination to detect. If you have a second modality (CITE-seq or VDJ-seq), a multi-omic method like MLtiplet detects over 70% of homotypic doublets versus under 20% for DoubletFinder.

Related pages

Related reading on the blog

Sources

  1. DoubletFinder: Doublet Detection in Single-Cell RNA Sequencing Data Using Artificial Nearest Neighbors — DoubletFinder workflow (paramSweep, pK, pN, nExp) and its heterotypic-only detection limitation
  2. Tuning hyperparameters of doublet-detection methods for single-cell RNA sequencing data — scDblFinder hyperparameter ranges (nf, pc, depth)
  3. Double-jeopardy: scRNA-seq doublet/multiplet detection using multi-omic profiling — Homotypic vs heterotypic doublet detection rates, DoubletFinder vs MLtiplet
  4. Comparative analysis of nuclei isolation methods for brain single-nucleus RNA sequencing — 10x loading doublet rate formula and isolation-method-specific doublet rates
  5. The impact of ambient contamination on demultiplexing methods for single-nucleus multiome experiments — Column-based isolation doubling ambient RNA contamination versus centrifugation
  6. DoubletDecon: Cell-State Aware Removal of Single-Cell RNA-Seq Doublets — Recommendation to correct low-quality singlets and use a consensus of multiple doublet tools

Part of the Doublets series.