3、BIOM格式结构详解:观察表、特征表、元数据、JSON格式规范

BIOM 格式,说白了就是一个 JSON 文件。但它不是那种松散的 JSON,它有一套非常严格的结构规范。我最初接触这个格式时,光看文档绕了好久,后来自己手写了几次例子才彻底搞明白。

它的核心思想很简单:用 稀疏矩阵 存丰度数据,再把行、列各自关联的元数据一并打包。你想想看,一张物种丰度表,行是 OTU/ASV,列是样本,大部分格子是 0——稀疏矩阵正好省空间,元数据又能让数据「自解释」,这才适合在工具链里流转。

下面这张图把 BIOM 的整体骨架画清楚了,我们边看图边讲。

BIOM 格式结构总览 BIOM JSON 文件 文件元数据 id format type generated_by 矩阵数据 shape matrix_type matrix_element_type data 行信息 行ID (特征/观察) 行元数据 (分类、注释等) 列信息 列ID (样本) 列元数据 (时间、处理、组别等) 行数 = 特征数(OTU/ASV/基因),列数 = 样本数 每个行/列可携带独立的元数据对象 文件元数据 矩阵数据 行信息 列信息 箭头表示 JSON 顶层字段 → 子结构
怎么看这张图? 从顶层的 BIOM JSON 出发,分出四个子结构。每个子结构背后都对应 JSON 里的具体字段。后面几节我会逐个拆开讲。

3.1 观察表与特征表:同一枚硬币的两面

很多初学者会被「观察表」和「特征表」这两个术语搞晕。我当初也困惑过——它们到底是不是同一个东西?

答案是:在 BIOM 的语境里,它们指的是同一张表,只是版本和工具的称呼习惯不同。

术语 来源 / 语境 实际含义
观察表 (Observation Table) BIOM 1.0 规范 矩阵中的行,每个「观察」是一个生物实体(OTU、ASV、基因等)
特征表 (Feature Table) BIOM 2.0+ / QIIME 2 生态 矩阵中的行,每个「特征」是一个分类单元或功能单元

在 BIOM 的 JSON 里,行的字段名统一叫 rows,每个元素包含 id 和可选的 metadata。无论你叫它「观察」还是「特征」,底层结构完全一样。

注意: 不要在一份 BIOM 文件里混用这两个术语。工具链通常根据 type 字段来判断语义——比如 "type": "OTU table" 或 "type": "Feature table"。混用可能导致下游解析出错。

3.2 矩阵数据:核心的丰度信息

矩阵数据是 BIOM 的心脏。它记录了两样东西:矩阵长什么样 和 非零值在哪儿。

3.2.1 关键字段

字段 类型 说明
shape [int, int] [行数, 列数],即 [特征数, 样本数]
matrix_type string "sparse" 或 "dense"
matrix_element_type string 元素类型:"int"、"float"、"unicode" 等
data array 矩阵数据本体(稀疏或稠密格式)

3.2.2 稀疏矩阵的存储方式

物种丰度表的特点就是「零多值少」。用稀疏矩阵只存非零值,文件体积能缩小 10-100 倍。这是 BIOM 最实用的设计之一。

稀疏格式的 data 是一个数组,每个元素是 [row_index, col_index, value]。

// 稀疏矩阵示例:3 行 4 列,共 5 个非零值
"shape": [3, 4],
"matrix_type": "sparse",
"matrix_element_type": "float",
"data": [
  [0, 0, 12.5],
  [0, 2, 3.2],
  [1, 1, 8.7],
  [1, 3, 1.4],
  [2, 0, 6.0]
]

你看,3×4=12 个格子,只存了 5 个非零值。我一开始用的时候担心索引会搞错,后来养成了习惯:拿到 BIOM 文件先用 shape 确认边界,再去看 data 里的索引是否越界。这是一个很实用的自检步骤。

经验之谈: 如果丰度是整数(比如序列条数),matrix_element_type 用 "int";如果是归一化的相对丰度,用 "float"。选错类型虽然不会报错,但下游工具读取时可能丢失精度。

3.3 行信息与列信息:给数据加上标签

只有一堆数字没法用,你得告诉计算机:第 0 行对应哪个 OTU,第 2 列对应哪个样本。这就是 rows 和 columns 的职责。

3.3.1 行信息(特征/观察)

rows 是一个数组,长度必须等于 shape[0]。每个元素包含:

  • id(必须):特征的唯一标识,比如 OTU 编号或 ASV 序列哈希。
  • metadata(可选):一个对象,存放该特征的分类信息、功能注释等。
"rows": [
  {
    "id": "OTU_1",
    "metadata": {
      "taxonomy": ["Bacteria", "Firmicutes", "Bacilli", "Lactobacillales", "Lactobacillus"]
    }
  },
  {
    "id": "OTU_2",
    "metadata": {
      "taxonomy": ["Bacteria", "Proteobacteria", "Gammaproteobacteria", "Enterobacterales"]
    }
  }
]

