Seurat Marker Genes & Cell-Type Annotation in R (pbmc3k)

Find the genes that define each cluster with FindAllMarkers, read canonical PBMC markers off the UMAP, and assign biological cell-type labels to produce an annotated UMAP

Bioinformatics

A practical guide to finding marker genes and annotating cell types in single-cell RNA-seq data with Seurat. Starting from a clustered pbmc3k object, run FindAllMarkers to get the genes that distinguish each cluster, visualize canonical PBMC markers with FeaturePlot and VlnPlot, reason from those markers to a cell-type label for every cluster, relabel with RenameIdents, and produce the annotated UMAP you put in a paper.

Published

June 29, 2026

Modified

July 7, 2026

TipKey takeaways
  • Clusters 0–8 are unnamed groups of similar cells. FindAllMarkers() finds the genes each cluster over-expresses versus all the others — the statistical evidence you annotate from.
  • Run it positive-only and lightly filtered: FindAllMarkers(only.pos = TRUE, min.pct = 0.25, logfc.threshold = 0.25) (a Wilcoxon test per gene per cluster). Sort by avg_log2FC for each cluster’s strongest markers.
  • Annotate by matching markers to known cell types. Plot canonical PBMC markers on the UMAP with FeaturePlot() / VlnPlot() and read which cluster lights up: IL7R → CD4 T, CD14/LYZ → monocytes, MS4A1 → B, CD8A → CD8 T, GNLY/NKG7 → NK, FCER1A → dendritic cells, PPBP → platelets.
  • RenameIdents() turns the cluster numbers into cell-type names; the relabelled DimPlot() is the signature figure of the whole analysis.
  • Be honest about what markers can and cannot resolve. Naive vs memory CD4 T is a genuinely subtle split, and the rare clusters (DC, platelet) rest on a single strong marker — label them, but say so.

Introduction

Clustering gave you nine groups, labelled 0 through 8. Those numbers are arbitrary: cluster 0 is “684 cells the graph placed together,” not “T cells.” This lesson does the interpretive work that makes the analysis useful — it names the clusters. The logic is simple and is the same every immunologist uses at the bench: find the genes that are switched on in one cluster and off in the rest, recognize those genes as the markers of a known cell type, and label the cluster accordingly.

Two steps do it. First, FindAllMarkers() computes, for every cluster, the genes that distinguish it from all other cells — the data-driven evidence. Second, you map markers to identities by checking a short list of canonical PBMC markers against the clusters and reasoning from biology to a name. We pick up from the clustering lesson on the same pbmc3k object, so the workflow transfers directly to your own clustered data.

Pick up from C4: rebuild the clustered object

Each code block here is self-contained, so it starts by re-creating the clustered object from the previous lesson — load the 10x matrix, build the Seurat object, QC-filter, normalize, find variable features, scale, run PCA, build the neighbor graph, cluster at resolution 0.5, and run UMAP. This is exactly the object you ended C4 with: 2,638 cells in 9 clusters.

library(Seurat)

# Re-establish the clustered object from C2 (QC) + C3 (normalize) + C4 (cluster + UMAP)
pbmc.data <- Read10X("_data/filtered_gene_bc_matrices/hg19")
pbmc <- CreateSeuratObject(pbmc.data, project = "pbmc3k",
                           min.cells = 3, min.features = 200)
pbmc[["percent.mt"]] <- PercentageFeatureSet(pbmc, pattern = "^MT-")
pbmc <- subset(pbmc, subset = nFeature_RNA > 200 & nFeature_RNA < 2500 & percent.mt < 5)
pbmc <- NormalizeData(pbmc)
pbmc <- FindVariableFeatures(pbmc, selection.method = "vst", nfeatures = 2000)
pbmc <- ScaleData(pbmc)
pbmc <- RunPCA(pbmc, features = VariableFeatures(pbmc))
pbmc <- FindNeighbors(pbmc, dims = 1:10)
pbmc <- FindClusters(pbmc, resolution = 0.5)
Modularity Optimizer version 1.3.0 by Ludo Waltman and Nees Jan van Eck

Number of nodes: 2638
Number of edges: 95927

