Chatomics Field GuideWhat They Don't Teach You

Glossary · Statistics, Artifacts and Pitfalls

Singular value decomposition (SVD)

The matrix factorization that actually runs when you call prcomp() or RunPCA(), and the reason your PCA either scales to a million cells or falls over.

By Ming "Tommy" Tang, Director of Bioinformatics in Big Pharma · Reviewed September 2026 · 3 min read

Also: SVD

Definition

SVD factorizes any matrix X into three matrices, X = U Σ V^T. U holds the left singular vectors (patterns across rows), Σ is a diagonal matrix of singular values ranked from largest to smallest, and V^T holds the right singular vectors (patterns across columns). Unlike eigendecomposition, SVD works on any rectangular matrix, not just square symmetric ones, which is why it, rather than covariance-matrix eigendecomposition, is the operation R's prcomp() and most PCA solvers actually run. When X is your centered data matrix, the columns of V are the principal component loadings and the singular values in Σ set how much variance each component explains.

You meet SVD the moment you call prcomp() in R, RunPCA() in Seurat, or scanpy.pp.pca() in Python. Every one of those functions is running singular value decomposition under the hood, not the covariance-matrix eigendecomposition most intro courses teach first. That choice matters in practice: which SVD solver you pick, and whether your matrix is sparse, decides whether the PCA finishes in seconds or exhausts your memory on a large single-cell dataset.

It also decides how you troubleshoot a PCA that looks wrong. The bug is rarely biological. More often it's which matrix you handed to SVD (genes as rows or cells as rows), whether you centered or scaled first, or whether raw-count PCA was ever the right tool for that data type at all. scATAC-seq is the clearest case where the answer is no.

Why it matters

scRNA-seq gene-by-cell matrices are routinely more than 90% zeros. Forming the covariance matrix explicitly, X^T X, squares the condition number of X, which degrades numerical stability on data that is already noisy and sparse. Running SVD directly on X, with irlba in R or scanpy's arpack/covariance_eigh solvers, skips that squaring entirely. Pick the wrong tool, a dense SVD on a matrix with hundreds of thousands of cells, and you wait hours or run out of memory for no statistical benefit; pick the truncated sparse solver and 50 components come out in the time it takes to load the data.

The second case is scATAC-seq. Peak-by-cell matrices are sparse and effectively binary, which violates the continuous, roughly normal data PCA implicitly assumes. That is why Signac's workflow runs RunTFIDF() before RunSVD(): TF-IDF reweights the matrix first so the SVD (called LSI in this context) finds biologically meaningful accessibility patterns instead of noise driven mostly by sequencing depth per cell.

Where people get it wrong

The common mistake is learning "PCA equals eigendecomposition of the covariance matrix" and then treating SVD as a separate, unrelated trick, when SVD is the actual computation prcomp() and RunPCA() perform. The identity X^T X = V Σ^2 V^T shows they land on the same answer: the right singular vectors V are the eigenvectors of the covariance matrix, and the singular values squared are the eigenvalues. The tell that you got the input orientation wrong is a nonsensical PCA plot: SVD expects observations in rows and features in columns, but genomics data usually arrives as genes (features) by samples (observations), so you must transpose first, prcomp(t(X)), not prcomp(X), or your "principal components" describe patterns across genes instead of across samples.

A second, quieter confusion is mixing up U and V. V's k-th column tells you which genes or features drive a given component and is what you inspect for loadings; U scaled by Σ gives the per-sample coordinates you actually plot on a PCA scatterplot.

A concrete example

In R, svd() and prcomp() give the same principal component scores once you transpose correctly, which is the easiest way to convince yourself SVD really is driving PCA under the hood rather than a separate covariance-eigendecomposition step.

r
# genes x samples -> transpose to samples x genes before SVD/PCA
X <- t(ncidat)

# direct SVD
sv <- svd(X)
pc_scores_svd <- X %*% sv$v   # equivalently sv$u %*% diag(sv$d)

# PCA via prcomp (internally calls SVD)
pca <- prcomp(X, center = TRUE, scale. = FALSE)

# variance explained by each PC, from the singular values
var_explained <- sv$d^2 / sum(sv$d^2)

Related terms

Questions people ask

What's the difference between SVD and PCA?

SVD is a general matrix factorization, X = U Σ V^T, that works on any matrix. PCA is what you get when you apply SVD to a centered, and often scaled, data matrix: the right singular vectors (V) become the component loadings and the singular values set the variance each component explains. Every PCA can be computed via SVD, but not every SVD is a PCA.

Is SVD the same as eigendecomposition?

No, but they are tightly related. Eigendecomposition only applies to square matrices, so classic PCA runs it on the covariance matrix X^T X. SVD decomposes X directly without forming that covariance matrix, and the identity X^T X = V Σ^2 V^T shows the two approaches converge on the same right singular vectors and eigenvalues.

Why is SVD more numerically stable than covariance-based PCA?

Forming X^T X squares the condition number of X, which amplifies floating-point error, especially on noisy, sparse genomics matrices. SVD decomposes X directly and never forms that covariance matrix, so it avoids that squaring and stays stable on larger, messier data.

Why does scATAC-seq use LSI instead of plain PCA?

scATAC-seq peak-by-cell matrices are sparse and near-binary, which violates the continuous, roughly normal data PCA implicitly assumes. LSI runs TF-IDF normalization first, then SVD on the transformed matrix (RunTFIDF() then RunSVD() in Signac), which captures accessibility patterns instead of noise from sequencing depth.

Do I need to transpose my matrix before running SVD for PCA?

Yes, if your data comes as genes or features in rows and samples or cells in columns, which is the standard genomics layout. SVD-based PCA functions expect observations in rows and features in columns, so run prcomp(t(X)) or svd(t(X)), not the raw genes-by-samples matrix.

Related pages

Related reading on the blog

Sources

  1. irlba: Fast Truncated Singular Value Decomposition and Principal Components Analysis — Truncated SVD approach and parameters for large sparse matrices
  2. Scanpy pp.pca documentation — SVD solver options (arpack, covariance_eigh, randomized, tsqr) and defaults
  3. Seurat pbmc3k tutorial — RunPCA() on variable features, downstream KNN construction in PCA space