跳到正文
百沐一下百沐一下
本节目录

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-countraw counts直接就是整数计数
STAR(--quantMode GeneCountsraw 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 过滤低表达基因CPMfilterByExpr 就是按 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 喂进去

这是新手最容易踩的坑,尤其当您拿到的是别人整理好的、已经归一化的矩阵时。几种典型翻车:

  1. 把 TPM/FPKM 矩阵当 countData 传给 DESeq2。 DESeq2 需要整数,遇到小数会直接报错(提示计数不是整数)。就算您取整绕过报错,结果也是错的——归一化被做了两遍。
  2. 把 TPM 传给 edgeR / limma-voom。 不会报错,但离散度和方差估计全偏,p 值不可信,比第 1 种更隐蔽、更危险。
  3. 拿 raw counts 直接画热图或比基因间高低。 长基因天然计数高,看到的「高表达」可能只是基因长,不是真的表达多。
  4. 用 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

相关