Running Louvain algorithm...
Maximum modularity in 10 random starts: 0.8728
Number of communities: 9
Elapsed time: 0 seconds
pbmc <- RunUMAP(pbmc, dims = 1:10)

table(Idents(pbmc))             # 9 clusters, largest first

  0   1   2   3   4   5   6   7   8 
684 481 476 344 291 162 155  32  13 

Nine clusters, 684 cells down to 13. Everything below runs on this object.

Find marker genes for every cluster

A marker gene for a cluster is a gene expressed much more in that cluster than in the rest of the cells. FindAllMarkers() finds them by testing, one cluster at a time, each gene’s expression in that cluster against all other cells with a Wilcoxon rank-sum test, and returning the genes that pass. Three arguments keep the output focused on usable markers:

  • only.pos = TRUE — keep only genes up in the cluster (over-expression is what names a cell type; you do not annotate from genes a cluster lacks).
  • min.pct = 0.25 — ignore genes detected in fewer than 25% of the cells in either group (a marker present in a handful of cells is not a defining feature).
  • logfc.threshold = 0.25 — ignore genes with less than a 0.25 log₂ fold-change (skip the barely-different genes; this is also what makes the function fast enough to run on every cluster).
library(Seurat)
library(dplyr)
pbmc.data <- Read10X("_data/filtered_gene_bc_matrices/hg19")
pbmc <- CreateSeuratObject(pbmc.data, project = "pbmc3k",
                           min.cells = 3, min.features = 200)
pbmc[["percent.mt"]] <- PercentageFeatureSet(pbmc, pattern = "^MT-")
pbmc <- subset(pbmc, subset = nFeature_RNA > 200 & nFeature_RNA < 2500 & percent.mt < 5)
pbmc <- NormalizeData(pbmc)
pbmc <- FindVariableFeatures(pbmc, selection.method = "vst", nfeatures = 2000)
pbmc <- ScaleData(pbmc)
pbmc <- RunPCA(pbmc, features = VariableFeatures(pbmc))
pbmc <- FindNeighbors(pbmc, dims = 1:10)
pbmc <- FindClusters(pbmc, resolution = 0.5)
Modularity Optimizer version 1.3.0 by Ludo Waltman and Nees Jan van Eck

Number of nodes: 2638
Number of edges: 95927

Running Louvain algorithm...
Maximum modularity in 10 random starts: 0.8728
Number of communities: 9
Elapsed time: 0 seconds
# One Wilcoxon test per gene per cluster; keep up-regulated, well-expressed, clearly-changed genes
markers <- FindAllMarkers(pbmc, only.pos = TRUE, min.pct = 0.25, logfc.threshold = 0.25)

# The two strongest markers per cluster, ranked by fold-change
markers %>%
  group_by(cluster) %>%
  slice_max(avg_log2FC, n = 2) %>%
  ungroup() %>%
  as.data.frame()
           p_val avg_log2FC pct.1 pct.2     p_val_adj cluster          gene
1   9.571984e-88   2.397366 0.447 0.108  1.312702e-83       0          CCR7
2   1.354319e-51   2.136530 0.342 0.103  1.857312e-47       0          LEF1
3  7.070375e-139   7.280842 0.299 0.004 9.696312e-135       1         FOLR3
4  3.380744e-121   6.737730 0.277 0.006 4.636352e-117       1       S100A12
5   2.966019e-58   2.089320 0.420 0.111  4.067598e-54       2          AQP3
6   5.030519e-34   1.874084 0.263 0.070  6.898854e-30       2        CD40LG
7  2.397625e-272   7.379757 0.564 0.009 3.288103e-268       3     LINC00926
8  2.745016e-237   7.135051 0.488 0.007 3.764515e-233       3        VPREB3
9  7.254885e-165   4.407540 0.577 0.055 9.949349e-161       4          GZMK
10  3.269389e-88   3.736401 0.419 0.061  4.483640e-84       4          GZMH
11 8.230606e-168   5.881212 0.370 0.005 1.128745e-163       5           CKB
12 1.689064e-212   5.428812 0.506 0.010 2.316382e-208       5        CDKN1C
13 8.097030e-179   6.221917 0.471 0.013 1.110427e-174       6        AKR1C3
14 5.382427e-112   6.065475 0.290 0.007 7.381461e-108       6        SH2D1B
15 1.456261e-207   8.026573 0.500 0.002 1.997116e-203       7      SERPINF1
16 1.480764e-220   7.633833 0.812 0.011 2.030720e-216       7        FCER1A
17  0.000000e+00  14.358608 0.615 0.000  0.000000e+00       8        LY6G6F
18 7.321398e-222  13.925994 0.385 0.000 1.004057e-217       8 RP11-879F14.2

