前两周处理一批公共数据时,我栽了一个大跟头:一批单细胞数据合并之后,落盘是个 h5ad 文件,30 GB 出头,六十万细胞,机器配了 64 GB 内存。我心想 64GB 总够了吧,结果 scanpy.read_h5ad() 一跑,内存直接冲到 58 GB,再往后所有分析流程全部卡死。后来我被迫认真做了一次“超大数据 h5ad 分割”,把文件拆成可独立加载的小块,才把整个项目救回来。这件事让我意识到,单细胞数据的“大”根本不是稀罕事,真正的问题是我们习惯性地把文件整个塞进内存再处理。
这篇我就围绕 h5ad 文件和超大数据分割,把整个踩坑和解决过程写出来。内容包括:h5ad 文件为什么动不动几十 GB、拿到 GEO 或其他来源的数据后怎么快速统一成可操作的 h5ad、真正适合大文件的分割姿势、以及我在实测中记录下来的内存和耗时表现。想直接把“大 h5ad”拆开用的人,这篇应该能帮你少走不少弯路。
1. 一套散装数据几十GB:先弄明白h5ad到底把空间吃在哪
很多人在分割文件之前,根本没搞懂手里的 h5ad 为什么这么大。于是最常见的做法就是盲目加内存、换服务器,最后发现文件大到连读取都成了问题。其实 h5ad 的体量来源是固定的几个部分,搞清了,分割方案自然就有了。
1.1 单细胞矩阵真实尺寸的测算与常见大文件来源
先做一个最朴素的估算。假设你的数据是 60 万细胞、3 万个基因(这个规模的现代单细胞项目很常见),如果表达矩阵用稠密 float32 存储,那理论体积是:
- 600,000 × 30,000 × 4 字节 = 72,000,000,000 字节,约 67 GB
这就是问题所在。只要矩阵是稠密形式,任何数据都很难真正处理。好在单细胞表达矩阵本质是稀疏的,绝大多数基因在绝大多数细胞里表达量是 0,因此实际存下来不会是 67 GB。h5ad 内部用 CSR/CSC 等稀疏格式保存 X,每个非零表达值大概占 8~12 字节(包含索引位和值),典型 10x 数据每个细胞中位表达基因数在两三千个左右,60 万细胞的非零元素大概在 15 亿量级,于是 X 本身能压到 15~18 GB,再加上索引、注释、坐标、底层 HDF5 的 chunk 开销,落到磁盘就是一个 30 GB 上下的文件。
但我在实际工作中发现,很多人忽略的另一大体积来源是 raw。假设你读入数据后做了 normalize、log1p,并把原始 counts 通过 adata.raw = adata.copy() 存了一份,那就等于同一套大矩阵又多存了一遍。文件里 raw/X 和 X 并存的时候,整体体积翻倍一点不奇怪。这种“为了后续恢复原始数据所以保留 raw”的习惯,在小文件上没问题,但对 30 GB 的超大 h5ad 来说,会直接拖累读写和分割。
另外 obs 和 var 表里的文本列也会占不少地方。尤其是从多个样本拼合的数据,obs 里常见的 sample_id、cell_type 等列,如果以纯字符串形式保存,HDF5 里会有大量重复字符串开销。uns 里通常还存着 UMAP 坐标、聚类树、PAGA 图等中间结果。这些跟“分割”本身没关系,却会实实在在地拖慢文件打开和写入。
1.2 先给文件做一个“内存体检”,再决定要不要分割
拿到一个巨大 h5ad,不要上来就 pd.read_h5ad() 直接加载,先用 backed 模式打开,做一次轻量级的检查:
python复制import anndata as ad
import scanpy as sc
file_path = "/mnt/data/big_scrna.h5ad"
adata = ad.read_h5ad(file_path, backed="r")
print(adata.shape) # (600000, 30000)
print("cells:", adata.n_obs, "genes:", adata.n_vars)
# obs 和 var 里有哪些列,几乎决定文件里文本占比
print(adata.obs.columns.tolist())
print(adata.var.columns.tolist())
# 看看是否有 raw,这两个矩阵同时存在是最占空间的
if adata.raw is not None:
print("raw shape:", adata.raw.shape)
else:
print("no raw")
# 常见冗余:uns 里的聚类/坐标/图结构
print(list(adata.uns.keys()))
backed 模式的意思是,文件不会整体读进内存,只是打开 HDF5 句柄,真正调用 .X 或 adata[...] 时才去底层读取对应 block。这个模式对超大文件非常关键,它让我可以只查看元数据和结构,而不用立刻面对 30 GB 的加载压力。
通过这些体检信息,基本就能判断手里的文件该走哪条路线:
- 如果
X和raw.X同时存在,文件体积几乎翻倍,优先考虑裁剪; - 如果
obs里大量字符串列是非重复系数低的 ID 字段,转成 category 收益不大,但如果是重复率高的注释标签,转 category 收益明显; - 如果
uns里塞了几十 MB 的中间结果,而下游根本不需要,清理后文件能小一截; - 如果文件去掉冗余后依然很大,这时候才真正需要做切片分割。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 拿到GEO或其他公开来源数据后,怎么统一成可操作的h5ad
很多人手上不是现成的 h5ad,而是从 GEO 或各种公共数据库下载的原始矩阵。此时候面临的第一步不是切割,而是把来源五花八门的数据先转换成一个干净的 h5ad。很多人问“如何用 Python 读取 GEO 单细胞数据”,其实这个问题的本质不是不知道函数名,而是进了误区:GEO 上发布的
