Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Cell calling

Droplet-based protocols such as 10x Genomics Chromium capture many more barcodes than cells. Most barcodes mark empty droplets that hold only ambient RNA, the free transcripts of the cell suspension. Cell calling decides which barcodes hold a cell. It comes before quality control: quality-control filters and their automatic bounds treat every barcode as a cell, so on an unfiltered matrix they center on the empty droplets and remove the cells.

ds.qc.call_cells implements EmptyDrops Lun et al., 2019. Barcodes with at most lower counts, 100 by default, form the ambient pool. Every barcode above lower is tested against the ambient profile: a barcode whose counts the ambient profile explains poorly holds a cell, however few counts it has. Barcodes with at least as many counts as the knee of the barcode-rank curve are kept without a test. The test recovers cells with little RNA that a total-count threshold drops.

This page imports an unfiltered 10x matrix, inspects its barcode-rank curve, calls cells, checks the test statistics, and continues with quality control and the standard analysis on the called cells. For a matrix whose cells were already called, start with Quality control across assays.

Download the raw PBMC matrix

We use the unfiltered matrix of “1k PBMCs from a Healthy Donor (v2 chemistry)” from 10x Genomics, released under CC BY 4.0. The download is about 18 MB and is checked against a pinned SHA-256 digest. The example uses a temporary directory; use a project directory instead to keep the download and the Zarr store.

Started: 2026-10-24T10:01:27.828537Z | Wall time: 1.043 s

Downloaded 18.4 MB

Started: 2026-10-24T10:01:28.874729Z | Wall time: 0.223 s

Import every barcode with counts

The raw matrix lists every barcode of the chemistry’s barcode list, 737,280 for this run, most of them without a single count. CrH5Reader(..., is_filtered=False) keeps the barcodes whose total exceeds filtering_cutoff, 500 by default. That default removes the ambient pool that cell calling tests against, so pass filtering_cutoff=0 to keep every barcode with a count. CyteArc stores counts densely, one row per barcode, so this store is much larger than one of called cells, and the import takes about half a minute.

We open the store with min_features_per_cell=-1. Cell calling reads every barcode whatever the I cell key holds, so the open-time feature floor cannot remove the ambient pool. The default floor would remove most barcodes, so the first open would keep I complete and warn instead; turning the floor off keeps I complete without the warning.

Barcodes with counts 142324 Barcodes with more than 100 counts 1191 Barcodes with more than 1,000 counts 961 Name: Barcodes, dtype: int64

Started: 2026-10-24T10:01:29.100118Z | Wall time: 30.086 s

Inspect the barcode-rank curve

Ranking barcodes by their total count gives the barcode-rank curve. Cells form a plateau of large totals, and the curve then drops steeply towards the empty droplets. qc.barcode_ranks fits a smoothing spline to the log-log curve above lower and reports two of its points: the knee, where the plateau bends into the drop, and the inflection, where the curve falls most steeply. It reads the totals stored when the store was prepared, not the counts.

qc.call_cells keeps the barcodes at or above the knee as cells without a test: a barcode with that many counts holds a cell even when its profile resembles the ambient one.

Log-log barcode-rank plot of total UMI counts against barcode rank: a plateau of large totals, a steep drop, and a long tail of small totals, with the fitted curve over the totals above 100 and dashed lines at the knee, the inflection, and the lower bound of 100.

Started: 2026-10-24T10:01:59.189074Z | Wall time: 0.345 s

Call cells with EmptyDrops

qc.call_cells sums the counts of the ambient pool per gene and smooths them with Good-Turing estimation, which gives every gene seen in some barcode a nonzero share. It models a barcode’s counts with a Dirichlet-multinomial distribution of that profile, whose precision alpha it estimates from the ambient pool; a small alpha means that empty droplets vary more than random sampling of one profile would make them. For each barcode, n_iter simulated ambient count vectors of its total, 10,000 by default, give the p-value (R + 1) / (n_iter + 1), where R counts those at most as probable as the barcode’s own counts. Barcodes at or above the knee get a p-value of 0, the p-values are adjusted with the Benjamini-Hochberg method, and the barcodes with an adjusted p-value of at most 0.001 are the called cells.