Read the columns. avg_log2FC is the average log₂ fold-change (cluster vs rest) — the marker’s strength; pct.1 and pct.2 are the fraction of cells expressing the gene inside the cluster vs outside. A good marker has a high avg_log2FC and a pct.1 far above pct.2. Cluster 0’s top hit, CCR7 (avg_log2FC ≈ 2.4, detected in 45% of its cells vs 11% elsewhere), is a textbook example.

Some clusters announce themselves here — CCR7/LEF1 (cluster 0) are naive T-cell genes, CD79A (cluster 3) is a B-cell gene, GZMK (cluster 4) is a cytotoxic T-cell gene, FCER1A (cluster 7) is a dendritic-cell gene. Others (FOLR3, CKB, AKR1C3) are real, highly-specific markers but not the names most people recognize. So rather than annotate gene-by-gene from this table, we check a short panel of canonical PBMC markers against the clusters — the fast, robust way to assign identities.

Visualize canonical markers on the UMAP

The reliable way to annotate is to take a handful of textbook PBMC markers — one or two per expected cell type — and ask which cluster expresses each one. FeaturePlot() colours the UMAP by a gene’s expression, so the answer is visual: the cluster that lights up for CD14 is the monocytes, the cluster that lights up for MS4A1 is the B cells, and so on.

library(Seurat)
pbmc.data <- Read10X("_data/filtered_gene_bc_matrices/hg19")
pbmc <- CreateSeuratObject(pbmc.data, project = "pbmc3k",
                           min.cells = 3, min.features = 200)
pbmc[["percent.mt"]] <- PercentageFeatureSet(pbmc, pattern = "^MT-")
pbmc <- subset(pbmc, subset = nFeature_RNA > 200 & nFeature_RNA < 2500 & percent.mt < 5)
pbmc <- NormalizeData(pbmc)
pbmc <- FindVariableFeatures(pbmc, selection.method = "vst", nfeatures = 2000)
pbmc <- ScaleData(pbmc)
pbmc <- RunPCA(pbmc, features = VariableFeatures(pbmc))
pbmc <- FindNeighbors(pbmc, dims = 1:10)
pbmc <- FindClusters(pbmc, resolution = 0.5)
Modularity Optimizer version 1.3.0 by Ludo Waltman and Nees Jan van Eck

Number of nodes: 2638
Number of edges: 95927

Running Louvain algorithm...
Maximum modularity in 10 random starts: 0.8728
Number of communities: 9
Elapsed time: 0 seconds
pbmc <- RunUMAP(pbmc, dims = 1:10)

# One canonical marker per expected PBMC population
FeaturePlot(pbmc, features = c("IL7R", "CD14", "LYZ", "MS4A1", "CD8A",
                               "FCGR3A", "GNLY", "FCER1A", "PPBP"))

A 3-by-3 grid of UMAP plots of the pbmc3k cells, each coloured by one canonical marker gene (grey = low, blue = high). IL7R is high across the large T-cell island; CD14 is confined to the central monocyte island; LYZ is bright across both monocyte clusters and the dendritic-cell cluster; MS4A1 marks the separate B-cell island; CD8A highlights a sub-region of the T-cell island; FCGR3A picks out one monocyte tip plus the NK cells; GNLY is high in the NK cluster at the edge of the T-cell island; FCER1A lights up a single tiny dendritic-cell cluster; PPBP marks a single, isolated tiny platelet cluster.

