4. Scanpy 安装与核心对象:AnnData 结构

好,我们正式开始接触工具链。这一章要做两件事:先把 Scanpy 环境装好,再搞明白它最核心的数据结构——AnnData。说白了,你后面所有分析、所有表格、所有图表,全是在跟这个对象打交道。

我个人的习惯是:先搞清数据结构再上手跑代码。不然光会敲命令,一出错连日志都看不懂。咱们一步一步来。

4.1 安装 Scanpy 及其依赖库

Scanpy 依赖的库不少,但核心就这几样:numpy、pandas、scipy、matplotlib,以及专门处理单细胞数据的 anndata 包。我建议优先用 pip 安装,如果网络不好就用 conda,但注意 conda 上的版本可能滞后一两个小版本。

我的习惯:新建一个干净的虚拟环境,Python 3.9 或 3.10 都行。我以前在 3.11 上踩过一些第三方依赖的坑,所以现在老老实实 3.10。

打开终端,依次执行:

# 创建虚拟环境(可选,但我强烈推荐)
python -m venv scanpy_env
source scanpy_env/bin/activate  # Windows 下用 scanpy_env\Scripts\activate

# 核心依赖
pip install numpy pandas scipy matplotlib

# Scanpy 主包
pip install scanpy

# 如果你之后要用 Harmony 校正,先装上 harmony-pytorch
pip install harmonypy

安装完成后,快速验证一下:

python -c "import scanpy as sc; print(sc.__version__)"

只要能打印出版本号,比如 1.9.6,说明环境通了。注意,Harmony 的 Python 实现叫 harmonypy,后面我们会详细用。

我曾经在一台旧服务器上装了半天一直报依赖冲突,后来发现是系统里的 setuptools 版本太老。解决办法:先升级 pip 和 setuptools——

pip install --upgrade pip setuptools wheel

这个坑我踩过两次,才记住。

4.2 AnnData 到底是什么?

AnnData 这个名字来自 “Annotated Data”——带注释的数据。它把表达矩阵、细胞元数据、基因元数据、降维结果、聚类标签等等,全部塞进一个对象的各个属性里。你想想看,一个单细胞数据集动不动几万个细胞两万个基因,你要是自己用 pandas 分开存储,很容易搞混索引、搞丢对应关系。AnnData 就是来解决这个问题的。

它的核心结构可以这么理解:

属性 类型 存储内容
X 稠密/稀疏矩阵 表达值矩阵(细胞 × 基因)
obs DataFrame 细胞级别的元数据(样本名、批次、细胞类型等)
var DataFrame 基因级别的元数据(基因名、染色体、质控标志等)
obsm 字典 of array-like 细胞的多维降维结果(如 PCA、UMAP 坐标)
varm 字典 of array-like 基因的多维降维结果(如基因载荷)
uns 字典 任意非结构化的元数据(配色方案、聚类参数、marker 基因列表等)

我个人做项目时,最喜欢把预处理规则、分析参数甚至版本号都记在 uns 里。这样后期复现时,直接看 adata.uns 就能知道当时是怎么跑的——省事又靠谱。

4.3 亲手创建一个 AnnData

我们不必一开始就加载十几万细胞的大数据,先用小例子摸清结构。下面这段代码会生成一个假表达矩阵,然后用它创建 AnnData:

import numpy as np
import pandas as pd
import scanpy as sc

# 生成一个 5 个细胞 × 3 个基因的随机表达矩阵
expr = np.random.randint(0, 20, (5, 3)).astype(float)

# 为细胞和基因命名
cell_names = ["cell_A", "cell_B", "cell_C", "cell_D", "cell_E"]
gene_names = ["gene_1", "gene_2", "gene_3"]

# 创建 AnnData 对象
adata = sc.AnnData(X=expr, obs=pd.DataFrame(index=cell_names), var=pd.DataFrame(index=gene_names))

# 给 obs 添加一点细胞信息
adata.obs["batch"] = ["batch1", "batch1", "batch2", "batch2", "batch2"]

print(adata)

输出会是这样的摘要:

