丰度数据基础:OTU/ASV表格、物种注释、元数据读入

做丰度数据分析,第一步就是搞清楚我们手里到底有什么数据。我刚开始接触这个领域时,拿到一个项目,对方丢给我一堆文件:有的叫 otu_table.txt,有的叫 taxonomy.txt,还有一堆 metadata.csv。当时我就懵了——这些文件到底长什么样?该怎么读进来?

其实说白了,丰度数据分析就三种核心数据:

  • 丰度表(OTU/ASV 表格)—— 谁(物种)在多少个样本里出现了
  • 物种注释 —— 这些物种从门到属的分类信息
  • 元数据 —— 样本本身的实验条件、分组信息

这三者缺一不可。先把它们读进 R 里,后面的分析和调优才有根基。

1. OTU/ASV 表格:丰度的“骨架”

OTU 和 ASV 本质上是同一类东西——都是对测序序列进行聚类后得到的“代表单元”。区别只是聚类算法不同。我习惯用 ASV 表做分析,因为它的分辨率更高,能区分到单碱基差异。但不管哪种,表格的格式大同小异。

标准格式长这样:

# OTU ID   Sample1   Sample2   Sample3
OTU_1       120        85       203
OTU_2        45       112        78
OTU_3       203        67       145

第一列是 OTU/ASV 的 ID,后面每列是一个样本,单元格里是丰度值(通常是整数)。

读入时我推荐用 read.table() 或 data.table::fread()。注意设置 row.names = 1 把第一列变成行名。如果文件里有注释行(以 # 开头),要加 comment.char = "#" 忽略掉。

# 用 data.table 快速读入
library(data.table)
otutab <- fread("otutab.txt", header = TRUE, sep = "\t")
otutab <- as.data.frame(otutab)
rownames(otutab) <- otutab[,1]
otutab <- otutab[,-1]
个人习惯:我总会在读入后立刻检查维度 dim(otutab),确保行数(物种数量)和列数(样本数量)和预期一致。有一次我读入后发现有 0 列,才发现文件分隔符是逗号而不是制表符——这种坑太常见了。

2. 物种注释:给每个 OTU/ASV 贴上“身份证”

光有丰度表,你只知道每个样本里有多少个“单元”,但不知道它是什么。物种注释就是用来给每个 OTU/ASV 一个分类学标签:从界到门、纲、目、科、属、种,层层剥开。

典型的注释文件结构:

# OTU ID   Kingdom    Phylum          Class        Order
OTU_1      Bacteria   Proteobacteria  Gammaproteobacteria  Enterobacterales
OTU_2      Bacteria   Firmicutes      Bacilli      Lactobacillales
OTU_3      Bacteria   Bacteroidetes   Bacteroidia  Bacteroidales

读入方法和丰度表类似,但要小心列分隔符。有时注释表会用制表符,有时用逗号。我见过有人在 Excel 里存成 CSV,结果分类名里自带逗号(比如 “Candidatus, Unknown”),导致读入时字段错位。解决办法是:用 quote = "\"" 或者直接改用制表符格式。

# 读入物种注释
tax <- read.table("taxonomy.txt", header = TRUE, sep = "\t", 
                  row.names = 1, stringsAsFactors = FALSE)
# 查看前几行
head(tax)

注意:注释和丰度表必须用同一套 OTU/ASV ID。顺序可以不一致,后续用 merge() 或 match() 对齐即可。

我曾经踩过的坑: 有一次我的 OTU 表有 2000 行,注释表却只有 1998 行。找来找去,发现有两个 OTU 在聚类时被合并了,但注释文件没有更新。所以读入后一定要用 setdiff() 检查 ID 是否一一对应。

3. 元数据:样本的“档案”

元数据记录了每个样本的实验信息:分组、时间、处理、个体编号等等。没有它,你的丰度表就是一堆冷冰冰的数字。

标准结构:

SampleID     Group   Time   Treatment
Sample1      Control  T0     Untreated
Sample2      Control  T0     Untreated
Sample3      Treat    T1     DrugA

读入方法同样简单。关键点是:样本名必须和丰度表的列名一致。

metadata <- read.csv("metadata.csv", row.names = 1)
# 检查样本名是否匹配
stopifnot(all(rownames(metadata) %in% colnames(otutab)))
我的小技巧:把元数据的第一列作为行名,这样后续用 match() 或 %in% 操作时更顺手。如果元数据里有多余的样本,或者丰度表里有多余的样本,一定要先做交集。我一般这样处理:
common_samples <- intersect(colnames(otutab), rownames(metadata))
otutab <- otutab[, common_samples]
metadata <- metadata[common_samples, ]

4. 三张表的整合:一篇结构图理清关系

我把这三张表的关系画成一张图。你看一眼,后面做 alpha 多样性、beta 多样性、差异分析时,心里就有数了。

丰度数据分析基础数据流 OTU/ASV 丰度表 行:物种,列:样本 值:丰度计数 物种注释表 行:物种,列:分类层级 域-门-纲-目-科-属 元数据表 行:样本,列:实验信息 分组/时间/处理 数据整合与对齐 按物种ID和样本ID做交集/匹配 可用的 phyloseq 对象 注意:ID 必须完全一致

5. 读入后的快速核查清单

数据读入后,我建议你按下面这张表逐项检查,避免后期分析跑出奇怪结果。

检查项 方法 预期结果
丰度表维度 dim(otutab) 行数>0,列数=样本数
丰度值类型 class(otutab[,1]) 整数或数值
注释表行数 nrow(tax) 等于丰度表行数
注释层级列数 colnames(tax) 至少包含Kingdom到Genus
元数据样本名匹配 all(rownames(metadata) %in% colnames(otutab)) TRUE
元数据分组列数 length(unique(metadata$Group)) 符合实验设计

总结一下:

  • 丰度表是核心,行是物种,列是样本。
  • 物种注释是字典,告诉每个ID是什么分类。
  • 元数据是样本的背景档案。
  • 读入后立即做 ID 一致性检查,否则后续所有分析都是废的。

这三张表读入 R 后,我一般会直接塞进 phyloseq 包里的 phyloseq() 函数,生成一个统一对象。但那是下一部分的内容了。今天到这里,先把基础打牢。下次我们聊“数据清洗与过滤”——嗯,你会发现很多有趣的问题藏在那些 0 值和低丰度 OTU 里。