Each panel localizes a marker to a region of the map. IL7R (interleukin-7 receptor) is high across the big upper island — the CD4 T cells. CD14 and LYZ mark the monocytes; LYZ spills into the dendritic-cell cluster too (both are myeloid). MS4A1 cleanly marks the separate B-cell island. CD8A and GNLY/NKG7 split the cytotoxic corner into CD8 T cells and NK cells. FCER1A and PPBP each light up a single tiny cluster — the dendritic cells and the platelets.

A violin plot says the same thing quantitatively: the distribution of each marker’s expression within each cluster, so you see not just where a gene is on but how strongly and in what fraction of the cells.

library(Seurat)
pbmc.data <- Read10X("_data/filtered_gene_bc_matrices/hg19")
pbmc <- CreateSeuratObject(pbmc.data, project = "pbmc3k",
                           min.cells = 3, min.features = 200)
pbmc[["percent.mt"]] <- PercentageFeatureSet(pbmc, pattern = "^MT-")
pbmc <- subset(pbmc, subset = nFeature_RNA > 200 & nFeature_RNA < 2500 & percent.mt < 5)
pbmc <- NormalizeData(pbmc)
pbmc <- FindVariableFeatures(pbmc, selection.method = "vst", nfeatures = 2000)
pbmc <- ScaleData(pbmc)
pbmc <- RunPCA(pbmc, features = VariableFeatures(pbmc))
pbmc <- FindNeighbors(pbmc, dims = 1:10)
pbmc <- FindClusters(pbmc, resolution = 0.5)
Modularity Optimizer version 1.3.0 by Ludo Waltman and Nees Jan van Eck

Number of nodes: 2638
Number of edges: 95927

Running Louvain algorithm...
Maximum modularity in 10 random starts: 0.8728
Number of communities: 9
Elapsed time: 0 seconds
VlnPlot(pbmc, features = c("IL7R", "CD14", "MS4A1", "CD8A",
                           "FCGR3A", "GNLY", "FCER1A", "PPBP"),
        ncol = 4, pt.size = 0)

A grid of violin plots, one panel per marker gene, with the nine clusters (0–8) on the x-axis and expression on the y-axis. IL7R is high in clusters 0 and 2 (the CD4 T cells); CD14 is high only in cluster 1; MS4A1 only in cluster 3; CD8A in cluster 4; FCGR3A in clusters 5 and 6 (highest in the FCGR3A+ monocytes); GNLY in cluster 6; FCER1A in cluster 7; PPBP in cluster 8. Each marker is high in exactly one or two clusters and near zero elsewhere, confirming each is cluster-specific.

The violins make the cluster-specificity unmistakable: every marker is high in one or two clusters and flat near zero in the rest. CD14 is high only in cluster 1; MS4A1 only in cluster 3; PPBP only in cluster 8. That one-to-one pattern is exactly what lets you assign a name to each cluster with confidence.

Map clusters to cell types

Now read the markers off the plots and reason to a label for each cluster. The table below is the result of that reasoning — each row is the evidence from this run’s markers plus the canonical panel, and the identity it implies.

Cluster Markers that defined it (this run) Cell type
0 IL7R, CCR7, LEF1 (high), no cytotoxic genes Naive CD4 T
1 CD14, LYZ, S100A8/9/12 CD14+ Mono
2 IL7R high but CCR7/LEF1 low, S100A4, CD2 Memory CD4 T
3 MS4A1, CD79A B
4 CD8A, GZMK, NKG7 CD8 T
5 FCGR3A, MS4A7, CDKN1C, CD14 low FCGR3A+ Mono
6 GNLY, NKG7 very high, no CD8A NK
7 FCER1A, CST3 DC
8 PPBP, PF4 Platelet

