Chapter 3 — Markers & Manual Cell-Type Annotation
Where this chapter sits. Companion to Lecture 03 and Tutorial 03. Builds on Chapter 2 (you have a clustered object). Prerequisites: familiarity with the canonical lineage panels for your tissue (the workshop uses PBMC; see Glossary for cell-type definitions).
3.1 The conceptual move from clusters to cell types
A cluster is a graph property: the output of community detection on the SNN graph. A cell type is a biological claim about identity that connects to a literature, an ontology (Cell Ontology), and a set of expected functional behaviors. Going from one to the other is the most subjective step in the entire scRNA-seq workflow, and the one reviewers will scrutinize most.
Two paradigms:
- Manual annotation (this chapter, Lecture 03): inspect canonical marker expression in each cluster and assign a name. Interpretable, but biased by the annotator’s experience.
- Reference-based annotation (Chapter 4, Lecture 04): map cells against a labeled reference atlas with Azimuth (other paradigms include SingleR and CellTypist). Reproducible, but biased by the reference’s coverage.
Best practice: run both, reconcile disagreements, document choices. Disagreements typically flag rare subtypes, transitional states, or biology your reference doesn’t cover. They are usually informative, not errors. The community protocol for annotating single-cell maps lays out exactly this combined manual-plus-automated workflow and is the reference to follow1.
3.2 Finding markers — the statistical core
FindAllMarkers(seu) runs, for each cluster \(c\), a Wilcoxon rank-sum test comparing the gene’s expression in cluster \(c\) vs all other clusters combined. The output is a per-cluster ranked gene list. Table 1 describes each output column and how to read it. The table captures five key quantities per gene per cluster; knowing how to read each column is the prerequisite for turning the output into a useful annotation. Most columns are self-explanatory in isolation but become informative only in combination: a high avg_log2FC with a low pct.1 is a gene that is highly upregulated on average but barely expressed in any individual cell of the cluster — a mathematical artifact of the ratio, not a biologically useful marker. Similarly, a high pct.1 with a high pct.2 means the gene is broadly expressed across most cell types and provides no discriminatory power. The most useful markers simultaneously have high pct.1, low pct.2, and high avg_log2FC.
FindAllMarkers and FindConservedMarkers. A “good” marker has high pct.1, low pct.2, and high avg_log2FC; ranking by avg_log2FC (or the derived score pct.1 - pct.2) gives a practical top-N list per cluster.
| Column | Meaning |
|---|---|
gene |
gene symbol |
cluster |
cluster being tested |
avg_log2FC |
log2 fold-change in cluster \(c\) vs others |
pct.1 |
fraction of cells in cluster \(c\) expressing the gene (UMI > 0) |
pct.2 |
fraction of cells in other clusters expressing the gene |
p_val |
raw Wilcoxon p-value |
p_val_adj |
Bonferroni-adjusted (Seurat’s choice) |
A “good” marker has high pct.1, low pct.2, and high avg_log2FC. The ranking by avg_log2FC (or a score like pct.1 - pct.2) gets you a top-N list per cluster.
Why the Wilcoxon test? Seurat defaults to the non-parametric Wilcoxon rank-sum test, and the large benchmark of single-cell DE methods found that simple tests like Wilcoxon are surprisingly robust and competitive for marker detection — the more elaborate model-based methods do not consistently beat it2. (presto provides a fast Wilcoxon implementation Seurat uses automatically when installed.) A crucial distinction from Chapter 6: marker detection here is a descriptive, within-dataset ranking used to name clusters, not a cross-condition inference about which genes change with treatment — that question requires pseudobulk and is the subject of Chapter 63.
FindAllMarkers and the Chapter 6 pseudobulk DESeq2 workflow are often confused but answer entirely different questions. Marker detection asks: “which genes are enriched in cluster \(c\) relative to all other cells in this same dataset?” It is a descriptive ranking within a single object, and its purpose is to name clusters by their characteristic genes. Differential expression across conditions asks: “does gene \(X\) change between treatment and control groups?” It is an inferential test that requires biological replicates and appropriate statistical calibration. Using FindAllMarkers to detect condition effects produces anti-conservative p-values (cells treated as independent replicates), while using DESeq2 across donors to find cluster markers ignores the within-cluster information. Keep the two workflows separate.
FindAllMarkers finds genes enriched in a cluster, not necessarily canonical markers. A gene can top the list of cluster \(c\) either because it’s a literature lineage marker (good) or because some technical / state effect happens to differentiate the cluster (e.g. ISGs in the unintegrated ifnb data). Cross-check the top hits against canonical panels before naming clusters from them.
FindAllMarkers(seu, min.pct = 0.1, logfc.threshold = 0.25, only.pos = FALSE). Defaults: min.pct = 0.1, logfc.threshold = 0.25, only.pos = FALSE. min.pct filters out genes detected in fewer than the specified fraction of cells in either group — raising it to 0.25 (the workshop default) significantly reduces runtime and cuts low-detection noise genes; lowering it can recover rare markers in small clusters. logfc.threshold is a pre-filter on the log2 fold-change that prevents testing genes with trivial effect sizes; the default 0.25 is conservative for marker purposes. only.pos = TRUE restricts output to upregulated markers — almost always what you want when naming a cluster by what it is rather than what it lacks. For small clusters (<50 cells), loosen both thresholds (e.g. min.pct = 0.1, logfc.threshold = 0.1) to avoid returning an empty table.
3.3 Conserved markers — for case-control designs
If your dataset has multiple conditions (e.g. CTRL vs STIM in ifnb), naive FindAllMarkers will surface treatment-induced genes as “cluster markers” — and you’ll mis-label populations by their interferon response rather than their lineage.
FindConservedMarkers runs the marker test inside each condition and reports genes that come up in both. This protects against treatment-driven mis-annotation:
FindConservedMarkers(seu,
ident.1 = 3, # cluster of interest
grouping.var = "stim") # condition columnThe output combines per-condition results, so you can see whether the gene is a marker in CTRL only, STIM only, or both. Both means it’s a stable lineage marker; one only means it’s a state marker.
Use FindConservedMarkers whenever you have a non-trivial perturbation in your design. It is the right default for any multi-condition study.
3.4 The five marker visualizations
Tutorial 03 §3 walks five canonical plot types. Each answers a different diagnostic question; choosing the right one speeds interpretation and produces clearer figures. Table 2 summarizes the five, then the subsections explain when each pays off. A common mistake is to pick one plot type and use it for everything — the DotPlot is elegant for a publication panel but hides bimodal expression that VlnPlot reveals immediately; the FeaturePlot shows spatial context that the DotPlot compresses away. For a rigorous annotation, you should consult at least two plot types per cluster: one to confirm specificity (e.g. DotPlot) and one to confirm distributional properties (e.g. VlnPlot).
DotPlot is the most widely used in publications; VlnPlot is best at the single-gene investigation stage.
| Function | Best question to ask | Strength | Caveat |
|---|---|---|---|
VlnPlot |
Is this gene on/off in exactly one cluster? | Shows full distribution; reveals bimodality | One gene per panel; clutters above 8 genes |
FeaturePlot |
Where on the UMAP does this gene concentrate? | Reveals spatial/topological structure | Overplotting in dense regions; use order = TRUE |
DotPlot |
Which markers define which clusters, at a glance? | Many genes × clusters in one compact figure | Hides bimodality; means can mask high-expressing minorities |
DoHeatmap |
Does the global cluster ↔︎ marker block structure look right? | Shows whole-dataset diagonal block pattern | Dominant clusters monopolize space; noisy at high cell counts |
RidgePlot |
Is this a graded / continuum marker? | Density curves capture soft boundaries | Less compact than VlnPlot; best for 3–8 clusters |
3.4.1 VlnPlot — distribution per cluster, one gene at a time
Best for bimodality, outliers, comparing 4–8 candidate markers side-by-side. A “good” marker has one cluster’s violin bulging while the rest sit at zero. If two clusters bulge similarly, the marker is shared by two cell types — keep looking.
3.4.2 FeaturePlot — single gene, every cell, on the UMAP
Best for spatial/topological structure on the embedding. A good marker shows expression concentrated in one UMAP region; a scattered FeaturePlot suggests the gene is either a poor marker or a technical signature.
3.4.3 DotPlot — mean × percent across clusters × genes
Best for the publication panel figure. Dot color = mean expression; dot size = percent of cells expressing. Compact: 16 markers × 12 clusters fits on one row. The single most-used plot in cell-type annotation papers.
3.4.4 DoHeatmap — top markers across clusters, every cell as a column
Best for showing the global cluster ↔︎ marker block structure. Each cluster’s top markers should “light up” in its own diagonal block. Caveat: it shows cells, not clusters, so a dominant cluster (40%+ of cells) visually monopolizes the plot. For balanced display, prefer DotPlot.
3.4.5 RidgePlot — density per cluster
Best for graded/continuum genes (e.g. naive→memory T-cell markers like S100A4). The density curves overlap, showing where the cell-type boundaries are soft.
3.5 The unintegrated-data special case: more clusters than cell types
When annotating unintegrated multi-condition data (as Tutorial 03 does), the condition effect is the dominant axis of variation, and cell types with a strong transcriptional response to the perturbation often split into two sister clusters — one CTRL cluster and one STIM cluster. The result is that you routinely see more clusters than biologically distinct cell types: for a PBMC dataset with ~12 known types, resolution 0.5 on unintegrated data typically yields 15–20 clusters. This is not a bug; it is the data faithfully representing what it sees before integration.
Practical consequences:
- You may legitimately assign the same cell-type label to two clusters (e.g. “CD14 Mono” for both a CTRL-dominated and a STIM-dominated monocyte cluster).
- Clusters whose top markers are interferon-stimulated genes (
ISG15,IFI6,IFIT3,MX1) rather than canonical lineage genes are often condition-split copies of an existing cell type. UseFindConservedMarkers(§3.3) to identify the cell-type signal underneath the ISG layer. - After integration (Chapter 5), these paired clusters collapse into single clusters, and per-cluster marker lists clean up considerably.
3.6 The decision loop for assigning labels
The practical workflow:
- Compute top markers per cluster with
FindAllMarkersorFindConservedMarkers - Cross-reference the top 5–10 against canonical panels (CellMarker 2.0, PanglaoDB, Azimuth references)
- Confirm visually with a DotPlot of the canonical panel × clusters (does each cluster’s expected marker actually concentrate there?)
- Assign labels with
RenameIdents, store inseu$celltype_manual - Document why you assigned each label — write a sentence per cluster in your lab notebook (a future reviewer or future-you will thank you)
A cluster number is an arbitrary label assigned by community-detection software; it has no biological meaning until you interpret it. Assigning “CD14+ Monocyte” to cluster 3 is a hypothesis — you are asserting that the cells in this group share a lineage, a developmental origin, a set of functional capabilities, and a canonical literature context. That hypothesis is supported by (1) high specificity of monocyte markers in the DotPlot, (2) absence of markers for competing identities, and (3) concordance with reference annotation (Chapter 4). The label becomes less defensible when any of those supports is weak. In practice, the annotation step is also the first place where novel biology becomes visible: a cluster with no good match in the canonical panel is either a technical artifact (doublet, ambient contamination — inspect per §3.8) or a cell state worth investigating further.
For the workshop’s ifnb PBMCs, the canonical panel is small and highly stereotyped (see also §3.5 above for how the unintegrated embedding changes how many clusters you will label). Table 3 lists the ten major PBMC populations and their most reliable lineage markers.
ifnb dataset. Markers are chosen for high pct.1 / low pct.2 on healthy donor PBMCs; expression patterns shift with activation state and may require FindConservedMarkers on multi-condition data.
| Cell type | Canonical markers |
|---|---|
| CD14+ Monocyte | CD14, LYZ, S100A8, S100A9 |
| CD16+ Monocyte | FCGR3A, MS4A7, CDKN1C |
| Dendritic cell | FCER1A, CST3, CLEC10A |
| pDC | LILRA4, IL3RA, CLEC4C |
| CD4 Naive T | IL7R, CCR7, LEF1 |
| CD4 Memory T | IL7R, S100A4 |
| CD8 T | CD8A, CD8B, GZMK |
| NK | GNLY, NKG7, KLRD1 |
| B | MS4A1, CD79A, CD79B |
| Megakaryocyte | PPBP, PF4 |
These panels are not folklore — they are curated in searchable databases such as PanglaoDB4 and CellMarker 2.05, which compile cell-type markers from thousands of single-cell studies. For tumor / non-immune tissues, the panel is much larger and more disputed; consult tissue-specific atlases.
3.7 Validating against a ground truth — reading the confusion table
When a ground-truth annotation is available (as with ifnb’s seurat_annotations), the standard check is to cross-tabulate your manual labels against the reference:
table(manual = Idents(seu), truth = seu$seurat_annotations)Reading the output: each row is one of your manual labels; each column is a ground-truth label; each cell is a count of cells with that combination. A perfect annotator sees each row concentrate its counts in a single column — one of your labels maps entirely to one ground-truth type. Off-diagonal counts are disagreements; their pattern is informative:
- Row split across two columns — you lumped two cell types together (e.g. your “NK” cluster contains both NK and CD8 T cells).
- Two rows that both map to the same column — you over-split one cell type into two clusters (common for condition-split clusters on unintegrated data).
- Column with no strong row — a ground-truth type that your clustering did not recover as a distinct cluster (often rare populations like pDC or Mk).
After integration (Chapter 5), re-cluster and re-annotate; the same cross-tab should tighten considerably because condition-split doublings collapse.
3.8 Risks and how to avoid them
Four annotation mistakes account for the majority of published errors in single-cell cell-type assignments.
Single-marker labels are the most common. A cluster top-marked by CD3D is “T cell” — but is it CD4 or CD8? Naive or memory? Activated or resting? Always label from combinations: marker presence plus absence of competing markers. The DotPlot format is ideally suited to this because it places the confirmatory and exclusionary markers side-by-side across all clusters simultaneously.
Marker leakage from ambient RNA can mislead annotation when the cell suspension was complex or had high cell death. If you skipped SoupX (Chapter 1), low-level marker expression in the wrong cluster may reflect ambient bleed rather than genuine expression. The classic symptom is every cluster showing low non-zero HBB — hemoglobin is a dominant ambient contaminant in whole-blood or bone-marrow samples. The fix is to re-run from Chapter 1 with SoupX applied.
Treatment state mistaken for cell type is a recurrent trap in perturbation studies. Interferon-stimulated genes (ISG15, IFI6, IFIT1, MX1) lighting up half a dataset usually reflects a stimulation condition, not a new cell type. Use FindConservedMarkers (§3.3) to see whether the lineage markers underneath the ISG signal are consistent with known types.
Doublet-driven “intermediate” types appear when two highly distinct cell populations (e.g. T and myeloid cells) end up doublet-enriched in the same cluster. A “T-myeloid intermediate” expressing both CD3D and CD14 is almost always a doublet artifact. Verify by inspecting the scDblFinder.class distribution per cluster — a cluster where >15–20% of cells were called doublets by scDblFinder should be discarded, not annotated.
3.9 The relationship to the Cell Ontology
The Cell Ontology (CL) is a community-maintained hierarchy of ~2,400 cell-type terms, each with a stable identifier (e.g. CL:0000236 = B cell). Ontologies are the backbone that lets atlas projects like the Human Cell Atlas combine annotations across thousands of datasets6. When you annotate, you should be assigning to an ontology term, not just typing a free-text label. Reasons:
- Interoperability. Cell-type labels in different studies can only be combined if they reference the same ontology
- Submission requirement. CELLxGENE Discover (the de-facto atlas browser) requires Cell Ontology IDs, not free-text labels
- Discoverability. Other groups can find your dataset by querying the ontology
The mapping from Seurat label to CL ID is a manual step. The Ontology Lookup Service is the searchable interface; type “B cell” and you get CL:0000236. Store the IDs in seu@meta.data$cell_type_ontology_term_id. Tutorial 17 walks the full mapping for the ifnb cell types.
3.10 Common errors
Table 4 lists the most common mistakes at the marker-detection and annotation stage, their root causes, and the fixes.
"integrated" assay for FindAllMarkers — always reset DefaultAssay(seu) <- "RNA" before running marker tests.
| You see | What’s wrong | What to do |
|---|---|---|
FindAllMarkers is very slow |
Wilcoxon implementation is the bottleneck | install.packages("presto") — Seurat detects it automatically |
| Top markers per cluster are mostly ribosomal | Your QC/normalization didn’t suppress ribosomal variance | Consider regressing out percent.rb; consider stricter HVG selection |
| Marker dot plot has gaps where canonical markers should be | Your dataset lacks that cell type, or you’re using the wrong assay | Check DefaultAssay(seu) is "RNA" for marker tests, not "integrated" |
RenameIdents errors |
Argument order or factor levels mismatch | Build the named vector explicitly: c("0" = "B", "1" = "CD14 Mono", ...) |
| Manual labels on the unintegrated object don’t match labels on the integrated object | Cluster numbers differ between integrations | Re-annotate after integration; Cluster numbers are not stable across re-clusterings, only per-cell labels are |
3.11 Where this material is also discussed
- Lecture: Lecture 03
- Tutorial: Tutorial 03
- Reference-based annotation (the complement): Chapter 4, Lecture 04
- Cell Ontology lookup: Appendix E §1.2, Tutorial 17
- Statistical foundations of the marker tests: Appendix B §§ 5–6
3.12 Going further
The community protocol for cell-type annotation1 is the single best next read; pair it with the DE-method benchmark that justifies the Wilcoxon default2 and the best-practices tutorial’s clustering-and-annotation chapter7. For marker panels, the curated databases — CellMarker 2.05 and PanglaoDB4 — are searchable by tissue and cell type. The Azimuth web tool automates annotation against curated atlases (the subject of Chapter 4), and scNotebooks Module 04 is a parallel walkthrough of manual plus automated annotation. The full curated list is on Key Papers, Reviews & Benchmarks.