# 05. Turning Raw Counts into Comparable Values

> Use interactive widgets to see how gene length and sequencing depth affect raw counts, then follow how TPM turns them into comparable proportions.

Counting the aligned reads for each gene produces a **raw count**. But this number mixes true expression with the effects of gene length and sequencing depth. Use the widgets to separate those effects and follow the process of converting the count into a value suited to the comparison you want to make.

> **Question for this lesson**: How do you turn raw counts that differ despite equal expression into comparable values?

If alignment attaches a `gene_id` to each event, quantification aggregates those events with `GROUP BY gene_id`. A raw count is an exact aggregate, but like request counts that depend on both runtime and server count, it also depends on conditions other than expression.

```python
raw_count = alignments.groupby("gene_id").size()
rpk = raw_count / gene_length_kb
tpm = rpk / rpk.sum() * 1_000_000
```

In this pseudocode, `raw_count` is the observation, `gene_length_kb` is fixed metadata for each gene, and `rpk.sum()` is a sample-specific scale. Which denominator is applied, and when, determines what a metric means.

> **In one line**: Raw counts are confounded by both **gene length** and **sequencing depth**, so they cannot be compared directly. TPM corrects *length → depth* in that order and fixes **the sum of expression in every sample at exactly 1,000,000**. A TPM value therefore provides the **same scale**: "how many of every million transcripts come from this gene."

## 1. Raw counts are affected by two factors

Counting reads aligned to each gene in a BAM file produces one integer, the raw count. The problem is that this number does not directly state **how strongly the gene was expressed**.

As a data transformation, the aggregation looks like this.

| Alignment record | Grouping key | Aggregate |
| --- | --- | --- |
| read 1 → `GENE_A` | `GENE_A` | |
| read 2 → `GENE_A` | `GENE_A` | `GENE_A: 2` |
| read 3 → `GENE_B` | `GENE_B` | `GENE_B: 1` |

Direct comparison would require every gene to have the same length and every sample to be sequenced to the same depth. Real data satisfies neither condition.

### Trap 1: gene length

Because mRNA is fragmented before sequencing, **a longer gene produces more fragments even when the number of mRNA molecules is the same**. Read count is therefore proportional not to expression alone but to **expression × length**. In the animation below, follow the stages **gene → transcription (mRNA) → fragmentation → alignment**. Both genes have the same expression, three mRNA molecules, yet long gene A produces 18 reads and short gene B produces 6. Dividing by length in the final stage makes them equal.



> This "÷ gene length" operation is the first normalization step, and its result is **RPK (reads per kilobase)**. Once length is removed, genes can finally be compared **within one sample**.

### Trap 2: sequencing depth

Sequencing the same sample more deeply increases every gene's count. Expression did not change; you simply **read more of it**. To compare **different samples**, divide each gene's count by the sample's **total read count, or depth**, to align their scales. Below, follow the same tissue sequenced at shallow and three-fold deeper coverage. Every raw count triples, but dividing by total reads makes the two samples equal again.



> Normalization must therefore solve two problems: **length**, which blocks comparisons among genes, and **depth**, which blocks comparisons among samples.

## 2. Correcting one step at a time: RPK → RPKM/FPKM → TPM

The metric depends on the **order in which the two corrections are applied**.

| Metric | Correction | Calculation |
| --- | --- | --- |
| **CPM** (counts per million) | Depth only | count ÷ (total reads/10⁶) |
| **RPK** (reads per kilobase) | Length only | count ÷ length (kb) |
| **RPKM / FPKM** | **Depth → length** | count ÷ (total reads/10⁶) ÷ length (kb) |
| **TPM** | **Length → depth** | Calculate count ÷ length, then divide again by **that sum** ×10⁶ |

- **RPKM** and **FPKM** are effectively the same value. Counting reads gives RPKM; counting **fragments**, where one paired-end read pair is one fragment, gives FPKM.
- **RPKM/FPKM divides by depth first, then length.** Its **sum therefore differs from sample to sample**.
- **TPM reverses the order.** First divide by length to obtain RPK, a per-gene "concentration." Then divide by **the sum of those RPK values** and multiply by one million. This last step is decisive: **dividing by the sum makes the result always total 10⁶**.

