Chatomics Field GuideWhat They Don't Teach You

Comparison · container format

SingleCellExperiment vs AnnData: Which One Should You Use?

The matrices are transposed relative to each other, and that mismatch is where most conversion bugs and silent data loss actually start.

By Ming "Tommy" Tang, Director of Bioinformatics in Big Pharma · Reviewed September 2026 · 4 min read

The verdict

Default to AnnData/h5ad as your storage and sharing format for new single-cell projects. h5ad is a documented on-disk format with a public spec, it's what CELLxGENE Discover requires for submission (AnnData >= 0.8.0), it documents sparse CSR/CSC encoding as part of the file format, and its embeddings (obsm) share the same cells-as-rows orientation as X, so you don't get the orientation flip that SingleCellExperiment has between assays and reducedDims. And since anndataR now reads and writes h5ad natively in R, choosing AnnData as your interchange format no longer locks you out of R if that's where you live.

The right call flips to SingleCellExperiment when your actual analysis work runs through scran, scater, or the OSCA workflow, or when you're storing spike-ins or antibody capture data alongside your main assay: altExp is purpose-built for that, and sizeFactors/colLabels are first-class slots kept in sync automatically on subsetting, instead of living as loosely-named columns you have to remember to preserve. If your pipeline is Bioconductor end to end, build in SingleCellExperiment from the start and only materialize an h5ad when you need to hand data to a Python tool or a public repository.

Both objects do the same job on paper: hold an expression matrix next to the metadata that gives it meaning, so you stop passing five separate variables around your analysis. The difference is which language ecosystem they were built for, and that choice ripples into everything else about them.

SingleCellExperiment is an S4 object extending Bioconductor's SummarizedExperiment. It has fixed, named slots: assays for expression matrices, colData for cell metadata, rowData for gene annotations, reducedDims for PCA/UMAP, altExp for alternative feature sets like spike-ins or antibody tags, plus dedicated sizeFactors and colLabels slots. Subsetting or combining an SCE keeps every slot in sync automatically, so you don't end up with metadata rows that no longer line up with the matrix columns.

AnnData is the object behind Scanpy and the scverse stack, backed by an HDF5 file format on disk (.h5ad). It's X (the primary matrix) plus obs/var (cell and gene metadata), layers (alternative matrices of the same shape as X), obsm/varm (embeddings), and uns (a catch-all for unstructured metadata). The most consequential difference: SCE's main assay is genes-by-cells, AnnData's X is cells-by-genes. Every conversion tool between the two has to transpose the matrix, and any hand-rolled indexing code that doesn't account for this will silently produce wrong results, not an error.

Head to head

CriterionSingleCellExperimentAnnDataEdge
Matrix orientationMain assay in assays() is genes-by-cells: rows are genes, columns are cells.X is cells-by-genes: rows are cells (obs), columns are genes (var).Tie
This is the single biggest source of conversion bugs; every conversion tool has to transpose the matrix, and manual code that doesn't will run without error but produce wrong results.
Object systemS4 object extending Bioconductor's SummarizedExperiment, with fixed named slots (assays, colData, rowData, reducedDims, altExp, sizeFactors, colLabels).Python class backed by an HDF5 (.h5ad) on-disk format, with attributes X, obs, var, layers, obsm/varm, uns.Tie
Dedicated slots for normalization and cluster labelssizeFactors and colLabels are first-class slots built into the object, kept in sync automatically when you subset or combine.No equivalent named slot; normalization factors and cluster labels live as ordinary columns in obs, tracked by convention rather than enforced by the object.SingleCellExperiment
Less risk of a downstream script quietly overwriting or losing this metadata.
Alternative feature sets (spike-ins, ADT/antibody tags)altExp stores an alternative feature set with the same cells as the main assay, purpose-built for spike-ins and antibody capture data.layers holds alternative matrices of identical shape to X (same genes and cells), built for alternative values of the same features, not a distinct feature set.SingleCellExperiment
Orientation consistency across slotsreducedDims stores PCA/UMAP with cells as rows, the opposite orientation from the main assay, which has cells as columns.obsm stores embeddings with cells as rows, matching X's own cells-as-rows orientation.AnnData
Fewer transpose mistakes when writing custom code against AnnData.
Canonical on-disk sharing formatNo single canonical on-disk file; objects are typically serialized as .rds, which is R/Bioconductor-specific.h5ad is a documented HDF5 format; CELLxGENE Discover requires it (AnnData >= 0.8.0) for public dataset submission.AnnData
Sparse matrix storageAssays commonly hold R Matrix-package sparse types (e.g. dgCMatrix); sparse support comes from that ecosystem, not from the SCE spec itself.The on-disk h5ad format explicitly documents CSR and CSC sparse encodings as part of the file spec.AnnData
Cross-language access to the other formatanndataR reads and writes .h5ad natively in R (read_h5ad(), as_SingleCellExperiment()) with no Python dependency.Getting an AnnData object out of an R SingleCellExperiment still commonly goes through zellkonverter, which needs reticulate and basilisk to manage a Python environment.SingleCellExperiment
It's now easier to bring AnnData into R than to push an SCE into Python.
Ecosystem and tool coverageBacks scran, scater, and the OSCA book: the standard Bioconductor QC, normalization, and clustering workflow.Backs Scanpy and the broader scverse stack, including scvi-tools and squidpy.Tie
Pick based on where your downstream analysis tools actually live, not on the container format itself.

