第4章 数据读取:scanpy.read_visium 与 scanpy.read_h5ad 详解

数据读取,是整个分析流程的起点。你拿到10x Visium的空间转录组数据,可能是从官方Space Ranger输出的文件夹,也可能是别人已经处理好的h5ad文件。怎么把它们正确读进来?这里面坑不少。

我个人最早做空间转录组时,就栽在路径和文件格式上。读进去的数据要么是空的,要么坐标全乱掉。后来才明白,scanpy.read_visium 和 scanpy.read_h5ad 这两个函数,用好了能省大量时间,用错了就是灾难。

这一章,咱们把这两个函数拆开揉碎,从参数、行为到避坑,一次讲清楚。

4.1 scanpy.read_visium:从Space Ranger输出目录加载空间数据

你手头如果是10x Visium原始的测序数据,通常会先跑Space Ranger流程,输出一个包含以下文件的目录:

  • filtered_feature_bc_matrix.h5(或 filtered_feature_bc_matrix 文件夹)
  • spatial/tissue_positions_list.csv
  • spatial/scale_factors.json
  • spatial/tissue_hires_image.png(高分辨率组织图像)
  • spatial/tissue_lowres_image.png
  • spatial/detected_tissue_image.jpg

read_visium 就是专门为这种结构设计的。它一次性把基因表达矩阵、空间坐标、组织图像都读进来,打包成一个 AnnData 对象。

4.1.1 基本用法

import scanpy as sc

adata = sc.read_visium(
    "path/to/SpaceRanger/output",
    genefile="filtered_feature_bc_matrix.h5",
    library_id="sample1",
    source_image_path=None,
    load_images=True
)

解释一下关键参数:

参数 说明 我的习惯
path Space Ranger输出目录的路径 建议用绝对路径,省得找半天
genefile h5文件路径,可以是“filtered_feature_bc_matrix.h5”或“raw_feature_bc_matrix.h5” 我一般用 filtered,除非你想看空白背景的全局表达
library_id 样本ID,用于标识该样本的切片 同一个项目里用统一命名,比如 donor1_region1
source_image_path 手动指定高分辨率图像路径,不指定会自动找 很少用,除非你手动重命名了图像文件
load_images 是否加载图像数据到AnnData 默认True。如果只做表达分析,可以False加快加载

返回值是一个 AnnData 对象。obsm 里包含了 spatial 坐标,uns 里存了图像和缩放因子。

4.1.2 关键避坑点

我曾经遇到过:Space Ranger输出的 spatial 目录下,如果没有 tissue_lowres_image.png,read_visium 会报错,但又不告诉你缺哪个文件。后来我检查文件清单,才发现是手动删除图片后没更新。

所以,如果你的数据读不进去,先确认 spatial 文件夹里都有什么文件。

如果你拿到的是 filtered_feature_bc_matrix 文件夹(三个文件:matrix.mtx, barcodes.tsv, features.tsv),read_visium 也支持!只需把 genefile 参数设置成文件夹路径,它会自动识别。

4.2 scanpy.read_h5ad:通用h5ad格式读取

h5ad是Scanpy原生的数据持久化格式。你可以把之前保存的 AnnData 对象直接读回来,比如:

adata = sc.read_h5ad("sample1.h5ad")

简单到离谱,对吧?但这里也有几个细节需要注意。

4.2.1 参数一览

参数 作用 说人话版
filename 文件路径 唯一必填项
backed 是否以“后援”模式打开(只读,不加载全部到内存) 超大文件时用,内存省一半
cache 是否使用缓存加速 我一般True,第二次打开飞快
gcs_url Google Cloud Storage路径(很少用) 跳过

关键区别:read_visium 是“组装”函数,它从多个文件里提取信息再合并;read_h5ad 是“反序列化”函数,直接恢复之前保存的整个对象。

4.2.2 版本兼容性问题

你问我哪个问题最常见?h5ad版本不兼容!比如你用Scanpy 1.8保存的h5ad,用Scanpy 1.9可能读不了。为什么会这样?因为h5ad底层用的是HDF5,而anndata的schema会随着版本更新变化。

解决方案:

  • 保存时指定一个稳定的版本:adata.write("file.h5ad", compression="gzip")
  • 更新Scanpy后,如果读旧文件报错,试试 anndata.read_h5ad("file.h5ad") 而不是 scanpy.read_h5ad
  • 实在不行,用 h5py 手动解包然后重新写成h5ad(这个我踩过两次坑)

