3、数据读取与可视化:Scanpy/Seurat 实战

好了,到了真正动手的环节了。前面我们聊了Visium的数据结构,明白了counts、image、spatial坐标这三件套。现在要做的,就是把磁盘上那一堆文件,变成我们能看、能分析的活数据。

说白了,第一步就是把数据「请」进内存。我个人习惯在Python里用Scanpy,在R里用Seurat。两套工具我都用过,各有顺手的地方。今天咱们就都过一遍,你看看哪个更合自己的口味。

3.1 环境准备与数据「三件套」

运行前,先确认你机器上装好了核心库。我遇到过几次因为环境版本不对,折腾半天的情况,所以建议用虚拟环境隔离。

# Python 环境 (Scanpy + Squidpy)
pip install scanpy[leiden] squidpy anndata

# R 环境 (Seurat)
install.packages("Seurat")
remotes::install_github("satijalab/seurat-data")

拿到手的Space Ranger输出目录,里面至少有这三个东西:

  • filtered_feature_bc_matrix.h5 — 表达矩阵,HDF5格式
  • spatial/tissue_positions_list.csv — 每个barcode的物理坐标 (如row, col)
  • spatial/tissue_hires_image.png — 组织切片高清图

避坑提醒:如果目录里只有 raw_feature_bc_matrix.h5,记得换用 filtered 版,否则你后面看到的点图里全是空白背景的点,根本看不清组织轮廓。

3.2 Scanpy 读取与基础对象构建

我用得最多的还是 Scanpy。它把数据读取和预处理串成了一条线,很顺手。最省事的方式,是用 Squidpy 的 sq.read.visium() 函数。它会自动去找那三件套,帮你拼好。我当年第一次用的时候,感觉像开盲盒,一步到位。

import scanpy as sc
import squidpy as sq

# 读取 Visium 输出目录
adata = sq.read.visium(path="./data/Visium_FFPE_Human_Breast_Cancer",
                       library_id="V1_Breast_Cancer_Block_A",
                       source_image_path="./data/Visium_FFPE_Human_Breast_Cancer/spatial")

# 看一眼对象长什么样
print(adata)
print(adata.obs.head())   # 元数据
print(adata.obsm.keys())  # 存储了 'spatial' 坐标矩阵

嗯,这里要注意:sq.read.visium 返回的 AnnData 对象里,最关键的东西是 adata.obsm['spatial']。它是一个 N×2 的 float 数组,存的是每个 spot 在原始高分辨率图像上的像素坐标 (x, y) —— 单位是像素,不是微米。

如果你手头只有 h5 文件,也可以手动构建:

import pandas as pd
import anndata

counts = sc.read_10x_h5("./data/filtered_feature_bc_matrix.h5")
coords = pd.read_csv("./data/spatial/tissue_positions_list.csv",
                     header=None, index_col=0)
coords.columns = ['in_tissue', 'row', 'col', 'pxl_row_in_fullres', 'pxl_col_in_fullres']

# 过滤掉组织外的 barcode
adata = counts[coords.index, :]
adata.obsm['spatial'] = coords.loc[:, ['pxl_col_in_fullres', 'pxl_row_in_fullres']].values
adata.obs['in_tissue'] = coords['in_tissue'].values

这种手动方式的好处是,你能清楚每一步在做什么,排查问题的时候心里有底。

3.3 Seurat 读取与对象构建

如果你更习惯 R 生态,Seurat 4.0 以上版本对 Visium 的支持已经很好了。我当年在项目里迁移数据时,发现两套工具读取的坐标顺序略有不同,所以建议养成先画散点图确认坐标方向的好习惯。

library(Seurat)
library(SeuratData)

# 直接从 10x 输出读取
seurat_obj <- Load10X_Spatial(
  data.dir = "./data/Visium_FFPE_Human_Breast_Cancer",
  filename = "filtered_feature_bc_matrix.h5",
  assay = "Spatial",
  slice = "slice1",
  image = "image1"
)

# 查看核心数据
seurat_obj[["Spatial"]]   # 表达矩阵
seurat_obj[["slice1"]]   # 图像与坐标信息

