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图,把读取到可视化的核心流程串起来了。你可以把它当作本章的路线图。
3.7 可视化进阶:控制与微调
默认出图虽然能用,但离「发表级」还有距离。我一般会做以下几件事:
- 调整点大小:
spot_size参数控制单个点直径,单位是像素。一般建议 0.5–0.8,太大遮住组织轮廓,太小看不清表达区域。 - 修改配色:Scanpy 默认用
viridis(连续) 和tab10(离散)。我习惯改成magma或自己定义的蓝色渐变,更有质感。 - 叠加分组信息:把聚类结果或病理注释传进
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左右)。
你想想看,只要上面这几点操作顺利,你就已经拿到了空间转录组分析的第一张地图。后面所有的聚类、差异表达、区域注释,都建立在这张图上。