count / TPM / FPKM / CPM 该用哪个
跟完这篇,您能分清 RNA-seq 表达矩阵里这四种单位各自归一化了什么,认出手上矩阵到底是哪种,知道哪一步该喂原始 counts、哪一步该用 TPM/CPM,并且不再犯「把 TPM 当 counts 塞进 DESeq2」这个最常见的错。
先记住一句话
这四个单位的差别,只在于「归一化了什么」——是否校正了测序深度、是否校正了基因长度。记住下面这张表,后面所有判断都从它出发。
| 单位 | 校测序深度 | 校基因长度 | 列和是否相等 | 典型用途 |
|---|---|---|---|---|
| raw counts(原始计数) | 否 | 否 | 否 | 差异分析的输入 |
| CPM | 是 | 否 | 否 | 过滤低表达、比同一基因跨样本 |
| FPKM / RPKM | 是 | 是 | 否 | 老流程遗留,能不用就不用 |
| TPM | 是 | 是 | 是(都=1e6) | 样本内跨基因比较、画表达量 |
一条准则先撂在这儿:差异分析用 raw counts,展示和样本内比较用 TPM,其余场景按需选 CPM。
四种单位在算什么
raw counts(原始计数)
比对后落在某个基因上的读段(reads/fragments)个数,是个整数,什么都没归一化。它同时受两件事影响:测序越深、计数越大;基因越长、能落的读段越多。所以两个样本之间、两个基因之间的 raw counts 不能直接比大小。
CPM(counts per million)
只把测序深度这一件事校掉:把每个基因的计数除以该样本的总计数,再乘一百万。counts[, j] 表示第 j 个样本这一列:
# counts:基因 × 样本 的原始计数矩阵
cpm[, j] <- counts[, j] / sum(counts[, j]) * 1e6
CPM 没有校正基因长度,所以能比「同一个基因在不同样本间」,但不能比「同一样本里长基因和短基因谁表达高」。
FPKM / RPKM
在 CPM 的基础上再除以基因长度(以 kb 计):既校深度,又校长度。RPKM 用于单端测序,FPKM 用于双端,算法一致,只是「read」还是「fragment」的叫法不同。它的短板是——每个样本所有基因的 FPKM 之和并不相等,导致跨样本比较时分母口径不一致,如今多被 TPM 取代。
TPM(transcripts per million)
同样校深度、校长度,但归一化顺序反过来:先按基因长度折算成「表达率」,再让每个样本的总和缩放到一百万。
# gene_len:每个基因的长度(kb),与 counts 的行一一对应
rate <- counts[, j] / gene_len # 先除以基因长度,得到「表达率」
tpm[, j] <- rate / sum(rate) * 1e6 # 再缩放,让这一列合计为 1e6
关键好处:每个样本的 TPM 列和都恰好是 1e6,相当于统一成了「每一百万转录本里占多少」,所以样本内跨基因、样本间同一基因都好比较。这也是当前展示表达量时的首选单位。
从上游工具拿到的是哪种单位
您很少自己从零算这些单位,通常是从定量工具的输出里直接拿。先看清楚拿到的是什么,能省掉一大半麻烦:
| 上游工具 | 默认输出的单位 | 备注 |
|---|---|---|
| featureCounts、HTSeq-count | raw counts | 直接就是整数计数 |
STAR(--quantMode GeneCounts) | raw counts | 输出每基因计数 |
| Salmon、kallisto | 估计 counts + TPM | 转录本级,需汇总到基因级 |
| RSEM | 估计 counts + TPM + FPKM | 三种都给,按需取 |
对 Salmon/kallisto/RSEM 这类「伪比对」工具,规范做法是用 tximport 把转录本级结果汇总到基因级,再把里面的估计 counts 交给 DESeq2,而不是自己拿 TPM 去做差异分析:
library(tximport)
library(DESeq2)
txi <- tximport(files, type = "salmon", tx2gene = tx2gene) # 汇总到基因级
dds <- DESeqDataSetFromTximport(txi, colData = coldata, design = ~ condition)
tximport 会同时带回 counts 和 TPM(在 txi$abundance 里),展示时想用 TPM 直接从这儿取即可。
差异分析必须用原始 counts
做差异表达(DE)时,DESeq2、edgeR 这类工具要的是 raw counts,不是 TPM/FPKM/CPM。原因是它们把计数当作服从负二项分布的整数来建模,并且内部自带归一化:
- DESeq2 用 median-of-ratios(几何均值中位数比)估计每个样本的 size factor;
- edgeR 用 TMM(trimmed mean of M-values)。
您如果先手动算成 TPM/FPKM 再喂进去,等于把长度和深度的信息提前抹掉了,工具再做一遍归一化只会得到错误的离散度估计和 p 值。正确姿势是直接把整数矩阵交给它:
library(DESeq2)
dds <- DESeqDataSetFromMatrix(
countData = counts, # 原始整数 counts,不是 TPM/FPKM
colData = coldata, # 样本分组信息
design = ~ condition
)
dds <- DESeq(dds) # 内部自动估 size factor 并归一化
res <- results(dds)
例外只在「工具明说要归一化值」时才成立,比如 limma-voom 走的是 log-CPM、单细胞里有各自的归一化流程——但普通 bulk RNA-seq 的 DESeq2/edgeR,认准 raw counts。
什么时候用 TPM / CPM
归一化后的单位不是拿来做检验的,而是拿来看和展示的。按下面这张表对号入座:
| 您要做的事 | 建议单位 | 说明 |
|---|---|---|
| 跑 DESeq2 / edgeR 差异分析 | raw counts | 工具自带归一化 |
| 比同一基因在不同样本间高低 | CPM 或 TPM | 二者都校了深度 |
| 比同一样本里不同基因谁表达高 | TPM | 必须校长度,CPM 不行 |
| 画热图 / boxplot / 单基因表达量图 | TPM(常取 log2(TPM+1)) | 展示用,标度稳定 |
| edgeR 过滤低表达基因 | CPM | filterByExpr 就是按 CPM 阈值筛 |
| WGCNA、聚类等下游 | 多用 log 化的 TPM 或 VST | 看具体方法要求 |
CPM 在 edgeR 里可以直接算:
library(edgeR)
cpm_mat <- cpm(counts) # 每百万计数
logcpm <- cpm(counts, log = TRUE, prior.count = 1) # 画图常用的 log 值
画图时常对 TPM 取 log2(TPM + 1),是为了压住高表达基因把尺度拉爆、顺便处理 0 值,属于常用惯例、非铁律,加多少伪计数可按数据调。
最常见的错:把 TPM 当 counts 喂进去
这是新手最容易踩的坑,尤其当您拿到的是别人整理好的、已经归一化的矩阵时。几种典型翻车:
- 把 TPM/FPKM 矩阵当 countData 传给 DESeq2。 DESeq2 需要整数,遇到小数会直接报错(提示计数不是整数)。就算您取整绕过报错,结果也是错的——归一化被做了两遍。
- 把 TPM 传给 edgeR / limma-voom。 不会报错,但离散度和方差估计全偏,p 值不可信,比第 1 种更隐蔽、更危险。
- 拿 raw counts 直接画热图或比基因间高低。 长基因天然计数高,看到的「高表达」可能只是基因长,不是真的表达多。
- 用 FPKM 做跨样本定量比较。 因为列和不等,分母口径不一致,换成 TPM 更稳妥。
怎么自查手上的矩阵是什么单位? 记住两个特征,一句代码就能看出来:
- 是不是整数——全是非负整数,基本就是 raw counts;带小数,就是归一化后的值。
- 每列加起来等于多少——列和都约等于 1e6(一百万),那是 TPM;列和各不相同、且是小数,多半是 FPKM 或 CPM。
all(mat == round(mat)) # TRUE → 很可能是 raw counts
head(colSums(mat)) # 列和都≈1e6 → TPM;各不相同 → 深度未统一
拿不准来源时,最稳的做法是回到上游拿一份 raw counts(featureCounts、HTSeq,或 Salmon/kallisto 经 tximport 汇总的估计 counts 都行),差异分析从原始计数起步,展示再自己算 TPM。
在平台上跑一遍
在百沐一下,您不用手动纠结单位换算:把原始 counts 矩阵和分组表上传,用一句话说清「做两组差异表达分析」,组学智能体会自动认出这是原始计数、走 DESeq2 的标准归一化,展示环节该用 TPM 的地方也替您换好。不确定手上是不是 raw counts,也可以直接把矩阵传上去让它帮您判断。
从这里开始:组学智能体使用指南,或直接进入 app.baimuyixia.com。