AnnData数据结构
什么是 AnnData
AnnData(Annotated Data) 是一个为“矩阵 + 丰富注释”而设计的 Python 容器,是 Scanpy/Squidpy、许多单细胞/空间转录组工具的核心数据结构。
它用一个形状为 (n_obs, n_vars)
的矩阵(通常是 细胞 × 基因)作为主体,并把与行、列、矩阵相关的一切元信息整齐地放在命名的“抽屉”里,保证对齐与可追溯。
数据模型总览
.X
:主体矩阵(通常存放当前分析用的表达矩阵)。可为numpy.ndarray
或scipy.sparse
。.obs
:行(细胞)的表格注释,pandas.DataFrame
,索引是细胞 ID。.var
:列(基因)的表格注释,pandas.DataFrame
,索引是基因 ID。.obsm
:行方向的多维嵌入/坐标(如 UMAP、t-SNE、空间坐标),每个键是一块n_obs × k
的数组,例如obsm['X_umap']
、obsm['spatial']
。.varm
:列方向的多维特征(如 PCA 的基因载荷),每个键是n_vars × k
。.obsp
:行方向的方阵(n_obs × n_obs
),如邻接图connectivities
、距离矩阵distances
。.varp
:列方向的方阵(n_vars × n_vars
),如基因-基因相关性。.layers
:与 .X 同形状的多版本矩阵层(如counts
、log1p
、normalized
)。.uns
:无结构字典,存放分析元数据、绘图参数、结果摘要(如uns['pca']
、uns['neighbors']
、uns['spatial']
)。.raw
:一个只读快照(通常保存“原始计数 + 当时的 var”),用于差异分析等需回溯原始表达的场景。
记忆法:
obs/var
是表格注释,obsm/varm
是多维数组,obsp/varp
是方阵图,layers
是多版本矩阵,uns
是杂项/配置。
最小工作样例
import numpy as np
import pandas as pd
import scipy.sparse as sp
import anndata as ad
# 假造 3 个细胞 × 5 个基因的 count 矩阵(稀疏)
X_counts = sp.csr_matrix(np.array([
[0, 3, 0, 1, 0],
[2, 0, 0, 0, 5],
[0, 1, 0, 0, 0],
], dtype=np.float32))
adata = ad.AnnData(
X=X_counts, # 主矩阵(当前版本)
obs=pd.DataFrame(index=['cell1','cell2','cell3']),
var=pd.DataFrame(index=['GeneA','GeneB','GeneC','GeneD','GeneE'])
)
# 加注释
adata.obs['sample'] = ['S1','S1','S2']
adata.var['mt'] = [False, False, True, False, False] # 例:线粒体基因标记
# 存层:保存原始计数
adata.layers['counts'] = adata.X.copy()
# 做一个“归一化后的表达”版本进 .X(示例,真实请用 Scanpy API)
X_norm = adata.layers['counts'].astype(np.float32)
libsize = np.asarray(X_norm.sum(1)).flatten()
X_norm = X_norm.multiply(1e4 / np.maximum(libsize, 1)) # CPM-like
adata.X = X_norm
# 嵌入 / 坐标
adata.obsm['X_umap'] = np.random.randn(adata.n_obs, 2).astype(np.float32)
# 写盘
adata.write_h5ad('demo.h5ad', compression='gzip')
读取与存储(h5ad / zarr / 10x 等)
- h5ad(最常用,单文件):
import scanpy as sc
adata = sc.read_h5ad('data.h5ad') # 读
adata.write_h5ad('data_out.h5ad', compression='gzip') # 写
- Zarr(分块、云端友好):
adata.write_zarr('data.zarr')
adata = ad.read_zarr('data.zarr')
- 10x:
adata = sc.read_10x_mtx('filtered_feature_bc_matrix/') # 目录
# 或 sc.read_10x_h5('filtered_feature_bc_matrix.h5')
- 大数据 “背后读取”(backed):适合巨型 h5ad 的只读/有限写场景
adata = sc.read_h5ad('big.h5ad', backed='r') # 'r' 只读,'r+' 可就地写
# 注意:backed 模式下许多操作受限,需小心/分步处理
切片、对齐与视图(非常重要)
- 基本切片
ad_small = adata[adata.obs['sample']=='S1', ['GeneA','GeneB']].copy()
视图 VS 副本:adata[…] 默认产生视图(view);在其上赋值会触发
setting on view
的警告。
规范做法:切片后立刻.copy()
再改。任何
.obs/.var/.obsm/...
与.X
都按索引严格对齐。
例如你重排了
adata = adata[sorted_idx, :]
,.obsm['X_umap']
会同步重排。
常见抽屉的典型内容与约定
.X
与.layers
- 建议把原始计数放入
layers['counts']
,把当前分析用矩阵放.X
。 - 规范管线常见层命名:
layers['counts']
:原始 UMI(int/float,稀疏 CSR/CSC)layers['normalized']
:size-factor/CPM 归一化后layers['log1p']
:log1p(normalized)
结果
- 好处:你可以在不同分析步骤间切换基质而不丢信息:
adata.X = adata.layers['log1p']
.obs
/.var
.obs
举例:cluster
、sample
、batch
、percent_mt
、n_genes_by_counts
等。.var
举例:gene_symbol
、mt
(是否线粒体基因)、highly_variable
等。- 类别型列请转为
category
,节省内存并利于分组:
adata.obs['cluster'] = adata.obs['cluster'].astype('category')
.obsm
/.varm
.obsm['X_pca']
、.obsm['X_umap']
、.obsm['spatial']
(Visium 二维坐标)。.varm['PCs']
:PCA 的基因载荷(n_vars × n_pcs
)。- 约定俗成:低维嵌入用键名以
X_
开头(如X_umap
/X_tsne
)。
.obsp
/.varp
.obsp['connectivities']
&.obsp['distances']
:邻居图(n_obs×n_obs
)。- 基因网络可放
.varp['correlation']
等。
.uns
- 保存步骤配置、颜色表、统计摘要、空间影像信息等:
- uns[‘neighbors’]:邻居图的元参数
- uns[‘pca’]:PCA 的方差解释等
- uns[‘spatial’]:Visium/Squidpy 使用的 切片图像 与 缩放参数
- uns[‘rank_genes_groups’]:差异分析结果及其元数据
.raw
常用于 把原始(或标准化但未 log)表达 + var 快照 冻结起来:
adata.raw = adata # 这会把当前 .X 和 .var 存为只读快照
有些新工作流更提倡用
layers['counts']
等分层管理,raw
主要用于兼容传统函数或需要只读对照的场景。
与 Scanpy 的常见配合
import scanpy as sc
# 1) 质控统计
sc.pp.calculate_qc_metrics(adata, qc_vars=['mt'], percent_top=None, log1p=False, inplace=True)
# 2) 归一化 + log1p(保留 counts)
adata.layers['counts'] = adata.X.copy()
sc.pp.normalize_total(adata, target_sum=1e4) # 改写 .X
sc.pp.log1p(adata) # 改写 .X -> log1p
adata.layers['log1p'] = adata.X.copy()
# 3) HVG
sc.pp.highly_variable_genes(adata, flavor='seurat_v3', n_top_genes=3000)
adata = adata[:, adata.var['highly_variable']].copy()
# 4) 标准化、PCA、邻居、UMAP
sc.pp.scale(adata, zero_center=True, max_value=10)
sc.tl.pca(adata, svd_solver='arpack')
sc.pp.neighbors(adata, n_neighbors=15, n_pcs=30)
sc.tl.umap(adata)
空间转录组特别提示(Visium/Squidpy)
- 空间坐标:
adata.obsm['spatial'] = (n_obs × 2)
的像素/坐标。 - 切片影像与缩放:
adata.uns['spatial'][library_id]['images'][...]
及['scalefactors']
。 - 分辨率/坐标系由
Squidpy/Scanpy
读入函数写入,绘图/邻域分析都会读这些键。
拼接与合并(多样本/批次)
- 推荐用
anndata.concat
(或scanpy.concat
),而非旧的AnnData.concatenate
:
import anndata as ad
adata_all = ad.concat(
[adata1, adata2, adata3],
join='outer', # 控制 var 对齐策略:'outer'/'inner'
label='batch', # 在 .obs 新增列记录来源
keys=['S1','S2','S3'],
index_unique='-'
)
- 变量(基因)对齐策略至关重要:
join='inner'
:取交集,适合不同平台但想要严格可比的基因集join='outer'
:取并集,不存在处补零(稀疏矩阵友好)
常用 API 速查
# 形状与索引
adata.shape; adata.n_obs; adata.n_vars
adata.obs_names[:5]; adata.var_names[:5]
# 新增/修改注释
adata.obs['cluster'] = cluster_labels.astype('category')
adata.var['mt'] = adata.var_names.str.upper().str.startswith('MT-')
# 选择细胞/基因
adata_sub = adata[adata.obs['cluster']=='T', :].copy()
adata_genes = adata[:, ['CD3D','CD3E','MS4A1']].copy()
# 层的切换
adata.layers['counts'] = adata.X.copy()
adata.X = adata.layers['log1p']
# 嵌入与邻接
adata.obsm['X_umap'].shape
adata.obsp['connectivities'].shape
# 读写
adata.write_h5ad('out.h5ad', compression='gzip')
adata2 = sc.read_h5ad('out.h5ad')
常见坑与排雷清单
- “setting on view” 警告:切片后马上
.copy()
再改。 - 基因/细胞名不唯一:请先
var_names_make_unique()
/obs_names_make_unique()
。 - 混用密集/稀疏类型:下游函数可能假设稀疏;统一为
csr_matrix
更稳。 - 把 Log 值当作计数:请将不同版本放入
layers
,并在分析函数中明确指定来源。 raw
与layers
混乱:要么规范使用raw
,要么全用layers
管理,避免两套并行而互相覆盖。- 连接(concat)忘了 join 策略:默认不一定是你要的;跨平台数据尤其要确认。
- 空间数据键名不匹配:确保
obsm['spatial']
与uns['spatial']
结构完整,否则绘图/邻域计算会失败。
小型工作流模板
import scanpy as sc
# 读取
adata = sc.read_10x_mtx('filtered_feature_bc_matrix/') # or read_h5ad
# 预处理
adata.var_names_make_unique()
sc.pp.calculate_qc_metrics(adata, qc_vars=['mt'], inplace=True)
# 保存 counts
adata.layers['counts'] = adata.X.copy()
# 归一化 + log1p (更新 .X)
sc.pp.normalize_total(adata, target_sum=1e4)
sc.pp.log1p(adata)
adata.layers['log1p'] = adata.X.copy()
# HVG
sc.pp.highly_variable_genes(adata, n_top_genes=3000, flavor='seurat_v3')
adata = adata[:, adata.var['highly_variable']].copy()
# 标准化与降维
sc.pp.scale(adata, max_value=10)
sc.tl.pca(adata, n_comps=50)
sc.pp.neighbors(adata, n_neighbors=15, n_pcs=30)
sc.tl.umap(adata)
# 写盘
adata.write_h5ad('analysis.h5ad', compression='gzip')