4. Scanpy 安装与核心对象:AnnData 结构
好,我们正式开始接触工具链。这一章要做两件事:先把 Scanpy 环境装好,再搞明白它最核心的数据结构——AnnData。说白了,你后面所有分析、所有表格、所有图表,全是在跟这个对象打交道。
我个人的习惯是:先搞清数据结构再上手跑代码。不然光会敲命令,一出错连日志都看不懂。咱们一步一步来。
4.1 安装 Scanpy 及其依赖库
Scanpy 依赖的库不少,但核心就这几样:numpy、pandas、scipy、matplotlib,以及专门处理单细胞数据的 anndata 包。我建议优先用 pip 安装,如果网络不好就用 conda,但注意 conda 上的版本可能滞后一两个小版本。
打开终端,依次执行:
# 创建虚拟环境(可选,但我强烈推荐)
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 里各个组件是如何关联的。我自己刚学时长记不住属性,就画了类似的图贴在桌边。
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。后面的所有批次校正、聚类、可视化,都会基于这个对象展开。千万不要小看这一章,哪怕你会用全部算法,一旦数据加载或结构搞错,后面的结果都是垃圾进垃圾出。