第二讲:数据导入与格式转换

做单细胞空间转录组分析,第一步就是把手头各种格式的数据给读进来。我刚开始接触这行时,面对.h5ad、.rds、.h5各种文件,确实懵过一阵。今天咱们就把这事捋清楚。

说白了,不同工具生态用不同格式。Scanpy 认 H5AD,Seurat 认 RDS,SPATA2 也有自己的规矩。我们做整合分析时,经常要在这些格式之间来回倒腾。别怕,核心套路就那么几种,掌握了就通。

核心要点:不管数据是什么格式,最终都是一个表达矩阵 + 空间坐标 + 元信息的组合。读数据就是把这套东西完整取出来。

2.1 H5AD 格式的读取

H5AD 是 Scanpy 生态的标准格式,底层是 Hierarchical Data Format。我自己项目中 70% 的数据都是这个格式。

在 Python 里,读 H5AD 就是一个函数的事:

import scanpy as sc

adata = sc.read_h5ad("sample_data.h5ad")
# 看一眼结构
print(adata)
# 输出类似:AnnData object with n_obs × n_vars = 10000 × 30000
#     obs: 'cell_type', 'sample_id'
#     var: 'gene_symbol'
#     obsm: 'spatial', 'X_pca'
#     uns: 'spatial_neighbors'

嗯,注意看 obsm['spatial'],这个就是空间坐标矩阵。没有这个,空间分析无从谈起。

我建议拿到数据后先做个快速检查:

# 检查必须有字段
assert 'spatial' in adata.obsm, "缺空间坐标!"
assert adata.X is not None, "缺表达矩阵!"
print("数据完整性检查通过")
避坑指南:我曾经遇到过 H5AD 文件里的 X 是稀疏矩阵格式,但某些函数要求稠密矩阵。记得用 adata.X.toarray() 转一下。

2.2 Seurat 对象的读取

跳转到 R 语言生态,Seurat 是绝对的主角。我早期做项目时,手头有一批 Seurat v3 格式的 rds 文件,折腾了好久才读对。

library(Seurat)
library(SeuratDisk)

# 方法一:直接读 rds
seurat_obj <- readRDS("seurat_object.rds")

# 方法二:从 h5seurat 读
seurat_obj <- LoadH5Seurat("spatial_data.h5seurat")

# 看一眼结构
print(seurat_obj)
# 输出类似:An object of class Seurat
# 17399 features across 8560 samples within 1 assay
# Active assay: RNA (17399 features, 0 variable features)

这里有个坑:readRDS 读的是整个 R 对象,如果文件是旧版 Seurat,可能兼容性有问题。我个人经验是优先用 LoadH5Seurat,更稳定。

2.3 SPATA2 对象的读取

SPATA2 是专门做空间转录组的 R 包,格式跟 Seurat 不太一样。我记得第一次用这个包时,文档看半天才明白它的数据结构。

library(SPATA2)

# 从预处理的 rds 文件读
spata_obj <- readRDS("spata_object.rds")

# 或者从空间转录组原始输出构建
spata_obj <- createSPATAObject(
  counts = count_matrix,
  spatial = spatial_info_df,
  sample_name = "tumor_sample_001"
)

SPATA2 对象的核心是里面藏了个 SpatialData 类的结构,空间坐标、图像、表达数据都封装在一起。你想想看,这其实比 Seurat 更专一,就是为空间数据设计的。

2.4 格式间的转换

好了,三种格式都读进来了。接下来就是核心问题:怎么在它们之间互转?

从/到 H5AD Seurat SPATA2
H5AD → - SeuratDisk 包 SPATA2 转换函数
Seurat → SeuratDisk 包 - SeuratToSpata2()
SPATA2 → spata2_to_h5ad() spata2_to_seurat() -

具体到代码,我个人常用的一条转换链路是这样的:

H5AD → Seurat:

# Python 端先把 anndata 转成 h5seurat
import scanpy as sc
import anndata2ri

adata = sc.read_h5ad("input.h5ad")
# 保存为 h5seurat 格式
adata.write_h5ad("temp.h5ad")
# 再用 R 的 SeuratDisk 转换
# R 代码:
# library(SeuratDisk)
# Convert("temp.h5ad", dest = "h5seurat", overwrite = TRUE)

Seurat → H5AD:

# R 代码
library(SeuratDisk)
# 保存为 h5seurat
SaveH5Seurat(seurat_obj, filename = "temp.h5seurat")
# 再转成 h5ad
Convert("temp.h5seurat", dest = "h5ad")

Seurat → SPATA2:

# R 代码
library(SPATA2)
library(Seurat)

spata_obj <- SeuratToSpata2(
  object = seurat_obj,
  assay_name = "Spatial",
  sample_name = "sample1"
)
重要提醒:转换时最容易丢的是空间坐标和图像信息。我曾经在 Seurat 转 H5AD 时发现 obsm['spatial'] 是空的,排查了半天才发现是 Seurat 对象里坐标存在 tissue.positions 子表里,需要手动提取。建议每次转换后打印结构确认完整性。

2.5 知识框架图

为了让你看得更清楚,我把整个数据导入和格式转换的逻辑画成了这张图:

单细胞空间数据导入与格式转换流程图 原始数据 10X Visium 等 H5AD Scanpy / Python Seurat R语言 / Seurat包 SPATA2 R语言 / SPATA2包 统一结构 表达矩阵 空间坐标 元信息 核心思路:不管什么来源,最终统一为 表达矩阵 + 空间坐标 + 元信息 三元组

2.6 避坑总结

  • 坐标格式不一致:H5AD 里空间坐标是矩阵,Seurat 里可能是数据框,转换时要确认行列不对齐。我建议统一用矩阵格式保存。
  • 基因名冲突:不同数据集的基因标识符可能不一样(符号 vs ID),转换时记得用 var_names 一一对应。我曾经因为大小写不一致,排查了一整天。
  • 压缩格式问题:某些 H5AD 文件用了 LZF 或 GZIP 压缩,R 端可能读不了。用 scanpy.read_h5ad(..., backed='r') 可以查看压缩类型。
  • Seurat 版本兼容:v3 和 v4/v5 的结构有变化。我习惯装 SeuratDisk 的开发版本来处理新旧格式。
实用建议:建立一个“数据清洗流水线”。无论数据来源是什么,先统一转成 H5AD 格式作为中间存储,再分发到不同工具。这样能减少很多麻烦。

2.7 实战检查清单

每次读入新数据,我都会过一遍这个清单:

  1. 行/列对应:cells × genes 维度和文档一致
  2. 空间坐标:obsm['spatial'] 存在,且行列数匹配(n_cells × 2)
  3. 无缺失值:表达矩阵里不要有 NaN
  4. 基因名唯一:var_names 没有重复
  5. 元信息完整:obs 里有必要的 sample_id、cell_type 等列

做到这几点,后续分析会顺畅很多。记住,数据导入这一步花的时间,后面十倍的返工都省回来了。