2. 数据格式与读取:常见空间数据格式与读取方法

空间单细胞数据,说白了就是给每个细胞或spot贴了个空间坐标。但各个平台输出的文件格式五花八门,不统一。
我刚开始接触空间转录组数据时,光是把h5ad转成Seurat对象就折腾了半天。所以这一章,咱们先把常见格式和读取方法捋清楚。

2.1 三大主流数据格式概览

目前主流的有三种:h5ad(Python系)、SpatialExperiment(R系)、Seurat(R系)。
我个人习惯是:如果上游用Scanpy处理,我就用h5ad;下游要用R做统计或可视化,就转成Seurat或SpatialExperiment。你想想看,选对格式能省不少事。

格式 核心框架 存储内容 典型扩展名
h5ad AnnData (Python) 表达矩阵、元数据、空间坐标、邻接图 .h5ad
SpatialExperiment SpatialExperiment (R) 表达矩阵、空间坐标、图像元数据 .rds 或 .rda
Seurat Seurat (R) 表达矩阵、降维、空间坐标、图像 .rds
小提示: h5ad是HDF5格式的,所以你用HDF5工具也能直接查看内部结构。我在排查数据异常时经常这么干。

2.2 h5ad 格式与读取

h5ad 是 Python 生态最常见的格式。内部结构分为:X(表达矩阵)、obs(细胞/spot元数据)、var(基因元数据)、obsm(包括空间坐标)、uns(非结构化元数据)。

我用 Python 读取 h5ad 的写法很简单:

import scanpy as sc
adata = sc.read_h5ad('sample.h5ad')
print(adata)
# AnnData object with n_obs × n_vars = 5000 × 20000
#     obs: 'batch', 'spot_id'
#     obsm: 'spatial'
#     uns: 'spatial_neighbors'

读取后,空间坐标一般在 adata.obsm['spatial'] 里。我遇到过一个坑:有些平台把坐标写在 adata.obs 里,命名成 array_row/array_col,手动检查一下才稳。

# 查看空间坐标
spatial_coords = adata.obsm['spatial']
print(spatial_coords.shape)  # 5000 x 2
注意: 如果用 sc.read_h5ad 读取超大文件(比如 10万+ spots),内存会炸。我曾经在16GB的笔记本上直接崩了。建议用 backed=True 模式,按需读取。
# backed 模式,避免 OOM
adata = sc.read_h5ad('large_sample.h5ad', backed='r')

2.3 SpatialExperiment 格式与读取

SpatialExperiment 是 R 生态里的专用格式。它继承自 RangedSummarizedExperiment,多了个 spatialCoords 槽位专门存坐标。

我通常这样读:

library(SpatialExperiment)
spe <- readRDS('sample_spe.rds')
# 或者直接读取 h5ad 转过来
spe <- readH5AD('sample.h5ad')
print(spe)
# class: SpatialExperiment
# dim: 20000 5000
# spatialCoords names: 'x' 'y'

提取坐标也很直接:

coords <- spatialCoords(spe)
head(coords)
#           x         y
# spot_1  100.5    200.3
# spot_2  101.2    201.1
避坑: 读入的 h5ad 如果 obs 里有因子类型列(factor),R会自动转成字符串。我在做小鼠脑切片项目时,发现batch信息被错误转换了。用 as.character() 或 factor() 手动修正一下就好。

2.4 Seurat 格式与读取

Seurat 格式是单细胞分析的经典格式。空间数据在 Seurat 里有个 Spatial 的 assay,里面包含表达矩阵、坐标(image 对象)、H&E 图像。

我一般这样读:

library(Seurat)
seurat_obj <- readRDS('sample_seurat.rds')
# 或者从 h5ad 转
seurat_obj <- Read10X_H5('filtered_feature_bc_matrix.h5')
# 再手动创建 Seurat 对象

查看空间坐标:

# 获取坐标
coords <- GetTissueCoordinates(seurat_obj)
head(coords)
#   tissue row col
# 1  slice1 100 200
# 2  slice1 101 200

我个人习惯在 Seurat 对象里同时保留多个模态(gene expression + spatial + protein),用 @assays 隔开。

2.5 格式互转实战

大部分时候你不会只用一个生态。比如你从10X Visium拿到的是h5和JSON,你用Seurat处理完,又想在Python里做批次校正。这时候格式转换就不可避免。

有一回我接了个项目,上游给了10个h5ad文件,但下游R包只认SpatialExperiment。我写了段代码批量转:

library(SpatialExperiment)
library(zellkonverter)

# h5ad -> SpatialExperiment
for (f in list.files(pattern = '*.h5ad')) {
  spe <- readH5AD(f)
  saveRDS(spe, gsub('.h5ad', '_spe.rds', f))
}

反过来也差不多:

import anndata
import scanpy as sc

# Seurat rds -> h5ad
# 先通过R导出为h5ad,然后再用Python读
# 在R中:
# library(Seurat)
# library(SeuratDisk)
# SaveH5Seurat(seurat_obj, filename = 'temp.h5Seurat')
# Convert('temp.h5Seurat', dest = 'h5ad')
核心建议:
  • 如果你团队主要用Python,就用h5ad + Scanpy。
  • 如果下游分析要用R(比如聚类可视化),优先SpatialExperiment。
  • Seurat适合深度单细胞经验的人,但空间数据多模态管理有些笨重。

2.6 本章知识结构

下图展示了三种格式的读取流程与互相转换的关系:

空间单细胞数据格式读取与互转流程图 空间单细胞数据格式读取与互转 原始数据 h5ad (AnnData) SpatialExperiment Seurat sc.read_h5ad() readRDS() / readH5AD() readRDS() / Read10X_H5() 💡 说明 虚线箭头表示格式互转,可使用 zellkonverter / SeuratDisk 等工具。 建议项目初期就统一格式,避免混合使用的兼容性问题。

2.7 读取并验证数据

无论用哪种格式读取,我建议第一步先做完整性验证。别等到下游分析才发现空间坐标丢了。

# Python
def validate_adata(adata):
    assert 'spatial' in adata.obsm, '空间坐标缺失!'
    assert adata.obs.shape[0] == adata.obsm['spatial'].shape[0], 'spot数和坐标数不一致'
    print('数据完整性检查通过')
# R
validate_spe <- function(spe) {
  stopifnot(ncol(spe) == nrow(spatialCoords(spe)))
  cat('SpatialExperiment 验证通过\n')
}
常见坑: 读入后,如果发现 spatial 坐标列名不是 'x'/'y',而是 'col'/'row' 或 'X'/'Y',记得手动重命名。我在处理 10X 数据时就遇到过一次,记得统一命名做后续分析。