20260910 110123 Rna Seq 差异表达分析全流程实现详解

20260910_110123_RNA_seq_差异表达分析全流程实现详解.md

基于 bioSkills 技能库(561 个生物信息学技能)的案例 1 实现
涉及技能:9 个 bio-* 技能 | 场景:肿瘤 vs 正常组织 bulk RNA-seq


场景假设

研究者在肿瘤组织(treat)和正常组织(ctrl)各取 3 个生物学重复样本,进行 bulk RNA-seq 测序(Illumina NovaSeq,PE150),目标是找出两组间的差异表达基因并定位受影响的生物学通路。


第一步:原始数据质控

技能:bio-read-qc-quality-reports + bio-fastq-quality
工具:FastQC + MultiQC + Biopython

1.1 逐文件 QC(FastQC)

# 对6个样本的R1/R2共12个FASTQ文件运行FastQC
fastqc -t 8 -o qc/raw/ raw_data/*.fastq.gz

FastQC 生成每个文件的 HTML 报告,包含 10 个模块(per-base quality、GC content、duplication、adapter content 等)。

关键注意点:

  • FastQC 的 pass/warn/fail 红绿灯是为 WGS DNA 校准的,RNA-seq 数据必然在 per-base content(随机六聚体偏倚)和 duplication(高表达转录本)模块上报红,这是正常的,不是数据问题。
  • NovaSeq/NextSeq 是 2-color chemistry,质量值是分箱的(只有 4 个值),箱线图看起来”方块化”是正常的。

1.2 汇总报告(MultiQC)

multiqc qc/raw/ -o qc/multiqc/ -f

MultiQC 把所有样本的报告聚合到一张总览表,方便横向比较。先读 General Statistics 表,看每个样本的 reads 数、Q30 比例、GC 含量是否有离群值。

1.3 编程级质量分析(Biopython)

from Bio import SeqIO
from collections import defaultdict

# 确认编码格式:2011年后的Illumina数据均为Phred+33('fastq')
# 千万不要猜编码——从仪器元数据确认

# 计算每个read位置的平均质量,识别3'端衰减
position_quals = defaultdict(list)
for record in SeqIO.parse('sample1_R1.fastq', 'fastq'):
    for i, q in enumerate(record.letter_annotations['phred_quality']):
        position_quals[i].append(q)

for pos in sorted(position_quals)[:20]:
    quals = position_quals[pos]
    print(f'Position {pos}: mean={sum(quals)/len(quals):.1f}')

关键判断标准:

Phred 分数 错误率 准确率
Q20 1/100 99%
Q30 1/1000 99.9%
Q40 1/10000 99.99%

Q30 比例是常规质量目标。


第二步:数据预处理

技能:bio-read-qc-fastp-workflow
工具:fastp(一步完成接头修剪 + 质量过滤 + poly-G 去除 + QC 报告)

# NovaSeq RNA-seq 预处理:轻修剪(aligner 会 soft-clip,不要 --dedup)
fastp -i raw_data/sample1_R1.fq.gz -I raw_data/sample1_R2.fq.gz \
      -o clean/sample1_R1.fq.gz -O clean/sample1_R2.fq.gz \
      --detect_adapter_for_pe \
      --trim_poly_g \
      -q 20 -l 50 \
      -w 8 \
      -h clean/sample1.html -j clean/sample1.json

关键参数解释:

  • --detect_adapter_for_pe:fastp 通过 R1/R2 overlap 分析自动检测接头,不需要手动指定接头序列,连只有 1 个碱基的 read-through 也能切掉。
  • --trim_poly_g:NovaSeq 是 2-color chemistry,G 是无信号碱基,poly-G 尾巴质量过滤器看不到但必须去除。
  • -q 20 -l 50:质量低于 Q20 的碱基过滤,过滤后短于 50bp 的 read 丢弃。
  • 绝对不加 --dedup:fastp 的 --dedup 是序列级去重,会删掉高表达转录本的真实 reads,RNA-seq 中绝不能用。

预处理后再 QC

fastqc -t 8 -o qc/trimmed/ clean/*.fastq.gz
multiqc qc/ -o qc/compare/ -f   # 同时对比 raw vs trimmed

第三步:基因组比对

技能:bioinformatics-align-star
工具:STAR(splice-aware aligner)

3.1 构建 STAR 索引(一次性)

# sjdbOverhang = readlength - 1 = 150 - 1 = 149
STAR --runMode genomeGenerate \
     --genomeDir star_index/ \
     --genomeFastaFiles GRCh38.fa \
     --sjdbGTFfile gencode.v44.annotation.gtf \
     --sjdbOverhang 149 \
     --runThreadN 16

关键点:--sjdbOverhang 应设为 read 长度减 1,决定剪接位点侧翼序列长度,直接影响 junction 检测灵敏度。

3.2 比对

for sample in sample1 sample2 sample3 sample4 sample5 sample6; do
  STAR --genomeDir star_index/ \
       --readFilesIn clean/${sample}_R1.fq.gz clean/${sample}_R2.fq.gz \
       --readFilesCommand zcat \
       --outFileNamePrefix aligned/${sample}_ \
       --outSAMtype BAM SortedByCoordinate \
       --quantMode GeneCounts \
       --twopassMode Basic \
       --runThreadN 8
done

关键参数:

  • --outSAMtype BAM SortedByCoordinate:直接输出按坐标排序的 BAM。
  • --quantMode GeneCounts:STAR 顺便生成 per-gene 计数,第 2 列是无链计数、第 3 列正向链、第 4 列反向链——可据此判断 library strandedness。
  • --twopassMode Basic:第一遍比对发现新剪接位点,第二遍用扩展后的 junction 注释重新比对,提高灵敏度。

STAR 特性:唯一比对的 read 获得 MAPQ=255,这在 SAM 规范中意为”MAPQ 不可用”,但对 STAR 而言就是”唯一比对”。对后续 featureCounts 计数无影响,但做 RNA variant calling 时 GATK 可能对 255 有不同处理。


第四步:基因水平定量

技能:featurecounts-counting
工具:featureCounts(Subread)

featureCounts -a gencode.v44.annotation.gtf \
              -o counts.txt \
              -s 2 \
              -p --countReadPairs \
              -T 8 \
              aligned/sample1_Aligned.sortedByCoord.out.bam \
              aligned/sample2_Aligned.sortedByCoord.out.bam \
              aligned/sample3_Aligned.sortedByCoord.out.bam \
              aligned/sample4_Aligned.sortedByCoord.out.bam \
              aligned/sample5_Aligned.sortedByCoord.out.bam \
              aligned/sample6_Aligned.sortedByCoord.out.bam

关键参数:

  • -s 2:反向链特异性。大部分 dUTP/TruSeq 文库是反向链特异性建库。如果链方向选错,计数会减半。不确定时用 RSeQC infer_experiment.py 先验证,或看 STAR GeneCounts 的第 3/4 列哪个数字大。
  • -p --countReadPairs:paired-end 数据按 fragment(read pair)计数,而不是按单条 read 计数(不加 -p 会导致计数翻倍)。

验证输出:

cat counts.txt.summary

正常 poly-A 文库的 Assigned 比例应在 70-90%。如果只有 ~50%,很可能链方向选错了。


第五步:计数矩阵质控

技能:count-matrix-qc
工具:DESeq2 (R) + Python

5.1 加载并检查

library(DESeq2)
library(edgeR)

counts <- read.csv('counts.txt', sep='\t', row.names=1, skip=1)
counts <- counts[, 6:11]  # 去掉前5列元数据列,只保留计数列
coldata <- data.frame(condition = factor(c('ctrl','ctrl','ctrl','treat','treat','treat')),
                      row.names = colnames(counts))

dds <- DESeqDataSetFromMatrix(countData = counts, colData = coldata, design = ~condition)

colSums(counts(dds))        # 每样本总reads
colSums(counts(dds) > 0)    # 每样本检出基因数

5.2 过滤低表达基因

# 用 edgeR 的 filterByExpr 做有原则的过滤,优于硬编码 CPM>1
keep <- filterByExpr(counts(dds), group = dds$condition)
dds <- dds[keep, ]
cat("保留基因数:", sum(keep), ",过滤基因数:", sum(!keep), "\n")

5.3 归一化和变换

# VST(方差稳定变换)用于 QC 可视化,不是用于 DE 检验
# n>30 用 vst(),n<30 用 rlog()——本例 n=6 偏小,但 vst 通常也能用
vsd <- vst(dds, blind = TRUE)   # blind=TRUE 用于无监督QC

关键区分:CPM/TPM 不校正组成偏倚,只按样本内总量缩放,只能用于可视化,不能用于 DE 归一化。DESeq2 用 median-of-ratios 估计 size factor 做归一化,这在 DESeq() 内部自动完成。

5.4 PCA 和样本关系

plotPCA(vsd, intgroup = 'condition')

# 样本距离热图
pheatmap::pheatmap(as.matrix(dist(t(assay(vsd)))), main = 'Sample distance (VST)')

判断标准:

PCA 模式 含义 行动
按 condition 分开 信号强 继续
按批次分开 批次主导 在 design 中加入 batch
某个样本离群 异常或样本错配 查QC、查size factor

5.5 离群值检测

dds <- DESeq(dds)
cooks <- assays(dds)[['cooks']]   # 注意:不是 results(dds)$cooksd
boxplot(log10(cooks), las = 2, main = "Cook's distance per sample")

5.6 批次效应处理

# 已知批次:放入 design 公式,让引擎在原始计数层面校正
design(dds) <- ~ batch + condition   # condition 在最后 = 感兴趣的对比

# 绝不要对 DE 输入数据使用 removeBatchEffect() 或 ComBat 后再喂给 DESeq2
# 那样会双重校正

致命情况:如果 batch 与 condition 完全混杂(比如所有 treat 在 batch A,所有 ctrl 在 batch B),统计上无法补救。


第六步:差异表达检验

技能:deseq2-basics
工具:DESeq2 (R)

library(DESeq2)

# 重新构建(如果上面改了 design)
dds <- DESeqDataSetFromMatrix(countData = counts, colData = coldata, design = ~condition)
dds <- DESeq(dds)

# 提取结果:显式命名对比,alpha=0.05
res <- results(dds, contrast = c('condition', 'treat', 'ctrl'), alpha = 0.05)
summary(res)

# LFC 收缩(apeglm):降噪但不改变 p 值
res_shrunk <- lfcShrink(dds, coef = 'condition_treat_vs_ctrl', type = 'apeglm')

两个关键陷阱:

  1. 默认 results() 静默使用最后一个系数——一定要用 contrast= 显式命名想要的对比,否则可能拿到错误的系数。
  2. 收缩后的 LFC 使用的是未收缩 Wald 检验的 p 值——p 值不变,不要在收缩后的值上做基于阈值的 FDR 声明。LFC 收缩是为了 ranking 和可视化,不是为了重新检验。

Wald vs LRT 选择:

检验 适用场景 特点
Wald 两组对比(默认) 检验单个系数是否为 0
LRT 多水平因子/综合检验 需要 reduced 模型,检验”任何差异”

七步:结果可视化

library(DESeq2)
library(EnhancedVolcano)
library(ggplot2)

7.1 离散度诊断

plotDispEsts(dds)

看基因级离散度(黑点)是否跟随拟合趋势线(红线),收缩后(蓝点)是否被拉向趋势——健康的表现。

7.2 p 值直方图

res_df <- as.data.frame(res)
ggplot(res_df, aes(x = pvalue)) + geom_histogram(bins = 50) + theme_bw()

零假设下应为均匀分布 + 0 附近一个尖峰。如果是 U 形,说明有隐藏批次效应需要加入协变量。

7.3 MA 图

# 未收缩 vs 收缩对比
par(mfrow = c(1, 2))
plotMA(res, ylim = c(-5, 5), main = 'Unshrunken')
plotMA(res_shrunk, ylim = c(-5, 5), main = 'apeglm shrunk')

收缩后低计数基因的噪声被拉向 0,云更紧凑。

7.4 火山图

library(EnhancedVolcano)
EnhancedVolcano(res_shrunk,
    lab = rownames(res_shrunk),
    x = 'log2FoldChange',
    y = 'pvalue',          # 注意:用未收缩的 p 值
    pCutoff = 0.05,
    FCcutoff = 1,
    max.overlaps = Inf)

关键标注:x 轴标注”shrunken log2FC (apeglm)”,y 轴标注”unshrunken Wald p-value”——收缩不重新计算 p 值。

7.5 差异基因热图

# 取 top 50 差异基因
top_genes <- head(order(res$padj), 50)
mat <- assay(vsd)[top_genes, ]
pheatmap::pheatmap(mat, scale = 'row',     # 行z-score:看pattern
                   annotation_col = coldata,
                   show_rownames = FALSE)

7.6 单基因表达图

# 查看某个关键基因的表达模式
plotCounts(dds, gene = 'TP53', intgroup = 'condition')

第八步:GSEA 通路富集

技能:pathway-gsea
工具:clusterProfiler (R)

8.1 构建排序列表

library(clusterProfiler)
library(org.Hs.eg.db)

# 用 DESeq2 的 stat(Wald 统计量)作为排名指标
# 关键:用带符号的、方差校准的统计量,不用原始 p 值
gl <- res$stat
names(gl) <- rownames(res)
gl <- sort(gl, decreasing = TRUE)

# 去掉 NA
gl <- gl[!is.na(gl)]

关键原则:排名指标必须是有符号(signed)且方差校准的统计量(如 DESeq2 的 stat、limma 的 t),绝不能用原始 p 值——p 值没有方向性。

8.2 运行 GSEA

# GO Biological Process
set.seed(42)   # 必须设种子,保证可重复
gse_go <- gseGO(geneList = gl,
                OrgDb = org.Hs.eg.db,
                ont = 'BP',
                minGSSize = 10,
                maxGSSize = 500,
                seed = TRUE)
gse_go <- setReadable(gse_go, OrgDb = org.Hs.eg.db)

# KEGG 通路
gse_kegg <- gseKEGG(geneList = gl,
                    organism = 'hsa',
                    minGSSize = 10,
                    maxGSSize = 500,
                    seed = TRUE)

8.3 查看结果

head(gse_go@result, 10)
列名 含义
enrichmentScore (ES) 富集得分
NES 归一化富集得分(跨基因集可比)
pvalue 原始 p 值
p.adjust FDR 校正后 p 值
leading_edge 核心驱动基因
core_enrichment leading edge 中的基因列表

8.4 可视化

# 气泡图
dotplot(gse_go, showCategory = 20) + ggtitle('GSEA GO BP')

# GSEA 运行得分曲线
library(enrichplot)
gseaplot(gse_go, by = 'count', geneSetID = 1)

全流程总结

原始 FASTQ
  │
  ├─ Step 1: FastQC + MultiQC 质控报告
  │          (RNA-seq 在 per-base content / duplication 报红是正常的)
  │
  ├─ Step 2: fastp 一步预处理
  │          (自动接头修剪 + poly-G去除 + 质量过滤,绝不 --dedup)
  │
  ├─ Step 3: STAR 比对
  │          (sjdbOverhang=149, twopassMode, --quantMode GeneCounts)
  │
  ├─ Step 4: featureCounts 定量
  │          (-s 2 反向链, -p 按fragment计数, 验证Assigned>70%)
  │
  ├─ Step 5: 计数矩阵 QC
  │          (filterByExpr过滤, VST变换, PCA聚类, Cook's离群值, 批次检查)
  │
  ├─ Step 6: DESeq2 差异检验
  │          (Wald检验, alpha=0.05, apeglm LFC收缩, 显式contrast)
  │
  ├─ Step 7: 可视化
  │          (离散度图, p值直方图, MA图, 火山图, 热图, plotCounts)
  │
  └─ Step 8: GSEA 通路富集
             (用 stat 排名, clusterProfiler gseGO/gseKEGG, NES+FDR判定)

涉及技能一览

步骤 技能名 工具 核心作用
1 bio-fastq-quality Biopython 编程级质量分析
1 bio-read-qc-quality-reports FastQC/MultiQC 全局质控报告
2 bio-read-qc-fastp-workflow fastp 一步预处理
3 bio-read-alignment-star-alignment STAR 剪接感知比对
4 bio-rna-quantification-featurecounts-counting featureCounts 基因水平定量
5 bio-rna-quantification-count-matrix-qc DESeq2 矩阵质控+PCA
6 bio-differential-expression-deseq2-basics DESeq2 差异检验+LFC收缩
7 bio-differential-expression-de-visualization DESeq2+ggplot2 诊断与结果图
8 bio-pathway-gsea clusterProfiler GSEA通路富集