表达量化方法:从FASTQ到基因表达矩阵
基因表达矩阵是后续一切分析的起点。说白了,就是把每个基因在每个样本里有多少条 reads 给统计出来。我入行那会儿,有人拿 Excel 手算——嗯,那都是历史了。现在主流就两条路:一条是先比对再计数(STAR + featureCounts),另一条是伪比对直接定量(Salmon)。两条路我都跑过,今天把关键细节拆开讲。
方法一:STAR + featureCounts
这是最经典的“先比对后计数”路线。STAR 负责把 reads 比对到参考基因组,速度极快但吃内存。featureCounts 则从 BAM 里把每个基因上的 read 覆盖数捞出来。
我个人习惯这么跑 STAR:
# 构建索引(只需要做一次)
STAR --runMode genomeGenerate \
--genomeFastaFiles Homo_sapiens.GRCh38.fa \
--sjdbGTFfile Homo_sapiens.GRCh38.88.gtf \
--runThreadN 16 \
--genomeDir star_index
# 比对
STAR --genomeDir star_index \
--readFilesIn sample_R1.fastq.gz sample_R2.fastq.gz \
--readFilesCommand zcat \
--outFileNamePrefix sample_ \
--outSAMtype BAM SortedByCoordinate \
--outFilterMultimapNmax 10 \
--runThreadN 16
--outFilterMultimapNmax 默认是 10。如果你的数据来自多拷贝基因家族,建议调低到 2-3,不然很多多比对 reads 会被错误分配。我曾经在一个重复序列高表达的样本上栽过跟头,调参前后差异能到 30%。
得到 BAM 后,用 featureCounts 计数:
featureCounts -T 16 -t exon -g gene_id \
-a Homo_sapiens.GRCh38.88.gtf \
-o counts.txt \
sample_Aligned.sortedByCoord.out.bam
-t exon 指按 exon 为计数单元,-g gene_id 则按基因 ID 汇总。注意 GTF 版本必须和索引一致,否则计数会错乱。
方法二:Salmon + tximport
Salmon 走的是伪比对路线——不把 reads 完整拼到基因组上,而是快速判断每条 read 可能来自哪个转录本。好处是内存占用小得多,而且直接处理 fastq,省掉生成 BAM 的磁盘开销。
跑 Salmon 前需要先准备转录本索引:
salmon index -t transcripts.fasta -i salmon_index --gencode -k 31
--gencode 是给 GENCODE 格式的 fasta 用的,如果是 RefSeq 就别加。我之前遇到过一个坑:不加 --gencode 会导致转录本 ID 解析错误,定量出来的 gene-level 汇总全乱。
定量命令:
salmon quant -i salmon_index -l A \
-1 sample_R1.fastq.gz -2 sample_R2.fastq.gz \
-p 16 --validateMappings -o sample_quant
-l A 让 Salmon 自动检测文库类型;--validateMappings 可以过滤掉低质量的比对,建议始终加上。
每个样本会生成一个 quant.sf 文件,里面是转录本水平的 TPM 和有效计数。要汇总到基因水平,需要用 R 的 tximport 包:
library(tximport)
files <- file.path("sample_quant", "quant.sf")
names(files) <- c("sample1","sample2")
txi <- tximport(files, type="salmon", tx2gene=tx2gene, countsFromAbundance="no")
counts <- txi$counts
countsFromAbundance="no" 表示保留原始的 count 值,不转换成 TPM。如果是做 DESeq2 这类基于负二项分布的差异分析,原始 count 才是正确的输入。
| 维度 | STAR + featureCounts | Salmon + tximport |
|---|---|---|
| 内存需求 | 高(30G~50G) | 低(8G~16G) |
| 运行速度 | 较快(但索引大) | 快(尤其多次运行) |
| 输出文件 | BAM(大) | quant.sf(小) |
| 多比对处理 | 需谨慎调参 | 内置概率模型更鲁棒 |
| 能否检测新剪接 | 可以(借助 STAR) | 不能(依赖已知转录组) |
| 基因水平汇总 | featureCounts 直接出 | 需 tximport |
合并表达矩阵
不管走哪条路,最终每个样本只有一个 count 文件。把它们拼成一张大表就是最后一步。我习惯用 Python/pandas 搞定:
import pandas as pd
import glob
files = glob.glob("*/quant.genes.sf") # Salmon 产物
cols = []
for f in files:
df = pd.read_csv(f, sep="\t", index_col=0, usecols=[0,5], header=0)
sample = f.split("/")[0]
df.columns = [sample]
cols.append(df)
matrix = pd.concat(cols, axis=1)
matrix.to_csv("expression_matrix.csv")
如果是 featureCounts,输出自带 Geneid 和样本列,直接合并即可。
常见调参问题
- 多比对 reads 要不要留:STAR 默认滤掉 >10 个比对,featureCounts 可以设
-M允许多比对。如果你研究的是基因家族或重复区,建议保留多比对并用--fracOverlap做更精细控制。 - 垃圾 reads 的影响:Salmon 的
--validateMappings能过滤掉低质量映射,但无法替代原始读长修剪。我习惯先把 FASTQ 用 fastp 切一下 adapter 和低质量尾。 - 基因水平 vs 转录本水平:表达矩阵通常用基因水平做差异分析。如果后续做异构体分析,Salmon 的 transcript-level 输出可以直接用
tximport(..., type="salmon")拿到转录本 count。
本章就到这里。记住一条原则:工具是手段,表达矩阵的质量才是命根子。多花十分钟理解参数,省下后面三天 debug 的时间。嗯,这样才划算。