Cell Type Annotation

Last updated on 2026-10-27 | Edit this page

Estimated time: 60 minutes

Overview

Questions

  • How do we systematically find the genes that define each cluster?
  • How do we assign cell type labels using known marker genes?
  • How does automated annotation with SingleR work?
  • When do manual and automated annotations disagree and what should we do?
  • What are best practices for confident, reproducible annotation?

Objectives

  • Run FindAllMarkers to identify differentially expressed genes for every cluster
  • Interpret marker gene statistics: avg_log2FC, pct.1, pct.2, and p_val_adj
  • Manually annotate clusters using known PBMC marker genes
  • Use SingleR with the Monaco Immune reference for automated cell type annotation
  • Compare manual and automated annotations and resolve discrepancies
Prerequisite

Prerequisites

This episode requires an RStudio session on the Negishi cluster. Launch RStudio (bioconductor) under Bioinformatics Apps on Open OnDemand as described in the Setup instructions. You will also need the pbmc_clustered.rds object saved at the end of the previous episode.

Setup


R

library(Seurat)
library(ggplot2)
library(patchwork)
library(SingleR)
library(celldex)
library(dplyr)

Loading the Clustered Data


We start from the clustered Seurat object saved at the end of the previous episode. Make sure the active identity is set to the resolution 0.5 clusters.

Set up the working directory to match the previous episodes:

R

work_dir <- paste0(
    "/scratch/negishi/", Sys.getenv("USER"),
    "/scrna_workshop/"
)
setwd(work_dir)
Callout

Starting here without the previous episode?

Copy the saved object from the workshop data first: file.copy(paste0(work_dir, "rds/pbmc_clustered.rds"), work_dir)

R

pbmc <- readRDS(paste0(work_dir, "pbmc_clustered.rds"))
Idents(pbmc) <- "RNA_snn_res.0.5"
DimPlot(pbmc, reduction = "umap", label = TRUE, label.size = 5) + NoLegend()
UMAP plot with 18 clusters labeled 0 through 17 at resolution 0.5, matching the clustering results from the previous episode.
Clustered UMAP from previous episode

You should see the UMAP with 18 clusters labeled by number (0 to 17), matching the clustering results from the previous episode.

Finding Marker Genes


Up to now, our clusters are just numbers. To give them biological meaning, we need to find the genes that distinguish each cluster from all other clusters. These are called marker genes – genes that are significantly upregulated in one cluster compared to the rest.

FindAllMarkers() runs a differential expression test for every cluster, comparing cells in that cluster against all other cells:

R

pbmc.markers <- FindAllMarkers(pbmc,
                               only.pos = TRUE,
                               min.pct = 0.25,
                               logfc.threshold = 0.25)
Parameter Value Description
only.pos TRUE Only return genes that are upregulated in the cluster (positive log fold change). We are interested in markers that define a cluster, not genes that are absent from it.
min.pct 0.25 Only test genes detected in at least 25% of cells in either the cluster or the rest. This speeds up the computation by skipping very rare genes that cannot be reliable markers.
logfc.threshold 0.25 Only test genes with at least a 0.25 log2 fold change between the cluster and the rest. This pre-filters out genes with trivially small differences.

This step may take 5 minutes.

OUTPUT

Calculating cluster 0
Calculating cluster 1
Calculating cluster 2
Calculating cluster 3
Calculating cluster 4
Calculating cluster 5
Calculating cluster 6
Calculating cluster 7
Calculating cluster 8
Calculating cluster 9
Calculating cluster 10
Calculating cluster 11
Calculating cluster 12
Calculating cluster 13
Calculating cluster 14
Calculating cluster 15
Calculating cluster 16
Calculating cluster 17

The result is a data frame with one row per gene per cluster. Let’s look at the key columns:

R

head(pbmc.markers)

OUTPUT

       p_val avg_log2FC pct.1 pct.2 p_val_adj cluster   gene
INPP4B     0   2.607036 0.972 0.345         0       0 INPP4B
CAMK4      0   1.653160 0.950 0.324         0       0  CAMK4
IL7R       0   2.067086 0.935 0.336         0       0   IL7R
CD3D       0   1.523352 0.923 0.334         0       0   CD3D
ITK        0   1.775408 0.920 0.338         0       0    ITK
BCL11B     0   1.586310 0.991 0.413         0       0 BCL11B