:::note[🧩 Why does reversing the order change the result's properties?]
RPKM corrects a gene's share of all reads by length. If its **denominator, total reads, is not fixed across samples**, the sums do not align. TPM instead divides again by **the sample's own total RPK**, fixing **Σ TPM = 1,000,000** by definition. The two metrics use the same corrections, but TPM forces the result into a **proportion, units of one millionth of the total**.
:::

Exact definitions, symbols, and formulas for each abbreviation are separated into the [RNA-seq Expression Normalization Metrics reference](/en/reference/rna-seq-normalization/). This lesson stays focused on the two problems TPM solves and the properties of its result.

## 3. The two-step TPM calculation

TPM requires exactly two steps.

> **Step 1.** Count per gene ÷ length (kb) = **RPK**: remove the bias that gives long genes more reads
>
> **Step 2.** Each RPK ÷ total RPK in that sample × 10⁶ = **TPM**: correct depth and fix the sum at one million

Switch among **Raw, RPKM, and TPM** in the calculator below. In particular, watch how the **column sum Σ** at the bottom changes. Sample A is the baseline, with all four genes expressed equally. Scenario buttons and sliders modify Sample B.



## 4. Why TPM works well for comparisons among samples

Set the calculator to **TPM** and try the Sample B scenarios. One property becomes clear:

> **Σ TPM = 1,000,000 in every scenario.** The total stays one million when depth triples and even when one gene surges.

This is the key. Because the sum is fixed, **one TPM value consistently means that gene's share of one million total transcripts**. It is not an absolute scale, but it is consistent, which is why you can place TPM values from two samples side by side.

By contrast, **Σ RPKM fluctuates** whenever the scenario changes. When totals differ among samples, the same RPKM value represents a different proportion in each sample, subtly misaligning a side-by-side comparison. [This is why TPM is now preferred over FPKM for expression comparisons](https://doi.org/10.1007/s12064-012-0162-3).

:::tip[In one sentence]
**RPKM divides by depth first, so its sums do not align; TPM divides by its own sum last, so it always totals one million.** This is why **TPM** is used when placing one sample alongside public data such as GTEx and PCAWG, as in [Sid's case](/en/lessons/rna-seq-analysis/).
:::

## 5. Caution: TPM is not universal

TPM does not solve every comparison. The widget already hints at its limitations.

- **TPM is a proportion, not an absolute amount.** Because the total is fixed at one million, if one gene surges, the calculator's "G4 surge," **TPM for other genes falls even when their true expression has not changed**. They divide the same pie, an effect called compositionality.
- **Rigorous differential expression, or DEG, analysis uses raw counts.** Tools such as DESeq2 require raw counts because they model count variability and sequencing depth directly.
- **The uses therefore differ**: use **TPM** to compare one sample with a reference distribution, detect outliers, or visually compare expression among genes and samples. Use **raw counts + a dedicated tool** to test statistical differential expression across several samples.

[Differential expression testing across several samples](/en/lessons/bulk-rna-deg/) explains how repeated-sample variation is handled. [Statistical testing and multiple testing](/en/reference/statistical-testing/) covers the negative-binomial distribution and multiple testing.

Running [RSEM to produce TPM](/en/practice/rna-seq-from-fastq/#3-4-calculate-tpm-with-rsem) and [featureCounts to produce raw counts](/en/practice/rna-seq-from-fastq/#3-5-create-raw-counts-with-featurecounts) from the same alignment makes the different uses of the two metrics visible at the file level.

## Summary: metrics at a glance

| Metric | Length correction | Depth correction | Compare genes within a sample | Compare across samples | Main use |
| --- | :---: | :---: | :---: | :---: | --- |
| **raw count** | ✗ | ✗ | ✗ | ✗ | Input to DEG tools such as DESeq2 and edgeR |
| **CPM** | ✗ | ✓ | ✗ | △ | Approximate depth correction |
| **RPK** | ✓ | ✗ | ✓ | ✗ | Intermediate step |
| **RPKM/FPKM** | ✓ | ✓ | ✓ | △, sums differ | Former standard, now discouraged |
| **TPM** | ✓ | ✓ | ✓ | ✓, Σ=10⁶ | Expression comparison and reference matching |

Next, [Workflows built by connected genes](/en/lessons/pathway/) explains how multiple genes connect to produce one cellular response. [Finding outlier genes in one sample](/en/lessons/rna-seq-analysis/) shows how to use the resulting TPM matrix in practice.

---

### Sources

- Wagner et al., *"Measurement of mRNA abundance using RNA-seq data: RPKM measure is inconsistent among samples"*, Theory in Biosciences 2012: [DOI](https://doi.org/10.1007/s12064-012-0162-3)
- Lior Pachter, *"Models for transcript quantification from RNA-Seq"*, TPM definition: [arXiv:1104.3889](https://arxiv.org/abs/1104.3889)
- [RSEM](https://github.com/deweylab/RSEM), [featureCounts (Subread)](https://subread.sourceforge.net/), [DESeq2](https://bioconductor.org/packages/DESeq2/)