磁盘上的格式#

注意

这些文档是为 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-typeencoding-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)

  • 该组可能包含一个映射 layerslayers中的条目必须是具有形状 (n_obs, n_var) 的密集或稀疏数组

  • 该组可能包含一个映射 obsmobsm 中的条目必须是稀疏数组、密集数组或数据框。这些条目必须具有大小为 n_obs 的第一维

  • 该组MAY包含一个映射varm。在varm中的条目MUST是稀疏数组、密集数组或数据框。这些条目MUST具有大小为n_var的第一维

  • 该组可能包含一个映射 obsp。在 obsp 中的条目必须是稀疏或密集数组。条目的前两个维度必须为 n_obs 的大小

  • 该组 MAY 包含一个映射 varpvarp 中的条目 MUST 是稀疏或密集数组。 条目的前两个维度 MUST 的大小为 n_var

  • 该组可能包含一个映射 unsuns 中的条目必须是 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_matrixscipy.sparse.csr_matrix)。这些格式通过三个一维数组表示一个二维稀疏数组,indptrindicesdata

注意

这些格式的完整描述超出了本文档的范围,但很容易被找到

我们将稀疏数组表示为一个 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)#

  • 每个稀疏数组必须是其自己的组

  • 该组必须包含数组 indicesindptrdata

  • 该组的元数据必须包含:

    • "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, 包括标准的 obsmvarmlayersuns。 值得注意的是,这一定义在 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}

离散值可以通过分类数组有效表示(类似于 factorsR 中)。 这些数组将值编码为小宽度整数 (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 中,字符串数组必须使用 numcodecsVLenUTF8 编解码器存储

  • 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" 必须具有相同的形状

尴尬数组#

警告

实验

通过awkward array对不规则数组的支持在0.9.0发行系列中被视为实验性。请将对其实现的反馈发送给 scverse/anndata

不规则数组在 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
}