The counts are read once, in blocks within the memory budget. The seed fixes the simulations, so the p-values are the same on any number of threads. The call saves the statistics of every barcode as a cell_calling artifact and returns the called cells as a cell selection.

Called cells 1045 Barcodes at or above the knee 770 Ambient barcodes (1 to 100 counts) 141133 Estimated alpha 937.4 Name: EmptyDrops, dtype: object

Started: 2026-10-24T10:01:59.537573Z | Wall time: 12.674 s

Check the test statistics

A barcode is limited when none of the simulated ambient count vectors was as improbable as its counts: its p-value is the smallest that 10,000 iterations can give. A limited barcode that is not called could become significant with more iterations, and qc.call_cells warns when one exists; call cells again with a larger n_iter then. Here every limited barcode is called.

limitedFalseTrue
called
False1460
True781264

Started: 2026-10-24T10:02:12.214491Z | Wall time: 0.017 s

Plot each tested barcode’s total against how improbable its counts are as ambient RNA. Called cells separate from the empty droplets above lower, which follow the ambient profile; called cells below the knee are those that a total-count threshold at the knee would have lost.

Scatter plot of the barcodes above 100 counts: total UMI count against minus the log-probability of their counts as ambient RNA, both on log scales, with called cells in red, the other barcodes in grey, and a dashed line at the knee.

Started: 2026-10-24T10:02:12.234095Z | Wall time: 0.175 s

275 of 1045 called cells have fewer counts than the knee.
count 275.0 mean 1516.0 std 825.0 min 105.0 25% 706.0 50% 1698.0 75% 2290.0 max 2548.0 Name: total, dtype: float64

Started: 2026-10-24T10:02:12.411901Z | Wall time: 0.011 s

Continue with quality control

The called cells are an ordinary cell selection: pass them as cell_selection= to the quality-control filters and other analyses. Called cells still need quality control, because EmptyDrops separates cells from empty droplets, not damaged cells from healthy ones.

Automatic filtering kept 917 of 1045 called cells.
Violin plots of RNA counts, detected genes, and mitochondrial percentage of the called cells.

Started: 2026-10-24T10:02:12.424952Z | Wall time: 0.576 s

The RNA recipe can call cells itself. Its cell_calling stage takes the keywords of qc.call_cells and tests the barcodes of I, which holds every barcode here, so with the same seed it calls the same cells as above. Its filtering stage then applies the same automatic thresholds to the called cells, and the run continues with the standard analysis. The run records a summary of each stage, such as the cell-calling stage’s knee and called cells and the cells that filtering kept. To analyze a selection that you called yourself, pass it as cells=cells with filtering=True; a recipe analyzes a given selection as it is unless filtering asks for quality control.

UMAP of the analyzed called cells colored by their Leiden clusters.
Called cells 1045 Cells after filtering 917 Name: Cells, dtype: int64

Started: 2026-10-24T10:02:13.004151Z | Wall time: 117.963 s

Common mistakes and limitations

  • Importing with the default filtering_cutoff=500 removes the ambient pool, and qc.call_cells raises. Import every barcode with a count, filtering_cutoff=0.

  • Each capture has its own ambient pool. In a store that merges several captures, call cells for each capture with cell_selection= and combine the selections, or pass cell_calling={"sample_column": ...} to the RNA recipe, which calls each capture apart.

  • lower must sit below the smallest cells and above most empty droplets. Barcodes at or below it are never called, and cells among them distort the ambient profile.

  • Barcodes at or above retain, the knee by default, are cells without a test. Pass a larger retain to test them too.

  • Cell calling does not remove ambient RNA from the called cells, and it does not detect doublets; those are separate steps.

  • CyteArc implements EmptyDrops from its paper and supplementary methods. Its simulations use CyteArc’s own random streams, so p-values near the threshold can differ from other implementations, by Monte Carlo error.

References
  1. Lun, A. T. L., Riesenfeld, S., Andrews, T., Dao, T. P., Gomes, T., participants in the 1st Human Cell Atlas Jamboree, & Marioni, J. C. (2019). EmptyDrops: distinguishing cells from empty droplets in droplet-based single-cell RNA sequencing data. Genome Biology, 20(1), 63. 10.1186/s13059-019-1662-y