磁盘上的格式#
注意
这些文档是为 anndata 0.8+ 编写的。 在此版本之前编写的文件在某些规范上可能有所不同,但新版本的库仍然可以读取它们。
AnnData 对象被保存到磁盘上的分层数组存储中,如 HDF5 (通过 H5py) 和 Zarr-Python。 这使得我们在磁盘和内存中具有非常相似的结构。
作为一个例子,我们将研究一个典型的 .h5ad/ .zarr 对象,它经过了分析。 这些结构在很大程度上是等价的,不过在类型编码上有一些小差异。
元素#
>>> import h5py
>>> store = h5py.File("for-ondisk-docs/cart-164k-processed.h5ad", mode="r")
>>> list(store.keys())
['X', 'layers', 'obs', 'obsm', 'obsp', 'uns', 'var', 'varm', 'varp']
>>> import zarr
>>> store = zarr.open("for-ondisk-docs/cart-164k-processed.zarr", mode="r")
>>> list(store.keys())
['X', 'layers', 'obs', 'obsm', 'obsp', 'uns', 'var', 'varm', 'varp']
通常,AnnData 对象由各种类型的元素组成。
每个元素在存储中编码为数组(或 hdf5 术语中的数据集)或元素集合(例如,组)。
我们使用其属性中的 encoding-type 和 encoding-version 键记录元素的类型。
例如,我们可以看到该文件从其元数据表示一个 AnnData 对象:
>>> dict(store.attrs)
{'encoding-type': 'anndata', 'encoding-version': '0.1.0'}
利用这些信息,我们能够根据在anndata中找到的不同元素类型将其分发给读者。
元素规范#
元素可以是存储层次结构中的任何对象(通常是数组或组),并带有所关联的元数据
一个元素必须在其元数据中具有一个字符串值字段
"encoding-type"元素必须在其元数据中具有一个字符串值字段
"encoding-version",该字段可以评估为一个版本
AnnData 规范 (v0.1.0)#
一个
AnnData对象必须是一个组。该组的元数据必须包括条目:
"encoding-type": "anndata","encoding-version": "0.1.0".一个
AnnData组必须包含条目"obs"和"var",这两个条目必须是数据框(尽管这可能只有一个没有列的索引)。该组 MAY 包含一个条目
X,该条目必须是密集或稀疏数组,且其形状必须为 (n_obs,n_var)该组可能包含一个映射
layers。layers中的条目必须是具有形状 (n_obs,n_var) 的密集或稀疏数组该组可能包含一个映射
obsm。obsm中的条目必须是稀疏数组、密集数组或数据框。这些条目必须具有大小为n_obs的第一维该组MAY包含一个映射
varm。在varm中的条目MUST是稀疏数组、密集数组或数据框。这些条目MUST具有大小为n_var的第一维该组可能包含一个映射
obsp。在obsp中的条目必须是稀疏或密集数组。条目的前两个维度必须为n_obs的大小该组 MAY 包含一个映射
varp。varp中的条目 MUST 是稀疏或密集数组。 条目的前两个维度 MUST 的大小为n_var该组可能包含一个映射
uns。uns中的条目必须是 anndata 编码类型。
稠密数组#
密集的数值数组在磁盘上有最简单的表示,因为它们在 H5py Datasets 和 Zarr Arrays 中有原生等价物。我们可以通过存储在 obsm 组中的降维示例来看到这一点:
>>> store["obsm/X_pca"]
<HDF5 dataset "X_pca": shape (164114, 50), type "<f4">
>>> store["obsm/X_pca"]
<zarr.core.Array '/obsm/X_pca' (164114, 50) float32 read-only>
>>> dict(store["obsm"]["X_pca"].attrs)
{'encoding-type': 'array', 'encoding-version': '0.2.0'}
稠密数组规范 (v0.2.0)#
稠密数组必须存储在数组对象中
稠密数组必须在其元数据中包含条目
'encoding-type': 'array'和'encoding-version': '0.2.0'
稀疏数组#
稀疏数组在HDF5或Zarr中没有原生表示形式,因此我们根据它们的内存结构定义了自己的表示形式。目前,AnnData对象支持两种稀疏数据格式,CSC和CSR(分别对应scipy.sparse.csc_matrix和scipy.sparse.csr_matrix)。这些格式通过三个一维数组表示一个二维稀疏数组,indptr、indices和data。
注意
这些格式的完整描述超出了本文档的范围,但很容易被找到。
我们将稀疏数组表示为一个 Group 在磁盘上,稀疏数组的类型和形状在 Group 的属性中定义:
>>> dict(store["X"].attrs)
{'encoding-type': 'csr_matrix',
'encoding-version': '0.1.0',
'shape': [164114, 40145]}
该组包含三个数组:
>>> store["X"].visititems(print)
data <HDF5 dataset "data": shape (495079432,), type "<f4">
indices <HDF5 dataset "indices": shape (495079432,), type "<i4">
indptr <HDF5 dataset "indptr": shape (164115,), type "<i4">
>>> store["X"].visititems(print)
data <zarr.core.Array '/X/data' (495079432,) float32 read-only>
indices <zarr.core.Array '/X/indices' (495079432,) int32 read-only>
indptr <zarr.core.Array '/X/indptr' (164115,) int32 read-only>
稀疏数组规范 (v0.1.0)#
每个稀疏数组必须是其自己的组
该组必须包含数组
indices、indptr和data该组的元数据必须包含:
"encoding-type",对于压缩稀疏行和压缩稀疏列,分别设置为"csr_matrix"或"csc_matrix"。"encoding-version",设置为"0.1.0""shape",这是一个长度为2的整数数组,其值是数组维度的大小
数据框架#
数据框以列格式保存为一组,因此数据框的每一列都作为单独的数组保存。 我们在这里保存了一些额外的信息在属性中。
>>> dict(store["var"].attrs)
{'_index': 'ensembl_id',
'column-order': ['highly_variable',
'means',
'variances',
'variances_norm',
'feature_is_filtered',
'feature_name',
'feature_reference',
'feature_biotype',
'mito'],
'encoding-type': 'dataframe',
'encoding-version': '0.2.0'}
这些属性标识了数据框的索引,以及列的原始顺序。每一列在这个数据框中被编码为它自己的数组。
>>> store["var"].visititems(print)
ensembl_id <HDF5 dataset "ensembl_id": shape (40145,), type "|O">
feature_biotype <HDF5 group "/var/feature_biotype" (2 members)>
feature_biotype/categories <HDF5 dataset "categories": shape (1,), type "|O">
feature_biotype/codes <HDF5 dataset "codes": shape (40145,), type "|i1">
feature_is_filtered <HDF5 dataset "feature_is_filtered": shape (40145,), type "|b1">
...
>>> store["var"].visititems(print)
ensembl_id <zarr.core.Array '/var/ensembl_id' (40145,) object read-only>
feature_biotype <zarr.hierarchy.Group '/var/feature_biotype' read-only>
feature_biotype/categories <zarr.core.Array '/var/feature_biotype/categories' (1,) object read-only>
feature_biotype/codes <zarr.core.Array '/var/feature_biotype/codes' (40145,) int8 read-only>
feature_is_filtered <zarr.core.Array '/var/feature_is_filtered' (40145,) bool read-only>
...
>>> dict(store["var"]["feature_name"].attrs)
{'encoding-type': 'categorical', 'encoding-version': '0.2.0', 'ordered': False}
>>> dict(store["var"]["feature_is_filtered"].attrs)
{'encoding-type': 'array', 'encoding-version': '0.2.0'}
数据框规格 (v0.2.0)#
数据框必须作为一个组存储
该组的元数据:
必须包含字段
"_index",其值是用于作为索引/行标签的数组的键必须包含编码元数据
"encoding-type": "dataframe","encoding-version": "0.2.0"必须包含
"column-order",一个表示列条目顺序的字符串数组
该组必须包含一个索引的数组
组中的每个条目必须对应一个具有相同第一维度的数组
每个条目应该共享块大小(在HDF5或zarr容器中)
映射#
映射只是存储为 Group 的文件。
这些与数据框和稀疏数组不同,因为它们没有任何特殊属性。
为 AnnData 对象中的任何 Mapping 创建一个 Group,
包括标准的 obsm、 varm、 layers 和 uns。
值得注意的是,这一定义在 uns 中是递归使用的:
>>> store["uns"].visititems(print)
[...]
pca <HDF5 group "/uns/pca" (3 members)>
pca/variance <HDF5 dataset "variance": shape (50,), type "<f8">
pca/variance_ratio <HDF5 dataset "variance_ratio": shape (50,), type "<f8">
[...]
>>> store["uns"].visititems(print)
[...]
pca <zarr.hierarchy.Group '/uns/pca' read-only>
pca/variance <zarr.core.Array '/uns/pca/variance' (50,) float64 read-only>
pca/variance_ratio <zarr.core.Array '/uns/pca/variance_ratio' (50,) float64 read-only>
[...]
映射规范 (v0.1.0)#
每个映射必须是它自己的组
该组的元数据必须包含编码元数据
"encoding-type": "dict","encoding-version": "0.1.0"
标量#
零维数组用于标量值(即单个值,如字符串、数字或布尔值)。这些仅应出现在 uns 内部,并且通常是保存的参数:
>>> store["uns/neighbors/params"].visititems(print)
method <HDF5 dataset "method": shape (), type "|O">
metric <HDF5 dataset "metric": shape (), type "|O">
n_neighbors <HDF5 dataset "n_neighbors": shape (), type "<i8">
random_state <HDF5 dataset "random_state": shape (), type "<i8">
>>> store["uns/neighbors/params"].visititems(print)
method <zarr.core.Array '/uns/neighbors/params/method' () <U4 read-only>
metric <zarr.core.Array '/uns/neighbors/params/metric' () <U9 read-only>
n_neighbors <zarr.core.Array '/uns/neighbors/params/n_neighbors' () int64 read-only>
random_state <zarr.core.Array '/uns/neighbors/params/random_state' () int64 read-only>
>>> store["uns/neighbors/params/metric"][()]
'euclidean'
>>> dict(store["uns/neighbors/params/metric"].attrs)
{'encoding-type': 'string', 'encoding-version': '0.2.0'}
标量规格 (v0.2.0)#
标量必须写成0维数组
数字标量
必须在它们的元数据中包含
"encoding-type": "numeric-scalar","encoding-version": "0.2.0"必须是一个单一的数字值,包括布尔值、无符号整数、带符号整数、浮点数或复浮点数
字符串标量
必须在其元数据中具有
"encoding-type": "string","encoding-version": "0.2.0"在 zarr 中,标量字符串必须作为固定长度的 unicode 数据类型存储
在 HDF5 中,标量字符串必须作为可变长度的 utf-8 编码字符串数据类型存储
分类数组#
>>> categorical = store["obs"]["development_stage"]
>>> dict(categorical.attrs)
{'encoding-type': 'categorical', 'encoding-version': '0.2.0', 'ordered': False}
离散值可以通过分类数组有效表示(类似于 factors 在 R 中)。 这些数组将值编码为小宽度整数 (codes),该整数映射到原始标签集 (categories)。 codes 数组中的每个条目是编码值在 categories 数组中的零基索引。 代表缺失值时,使用代码 -1。 我们将这两个数组分开存储。
>>> categorical.visititems(print)
categories <HDF5 dataset "categories": shape (7,), type "|O">
codes <HDF5 dataset "codes": shape (164114,), type "|i1">
>>> categorical.visititems(print)
categories <zarr.core.Array '/obs/development_stage/categories' (7,) object read-only>
codes <zarr.core.Array '/obs/development_stage/codes' (164114,) int8 read-only>
分类数组规范 (v0.2.0)#
类别数组必须作为一个组存储
该组的元数据必须包含编码元数据
"encoding-type": "categorical","encoding-version": "0.2.0"组的元数据必须包含布尔值字段
"ordered",它指示类别是否是有序的该组必须包含一个名为
"codes"的整数值数组,其最大值为类别数量 - 1该
"codes"数组可以包含有符号整数值。如果是这样,代码-1表示缺失值
该组必须包含一个名为
"categories"的数组
字符串数组#
字符串数组的处理方式与数字数组不同,因为numpy实际上并没有很好的方式来表示unicode字符串的数组。anndata假设字符串是文本类型的数据,因此它使用可变长度编码。
>>> store["var"][store["var"].attrs["_index"]]
<HDF5 dataset "ensembl_id": shape (40145,), type "|O">
>>> store["var"][store["var"].attrs["_index"]]
<zarr.core.Array '/var/ensembl_id' (40145,) object read-only>
>>> dict(categorical["categories"].attrs)
{'encoding-type': 'string-array', 'encoding-version': '0.2.0'}
字符串数组规范 (v0.2.0)#
字符串数组必须存储在数组中
数组的元数据必须包含编码元数据
"encoding-type": "string-array","encoding-version": "0.2.0"在
zarr中,字符串数组必须使用numcodecs的VLenUTF8编解码器存储在
HDF5中,字符串数组必须使用可变长度字符串数据类型存储,编码为utf-8
可空的整数和布尔值#
我们支持与 Pandas 可空整数和布尔数组的输入输出。
我们在磁盘上以类似于 numpy 掩码数组、 julia 可空数组或 arrow 有效位图的方式进行表示(有关更多讨论,请参见 #504)。
也就是说,我们存储一个指示数组(或掩码),它是与所有值数组相伴随的 null 值的指示。
>>> from anndata import write_elem
>>> null_store = h5py.File("tmp.h5", mode="w")
>>> int_array = pd.array([1, None, 3, 4])
>>> int_array
<IntegerArray>
[1, <NA>, 3, 4]
Length: 4, dtype: Int64
>>> write_elem(null_store, "nullable_integer", int_array)
>>> null_store.visititems(print)
nullable_integer <HDF5 group "/nullable_integer" (2 members)>
nullable_integer/mask <HDF5 dataset "mask": shape (4,), type "|b1">
nullable_integer/values <HDF5 dataset "values": shape (4,), type "<i8">
>>> from anndata import write_elem
>>> null_store = zarr.open()
>>> int_array = pd.array([1, None, 3, 4])
>>> int_array
<IntegerArray>
[1, <NA>, 3, 4]
Length: 4, dtype: Int64
>>> write_elem(null_store, "nullable_integer", int_array)
>>> null_store.visititems(print)
nullable_integer <zarr.hierarchy.Group '/nullable_integer'>
nullable_integer/mask <zarr.core.Array '/nullable_integer/mask' (4,) bool>
nullable_integer/values <zarr.core.Array '/nullable_integer/values' (4,) int64>
>>> dict(null_store["nullable_integer"].attrs)
{'encoding-type': 'nullable-integer', 'encoding-version': '0.1.0'}
可空整数规格 (v0.1.0)#
可空整数必须作为一组存储
该组的属性必须包含编码元数据
"encoding-type": "nullable-integer","encoding-version": "0.1.0"该组必须在键
"values"下包含一个整数值数组该组必须在键
"mask"下包含一个布尔值数组
可空布尔规格 (v0.1.0)#
可为空的布尔值必须作为一组存储
该组的属性必须包含编码元数据
"encoding-type": "nullable-boolean","encoding-version": "0.1.0"该组必须包含一个布尔值数组,键为
"values"该组必须在键
"mask"下包含一个布尔值数组数组
"values"和"mask"必须具有相同的形状
尴尬数组#
不规则数组在 anndata 中通过 Awkward Array 库得到支持。为了在磁盘上存储,我们使用 ak.to_buffers 将不规则数组分解为其组成数组,然后使用 anndata 的方法写入这些数组。
>>> store["varm/transcript"].visititems(print)
node1-mask <HDF5 dataset "node1-mask": shape (5019,), type "|u1">
node10-data <HDF5 dataset "node10-data": shape (250541,), type "<i8">
node11-mask <HDF5 dataset "node11-mask": shape (5019,), type "|u1">
node12-offsets <HDF5 dataset "node12-offsets": shape (40146,), type "<i8">
node13-mask <HDF5 dataset "node13-mask": shape (250541,), type "|i1">
node14-data <HDF5 dataset "node14-data": shape (250541,), type "<i8">
node16-offsets <HDF5 dataset "node16-offsets": shape (40146,), type "<i8">
node17-data <HDF5 dataset "node17-data": shape (602175,), type "|u1">
node2-offsets <HDF5 dataset "node2-offsets": shape (40146,), type "<i8">
node3-data <HDF5 dataset "node3-data": shape (600915,), type "|u1">
node4-mask <HDF5 dataset "node4-mask": shape (5019,), type "|u1">
node5-offsets <HDF5 dataset "node5-offsets": shape (40146,), type "<i8">
node6-data <HDF5 dataset "node6-data": shape (59335,), type "|u1">
node7-mask <HDF5 dataset "node7-mask": shape (5019,), type "|u1">
node8-offsets <HDF5 dataset "node8-offsets": shape (40146,), type "<i8">
node9-mask <HDF5 dataset "node9-mask": shape (250541,), type "|i1">
>>> store["varm/transcript"].visititems(print)
node1-mask <zarr.core.Array '/varm/transcript/node1-mask' (5019,) uint8 read-only>
node10-data <zarr.core.Array '/varm/transcript/node10-data' (250541,) int64 read-only>
node11-mask <zarr.core.Array '/varm/transcript/node11-mask' (5019,) uint8 read-only>
node12-offsets <zarr.core.Array '/varm/transcript/node12-offsets' (40146,) int64 read-only>
node13-mask <zarr.core.Array '/varm/transcript/node13-mask' (250541,) int8 read-only>
node14-data <zarr.core.Array '/varm/transcript/node14-data' (250541,) int64 read-only>
node16-offsets <zarr.core.Array '/varm/transcript/node16-offsets' (40146,) int64 read-only>
node17-data <zarr.core.Array '/varm/transcript/node17-data' (602175,) uint8 read-only>
node2-offsets <zarr.core.Array '/varm/transcript/node2-offsets' (40146,) int64 read-only>
node3-data <zarr.core.Array '/varm/transcript/node3-data' (600915,) uint8 read-only>
node4-mask <zarr.core.Array '/varm/transcript/node4-mask' (5019,) uint8 read-only>
node5-offsets <zarr.core.Array '/varm/transcript/node5-offsets' (40146,) int64 read-only>
node6-data <zarr.core.Array '/varm/transcript/node6-data' (59335,) uint8 read-only>
node7-mask <zarr.core.Array '/varm/transcript/node7-mask' (5019,) uint8 read-only>
node8-offsets <zarr.core.Array '/varm/transcript/node8-offsets' (40146,) int64 read-only>
node9-mask <zarr.core.Array '/varm/transcript/node9-mask' (250541,) int8 read-only>
数组的长度保存在它自己的 "length" 属性中,
而数组结构的元数据被序列化并保存到
“form” 属性中。
>>> dict(store["varm/transcript"].attrs)
{'encoding-type': 'awkward-array',
'encoding-version': '0.1.0',
'form': '{"class": "RecordArray", "fields": ["tx_id", "seq_name", '
'"exon_seq_start", "exon_seq_end", "ensembl_id"], "contents": '
'[{"class": "BitMaskedArray", "mask": "u8", "valid_when": true, '
'"lsb_order": true, "content": {"class": "ListOffsetArray", '
'"offsets": "i64", "content": {"class": "NumpyArray", "primitive": '
'"uint8", "inner_shape": [], "parameters": {"__array__": "char"}, '
'"form_key": "node3"}, "parameters": {"__array__": "string"}, '
'"form_key": "node2"}, "parameters": {}, "form_key": "node1"}, '
...
'length': 40145}
这些可以使用
ak.from_buffers
函数作为笨拙的数组读取:
>>> import awkward as ak
>>> from anndata.io import read_elem
>>> awkward_group = store["varm/transcript"]
>>> ak.from_buffers(
... awkward_group.attrs["form"],
... awkward_group.attrs["length"],
... {k: read_elem(v) for k, v in awkward_group.items()}
... )
>>> transcript_models[:5]
[{tx_id: 'ENST00000450305', seq_name: '1', exon_seq_start: [...], ...},
{tx_id: 'ENST00000488147', seq_name: '1', exon_seq_start: [...], ...},
{tx_id: 'ENST00000473358', seq_name: '1', exon_seq_start: [...], ...},
{tx_id: 'ENST00000477740', seq_name: '1', exon_seq_start: [...], ...},
{tx_id: 'ENST00000495576', seq_name: '1', exon_seq_start: [...], ...}]
-----------------------------------------------------------------------
type: 5 * {
tx_id: ?string,
seq_name: ?string,
exon_seq_start: option[var * ?int64],
exon_seq_end: option[var * ?int64],
ensembl_id: ?string
}
>>> transcript_models[0]
{tx_id: 'ENST00000450305',
seq_name: '1',
exon_seq_start: [12010, 12179, 12613, 12975, 13221, 13453],
exon_seq_end: [12057, 12227, 12697, 13052, 13374, 13670],
ensembl_id: 'ENSG00000223972'}
------------------------------------------------------------
type: {
tx_id: ?string,
seq_name: ?string,
exon_seq_start: option[var * ?int64],
exon_seq_end: option[var * ?int64],
ensembl_id: ?string
}