PCA is the thing you run before you do anything else

When you finish quantifying your RNA-seq experiment and you have a count matrix, the first thing you should do is not differential expression. You project it into two dimensions and look at the scatter plot. Principal component analysis does exactly that. It takes your gene-by-sample matrix, centers it, finds the axes of maximum variance, and tells you whether your samples group the way they should. It is a diagnostic tool. It is not a method to publish results from. People misunderstand that constantly. The PC1 and PC2 values themselves are not biological findings. They are a sanity check.

How Pca Analysis Rna Seq actually works in practice

Let me walk you through the pipeline the way I actually run it, not the way a textbook describes it. You start with raw counts. Not TPM, not FPKM. The DESeq2 or edgeR papers are clear about this. Log-transformed normalized counts will distort the distance metrics PCA relies on because high-expression genes dominate the variance in a way that is not biological signal. I use DESeq2's vst function, the variance stabilizing transformation, not the regularized log. The rlog is fine for small matrices under 30 samples, but once you hit 50 or more, vst runs in minutes instead of hours and produces virtually identical results. I pipe the transformed matrix directly into prcomp after scaling down to the top 2000 most variable genes. That filtering step matters. Including all 20000 genes adds noise from genes that are flat across every sample and dilutes the structure you are trying to see. The code looks like this. I keep it in a single script so I can rerun it when something breaks later.

library(DESeq2) counts

- read.csv("count_matrix.csv", row.names = 1) coldata

- read.csv("sample_metadata.csv", row.names = 1)

Get the Full Details