The columns mean:

Column Meaning
p_val Raw p-value from the Wilcoxon rank-sum test (default test).
avg_log2FC Average log2 fold change between the cluster and all other cells. A value of 1.0 means the gene is on average 2x higher in this cluster.
pct.1 Fraction of cells in the cluster where the gene is detected.
pct.2 Fraction of cells outside the cluster where the gene is detected.
p_val_adj Bonferroni-adjusted p-value. Use this for significance filtering (typically < 0.05).
cluster Which cluster this gene is a marker for.
gene The gene symbol.

A good marker gene has a high avg_log2FC, high pct.1 (expressed in most cells of the cluster), low pct.2 (not expressed in other clusters), and a significant p_val_adj.

Animation: How FindAllMarkers scores a gene

Top markers per cluster

Let’s extract the top 5 markers per cluster ranked by fold change:

R

top5 <- pbmc.markers %>%
    dplyr::group_by(cluster) %>%
    dplyr::slice_max(n = 5, order_by = avg_log2FC)
top5

OUTPUT

# A tibble: 90 × 7
# Groups:   cluster [18]
   p_val avg_log2FC pct.1 pct.2 p_val_adj cluster gene
   <dbl>      <dbl> <dbl> <dbl>     <dbl> <fct>   <chr>
 1     0       5.23 0.258 0.011         0 0       IATPR
 2     0       3.69 0.345 0.036         0 0       ST8SIA1
 3     0       3.17 0.346 0.052         0 0       IL2RA
 4     0       2.88 0.414 0.072         0 0       ICOS
 5     0       2.75 0.521 0.103         0 0       FAAH2
 6     0       4.46 0.29  0.023         0 1       ENSG00000289381
 7     0       4.28 0.5   0.05          0 1       AQP9
 8     0       4.13 0.423 0.043         0 1       MTARC1
 9     0       4.09 1     0.549         0 1       S100A8
10     0       3.98 0.996 0.248         0 1       S100A12   

Visualizing markers with a heatmap

A heatmap of top marker genes across all clusters shows the expression patterns at a glance. Each column is a cell (grouped by cluster), each row is a gene, and color intensity represents scaled expression:

R

top3 <- pbmc.markers %>%
    dplyr::group_by(cluster) %>%
    dplyr::slice_max(n = 3, order_by = avg_log2FC)

DoHeatmap(pbmc, features = top3$gene) + NoLegend()
Heatmap showing expression of the top 3 marker genes per cluster across all 18 clusters. Each cluster shows a distinct block of upregulated genes, confirming distinct transcriptional identities.
Heatmap of top 3 markers per cluster

Each cluster should show a distinct block of highly expressed genes (bright yellow/white) that are low in other clusters (dark purple). Clusters with very similar heatmap profiles may represent subtypes of the same cell type.

Dot plot summary

A dot plot provides a compact summary of marker expression across clusters. The dot size encodes the percentage of cells expressing the gene, and the color intensity encodes the average expression level:

R

DotPlot(pbmc, features = unique(top3$gene)) + coord_flip() +
    theme(axis.text.x = element_text(angle = 45, hjust = 1))
Dot plot showing expression of top 3 marker genes across all 18 clusters. Dot size indicates the percentage of cells expressing each gene; dot shading indicates average expression level.
Dot plot of top marker genes

Manual Annotation with Known Markers


Now we match the marker gene patterns to known PBMC cell types. The table below lists the canonical markers for each expected cell type:

Cell type Key markers Expected %
CD4+ T cells CD3D, IL7R, CCR7 ~35%
CD8+ T cells CD3D, CD8A, CD8B ~10%
B cells MS4A1, CD79A ~10%
CD14+ Monocytes LYZ, CD14, S100A9 ~20%
FCGR3A+ Monocytes FCGR3A, MS4A7 ~5%
NK cells GNLY, NKG7, KLRD1 ~10%
Dendritic cells FCER1A, CST3 ~3%
Plasmacytoid DCs (pDC) LILRA4, IL3RA ~0.5%
Platelets PPBP, PF4 ~2%
Haematopoietic progenitors (HSPC) CD34 <0.5%

Let’s visualize these markers on the UMAP to see which clusters they map to:

