4. phyloseq包详解:从零搭起你的物种丰度分析骨架

说到 R 语言做宏基因组可视化,phyloseq 这个包你绕不开。它就像分析流程里的“集装箱”,把 OTU 丰度表、物种注释表、样本元数据全整合在一个对象里。我刚开始用 phyloseq 时,总觉得这步有点“黑盒”,后来踩过不少坑,才摸透了它到底是怎么把这些数据捏在一起的。

4.1 phyloseq对象创建:把三张表合并成一个“数据容器”

要建一个 phyloseq 对象,你手头至少得有这三样东西:

  • OTU 丰度表(行是 OTU,列是样本)
  • 物种注释表(行是 OTU,列是分类级别)
  • 样本元数据表(行是样本,列是实验信息)

这三张表说白了,就是分开放着谁也用不上,必须塞进同一个结构里才方便分析。

4.1.1 数据准备:先把表格整成 phyloseq 能认的格式

phyloseq 不会凭空变出数据。你得先把原始的 OTU 表格读进来,再转成它要求的格式。我习惯用 readr::read_csv() 或 data.table::fread() 读原始数据,然后用 otu_table() 函数来构建正确格式:

library(phyloseq)
library(tidyverse)

# 读取原始 OTU 丰度表(行是 OTU,列是样本)
otu_raw <- read.csv("otutable_raw.csv", row.names = 1)

# 构建 OTU 丰度矩阵
otu_mat <- as.matrix(otu_raw)
OTU <- otu_table(otu_mat, taxa_are_rows = TRUE)

关键参数:taxa_are_rows = TRUE 表示你的矩阵里每一行代表一个 OTU,每一列代表一个样本。如果你的数据是转置的(行是样本、列是 OTU),就设成 FALSE。

物种注释表也一样,把它转成 taxonomyTable 对象:

# 读取物种注释表(行是 OTU,列是 Kingdom/Phylum/Class 等)
tax_raw <- read.csv("taxonomy_raw.csv", row.names = 1)

# 构建物种注释矩阵
tax_mat <- as.matrix(tax_raw)
TAX <- tax_table(tax_mat)

至于样本元数据表,它必须是个 sample_data 对象,而且行名要与 OTU 表的列名完全一致——这就是个坑。我曾经因为排序不同,结果样本匹配不上,查了半天才发现是行名顺序的锅。

# 读取样本元数据(行是样本,列是分组/处理/时间等)
sam_raw <- read.csv("sample_metadata.csv", row.names = 1)

# 构建 sample_data 对象
SAM <- sample_data(sam_raw)

我的建议:无论你用 CSV 还是 Excel,养成习惯在 R 里先把行名设置好,用 rownames() 检查一遍。三张表的行名/列名必须能对应上,这一步省了后边省大麻烦。

4.1.2 合并三张表:phyloseq 函数怎么做到“一键整合”?

三张表各自就绪后,用 phyloseq() 函数把它们包在一起,就跟打包行李一样:

# 合并 OTU + 物种注释 + 样本元数据
physeq <- phyloseq(OTU, TAX, SAM)

# 检查对象基本信息
physeq
# 输出示例:
# phyloseq-class experiment-level object
# otu_table()   OTU Table:         [ 200 taxa and 24 samples ]
# tax_table()   Taxonomy Table:    [ 200 taxa by 6 taxonomic ranks ]
# sample_data() Sample Data:       [ 24 samples by 8 sample variables ]

我踩过的坑:phyloseq() 函数会自动按行名和列名做匹配。但如果你 OTU 表里有缺失的行(比如某些 OTU 在物种注释表里没对应项),它会直接丢掉那些数据。我曾经因此丢了一小部分 OTU,事后才发现。

合并完成后,你就可以用 physeq 这个对象直接跑各种分析函数,比如 plot_bar()、estimate_richness() 等。

phyloseq 对象创建的核心流程 phyloseq 对象合并流程 OTU 丰度表 物种注释表 样本元数据 otu_table() tax_table() sample_data() phyloseq() 合并三表 + 自动匹配 输出 phyloseq 对象 包含 OTU/TAX/SAM 行名/列名必须匹配,否则 phyloseq() 会丢弃不匹配的数据 建议创建前先用 rownames() 检查各表索引

4.2 常用访问器函数:从 phyloseq 对象里“掏出”你想要的表

