宏基因组微生物分类鉴定全流程实现详解¶
基于 bioSkills 技能库(561 个生物信息学技能)的案例 3 实现
涉及技能:5 个 bio-metagenomics- / bio-microbiome- 技能 | 场景:肠道宏基因组 shotgun 测序数据
场景假设¶
研究者拿到一批人类肠道宏基因组(shotgun 全基因组鸟枪法)测序数据(双端 150bp),目标是:
- 回答”样本里有哪些微生物”(分类鉴定,物种水平)
- 估计各物种的相对丰度(细胞分数 vs 读段分数)
- 解析群落的功能潜能(基因家族与代谢通路)
- 比较不同分组(健康 vs 疾病)的群落多样性
数据特征:paired-end reads,含宿主污染(人类 DNA)、接头与低质量碱基,需要先质控再去宿主。
第一步:质控与宿主去除(前置必备)¶
技能:bio-fastq-quality-control + bio-contamination-screening(或 bio-contamination-controls)
工具:fastp + BBDuk / KneadData
1.1 质控¶
fastp -i R1.fastq.gz -I R2.fastq.gz \
-o clean_R1.fastq.gz -O clean_R2.fastq.gz \
--qualified_quality_phred 20 --length_required 50 \
--detect_adapter_for_pe --thread 8
关键点:
- --length_required 50 保留至少 50 bp 的读段(Kraken2 对短读分类效果差)
- 去接头是分类的前置条件,接头会制造”宿主 DNA”假信号
1.2 宿主去除¶
bowtie2 -x human_genome_index -1 clean_R1.fastq.gz -2 clean_R2.fastq.gz \
--un-conc-gz host_removed_R%.fastq.gz --very-sensitive
关键点:宿主 reads 占低生物量样本的绝大部分,不去除会淹没微生物信号;且 GenBank 中 >200 万条记录带有人类/载体污染序列(Steinegger & Salzberg 2020),必须去宿主。
二、Kraken2 分类 + Bracken 丰度重估¶
技能:bio-metagenomics-kraken(primary_tool: Kraken2)+ bio-abundance-estimation(Bracken)
工具:Kraken2 + Bracken + KrakenTools
2.1 基础分类命令¶
kraken2 --db "$KRAKEN_DB" \
--paired --gzip-compressed --threads 8 \
--confidence 0.1 \ # 从默认0提高到0.1-0.4,抑制单k-mer假阳性
--minimum-hit-groups 2 \ # 要求>=2个独立hit区域(临床建议3)
--report out.kreport \
--output out.kraken \
R1.fq.gz R2.fq.gz
核心洞察——Kraken 报告是数据库条件的相似性账本,不是样本清单:
1. 分类 ≠ 存在:--confidence 0 时一个共享 k-mer 就把读分类到数据库最近亲属,即使真实生物不存在
2. 读数 ≠ 丰度:counts 随基因组长度和拷贝数变化,报告百分比是片段分数,不是细胞分数。Bracken 修复的是错误分类层级问题,不修复这个
3. 报告尾部的分类单元是假设,不是发现:单区域 hit、哈希碰撞、污染参考序列会填充长尾
2.2 假阳性控制:独特 minimizer¶
kraken2 --db "$KRAKEN_DB" --paired --confidence 0.1 \
--report-minimizer-data \ # 插入2列:总 minimizer + 独特 minimizer
--report out.krep --output /dev/null \
R1.fq.gz R2.fq.gz
# 高reads但低独特minimizer的物种 = 假阳性(单一保守区域反复命中)
不要硬编码 “>=1024 unique k-mers”——那是临床数据/特定数据库的经验值,不是常量。用阴性对照标定自己的阈值。
2.3 自建数据库¶
kraken2-build --download-taxonomy --db custom_db
for lib in bacteria archaea viral human UniVec_Core; do
kraken2-build --download-library "$lib" --db custom_db
done
kraken2-build --build --db custom_db --threads 16
kraken2-build --clean --db custom_db # 删除 library/ + taxonomy/ 减小体积
bracken-build -d custom_db -t 16 -k 35 -l 150 # -k 必须等于 Kraken2 的 k(35);-l = 读长
大坑:
- --max-db-size 会随机下采样 k-mers 来凑容量,这就是预建 8GB/16GB 数据库的做法,也是为什么高 confidence 会崩溃——读太少无法通过高阈值
- 数据库版本是最大的实验变量:数据库里没有的分类单元,无论怎么调参都检测不到
2.4 交接给 Bracken¶
bracken -d "$KRAKEN_DB" -i out.kreport -o out.bracken -w out.bracken.kreport \
-r 150 \ # 必须匹配 built databaseRLENmers.kmer_distrib 且等于真实读长
-l S -t 10 # species level;-t 是重分配下限,不是置信度
Bracken 修复的只有”错误层级”问题——它把滞留在 genus 的 reads 按基因组先验重新分配到物种。它不修复基因组大小偏差,也不修复假阳性(甚至可能把不存在物种的 reads 重新分配给最近的近亲来放大它)。所以 先做假阳性控制再跑 Bracken。
2.5 常见错误对照表¶
| 错误/症状 | 原因 | 解决 |
|---|---|---|
| 成百上千个不可能物种 | --confidence 0,无 hit-group 下限 |
提高 confidence;--minimum-hit-groups >=2 |
| Bracken “kmer_distrib file not found” | -r 没有匹配的内置分布 |
bracken-build -l <readlen> |
| 报告列错位 | --report-minimizer-data 插入了2列 |
用 8 列布局解析 |
| 小型数据库近乎零分类 | 截断数据库 + 高 confidence | 用大数据库,或降低 confidence |
三、MetaPhlAn 4:标记基因法(互补视角)¶
技能:bio-metagenomics-metaphlan(primary_tool: MetaPhlAn)
工具:MetaPhlAn 4.1+ / Bowtie2 / minimap2
3.1 基本分类¶
# paired-end 作为 ONE 逗号分隔参数(MetaPhlAn 按单端处理,不用配对信息)
metaphlan reads_R1.fastq.gz,reads_R2.fastq.gz \
--input_type fastq \
--index mpa_vJun23_CHOCOPhlAnSGB_202403 \ # 钉住 index,数据库版本是批次变量
--nproc 8 \
--mapout sample.map.bz2 \ # 缓存 read->marker 映射(4.2 前是 --bowtie2out)
--output_file profile.txt
核心洞察——MetaPhlAn 百分比是细胞分数,不是读段分数:
- 标记基因测 WHO 在(细胞);k-mer 测 HOW MUCH DNA 在(读段)
- MetaPhlAn 的百分比是基因组大小归一化的分类丰度;Kraken/Bracken 的百分比是 reads 的序列丰度
- 两者不可合并:它们衡量不同量纲的东西(Sun 2021 Nat Methods 18:618),合并成一张表互相比对是常见错误
3.2 SGB 单位¶
MetaPhlAn 4 的原子分类单位是 SGB(物种级基因组分箱,~95% ANI 簇):
- kSGB 有培养参考基因组、有拉丁名
- uSGB 只由 MAG 定义(需要 >=5 个 MAG),报告为占位 ID 没有名字——不要丢弃 uSGB 行,这是 MetaPhlAn 4 的核心创新,能解释 ~20% 更多肠道读段
3.3 从映射缓存重剖分¶
metaphlan sample.map.bz2 --input_type mapout \ # 前4.2是 bowtie2out
--tax_lev s \ # k,p,c,o,f,g,s,t
--stat_q 0.2 \ # 截断均值:去掉上下20%,平均中间60%
--output_file profile_species.txt
保存一次 --mapout,之后不同层级/估计器设置无需重新比对——重比对是昂贵步骤,下游全部免费。
3.4 未知分数的缩放效应¶
- 未知估计关闭(4.2 前默认):已知 taxa 吸收 100%,数据库缺失群落不可见,高估每个已知分类单元
- 未知估计开启(4.2 默认):出现 UNCLASSIFIED 行,每个已知丰度按比例缩小
- 在土壤/海洋/瘤胃中,未知分数可能是样本中最大的”taxon”
- 4.2 两个 flag 重命名:
--bowtie2out→--mapout;--bowtie2db→--db_dir
3.5 MetaPhlAn 常见错误¶
| 错误 | 原因 | 解决 |
|---|---|---|
| 与 Kraken 百分比合并 | 分子分数 vs 读段分数,量纲不同 | 分开放;如需统一用基因组长度换算 |
| 低映射率 = QC 失败 | 只打标记基因,低映射正常 | 低映射正常;大未知 = 数据库缺失 |
| 合并后行不齐 | 不同 index 构建的 profiles | 钉住一个 index 全部重建 |
四、HUMAnN 3:功能谱分析¶
技能:bio-metagenomics-functional-profiling(primary_tool: HUMAnN)
工具:HUMAnN 3.6+ / MetaPhlAn / DIAMOND / Bowtie2
4.1 分层搜索算法¶
HUMAnN 不是对每条读段暴力翻译。三个层级逐层过滤:
- 分类学预筛(MetaPhlAn):检测物种,然后只对高于
--prescreen-threshold(默认 0.01%)的物种构建样本特异 ChocoPhlAn 泛基因组 - 核苷酸泛基因组搜索(Bowtie2):比对到泛基因组的读段得到 UniRef90 家族 + 高置信物种标签
- 翻译蛋白搜索(DIAMOND):未通过的读段 6 框翻译后比对全 UniRef90/50;只有家族,物种是推断的
|unclassified
4.2 运行命令¶
# 预QC:接头/质量trim + 去宿主(例如 KneadData)。宿主读段膨胀 UNMAPPED 且浪费 DIAMOND 时间
cat sample_R1.fq.gz sample_R2.fq.gz > sample.fq.gz # 配对读段没有原生配对,拼接成单文件
humann --input sample.fq.gz --output out/ \
--taxonomic-profile sample_metaphlan.tsv \ # 复用 MetaPhlAn profile;不要 --remove-temp-output
--threads 8 # 默认:prescreen 0.01, translated-id 80, gap-fill on, minpath on
# 归一化:跨样本统计前先 PER SAMPLE 归一化(RPK 是深度依赖的)
humann_renorm_table -i out/sample_genefamilies.tsv -o out/sample_cpm.tsv -u cpm
humann_join_tables -i out -o merged_pathabundance.tsv --file_name pathabundance
humann_regroup_table -i merged_pathabundance.tsv -g uniref90_ko -o merged_ko.tsv # 会加 UNGROUPED 行
humann_split_stratified_table -i merged_pathabundance.tsv -o . # 统计跑在 UNSTRATIFIED 文件上
4.3 不要丢弃 UNMAPPED/UNINTEGRATED¶
import pandas as pd
df = pd.read_csv('merged_pathabundance_unstratified.tsv', sep='\t', index_col=0)
meta = pd.read_csv('metadata.tsv', sep='\t', index_col=0)
unmapped = df.loc[['UNMAPPED', 'UNINTEGRATED']] # 分母,检查但不要丢弃
g1 = meta.index[meta['condition'] == 'healthy']
g2 = meta.index[meta['condition'] == 'disease']
unmapped_shift = unmapped[g2].mean(axis=1) - unmapped[g1].mean(axis=1)
print(unmapped_shift.round(1))
# 然后交给 MaAsLin2/ANCOM-BC(对组分与零膨胀建模),而不是裸 t-test/Mann-Whitney
为什么不能丢:丢弃后重新归一化会把已分配的特征重新缩放到和为 1,凭空制造丰度并抹掉数据库覆盖信号。两个样本 UNMAPPED 20% vs 60% 会看起来一模一样。
4.4 功能谱 = 潜力,不是活性¶
一个基因家族或”完整通路”是该群落编码的能力——从来不是速率、通量或表达证据。RNA(宏转录组)才说表达。叙述”疾病微生物组上调 X”是绝对错误——它只是更丰富地携带了该基因。
五、多样性分析(α 和 β 多样性)¶
技能:bio-microbiome-diversity-analysis(primary_tool: phyloseq)
工具:QIIME2 / phyloseq / vegan / scikit-bio
5.1 三个”把手”决定答案¶
一个多样性值不是测量,而是三个选择的输出:
1. 抽平深度(rarefaction depth)
2. 系统发育树(tree)
3. 度量(metric)
--p-sampling-depth是”披着归一化外衣的样本删除旋钮”:core-metrics 把每个样本稀平到该深度,并静默丢弃总计数低于它的样本——不警告,只是 PCoA 上少几个点。丢弃的通常是最低产出的样本(最低生物量的拭子、病重患者、失败提取),丢失几乎从不随机。
5.2 选择抽样深度¶
qiime feature-table summarize --i-table table.qza --o-visualization table.qzv # 深度信息在这
qiime diversity alpha-rarefaction \
--i-table table.qza --i-phylogeny rooted-tree.qza \
--p-max-depth 20000 \
--m-metadata-file metadata.tsv --o-visualization alpha-rarefaction.qza
qiime diversity core-metrics-phylogenetic \
--i-phylogeny rooted-tree.qza --i-table table.qza \
--p-sampling-depth 10000 \ # 在 observed-features 平台上;会静默丢弃低深度样本
--m-metadata-file metadata.tsv --output-dir core-metrics-results
规则:深度要在 observed-features 平台的饱和区;不要用 min(sample_sums)——一个小文库会把所有样本拖到欠饱和噪声;报告丢弃样本列表;在邻近深度确认结论。
5.3 建树(建模选择)¶
优先 SEPP 片段插入到全长参考树(或 Greengenes2),而不是 de novo MAFFT+FastTree。~250bp 读长的 de novo 树分辨率差、中点根任意,会让 unweighted UniFrac 不稳定(Janssen 2018)。
qiime fragment-insertion sepp \
--i-representative-sequences rep-seqs.qda \
--i-reference-database sepp-refs-gg-13-8.qda \
--p-threads 4 \
--o-tree insertion-tree.qda --o-placements insertion-placements.qda
qiime fragment-insertion filter-features \
--i-table table.qda --i-tree insertion-tree.qda \
--o-filtered-table table-sepp.qda --o-removed-table removed-table.qda
5.3 α 多样性(R)¶
library(phyloseq); library(vegan)
ps_rare <- rarefy_even_depth(ps, sample.size = chosen_depth, rngseed = 42, replace = FALSE)
alpha <- estimate_richness(ps_rare, measures = c('Observed', 'Shannon', 'InvSimpson'))
alpha$Group <- sample_data(ps_rare)$Group
alpha$Shannon_eff <- exp(alpha$Shannon) # 有效种数(Hill q=1),跨工具可比
kruskal.test(Shannon ~ Group, data = alpha)
QIIME2 的 Shannon 是 log2(bits),R 是自然对数(nats)——数值差一个常数因子,直接比较看起来像真实效应。报告 exp(H') 可绕过基数问题。
5.4 β 多样性(必须加权 + 非加权都要报告)¶
wu <- UniFrac(ps_rare, weighted = TRUE) # 优势谱系视角
uwu <- UniFrac(ps_rare, weighted = FALSE) # 稀有谱系 + 拓扑视角
gu <- as.dist(GUniFrac::GUniFrac(t(as(otu_table(ps_rare), 'matrix')), phy_tree(ps_rare), alpha = 0.5)$unifracs[, , 'd_0.5'])
meta <- data.frame(sample_data(ps_rare))
adonis2(wu ~ Group, data = meta, permutations = 999) # 显著 = 质心位置差异
permutest(betadisper(wu, meta$Group)) # 强制:是离散度差异还是位置差异?
如果不做 betadisper,adonis2 的显著可能只是”一组变异更大”而不是”群落组成不同”——尤其不平衡设计(Anderson & Walsh 2013)。unweighted 与 weighted 结论相反时,说明变化在稀有谱系 vs 优势谱系,必须都报告并说明各自暗示的谱系轴。
六、可视化与群落统计¶
技能:bio-metagenomics-visualization(primary_tool: phyloseq)
工具:phyloseq / vegan / microViz / pandas + scikit-bio / matplotlib
6.1 诚实的组成图¶
Krona 是”里面有什么”的最诚实答案——完整可钻取层级,而不是预先折叠到 top-10 的柱状图:
krepdeport2krona.py -r kraken.kreport -o krona.txt && ktImportText krona.txt -o krona.html
柱状图规则:把稀有 taxa 折叠成”Other”时必须标注隐藏了多少 taxa 和多少百分比;每 n 展示(绝不叠加一个代表样本);≤12 种色盲安全色;如果有总生物量数据就同时展示绝对负载(相对丰度会隐藏群落 bloom/collapse 的真实幅度)。
6.2 α 多样性:Hill 数 + 丰富度诚实性¶
alpha <- estimate_richness(ps_counts, measures = c('Shannon', 'InvSimpson'))
hill_q1 <- exp(alpha$Shannon) # 有效种数 q=1
hill_q2 <- alpha$InvSimpson # q=2 优势度加权
shotgun richness 是数据库 + 置信度阈值的读出,不是生物学:25-70% 的 shotgun 物种可能是假阳性。Observed/Chao1 只能用在整数 counts(Bracken 可以,MetaPhlAn 百分比不行)。
6.3 β 多样性:组成学正确选择¶
| 距离 | 组成学正确 | 需要树 | 何时 |
|---|---|---|---|
| Bray-Curtis | 否 | 否 | 领域默认、直观;标注不协调 |
| Aitchison(CLR 上的欧氏) | 是 | 否 | 组成学正确选择,需处理零 |
| RPCA (DEICODE) | 是 | 否 | 稀疏无伪计数 |
| UniFrac(加权/非加权) | 部分 | 是 | 仅当有树(MetaPhl SGB 树) |
library(vegan)
dist_bc <- vegdist(otu_matrix, method = 'bray')
pm <- adonis2(dist_bc ~ group, permutations = 999, by = 'terms')
bd <- betadisper(dist_bc, group); permutest(bd, permutations = 999) # 必须配对
6.4 Python 组成学排序¶
import pandas as pd
from skbio.stats.composition import clr, multi_replace
from sklearn.decomposition import PCA
ab = pd.read_csv('merged_abundance.txt', sep='\t', index_col=0)
ab = ab[ab.index.str.contains(r'\|s__') & ~ab.index.str.contains(r'\|t__')] # 物种行
proportions = (ab.T.values / ab.T.values.sum(axis=1, keepdims=True))
clr_mat = clr(multi_replace(proportions)) # 零替换 + CLR(不是 StandardScaler)
pca = PCA(n_components=2).fit(clr_mat)
coords = pca.transform(clr_mat)
6.5 差异丰度:共识,不是单工具¶
DA 方法跨数据集中差异巨大(Nearing 2022):报告的显著 taxa 更依赖工具而非生物学。至少跑两个组成学感知的方法,交集 = 高置信,并集 = 探索性,全部报告。ALDEx2 与 ANCOM-II 最保守/一致;LinDA 与 ANCOM-BC FDR 控制好。禁止:未校正的 Wilcoxon/t-test 直接作用于原始相对丰度——一行代码同时错在组成学和多重检验上。
| 工具 | 模型 | 引用 |
|---|---|---|
| ALDEx2 | Dirichlet 蒙特卡洛 → CLR | Fernandes 2014 Microbiome |
| ANCOM-BC | log-linear + 抽样分数偏倚校正 | Lin & Peddada 2020 Nat Commun |
| MaAsLin2 | 一般线性模型 + 协变量 | Mallick 2021 PLoS Comput Biol |
| LinDA | CLR 上线性模型 + 偏倚校正 | Zhou 2022 Genome Biol |
七、全流程总结¶
原始 shotgun reads(R1+R2)
│
├─ Step 1: fastp 质控(去接头、去低质量)→ Bowtie2 宿主去除
│
├─ Step 2: Kraken2 分类(--confidence 0.1 --minimum-hit-groups 2)
│ → Bracken 丰度重估计(-r 读长匹配)
│ → 假阳性控制(--report-minimizer-data / KrakenUniq)
│
├─ Step 3: MetaPhlAn 4 标记基因剖面(细胞分数,uSGB 量化未知 taxa)
│ (注意:与 Kraken 分数不可合并)
│
├─ Step 4: HUMAnN 3 功能谱(MetaPhl 预筛 → Bowtie2 泛基因组 → DIAMOND)
│ → 保留 UNMAPPED/UNINTEGRATED → 归一化到 CPM
│
├─ Step 5: 多样性(α: Hill q=0/1/2;β: adonis2 + betadisper 配对)
│
└─ Step 6: 可视化(Krona / 堆叠柱 / CLR-PCA / 差异丰度共识)
涉及技能一览¶
| 步骤 | 技能名 | 工具 | 核心作用 |
|---|---|---|---|
| 1 | bio-metagenomics-kraken |
Kraken2 + Bracken | k-mer 分类 + 丰度重估计 |
| 1-2 | bio-metagenomics-metaphlan |
MetaPhlAn 4 | 标记基因分类(细胞分数) |
| 3 | bio-metagenomics-functional-profiling |
HUMAnN 3 | 功能基因 + 通路丰度 |
| 4 | bio-microbiome-diversity-analysis |
QIIME2/phyloseq | α/β 多样性 |
| 5 | bio-metagenomics-visualization |
phyloseq/vegan/microViz | 可视化 + 群落统计 |
三步走总结¶
- “谁在里面”:Kraken2 + Bracken(读段分数)+ MetaPhlAn(细胞分数)双视角
- “能做什么”:HUMAnN 功能谱(潜力,不是活性)
- “怎么比较”:多样性 + 排序 + 差异丰度共识