R

FeaturePlot(pbmc,
            features = c("CD3D", "IL7R", "CD8A", "MS4A1",
                         "LYZ", "FCGR3A", "GNLY", "FCER1A"),
            ncol = 4)
Grid of 8 UMAP panels showing expression of CD3D, IL7R, CD8A, MS4A1, LYZ, FCGR3A, GNLY, and FCER1A. Each marker highlights a distinct UMAP region: CD3D and IL7R in the upper-left T cell area, CD8A in a small cluster on the right, MS4A1 in B cells on the right, LYZ in monocytes at the lower-left, FCGR3A in a small cluster at the lower-right, GNLY in NK cells at the upper-center, and FCER1A in a small dendritic cell group.
FeaturePlot of 8 canonical PBMC markers

And as violin plots to see expression across clusters:

R

VlnPlot(pbmc,
        features = c("CD3D", "IL7R", "CD8A", "MS4A1",
                     "LYZ", "FCGR3A", "GNLY", "PPBP"),
        ncol = 4,
        pt.size = 0)
Violin plots of CD3D, IL7R, CD8A, MS4A1, LYZ, FCGR3A, GNLY, and PPBP across all 18 clusters. Each marker shows high expression in the clusters corresponding to its known cell type and low expression elsewhere.
Violin plots of 8 PBMC markers across clusters

Based on these plots and the FindAllMarkers results, we can build a mapping from cluster numbers to cell type names: find which clusters express each set of markers.

Callout

Check the mapping against your own clusters

The labels below fit the 18 clusters that this lesson’s matrix gives with the package versions in the setup instructions. If your matrix or your package versions differ, the number or the order of the clusters can change, and a label vector written for someone else’s clusters puts the wrong names on yours without any error. Before trusting the labels, plot the canonical markers per cluster and compare each row with the table above and the comment next to each label below:

R

DotPlot(pbmc, features = c("CD3D", "IL7R", "CCR7", "CD8A", "CD8B", "MS4A1",
                           "CD79A", "LYZ", "CD14", "FCGR3A", "MS4A7", "GNLY",
                           "NKG7", "FCER1A", "CLEC10A", "LILRA4", "IL3RA",
                           "PPBP", "PF4", "CD34")) + coord_flip()

The code below also stops if your number of clusters does not match the number of labels.

Animation: Labels are assigned by position

R

# Build the cluster-to-cell-type mapping
# Adjust cluster IDs to match YOUR results
new.cluster.ids <- c(
    "CD4 T",           # 0: CD3D+, IL7R+, CD8A-
    "CD14 Mono",       # 1: LYZ+, CD14+, S100A9+
    "CD14 Mono",       # 2: LYZ+, CD14+, S100A9+
    "CD4 T",           # 3: CD3D+, IL7R+, CCR7+ (naive)
    "B",               # 4: MS4A1+, CD79A+
    "CD8 T",           # 5: CD3D+, CD8A+, CD8B+, NKG7+ (cytotoxic)
    "CD8 T",           # 6: CD3D+, CD8A+, IL7R+, NKG7+, CD8B low
    "NK",              # 7: GNLY+, NKG7+, KLRD1+, CD3D-
    "B",               # 8: MS4A1+, CD79A+
    "FCGR3A+ Mono",    # 9: FCGR3A+, MS4A7+
    "CD8 T",           # 10: CD3D+, CD8A+, CD8B+, CCR7+ (naive)
    "CD14 Mono",       # 11: LYZ+, CD14+
    "DC",              # 12: FCER1A+, CLEC10A+, CST3+
    "Platelet",        # 13: PPBP+, PF4+
    "B",               # 14: MS4A1+, CD79A+, some T cell signal
    "pDC",             # 15: LILRA4+, IL3RA+
    "CD14 Mono",       # 16: LYZ+, CD14+, some MS4A1
    "HSPC"             # 17: CD34+, CD3D-
)
if (nlevels(pbmc) != length(new.cluster.ids)) stop("pbmc has ", nlevels(pbmc), " clusters but new.cluster.ids has ", length(new.cluster.ids), " labels; check the markers and edit new.cluster.ids")
names(new.cluster.ids) <- levels(pbmc)
pbmc <- RenameIdents(pbmc, new.cluster.ids)
Callout