Use SingleCellExperiment when

  • Your normalization and clustering pipeline runs through scran and scater following the OSCA workflow.
  • You need sizeFactors and colLabels tracked as enforced, first-class slots instead of freeform metadata columns.
  • You're storing spike-in controls or antibody-derived tags (CITE-seq) alongside the main assay and want altExp to keep them cell-aligned automatically.
  • Your team or CI pipeline is built around SummarizedExperiment-derived objects and other Bioconductor infrastructure.
  • You want subsetting and merging operations to automatically keep every metadata slot in sync with the matrix.

Use AnnData when

  • You're building on Scanpy, scvi-tools, squidpy, or another scverse tool.
  • You need to submit to or pull data from CELLxGENE Discover, which requires h5ad with AnnData 0.8.0 or greater.
  • You want one consistent cells-as-rows orientation across X, obsm, and layers instead of the orientation flip between SCE's assays and reducedDims.
  • You're distributing a dataset outside your own R session and want a documented, language-agnostic on-disk format with native sparse (CSR/CSC) support.
  • You're primarily working in Python and want to avoid the reticulate/basilisk bridging overhead needed to move data the other direction.

Switching between them

The transpose is not optional and not automatic if you write your own conversion code: SCE's main assay is genes-by-cells, AnnData's X is cells-by-genes, so every element-wise or matrix operation you've already written for one needs re-checking against the other's orientation. reducedDims (SCE, cells as rows) maps to obsm (AnnData, also cells as rows) without a further flip, since AnnData is internally consistent on that axis while SCE isn't.

sizeFactors and colLabels have no dedicated AnnData slot; expect them to land as ordinary columns in obs after conversion, and confirm they made it there under a name you recognize rather than assuming the tool preserved them under the original slot name. altExp (spike-ins, antibody capture) has no direct AnnData equivalent either; anndataR's documentation states it handles alternative experiments across both conversion directions, but since the target isn't a first-class AnnData concept the way altExp is native to SCE, verify shapes and keys post-conversion rather than trusting the round trip blind.

For tooling: anndataR's read_h5ad(h5ad_file, as = "SingleCellExperiment") and as_SingleCellExperiment(anndata_object) go AnnData-to-R natively, no Python required. Going the other way, zellkonverter's SCE2AnnData() and AnnData2SCE() bridge through reticulate and basilisk, and you should run AnnDataDependencies() first to check and pin the Python anndata package version, since version mismatches between the R and Python sides are a known source of breakage. sceasy, anndata2ri, and convert2anndata are alternative converters worth knowing about if one path fails on your specific object. None of the source documentation used here details benchmark numbers or specific data-loss edge cases for datasets over a million cells, so treat any large-scale round trip as untested until you've checked it yourself: convert a small subset first and diff the metadata before running it on the full object.

