L03 — Markers & Manual Cell-Type Annotation

Where this lecture fits

Goals of this lecture

  • Distinguish clusters (a graph property) from cell types (a biological label)
  • Find robust cluster markers with FindAllMarkers / FindConservedMarkers
  • Read DotPlots / FeaturePlots like a clinician
  • Assign labels you can defend with literature

Note

Why this matters. A cluster is a partition of a graph. A cell type is a biological claim. Going from one to the other is the most subjective step in the entire workflow — and the one reviewers will scrutinize most.

Clusters are not cell types

The annotation workflow

The annotation workflow

  • Manual (this lecture) — interpretable but biased by the annotator
  • Automatic / reference-based (Lec 04) — fast and reproducible but biased by the reference
  • Best practice: run both, reconcile disagreements, document choices

Note

Which do you reach for first?

  • For well-referenced model organisms (human, mouse), reference-based annotation (Lec 04 — Azimuth; other tools include SingleR and CellTypist) is increasingly the practical starting point, with manual markers used to confirm and to catch novel states.

  • For non-model systems — where no curated reference exists — manual, marker-driven annotation remains essential and is often the only option. This workshop teaches both so you can choose based on your system; the two lectures are a connected pair, not alternatives.

Finding markers

FindAllMarkers — every cluster vs. all others

This step allows you to identify markers that differentiate a cluster against all other clusters:

markers <- FindAllMarkers(seu,
                          only.pos        = TRUE,
                          min.pct         = 0.25,
                          logfc.threshold = 0.25)

markers |>
  group_by(cluster) |>
  slice_max(avg_log2FC, n = 10)
  • only.pos = TRUE keeps just the up-regulated markers (what you’d actually use to name a cluster)
  • min.pct = 0.25 requires the gene be detected in at least 25% of cells in either group
  • Default test is the Wilcoxon rank-sum

Tip

Defaults that work for most 10x datasets. min.pct = 0.25, logfc.threshold = 0.25. Loosen them for very small clusters, tighten them only if results are overwhelming.

FindConservedMarkers — markers that hold across conditions

When your dataset has multiple conditions (e.g. stimulated vs. control PBMCs in the ifnb example), you usually want markers that define a cluster regardless of condition:

DefaultAssay(seu) <- "RNA"
cluster3_markers <- FindConservedMarkers(seu,
                                         ident.1       = 3,
                                         grouping.var  = "stim")
head(cluster3_markers)

This runs the marker test inside each condition and reports genes that come up in both — protecting you from labelling a cluster by a treatment-induced gene rather than its lineage marker.

Warning

Don’t skip this step in case-control designs.

  • Naïve FindAllMarkers on a stim-vs-ctrl dataset will surface treatment-response genes as “cluster markers”, and you’ll mis-label populations.

FindAllMarkers & FindConservedMarkers — what’s shared

Both answer the same underlying question — “which genes mark this cluster?” — with the same machinery:

  • Compare one cluster vs. all other cells (a one-vs-rest contrast)
  • Run the same per-gene DE test under the hood — default Wilcoxon rank-sum (test.use applies to both)
  • Honor the same filters: min.pct, logfc.threshold, only.pos
  • Return the same per-gene statistics: avg_log2FC, p_val, p_val_adj, and the detection rates pct.1 / pct.2

Note

So the building block is identical — FindConservedMarkers is essentially FindMarkers run repeatedly. What changes is how condition is handled (next slide).

FindAllMarkers vs. FindConservedMarkers — what differs

FindAllMarkers FindConservedMarkers
Question What distinguishes a cluster? What marks a cluster in every condition?
Condition Pools all cells, ignores it Splits by grouping.var, tests within each
Clusters Loops over all automatically One per call (ident.1)
Significance Single test per gene Per-condition tests combined (meta-analysis)
Output One frame, has a cluster column Per-condition columns (CTRL_…, STIM_…) + combined p-values
Needs metap; enough cluster cells in each group
Use when Single condition, or just want identity Case–control / multi-condition designs

Tip

The trap it avoids. Pooling stim-vs-ctrl, FindAllMarkers can surface a treatment-response gene as a “cluster marker.” FindConservedMarkers keeps only genes that mark the cluster within both CTRL and STIM — so you label by lineage, not treatment.

Reading marker plots well

