Chatomics Field GuideWhat They Don't Teach You

Glossary · Single-Cell and Spatial

CITE-seq

Your ADT counts are not RNA counts in disguise, and normalizing them like RNA is how CITE-seq experiments quietly go wrong.

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

Also: ADT, antibody-derived tags

Definition

CITE-seq (Cellular Indexing of Transcriptomes and Epitopes by Sequencing) is a single-cell assay that measures surface protein abundance and mRNA expression in the same cell. It works by labeling cells with antibody-derived tags (ADTs): antibodies conjugated to an oligonucleotide carrying a PCR handle, a unique antibody barcode, and a polyA tail. That polyA tail lets the ADT hybridize to the same oligo-dT beads used for mRNA capture, so each cell barcode yields two parallel count matrices, one for RNA and one for protein, that must be processed and normalized as distinct data types.

You meet this term the moment a CITE-seq run comes back with two count matrices instead of one, or when a Seurat object shows a Protein assay sitting next to RNA. The decision it forces isn't "how do I cluster this," it's "which matrix do I trust for which claim." Surface protein resolves cell identity that RNA dropout obscures, since protein detection is far less sparse than transcript counts, but only if you've dealt with the noise specific to antibody-derived tags (ADTs) first.

The place this term actually decides something is normalization. RNA-seq intuition (library-size scaling, log1p) does not transfer cleanly to ADT counts, because ADTs carry a background source that RNA doesn't: unbound antibody that leaks into every droplet during encapsulation, whether or not a cell is present.

Why it matters

Skip ADT-specific correction and ambient antibody background inflates protein counts uniformly across empty and cell-containing droplets. A highly expressed marker reads high everywhere and drowns out real differences between cell types; a low-abundance marker on a rare population gets swamped into that same background and disappears. Concrete case: free CD14 antibody floats into every droplet during encapsulation, so T cells pick up a low but nonzero CD14 signal. Normalize with a method blind to that background and you'll call "double-positive" hybrid cells that don't exist, or dismiss a real CD14-dim monocyte subset as noise. Get the correction right and your weighted-nearest-neighbor (WNN) clusters reflect real joint RNA+protein cell states; get it wrong and you're publishing an artifact of antibody titration.

Where people get it wrong

Practitioners default to normalizing ADT counts the way they'd normalize RNA (library-size scaling, then log1p), or they run CLR (centered log ratio) normalization and assume that alone removes ambient background. It doesn't. CLR rescales each cell's protein counts relative to its own geometric mean, which makes counts comparable across cells, but it has no information about which fraction of any one antibody's signal is ambient versus cell-bound, because it never looks at empty droplets. Removing that background requires a method like dsb, which explicitly models it using empty-droplet ADT profiles and isotype controls, or a Bioconductor baseline/median approach. A second, quieter confusion: treating a "negative" ADT signal as a true biological negative, when it can just as easily be antibody titration or cross-reactivity. Validate antibody clones against flow cytometry before trusting specificity claimed on a catalogue sheet.

A concrete example

CLR runs on the cell-containing matrix alone and never sees the empty droplets, so it can't tell you how much of a given antibody's signal is ambient. dsb requires both the cell matrix and the empty-droplet matrix, plus isotype controls, to subtract droplet- and protein-specific background before you call a marker positive.

r
# CLR normalization: per-cell scaling, does not remove ambient background
seurat_obj <- NormalizeData(seurat_obj, assay = "ADT",
                             normalization.method = "CLR", margin = 2)

# dsb normalization: models ambient noise using empty droplets + isotype controls
library(dsb)
adt_norm <- DSBNormalizeProtein(
  cell_protein_matrix  = raw_adt_cells,
  empty_drop_matrix    = raw_adt_empty_droplets,
  denoise.counts       = TRUE,
  use.isotype.control  = TRUE
)

Related terms

Questions people ask

What is CITE-seq in single-cell analysis?

CITE-seq adds antibody-derived tags to a standard single-cell RNA-seq workflow so you get surface protein levels and transcript counts from the same cell. It matters most when transcript-level markers are too sparse or don't separate closely related subsets, since protein detection is far less sparse than RNA.

What does ADT mean in CITE-seq?

ADT stands for antibody-derived tag: an antibody conjugated to an oligonucleotide carrying a PCR handle, a unique barcode for that antibody, and a polyA tail. The polyA tail lets it get captured and reverse-transcribed on the same bead as the cell's mRNA, which is why "ADT count" refers specifically to the protein-tag read count, distinct from RNA UMI counts.

How do you normalize CITE-seq ADT data?

The two common approaches are CLR (centered log ratio, Seurat's default with margin = 2) and dsb (denoised and scaled by background), which additionally uses empty-droplet ADT profiles and isotype controls to remove ambient antibody noise. Use dsb when you have the raw, unfiltered droplet output, since CLR alone doesn't correct for background contamination.

Why is my CITE-seq protein data so noisy?

Three sources dominate: unbound antibody floating free in suspension gets encapsulated into every droplet (ambient noise), antibodies bind some cells non-specifically, and titration isn't perfectly matched to epitope abundance. Empty droplets in your raw matrix show the ambient floor directly, which is why dsb uses them as a reference.

Is CLR normalization enough for CITE-seq, or do I need dsb?

CLR puts protein counts on a comparable per-cell scale but can't tell how much of any antibody's signal is background, since it never examines empty droplets. If your raw unfiltered matrix includes empty droplets, use dsb or a baseline/median approach instead of relying on CLR alone.

Related pages

Related reading on the blog

Sources

  1. Part 4 CITE-seq normalization using empty droplets with the dsb package — Ambient ADT noise from unbound antibody and the dsb correction approach
  2. Part 1 How to use Salmon/Alevin to preprocess CITE-seq data — Definition of CITE-seq and ADT structure (PCR handle, barcode, polyA tail)
  3. Part 3 Centered log ratio (CLR) normalization for CITE-seq protein count data — CLR normalization mechanics and margin=2 usage in Seurat
  4. dsb R package end-to-end workflow — DSBNormalizeProtein parameters: denoise.counts and use.isotype.control
  5. Integrating with protein abundance (Bioconductor OSCA.advanced) — Median-based/baseline normalization as a Bioconductor alternative to CLR