Annotation is iterative

Your first-pass annotation will not always be correct. This is normal. After assigning labels, go back and check:

  • Do the labels make biological sense? Is the proportion of each cell type reasonable for PBMCs?
  • Are there clusters where multiple marker sets overlap? This may indicate a mixed cluster that should be sub-clustered at higher resolution.
  • Are there clusters with no clear markers? These may be low-quality cells that slipped through QC, or a rare cell type you didn’t expect.

Annotation is an iterative process: assign labels, verify with markers, revise, and repeat until you are confident in the assignments.

Visualize the final annotated UMAP:

R

DimPlot(pbmc, reduction = "umap", label = TRUE, repel = TRUE) + NoLegend()
UMAP plot with cells labeled by manually assigned cell type names: CD4 T, CD14 Mono, B, CD8 T, NK, FCGR3A+ Mono, DC, Platelet, pDC, and HSPC.
Annotated UMAP with cell type labels

Store the annotation in the metadata so it persists after saving:

R

pbmc$manual_annotation <- Idents(pbmc)

Automated Annotation with SingleR


Manual annotation requires expert knowledge of marker genes for every expected cell type. SingleR offers an automated alternative: it compares the expression profile of each cell to a labeled reference dataset and assigns the label of the most similar reference sample.

Animation: How SingleR labels a cell

Download the reference

We use the Monaco Immune Data reference from the celldex package. This reference contains bulk RNA-seq profiles of sorted human immune cell populations, making it well-suited for PBMC annotation.

R

ref <- celldex::MonacoImmuneData()
ref

OUTPUT

class: SummarizedExperiment
dim: 46077 114
metadata(0):
assays(1): logcounts
rownames(46077): A1BG A1BG-AS1 ... ZYX ZZEF1
rowData names(0):
colnames(114): DZQV_CD8_naive DZQV_CD8_CM ... G4YW_Neutrophils G4YW_Basophils
colData names(3): label.main label.fine label.ont

The reference has 114 samples across multiple immune cell types. The label.main column has broad cell type labels (e.g., “Monocytes”, “CD4+ T cells”) that match well with our expected PBMC types.

Run SingleR

SingleR works on a SingleCellExperiment object. We convert our Seurat object and then run the annotation:

R

# Convert to SingleCellExperiment
sce <- as.SingleCellExperiment(DietSeurat(pbmc))

# Run SingleR with Monaco Immune reference
results <- SingleR(test = sce,
                   ref = ref,
                   labels = ref$label.main)
Parameter Value Description
test sce The query dataset as a SingleCellExperiment. SingleR uses the log-normalized expression values.
ref ref The reference dataset with known labels.
labels ref$label.main Which column of the reference metadata to use as cell type labels. label.main gives broad categories; label.fine gives more specific subtypes.

This runs for 1–2 minutes. SingleR assigns a label to every individual cell, not to clusters. Let’s transfer the labels to our Seurat object:

R

pbmc$singler_labels <- results$labels

Visualize the SingleR annotations on the UMAP:

R

DimPlot(pbmc, group.by = "singler_labels", reduction = "umap",
        label = TRUE, repel = TRUE) + NoLegend()
UMAP plot with cells colored by SingleR automated annotation labels from the Monaco Immune reference, showing CD4+ T cells, CD8+ T cells, T cells, Monocytes, B cells, NK cells, Dendritic cells, Progenitors, and Basophils.
SingleR annotated UMAP

Comparing manual and automated annotations

The real power of annotation comes from comparing multiple approaches. Let’s create a cross-tabulation of our manual labels versus SingleR labels:

R

table(Manual = pbmc$manual_annotation, SingleR = pbmc$singler_labels)

OUTPUT

              SingleR
Manual         B cells Basophils CD4+ T cells CD8+ T cells Dendritic cells Monocytes NK cells Progenitors
  CD4 T              3         0         2586          183               0         5        1           1
  CD14 Mono          3         0            0            0              32      3259        1           0
  B               1614         0            0            0               5         0       15           0
  CD8 T              0         0            9          400               0         0       31           0
  NK                 0         0            3            1               0         0      607           0
  FCGR3A+ Mono       0         0            0            0               1       428        0           0
  DC                 0         0            0            0             157        19        0           0
  Platelet           4         2            3            2               1         6        0         154
  pDC                0         0            0            0              78         0        0           0
  HSPC               0         0            1            0               0         0       11          21
              SingleR