读取之后,Seurat 会把坐标放在 GetTissueCoordinates(seurat_obj) 里,包含 imagecol 和 imagerow 两列。注意,它的坐标原点在左上角,y轴向下——这一点和 Python 里默认的坐标系方向是相反的。画图的时候通常看不出来,但如果你要叠加自定义图形,就得考虑这个差异。

3.4 核心概念对比表

概念 Scanpy (Python) Seurat (R)
核心对象 AnnData SeuratObject
坐标存储 adata.obsm['spatial'] GetTissueCoordinates(x)
坐标单位 像素 (pxl_col_in_fullres) 像素 (imagecol)
图像存储 adata.uns['spatial'][library_id]['images']['hires'] x@images$slice1@image
读取函数 sq.read.visium() Load10X_Spatial()

3.5 生成第一张空间点图

数据读进来了,下一步当然是看它长什么样。我个人觉得,空间转录组学最迷人的一刻,就是看到那些彩色的点落在组织切片上——你一下子就知道哪个区域表达高、哪个区域低。

Scanpy 版本

sc.pl.spatial(adata,
              img_key="hires",
              color="n_genes_by_counts",
              spot_size=0.6,
              show=True)

这个 color 参数可以换成任意 obs 或 var 里的列名。比如你想看某个基因的表达:

sc.pl.spatial(adata, color="EGFR", spot_size=0.6, ncols=2)

Seurat 版本

SpatialDimPlot(seurat_obj, pt.size.factor = 1.2)
SpatialFeaturePlot(seurat_obj, features = "EGFR", pt.size.factor = 1.2)

两个函数出来的图,视觉上很接近。但细看你会发现:Scanpy默认的点颜色更亮一些,Seurat则偏柔和。这不是数据问题,是各自默认配色方案的原因。

常见坑:如果出现全灰图,先检查 spot_size 是否太大导致重叠,或者 img_key 是否写成了小写 "hires"。我曾经在区分 hires 和 lowres 时搞混过,结果图上的点全部偏移了30像素。

3.6 基础知识体系

下面这张SVG图,把读取到可视化的核心流程串起来了。你可以把它当作本章的路线图。

数据读取与可视化知识体系 Space Ranger 输出目录 表达矩阵 (h5/mtx) 空间坐标 (csv) 组织图像 (png) Scanpy + Squidpy 读取 Seurat + SeuratData 读取 空间点图 / 基因表达可视化

3.7 可视化进阶:控制与微调

默认出图虽然能用,但离「发表级」还有距离。我一般会做以下几件事:

  1. 调整点大小:spot_size 参数控制单个点直径,单位是像素。一般建议 0.5–0.8,太大遮住组织轮廓,太小看不清表达区域。
  2. 修改配色:Scanpy 默认用 viridis (连续) 和 tab10 (离散)。我习惯改成 magma 或自己定义的蓝色渐变,更有质感。
  3. 叠加分组信息:把聚类结果或病理注释传进 color 参数,就能看到区域划分。
# Scanpy 微调示例
sc.pl.spatial(adata,
              img_key="hires",
              color=["cluster", "n_genes_by_counts"],
              palette=["#e74c3c", "#3498db", "#2ecc71", "#f39c12"],
              spot_size=0.6,
              ncols=2,
              frameon=False,
              show=True)
# Seurat 微调示例
SpatialDimPlot(seurat_obj,
               group.by = "seurat_clusters",
               cols = c("#e74c3c", "#3498db", "#2ecc71", "#f39c12"),
               pt.size.factor = 1.2,
               image.alpha = 0.3)

实用技巧:如果点图与组织图像对不上位置,先检查坐标单位是否一致。Scanpy 默认用全分辨率的 pxl_col_in_fullres,而图像路径里的 tissue_hires_image.png 也是全分辨率。一旦匹配上了,点就能完美落到切片上。

3.8 常见问题与避坑

  • 读取报错 "No such file":检查目录结构是不是 Space Ranger 的标准格式。我碰到过有人自己手动改了文件夹名,导致 sq.read.visium() 找不到 spatial 子目录。
  • 图像显示为空白:确认图像文件是不是损坏的。用 PIL.Image.open 或 magick 命令行先看一眼。
  • 坐标偏移明显:检查是否用错了 lowres 图像(600px左右)。

你想想看,只要上面这几点操作顺利,你就已经拿到了空间转录组分析的第一张地图。后面所有的聚类、差异表达、区域注释,都建立在这张图上。