AnnData object with n_obs × n_vars = 5 × 3
    obs: 'batch'

你看,它自动就记住了 obs 里有哪些列。访问表达矩阵用 adata.X,访问某个细胞的信息用 adata.obs.loc["cell_A"],非常直观。

我记得第一次自己建 AnnData 时,把 obs 和 var 搞反了。结果后面画热图时,细胞名称全变成了基因名,那叫一个乱。所以这里提醒一下:obs 是观测(细胞),var 是变量(基因),别记反。

4.4 读取真实数据:基于 10x 格式

实际项目中,数据大多来自 10x Genomics 的 Cell Ranger 输出,典型文件包括:

  • matrix.mtx —— 稀疏表达矩阵
  • barcodes.tsv —— 细胞条码
  • features.tsv —— 基因列表(旧版叫 genes.tsv)

Scanpy 提供了 sc.read_10x_mtx 函数,一行代码搞定:

adata = sc.read_10x_mtx("path/to/filtered_feature_bc_matrix/", var_names="gene_symbols")

这个函数会自动读取并组合,返回一个已填充好的 AnnData 对象。注意参数 var_names 可以选 "gene_symbols" 或 "gene_ids",我一般选 "gene_symbols" 方便看图。

避坑指南:有些数据源的 barcodes.tsv 或者 features.tsv 文件名不标准(比如多了 .gz 后缀),sc.read_10x_mtx 能自动识别压缩文件。但如果文件夹里有多余文件,记得只放这三个核心文件,或者直接用 sc.read_10x_h5 读取 h5 格式的矩阵。

4.5 AnnData 内部结构图

下面这张图帮你直观理解 AnnData 里各个组件是如何关联的。我自己刚学时长记不住属性,就画了类似的图贴在桌边。

AnnData 核心结构 X 表达矩阵 (n_obs × n_vars) 稠密或稀疏矩阵 obs DataFrame 细胞名称、批次、 cell_type、n_counts…… var DataFrame 基因名称、染色体、 质控标志、高变基因…… obsm 字典:键为名称, 值为降维坐标数组 varm 字典:键为名称, 值为基因降维结果 uns 任意非结构化数据 聚类参数、配色方案、 marker gene 列表、版本号 说明: • 矩阵 X 的行与 obs 的行一一对应,列与 var 的行一一对应。 • obsm 和 varm 分别存储细胞和基因的降维结果(如 PCA 坐标、SPRING 布局等)。 • uns 用来记录任何不适合放在上述结构中的信息,例如参数、标签列表等。

4.6 常用操作与习惯

一旦创建了 AnnData,你就可以像操作 pandas DataFrame 一样操作它。例如:

  • 查看前几个细胞:adata.obs.head()
  • 查看基因名:adata.var_names[:5]
  • 筛选细胞:adata[adata.obs["batch"] == "batch1"] 返回一个新 AnnData,只包含 batch1 的细胞
  • 保存到硬盘:adata.write("my_data.h5ad"),这是 h5ad 格式,后续用 sc.read() 读取

这里我多说一句:永远别直接在原始数据上改东西。我每次读取数据后,第一件事是做一份拷贝做原始备份,然后所有预处理、过滤、归一化都在拷贝上操作。这样万一某步做错了,不用重新下载几十 GB 的数据。

另外,adata.X 默认是稠密矩阵,如果数据太大(比如超过 5 万细胞),建议转成稀疏矩阵以节省内存:

import scipy.sparse as sp
adata.X = sp.csr_matrix(adata.X)

你可以随时用 adata 打印摘要,看看当前有多少细胞、多少基因、哪些 obs/var 列被填充了。比如我每次跑完一个过滤步骤,都会打印一次确认结果——这几乎成了我的条件反射。

4.7 本章小结

到这一步,你已经学会了 Scanpy 的安装和核心数据结构 AnnData。后面的所有批次校正、聚类、可视化,都会基于这个对象展开。千万不要小看这一章,哪怕你会用全部算法,一旦数据加载或结构搞错,后面的结果都是垃圾进垃圾出。