1、AnnData 概述:单细胞转录组数据标准格式
大家好,我是老陈。搞单细胞数据分析这些年,我踩过不少坑。今天咱们聊聊 AnnData——这个在单细胞领域绕不开的数据结构。
说白了,AnnData 就是一个专门为单细胞组学数据设计的容器。为什么需要它?你想想看,一个典型的单细胞实验会产出数万个细胞、两万多个基因的表达矩阵,再加上降维坐标、聚类结果、细胞注释……这些数据七零八落地放着,不出三天你自己都找不着北。
AnnData 就是来解决这个问题的。它把所有相关数据组织在一起,让你能像操作一个“智能文件夹”一样管理分析流程。
1.1 为什么是 AnnData?
2018 年左右我刚开始转做单细胞时,大家的通用做法是把表达矩阵放在 CSV 里,元数据单独放一个 Excel,降维坐标又存另一个文件。结果呢?做一次整合分析,光是对齐样本就花了半天。
AnnData 的出现改变了这个局面。它由 Alex Wolf 和 Fabian Theis 团队在开发 Scanpy 时设计,核心目标就三点:
- 统一:所有数据放一个对象,不再东一个文件西一个文件
- 高效:底层基于 NumPy 和稀疏矩阵,几万个细胞也跑得动
- 可扩展:随时往对象里加新的计算结果,不破坏原有结构
我个人习惯是:任何一个新项目,第一步就是把原始数据读成 AnnData。这样一来,后续无论做质控、标准化还是降维聚类,数据都不会跑偏。
1.2 AnnData 与 Scanpy 生态
AnnData 是 Scanpy 的“灵魂”,但并不绑死在 Scanpy 上。我现在同时用 Scanpy 做预处理、用 scVI 做批次校正、用 CellRank 做细胞命运分析——它们都读写 AnnData 对象。
整个生态大概长这样:
# 核心库
anndata # 数据容器
scanpy # 分析框架(预处理、聚类、可视化)
# 扩展库
scvelo # RNA 速率分析
scvi-tools # 深度学习模型(批次校正、注释)
cellrank # 细胞命运推断
squidpy # 空间转录组分析
你看,底层统一用 AnnData。这就好比 iPhone 和 App Store 的关系——AnnData 是操作系统,各种工具是上面的应用。
pip install anndata==0.8.0 锁定。
1.3 AnnData 对象核心结构
好,现在咱们拆开 AnnData 看看里面到底有什么。我画了一张结构图,你一看就明白:
结构图看明白了?我挑几个重点展开讲。
1.3.1 X —— 表达矩阵
这是 AnnData 的核心,行是细胞,列是基因,存储表达值(通常是 UMI 计数)。
在我早期做 scRNA-seq 时,有个教训一直记忆犹新:千万注意 X 的数据类型。默认读进来是 CSR 稀疏矩阵(十亿级的零值存成稠密矩阵会爆内存)。
import anndata as ad
import numpy as np
# 创建一个最小 AnnData 对象
X = np.array([
[3, 0, 1, 8],
[0, 4, 0, 2],
[7, 1, 5, 0]
])
cells = ['cell_A', 'cell_B', 'cell_C']
genes = ['gene_1', 'gene_2', 'gene_3', 'gene_4']
# 推荐显式指定 dtype
adata = ad.AnnData(X=X.astype(np.float32), obs=cells, var=genes)
# 检查矩阵形状
print(f"细胞数: {adata.n_obs}, 基因数: {adata.n_vars}")
# 稀疏存储检查(大项目必备)
print(f"X 是否为稀疏矩阵: {adata.isbacked}")
adata = ad.AnnData(X, backed='r') 打开只读内存映射模式。我第一次处理 50 万细胞的 10X 数据时忘了这茬,直接 OOM 崩溃。血泪教训。
1.3.2 obs 和 var —— 行/列元数据
obs 存细胞信息,var 存基因信息。说白了就是一个有索引的 Pandas DataFrame。
# 添加细胞元数据
adata.obs['batch'] = ['batch1', 'batch1', 'batch2']
adata.obs['n_genes_by_counts'] = [3, 2, 3] # 每个细胞检测到的基因数
# 添加基因元数据
adata.var['highly_variable'] = [True, False, True, False]
adata.var['mean_counts'] = [3.33, 1.67, 2.00, 3.33]
# 查看
print(adata.obs.head())
print(adata.var.head())
注意:obs 索引名必须和 X 的行名一一对应,var 同理。我以前手滑把索引顺序弄反过,结果画 UMAP 时发现细胞标签全乱了——排查了两天才定位到问题。
adata.obs['new_col'] = list,长度和顺序必须严格匹配。我建议用 adata.obs['new_col'] = pd.Series(index=adata.obs_names, data=...) 来确保索引对齐。
1.3.3 obsm 和 varm —— 嵌入空间与基因扩展
obsm 存细胞层面的多维结果,比如 PCA、UMAP、Harmony 校正后的坐标。这些数据是“附加”的,不直接参与计算,但可视化时离不开它们。
# 模拟 PCA 结果 (10 个主成分)
pca_result = np.random.rand(adata.n_obs, 10)
adata.obsm['X_pca'] = pca_result
# 模拟 UMAP 结果 (2 维)
umap_result = np.random.rand(adata.n_obs, 2)
adata.obsm['X_umap'] = umap_result
# 查看
print("obsm 的键:", list(adata.obsm.keys()))
print("PCA 矩阵形状:", adata.obsm['X_pca'].shape)
varm 则相反,存储基因层面的“载荷”或“权重”,比如 PCA 的 loading。
# 模拟 PCA loading (基因 x 主成分)
pca_loading = np.random.rand(adata.n_vars, 10)
adata.varm['PCs'] = pca_loading
print("varm 中 PCs 形状:", adata.varm['PCs'].shape)
| 属性 | 维度 | 描述 | 典型示例 |
|---|---|---|---|
obsm |
n_obs × k | 细胞嵌入/降维结果 | PCA、UMAP、Harmony |
varm |
n_vars × k | 基因特征/载荷 | PCA loading、基因权重 |
1.3.4 uns —— 非结构化数据区
这是 AnnData 的“杂物间”。聚类参数、配色方案、差异基因列表……任何没法塞进前几个属性的东西,都往这里扔。
# 存储聚类参数
adata.uns['leiden_params'] = {'resolution': 1.0, 'n_neighbors': 15}
# 存储绘图配色
adata.uns['cluster_colors'] = {
'cluster_1': '#1f77b4',
'cluster_2': '#ff7f0e'
}
# 存储差异基因排序
adata.uns['rank_genes_groups'] = {
'names': ['gene_1', 'gene_3']
}
print("uns 中所有键:", list(adata.uns.keys()))
'description' 键记录做了什么。
1.4 一图胜千言:AnnData 的“四层”数据模型
最后给你一个速查表,这是我在带新人时反复用的。把它贴在你的显示器边上:
AnnData 对象 = {
X : (n_obs, n_vars) → 表达矩阵(必须)
obs : (n_obs, m) → 细胞级元数据(Pandas DataFrame)
var : (n_vars, p) → 基因级元数据(Pandas DataFrame)
obsm : (n_obs, k) → 细胞级"高维"扩展(PCA、UMAP)
varm : (n_vars, q) → 基因级"高维"扩展(PCA loading)
uns : dict → 剩余一切(参数、配色、注释)
}
嗯,这一章就聊到这儿。AnnData 的概念看似多,但你只要抓住“行是细胞、列是基因、中间是表达值”这个主线,后面的内容就顺了。