Single-cell analyses in Python keep their cells in an AnnData Virshup et al., 2024, and
scverse tools keep pseudobulk profiles in one of two layouts: a long AnnData with a row per
sample and cell type, as scanpy’s get.aggregate and decoupler’s pp.pseudobulk make
it Wolf et al., 2018Mompel et al., 2022, or a MuData with a modality per cell type, as LIANA
makes it for its analyses of several views Bredikhin et al., 2022Dimitrov et al., 2024. CytePool
pools cells from an AnnData, reads and writes both layouts, and keeps a cube’s
fingerprint through the conversion, so that a cube can go to these tools and come back
as the same cube. The conversions need CytePool’s anndata or mudata extra
(Installation); the cells are those of the simulated PBMC study
(Datasets).
Cells in an AnnData¶
An AnnData of cells has the counts in X, or in a layer, and the sample and cell type
of every cell in obs. from_cells pools them as they are; the sample table comes
from the columns of obs that describe the samples.
from importlib.metadata import version
import anndata
import numpy as np
import pandas as pd
import scipy.sparse
import cytepool
from cytepool.cube import from_cells
from cytepool.io.anndata import from_anndata, from_mudata, to_anndata, to_mudata
arrays = np.load("simulated_pbmc_cells.npz")
sample = arrays["cell_sample"].astype(str)
cells = anndata.AnnData(
X=scipy.sparse.csr_matrix(
(arrays["data"], arrays["indices"], arrays["indptr"]),
shape=tuple(arrays["shape"]),
),
obs=pd.DataFrame(
{
"sample": sample,
"donor": [name.split("_")[0] for name in sample],
"condition": [name.split("_")[1] for name in sample],
"cell_type": arrays["cell_type"].astype(str),
},
index=[f"cell_{i}" for i in range(sample.size)],
),
var=pd.DataFrame(index=arrays["feature_id"].astype(str)),
)
print(f"CytePool {cytepool.__version__}, anndata {version('anndata')}")
print(cells, "\n")
samples = cells.obs[["sample", "donor", "condition"]].drop_duplicates()
cube = from_cells(
cells.X,
samples=cells.obs["sample"],
groups=cells.obs["cell_type"],
sample_table=samples.rename(columns={"sample": "sample_id"}),
features=cells.var_names,
)
print(cube)CytePool 0.1.dev57+gb65bc7e32, anndata 0.13.4
AnnData object with n_obs × n_vars = 19151 × 2000
obs: 'sample', 'donor', 'condition', 'cell_type'
layers: None (.X)
Cube with 24 samples x 6 groups x 2,000 features
layers: counts (uint32), detected (uint32), log_sum (float64), log_sum_sq (float64), linear_sum (float64)
filled: 144 of 144 sample x group entries have cells (100.0%)
samples: sample_id, donor, condition
groups: group_id
features: feature_id
A cube as a long AnnData¶
to_anndata writes a row for every sample and cell type with cells, the layout
that scanpy and decoupler make: obs holds the sample and cell type of each row, the
columns of the sample and group tables and the number of cells, and every layer of the
cube is a layer of the same name. X is left empty, so that no layer is stored twice;
pseudobulk.X = pseudobulk.layers["counts"].copy() gives tools that read X the
counts. uns["cytepool"] records the cube’s fingerprint and provenance, and
from_anndata reads the AnnData back as the same cube, here after a round trip
through an .h5ad file.
pseudobulk = to_anndata(cube)
print(pseudobulk)
pseudobulk.write_h5ad("pseudobulk.h5ad")
back = from_anndata(anndata.read_h5ad("pseudobulk.h5ad"))
print("\nthe same cube after the .h5ad file:", back.fingerprint() == cube.fingerprint())
pseudobulk.obs.head()AnnData object with n_obs × n_vars = 144 × 2000
obs: 'sample_id', 'group_id', 'donor', 'condition', 'n_cells'
uns: 'cytepool'
layers: 'counts', 'detected', 'log_sum', 'log_sum_sq', 'linear_sum'
the same cube after the .h5ad file: True
A cube as a MuData¶
to_mudata writes a modality for every cell type, whose rows are the samples with
cells in it, as LIANA’s multi.adata_to_views makes them, with the columns of the
sample table in the MuData’s obs. from_mudata reads it back.
views = to_mudata(cube)
print(views)
print(
"\nthe same cube after the MuData:",
from_mudata(views).fingerprint() == cube.fingerprint(),
)MuData object with n_obs × n_vars = 24 × 12000
obs: 'donor', 'condition'
uns: 'cytepool'
6 modalities
B cells: 24 × 2000
obs: 'n_cells'
layers: 'counts', 'detected', 'log_sum', 'log_sum_sq', 'linear_sum'
CD4 T cells: 24 × 2000
obs: 'n_cells'
layers: 'counts', 'detected', 'log_sum', 'log_sum_sq', 'linear_sum'
CD8 T cells: 24 × 2000
obs: 'n_cells'
layers: 'counts', 'detected', 'log_sum', 'log_sum_sq', 'linear_sum'
Dendritic cells: 24 × 2000
obs: 'n_cells'
layers: 'counts', 'detected', 'log_sum', 'log_sum_sq', 'linear_sum'
Monocytes: 24 × 2000
obs: 'n_cells'
layers: 'counts', 'detected', 'log_sum', 'log_sum_sq', 'linear_sum'
NK cells: 24 × 2000
obs: 'n_cells'
layers: 'counts', 'detected', 'log_sum', 'log_sum_sq', 'linear_sum'
the same cube after the MuData: True
Profiles that scanpy, decoupler or LIANA pooled¶
from_anndata recognises the layouts of scanpy.get.aggregate and
decoupler.pp.pseudobulk, and from_mudata that of LIANA’s views, and reads from
each the counts, the number of cells that detect each gene and the number of cells of
every profile. Each gives the counts, detecting cells and cell numbers that
from_cells computes from the same cells, down to the fingerprint, as the
Validation page shows for scanpy 1.12.4,
decoupler 2.2.0 and LIANA 1.10.0:
import decoupler as dc
import liana as li
import scanpy as sc
summed = sc.get.aggregate(
cells, by=["sample", "cell_type"], func=["sum", "count_nonzero"]
)
cube = from_anndata(summed, sample="sample", group="cell_type")
pooled = dc.pp.pseudobulk(cells, sample_col="sample", groups_col="cell_type")
cube = from_anndata(pooled, sample="sample", group="cell_type")
views = li.multi.adata_to_views(
cells, groupby="cell_type", sample_key="sample", obs_keys=["condition"]
)
n_cells = pd.crosstab(cells.obs["sample"], cells.obs["cell_type"])
cube = from_mudata(views, n_cells=n_cells)LIANA’s views carry no cell numbers, so they come from the cells, as the crosstab of
their samples and cell types. Means, variances and medians are refused: a cube keeps
sums, and every tool that gives means gives sums too, with func="sum" in scanpy and
mode="sum", its default, in decoupler.
Choices¶
From cells or from profiles.
from_cellson the cells computes every layer of a cube, the sums of normalized values included, and needs no other tool; an import of the profiles that another tool pooled keeps that tool’s choices of cells and samples.Import before filtering. decoupler’s
pp.filter_samples, which LIANA’s views apply when givenfilter_samples_kwargs, drops profiles, and a profile that is missing looks like a sample without cells. Import every profile, and leave out those with few cells in the analysis (min_cellsoflimma_voom).Other layouts. An AnnData of profiles that no tool above made needs
sample=,group=andlayers=, such aslayers={"counts": "X"}, andn_cells=, the name of a column ofobsor a crosstab of the cells.Files.
.h5ad,.zarrand.h5mufiles keep the fingerprint through a round trip; a cube file (Save, share and trace cubes) also reads one cell type at a time.
- Virshup, I., Rybakov, S., Theis, F. J., Angerer, P., & Wolf, F. A. (2024). anndata: access and store annotated data matrices. Journal of Open Source Software, 9(101), 4371. 10.21105/joss.04371
- Wolf, F. A., Angerer, P., & Theis, F. J. (2018). SCANPY: large-scale single-cell gene expression data analysis. Genome Biology, 19(1), 15. 10.1186/s13059-017-1382-0
- Badia-i Mompel, P., Vélez Santiago, J., Braunger, J., Geiss, C., Dimitrov, D., Müller-Dott, S., Taus, P., Dugourd, A., Holland, C. H., Ramirez Flores, R. O., & Saez-Rodriguez, J. (2022). decoupleR: ensemble of computational methods to infer biological activities from omics data. Bioinformatics Advances, 2(1), vbac016. 10.1093/bioadv/vbac016
- Bredikhin, D., Kats, I., & Stegle, O. (2022). MUON: multimodal omics analysis framework. Genome Biology, 23(1), 42. 10.1186/s13059-021-02577-8
- Dimitrov, D., Schäfer, P. S. L., Farr, E., Rodriguez-Mier, P., Lobentanzer, S., Badia-i Mompel, P., Dugourd, A., Tanevski, J., Ramirez Flores, R. O., & Saez-Rodriguez, J. (2024). LIANA+ provides an all-in-one framework for cell-cell communication inference. Nature Cell Biology, 26(9), 1613–1622. 10.1038/s41556-024-01469-w