AnnData and MuData#
scverse keeps pseudobulk profiles in two layouts, and CytePool reads and writes both.
to_anndata() and from_anndata()
convert a cube to and from a long AnnData, with a row per sample and cell type, as
scanpy.get.aggregate and decoupler.pp.pseudobulk make it;
to_mudata() and from_mudata()
convert it to and from a MuData with a modality per cell type and the samples as its
rows, as LIANA’s adata_to_views makes it. An import takes only sums and counts of
cells, so a cube made from aggregated profiles holds what one made from the cells
would hold, and a cube exported and read back has the same fingerprint. anndata and
mudata are optional: install CytePool’s anndata or mudata extra.
Converting cubes to and from AnnData and MuData, the data structures of scverse.
A cube holds a samples x features matrix for every group, and an AnnData (Virshup et
al. 2024) holds 2-D matrices only (since anndata 0.13, X and every layer must be
2-D). scverse therefore keeps pseudobulk profiles in one of two layouts, and CytePool
reads and writes both:
a long AnnData, with one row per sample and group, as
scanpy.get.aggregate(Wolf, Angerer and Theis 2018) anddecoupler.pp.pseudobulk(Badia-i-Mompel et al. 2022) make it:to_anndataandfrom_anndata;a MuData (Bredikhin, Kats and Stegle 2022) with one modality per group and the samples as its observations, as LIANA’s
liana.multi.adata_to_views(Dimitrov et al. 2024) makes it:to_mudataandfrom_mudata.
anndata and mudata are optional dependencies: install CytePool’s anndata or
mudata extra, or the packages themselves. Only to_anndata and to_mudata
import them; from_anndata and from_mudata read the object they are given.
- What an import takes:
A cube’s layers are sums over the cells of each sample and group, so an import takes only what is such a sum or gives one exactly: summed counts, the number of cells detecting each feature, and sums of normalized or log-normalized values. The source of a cube layer is
Xor a layer of the AnnData (of every modality, for a MuData), read as sums, except where its name says what scanpy or decoupler stored in it:count_nonzero(scanpy) counts the cells with a nonzero value, and givesdetectedonly;psbulk_props(decoupler) is the fraction of cells with a nonzero value. It givesdetectedwith the cell numbers: each fraction times its number of cells must be a whole number, to within the precision the fractions are stored in, and rounding it recovers the number of cells exactly;mean,varandmedianhold means, variances and medians, which are refused. A variance or a median gives no sum, and a mean times its number of cells gives the sum exactly only where the sum is a whole number; every tool that gives means gives sums too: aggregate withfunc="sum"(scanpy) ormode="sum"(decoupler).
Counts and
detectedbecome integers, asfrom_cellsmakes them: uint32 when every value fits, uint64 otherwise, and float64 for counts that are not whole numbers; sums of whole numbers that a tool stored as floats become integers. Counts are checked against the cells thatdetectedcounts: a sum of counts is positive exactly where a cell has a positive count, and a sum of whole counts (counts that come as integers, or from a recognised layout) is at least the number of such cells, which means, medians and counts of other cells break.A combination of sample and group without a row is an entry without cells, which only cell numbers can record: an import with such a combination and no cell numbers is refused. A row removed after pooling, as by decoupler’s
filter_samplesor LIANA’smin_cells, is such a combination too, so its cells are then missing from merged groups and pooled samples of the cube: import the profiles before filtering them, and leave out samples with few cells in the analysis instead (limma_voomhasmin_cells).- Recognised layouts:
The arguments left as None take their values from the layout of the data; a long AnnData that CytePool did not make still needs
sample=andgroup=, the columns of its labels.CytePool’s own export, which
uns["cytepool"]marks (see “The record in uns” below): every layer, the cell numbers, the tables, the normalization and the label columns, as they were exported.decoupler.pp.pseudobulk(anobscolumnpsbulk_cells, orpsbulk_n_cellsas older versions name it): counts fromX, cell numbers from that column, anddetectedfrompsbulk_props. Whateverlayersgives,Xmust be a sum of counts: positive exactly wherepsbulk_propsis, which medians are not; where whole, at least the number of detecting cells that the fractions bound even without cell numbers, which maxima and means seldom are; and, row by row, at most the total of the pooled counts that decoupler computes, whatever itsmode, aspsbulk_counts(less after a filter of the features, such asdecoupler.pp.filter_by_expr), without being that total over the cells, as means are.scanpy.get.aggregate(anobscolumnn_obs_aggregated): counts from the layersum,detectedfromcount_nonzerowhen present, and cell numbers fromn_obs_aggregated.liana.multi.adata_to_views, for a MuData whose every modality has apsbulk_propslayer: counts fromX, checked as decoupler’s, and cell numbers, and sodetected, fromuns["psbulk_stats"]when it holds them. LIANA keeps cell numbers only when itskeep_statsfinds decoupler’s columnpsbulk_n_cells, which decoupler 2 namespsbulk_cells, so they are usually missing: passn_cells, for examplepandas.crosstabof the cells’ sample and group labels.
Counts that a recognised layout gives must be whole numbers; give
layersto import counts that are not, or to read asumof normalized values. Data of no recognised layout needlayers.- Export:
to_anndatawrites a row for every entry with cells (every entry of a cube without cell numbers), the rows of each group together, groups and samples in the cube’s order. Entries without cells have no row, as inscanpy.get.aggregate.obsholdssample_idandgroup_id, as categorical columns, the other columns of the sample and group tables, andn_cells;obs_namesare<sample id>_<group id>, or the row positions when those repeat.varholds the feature table, indexed by feature id, and each layer of the cube is a layer of the same name.Xis left empty, so that no layer is stored twice:adata.X = adata.layers["counts"].copy()gives tools that readXthe counts.to_mudatawrites a modality per group, named by the group id, whose observations are the samples with cells in that group (every sample, without cell numbers), named by sample id, withn_cellsin itsobs, every layer under its own name, andXempty. Itsvar_namesare<group id>:<feature id>, as LIANA writes them, so that they are unique across modalities;varholds the other columns of the feature table. The columns of the sample table are on the MuData’sobs. A MuData file cannot store a modality whose name holds/or NUL, or is.or..: rename such a group first withCube.regroup.anndata writes only some columns, so table columns are written in a form it stores and reads back as they were: datetimes as ISO 8601 text (ISO 2019),
NaTwhere missing, timedeltas as int64 counts of their unit, and a text column without any value as float64 NaN. Column names have_in place of/and NUL, and.,..and_index, which anndata reserves, end in_. A sample or group column whose name is taken is prefixed withsample_orgroup_.- The record in uns:
uns["cytepool"](of the AnnData, or of the MuData) holdsformat("cytepool.cube.anndata"or"cytepool.cube.mudata"),format_version(1), the cube’sfingerprint, and as JSON text itsprovenanceandnormalization, itslayerswith the type of each, whether it has cell numbers (n_cells), itssamplesandgroupstables, with the samples and groups that have no row, and the name,varcolumn and type of each column of itsfeaturestable. Only text and integers are stored, which every anndata writer keeps.Reading the record, an import gives back the cube: the same tables (column types included), layers of the same types and the same values, so the same fingerprint, unless the data were changed in between. A layer converted to another type is converted back when that keeps its values; a layer whose values its type no longer holds was changed, and is refused (give
layersto read it as what it holds). Every argument overrides what the record gives. The tables of samples and groups come from the record, soobscolumns added later are not read; features come fromvar, so a subset of features gives a cube of those features. The provenance is the exported cube’s, followed by a record of the import with the fingerprint that the export stored:cube.fingerprint() == adata.uns["cytepool"]["fingerprint"]says whether the AnnData still holds the exported cube.
Example
>>> import numpy as np
>>> from cytepool.cube import from_cells
>>> cube = from_cells(
... np.array([[1, 0], [3, 2], [0, 4]]),
... samples=["S1", "S1", "S2"],
... groups=["B", "T", "B"],
... layers=["counts", "detected"],
... )
>>> adata = to_anndata(cube)
>>> adata.obs_names.tolist()
['S1_B', 'S2_B', 'S1_T']
>>> from_anndata(adata).fingerprint() == cube.fingerprint()
True
References
Virshup I, Rybakov S, Theis FJ, Angerer P, Wolf FA (2024). anndata: access and store annotated data matrices. Journal of Open Source Software 9, 4371. doi:10.21105/joss.04371
Bredikhin D, Kats I, Stegle O (2022). MUON: multimodal omics analysis framework. Genome Biology 23, 42. doi:10.1186/s13059-021-02577-8
Wolf FA, Angerer P, Theis FJ (2018). SCANPY: large-scale single-cell gene expression data analysis. Genome Biology 19, 15. doi:10.1186/s13059-017-1382-0
Badia-i-Mompel P, Velez Santiago J, Braunger J, Geiss C, Dimitrov D, Mueller-Dott S, Taus P, Dugourd A, Holland CH, Ramirez Flores RO, Saez-Rodriguez J (2022). decoupleR: ensemble of computational methods to infer biological activities from omics data. Bioinformatics Advances 2, vbac016. doi:10.1093/bioadv/vbac016
Dimitrov D, Schaefer PSL, Farr E, Rodriguez-Mier P, Lobentanzer S, Badia-i-Mompel P, Dugourd A, Tanevski J, Ramirez Flores RO, Saez-Rodriguez J (2024). LIANA+ provides an all-in-one framework for cell-cell communication inference. Nature Cell Biology 26, 1613-1622. doi:10.1038/s41556-024-01469-w
ISO (2019). ISO 8601-1:2019, Date and time: representations for information interchange, part 1: basic rules. International Organization for Standardization, Geneva.
- cytepool.io.anndata.to_anndata(cube)#
Return a cube as a long AnnData, with a row per sample and group with cells.
The rows of each group come together, groups and samples in the cube’s order; a cube without cell numbers gives a row for every entry.
obsholdssample_idandgroup_id(categorical), the other columns of the sample and group tables, andn_cells;varthe feature table, indexed by feature id; each layer of the cube is a layer of the same name, andXis empty.uns["cytepool"]holds the cube’s fingerprint and provenance, and whatfrom_anndataneeds to give back the cube with the same fingerprint. The module documentation describes the layout (“Export”) and the record (“The record in uns”).- Parameters:
cube – The cube. A lazy cube is read one group at a time.
- Returns:
An
anndata.AnnData.- Raises:
TypeError – If
cubeis not a Cube.ImportError – If anndata is not installed.
ValueError – If a lazy layer gives a value that breaks a constraint (see
Cube).OverflowError – If a sum of a derived lazy layer does not fit.
Explained in: The cube and its additive layers
- cytepool.io.anndata.from_anndata(adata, *, sample=None, group=None, layers=None, n_cells=None, sample_table=None, group_table=None, normalization=None, source=None)#
Build a cube from a long AnnData of pooled profiles, a row per sample and group.
Each row is the profile of the cells of one sample and group, as
scanpy.get.aggregateanddecoupler.pp.pseudobulkmake them; a combination without a row is an entry without cells. With the defaults, the layout of the AnnData chooses the source of each layer and of the cell numbers, and an AnnData thatto_anndatamade gives back its cube with the same fingerprint. The module documentation says what each source may hold (“What an import takes”) and which layouts are recognised.- Parameters:
adata – The AnnData.
sample – The
obscolumn with the sample of each row, as text or integers (which become their decimal text), asfrom_cellstakes labels. Required unless CytePool made the AnnData.group – The
obscolumn with the group, usually the cell type, of each row. Required unless CytePool made the AnnData.layers – Cube layer name to its source,
"X"or the name of a layer, for example{"counts": "sum", "detected": "count_nonzero"}.countsis required. By default the sources of the recognised layout.n_cells – The cells pooled into each row: the name of an
obscolumn, or a table with samples as rows and groups as columns, aligned by its labels, such aspandas.crosstab(cells.obs["donor"], cells.obs["cell_type"])for the cells that were pooled. By default the column of the recognised layout, or none.sample_table – Sample metadata with a text column
sample_idthat holds every sample label, asfrom_cellstakes it: its rows set the order of the samples, and a sample without rows has no cells. By default the samples are the sorted distinct labels, with theobscolumns that are constant within every sample, over the rows with cells.group_table – Group metadata, used in the same way. By default the groups are the sorted distinct labels, with the other
obscolumns that are constant within every group.normalization – How the normalized values were computed, required with
log_sum,log_sum_sqorlinear_sum(seeCube).source – A record of where the profiles came from, as a JSON-serialisable mapping, kept in the cube’s provenance as given (see
from_cells).
- Returns:
A cube. Its provenance holds
sourceand how the AnnData was read; for an AnnData that CytePool made, it is the provenance of the exported cube, followed by a record of this import (cytepool.cube.provenancedocuments both).- Raises:
TypeError – If
adatais not an AnnData, an argument has the wrong type, a source does not hold numbers, a column ofvarcannot be a column of the feature table, or an argument is or holds a numpy masked array.KeyError – If a column or layer it names is missing.
ValueError – If a source holds means, variances or medians, or values that are not the sums, counts or fractions it is read as, such as counts that are not sums over the cells that
detectedcounts; a layer that CytePool exported was changed; a row repeats an entry; a label is missing or not in a given table; a combination has no row and there are no cell numbers to mark it as an entry without cells; a value breaks a constraint ofCube; oruns["cytepool"]is not a record this version reads.
Explained in: The cube and its additive layers, Cube files and provenance records
- cytepool.io.anndata.to_mudata(cube)#
Return a cube as a MuData with a modality per group, the samples as rows.
Each modality is named by its group id and holds the samples with cells in that group (every sample, for a cube without cell numbers), named by sample id, with
n_cellsin itsobs, every layer of the cube under its own name,Xempty, and the feature table invar, whose names are<group id>:<feature id>as LIANA writes them. The columns of the sample table are on the MuData’sobs, anduns["cytepool"]holds the cube’s fingerprint, provenance and whatfrom_mudataneeds to give back the cube with the same fingerprint (see “Export” and “The record in uns” in the module documentation).- Parameters:
cube – The cube. A lazy cube is read one group at a time.
- Returns:
A
mudata.MuData.- Raises:
TypeError – If
cubeis not a Cube.ImportError – If anndata or mudata is not installed.
ValueError – If the cube has no groups (
to_anndatakeeps such a cube), or a lazy layer gives a value that breaks a constraint (seeCube).OverflowError – If a sum of a derived lazy layer does not fit.
Explained in: The cube and its additive layers
- cytepool.io.anndata.from_mudata(mdata, *, layers=None, n_cells=None, sample_table=None, group_table=None, normalization=None, source=None)#
Build a cube from a MuData with a modality per group and the samples as rows.
Each modality is a group, named by the modality’s name, and each of its rows the profile of a sample, named by its observation name, as LIANA’s
liana.multi.adata_to_viewsmakes them; a sample without a row in a modality has no cells in that group. Every modality must hold the same features: itsvar_names, without the prefix<modality name>:that LIANA adds. The feature table is thevarof the first modality, and the samples’ metadata the columns of the MuData’sobs. With the defaults, the layout chooses the sources, and a MuData thatto_mudatamade gives back its cube with the same fingerprint (see the module documentation).- Parameters:
mdata – The MuData.
layers – Cube layer name to its source in every modality,
"X"or the name of a layer.countsis required. By default the sources of the recognised layout.n_cells – The cells pooled into each row: the name of an
obscolumn of every modality, or a table with samples as rows and groups as columns, aligned by its labels, such aspandas.crosstabof the cells’ sample and group labels. By default the cell numbers of the recognised layout, or none.sample_table – Sample metadata with a text column
sample_idthat holds every sample, as infrom_anndata. By default the samples are the sorted distinct observation names, with the columns of the MuData’sobs.group_table – Group metadata with a text column
group_idthat holds every modality name. By default the groups are the sorted modality names.normalization – How the normalized values were computed, required with
log_sum,log_sum_sqorlinear_sum(seeCube).source – A record of where the profiles came from, kept in the cube’s provenance as given (see
from_cells).
- Returns:
A cube, with a provenance as
from_anndatagives it.- Raises:
TypeError – If
mdatais not a MuData, an argument has the wrong type, a source does not hold numbers, or an argument is or holds a numpy masked array.KeyError – If a column or layer it names is missing from a modality.
ValueError – If the modalities hold different features, or for the reasons
from_anndatagives.
Explained in: The cube and its additive layers, Cube files and provenance records