A few of these deserve their reasoning spelled out, because annotation is a judgment, not a lookup:

  • Clusters 0 and 2 are both CD4 T cells, and telling them apart is the subtle call. Both are IL7R-high with no cytotoxic genes, so both are helper (CD4) T cells. The split is CCR7 and LEF1: high in cluster 0 (a naive signature) and low in cluster 2, which instead carries S100A4 (a memory/activation marker). They sit side by side on the UMAP for exactly this reason — they are close biology. Calling one “naive” and the other “memory” is defensible here but is the assignment you would flag as the least certain.
  • Clusters 4 and 6 share the cytotoxic markers NKG7 and GNLY. What separates them is CD8A: present in cluster 4 (CD8 T cells) and absent in cluster 6, whose GNLY/NKG7 are sky-high with no CD8 — the signature of NK cells.
  • Clusters 5 vs 1 are both monocytes but different kinds. Cluster 1 is CD14-high (classical, CD14+); cluster 5 is CD14-low and FCGR3A/MS4A7-high (non-classical, FCGR3A+).
  • Clusters 7 (DC) and 8 (platelet) are rare — 32 and 13 cells. Each rests on a single, very strong marker (FCER1A for dendritic cells, PPBP for platelets). The markers are unambiguous, but with so few cells treat these labels as well-supported rather than richly characterized.

This mapping matches the long-established pbmc3k annotation — reassuring, but note we derived it from this run’s own markers, not assumed it. Always annotate from your data: cluster numbers are arbitrary and shift with parameters, so a cluster’s identity comes from its genes, never from its number.

Relabel the clusters and draw the annotated UMAP

With the mapping decided, RenameIdents() replaces the cluster numbers with cell-type names, in cluster order (0 → 8). The relabelled DimPlot() is the figure the whole analysis was building toward — a map of blood, with every population named.

library(Seurat)
pbmc.data <- Read10X("_data/filtered_gene_bc_matrices/hg19")
pbmc <- CreateSeuratObject(pbmc.data, project = "pbmc3k",
                           min.cells = 3, min.features = 200)
pbmc[["percent.mt"]] <- PercentageFeatureSet(pbmc, pattern = "^MT-")
pbmc <- subset(pbmc, subset = nFeature_RNA > 200 & nFeature_RNA < 2500 & percent.mt < 5)
pbmc <- NormalizeData(pbmc)
pbmc <- FindVariableFeatures(pbmc, selection.method = "vst", nfeatures = 2000)
pbmc <- ScaleData(pbmc)
pbmc <- RunPCA(pbmc, features = VariableFeatures(pbmc))
pbmc <- FindNeighbors(pbmc, dims = 1:10)
pbmc <- FindClusters(pbmc, resolution = 0.5)
Modularity Optimizer version 1.3.0 by Ludo Waltman and Nees Jan van Eck

Number of nodes: 2638
Number of edges: 95927

Running Louvain algorithm...
Maximum modularity in 10 random starts: 0.8728
Number of communities: 9
Elapsed time: 0 seconds
pbmc <- RunUMAP(pbmc, dims = 1:10)

# Map cluster numbers (0-8, in order) to the cell types we read from the markers
new.ids <- c("Naive CD4 T", "CD14+ Mono", "Memory CD4 T", "B", "CD8 T",
             "FCGR3A+ Mono", "NK", "DC", "Platelet")
names(new.ids) <- levels(pbmc)
pbmc <- RenameIdents(pbmc, new.ids)

DimPlot(pbmc, reduction = "umap", label = TRUE, pt.size = 0.5) + NoLegend()

A UMAP of the 2,638 pbmc3k cells with every cluster relabelled as a cell type. The large T/NK island holds Naive CD4 T and Memory CD4 T cells next to each other, with CD8 T and then NK extending to one edge. The monocyte island holds CD14+ Mono and FCGR3A+ Mono with the small DC cluster beside them. The B cells form their own separate island, and the Platelets a tiny isolated group. Each point is one cell and each cell type is labelled at its centre.

Read it as the biology it now encodes. The CD4 T cells (naive and memory) sit together with the CD8 T and NK cells in one connected territory — the lymphocyte continuum, with the cytotoxic populations (CD8 T, NK) at one edge. The two monocyte populations form a second island, with the dendritic cells nearby (all myeloid). The B cells stand apart as their own island, and the platelets — the most distinct population of all — sit completely isolated. The geometry you saw as numbered clusters in C4 is now a labelled map of peripheral blood.

Report it like this. A methods sentence you can paste into a paper:

Cells were clustered on the first 10 principal components (FindNeighbors/FindClusters, resolution 0.5), yielding 9 clusters. Marker genes were identified with FindAllMarkers (Wilcoxon rank-sum test, only.pos = TRUE, min.pct = 0.25, logfc.threshold = 0.25), and clusters were annotated by canonical PBMC markers (IL7R, CCR7, S100A4, CD14, LYZ, MS4A1, CD8A, GZMK, FCGR3A, MS4A7, GNLY, NKG7, FCER1A, CST3, PPBP) as naive CD4 T, memory CD4 T, CD8 T, NK, CD14+ monocytes, FCGR3A+ monocytes, B cells, dendritic cells, and platelets (Seurat 5.4.0).

Common issues

Top markers by fold-change aren’t always the textbook ones. Sorting by avg_log2FC surfaces the most specific genes, which can be obscure (FOLR3, LY6G6F) rather than the famous markers (CD14, PPBP). That is expected — a rare, perfectly cluster-specific gene scores a huge fold-change. Use the unbiased top-markers table to characterize a cluster, but annotate identities from a curated canonical marker panel plotted with FeaturePlot/VlnPlot, where the textbook genes do the naming.

RenameIdents() assigns labels by position, so order matters. names(new.ids) <- levels(pbmc) pairs your labels with the current identities in order (0, 1, 2, …). If your label vector is out of order, you will confidently mislabel every cluster. After renaming, always sanity-check with a DimPlot and a couple of VlnPlots that each named cluster expresses the marker you claim.

A cluster with no clear markers, or two cell types’ markers at once. Sometimes a cluster expresses markers of two lineages (a possible doublet) or has no distinctive genes at all (low-quality cells that slipped past QC). Don’t force a label. Flag it, consider tightening QC or the resolution, and — for doublets specifically — a dedicated tool such as DoubletFinder is the right next step.

Frequently asked questions

Run FindAllMarkers(pbmc, only.pos = TRUE, min.pct = 0.25, logfc.threshold = 0.25) on your clustered object. It performs a Wilcoxon rank-sum test for every gene in every cluster (cluster vs all other cells) and returns the up-regulated, well-expressed, clearly-changed genes. Sort each cluster’s markers by avg_log2FC to see its strongest defining genes.

FindAllMarkers() tests every cluster against all the others in one call — use it to annotate the whole dataset. FindMarkers(ident.1 = "A", ident.2 = "B") compares two specific groups — use it when you want the differentially expressed genes between, say, two T-cell subsets, or one cluster versus one other.

Plot a short panel of canonical markers for your tissue (for PBMCs: IL7R, CD14, LYZ, MS4A1, CD8A, FCGR3A, GNLY, NKG7, FCER1A, PPBP) with FeaturePlot() or VlnPlot(), see which cluster each marker lights up, then map cluster → cell type and apply it with RenameIdents(). Always derive the mapping from your own data’s markers — cluster numbers are arbitrary and change with parameters.

avg_log2FC is the average log₂ fold-change of the gene in the cluster versus all other cells (higher = stronger marker). pct.1 is the fraction of cells in the cluster expressing the gene; pct.2 is the fraction outside it. A strong marker has a high avg_log2FC and a pct.1 much larger than pct.2.

For peripheral blood: IL7R/CCR7 (naive CD4 T), IL7R/S100A4 (memory CD4 T), CD8A/GZMK (CD8 T), GNLY/NKG7 (NK), CD14/LYZ (CD14+ monocytes), FCGR3A/MS4A7 (FCGR3A+ monocytes), MS4A1/CD79A (B cells), FCER1A/CST3 (dendritic cells), and PPBP/PF4 (platelets).

Test your understanding

Starting from the clustered pbmc3k object, run FindAllMarkers with the standard settings, pull the top 5 markers for cluster 3 by avg_log2FC, and confirm with a VlnPlot of MS4A1 that cluster 3 is the B cells.

Filter the markers data frame to cluster == 3, then slice_max(avg_log2FC, n = 5). VlnPlot(pbmc, features = "MS4A1") shows the per-cluster distribution — the B-cell cluster should be the only one that is high.

