4. BIOM 格式版本差异:BIOM 1.0 vs 2.0,HDF5 vs JSON

好,咱们聊聊版本。BIOM 格式从 1.0 到 2.0 的变化,说白了就是一次脱胎换骨。我刚接触时也觉得,不就是个格式嘛,升级就升级呗。

结果第一次处理一个 2GB 的 JSON 文件,直接把我笔记本内存撑爆了。嗯,从那以后,我深刻理解了为什么要有 2.0。

4.1 核心差异在哪?

最根本的区别,就是底层存储格式换了。

  • BIOM 1.0:只能基于 JSON(纯文本)。
  • BIOM 2.0:强制基于 HDF5(二进制)。

你想想看,JSON 是给人读的,但机器读起来效率就低了。而 HDF5 是给机器读的,人类想直接看?没门。

我的经验是:小数据集(比如几百个 OTU,几十个样本)用 1.0 JSON 完全没问题。一旦样本数上千,或者要做大规模宏基因组分析,必须上 2.0 HDF5。

4.2 BIOM 1.0:轻量但膨胀

BIOM 1.0 的结构很简单,就是标准的 JSON 对象。核心字段不多:

{
  "id": "MyTable",
  "format": "Biological Observation Matrix 1.0.0",
  "format_url": "http://biom-format.org",
  "type": "OTU table",
  "generated_by": "QIIME 1.9.1",
  "date": "2020-01-15",
  "matrix_type": "dense",
  "matrix_element_type": "int",
  "shape": [5, 3],
  "data": [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9, 10, 11], [12, 13, 14]],
  "rows": [
    {"id": "OTU1", "metadata": {"taxonomy": ["Bacteria", "Firmicutes"]}},
    {"id": "OTU2", "metadata": {"taxonomy": ["Bacteria", "Bacteroidetes"]}}
  ],
  "columns": [
    {"id": "Sample1", "metadata": {"host": "Human", "body_site": "gut"}},
    {"id": "Sample2", "metadata": null}
  ]
}

看着挺清晰,对吧?但这里有个坑:metadata 重复。每个 OTU 的 taxonomy 信息都会重复存储,哪怕 10000 个 OTU 都属于同一个门。文件体积就这么被撑大了。

曾经有一次,我用 1.0 格式导出一个 500 个样本、10000 个 OTU 的丰度表,JSON 文件直接飙到 1.5GB。加载花了 15 分钟,解析又花了 10 分钟。那时候我就想,这必须得改。

4.3 BIOM 2.0:高效但复杂

BIOM 2.0 改用 HDF5,这是个什么概念?

HDF5 是一种分层数据格式,它把数据存在一个文件系统中。你可以把它想象成一个压缩的、带索引的小型数据库。

2.0 的 biom 文件,用 HDF5 工具查看,结构大概是这样:

/
├── observation
│   ├── matrix
│   │   ├── data          (dataset, int)
│   │   ├── indices       (dataset, int)
│   │   └── indptr        (dataset, int)
│   └── metadata
│       └── taxonomy      (dataset, variable-length string)
├── sample
│   ├── matrix
│   │   ├── data
│   │   ├── indices
│   │   └── indptr
│   └── metadata
│       └── host          (dataset, string)
├── observation_group
│   └── group_metadata    (dataset, ...)
└── sample_group
    └── group_metadata    (dataset, ...)

你看到了什么?数据被拆成了 CSR(Compressed Sparse Row)格式 存储。data、indices、indptr 三剑客,专门对付稀疏矩阵。要知道,丰度表绝大多数都是稀疏的(大部分 OTU 在大部分样本里是 0),这个设计太聪明了。

我个人的习惯是:所有新项目都默认生成 2.0 HDF5 格式。只有在跟老软件(比如 QIIME 1)交互时,才转成 1.0 JSON。

4.4 文档级对比

对比维度 BIOM 1.0 (JSON) BIOM 2.0 (HDF5)
底层格式 纯文本 二进制
文件大小 大(膨胀) 小(压缩)
读取速度 慢(需全量加载) 快(支持随机读取)
稀疏矩阵支持 支持但不高效 原生高效存储
元数据 重复存储 分级去重存储
兼容性 几乎所有旧工具 新工具、Python/R
人类可读 是(可用文本编辑器打开) 否(需专用工具)

4.5 什么时候用哪个?

我给你几个场景,你根据情况选:

  1. 数据量小(< 50MB):用 1.0 JSON。方便调试,方便看数据长啥样。
  2. 数据量大(> 500MB):强制用 2.0 HDF5。别犹豫,否则你会卡死。
  3. 元数据复杂:用 2.0。它的分组机制可以存多层结构,1.0 的平铺机制存着存着就乱了。
  4. 需要跨语言调用:优先 2.0。HDF5 在 C/C++、Python、R、Java 里都有成熟接口。JSON 虽然也有,但遇到大文件就露怯了。

4.6 避坑指南

我曾经踩过一个坑:同一个项目里,课题组用 QIIME 2 出了 2.0 的 biom,我这边用 Python 的 biom-format 包读成 1.0 的 JSON 对象。结果 taxonomy 字段死活解析不对。

后来发现,2.0 里的 taxonomy 是个 变长字符串数组,而 1.0 里是个固定 JSON 数组。转换时数据截断了。

解决办法很简单:

# 如果手里是 2.0 格式,先转成 1.0 再操作
biom convert -i table.biom -o table.json --to-json

# 或者直接用 h5py 读取 2.0 原始数据
import h5py
with h5py.File('table.biom', 'r') as f:
    data = f['observation/matrix/data'][:]
    print(data.shape)

记住:版本转换时,metadata 的结构会发生变化。尤其是 taxonomy 这类层级信息,一定要检查转换前后的字段是否一致。

4.7 格式选择流程图

下面这张图帮你快速决策,根据你的数据量和元数据复杂度来选择版本:

BIOM 格式版本选择流程 开始:丰度表 数据量 < 50MB? (文件大小) 元数据是否复杂? (层次结构 > 3 层) 选 BIOM 2.0 HDF5 格式 选 BIOM 1.0 JSON 格式 是 否 提示: 元数据复杂 = 有多层级分类、分组信息(如:门→纲→目→科→属) 或涉及不同分组间的交叉引用。

4.8 转换实战

讲到这里,肯定有人要问:如果手里是 1.0,怎么转 2.0?反过来呢?

工具链很成熟。我用 biom-format 工具包的命令行,基本能解决 99% 的问题。

1.0 JSON → 2.0 HDF5

biom convert \
  -i table_1.0.json \
  -o table_2.0.biom \
  --to-hdf5 \
  --table-type="OTU table"

2.0 HDF5 → 1.0 JSON

biom convert \
  -i table_2.0.biom \
  -o table_1.0.json \
  --to-json

注意:从 2.0 转回 1.0 时,某些 metadata 字段可能丢失。因为 1.0 不支持 HDF5 的复杂分组结构。我曾因为 taxonomy 字段里的 lineage 信息被展平,导致后续分析代码报错。所以,转出后一定要检查。

4.9 总结

BIOM 1.0 和 2.0 没有绝对的好坏,只有适不适合。你可以这样记:

  • 1.0 JSON:轻量调试神器,适合小项目和教学。
  • 2.0 HDF5:生产环境标配,应对大规模数据。

我个人现在写 pipeline,默认全部输出 2.0。只有遇到老旧的上下游模块时,才在中间环节转成 1.0 做桥接。版本选择没有对错,别在格式上纠结太久,核心是把数据存下来、读出来、分析清楚。