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 冲突了,折腾了两小时才解决。后来我学乖了,新建环境再装,啥事没有。

注意 Python 版本:Scanpy 1.9+ 建议用 Python 3.8–3.11。Python 3.12 的某些依赖还没完全跟上,我身边有人强行上 3.12 结果报错,最后还是退回了 3.10。

如果你用 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 步。大多数单细胞项目都逃不出这个框架。

  1. 读取数据 — sc.read() 或 sc.read_10x_mtx()
  2. 质量控制 — sc.pp.filter_cells()、sc.pp.filter_genes()、sc.pp.calculate_qc_metrics()
  3. 归一化 — sc.pp.normalize_total() + sc.pp.log1p()
  4. 高变基因筛选 — sc.pp.highly_variable_genes()
  5. 降维 — sc.tl.pca()、sc.pp.neighbors()、sc.tl.umap()
  6. 聚类 — sc.tl.leiden()
  7. 可视化与注释 — 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 的高变基因筛选很关键——它决定了后续降维和聚类的质量。我一般选 2000–5000 个基因,再多的话计算量上去了,但收益并不明显。

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 步流程对应起来,你看完应该能更直观地理解每一步在操作什么。

Scanpy 分析流程与 AnnData 结构对应 Scanpy 分析流程与 AnnData 结构对应图 1. 读取数据 sc.read() / read_10x 2. 质量控制 filter_cells / filter_genes 3. 归一化 + log normalize_total / log1p 4. 高变基因筛选 highly_variable_genes 5. 降维 PCA → neighbors → UMAP 6. 聚类 sc.tl.leiden() 7. 可视化与注释 sc.pl.umap() / dotplot() AnnData 核心结构 .X 表达矩阵 .obs 细胞元数据 .var 基因元数据 .obsm 降维坐标 .uns 非结构化数据 .varm 基因载荷 AnnData 将表达矩阵、细胞/基因元数据、 降维结果、聚类标签等统一管理。 每一步流程都会更新对应的 .X / .obs / .obsm 写入 .X 更新 .var['highly_variable'] 写入 .obsm['X_pca'] 写入 .obs['leiden']

4.6 给你的个人建议

如果你刚接触 Scanpy,我建议你从一个小数据集开始跑一遍完整流程。比如 10x 的 pbmc3k 数据集,网上到处都能下。不要一上来就上百 GB 的数据,容易出问题还不好排查。

  • 跑流程时,每做完一步就 print(adata) 看一下对象形状,确保你的操作生效了。
  • 把关键中间结果存成 .h5ad 文件,这样万一后面跑挂了,不用从头再来。
  • 多看看 sc.pl 系列的可视化函数,Scanpy 的图表比 Seurat 更灵活,但默认配色有时候需要手动调一下。
一个小技巧:Scanpy 的 sc.settings.verbosity 参数可以控制日志输出的详细程度。调试时设成 3,运行时设成 1,既能看到关键信息,又不会被刷屏。

嗯,关于入门就聊这么多。Scanpy 的学习曲线其实比 Seurat 更平缓——你只要记住 AnnData 这个核心结构,剩下的都是围绕着它在转。拿起一个小数据,照着上面的代码敲一遍,你就会发现其实没那么复杂。