library(Seurat)
library(dplyr)
pbmc.data <- Read10X("_data/filtered_gene_bc_matrices/hg19")
pbmc <- CreateSeuratObject(pbmc.data, project = "pbmc3k",
                           min.cells = 3, min.features = 200)
pbmc[["percent.mt"]] <- PercentageFeatureSet(pbmc, pattern = "^MT-")
pbmc <- subset(pbmc, subset = nFeature_RNA > 200 & nFeature_RNA < 2500 & percent.mt < 5)
pbmc <- NormalizeData(pbmc)
pbmc <- FindVariableFeatures(pbmc, selection.method = "vst", nfeatures = 2000)
pbmc <- ScaleData(pbmc)
pbmc <- RunPCA(pbmc, features = VariableFeatures(pbmc))
pbmc <- FindNeighbors(pbmc, dims = 1:10)
pbmc <- FindClusters(pbmc, resolution = 0.5)

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

markers %>%
  filter(cluster == 3) %>%
  slice_max(avg_log2FC, n = 5)        # LINC00926, VPREB3, CD79A, ... — all B-cell genes

VlnPlot(pbmc, features = "MS4A1")     # high only in cluster 3

Cluster 3’s top markers include CD79A and MS4A1 — both B-cell genes — and the MS4A1 violin is high in cluster 3 alone. The label “B cells” is well supported.

A. CD8 T cells — they express cytotoxic genes. B. NK cells — cytotoxic markers without CD8. C. CD14+ monocytes.

B. NK cells. Both CD8 T cells and NK cells express the cytotoxic genes GNLY and NKG7, so those alone don’t distinguish them. The discriminator is CD8A: present in CD8 T cells, absent in NK cells. High cytotoxic markers without CD8A is the NK signature.

Conclusion

Annotation is the payoff of the single-cell workflow, and it is two moves. FindAllMarkers(only.pos = TRUE, min.pct = 0.25, logfc.threshold = 0.25) gives you the genes that define each cluster, and a curated panel of canonical markers — plotted with FeaturePlot and VlnPlot — tells you which known cell type each cluster is. You reasoned from markers to identities (naive vs memory CD4 T from CCR7; CD8 T vs NK from CD8A; classical vs non-classical monocytes from CD14 vs FCGR3A), relabelled with RenameIdents(), and drew the annotated UMAP — a named map of peripheral blood. The durable lesson: cluster numbers are arbitrary, identities come from genes, and an honest annotation says which labels are certain and which are judgment calls.

That completes the pbmc3k guided analysis — from a raw 10x matrix to named cell types. Going further usually means more cells and more conditions: integrating multiple samples, correcting batch effects, scoring cell states, or modelling differentiation trajectories — analyses that are both pipeline-heavy and compute-heavy. That is where a managed compute environment earns its place: the free lessons teach the engine (Seurat), and running it at scale on real datasets is what Arkon is built for.

References

  • Hao, Y., et al. (2021). Integrated analysis of multimodal single-cell data (Seurat v4). Cell, 184(13), 3573–3587.
  • Satija, R., Farrell, J. A., Gennert, D., Schier, A. F., & Regev, A. (2015). Spatial reconstruction of single-cell gene expression data. Nature Biotechnology, 33(5), 495–502.
  • Stuart, T., et al. (2019). Comprehensive integration of single-cell data. Cell, 177(7), 1888–1902.
  • 10x Genomics. 3k PBMCs from a Healthy Donor — the canonical pbmc3k filtered gene–barcode matrix (Cell Ranger 1.1.0).

Reuse

Citation

BibTeX citation:
@online{2026,
  author = {},
  title = {Seurat {Marker} {Genes} \& {Cell-Type} {Annotation} in {R}
    (Pbmc3k)},
  date = {2026-06-29},
  url = {https://www.datanovia.com/learn/bioinformatics/single-cell/scrnaseq-marker-genes-annotation.html},
  langid = {en}
}
For attribution, please cite this work as:
“Seurat Marker Genes & Cell-Type Annotation in R (Pbmc3k).” 2026. June 29. https://www.datanovia.com/learn/bioinformatics/single-cell/scrnaseq-marker-genes-annotation.html.