Chatomics Field GuideWhat They Don't Teach You

Conversion · Seurat object (RDS) → SingleCellExperiment (RDS)

How to Convert Seurat object to SingleCellExperiment (Without Losing Your Metadata)

as.SingleCellExperiment() is one line, but that line decides which assay becomes primary, drops your scaled matrix, and can hand back an empty counts assay without ever throwing an error.

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

Seurat object (RDS)
.rds, .RDS · coordinates: n/a
SingleCellExperiment (RDS)
.rds · coordinates: n/a

You need this conversion the moment your Seurat-based QC and clustering results have to talk to a Bioconductor pipeline: scran for size-factor normalization, scater for QC diagnostics, or anything built on the OSCA book. as.SingleCellExperiment() is the built-in bridge, and for a clean object it really is one line.

That one line makes decisions you didn't explicitly ask for. It keeps counts and logcounts and drops scale.data outright, because Bioconductor treats scaling as a reproducible downstream step rather than stored data. It turns your first (or specified) assay into the SCE's main experiment and pushes every other assay - ADT counts from CITE-seq, a spatial layer - into altExp(). Reductions like PCA and UMAP survive the trip; neighbor graphs and SNN networks do not, because SingleCellExperiment has no slot to hold them.

The failure mode that actually bites people is silent, not loud. A legacy Seurat object with raw.data stored as a data.frame, or a Seurat v5 object with per-sample layers that were never merged, converts without a single warning and comes back with an empty or malformed counts assay. Nothing crashes. You just inherit garbage two steps downstream unless you check assayNames(sce) and the assay dimensions before you trust the object.

The commands

Type your file names once; every command below updates.

  1. 01Seuratv5.5.1

    r
    seurat_obj <- readRDS("sample.rds")
    sce <- as.SingleCellExperiment(seurat_obj)
    saveRDS(sce, "sample.rds")

    The default call. It reads the object's active assay, builds counts and logcounts assays in the new SCE, moves @meta.data into colData, and copies any stored reductions into reducedDims. It assumes NormalizeData() already ran - if not, logcounts is just a copy of counts, not an error.

  2. 02Seurat

    r
    sce <- as.SingleCellExperiment(seurat_obj, assay = "RNA")

    For multi-modal objects (CITE-seq ADT, spatial) with more than one assay, name the assay explicitly rather than relying on whichever one happens to be active. Every other assay on the object is not dropped - it lands in altExp(sce), so check altExpNames(sce) afterward.

  3. 03Seurat

    r
    seurat_obj[["RNA"]] <- JoinLayers(seurat_obj[["RNA"]])
    sce <- as.SingleCellExperiment(seurat_obj)

    Seurat v5 stores counts/data as layers and can split them further per sample (counts.sample1, counts.sample2, ...) after merging without integration. as.SingleCellExperiment() expects one merged layer per assay; join them first or the resulting assay comes back empty or the wrong shape.

  4. 04Seurat

    r
    seurat_obj@raw.data <- as.matrix(seurat_obj@raw.data)
    sce <- as.SingleCellExperiment(seurat_obj)

    Only relevant for legacy Seurat v2 objects. If raw.data was stored as a data.frame instead of a matrix or dgCMatrix, conversion silently returns an SCE with no counts or logcounts assay; coercing it to a matrix first fixes that.

Coordinates, strand, names, builds

This conversion has no genomic coordinates or strand to preserve - both objects hold expression matrices, not genomic ranges. What matters instead is slot mapping and orientation. Both Seurat and SingleCellExperiment put genes as rows and cells as columns, so there is no transpose to worry about, unlike some Python round-trips. Cell and gene names carry over as colnames/rownames on both sides, so identical(colnames(sce), colnames(seurat_obj)) should hold. Genome build and chromosome naming (chr1 vs 1) only matter here if your Seurat object's feature metadata already carried gene annotations - as.SingleCellExperiment() does not fetch or reconcile any build information on its own, it only moves what was already attached. The real losses are structural, not coordinate-based: scale.data is dropped every time, neighbor graphs and SNN networks never transfer, and any custom slots you bolted onto the Seurat object (commands log, tool-specific S4 subclasses) are not represented in SCE's assay/colData/rowData/reducedDims model.

