Learning Single-Cell Analysis with Theis Lab: Format Conversion — Understanding the Underlying Logic (Part 2)
Single-Cell Best Practices
Memory-Based Interoperability
SECTION PREVIEW
- Using Seurat's built-in functions to convert between SingleCellExperiment and Seurat objects
- Using the
rpy2andreticulatepackages for mutual interoperability between R and Python - Understanding format conversion for multimodal data
This section corresponds to Section 5.4 In-memory interoperability in the original book.
In the previous article, we introduced two strategies for format conversion: on-disk conversion and in-memory conversion. In this article, we will focus on the methods and principles behind in-memory interoperability for single-cell data. This approach typically requires running two programming language environments simultaneously, allowing them to access the same object or convert between objects as needed. Usually, one language (such as R) serves as the primary environment, while the other (such as Python) accesses data through an interface. When analyses require real-time interaction between the two languages, this strategy lets researchers work in both environments concurrently. In-memory interoperability is also commonly used when creating multilingual documents (like this tutorial series).
That said, the trade-offs are real: researchers need to be reasonably proficient in both languages, and some complex objects may not be fully supported across both ecosystems. Memory interoperability also places greater demands on hardware, since duplicating data objects consumes additional memory — making it less practical for very large datasets.
01 Interoperability Within the R Ecosystem (SCE & Seurat)
Before discussing cross-language R–Python conversion, let's first look at conversion between the two major single-cell frameworks within R: Seurat and SingleCellExperiment (SCE). This is relatively straightforward, because the Seurat package provides built-in conversion functions for exactly this purpose.
Seurat → SingleCellExperiment:
sce_from_seurat <- Seurat::as.SingleCellExperiment(seurat)
sce_from_seurat
class: SingleCellExperiment
dim: 2000 100
metadata(0):
assays(2): X logcounts
rownames(2000): Gene-0 Gene-1 ... Gene-1998 Gene-1999
rowData names(0):
colnames(100): Cell_0 Cell_1 ... Cell_98 Cell_99
colData names(9): n_genes_by_counts log1p_n_genes_by_counts ...
pct_counts_in_top_500_genes ident
reducedDimNames(2): PCA UMAP
mainExpName: NULL
altExpNames(0):
SingleCellExperiment → Seurat:
seurat_from_sce <- Seurat::as.Seurat(sce_from_seurat)
seurat_from_sce
An object of class Seurat
2000 features across 100 samples within 1 assay
Active assay: RNA (2000 features, 0 variable features)
2 dimensional reductions calculated: PCA, UMAP
The tricky part here is dealing with differences in the internal structure of the two object types — you need to set the correct parameters so the conversion function knows which information to transfer and where to store it. That said, a full conversion from Seurat to SingleCellExperiment is often unnecessary, because many core Bioconductor single-cell analysis packages also accept plain matrices as input:
# Use the Bioconductor scuttle package to compute CPM,
# passing in the counts matrix directly from the Seurat object
cpm <- scuttle::calculateCPM(Seurat::GetAssayData(seurat, slot = "counts"))
02 Accessing R from Python
The rpy2 package allows you to call R functions and access R objects from within a Python session. For example:
counts_mat = adata.layers["counts"].T
rpy2.robjects.globalenv["counts_mat"] = counts_mat
cpm = rpy2.robjects.r("scuttle::calculateCPM(counts_mat)")
cpm
<2000x100 sparse matrix of type '<class 'numpy.float64'>'
with 126146 stored elements in Compressed Sparse Column format>
If you are working in a Jupyter notebook, you can also use the IPython magic interface to create native R code cells:
%%R -i counts_mat -o magic_cpm
# R code below
magic_cpm <- scuttle::calculateCPM(counts_mat)
# Back in Python
magic_cpm # The result computed in R is now available in Python
For single-cell data specifically, the anndata2ri package is particularly useful. It is an extension of rpy2 that allows R to treat AnnData objects as SingleCellExperiment objects, eliminating the need for explicit conversion and enabling R code to operate directly on Python objects:
%%R -i adata
qc <- scuttle::perCellQCMetrics(adata)
head(qc)
sum detected total
Cell_0 2005 1274 2005
Cell_1 1941 1233 1941
Cell_2 2011 1270 2011
Cell_3 1947 1268 1947
Cell_4 1933 1265 1933
Cell_5 2031 1289 2031
Note: if the object contains data types that are not supported by the converter, you may need to modify the object before it can be read correctly.
03 Accessing Python from R
Accessing Python from an R session works in a symmetric fashion, but the interface is provided by the {reticulate} package:
# R
# Convert an R object to a Python object
reticulate_list <- reticulate::r_to_py(LETTERS)
print(reticulate_list)
# Use Python built-in functions
py_builtins <- reticulate::import_builtins()
py_builtins$zip(letters, LETTERS)
If you are working in an RMarkdown or Quarto document, you can also use reticulate's Python engine to write native Python code chunks. This allows you to access objects from the other language using the magic variables r and py:
# R code chunk — accessing a Python object
print(py$py_object)
# Python code chunk — accessing an R object
print(r.r_object)
Unlike anndata2ri, there is currently no R package that provides a direct interface for Python to treat SingleCellExperiment or Seurat objects as AnnData objects. However, you can still access most of an AnnData object's content through reticulate:
# R
# Print the AnnData object from the Python environment
py$adata
# Access the obs slot; pandas DataFrames are automatically converted to R data.frames
head(py$adata$obs)
As mentioned previously, the R package {anndata} also provides an interface for working with AnnData objects in R — but most downstream analysis tools do not yet support this mode. Therefore, when performing complex analyses that require the full object, it may still be necessary to convert the entire object from R to Python (or vice versa). While this approach requires a full copy of the data and consumes substantial memory, it also provides much broader compatibility with analysis tools. This is where packages like {zellkonverter} — mentioned in the previous article — come in:
# Convert AnnData to SingleCellExperiment
sce <- zellkonverter::AnnData2SCE(adata, verbose = TRUE)
# Convert back to AnnData
adata2 <- zellkonverter::SCE2AnnData(sce, verbose = TRUE)
04 Interoperability for Multimodal Data
The complexity of multimodal data introduces additional challenges for interoperability. Different data structures handle multiple modalities in different ways:
- SingleCellExperiment: stores multiple modalities as "alternative experiments" (
altExps), requiring alignment along the cell dimension (columns) - Seurat: stores multiple modalities as separate "assays"
- AnnData: is inherently single-modal and cannot directly store multiple modalities
To address this limitation, the MuData object was developed. Its designers had interoperability in mind from the start. Although MuData's primary home is Python, the authors provide:
{MuDataSeurat}R package: reads the on-disk H5MU format into a Seurat object{MuData}R package: reads the H5MU format into a BioconductorMultiAssayExperimentobject
While this official support is highly practical, some inconsistencies remain due to structural differences between the ecosystems.
Using MuData in Python
In Python, using MuData is very intuitive:
# Read an H5MU file
mdata = mudata.read_h5mu("datasets/original.h5mu")
print(mdata)
# Save to a new H5MU file
python_h5mu_file = Path(temp_dir.name) / "python.h5mu"
mdata.write_h5mu(python_h5mu_file)
MuData object with n_obs × n_vars = 411 × 56
obs: 'louvain', 'leiden', 'leiden_wnn', 'celltype'
var: 'gene_ids', 'feature_types', 'highly_variable'
obsm: 'X_mofa', 'X_mofa_umap', 'X_umap', 'X_wnn_umap'
...
2 modalities
prot: 411 x 29
...
rna: 411 x 27
...
Using MuData in R
In R, MuData can be used in two main ways:
1. Bioconductor
Use the {MuData} package to read MuData into a MultiAssayExperiment object:
# Read as a MultiAssayExperiment object
mae <- MuData::readH5MU("datasets/original.h5mu")
print(mae)
# Save as an H5MU file
bioc_h5mu_file <- tempfile(fileext = ".h5mu")
MuData::writeH5MU(mae, bioc_h5mu_file)
A MultiAssayExperiment object of 2 listed
experiments with user-defined names and respective classes.
Containing an ExperimentList class object of length 2:
[1] prot: SingleCellExperiment with 29 rows and 411 columns
[2] rna: SingleCellExperiment with 27 rows and 411 columns
05 Interoperability with Other Languages
Beyond R and Python, single-cell data can also interoperate with other programming languages:
Julia
- Muon.jl: supports reading and writing AnnData/MuData objects and H5AD/H5MU files
- scVI.jl: supports interaction via the H5AD format
JavaScript
- Vitessce: supports AnnData visualization via the Zarr format
- kana toolset: can parse H5AD and RDS formats
Rust
- anndata-rs: a high-performance H5AD read/write implementation
06 Postscript
This section concludes the "Introduction" portion of the series. By now, you should have a solid foundational understanding of the key workflows and tools used in single-cell analysis. Starting from the next installment, we will enter the formal downstream analysis pipeline, beginning with quality control — including how to select appropriate QC thresholds and remove the effects of low-quality cells, doublets/multiplets, and ambient RNA. Reply with the keyword scbp05 in the backend to access the original English text and complete code for this chapter.
The most rigorous single-cell analysis tutorial series, written by leading experts in the field, continues! If you'd like to follow along with new updates, give us a ✨ so the latest posts appear on your homepage:
Previous tutorials in this series ⬇️:
Learning Single-Cell with Theis Lab: MuData — Your Companion for Multimodal Data
Learning Single-Cell with Theis Lab: An Introduction to AnnData
Learning Single-Cell with Theis Lab: Scanpy + Muon — Your Single-Cell Swiss Army Knife
Originally posted on WeChat Official Account: bioinfo_studyroom