Manual         T cells
  CD4 T            348
  CD14 Mono          0
  B                 11
  CD8 T           1272
  NK                23
  FCGR3A+ Mono       0
  DC                 0
  Platelet           3
  pDC                0
  HSPC               6

The table shows broad agreement, and the disagreements that remain are instructive:

  • Strong agreement: SingleR calls 3,259 of 3,295 CD14 Monocytes “Monocytes”, 1,614 of 1,645 B cells “B cells”, 607 of 634 NK cells “NK cells”, 428 of 429 FCGR3A+ Monocytes “Monocytes”, and 157 of 176 DCs “Dendritic cells”. These cell types have distinctive transcriptional profiles that both methods recognize.
  • CD4 T cells: 2,586 of 3,127 are “CD4+ T cells”. Most of the rest (348) get the generic “T cells” label.
  • CD8 T cells: only 400 of 1,712 are “CD8+ T cells”; 1,272 are generic “T cells”. Nearly all of those come from cluster 6, which expresses CD8A, IL7R and NKG7 but little CD8B. That pattern fits mucosal-associated invariant T (MAIT) cells, which the Monaco reference groups with gamma-delta T cells under its generic “T cells” label. Our coarse “CD8 T” label is defensible; sub-clustering would separate them.
  • Platelets called “Progenitors”: 154 of our 175 platelets (PPBP+, PF4+) are called “Progenitors”. Monaco Immune Data has no platelets, so SingleR assigns the closest profile it has. Always check rare or non-standard cell types with markers.
  • pDCs called “Dendritic cells”: all 78 pDCs (LILRA4+, IL3RA+) get the coarser “Dendritic cells” label, which is correct at this level; Monaco’s fine labels separate plasmacytoid from myeloid DCs.
  • HSPC: the 39 CD34+ cells of cluster 17 are mostly “Progenitors” (21), with some “NK cells” (11). A cluster this small is often a mixture; it is labelled for completeness.

The key takeaway: agreement on the large populations gives confidence in both methods, and every disagreement has a reason you can check with marker genes. A large disagreement (for example, a whole cluster you labelled as T cells called “B cells” by SingleR) is a signal to go back to the markers for that cluster before trusting either label. Use both methods and investigate every disagreement.

Visualize both annotations side by side:

R

p1 <- DimPlot(pbmc, group.by = "manual_annotation", label = TRUE, repel = TRUE) +
    NoLegend() + ggtitle("Manual annotation")
p2 <- DimPlot(pbmc, group.by = "singler_labels", label = TRUE, repel = TRUE) +
    NoLegend() + ggtitle("SingleR annotation")
p1 + p2
Two UMAP plots side by side. Left panel shows manual annotation with CD4 T, CD14 Mono, B, CD8 T, NK, FCGR3A+ Mono, DC, Platelet, pDC, and HSPC labels. Right panel shows SingleR annotation with CD4+ T cells, CD8+ T cells, T cells, Monocytes, B cells, NK cells, Dendritic cells, Progenitors, and Basophils. Most cell groups receive consistent labels between the two methods.
Manual vs SingleR annotation side by side

When the two methods agree, you can be very confident in the label. When they disagree, check the marker gene expression for the disputed cluster and decide which label fits better. In general:

  • Trust manual annotation when the markers are clear and well-established for the tissue type you are studying
  • Trust SingleR when you are unfamiliar with the tissue or when the reference provides finer subtypes than you would identify manually

Save the final annotated object:

R

saveRDS(pbmc, file = "pbmc_annotated.rds")

Annotation Best Practices


Callout