对象创建好之后,你不能总盯着 physeq 看。实际分析中,你得频繁从里面提取 OTU 表、物种注释、样本数据,或者修改它们。phyloseq 提供了一组配套的访问器函数,说白了就是“打开集装箱取零件”。

4.2.1 提取函数:怎么把数据从 phyloseq 里拿出来?

名字很好记,直接用和创建时一样的函数名就能提取:

# 提取 OTU 丰度表
otutab <- otu_table(physeq)
class(otutab)  # "otu_table" 对象
otutab_df <- as.data.frame(otutab)  # 转回普通 data.frame

# 提取物种注释表
taxtab <- tax_table(physeq)
taxtab_df <- as.data.frame(taxtab)

# 提取样本元数据
samtab <- sample_data(physeq)
samtab_df <- as.data.frame(samtab)

你想想看:为什么 phyloseq 要用自己的对象结构,而不是直接留成 data.frame?因为有了这个统一的对象,后边所有分析函数(如 plot_heatmap()、psmelt())才能直接调用,而不用每次手动传三张表。说白了,就是封装。

还有一个很少人用但我觉得很实用的函数 nsamples() 和 ntaxa(),能快速返回样本数和 OTU 数量:

nsamples(physeq)  # 样本数量
ntaxa(physeq)     # OTU 数量

4.2.2 修改函数:想在 phyloseq 对象里替换某张表?

你可能会想,我要是把 OTU 表经过去除低丰度 OTU 之后,怎么放回原对象? 很多时候,你需要在不重建整个对象的情况下更新内部的数据。这时用相同的函数名,把 new 参数塞进去就行。

# 假设我们过滤了低丰度 OTU,得到了新的 OTU 表 new_otutab
# 把它替换回原 phyloseq 对象
physeq_new <- otu_table(physeq) <- new_otutab

# 或者用更明确的写法
physeq_new <- phyloseq(new_otutab, tax_table(physeq), sample_data(physeq))

我比较推荐第二种写法——显式地把三张表重新组合,不容易手滑改错。如果你只是临时修改某个表,可以用 sample_data(physeq)$ 语法直接操作:

# 给样本元数据增加一列:把 day 转为 factor
sample_data(physeq)$day_factor <- factor(sample_data(physeq)$day)

# 修改已有的列
sample_data(physeq)$group <- recode(sample_data(physeq)$group, 
                                     "Control" = "Ctrl", 
                                     "Treatment" = "Treat")

我曾经遇到过:用 sample_data(physeq)$newcol 添加新列时,忘记把修改后的对象重新赋值回去,结果发现原始对象中加上的新列完全没被保存。嗯,这里要注意:如果你希望修改生效,别忘用 physeq <- ... 给对象重新赋值。

4.3 一个快速检查表:你该选哪种提取/修改方式?

场景 推荐操作 注意事项
只读 OTU 丰度表 otu_table(physeq) 结果是个 phyloseq 内部对象,不能直接运算
读完后做矩阵运算 as(otu_table(physeq), "matrix") 用 as() 强制转换,搞定再转回来
添加一列分组信息 sample_data(physeq)$newcol <- value 改完后要重新赋值:physeq <- physeq
替换整个物种注释表 重建:phyloseq(OTU, newTAX, SAM) 这样最安全,避免索引错乱
快速查看数据概况 直接用 physeq 打印 会显示各表的维度

4.4 嵌套操作与管道:更优雅的提取链

看过我代码的人都知道,我特别喜欢用 magrittr 的管道操作来连写提取和转换:

library(dplyr)

# 从 physeq 中提取 OTU 表,转为数据框,计算每行(每个OTU)的总丰度
otu_total <- otu_table(physeq) %>%
  as("matrix") %>%
  as.data.frame() %>%
  mutate(rowsum = rowSums(across(where(is.numeric))))

说白了,phyloseq 的强大之处不在于它有多少新奇的功能,而在于它把这三张表严格地约束在一起。你只要掌握了对象创建和访问器函数,后面那些可视化函数(比如 plot_bar、plot_heatmap)才能稳稳地建立在统一的数据结构上。

一句话总结:创建 phyloseq 对象的关键是保证三张表的索引一致;访问和修改对象时,用对应的访问器函数最安全;如果只是简单查看,直接用 physeq 打印就够了。