2. 数据格式与预处理:Visium输出文件、质量控制与过滤

做空间转录组的第一步,就是搞清楚Visium平台到底给了我们什么。我最初接触这个平台时,对着三个文件看了半天,心想:这仨东西怎么拼一起?后来在项目里一遍遍折腾,才摸清它们的脾气。说白了,一套标准的Visium输出就三样:H5、TIFF、JSON。缺一不可,少一个你就没法往下走。

2.1 Visium输出的三大文件

下表把这三种文件的角色说清楚了。建议你打印出来贴在工位上,或者至少记住核心区别。

文件后缀核心内容用途
H5.h5基因表达矩阵(UMI计数)定量每个spot的基因表达
TIFF.tiff组织切片的高分辨率染色图像图像配准、组织识别、spot坐标映射
JSON.json空间坐标、组织检测结果、元数据定义每个spot的位置和组织归属

这三者的关系我画了一张图,看完你就能明白整个预处理链路。

Visium数据预处理流程 H5 表达矩阵 TIFF 图像 JSON 空间坐标 数据对齐与整合 质量控制(QC) 过滤低质量点 最终得到: 干净的表达矩阵 + 空间坐标

你看,数据从三个源头进来,先做对齐整合,再做质量控制,最后过滤掉那些“污染”或“low quality”的spot。这张图的流程我建议你用自己的数据跑一遍,体会会更深。

2.2 H5文件:表达矩阵的存储格式

H5是HDF5格式,说白了就是一个多层文件夹结构的二进制文件。你可以把它想象成一个压缩包,里面包含:基因名列表、barcode列表、以及一个稀疏的计数矩阵。

我用Python的h5py库打开过一个实际数据,结构大致是这样:

import h5py
with h5py.File("sample_filtered_feature_bc_matrix.h5", "r") as f:
    print(f.keys())
    # 输出: <KeysViewHDF5 ['matrix', 'features', 'barcodes']>
    print(f['matrix'].keys())
    # 输出: <KeysViewHDF5 ['barcodes', 'data', 'indices', 'indptr', 'shape']>
    print(f['matrix/shape'][:])
    # 输出: [36601  4775]  # 36601个基因,4775个spot

我记得第一次看这个结构时,被“indices”、“indptr”搞晕了。后来才明白,这就是标准的CSR稀疏矩阵存储。你不需要自己写解析代码,因为现成的工具(比如Scanpy的sc.read_10x_h5())已经帮你处理好了。但懂一点底层总没坏处。

实用技巧: 如果你用的Space Ranger输出,文件夹里通常还有一个subdir叫“filtered_feature_bc_matrix”,里面包含.mtx、.tsv等文件。H5只是其中一个打包版本。我个人习惯直接读H5,因为它更快,而且不会漏掉图像元数据。

2.3 TIFF图像:组织切片的高分辨率图像

TIFF文件里存的是组织切片的染色图——通常是H&E染色后的高分辨率扫描。这张图有多重要呢?它决定了你每个spot对应的组织位置是否准确。Space Runner在跑的时候,已经用图像做了alignment,把spot坐标和像素坐标对应起来了。

但你拿到TIFF后,可能还需要做两件事:一是检查图像方向,二是手动确认组织区域是否覆盖了所有spot。我遇到过好几次:在项目初期,我们怀疑某个spot的组织注释不对,后来发现是TIFF在读取时被自动旋转了90度。嗯,这里要注意——很多看图软件和Python的图像库对TIFF Orientation的解读不一样。

# 用tifffile读取,保持原始方向
import tifffile as tif
img = tif.imread("tissue_hires_image.tiff")
print(img.shape)  # (2048, 2049, 3) - 高度x宽度x通道

空间坐标和图像之间的转换,靠的是JSON里提供的“tissue_positions_list”和“scalefactors_json.json”。这些坐标是相对于图像像素的,但实际分析时会归一化到hm距离,所以别搞混单位。

2.4 JSON文件:空间坐标与元数据

JSON文件像是一个说明书。我随便打开一个样本的“spatial/tissue_positions_list.json”,内容是这样的:

{
  "barcode": "ACGGCTGAGTCTCGCT-1",
  "in_tissue": 1,
  "array_row": 0,
  "array_col": 0,
  "pxl_row_in_fullres": 2130,
  "pxl_col_in_fullres": 1500
}