Guidelines for confident annotation

  1. Use multiple references. No single reference is perfect. The celldex package provides several options for human immune cells:

    • MonacoImmuneData() – sorted immune populations, best for PBMCs
    • HumanPrimaryCellAtlasData() (HPCA) – broader coverage including non-immune cell types, but less specific for immune subtypes
    • DatabaseImmuneCellExpressionData() (DICE) – another immune-focused reference with different experimental conditions

    Running SingleR with multiple references and checking for agreement increases confidence.

  2. Cross-validate automated labels with marker genes. Never accept automated labels blindly. Always check that the assigned label is consistent with the expression of established markers for that cell type.

  3. Document your decisions. Record which markers you used, which reference datasets, and any ambiguous cases you resolved manually. This makes your analysis reproducible and allows others to evaluate your annotation choices.

  4. Sub-cluster when needed. If a cluster shows mixed marker expression (e.g., both CD4 and CD8 markers), it may contain two cell types that were merged at the current resolution. Re-run FindClusters() at higher resolution on just that subset of cells to resolve the mixture:

    R

    subset_cells <- subset(pbmc, idents = "mixed_cluster")
    subset_cells <- FindNeighbors(subset_cells, dims = 1:15)
    subset_cells <- FindClusters(subset_cells, resolution = 0.5)
Challenge

Challenge 1: Identifying T cell subtypes from marker combinations

Plot five T cell markers across the annotated groups. In the CD4 T group, CD3D and IL7R are expressed almost everywhere, but SELL and CCR7 only in part of it. What does that tell you about the composition of this group? Then look at CD69, an early activation marker: which groups express it, and can it pick out activated T cells in this dataset?

R

VlnPlot(pbmc, features = c("CD3D", "SELL", "CCR7", "CD69", "IL7R"), ncol = 5, pt.size = 0)
Violin plots of CD3D, SELL, CCR7, CD69, and IL7R across the ten annotated cell types. CD3D and IL7R are high in CD4 T and CD8 T cells and near zero elsewhere. SELL is expressed in most groups. CCR7 is expressed mainly in CD4 T cells. CD69 is high in CD4 T, CD8 T, B, NK and HSPC cells alike.
Violin plots of T cell subtype markers

Percent of cells in each group with any expression of the marker:

Marker CD4 T CD8 T B NK Interpretation
CD3D 92% 91% 9% 13% Confirms T cell identity
IL7R (CD127) 93% 85% 11% 13% Naive and memory T cells
SELL (CD62L) 69% 41% 80% 45% Lymph node homing; naive and central memory T cells
CCR7 55% 26% 34% 6% Lymph node homing; high in naive, low in effector memory T cells
CD69 88% 87% 90% 87% Early activation marker

The CD4 T group is a mixture. Nearly every cell is CD3D+ and IL7R+, so the group is T cells throughout. But only about two thirds express SELL and about half express CCR7. The SELL+ CCR7+ cells are naive and central memory T cells, which recirculate through lymph nodes; the SELL- CCR7- cells are effector memory T cells. Most of the CCR7 signal comes from cluster 3, one of the two clusters we merged into “CD4 T”.

CD8 T cells are more differentiated. SELL (41%) and CCR7 (26%) are lower than in CD4 T cells, so the CD8 T group holds more effector and effector memory cells, including the MAIT-like cluster 6.

CD69 does not separate activated cells here. It is expressed in almost 90% of CD4 T, CD8 T, B and NK cells alike. CD69 is an immediate early gene that can also be switched on during sample collection and processing, so in a dataset like this one it does not mark a recently activated subset. Check whether an “activation” marker is specific before interpreting it.

SELL alone does not mark T cells. B cells (80%), pDCs and HSPCs express it too; you need CD3D to confirm T cell identity.

This illustrates why our coarse “CD4 T” label is a simplification. With higher clustering resolution or sub-clustering, you could separate the naive and memory subsets. For many analyses this level of granularity is sufficient, but for studies focused on T cell biology you would want to sub-cluster further.

Challenge

Challenge 2: Comparing two SingleR references

Run SingleR with both MonacoImmuneData and HumanPrimaryCellAtlasData. For which cell types do the two references disagree? Why might they disagree?

R

# Monaco Immune reference (already computed above)
ref_monaco <- celldex::MonacoImmuneData()
results_monaco <- SingleR(test = sce, ref = ref_monaco, labels = ref_monaco$label.main)

# Human Primary Cell Atlas reference
ref_hpca <- celldex::HumanPrimaryCellAtlasData()
results_hpca <- SingleR(test = sce, ref = ref_hpca, labels = ref_hpca$label.main)

# Transfer labels
pbmc$singler_monaco <- results_monaco$labels
pbmc$singler_hpca <- results_hpca$labels

# Compare
table(Monaco = pbmc$singler_monaco, HPCA = pbmc$singler_hpca)

