2. 创建 AnnData 对象:从 CSV/MTX/H5AD 文件读取、手动构造与质量控制
说实话,做单细胞数据分析这几年,我每次拿到新数据的第一件事,就是把它装进 AnnData 对象里。你想想看,后面所有的降维、聚类、差异分析,全都依赖这个容器搭得稳不稳。今天我们就来把创建 AnnData 这件事彻底讲透。
2.1 AnnData 对象的基本结构
AnnData 说白了就是一个数据仓库。它把表达矩阵、细胞元数据、基因元数据、降维结果、邻接图全整合在一起。我个人习惯用一个公式来记:
AnnData = X(表达矩阵)+ obs(细胞注释)+ var(基因注释)+ uns(非结构化数据)+ obsm/varm(降维结果)+ obsp/varp(邻接图)
核心组件一览:
| 属性 | 存储内容 | 维度要求 |
|---|---|---|
.X |
表达矩阵(细胞×基因) | n_obs × n_vars |
.obs |
细胞级别的元数据(批次、样本、细胞类型等) | n_obs × m |
.var |
基因级别的元数据(基因名、染色体位置等) | n_vars × p |
.uns |
非结构化数据(配色方案、参数记录等) | 无固定维度 |
.obsm |
细胞的多维降维结果(PCA、UMAP 坐标等) | n_obs × d |
.varm |
基因的多维降维结果(基因在 PCA 上的载荷等) | n_vars × d |
.obsp |
细胞间的邻接关系(KNN 图、距离矩阵) | n_obs × n_obs |
.varp |
基因间的邻接关系(共表达网络等) | n_vars × n_vars |
2.2 从 CSV 文件读取——最接地气的方式
实验室里经常有人给我一个 Excel 转出来的 CSV 文件,行是细胞、列是基因。这种场景下我一般这么干:
import pandas as pd
import anndata as ad
# 用 pandas 先把数据读进来,顺便做个体检
counts_df = pd.read_csv('expression_matrix.csv', index_col=0)
metadata_df = pd.read_csv('cell_metadata.csv', index_col=0)
# 检查行名是否对应
print("表达矩阵行数:", counts_df.shape[0])
print("元数据行数:", metadata_df.shape[0])
# 创建 AnnData 对象
adata = ad.AnnData(X=counts_df.values, obs=metadata_df, var=pd.DataFrame(index=counts_df.columns))
print(adata)
这里有个坑——adata.obs_names 和 adata.var_names 的一致性。我曾经在项目里因为 CSV 文件的索引列有空格没去掉,导致后面过滤细胞时 obs 和 var 对不上,排查了大半天。所以养成习惯:读进来后先跑一句 adata.obs_names_make_unique()。
2.3 从 Market Matrix (MTX) 文件读取——10X 数据的标准姿势
做单细胞测序的人对 MTX 格式再熟悉不过了。每次拿到 Cell Ranger 的输出,我都是这样直接读取:
import scanpy as sc
# 一行搞定,scanpy 自动识别 .mtx.gz 后缀
adata = sc.read_10x_mtx('filtered_feature_bc_matrix/', var_names='gene_symbols')
print(adata)
如果你手头是 matrix.mtx + barcodes.tsv + features.tsv 三件套,直接用 sc.read_10x_mtx 最省事。但要注意:
- var_names 参数:设成 'gene_symbols' 会直接拿基因名当变量名;如果设成 'gene_ids',那就用 ENSEMBL ID。我个人偏好基因名,好记。
- gzipped:10X 默认文件都是 .gz 压缩的,scanpy 自动处理。
- cache:如果数据量很大(比如 50 万细胞以上),建议加个
cache=True,第二次加载会快很多。
2.4 从 H5AD 文件读取——最利落的存取方式
H5AD 是 AnnData 的原生二进制格式,读写最快,且能完整保留 .uns、.obsm 等所有属性。我在项目中经常用它来做中间结果缓存:
# 直接加载
adata = ad.read_h5ad('processed_data.h5ad')
print(adata)
# 也可以筛选加载,省内存
adata = ad.read_h5ad('big_data.h5ad', backed='r')
print(adata.obs.head())
backed='r' 是内存紧张时的救命稻草。它不会把完整矩阵读进内存,而是一个一个切片去取。不过坏处是写操作有限——你只能追加 .obs 或 .var 列,不能直接改 .X。
| 读取方式 | 适用场景 | 速度 | 内存占用 |
|---|---|---|---|
| sc.read_10x_mtx | Cell Ranger 原始输出 | 中等(依赖文件大小) | 高 |
| ad.read_h5ad(全量) | 中间结果、预过滤数据 | 快 | 高 |
| ad.read_h5ad(backed) | 超大文件、低内存环境 | 慢(I/O 瓶颈) | 低 |
| pd.read_csv + ad.AnnData | 实验室小批量数据、CSV 交接 | 需要手动构造 | 高 |
2.5 手动构造示例数据——调试时最常用
有时候算法写了一半,想快速验证流程对不对,这时候手动造一个小的 AnnData 最方便:
import numpy as np
import anndata as ad
# 造一个 10 个细胞 × 5 个基因的小矩阵
X = np.array([
[1, 0, 3, 0, 0],
[0, 2, 0, 4, 0],
[1, 1, 1, 1, 1],
[0, 0, 5, 0, 0],
[3, 0, 0, 0, 2],
[0, 0, 0, 0, 0],
[2, 2, 0, 0, 0],
[0, 0, 0, 3, 0],
[1, 0, 0, 0, 0],
[0, 0, 0, 0, 1]
])
obs = pd.DataFrame({
'batch': ['A', 'A', 'A', 'B', 'B', 'B', 'C', 'C', 'C', 'C'],
'n_counts': X.sum(axis=1)
}, index=[f'cell_{i}' for i in range(10)])
var = pd.DataFrame({
'gene_symbol': ['GAPDH', 'ACTB', 'TP53', 'EGFR', 'MYC']
}, index=[f'gene_{i}' for i in range(5)])
adata = ad.AnnData(X=X, obs=obs, var=var)
print(adata)
输出长这样:
AnnData object with n_obs × n_vars = 10 × 5
obs: 'batch', 'n_counts'
var: 'gene_symbol'
这招我在单元测试里用得特别多。造几个零值多、有全零行/列的小矩阵,专门用来测过滤函数是否正常工作。
2.6 数据质量控制基础——别让脏数据毁了下游分析
创建好 AnnData 对象之后,我从来不会直接往下跑 PCA。先给数据做个体检再说。我的 QC 三件套:
- 线粒体基因占比:高比例意味着细胞膜破损,是死细胞或受损细胞的标志。
- 总 UMI 计数:太低的可能是空滴或双细胞碎片,太高的可能是双细胞。
- 检测到的基因数:太少说明细胞活性差,太多也可能是双细胞。
快速计算线粒体占比:
# 假设基因名是 'mt-' 开头表示线粒体基因
adata.var['mt'] = adata.var_names.str.startswith('MT-')
sc.pp.calculate_qc_metrics(adata, qc_vars=['mt'], percent_top=None, log1p=False, inplace=True)
# 看看分布
print(adata.obs[['n_genes_by_counts', 'total_counts', 'pct_counts_mt']].describe())
我会根据这个分布设定一个经验阈值:
# 过滤细胞
sc.pp.filter_cells(adata, min_genes=200)
sc.pp.filter_genes(adata, min_cells=3)
# 手动过滤线粒体占比 > 20% 的细胞
adata = adata[adata.obs.pct_counts_mt < 20, :].copy()
print(f"过滤后细胞数: {adata.n_obs}")
我曾经有个项目,就是因为没有事先滤掉线粒体高表达细胞,导致后续的降维聚类结果完全偏离了生物学意义——那些死细胞自己聚成了一团,还特别稳定。嗯,从那以后 QC 再也没跳过。
下面这张图帮你理清 AnnData 的创建与质控流程:
好了,整个流程其实就三步:读进来 → 做体检 → 滤脏数据。你可以根据数据来源选不同的读取方式,但 QC 这步千万别偷懒。