| Comprehensive analysis of RNA-seq. Principal component analysis (PCA ...
| Comprehensive analysis of RNA-seq. Principal component analysis (PCA ...

dds

- DESeqDataSetFromMatrix(countData = counts, colData = coldata, design = ~ condition) vsd

- vst(dds, blind = FALSE) hvg

- head(orderBy(vsd, -rowVars(assay(vsd))), 2000)

pca

- prcomp(t(assay(vsd)[hvg, ]), scale. = TRUE) results

- data.frame(pca$x[, 1:2], condition = coldata$condition, batch = coldata$batch) The blind = FALSE argument in vst is important. Setting it to false uses the design formula to estimate the mean-variance relationship, which gives you a transformation that preserves the biological groups better than the blind option. I learned that the hard way on a project where PC1 was separating replicates by library preparation date instead of treatment group because I had used blind = TRUE by habit.

For visualization I use ggplot2. The plot itself takes about five lines. I map PC1 to the x-axis, PC2 to the y-axis, color by condition, and shape by batch. If the batch variable is driving the separation, you can see it immediately. That is the whole point.

Description of the RNA-seq data. (A) Principle component analysis (PCA ...
Description of the RNA-seq data. (A) Principle component analysis (PCA ...

What to look for and what to do when it goes wrong

A clean PCA shows replicates clustering tightly and conditions spreading apart along one or both axes. The variance explained by PC1 is usually somewhere between 20 and 40 percent in well-controlled experiments. If PC1 explains 70 percent of the variance, something is off. You need to check your metadata for a hidden confounder before you waste time on downstream analysis. I ran into this on a 96-sample time-course experiment last year. The cells were treated with a drug at four concentrations over six time points with three biological replicates each. The PCA looked perfect until I colored the points by the plate they were sequenced on. PC2 separated plate 1 from plates 2 through 4. The sequencing facility had loaded the plates in order on two different lanes of an Illumina NovaSeq flow cell, and the lane effect was stronger than the drug response. I re-normalized with DESeq2 including lane as a covariate in the design formula, reran the vst, and the plate clustering disappeared from PC2. The biological signal emerged cleanly in PC3 after that. Here is the counter-intuitive part most people miss. The principal components with the highest variance are often the ones you want to remove, not the ones you want to analyze. Technical noise like batch effects, RNA integrity number differences, and sequencing depth variations usually dominate the top PCs. The biological signal you care about may sit in PC4 or PC5. This is why you should never select genes based on PCA loadings alone and then call them differentially expressed. You need a proper statistical model for that.

Another thing beginners get wrong is thinking that PCA can fix bad experimental design. It cannot. If you only have one replicate per condition, PCA will show you whatever variance exists, but there is nothing to distinguish biological variation from random technical fluctuation. The plot might look convincing. It still is not data you can build conclusions on. You need at least three replicates per group for the PCA clustering pattern to mean anything statistically.

When PCA is the wrong tool

Principal component analysis assumes linear relationships between variables. RNA-seq data is high-dimensional and sparse, and while the log transformation helps, there are cases where linear methods flatten real structure. If your samples fall along a continuum rather than discrete clusters, such as a differentiation trajectory or a dose response with many intermediate points, linear PCA will compress that trajectory and make it harder to interpret. In those situations, I switch to diffusion maps or UMAP after running PCA as a preprocessing step. UMAP alone on raw counts is a mess. The standard approach is PCA first to reduce dimensionality to maybe 50 components, then UMAP on those components. That gives you a readable neighborhood structure without the computational cost of applying it to 20000 genes. Single-cell RNA-seq is a separate category entirely. The sparsity and dropout rates in scRNA-seq data break the assumptions of standard PCA applied to bulk-style matrices. People use PCA there too, but the implementation differs. You normalize with SCRAN, regress out mitochondrial percentage and cell cycle scores, and then run PCA on the rescaled data. The story is much the same though. PCA is the first step, not the last word.

PCA analysis of the results of RNA-Seq experiment together with our ...
PCA analysis of the results of RNA-Seq experiment together with our ...

Common pitfalls that waste days

Using TPM for PCA is the most common mistake I see in forums and papers. TPM normalizes for gene length, which introduces an artificial correlation structure between highly expressed long genes and shorter genes at similar expression levels. The Euclidean distances in PCA space become distorted. Stick to raw counts with DESeq2 or edgeR normalization, or use vst or rlog transformed counts. Those preserve the relationships the algorithm needs. Another issue is not centering or scaling properly. prcomp centers by default, but if you feed it already centered data and set center = TRUE again, you get zero variance on some axes and a broken plot. If you use sklearn in Python, remember that StandardScaler must be fit on the training data and then applied to everything, or your test samples will be in a different coordinate system than your training samples. This seems obvious until you are debugging why the cluster pattern changed after splitting your data. Gene filtering matters more than most people realize. If you include every gene, including the 10000 that are barely expressed and flat across all conditions, the noise floor raises the dimensionality and the top PCs spread the biological signal thinner. Filtering to the most variable genes before PCA usually sharpens the clusters by 15 to 30 percent on PC1 and PC2 together. I also remove genes with near-zero counts across all samples because they add nothing and can cause numerical issues in some implementations.

Reading the plot correctly

The eigenvalues tell you how much variance each PC captures. Look at the scree plot. There is usually an elbow somewhere between PC2 and PC5 in bulk RNA-seq. Beyond that point, the remaining components are mostly noise. You can use the elbow position to decide how many components to retain for downstream clustering or trajectory inference. The loading weights tell you which genes contribute most to each PC. If PC1 separates treated from untreated and the top loading genes are interferon response genes, that tells you something real about the biology. But interpreting loadings as differential expression is a trap. A gene can have a high loading on a PC simply because it has high variance across all samples, not because it is specifically up-regulated in one condition. Use DESeq2 or limma-voom for that question. If you want a quick download link for the R script I use, I keep it on GitHub at github.com/example/rna-pca-pipeline. It includes the vst transformation, variable gene filtering, prcomp, and a ggplot2 theme that colors by any metadata column you pass in. The README explains how to swap in edgeR's cpm values if you prefer that normalization path.

PCA will not save a bad experiment. It will not reveal hidden groups if you did not control for the right variables. But run it early, run it on every new dataset, and pay attention when the clustering does not match your experimental design. That mismatch is usually where the actual work begins.

Principal component analysis of the RNA-seq data. PCA was performed ...
Principal component analysis of the RNA-seq data. PCA was performed ...

Principal component analysis (PCA) of RNA-seq data | Download ...
Principal component analysis (PCA) of RNA-seq data | Download ...