数据格式破译:h5、mtx、spatial 与矩阵家族

做空间转录组分析,第一步就是跟文件夹和文件格式打交道。说实话,我早期经常因为读错文件把自己坑惨——不是矩阵反了就是坐标对不上。所以这章咱们把最基础的三种数据格式彻底说透:h5、mtx 和 spatial 目录,以及它们背后的 barcode 矩阵和特征矩阵。

1. h5 格式:一个文件搞定一切

h5(HDF5)是 Hierarchical Data Format 的缩写。说白了就是一个能塞进无数数据集的超级容器。我个人很喜欢这种格式,因为它把表达矩阵、空间坐标、甚至图像信息全部打包在一个文件里,移植的时候不会丢三落四。

一个标准的 10x Visium h5 文件,内部结构大概长这样:

file.h5
├── /matrix
│   ├── barcodes (dataset)
│   ├── data (dataset)
│   ├── indices (dataset)
│   ├── indptr (dataset)
│   ├── shape (dataset)
│   └── features
│       ├── _all_tag_keys (dataset)
│       ├── feature_type (dataset)
│       ├── genome (dataset)
│       ├── id (dataset)
│       └── name (dataset)
├── /spatial
│   ├── scalefactors_json (dataset)
│   ├── tissue_hires_image (dataset)
│   ├── tissue_lowres_image (dataset)
│   └── tissue_positions_list (dataset)

看到那个 /matrix/barcodes 了吗?这就是我们后面要说的 barcode 矩阵的列名。而 /matrix/features 对应的就是特征矩阵的行信息。

一点经验:读取 h5 时注意 HDF5 版本兼容。我遇到过用 h5py 3.x 读 2.x 写的老文件,直接报 "Incompatible HDF5 version"。强推用 h5py.File(file, 'r') 时加上 swmr=True,能绕开不少坑。

2. mtx 格式:稀疏矩阵三件套

mtx 全称 Market Exchange Format,是稀疏矩阵的标准存储方式。10x 把三个文件扔进一个文件夹:

filtered_feature_bc_matrix/
├── barcodes.tsv.gz
├── features.tsv.gz
└── matrix.mtx.gz

这三个文件必须配套使用。我来还原我当年踩过的坑:

我曾经 拿 Seurat::Read10X 读一个目录,结果矩阵的行列顺序完全反了。后来发现是 matrix.mtx 里的行(features)和列(barcodes)的索引顺序和 barcodes.tsv / features.tsv 不匹配——因为我手动解压修改了文件顺序。记住:永远不要私自调整这三个文件的顺序,要改就用专门的函数操作。

现在看一个标准的 matrix.mtx 文件头:

%%MatrixMarket matrix coordinate integer general
%
32285 36601 171920671
32285 1 1
32285 2 1
...

第一行表示这是一份坐标格式的整数矩阵。第二行:32285 个基因(行),36601 个 barcode(列),1.7 亿个非零值。后面每行一个非零坐标:行号、列号、计数。

3. spatial 目录:把基因放回组织里

有了表达矩阵还不够,你要知道每个 spot 长在哪个物理位置。spatial 目录就是干这个的:

spatial/
├── tissue_positions_list.csv
├── scalefactors_json.json
├── tissue_hires_image.png
└── tissue_lowres_image.png

tissue_positions_list.csv 每一行对应一个 barcode,包含:barcode(去掉后缀)、在低分辨率图像上的行/列坐标(px_col, px_row)、在组织切片上的高分辨率坐标(x, y)、以及是否有组织的标志(in_tissue=1/0)。注意坐标原点是左上角,和图像坐标系一致。

scalefactors_json.json 则保存了高分辨图、低分辨图、以及原图之间的缩放比例。读图像时要先除这个系数,否则坐标会偏移。

4. barcode 矩阵与特征矩阵详解

很多新手搞混这两个概念。我习惯这样区分:

矩阵类型 行 列 数值含义 典型文件
barcode 矩阵 基因 / 特征 spots / 捕获点 每个 spot 上每个基因的 count barcodes.tsv(列名)
matrix.mtx(计数)
特征矩阵 基因 / 特征 属性列(id, name, type 等) 特征的元信息 features.tsv 或 h5 内 /matrix/features
空间坐标矩阵 spots 坐标 + 标志位 每个 spot 的 x, y, in_tissue tissue_positions_list.csv 或 h5 内 /spatial/tissue_positions_list

你可以把 barcode 矩阵想象成一个巨大的稀疏表——行是 2 万个基因,列是几千个 spot,里面填的是 UMI count。而特征矩阵是这张表的「行注释」。空间坐标矩阵则是「列注释」的扩展(除了哪个 spot,还要告诉我它在哪)。

5. 一张图看懂数据流

下面这张 SVG 图把从原始文件到关键矩阵的转换关系画出来了。这也是我每次做新项目前必画的路线图,对着它调代码不容易走歪。

空间转录组数据解析流程 数据格式到矩阵的解析路径 h5 文件 /matrix + /spatial mtx 三件套 matrix.mtx + 两个tsv spatial 目录 坐标 + 缩放 + 图像 解析器 (Read10X / ReadVisium) barcode 矩阵 spots × 基因 (稀疏) 特征矩阵 基因元信息表 空间坐标 x, y, in_tissue 解析器统一输出:barcode 矩阵(稀疏矩阵)+ 特征矩阵(data.frame)+ 空间坐标(data.frame)

6. 实战:用代码把三种格式统一读进来

不管原始数据是 h5 还是 mtx+spatial,最终我们都要拿到两个矩阵和一个坐标表。以 R 语言为例,最常用的做法:

读取 h5(使用 Seurat)

library(Seurat)
# 直接从 h5 读入
data <- Read10X_h5("raw_feature_bc_matrix.h5")
# 返回一个稀疏矩阵,列是 barcode,行是 feature
spatial_coords <- ReadVizgen("spatial/")  # 或者手动解析 h5 内 /spatial/tissue_positions_list

读取 mtx + spatial

library(Seurat)
# 读 mtx 三件套(需要保证目录下只有这三个文件)
mat <- Read10X("filtered_feature_bc_matrix/")
# 读空间坐标
spatial_data <- read.csv("spatial/tissue_positions_list.csv", header = FALSE)
# 根据 barcode 匹配矩阵列和坐标行

跨格式检查

无论哪种方式,最终你得到的 barcode 矩阵列数应等于 tissue_positions_list.csv 的行数(除去 header)。我当时做验证时写过一个简单脚本:

# 检查 barcode 是否完全一致
bc_mat <- colnames(mat)
bc_coord <- spatial_data$V1  # 假设第一列是 barcode
if (all(bc_mat == bc_coord)) {
  print("✅ barcode 匹配,可以生成 Seurat 对象")
} else {
  stop("❌ barcode 不匹配,检查文件是否对应同一份组织切片")
}

核心原则:barcode 是 spot 的唯一标识符,它串联了表达矩阵的列和空间坐标表的行。一旦出现错配,后面所有空间分析都是废的。务必在数据加载后立即做一致性校验。

数据格式搞清楚了,下一步才能放心地做 Spot 过滤。这章讲的这些文件结构你最好手头备一个实际数据集练一练,打开看看 h5 的 group 和 dataset,数数 mtx 的非零值,核对一下 spatial 里的坐标单位,这样以后遇到奇怪的格式变化也能一眼识破。