What a “good” marker looks like

  • Specific — high in the cluster, low elsewhere (DotPlot’s dot intensity)
  • Pervasive — expressed in most cells of the cluster (DotPlot’s dot size)
  • Backed by literature — appears in canonical lineage panels for the tissue

The five marker visualizations

Tutorial 03 walks through five canonical plots that are each best for a different question:

Plot What it shows When to use it
VlnPlot distribution per cluster, one gene at a time Bimodality, outliers, comparing 4–8 candidate markers
FeaturePlot one gene, every cell, on the UMAP Spatial structure on the embedding
DoHeatmap top-N markers across clusters, cell-by-cell Eyeballing the global cluster ↔︎ marker structure
RidgePlot density per cluster Gradient / continuum genes
DotPlot mean expr × % positive, per (cluster × gene) Compact panel display — best for the publication figure

VlnPlot — one gene per cluster, distribution-aware

  • A “good marker” violin: one cluster bulges, the rest sit at zero.
  • If two clusters bulge similarly, the marker is shared by two cell types — keep looking for a discriminator.

FeaturePlot — single gene on UMAP

  • FeaturePlot is the “geographic” view of a marker
  • Does it have a single home on the UMAP, or is it scattered?
  • Scattered = bad marker (or technical artifact).

DotPlot — the workhorse panel figure

  • A very good plot for a publication figure
  • Panel of canonical markers along x, clusters along y, dot size = % cells expressing, dot color = mean expression.
  • Eight clusters fit on one row; you can scale to 30+ clusters easily.

Heatmap — global cluster ↔︎ marker structure

  • Each cluster’s diagonal block “lights up” — its top markers are high in its cells and low elsewhere.
  • A heatmap that doesn’t show this pattern means the markers aren’t actually specific to their assigned cluster.
  • One trap. Heatmaps show cells, not clusters
  • A dominant cluster (40% of cells) visually monopolizes the plot.

Picking a plot

  • Confirming one candidate marker for one clusterVlnPlot (4 candidates side-by-side)
  • Showing where one gene “lives” on the UMAPFeaturePlot
  • Reviewer wants every cluster on one figureDotPlot
  • Showing the overall cluster ↔︎ marker block structureDoHeatmap
  • Comparing graded / continuum expressionRidgePlot

Warning

Genes that are merely enriched on a Wilcoxon test are not always markers in the canonical sense. Cross-check at least two genes per cluster, and prefer ones that appear in published lineage panels (CellMarker 2.0, PanglaoDB).

Assigning labels

More clusters than cell types — the un-integrated over-split

Warning

  • On un-integrated data you will see more clusters than named cell types.

  • In Tutorial 03 (which follows Tutorial 02’s un-integrated clustering), a resolution of ≈ 0.5 on the ifnb dataset typically yields ~15–20 clusters even though the data contain roughly 13 annotatable PBMC cell types.

  • The excess clusters arise because each cell type appears as a CTRL version and a STIM version — two islands in the UMAP that get separate cluster IDs. - Attempting to find conserved markers for all of them and label every one can trigger downstream errors (too few cells in a condition slice). The practical solutions:

  • Use FindConservedMarkers only for clusters where both conditions are present in sufficient numbers.

  • Accept that some cluster pairs will be annotated as the same cell type and merged before further analysis.

  • Or move straight to integrated data (Tutorial 05) where CTRL and STIM cells of the same type co-cluster correctly.

The decision loop

  1. Look at top 10 markers per cluster (slice_max)
  2. Cross-reference against CellMarker 2.0, PanglaoDB, or a tissue-specific atlas
  3. Confirm with FeaturePlot / DotPlot of the canonical 2–3 markers
  4. Assign — and write down why in your notebook
new_ids <- c("0" = "CD4 T", "1" = "CD14 Mono", "2" = "B",
             "3" = "CD8 T", "4" = "NK",  "5" = "DC")
seu <- RenameIdents(seu, new_ids)
seu$celltype <- Idents(seu)

Tip

Reference panels worth bookmarking:

Recap & what’s next

What to remember from Lecture 03

  • Clusters ≠ cell types. The biology is in the labels you give the clusters.
  • FindAllMarkers finds enriched genes; FindConservedMarkers finds condition-robust ones — use the latter in case-control designs.
  • Document why you assigned each label — future-you (and reviewers) will thank you
  • Manual + reference-based (Lec 04) annotation are complementary, not redundant