基于 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 文库是反向链特异性建库。如果链方向选错,计数会减半。不确定时用 RSeQCinfer_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')
两个关键陷阱:
- 默认
results()静默使用最后一个系数——一定要用contrast=显式命名想要的对比,否则可能拿到错误的系数。 - 收缩后的 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通路富集 |