Check the output before you trust it

  1. 01Dimensions match the source object

    r
    dim(sce); dim(seurat_obj)

    Expected Identical rows (genes) and columns (cells) in both objects - no transpose, no dropped cells.

  2. 02counts and logcounts are both populated

    r
    assayNames(sce); range(assay(sce, "counts")); range(assay(sce, "logcounts"))

    Expected assayNames includes "counts" and "logcounts"; counts range is non-negative integers; logcounts differs from counts if NormalizeData() ran before conversion.

  3. 03Cell metadata transferred

    r
    all(colnames(seurat_obj@meta.data) %in% colnames(colData(sce)))

    Expected TRUE - every metadata column you had in @meta.data is present in colData(sce).

  4. 04Reductions carried over

    r
    reducedDimNames(sce)

    Expected Lists the same reduction names you had in Seurat (e.g. "PCA", "UMAP"); an empty result means they weren't computed before conversion, not that conversion dropped them.

  5. 05scale.data is gone on purpose, not by accident

    r
    assayNames(sce)

    Expected Only counts, logcounts, and any other real assays you had - never scale.data. If it's missing, that's expected behavior, not a bug to chase.

  6. 06No cells or genes silently vanished

    r
    identical(ncol(sce), ncol(seurat_obj)) && identical(nrow(sce), nrow(seurat_obj))

    Expected TRUE - if FALSE, you likely converted before finishing subsetting or after a failed JoinLayers() call.

Errors you will see, and what they mean

SCE object has no counts or logcounts assay after conversion
Cause: The source Seurat object's raw.data (or counts data) was stored as a data.frame rather than a matrix or dgCMatrix, so as.SingleCellExperiment() couldn't recognize it as assay data. Fix: Coerce it first: seurat_obj@raw.data <- as.matrix(seurat_obj@raw.data), then re-run the conversion.
counts assay in the SCE is empty, wrong-dimension, or split into counts.1 / counts.2 style columns
Cause: Seurat v5's layer-based assay structure keeps counts/data as separate layers, and merging multiple samples without integration leaves them split (counts.sample1, counts.sample2, ...) instead of one merged layer. Fix: Join the layers before converting: seurat_obj[["RNA"]] <- JoinLayers(seurat_obj[["RNA"]]).
scale.data / ScaleData() output is missing from the SCE
Cause: Not a bug. as.SingleCellExperiment() only ever carries counts and logcounts by design; scale.data is dropped because Bioconductor treats scaling as a reproducible downstream step, not stored data. Fix: Pull the scaled matrix from the original Seurat object with GetAssayData(seurat_obj, slot = "scale.data") if you still need it, or recompute scaling on the SCE with scater/scran functions.
Filters or joins on colData(sce) silently drop rows or return NA downstream
Cause: Inconsistent metadata column naming or mixed missing-value coding (e.g. paper_expression_subtype vs paper_Expression.Subtype, or NA written as "[Not Available]") carried straight through from @meta.data into colData without being caught by the conversion. Fix: Standardize column names and NA coding in the Seurat object's meta.data before converting, using janitor::clean_names() and na_if(); the conversion won't clean messy metadata for you.
Neighbor graphs (SNN/KNN) computed in Seurat aren't found anywhere in the SCE
Cause: Graphs and network information never transfer during conversion - they have no representation in the SingleCellExperiment data model. Fix: Recompute graphs on the SCE with scran::buildSNNGraph() (or equivalent) if a downstream Bioconductor step needs them; don't expect them to reappear after conversion.

Questions people ask

Does as.SingleCellExperiment() work with Seurat v5 objects?

Yes, but v5's layer-based assay structure can produce an empty or split counts assay if the object has multiple unmerged layers (e.g. counts.sample1, counts.sample2 after merging without integration). Run JoinLayers() on the assay before converting and check assayNames(sce) afterward.

Why is my scale.data missing after converting to SingleCellExperiment?

It's dropped by design, not by bug. as.SingleCellExperiment() only carries over counts and logcounts, reflecting the Bioconductor convention that scaling is a reproducible downstream step rather than stored data. Keep the original Seurat object around if you need the scaled matrix.

Do UMAP, PCA, and tSNE coordinates survive the conversion?

Yes, dimensional reductions transfer into reducedDims(sce) and stay accessible for plotting. What does not survive is any neighbor graph (SNN or KNN) Seurat computed alongside them, since SCE has no slot for graphs.

How do I convert a SingleCellExperiment back to Seurat?

Use as.Seurat(sce, counts = "counts", data = "logcounts"), naming which SCE assays map to Seurat's counts and data slots explicitly. It will not guess the mapping for you.

Which assay becomes the main one if my Seurat object has both RNA and ADT (CITE-seq)?

Whichever assay is active, or whichever you pass to the assay= argument, becomes the SCE's primary assay. The rest land in altExp(sce) rather than being dropped, so check altExpNames(sce) instead of assuming they disappeared.

Related pages

Sources

  1. as.SingleCellExperiment function - Seurat reference — Function signature and default behavior for as.SingleCellExperiment(), including the assay argument.
  2. An introduction to the SingleCellExperiment class — Canonical description of the assays/colData/rowData/reducedDims slot structure the conversion targets.
  3. Conversion to SingleCellExperiment from Seurat objects — Source of the raw.data-as-data.frame bug and the as.matrix() workaround for legacy objects.
  4. Interoperability between single-cell object formats - Seurat v4.3 vignette — Documents that reductions transfer during conversion while graphs and network info do not.