if (!require("BiocManager"))
install.packages("BiocManager")
BiocManager::install("MultiAssaySpatialExperiment")Load the package:
If you use MultiAssaySpatialExperiment in your research,
please cite the package. Citation information is available via:
If you have spent a few decades designing databases, you have seen the same story many times: someone builds a system that works for one use case, and it spreads. Later, a new use case appears that does not quite fit. People bolt on workarounds, and the design gets messy. Eventually, someone steps back and asks: what would we build if we started from the problem we actually have?
Spatial omics is at that stage. We already have excellent tools for
single-assay spatial data: SpatialExperiment and
SpatialFeatureExperiment handle one experiment at a
time—one technology, one matrix, one set of spatial coordinates. They
work well when your question is “what genes are expressed in this Visium
section?” or “where are these MERFISH cells in tissue?” But
increasingly, people ask questions that cut across assays: “I have RNA,
protein, and chromatin from the same tissue—how do I link them and their
spatial coordinates in one place?” That is a different problem. It needs
a multi-assay structure that can carry spatial context and keep
everything consistent when you subset, or merge.
MultiAssayExperiment (MAE) solves the multi-assay part.
It gives you a central mapping table—the sampleMap—that
links assay columns to specimens. One source of truth, explicit foreign
keys, and validity checks that keep the links correct. When you subset
by specimen, the maps update. No orphaned rows. No hunting through
metadata to figure out which assay column belongs to which sample.
What MAE does not have is spatial geometry: coordinates for cells or
spots, cell boundaries, tissue images. That is where
MultiAssaySpatialExperiment (MASE) comes in. MASE extends
MAE with spatial elements—points, shapes, images, labels—and a second
central mapping table, the spatialMap, that links assay
columns to those elements. Same philosophy as MAE: one place to look,
one place to update, referential integrity enforced. Subsetting updates
spatialMap (and, for most column- and assay-level
operations, linked spatial layers) so you do not have to clean up
coordinates or drop orphaned geometries manually.
Use MultiAssaySpatialExperiment when:
SpatialExperiment or SpatialFeatureExperiment
for downstream tools.A MultiAssaySpatialExperiment object contains:
ExperimentList — your assays (can be matrices,
SummarizedExperiment, SingleCellExperiment, SpatialExperiment, or other
supported classes)colData — specimen/sample metadata (one row per
biological unit)sampleMap — links assay columns to specimens (primary
identifiers)metadata — experiment-level metadatapoints — named lists of point coordinates (DataFrame
with x, y, instance_id)shapes — named lists of polygons or other geometries
(DataFrame with sfc geometry column)images — raster images (tissue photos, microscopy)labels — segmentation masks (raster with instance
IDs)spatialMap — links assay columns to spatial element
rows (assay, colname,
element_type, region,
instance_id)imgData — links specimens to images
(sample_id, image_id,
scaleFactor, and optional raster metadata)The following diagram shows how these components relate:
Entity-Relationship Diagram showing MASE relational schema
The ERD shows the actual class definition with all slots:
Inherited from MultiAssayExperiment (tan boxes): -
ExperimentList — named list of assays (SE, SCE, matrices,
etc.) - colData — specimen metadata (rownames = primary
identifiers) - sampleMap — links assay columns to specimens
via (assay, primary, colname)
MASE-specific slots (blue boxes): -
points — PointsLayerList: named list of
DataFrames with coordinates - shapes —
ShapesLayerList: named list of DataFrames with
geometries
- images — RasterLayerList: named list of
SpatialImage/matrix (tissue photos, H&E, etc.) - labels
— RasterLayerList: named list of SpatialImage/matrix
(segmentation masks) - spatialMap — DataFrame linking assay
columns to spatial elements via
(assay, colname, element_type, region, instance_id) -
imgData — DataFrame linking specimens to images via
(sample_id, image_id)
Key relationships: -
spatialMap$element_type specifies which spatial slot:
"points" or "shapes" -
spatialMap$region references a layer name
within that slot (e.g., "coords", "cells") -
spatialMap$instance_id references a row within that layer’s
DataFrame - Foreign key:
(element_type, region, instance_id) →
slot(mase, element_type)[[region]] - Each layer is accessed
as a named element: points[[region]],
shapes[[region]], images[[image_id]],
labels[[image_id]]
The ExperimentList holds your experimental data. Each
element is an assay (e.g., RNA-seq, proteomics, ATAC-seq). Assays can be
of different classes:
matrix — base R matrixSummarizedExperiment — single-assay container with
rowData, colDataSingleCellExperiment — extends SummarizedExperiment
with reducedDims, etc.SpatialExperiment — extends SingleCellExperiment with
spatialCoordsSpatialFeatureExperiment — extends SpatialExperiment
with sf geometriesRequirements for classes in ExperimentList:
[ subsettingcolnames() (used for linking to specimens via
sampleMap)The colData is a DataFrame with one row per
specimen (biological unit). Row names are primary identifiers. Columns
store metadata like patient ID, tissue type, treatment, or any other
specimen-level attribute.
Terminology: a specimen is a
colData row; an observation is an assay
column (cell, spot, or bin). The sampleMap links
observations to specimens.
The sampleMap is a three-column
DataFrame:
| Column | Type | Description |
|---|---|---|
assay |
factor | Name of the assay in ExperimentList |
primary |
character | Row name in colData (specimen ID) |
colname |
character | Column name in the assay |
This table says: “column X of assay Y belongs to specimen Z.” Multiple assay columns can map to the same specimen (technical or biological replicates).
The points slot is a PointsLayerList (a
named list of DataFrame objects). Each element represents a
set of point coordinates (e.g., cells, spots, molecules). Required
columns:
x, y — numeric coordinatesinstance_id — character identifier for each pointOptional columns: z (for 3D), feature annotations,
quality metrics.
The shapes slot is a ShapesLayerList (a
named list of DataFrame objects). Each element represents a
set of polygons, lines, or other geometries. Required columns:
geometry — sfc (simple feature column from the
sf package)instance_id — character identifier for each shapeShapes are commonly used for cell boundaries, tissue regions, or annotation areas.
The images slot is a RasterLayerList (a
named list). Each element is a raster image (e.g., tissue H&E,
fluorescence microscopy). Images can be:
SpatRaster (from the terra package)The labels slot is a RasterLayerList (a
named list). Each element is a raster where pixel values are instance
IDs (e.g., cell ID 1, 2, 3, …). Labels are used for segmentation masks
that define which pixels belong to which object.
The spatialMap is a five-column
DataFrame:
| Column | Type | Description |
|---|---|---|
assay |
factor | Name of the assay in ExperimentList |
colname |
character | Column name in the assay |
element_type |
character | Spatial slot: "points" or "shapes" |
region |
character | Layer name within that slot (e.g., "coords",
"cells") |
instance_id |
character | Row identifier in the spatial layer |
This table says: “column X of assay Y is spatially located at row Z
of element W.” The spatialMap is the key innovation: it
allows multiple assays to share the same spatial elements, and it keeps
spatial links consistent during subsetting.
Validity constraints:
spatialMap must correspond to a valid row
in sampleMap (same assay + colname).instance_id must exist in the corresponding
points or shapes element.The imgData is a DataFrame with one row per
image–specimen pair (Visium readers populate SPE-compatible columns).
Typical columns include:
sample_id — row name in colData (specimen
identifier)image_id — name of the image in the images
slotscaleFactor — numeric scale factor for pixel ↔︎
coordinate conversiondata, width, height,
path — raster payload or file reference (when loaded).Here is a minimal example to get you started:
# Create a simple expression matrix
mat <- matrix(rnorm(20), nrow = 5, ncol = 4,
dimnames = list(paste0("Gene", 1:5), paste0("Cell", 1:4)))
# Create point coordinates
pts <- DataFrame(
x = c(1.2, 2.5, 3.1, 4.8),
y = c(1.5, 2.3, 3.7, 4.2),
instance_id = paste0("Cell", 1:4))
# Construct MASE
mase <- MultiAssaySpatialExperiment(
experiments = ExperimentList(rna = mat),
colData = DataFrame(row.names = paste0("Cell", 1:4)),
sampleMap = DataFrame(
assay = factor("rna", "rna"),
primary = paste0("Cell", 1:4),
colname = paste0("Cell", 1:4)
),
points = PointsLayerList(coords = pts),
spatialMap = DataFrame(
assay = factor("rna", "rna"),
colname = paste0("Cell", 1:4),
element_type = "points",
region = factor("coords", "coords"),
instance_id = paste0("Cell", 1:4)
)
)
mase
#> A MultiAssaySpatialExperiment object of 1 listed
#> experiment with a user-defined name and respective class.
#> Containing an ExperimentList class object of length 1:
#> [1] rna: matrix with 5 rows and 4 columns
#> Functionality:
#> experiments() - obtain the ExperimentList instance
#> colData() - the primary/phenotype DataFrame
#> sampleMap() - the sample coordination DataFrame
#> `$`, `[`, `[[` - extract colData columns, subset, or experiment
#> *Format() - convert into a long or wide DataFrame
#> assays() - convert ExperimentList to a SimpleList of matrices
#> exportClass() - save data to flat files
#> Spatial elements:
#> spatialImages: 0 elements
#> spatialLabels: 0 elements
#> spatialPoints: 1 element
#> spatialShapes: 0 elements
#> imgData: NULL
#> spatialMap: presentFor more construction examples, including:
See the Working with MultiAssaySpatialExperiment vignette.
All MAE accessors work on MASE:
# Get experiments
experiments(mase)
#> ExperimentList class object of length 1:
#> [1] rna: matrix with 5 rows and 4 columns
# Get specimen metadata
colData(mase)
#> DataFrame with 4 rows and 0 columns
# Get sampleMap
sampleMap(mase)
#> DataFrame with 4 rows and 3 columns
#> assay primary colname
#> <factor> <character> <character>
#> 1 rna Cell1 Cell1
#> 2 rna Cell2 Cell2
#> 3 rna Cell3 Cell3
#> 4 rna Cell4 Cell4
# Subset by assay
mase[, , "rna"]
#> A MultiAssaySpatialExperiment object of 1 listed
#> experiment with a user-defined name and respective class.
#> Containing an ExperimentList class object of length 1:
#> [1] rna: matrix with 5 rows and 4 columns
#> Functionality:
#> experiments() - obtain the ExperimentList instance
#> colData() - the primary/phenotype DataFrame
#> sampleMap() - the sample coordination DataFrame
#> `$`, `[`, `[[` - extract colData columns, subset, or experiment
#> *Format() - convert into a long or wide DataFrame
#> assays() - convert ExperimentList to a SimpleList of matrices
#> exportClass() - save data to flat files
#> Spatial elements:
#> spatialImages: 0 elements
#> spatialLabels: 0 elements
#> spatialPoints: 1 element
#> spatialShapes: 0 elements
#> imgData: NULL
#> spatialMap: presentMASE provides accessors for spatial elements:
# Get all spatial points
spatialPoints(mase)
#> PointsLayerList of length 1
#> [1] coords: DFrame (4 x 3)
# Get names of point elements
spatialPointNames(mase)
#> [1] "coords"
# Get a specific point element
spatialPoint(mase, "coords")
#> DataFrame with 4 rows and 3 columns
#> x y instance_id
#> <numeric> <numeric> <character>
#> 1 1.2 1.5 Cell1
#> 2 2.5 2.3 Cell2
#> 3 3.1 3.7 Cell3
#> 4 4.8 4.2 Cell4
# Get spatial images (empty in this example)
spatialImages(mase)
#> RasterLayerList of length 0
# Get spatial labels (empty in this example)
spatialLabels(mase)
#> RasterLayerList of length 0# Get spatialMap
spatialMap(mase)
#> DataFrame with 4 rows and 5 columns
#> assay colname element_type region instance_id
#> <factor> <character> <character> <factor> <character>
#> 1 rna Cell1 points coords Cell1
#> 2 rna Cell2 points coords Cell2
#> 3 rna Cell3 points coords Cell3
#> 4 rna Cell4 points coords Cell4
# Get imgData (empty in this example)
imgData(mase)
#> NULLMASE supports all MAE subsetting operations, with propagation to spatial slots where applicable:
subsetByColData(x, y) — subset by specimen; updates
spatialMap and imgData (point and shape rows
are not trimmed unless removed via the map)subsetByRow(x, y, ...) — subset assay features (spatial
layers unchanged)subsetByColumn(x, y) — subset assay columns; when
y is a list, also trims linked points, shapes, images, and
labels via spatialMapsubsetByAssay(x, y) — subset by assay name; keeps
spatial layers referenced in spatialMap for the retained
assaysx[i, j, k, ...] — combined subsetting (specimens,
columns, assays, features)Filter by observation metadata using the standard MAE list interface,
for example
subsetByColumn(mase, list(rna = colData(experiments(mase)[["rna"]])$tissue == "tumor")).
Spatial subsetting (geometry first, then assays via
spatialMap):
subsetByBoundingBox(x, xmin, xmax, ymin, ymax, ...) —
rectangular ROIsubsetByPolygon(x, polygon, ...) — polygon ROISee the Working with MultiAssaySpatialExperiment vignette for detailed subsetting examples.
MASE provides generics for spatial annotation and aggregation:
annotateWithRegions(x, points =, shapes =, ...) —
annotate measurement points with the shapes (cells, segments) they fall
inaggregateByRegion(x, by =, FUN =, ...) — aggregate
assay values by annotated region (e.g., sum transcript counts per
cell)spatialJoin(x, y, ...) — join two spatial layer tables
(DataFrame × DataFrame via sf; lower-level primitive for
custom annotation workflows)These functions use the sf package for spatial joins
(default: st_intersects). You can customize the join
function (e.g., st_nearest_feature).
See the Working with MultiAssaySpatialExperiment vignette for detailed examples.
MASE can convert to and from other Bioconductor spatial classes (when the relevant packages are installed):
as(spe, "MultiAssaySpatialExperiment") — wrap a
SpatialExperiment as a single-assay MASEas(mase, "SpatialExperiment") — requires exactly one
SPE-compatible assayas(sfe, "MultiAssaySpatialExperiment") — wrap a
SpatialFeatureExperiment (requires
SpatialFeatureExperiment)as(mase, "SpatialFeatureExperiment") — requires exactly
one SPE/SFE-compatible assaySee the MultiAssaySpatialExperiment use cases vignette for coercion examples.
The most common operations, grouped by task (see the man pages with
? and the other vignettes for details):
| Task | Functions |
|---|---|
| Construct | MultiAssaySpatialExperiment(), prepMASE(),
buildSpatialMap() |
| Read a platform | readXeniumMASE(), readVisiumMASE(),
readVisiumHDMASE(), readCosMxMASE(),
readMERSCOPEMASE() |
| Spatial accessors | spatialPoints(), spatialShapes(),
spatialImages(), spatialLabels(),
spatialMap() |
| MAE accessors | experiments(), colData(),
sampleMap(), subsetByAssay() |
| Subset | mase[rows, cols, assays], by region (bounding box /
polygon), by specimen |
| Annotate / aggregate | annotateWithRegions(), aggregateByRegion()
(points ↔︎ shapes joins) |
| Labels ↔︎ shapes | rasterize (shapes → labels), vectorize (labels → shapes) |
| Coerce | as(spe, "MultiAssaySpatialExperiment"),
as(mase, "SpatialExperiment") |
After reading this introduction, proceed to:
SpatialExperiment /
SpatialFeatureExperiment; multi-assay integration (Xenium +
Visium); coordinate transformation and alignment; and multi-sample
analysis.sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.04 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
#> [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
#> [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
#> [9] LC_ADDRESS=C LC_TELEPHONE=C
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats4 stats graphics grDevices utils datasets methods
#> [8] base
#>
#> other attached packages:
#> [1] MultiAssaySpatialExperiment_0.99.2 MultiAssayExperiment_1.39.0
#> [3] SummarizedExperiment_1.43.0 Biobase_2.73.1
#> [5] GenomicRanges_1.65.1 Seqinfo_1.3.0
#> [7] IRanges_2.47.2 S4Vectors_0.51.5
#> [9] BiocGenerics_0.59.10 generics_0.1.4
#> [11] MatrixGenerics_1.25.0 matrixStats_1.5.0
#> [13] BiocStyle_2.41.0
#>
#> loaded via a namespace (and not attached):
#> [1] sass_0.4.10 class_7.3-23
#> [3] SparseArray_1.13.2 KernSmooth_2.23-26
#> [5] lattice_0.22-9 digest_0.6.39
#> [7] magrittr_2.0.5 evaluate_1.0.5
#> [9] grid_4.6.1 fastmap_1.2.0
#> [11] jsonlite_2.0.0 Matrix_1.7-5
#> [13] e1071_1.7-17 DBI_1.3.0
#> [15] BiocManager_1.30.27 SingleCellExperiment_1.35.2
#> [17] codetools_0.2-20 jquerylib_0.1.4
#> [19] abind_1.4-8 cli_3.6.6
#> [21] rlang_1.3.0 units_1.0-1
#> [23] XVector_0.53.0 cachem_1.1.0
#> [25] DelayedArray_0.39.3 yaml_2.3.12
#> [27] BiocBaseUtils_1.15.1 otel_0.2.0
#> [29] S4Arrays_1.13.0 tools_4.6.1
#> [31] SpatialExperiment_1.23.0 buildtools_1.0.0
#> [33] R6_2.6.1 proxy_0.4-29
#> [35] lifecycle_1.0.5 classInt_0.4-11
#> [37] magick_2.9.1 bslib_0.11.0
#> [39] Rcpp_1.1.2 sf_1.1-1
#> [41] xfun_0.60 sys_3.4.3
#> [43] knitr_1.51 rjson_0.2.23
#> [45] htmltools_0.5.9 rmarkdown_2.31
#> [47] maketools_1.3.2 compiler_4.6.1