4. Scanpy 入门:安装、主要数据结构、基本流程
好,我们正式开始 Scanpy 了。说实话,很多做单细胞的人一开始都被 Seurat 带入门,但一旦数据量上来——比如超过 10 万细胞——R 那个内存就开始吃紧。这时候 Scanpy 就是一个很实在的替代方案。我最早接触 Scanpy 是在处理一个 30 万细胞的骨髓数据时,Seurat 跑到一半直接崩了,换了 Scanpy 半小时搞定。从那以后,我就再也没小看过 Python 生态。
4.1 安装 Scanpy:几种方式,一个原则
安装本身不复杂,但我踩过不少坑。先给你看最稳的方式:
# 推荐用 conda 创建独立环境
conda create -n scanpy_env python=3.9
conda activate scanpy_env
# 核心安装
pip install scanpy
# 如果要用到更多工具
pip install scanpy[leiden]
我个人习惯用 conda 管理环境,因为 Python 的包依赖有时候会很折腾。有一次我在公司服务器上直接用 pip install scanpy,结果跟系统级的 NumPy 冲突了,折腾了两小时才解决。后来我学乖了,新建环境再装,啥事没有。
如果你用 Apple Silicon 芯片的 Mac,安装完最好验证一下:
python -c "import scanpy as sc; print(sc.__version__)"
能正常输出版本号,就算成了。
4.2 核心数据结构:AnnData
Scanpy 的核心数据对象叫 AnnData,全称是 "Annotated Data"。说白了,就是一个能同时存表达矩阵、细胞元数据、基因元数据的大容器。
我第一次接触 AnnData 时觉得很像 Seurat 的 SeuratObject,但更轻量,也更贴近 Python 的 DataFrame 思维。
| 属性 | 维度 | 存储内容 | 类比 Seurat |
|---|---|---|---|
.X |
细胞 × 基因 | 表达矩阵(稀疏/稠密) | GetAssayData() |
.obs |
细胞 × 属性 | 细胞级元数据(样本、cluster 等) | |
.var |
基因 × 属性 | 基因级元数据(HVG、symbol 等) | rownames |
.obsm |
细胞 × 降维坐标 | PCA、UMAP、tsne 等嵌入 | Embeddings() |
.varm |
基因 × 降维载荷 | PCA 载荷等 | Loadings() |
.uns |
无固定形状 | 非结构化数据(颜色、参数等) | |
你想想看,一个 AnnData 对象就把所有东西都串起来了。我经常在项目里一次性加载多个样本,然后合并成一个 AnnData,后续所有操作都围绕这一个对象,非常清晰。
AnnData 对象取名时,都会带上样本数或条件,比如 adata_15k_ctrl。这样在 Notebook 里来回切换时,不容易搞混。
4.3 基本分析流程:7 步走
Scanpy 的标准流程其实很固定,我归纳成 7 步。大多数单细胞项目都逃不出这个框架。
- 读取数据 —
sc.read()或sc.read_10x_mtx() - 质量控制 —
sc.pp.filter_cells()、sc.pp.filter_genes()、sc.pp.calculate_qc_metrics() - 归一化 —
sc.pp.normalize_total()+sc.pp.log1p() - 高变基因筛选 —
sc.pp.highly_variable_genes() - 降维 —
sc.tl.pca()、sc.pp.neighbors()、sc.tl.umap() - 聚类 —
sc.tl.leiden() - 可视化与注释 —
sc.pl.umap()、sc.pl.dotplot()等
我记得第一次跑完整流程时,最让我惊讶的是 sc.tl.leiden() 的速度——10 万细胞,十几秒就出结果了。对比 Seurat 的 FindClusters(),确实快不少。
下面是一个典型的最小化工作流:
import scanpy as sc
# 1. 读取
adata = sc.read_10x_mtx("filtered_feature_bc_matrix/")
# 2. QC
sc.pp.filter_cells(adata, min_genes=200)
sc.pp.filter_genes(adata, min_cells=3)
# 3. 归一化
sc.pp.normalize_total(adata, target_sum=1e4)
sc.pp.log1p(adata)
# 4. 高变基因
sc.pp.highly_variable_genes(adata, n_top_genes=2000)
adata = adata[:, adata.var.highly_variable]
# 5. 降维
sc.tl.pca(adata)
sc.pp.neighbors(adata)
sc.tl.umap(adata)
# 6. 聚类
sc.tl.leiden(adata)
# 7. 可视化
sc.pl.umap(adata, color="leiden")
这段代码我在项目里改过无数次了,基本上每次新项目都是从这里开始的。你可以直接复制用,但注意数据路径要换成你自己的。
4.4 避坑指南:我踩过的几个雷
做了几年单细胞分析,有些坑我帮你们提前踩过了。
- 读取数据时注意稀疏格式。Scanpy 默认用稀疏矩阵存
.X,但如果你的数据是稠密的,比如已标准化过的矩阵,记得用dtype参数控制,不然内存会爆。 - 不要反复执行
.raw覆盖。很多新手在归一化后直接覆盖原始数据,结果想回溯时发现回不去了。我建议在归一化后设置adata.raw = adata,保留一份原始表达。 - 小心
.obs的索引对齐。AnnData 的.obs和.X是通过行名对齐的。如果你手动修改了.obs的 index,一定要确保跟.X一致。我曾经因为重置了 index 导致后续全部错位,查了两天才发现。 - 高变基因筛选后别忘记子集化。很多人调完
highly_variable_genes就直接跑 PCA,结果发现 PCA 还是用所有基因算的。记得加上adata = adata[:, adata.var.highly_variable]。
sc.pp.log1p() 之前没有先做 normalize_total。直接对原始 count 做 log 转换会导致不同细胞之间的表达量不可比。顺序一定要先归一化,再 log。
4.5 核心流程与数据结构的关系图
下面这张图把 AnnData 的结构和 7 步流程对应起来,你看完应该能更直观地理解每一步在操作什么。
4.6 给你的个人建议
如果你刚接触 Scanpy,我建议你从一个小数据集开始跑一遍完整流程。比如 10x 的 pbmc3k 数据集,网上到处都能下。不要一上来就上百 GB 的数据,容易出问题还不好排查。
- 跑流程时,每做完一步就
print(adata)看一下对象形状,确保你的操作生效了。 - 把关键中间结果存成
.h5ad文件,这样万一后面跑挂了,不用从头再来。 - 多看看
sc.pl系列的可视化函数,Scanpy 的图表比 Seurat 更灵活,但默认配色有时候需要手动调一下。
sc.settings.verbosity 参数可以控制日志输出的详细程度。调试时设成 3,运行时设成 1,既能看到关键信息,又不会被刷屏。
嗯,关于入门就聊这么多。Scanpy 的学习曲线其实比 Seurat 更平缓——你只要记住 AnnData 这个核心结构,剩下的都是围绕着它在转。拿起一个小数据,照着上面的代码敲一遍,你就会发现其实没那么复杂。