3.3.2 列信息(样本)

columns 也是数组,长度等于 shape[1]。每个元素同样包含 id 和可选的 metadata:

"columns": [
  {
    "id": "sample_01",
    "metadata": {
      "time_point": "T0",
      "treatment": "control",
      "host": "patient_A"
    }
  },
  {
    "id": "sample_02",
    "metadata": {
      "time_point": "T1",
      "treatment": "drug",
      "host": "patient_A"
    }
  }
]
踩坑提醒: 我曾经生成过一份 BIOM 文件,rows 长度跟 shape[0] 对不上,结果 QIIME 2 导入时直接报 ValueError。解析器不会帮你自动补齐或截断,必须严格一致。写代码生成 BIOM 时,记得加一个断言检查。

3.4 元数据:让数据会说话

BIOM 里的元数据分三个层级:

  1. 文件级元数据:放在顶层,如 id、format、type、generated_by、date。它们描述这份数据从哪来、用什么工具生成。
  2. 行元数据:放在 rows[i].metadata,描述每个特征的分类、注释等。
  3. 列元数据:放在 columns[j].metadata,描述每个样本的实验条件、临床信息等。

我的建议是:不要把「文件级元数据」塞到行或列的 metadata 里。虽然 JSON 本身不限制,但下游工具往往有固定的读取位置。比如 QIIME 2 会从 columns 的 metadata 中读取样本分组信息,如果你把它放在顶层,它就找不到。

核心原则: 元数据的位置决定了它的作用域。文件级 → 全局,行级 → 特征,列级 → 样本。各归各位,工具链才顺畅。

3.5 JSON 格式规范:每个字段都有意义

BIOM 2.0 的 JSON Schema 定义了一套完整的字段。我用一张表把顶层字段列清楚:

字段 类型 必填 说明
id string 否 数据集唯一标识,建议用 UUID 或项目代码
format string 是 格式版本,如 "Biological Observation Matrix 2.0"
format_url string 否 指向规范文档的 URL
type string 否 数据类型,如 "OTU table"、"Feature table"、"Pathway table"
generated_by string 否 生成此文件的工具名称及版本
date string 否 创建日期,推荐 ISO 8601 格式
shape [int, int] 是 [行数, 列数]
matrix_type string 是 "sparse" 或 "dense"
matrix_element_type string 是 "int"、"float"、"unicode"
data array 是 矩阵数据,格式由 matrix_type 决定
rows array 是 行信息数组,每个元素含 id 和可选 metadata
columns array 是 列信息数组,每个元素含 id 和可选 metadata

format 是唯一一个必须出现在顶层的字段。为什么?因为解析器拿到文件后,第一件事就是读 format 来判断用哪个版本的解析逻辑。少了它,工具链直接罢工。

3.6 完整的 BIOM JSON 示例

把上面所有的知识点揉在一起,一个最小但完整的 BIOM 2.0 文件长这样:

{
  "id": "example_16S_v1",
  "format": "Biological Observation Matrix 2.0",
  "format_url": "https://biom-format.org/specification_2_0.html",
  "type": "OTU table",
  "generated_by": "my_custom_script v0.3",
  "date": "2025-03-21T10:30:00+08:00",
  "shape": [3, 2],
  "matrix_type": "sparse",
  "matrix_element_type": "int",
  "data": [
    [0, 0, 42],
    [0, 1, 15],
    [1, 0, 7],
    [2, 1, 23]
  ],
  "rows": [
    {
      "id": "ASV_001",
      "metadata": {
        "taxonomy": ["Bacteria", "Firmicutes"]
      }
    },
    {
      "id": "ASV_002",
      "metadata": {
        "taxonomy": ["Bacteria", "Bacteroidota"]
      }
    },
    {
      "id": "ASV_003",
      "metadata": {
        "taxonomy": ["Bacteria", "Actinobacteria"]
      }
    }
  ],
  "columns": [
    {
      "id": "sample_A",
      "metadata": {
        "body_site": "gut",
        "disease": "healthy"
      }
    },
    {
      "id": "sample_B",
      "metadata": {
        "body_site": "gut",
        "disease": "IBD"
      }
    }
  ]
}

这个例子 3 行 2 列,稀疏矩阵只存了 4 个非零值。我平时自测 BIOM 生成脚本时,就会先构造这样一个最小用例,确认结构对了再上真实数据。养成这个习惯,能省去不少排查时间。

自检清单:
✅ rows 长度 = shape[0]
✅ columns 长度 = shape[1]
✅ data 中所有 row_index ∈ [0, shape[0]-1],col_index ∈ [0, shape[1]-1]
✅ format 字段存在且版本正确

BIOM 的结构讲到这里,核心就是三块:矩阵数据存丰度,行信息标特征,列信息标样本,元数据散落在各自层级中。你写代码生成或解析 BIOM 时,脑子里始终装着这张三层结构图,就不会迷路。