Pitfalls with either

  • Writing manual conversion or indexing code that assumes matrix orientation carries over unchanged: SCE is genes-by-cells and AnnData's X is cells-by-genes, so the code runs without error but silently returns wrong values. Fix: always transpose explicitly, or better, use anndataR/zellkonverter rather than hand-rolling the conversion.
  • Assuming reducedDims and the main assay share the same orientation inside SingleCellExperiment: reducedDims stores cells as rows, the main assay stores cells as columns. Fix: check dim() on each slot explicitly before writing custom accessor code, don't assume consistency.
  • Running zellkonverter without checking the Python anndata package version first: mismatches between the R-side expectations and the installed Python anndata version cause conversion failures or subtly wrong output. Fix: run AnnDataDependencies() to verify and pin the environment before converting.
  • Assuming sizeFactors and colLabels survive a round trip through AnnData under recognizable names: AnnData has no dedicated slot for either, so they land as generic obs columns and can be silently dropped or renamed by the conversion tool. Fix: after converting, explicitly inspect obs and uns for these fields rather than assuming they're there.
  • Trusting that altExp (spike-ins, antibody capture) converts cleanly into an equivalent AnnData structure: altExp is native to SCE with no one-to-one AnnData slot, so the target shape depends entirely on how the conversion tool chooses to represent it. Fix: confirm shapes and keys manually after conversion instead of assuming a lossless mapping.

Questions people ask

Can I convert between SingleCellExperiment and AnnData without losing data?

Mostly, using anndataR or zellkonverter, which handle assays, dimensional reductions, and metadata in both directions. But SingleCellExperiment's dedicated sizeFactors and colLabels slots have no equivalent named slot in AnnData, so after conversion, check obs and uns to confirm those values landed somewhere and weren't dropped or renamed.

Why are my gene expression values transposed after I convert?

Because the two formats store the matrix in opposite orientations: SingleCellExperiment assays are genes-by-cells, AnnData's X is cells-by-genes. Conversion tools transpose automatically, but any manual conversion code or custom accessor you write has to do this explicitly or you'll get a matrix that runs, but is wrong.

Do I need Python to read an h5ad file in R?

No. The anndataR package reads and writes .h5ad files natively in R via read_h5ad() and as_SingleCellExperiment(), with no Python dependency. That's a newer, lighter path than the older zellkonverter route, which bridges to Python through reticulate and basilisk.

Which format does CELLxGENE Discover require?

CELLxGENE Discover requires the HDF5-backed AnnData format (h5ad), specifically AnnData version 0.8.0 or greater, as its canonical submission format.

What happens to SingleCellExperiment's altExp slot when I convert to AnnData?

anndataR's documentation states it handles alternative experiments in both conversion directions, but altExp is a concept native to SCE with no direct one-to-one AnnData slot. Always verify shapes and keys after conversion rather than assuming the round trip is exact.

Related pages

Sources

  1. The SingleCellExperiment Class - OSCA Introduction — Slot structure, altExp, sizeFactors, colLabels, reducedDims orientation
  2. AnnData On-disk Format Documentation — X/obs/var/layers/obsm/uns structure and CSR/CSC sparse encoding
  3. anndataR: Using SingleCellExperiment with H5AD Files — Matrix transposition, read_h5ad(), as_SingleCellExperiment(), bidirectional conversion coverage
  4. zellkonverter: Converting Between scRNA-seq Objects — SCE2AnnData()/AnnData2SCE(), reticulate/basilisk dependency, AnnDataDependencies()
  5. Chapter 4 Data Infrastructure - OSCA — Bioconductor vs scverse ecosystem split
  6. CELLxGENE Single-Cell Curation Schema — h5ad and AnnData >=0.8.0 requirement for CELLxGENE Discover
  7. anndata2ri: Convert between AnnData and SingleCellExperiment — Additional conversion tooling
  8. sceasy: Convert Single-Cell Data Formats — Additional conversion tooling
  9. convert2anndata: SCE and Seurat to AnnData Conversion — Additional conversion tooling