Doublets and ambient RNA: the two artefacts that invent cell types
How to tell a real rare population from a doublet cluster or ambient contamination, which tools to use, and the order to run them in.
Do this on your own data: the expected doublet rate depends on how many cells you loaded, so the number in this guide is not yours. The free QC report computes the expected doublet count from your own matrix: omics.hstgenomics.com/qc-check. A worked example with the QC steps in order: omics.hstgenomics.com/sample.
Want it done rather than explained? We do analysis only — send a count matrix, an h5ad or Seurat object, Cell Ranger output or a public accession and get publication-ready results with the code that produced them, at a fixed price agreed before any work starts: omics.hstgenomics.com/quote.
Almost every "novel intermediate population" that does not replicate is one of two things: two cells captured in one droplet, or free-floating RNA that got counted in everybody's droplet. Both produce clusters that look biologically exciting and are entirely technical.
Doublets
On droplet platforms the doublet rate scales with loading — roughly 0.8% per 1,000 cells recovered on standard 10x 3' v3.1 chemistry, so a 10,000 cell run carries about 8% (their published table: 0.4% at 500 cells, 0.8% at 1,000, 3.9% at 5,000, 7.6% at 10,000). High-throughput and newer GEM-X chemistries are lower, so check the figure for the kit you actually used. Those doublets cluster together, because a T cell plus a monocyte looks consistently like a T-cell-monocyte hybrid, not like random noise. The result is a cluster co-expressing two lineages, which reads as a "transitional state" if you want it to.
Detection. scDblFinder, DoubletFinder and Scrublet all simulate artificial doublets from your own data and score each barcode against them. Run detection per sample, never on a merged object — a doublet can only form within one droplet emulsion, and merging distorts the simulation.
The judgement call. Tools flag more barcodes than are truly doublets. Before deleting a cluster, check whether it co-expresses mutually exclusive lineage markers (CD3 and CD19, say), whether its UMI count is roughly the sum of its two putative parents, and whether it disappears when you load fewer cells. A real transitional population shares a continuum with its neighbours; a doublet cluster sits between two parents with no intermediate cells.
Ambient RNA
Dying and damaged cells leak RNA into the suspension. Every droplet then picks up a background of the most abundant transcripts in that sample. Consequences: hepatocyte genes appear in immune cells in liver data, haemoglobin appears everywhere in blood, and every cluster looks slightly like the dominant cell type.
Detection and correction. SoupX estimates the contamination fraction using genes that should be zero in a given cell type; CellBender models it directly from the raw, unfiltered barcode matrix — which means you need to keep the raw output, not only the filtered matrix. Typical contamination is 2–10%; above 20% something went wrong at dissociation and correction is patching, not fixing.
The tell. A marker gene that is "expressed" at low level in literally every cluster is almost always ambient. A real low-level expresser is patchy; ambient is uniform.
Order of operations
- Empty droplet removal (EmptyDrops or the platform's own call).
- Ambient RNA estimation and correction — needs the raw matrix, so do it before you discard it.
- Doublet detection, per sample.
- Cell-level QC filtering on genes / UMIs / mitochondrial fraction.
- Only then: normalisation, integration, clustering.
Running QC filtering first is the common mistake. It removes the low-count barcodes that ambient models need in order to estimate the background, so the correction ends up weaker than the contamination.
What to report
State the doublet rate expected and the rate removed, the ambient contamination estimate per sample, and the tool versions. If you did not correct for ambient RNA, say so — reviewers increasingly ask, and "we checked and it was low" is a perfectly good answer when it is true.