4. 数据读取

拿到Visium的原始数据之后,第一件事就是把它们拉进Python环境。

我个人习惯用scanpy,它封装好了Visium数据的专用读取器,一行代码就能把空间转录组的三个核心文件——组织切片图像、斑点坐标、基因表达矩阵——打包成一个对象。这个对象叫AnnData,是单细胞和空间转录组数据的事实标准。

嗯,AnnData长得有点像Pandas DataFrame套了个壳,但多了很多专用槽位。今天我们就把它拆开看个明白。

4.1 从原始目录读取Visium数据

Space Ranger输出的是一个标准文件夹,包含三个文件:

  • filtered_feature_bc_matrix.h5(基因表达矩阵)
  • spatial/tissue_positions_list.csv(斑点坐标)
  • spatial/tissue_hires_image.png(高分辨率组织图像)

Scanpy的read_visium函数能自动识别并加载它们。

<code>import scanpy as sc
adata = sc.read_visium("path/to/Visium_output/outs/")
print(adata)

输出类似这样:

<code>AnnData object with n_obs × n_var = 4992 × 36601
    obs: 'in_tissue', 'array_row', 'array_col'
    var: 'gene_ids', 'feature_types', 'genome'
    uns: 'spatial', 'hires_image', 'lowres_image'
    obsm: 'spatial'

看到 obs、var、uns、obsm 这些字段没有?它们就是 AnnData 的核心骨架。

提醒:如果遇到文件路径包含中文或空格,Scanpy 可能报错。我建议所有项目路径都用纯英文、下划线命名。

4.2 AnnData对象结构:一张图讲清楚

AnnData 就像一个有五个抽屉的柜子。每个抽屉放不同类型的数据。下图是我画的简化结构:

AnnData 对象核心结构 AnnData obs 样本/斑点元数据 var 基因元数据 X 表达矩阵(主数组) obsm 降维坐标/空间坐标 (每个观测一个向量) varm 基因级附加矩阵 uns 非结构化元数据 组织图像、聚类颜色、 差异结果、参数日志 layers 规范化/对数化备份 说明:箭头表示数据从主对象流向子槽位

4.3 逐个拆解AnnData的“抽屉”

下面我用一个表格帮你快速对照每个字段的用途:

字段中文名存储内容形状/类型典型例子
adata.obs观测元数据每个斑点的属性n_obs × m(DataFrame)in_tissue, total_counts, Pct_mito
adata.var特征(基因)元数据每个基因的属性n_var × k(DataFrame)gene_ids, mt(是否线粒体)
adata.X表达矩阵归一化或原始UMI计数n_obs × n_var(稀疏矩阵)多为scipy.sparse.csr_matrix
adata.obsm观测级多维矩阵每个观测对应一个向量(降维/坐标)n_obs × d(数组/DataFrame)'spatial'(XY坐标)、'X_pca'(PCA嵌入)
adata.varm特征级多维矩阵每个基因对应一个向量n_var × d(数组)用于存储基因贡献度等
adata.uns非结构化数据任意Python对象(字典)dict组织图像数组、聚类颜色、差异分析结果
adata.layers额外表达层同一形状的不同变换版本dict of sparse matriceslog1p层、scaled层

你看,.obs 和 .var 用的是 Pandas DataFrame,可以像操作表格一样筛选、添加列。我一个项目里,需要在 .obs 里加一列“区域编号”,直接写 adata.obs['region'] = ['A1', 'A2', ...] 就搞定。

4.4 实际操作:读取后检查内容

假设你已经用 sc.read_visium 载入了数据,接下来我习惯先看三样东西:

<code># 1. 看obs的前几行
print(adata.obs.head())

# 2. 看var的前几行(一般只显示基因名)
print(adata.var.head())

# 3. 看uns的keys
print(adata.uns.keys())

输出类似:

<code>           in_tissue  array_row  array_col
barcode_1       True         0         0
barcode_2       True         0         1
...

           gene_ids feature_types  genome
gene_A    ENSG...    Gene Expression  GRCh38
...

dict_keys(['spatial', 'hires_image', 'lowres_image', 'spatial_neighbors'])

看到 uns['spatial'] 里存的就是图像缩放信息,obsm['spatial'] 里是每个斑点的像素坐标(Y轴已经翻转好,可以直接用)。

注意:Visium数据默认扫描方向导致Y坐标上下颠倒,但Scanpy的read_visium已经帮你翻转了。adata.obsm['spatial'][:, 1] 就是图像Y坐标,无需额外处理。

4.5 我个人避坑经验

有一次我帮同事调试,他读完数据后adata里面obs是空的,怎么看都少列。排查半天发现是他拷贝数据时把spatial/子目录漏了。read_visium依赖这个子目录里的CSV文件。缺了这个,obs里就只有默认索引。

另外,如果内存不太宽裕,建议读取后马上检查adata.X的稀疏矩阵类型。默认是csr_matrix,占内存小。但如果你误用了.toarray(),分分钟爆内存。我遇到过一个52G内存的服务器,toarray()后直接OOM。

总结:AnnData的每一个槽位都有明确用途。掌握obs(斑点标签)、obsm(坐标)、uns(图像)这三个,你就可以开始做空间可视化。剩下的随着分析过程会自然填满。