我曾经在合作项目中,对方用Scanpy 1.9.0,我用1.8.2,他给我的h5ad我死活读不了。最后我用 anndata.read_h5ad("file.h5ad", backed="r") 才勉强读出来。

所以,团队协作时,最好统一anndata和scanpy版本。

4.3 两者的典型使用场景比较

你想想看,什么时候该用哪个?

场景 推荐函数 原因
10x Visium原始Space Ranger输出 read_visium 自动解析文件夹结构,加载图像和坐标
别人处理好的AnnData read_h5ad 直接恢复全部结果,包括归一化、降维等
自己保存的中间结果 read_h5ad 速度快,占用空间小(压缩)
想自己构造空间坐标矩阵 都不合适 需要用 sc.pp.read_visium 底层函数或手动构造

4.4 数据读取的常见流程

下面这张图,把从原始文件到 AnnData 对象的路径画了出来。我建议你把它保存下来,以后遇到数据读取问题时,看一眼就明白走哪条路。

空间转录组数据读取流程 Space Ranger输出目录 filtered_feature_bc_matrix.h5 spatial/ (图像+坐标+缩放) scanpy.read_visium() 自动组装AnnData AnnData .X .obsm .uns 已有的 .h5ad 文件 其他分析流程的输出 scanpy.read_h5ad() 直接反序列化 AnnData 恢复全部内容 注意: read_visium需要完整的Space Ranger输出结构;read_h5ad要求h5ad文件版本与当前anndata兼容。 建议先print(adata)确认数据完整性。

4.5 实战:用read_visium加载一份真实的10x Visium数据

假设你下载了10x官网的“Human Breast Cancer”示例数据。解压后目录结构大概是这样:

Breast_Cancer_FFPE/
├── filtered_feature_bc_matrix.h5
├── spatial/
│   ├── tissue_positions_list.csv
│   ├── scale_factors.json
│   ├── tissue_hires_image.png
│   ├── tissue_lowres_image.png
│   └── detected_tissue_image.jpg
└── raw_feature_bc_matrix.h5 (可选)

在Python中这样加载:

import scanpy as sc

adata = sc.read_visium("Breast_Cancer_FFPE/",
                       genefile="filtered_feature_bc_matrix.h5",
                       library_id="breast_cancer1")

print(adata)
# AnnData object with n_obs × n_vars = 3657 × 19135
#     obs: 'in_tissue', 'array_row', 'array_col'
#     uns: 'spatial'
#     obsm: 'spatial'

看到 obsm['spatial'] 和 uns['spatial'] 了吗?这就是空间坐标和图像信息。我建议你加载完后,先打印一下这两个属性确认坐标值是否合理。

个人习惯:加载完成后立刻执行 sc.pl.spatial(adata, img_key="hires") 画一张图,看看成像和点阵有没有对齐。如果不对齐,很可能 scale_factors.json 里的缩放因子写错了,或者图像文件名不对。

4.6 数据读取后的快速检查清单

  1. 检查 adata.X 的形状:行数一般就是spot数,列数是基因数。常见Visium V1有~4000~5000个spot,V2有~5000~6000。
  2. 检查 adata.obsm['spatial'] 的列名:通常是 ["pixel_x", "pixel_y"],在组织图像上的像素坐标。不是阵列坐标(array_row, array_col 在 adata.obs 里)。
  3. 检查 adata.uns['spatial'][library_id]['scalefactors'] 里是否有 tissue_hires_scalef 等字段。
  4. 如果使用 read_h5ad,打印 adata 时注意是否有 obsm 和 uns,确保之前保存时没有丢失关键信息。

最后,说一个我踩过的大坑:如果 read_visium 报错 "FileNotFoundError: No such file or directory: 'tissue_lowres_image.png'",别慌。看看 spatial 文件夹里是不是只有 tissue_hires_image.png 而没有低分辨率图?10x官方有些示例数据只包含高分辨率图。解决办法:在 read_visium 里加上 source_image_path="spatial/tissue_hires_image.png" 强制指定高分辨率图。

嗯,读取这一步搞定,后面的分析才能站得住。记住:数据读不对,后面所有分析都是白费功夫。多用 print() 和 sc.pl.spatial() 来验证,别信自己的直觉。