这里in_tissue字段决定了一个spot是否落在组织区域内。有些spot可能离组织很远,根本没有捕获到细胞,它们理论上就不应该参与后续分析。不过,有时候组织边缘的spot也可能标记为1,但实际上只包含少量碎片——这就是需要QC的原因。

另一个关键文件是scalefactors_json.json,它提供了将像素坐标转换为um坐标的缩放因子:

{
  "spot_diameter_fullres": 89.56,
  "fullres": "hires",
  "tissue_hires_scalef": 0.251289,
  "fiducial_diameter_fullres": 190.0
}

我建议你先检查tissue_hires_scalef——如果它特别小,说明你的hires图像分辨率太低,可能看不清细节。那也没关系,低分辨率图像不影响表达矩阵计算。

2.5 数据质量控制(QC)

为什么需要QC?你想想看,一个spot的直径大约55微米,可以捕获1到10个细胞。但如果这个spot刚好沾上了坏死的组织碎片、或者被气泡污染,它的UMI计数和基因数就会异常。不剔除这些异常点,后面的聚类和差异分析结果就会飘。

常见的QC指标有三个:

  • 总UMI计数:过低的往往表示没有捕获到细胞,过高的可能是多重捕获。
  • 检测到的基因数:通常和总UMI计数正相关,但如果基因数少而UMI多,说明可能是光学噪声或非特异结合。
  • 线粒体基因占比:超过15%-20%通常表示细胞膜破损或组织坏死。

我记得在分析一个肿瘤样本时,发现一批spot的线粒体比例高达40%——后来跟病理确认,那个区域正好是坏死区。所以高线粒体比例不只是技术质量问题,也可能提示真实的生物学特征。但作为预处理,我们还是先把它剔除掉,再单独分析。

注意: 线粒体基因在不同物种中的前缀不一样。人用“MT-”,小鼠用“mt-”。如果你读的是其他物种,记得手动检查MITO基因列表。我吃过这个亏:有一次跑小鼠数据没改前缀,结果所有spot的线粒体比例都是0,差点漏掉了真正的坏点。

2.6 过滤低质量点

拿到QC指标后,就该动手筛掉那些糟糕的spot了。以Scanpy为例,做法是这样的:

import scanpy as sc
import pandas as pd

# 读取数据
adata = sc.read_10x_h5("filtered_feature_bc_matrix.h5")

# 计算QC指标
adata.var["mt"] = adata.var_names.str.startswith("MT-")  # 人
sc.pp.calculate_qc_metrics(adata, qc_vars=["mt"], percent_top=None, log1p=False)

# 过滤条件
sc.pp.filter_cells(adata, min_genes=200)
sc.pp.filter_cells(adata, max_genes=6000)
adata = adata[adata.obs.pct_counts_mt < 20, :]

这些阈值(min_genes=200, max_genes=6000, mt<20%)是比较常规的。但实际项目里我建议你画个violin plot看看分布再定:

sc.pl.violin(adata, ['n_genes_by_counts', 'total_counts', 'pct_counts_mt'],
             multi_panel=True)

目测一下异常峰的边界在哪里。比如有些样本总UMI计数普遍偏低,用200基因的阈值可能会把好多好的也都扔掉。反过来,如果基因数普遍高,6000上限也可能太紧。

我的习惯: 我会先设一个很宽松的阈值(比如 min_genes=100),过一遍后画图,再根据图上“秃头的尾巴”来调整。不需要追求一次到位。

2.7 常见避坑指南

  • 千万别把过滤后的数据覆盖原始文件 – 我建议保留一个raw层,或者至少留一个未过滤的备份。因为后续分析可能需要回溯。
  • 检查JSON里的“in_tissue”变量 – 如果这个标志为0,表示该spot不在组织上,但在某些数据集里它也可能包含了背景信号。我一般只保留in_tissue==1的spot,再进行QC。
  • 注意TIFF和H5的barcode顺序是否匹配 – 虽然Space Ranger保证它们是对齐的,但如果你自己手动修改过文件,就可能错位。保险做法是用JSON里的barcode列表去重排H5的列序。
  • 线粒体占比的阈值不要死套20% – 对于某些代谢旺盛的组织(如肝脏),正常细胞线粒体基因占比就可能超过20%。此时需要结合图像看组织学形态来定。

做完这些,你手里就有了一份干净的空间表达数据:每个spot有可靠的UMI计数和明确的组织位置。接下来可以放心做降维、聚类和美哭了的花花图了——不过那是后面章节的事了。