OUTPUT

                 HPCA
Monaco            B_cell   BM  CMP  GMP HSC_-G-CSF  MEP Monocyte NK_cell Platelets Pre-B_cell_CD34-
  B cells           1575    0    0    0          1    0        3       0         2                1
  Basophils            0    0    0    0          0    0        0       0         2                0
  CD4+ T cells         0    0    0    0          0    0        0       0         0                0
  CD8+ T cells         0    0    0    0          0    0        0       0         1                0
  Dendritic cells     36    0    0    8          1    0      218       0         1                6
  Monocytes            4    0    0    0          7    0     3669       5         5                8
  NK cells             2    0    0    0          0    0        0     649         0                1
  Progenitors          0    1   20    0          0    1        3       0       151                0
  T cells              3    0    0    0          0    0        0      20         1                0
                 HPCA
Monaco            Pro-Myelocyte T_cells
  B cells                     1      41
  Basophils                   0       0
  CD4+ T cells                0    2602
  CD8+ T cells                0     585
  Dendritic cells             0       4
  Monocytes                   0      19
  NK cells                    0      14
  Progenitors                 0       0
  T cells                     0    1639

The two references agree well on some cell types and disagree sharply on others. Here are the key patterns:

1. Strong agreement: Monocytes, B cells, NK cells. Monaco “Monocytes” map almost entirely to HPCA “Monocyte” (3,669 of 3,717). Monaco “B cells” map to HPCA “B_cell” (1,575 of 1,624). Monaco “NK cells” map to HPCA “NK_cell” (649 of 666). These cell types have distinctive profiles that both references recognize.

2. T cell subtyping collapses in HPCA. Monaco distinguishes “CD4+ T cells” (2,602), “CD8+ T cells” (586), and generic “T cells” (1,663). HPCA labels all of these simply as “T_cells.” If your analysis requires T cell subtype resolution, Monaco is the better reference. Interestingly, Monaco assigns 41 B cells to HPCA “T_cells,” suggesting a small population that sits near the T/B boundary (possibly NKT cells or doublets).

3. Dendritic cells split across HPCA labels. Monaco calls 274 cells “Dendritic cells,” but HPCA scatters these across “Monocyte” (218), “GMP” (8), “Pre-B_cell_CD34-” (6), “B_cell” (36), and only 4 as “T_cells.” HPCA lacks a dedicated dendritic cell category with sufficient resolution, so DCs get assigned to the nearest available profile – usually monocytes, which share many myeloid markers.

4. Progenitors become Platelets in HPCA. Monaco labels 176 cells as “Progenitors,” and HPCA calls 151 of those “Platelets.” Most of them (154) are the cells we manually annotated as platelets; another 21 are our HSPC cluster. Monaco does not have a platelet reference profile, so it assigns platelets to “Progenitors”, the nearest match it has. HPCA does include a platelet profile and gets this right. Neither reference is wrong – they just have different coverage.

5. HPCA introduces hematopoietic progenitor labels. HPCA assigns some cells to “BM,” “CMP,” “GMP,” “MEP,” “HSC_-G-CSF,” and “Pro-Myelocyte” – categories that are rare in blood (our 39-cell HSPC cluster is the genuine case). Most of these calls are artifacts of the HPCA reference containing bone marrow profiles. When a cell’s expression sits between two immune types, HPCA may assign a progenitor label rather than committing to either.

Lesson: No single reference is universally best. Monaco gives better immune subtype resolution (especially for T cells and DCs) and avoids spurious non-immune labels, making it the better default for PBMCs. HPCA correctly identifies platelets where Monaco fails. The practical approach: use Monaco as your primary reference for immune tissues, cross-check disagreements with HPCA, and always validate automated labels against known marker genes.

Key Points
  • FindAllMarkers identifies genes upregulated in each cluster versus all others; key output columns are avg_log2FC, pct.1, pct.2, and p_val_adj
  • Manual annotation maps cluster marker genes to known cell-type-specific markers and requires domain expertise
  • SingleR automates annotation by comparing each cell’s expression profile to a labeled reference dataset
  • Combining manual and automated approaches increases confidence; agreement between methods strongly supports the assigned label
  • Annotation is iterative and should be cross-validated with multiple references and marker gene visualization