RNA-seq Normalization: CPM, TPM, FPKM

Raw counts are not comparable across genes or samples. Learn what CPM, FPKM, and TPM each correct for, why TPM beats FPKM, and the one rule that keeps DESeq2 happy.

🟢 Beginner ⏱️ ~20 min 🧠 Concepts + R

Before you start

Learning objectives

By the end of this lesson you will be able to: explain why raw counts cannot be compared directly, say what CPM, FPKM, and TPM each correct for, explain why TPM is preferred over FPKM, and state the rule that DESeq2 and edgeR must receive raw integer counts, not normalized values.

Why raw counts are not comparable

A raw count is simply the number of reads that mapped to a gene. Two things make raw counts unfair to compare. First, library size (sequencing depth): if sample A was sequenced twice as deeply as sample B, every gene in A will tend to show roughly twice the counts, even when nothing biological changed. Second, gene length: a long gene is a bigger target, so it collects more reads than a short gene at the same true expression level. Because of these two effects, you cannot read a count of 800 in one place and 400 in another and conclude that the first is twice as active.

Two confoundersDepth and length

Depth (library size) makes whole samples scale up or down together. Gene length makes long genes look more expressed than short ones within a sample. Each normalization method removes one or both of these.

1What each method corrects for

CPM (counts per million) divides each count by the library size and multiplies by a million. It corrects for depth only, so it makes samples comparable but still treats a long gene as if it were more expressed than a short one. RPKM and FPKM (the F is for fragments, used with paired-end reads) correct for depth and gene length. TPM (transcripts per million) also corrects for both, but in an order that gives it a useful property: within every sample, all TPM values add up to the same total (one million). That fixed total is what makes TPM easier to compare across samples than FPKM, whose per-sample totals can drift.

What gets corrected CPM depth: yes length: no use within a sample FPKM depth: yes length: yes totals can drift between samples TPM depth: yes length: yes every sample sums to 1,000,000 For differential expression, none of these: DESeq2 and edgeR take raw counts.
CPM fixes depth; FPKM and TPM fix depth and length; only TPM gives every sample the same fixed total, which is why it travels better across samples.
MethodCorrects depth?Corrects gene length?Best used for
CPMYesNoQuick sample-to-sample comparison and simple filtering of low-count genes
FPKM / RPKMYesYesOlder within-sample reports; totals can differ between samples, so use with care
TPMYesYesWithin-sample comparison and visualization; every sample sums to the same total
DESeq2 median-of-ratios (or edgeR TMM)YesNo (length cancels in the test)Differential expression; computed internally from raw counts, not something you precompute

2Why TPM beats FPKM across samples

FPKM divides by length first and by a depth factor second, and the result is that the per-sample total of all FPKM values is not fixed: it can come out different for each sample. When you then compare a gene's FPKM between two samples, you are comparing fractions of two different totals, which is subtly misleading. TPM is built so the total is always one million in every sample, so a TPM value is a true proportion of that sample's transcript pool. That shared denominator is the reason TPM is the preferred unit for comparing the same gene across samples and for heatmaps and other visuals.

3The rule that keeps DESeq2 happy

This is the part beginners get wrong most often. For differential expression, DESeq2 and edgeR do their own normalization internally, the median-of-ratios method in DESeq2 and TMM in edgeR, and they must be given raw integer counts. Never feed them CPM, FPKM, or TPM. Those values are already scaled and are no longer counts, and the statistical model that DESeq2 uses assumes count-like data with a specific mean to variance relationship. Use TPM (or CPM) for within-sample comparison, plotting, and reporting; hand DESeq2 the raw count matrix and let it normalize for you.

One-line summary

CPM corrects depth; FPKM and TPM correct depth and length; TPM is preferred across samples because every sample sums to the same total. For differential expression, give DESeq2 or edgeR raw counts and let them normalize internally.

A worked example

CPM is the simplest formula. For gene g in a sample, CPM = (count for g / total mapped reads in that sample) × 1,000,000. The example below shows that calculation in R, then shows the contrasting rule: DESeq2 receives the raw count matrix, never a normalized one.

# --- CPM by hand (within-sample / visualization use) ---
# counts: a gene-by-sample matrix of RAW integer counts
counts <- matrix(c(800, 400, 1500,
                   100,  50,  300),
                 nrow = 2, byrow = TRUE,
                 dimnames = list(c("geneA", "geneB"),
                                 c("s1", "s2", "s3")))

lib_size <- colSums(counts)          # total mapped reads per sample
cpm <- t(t(counts) / lib_size) * 1e6 # divide each column by its depth, scale to per-million
round(cpm, 1)

# edgeR has a built-in equivalent:
# library(edgeR); cpm(counts)

# --- The rule for differential expression ---
# DESeq2 takes the RAW count matrix and normalizes internally
# (median-of-ratios). Do NOT pass it CPM / FPKM / TPM.
library(DESeq2)
dds <- DESeqDataSetFromMatrix(countData = counts_raw,  # raw integers
                              colData   = sample_info,
                              design    = ~ cell + dex) # dex = treatment column
dds <- DESeq(dds)        # estimates size factors + dispersions internally
res <- results(dds)

Check your understanding

Why can you not compare raw counts directly between two genes in the same sample?
Right. Within a sample, a longer gene is a bigger target and accumulates more reads at the same true expression, so length must be corrected before comparing genes.
What does CPM correct for?
Correct. CPM scales by library size to make samples comparable, but it does not adjust for gene length.
Why is TPM preferred over FPKM for comparing a gene across samples?
Exactly. TPM is built so each sample sums to one million; FPKM totals can drift between samples, making cross-sample comparison less reliable.
You are about to run DESeq2 for differential expression. What should you give it?
Right. DESeq2 uses median-of-ratios on raw counts. Passing it TPM or FPKM breaks the count-based model and gives invalid results.
When is TPM the right choice?
Correct. Use TPM for comparing and plotting expression; for differential testing, use raw counts and let the tool normalize.
Next in Track 3

Differential expression with DESeq2 →