Advanced Tutorial — PBMC 8k Clustering & Subclustering (R Seurat vs Truecell)¶
A more complex companion to the PBMC 3k tutorial. It reproduces the standard Seurat guided-clustering workflow on a larger 10x Genomics dataset (~8,400 PBMCs, GRCh38) and then adds the advanced subclustering step the Seurat reference papers use to resolve fine-grained immune states: the T/NK lymphoid compartment is isolated and re-analysed from scratch to separate naive CD4, memory CD4, CD8, and NK populations that the global clustering merges.
Dataset: 8k PBMCs from a Healthy Donor — 10x Genomics (GRCh38, v2) Python: Truecell v0.2.0 Methodology: Satija et al. 2015 · Butler et al. 2018 · Hao et al. 2021
Scope note. This tutorial stays within the single-assay RNA graph-based clustering workflow these papers established — which is exactly where subclustering shines. SCTransform and multimodal WNN are covered in the other tutorials; anchor-based (CCA/RPCA) integration is not yet ported.
About the figures. Each step shows a genuine side-by-side comparison: the left image is real R Seurat output (
pbmc8k_subclustering_verify.R, titled "R Seurat – …") and the right image is Truecell (generate_advanced_plots.py). Both run the identical pipeline and the same two annotation heuristics on the same data; clustering is stochastic and the two UMAP libraries (uwotvsumap-learn) place clusters differently, so compare the structure, not exact positions or cluster numbers.
Run everything with:
python tutorials/pbmc8k_subclustering_tutorial.py # printed validation
python tutorials/generate_advanced_plots.py # writes figures_advanced/ (Truecell figures)
Rscript tutorials/pbmc8k_subclustering_verify.R # writes figures_advanced/r_* (R Seurat figures)
Step 1 · Load Data & Create Object¶
| R (Seurat) | Python (Truecell) |
|---|---|
Step 2 · QC & Filtering¶
Step 3 · Normalize → HVG → Scale → PCA¶
Step 4 · Global Clustering & UMAP¶
Step 5 · Lineage Markers & Broad Annotation¶
Canonical lineage markers identify the major PBMC populations. Truecell's
annotate_clusters() helper z-scores each marker across clusters and assigns
every cluster to its most-enriched lineage.
Top markers per cluster (DoHeatmap → do_heatmap):
| R (Seurat) | Python (Truecell) |
|---|---|
![]() |
![]() |
Step 6 · Subcluster the T/NK Compartment ⭐¶
This is the advanced step. The global clustering lumps several T-cell states together. We isolate the T/NK lineages and re-run the entire workflow on just those cells — recomputing variable genes, PCA, neighbours, clusters, and UMAP within the compartment. This is the standard Seurat subclustering recipe.
The whole pipeline is wrapped in
run_pipeline()/run_full()in pbmc8k_subclustering_tutorial.py, so the subclustering is a singlepbmc.subset(...)followed by the same calls.
Step 7 · Annotate the Fine T/NK Subsets¶
Flat marker-argmax fails for fine T subsets (the high-magnitude naive markers
CCR7/SELL outscore the low-magnitude but definitive CD8B), so
annotate_tnk_subsets() gates in lineage-priority order — NK (CD3⁻, NKG7/GNLY⁺)
→ CD8/cytotoxic T (CD8A/B or a CD3⁺ NKG7/GZMK program, which also captures
MAIT/γδ T) → CD4 Naive (CCR7/SELL/LEF1) → CD4 Memory (IL7R/S100A4).
What the subclustering reveals¶
Substructure that the 12 global clusters did not separate:
| Subset | Defining markers | Notes |
|---|---|---|
| CD4 Naive | CCR7, SELL, LEF1 (+ ribosomal) |
quiescent helper T |
| CD4 Memory | IL7R, S100A4, LTB |
activated/memory helper T |
| CD8 T | CD8A, CD8B, GZMK, CCL5 |
naive + cytotoxic effector |
| NK | GNLY, NKG7, KLRD1, PRF1 |
CD3-negative cytotoxic |
The CD8/cytotoxic group also resolves a TRDC/TRGC1 γδ-T subcluster and a
KLRB1/NCR3 MAIT subcluster — unconventional cytotoxic T cells that are
invisible at the global resolution.
How close is this to Seurat?¶
Every figure above is a side-by-side, which means it was checked by eye.
pbmc8k_subclustering_verify.R also writes a numeric handoff, and --report
compares it:
python tutorials/pbmc8k_subclustering_tutorial.py # writes the Python side
Rscript tutorials/pbmc8k_subclustering_verify.R # writes the R side
python tutorials/pbmc8k_subclustering_tutorial.py --report
Nothing is pinned across the two sides. Both run the whole two-stage pipeline from the same 10x bytes.
| Stage | truecell vs Seurat 5.5.1 |
|---|---|
| QC | the same 7,475 barcodes; nCount and nFeature exact, percent.mt to 5.3e-15 |
| Normalized data | total to 8.2e-13 relative · kNN graph 149,500 on both |
| Stage 1 clusters | 13 vs 12 · ARI 0.977 · 7,341/7,475 cells agree |
| Broad lineage, per cell | 0.9858 |
| T/NK compartment | Jaccard 0.9991 — 4,631 of 4,635 cells are the same barcodes |
| Stage 2 subclusters | 12 vs 11 · ARI 0.916 on the shared cells |
| T/NK subset label, per cell | 0.9821; the four subset sizes agree within 25 cells |
Why the compartment is compared by barcode¶
The T/NK compartment is the number to watch, and it is the reason the handoff dumps cell tables rather than summary counts. Everything in stage two is conditioned on which cells stage one selected — so a subclustering fed from the wrong global clusters would still produce a compartment of a plausible size, still produce subclusters, still produce a marker table, and a size check would call all of it a match. Only comparing the barcodes can tell "the same 4,600 cells" from "4,600 cells". Here they are the same to four cells.
The extra cluster¶
truecell finds 13 global clusters where Seurat finds 12. Seurat's 100-cell cluster 11 holds both Platelet and DC; truecell splits it into a 54-cell DC cluster and a 53-cell Platelet cluster. Two distinct myeloid lineages, so the split is the more resolved answer.
Note that this runs opposite to the PBMC 3k tutorial, where Seurat resolves a 32-cell DC cluster that truecell folds into CD14+ Mono. Both are the same borderline population landing on different sides of a resolution threshold, and neither run is uniformly finer than the other — which is worth knowing before reading a cluster count as a verdict.
API Translation (additions beyond the PBMC 3k tutorial)¶
| Task | R (Seurat) | Python (Truecell) |
|---|---|---|
| Load PBMC 8k | Read10X("…/GRCh38/") |
pbmc8k() |
| Subset a lineage | subset(pbmc, idents = …) |
pbmc.subset(cells = …) |
| Stash current ids | pbmc$old <- Idents(pbmc) |
pbmc.stash_ident("old") |
| Re-cluster subset | re-run FindVariableFeatures…FindClusters |
re-run find_variable_features…find_clusters |
References¶
Satija R, Farrell JA, Gennert D, Schier AF, Regev A (2015). Spatial reconstruction of single-cell gene expression data. Nature Biotechnology 33, 495–502. https://doi.org/10.1038/nbt.3192
Butler A, Hoffman P, Smibert P, Papalexi E, Satija R (2018). Integrating single-cell transcriptomic data across different conditions, technologies, and species. Nature Biotechnology 36, 411–420. https://doi.org/10.1038/nbt.4096
Hao Y, Hao S, Andersen-Nissen E, et al. (2021). Integrated analysis of multimodal single-cell data. Cell 184, 3573–3587. https://doi.org/10.1016/j.cell.2021.04.048
10x Genomics (2017). 8k PBMCs from a Healthy Donor. https://www.10xgenomics.com/datasets/8-k-pbm-cs-from-a-healthy-donor-2-standard-2-1-0





















