Chapter 2 — Dimensionality Reduction & Clustering
Where this chapter sits. Companion to Lecture 02 and Tutorial 02. Builds on Chapter 1 (you start with a scaled HVG matrix). Prerequisites: Appendix B §§ 4–9 (linear models, eigendecomposition), P2 (sparse matrices, factors).
2.1 The mental model: project, partition, paint
After preprocessing, every cell is a point in a ~2,000-dimensional gene-expression space (one dimension per HVG). That space has too many dimensions to do anything useful with directly: most pairs of cells are nearly equidistant under Euclidean distance — the curse of dimensionality — and “neighbors” stop carrying meaning.
Three steps fix this:
- Project to a lower-dimensional space that keeps the structured variance (PCA → 20–50 PCs)
- Partition the cells into communities of mutually-similar neighbors (k-NN graph + Louvain/Leiden)
- Paint the result onto a 2-D picture for inspection (UMAP or t-SNE)
Each step is a separate idea with its own theory and failure modes. This chapter walks them in order.
Projection, partitioning, and painting are conceptually independent but computationally sequential: PCA produces the coordinate space that the neighbor graph requires; the neighbor graph produces the community structure that clustering formalizes; and UMAP or t-SNE produces the 2-D picture of the clustering, not a replacement for it. A common mistake is to draw conclusions from the UMAP shape without realizing that the clusters shown on it were computed in PC space — the UMAP layout can differ substantially between runs with different random seeds, but the cluster assignments are determined by the SNN graph and do not change with the UMAP. Read the clusters first; consult the UMAP only as a visual summary.
2.2 PCA — what it does and why it works first
Principal Component Analysis finds orthogonal directions of maximum variance in the data. Concretely, given the centered, scaled HVG matrix \(\tilde X\) (cells × genes), PCA computes the singular value decomposition
\[\tilde X = U \Sigma V^\top\]
where:
- \(V\) (genes × \(k\)) holds the loadings — each column is a direction in gene space
- \(U\Sigma\) (cells × \(k\)) holds the scores — each cell’s coordinates in PC space
- \(\sigma_i^2 / \sum \sigma_j^2\) is the fraction of variance explained by PC \(i\)
In scRNA-seq the first ~10 PCs usually separate major cell-type axes (T vs B vs myeloid; epithelial vs stromal vs immune); PCs 10–30 resolve subtypes (CD4 vs CD8, naive vs memory); beyond ~50, you’re almost entirely in noise.
Why PCA first? Three reasons work together. First, compression: 2,000 genes collapse to 30 PCs, making every downstream step faster. Second, denoising: the discarded high-PC directions are dominated by per-cell sampling noise rather than biological signal, so dropping them actually improves the quality of neighbor graphs and cluster solutions. Third, decorrelation: PCs are orthogonal by construction, so Euclidean distance in PC space behaves the way intuition expects — which it does not in raw HVG space, where correlated genes inflate distances along shared axes.
UMAP and clustering operate on PCs, not raw expression, exactly because of (3). Skip PCA and try to UMAP the scaled HVG matrix and you’ll get visibly worse embeddings, slower runtime, and unstable cluster solutions. In practice the SVD is computed by a randomized / truncated solver (irlba in Seurat, scanpy’s arpack/randomized backends), which returns only the top ~50 components and scales to atlas-sized matrices where a full eigendecomposition would be intractable1. The choice of PCA as the linear backbone of the single-cell workflow is shared across Seurat, Scanpy, and Bioconductor1–3.
Each PC is a linear combination of all HVGs — a weighted sum of gene expression values — chosen so that cell scores along that axis have maximum variance, subject to the constraint of being orthogonal to all previous PCs. In biological terms, PC1 captures the largest source of transcriptional variation in the dataset (often a cell-type axis, or a batch/condition axis on unintegrated data); PC2 captures the next largest source, uncorrelated with PC1; and so on. Because PCA finds axes of maximum variance in a linear sense, it is interpretable: the loadings tell you exactly which genes drive cells apart along each PC, making it the only major dimensionality reduction in the workflow where you can inspect a component and understand its biology. UMAP and t-SNE lack this interpretability entirely.
RunPCA(seu, npcs = 50). Default: 50. Sets the maximum number of principal components computed. More PCs cost more computation but ensure you don’t miss a low-variance biological axis. For ElbowPlot to be informative you need at least as many PCs as you expect to use (typically 30–50); requesting 50 upfront is a safe standard. Note that npcs is the maximum computed — you then choose a subset (e.g. dims = 1:20) for FindNeighbors and RunUMAP. Increasing npcs beyond 50 rarely helps for standard 10x runs of < 200k cells.
2.3 Choosing how many PCs to keep
The standard tool is the scree plot (sometimes called an elbow plot): for each PC, plot the variance it explains, in descending order. The shape is a steep initial drop, an elbow, and a long noise floor. Table 1 summarizes the main PC-selection strategies, their cost, and when to reach for each.
| Strategy | How it works | Typical result | Notes |
|---|---|---|---|
| Scree / elbow plot (visual) | Identify the “elbow” in variance-explained curve | PC 15–30 for 5–50k-cell runs | Fast; subjective but usually fine |
| JackStraw permutation test | Compare each PC’s variance to a null by random permutation | Statistically principled cutoff | Very slow for >50k cells |
| Fixed high bound (e.g. 50 PCs) | Accept more noise dimensions in exchange for not missing low-variance subtypes | Typical for atlas-scale (>100k cells) | Safest default when in doubt |
| Marker-based sanity check | Inspect gene loadings; confirm biology not QC drives the retained PCs | Flags mito/ribo-dominated PCs | Should always accompany any other method |
Rules of thumb:
- The elbow is usually between PC15 and PC30 for 10x runs of 5–50k cells
- Lean high rather than low: going from 20 to 30 PCs rarely hurts; going from 30 to 50 mostly costs runtime
- A more principled alternative is the JackStraw permutation test (
Seurat::JackStraw), which compares each PC’s variance to a null distribution; slow on large datasets - For atlas-scale (>100k cells), 50 PCs is typical
The number of PCs is one of two knobs (the other is clustering resolution) that jointly determine how many cell types you recover, and the choice is genuinely consequential — benchmarks of clustering pipelines show that the number of estimated cell types is sensitive to both4,5. Leaning slightly high on PCs is the safer error, since dropping a real low-variance subtype is harder to detect than carrying a few noise dimensions.
A PC dominated by mitochondrial or ribosomal genes is a red flag: it’s tracking cell quality, not biology. If PC1 looks like that, your QC was too lax — go back to filtering.
2.5 Graph-based community detection (Louvain & Leiden)
Both algorithms optimize modularity — a measure of how much denser within-community edges are than the random expectation. Louvain6 is Seurat’s default and is what the workshop tutorials run (FindClusters(seu, resolution = 0.5), i.e. algorithm = 1), because it has no extra dependencies. Leiden7 is the modern preferred refinement — it fixes a real defect in Louvain, which can produce internally disconnected communities, by guaranteeing well-connected communities — but it needs the leidenalg Python package via reticulate, so we present it as the one-line upgrade rather than the default.
Leiden iterates (Louvain follows the same first and third steps, without the refinement guarantee):
- Local moves: each cell joins the neighbor’s community if doing so increases modularity
- Refinement (Leiden only): split sub-communities that have low internal connectivity
- Aggregation: collapse each community into a node and run again
The final partition is a labeling of every cell into a numbered cluster. To switch from the Louvain default to Leiden, add algorithm = 4: FindClusters(seu, resolution = 0.5, algorithm = 4) (algorithm = 4 selects Leiden; 1 is Louvain) — once leidenalg is installed.
The resolution parameter controls granularity:
- Low resolution (0.1–0.3) → few, broad clusters (immune cell families)
- Medium (0.5–0.7) → typical PBMC-scale dataset, 8–15 clusters
- High (1.0+) → many small clusters; subdivides cell states
There is no single “right” resolution. The right one is the one that matches your biological question. Run several and inspect with clustree to see how clusters split as resolution increases. Resolutions where the clustree topology is stable (clusters don’t shuffle membership across nearby resolutions) are defensible. Modularity optimization has a known resolution limit — it can fail to detect communities smaller than a scale set by the total graph size — which is part of why a single global resolution rarely resolves both abundant and rare populations well, and why iterative subclustering of a coarse partition is often more effective than pushing one global resolution very high. Unsupervised clustering remains one of the harder, less-settled steps of the pipeline; systematic comparisons find that no method dominates across datasets and that the recovered cluster count is method- and parameter-dependent4,5,8.
FindClusters(seu, resolution = 0.8). Default: 0.8. Controls the granularity of community detection: higher values yield more, smaller clusters; lower values yield fewer, larger clusters. There is no universally correct resolution — the right value depends on your biological question. A practical workflow is to run several resolutions (e.g. c(0.1, 0.3, 0.5, 0.8, 1.0)) simultaneously, visualize the hierarchy with clustree, and choose the resolution where each cluster can be given a defensible biological identity. For the workshop ifnb PBMC dataset, resolution = 0.5 is the tutorial default; on unintegrated data this typically yields 15–20 clusters (more than the ~13 biologically distinct types, because condition-split duplicates appear). Post-integration, the same resolution usually collapses to ~13 clean clusters.
2.6 t-SNE and UMAP: visualization, not analysis
Once you have clusters defined on the SNN graph, you want a 2-D picture of them. Two non-linear methods dominate:
Table 2 summarizes both methods side by side before the subsections walk the details.
| Property | t-SNE | UMAP |
|---|---|---|
| Objective | Minimize KL divergence between Gaussian (high-D) and Student-\(t\) (low-D) neighborhood distributions | Minimize cross-entropy between fuzzy topological graph representations |
| Loss symmetry | Asymmetric — penalizes wrong-far neighbors heavily; barely penalizes wrong-close | Symmetric — penalizes both directions |
| Local neighborhood preservation | Excellent | Good |
| Global structure preservation | Poor (inter-cluster distances not meaningful) | Moderate (positions roughly interpretable) |
| Key tuning parameter | Perplexity (default 30): lower → more local; higher → smoother | n.neighbors (default 30) + min.dist (default 0.3) |
| Stability across parameter sweep | Sensitive to perplexity | More stable across n.neighbors |
| Workshop default? | No (available via RunTSNE) |
Yes (RunUMAP) |
2.6.1 t-SNE
Models pairwise distances in high-D as a probability distribution (Gaussian, with bandwidth set per-cell to match a target perplexity), and tries to find a 2-D arrangement whose distance distribution (Student’s \(t\) in low-D, hence “t” in t-SNE) has minimal Kullback–Leibler divergence to the original10. The heavy-tailed Student’s \(t\) in the low-D space is what solves the “crowding problem” of the original SNE — it gives moderately distant points room to spread out in 2-D.
The KL loss is asymmetric: it heavily penalizes putting true neighbors far apart but only weakly penalizes putting unrelated cells nearby. This is why t-SNE preserves local neighborhoods beautifully and why cluster-to-cluster distances on a t-SNE plot are not meaningful.
The perplexity parameter (default 30) is roughly “how many neighbors does each cell consider”. Low perplexity (5–15) emphasizes very local structure; high (50+) smooths over fine subtypes. Worth running at multiple perplexities and looking for stable structure across them; Kobak & Berens give practical, single-cell-specific guidance on perplexity, learning rate, and initialization11.
2.6.2 UMAP
Builds a fuzzy weighted graph of local neighborhoods (per-cell normalization makes points in dense and sparse regions both still see their own local neighbors) and finds a 2-D embedding by stochastic gradient descent on a cross-entropy objective.
The cross-entropy is symmetric in its penalty structure (both “wrong-far” and “wrong-close” errors are penalized), so UMAP preserves more global topology than t-SNE — not perfectly, but enough that cluster positions are roughly meaningful (still don’t measure pseudotime in UMAP coordinates)12,13. An important caveat from the methods literature: much of the global structure that t-SNE and UMAP appear to preserve actually comes from the initialization (PCA-based initialization preserves global layout; random initialization does not), rather than from the algorithms themselves — so the common claim that “UMAP preserves global structure better than t-SNE” is largely an artifact of their default initializations9.
UMAP is the workshop default. Two parameters:
n.neighbors(default 30): smaller = more local; larger = more globalmin.dist(default 0.3): smaller = tighter clusters with clear gaps; larger = spread-out, good for continua
UMAP is noticeably more stable across n.neighbors than t-SNE is across perplexity. Usually one well-chosen UMAP is enough.
UMAP optimizes a cross-entropy objective over a fuzzy weighted neighborhood graph: it penalizes both placing true neighbors too far apart (attraction) and placing unrelated cells too close together (repulsion). This symmetric penalty preserves more global topology than t-SNE, which only penalizes the first error. However, “more global” does not mean “faithful” — inter-cluster distances on a UMAP are not a reliable measure of biological dissimilarity, and apparent density differences are artifacts of the min.dist parameter, not cell-type frequencies. UMAP is correctly read as a visualization of the cluster solution (computed on the SNN graph in PC space), not as a geometric analysis in its own right. It is valid for: showing cluster assignments, overlaying gene expression as a continuous gradient (FeaturePlot), and spotting dramatic batch effects as parallel sample islands. It is not valid for: distance-based statistics, trajectory direction, or density quantification.
RunUMAP(seu, dims = 1:30, n.neighbors = 30, min.dist = 0.3). Defaults: n.neighbors = 30, min.dist = 0.3. n.neighbors sets how many nearest neighbors each cell considers when building UMAP’s local graph — smaller values (5–10) emphasize very local detail and produce more fragmented embeddings; larger values (50+) emphasize global topology and can blur subtypes together. min.dist sets the minimum separation between points in the 2-D embedding — smaller values (0.05–0.1) produce tight, compact clusters with clear gaps between them (best for discrete cell types); larger values (0.5–0.8) spread cells across continuous gradients (best for developmental trajectories or smooth state transitions). For the workshop ifnb PBMC dataset, the defaults produce a clean, interpretable embedding.
2.7 What UMAPs do not tell you
The most-violated rule in scRNA-seq figure interpretation:
A UMAP plot does not tell you:
- Inter-cluster distance. Two clusters drawn far apart are not necessarily more dissimilar than two drawn nearby.
- Cluster density. Apparent point density depends on
min.distand is not biological cell density. - Trajectory direction. Apparent gradients can be optimization artifacts; use trajectory tools (Slingshot, Monocle3, scVelo) for that question.
A UMAP is good for: showing the cluster solution from §2.5, mapping a gene’s expression onto cells (FeaturePlot), and spotting batch effects (color by sample after integration). A UMAP is not good for: differential testing, distance-based statistics, or claims about lineage relationships.
Build your intuition interactively. If UMAP (or t-SNE) feels like a black box — which features of a plot are safe to read, and how the knobs reshape it — work through Google PAIR’s interactive explainer Understanding UMAP, which lets you turn n.neighbors and min.dist and watch the embedding respond in real time. For a step-by-step written walkthrough, see DataCamp’s Understanding UMAP: a guide to dimensionality reduction. Both are the best cure for over-interpreting an embedding.
2.8 Reading the PC loadings output
When you print PCA results with print(seu[["pca"]], dims = 1:5, nfeatures = 5), you see the top 5 positive and negative gene loadings for the first five PCs. These are the genes pushing cells to the high or low end of each PC axis. Table 3 shows the three canonical patterns and their interpretation. What to look for:
- PC1 dominated by interferon-stimulated genes (
ISG15,IFI6,IFIT1,MX1, etc.) signals that a condition/batch effect is the dominant source of variance — the axis is separating STIM from CTRL, not cell types. This is the expected starting state for the unintegratedifnbdata. - Later PCs (2–5) showing cell-type markers (
LYZ,CD14for monocytes;MS4A1,CD79Afor B cells;GNLY,NKG7for NK) are reassuring — cell-type biology is still recoverable even when a condition effect dominates PC1. - A PC dominated by mitochondrial or ribosomal genes is a red flag for insufficient QC: those axes are tracking cell quality, not biology.
RunPCA.
| Pattern in top loadings | Interpretation | Action |
|---|---|---|
ISGs (ISG15, IFI6, MX1) dominate PC1 |
Stimulation / condition effect is the largest variance axis | Expected for multi-condition data; proceed to integration (Chapter 5) |
Cell-type markers (CD14, MS4A1, GNLY) in PCs 2–5 |
Cell-type biology is recoverable despite batch in PC1 | Keep these PCs; reassuring sign |
Mitochondrial genes (MT-ND1, MT-CO1) dominate any PC |
Cell-quality axis survived filtering | Re-examine QC thresholds; consider regressing percent.mt |
Ribosomal genes (RPL, RPS) dominate any PC |
Translational-activity axis; often confounded with cell state | Exclude ^RP[SL] from HVGs or regress percent.rb |
DimHeatmap(seu, dims = 1, cells = 500, balanced = TRUE) provides a complementary view: rows are genes (top positive and negative loadings), columns are the 500 cells with the most extreme PC1 scores. A clean two-block structure on PC1 is the visual signature of a strong batch or condition axis.
2.9 The cross-tabulation diagnostic
After clustering, table(seu$cluster_col, seu$sample) is one of the most informative two-line diagnostics in scRNA-seq. Computing the proportion form:
prop.table(table(cluster = seu$RNA_snn_res.0.5, sample = seu$stim), margin = 1)gives a clusters × samples table where each row sums to 1. On unintegrated data, many clusters will be >80–95% one sample — a direct measure of how completely the batch effect has split the data. After integration, the same table should show most clusters at ~50% per sample (for a balanced design). Clusters that remain heavily skewed after integration are either genuine single-condition biology (e.g. an IFN-activated state present only in STIM) or residual under-correction — biological knowledge distinguishes the two.
2.10 The single-sample workflow ends here; multi-sample needs Chapter 5
Everything above is the single-sample workflow: one biological condition, one batch, no need to align across samples. If you have only one sample, you read off cell types from the clusters (Chapter 3) and you’re done.
If you have two or more samples and the unintegrated UMAP shows them as separate islands — every cell type appearing twice or more, once per sample — you have a batch effect problem and need integration. That’s Chapter 5.
The diagnostic is direct: compute the unintegrated UMAP (this chapter), color it by sample. If samples interleave within clusters, you don’t need integration. If samples form parallel structures, you do.
2.11 Common errors
Table 4 collects the errors most frequently encountered during dimensionality reduction and clustering, together with their root cause and the fix.
| You see | What’s wrong | What to do |
|---|---|---|
RunPCA errors with “no variable features” |
You skipped FindVariableFeatures |
Run it before ScaleData |
| The scree plot has no clear elbow | Either your QC was insufficient or your data is genuinely high-rank | Inspect the top loadings; try 30 PCs |
| Cluster numbers reshuffle between R sessions | You didn’t set.seed() |
set.seed(2026) before FindClusters and RunUMAP; use random.seed = 2026 argument |
| UMAP shows a cluster that has no biology | Sometimes batch / depth artifact, sometimes a rare cell type — inspect markers (Chapter 3) | Don’t dismiss; investigate |
FindAllMarkers / FindMarkers is very slow |
Install presto; Seurat detects it automatically and uses a much faster Wilcoxon implementation (this speeds up marker tests, not FindClusters itself) |
install.packages("presto") |
2.12 Where this material is also discussed
- Lecture: Lecture 02 — Dim Reduction & Clustering
- Tutorial: Tutorial 02 — Dim Reduction & Clustering
- Linear-algebra background: Appendix B §§ 4–9
- Glossary entries: PCA, UMAP, t-SNE, Leiden, SNN
- Next chapter: Chapter 3 — Markers & Manual Annotation
- For multi-sample data: Chapter 5 — Multi-Sample Integration
2.13 Going further
For the embedding methods, go to the primary sources: the t-SNE10 and UMAP12 papers, UMAP’s single-cell debut13, and the two essential “how to use them responsibly” papers9,11. For clustering, the challenges review4 and two benchmarks — of clustering methods8 and of cell-number estimation5 — are the references to consult when choosing an algorithm and resolution. Hands-on intuition is best built with Google PAIR’s interactive Understanding UMAP and DataCamp’s written guide to dimensionality reduction. Two further tools worth knowing: densMAP, a UMAP variant that also preserves cluster densities (addressing limitation 2 in §2.7), and openTSNE, a faster, more controllable t-SNE implementation. The full curated list is on Key Papers, Reviews & Benchmarks.