Skip to content

ExpressionSet vs SummarizedExperiment vs GRanges: Bioconductor Data Structures

1. Introduction

In high-throughput computational biology, managing high-dimensional assay matrices alongside sample phenotypes and genomic annotations requires robust container architectures. Within the Bioconductor ecosystem, three primary classes have defined data manipulation across different eras: ExpressionSet, SummarizedExperiment, and GenomicRanges (GRanges).

While ExpressionSet underpinned the microarray era, modern high-throughput sequencing (RNA-seq, ChIP-seq, ATAC-seq) demanded a modernized architecture that natively embeds genomic coordinates and houses multi-layer assay matrices. This spurred the development of SummarizedExperiment (and its coordinate-aware subclass, RangedSummarizedExperiment), powered internally by GRanges.

This guide details the architectural differences across these three fundamental classes, provides an actionable migration path for legacy datasets, and demonstrates hands-on R code for modern genomic analysis.


2. Comparison Matrix: ExpressionSet vs. SummarizedExperiment vs. GRanges

The following table summarizes the structural design and operational capabilities of each data structure:

Architectural DimensionExpressionSet (Biobase)SummarizedExperiment (SummarizedExperiment)GRanges (GenomicRanges)
Era & FocusLegacy Microarray era (~2004)Modern High-Throughput Sequencing (~2015+)Universal Genomic Interval Engine (~2010+)
Primary Data StoredSingle primary matrix (exprs)Multiple named assay matrices (assays)1-based genomic coordinates (chr, start, end, strand)
Feature AnnotationsfeatureData (AnnotatedDataFrame)rowData (DataFrame)Metadata columns (mcols)
Genomic CoordinatesNone (requires external lookups)Native rowRanges (GRanges or GRangesList)Core foundation (seqnames, ranges, strand)
Sample MetadataphenoData (AnnotatedDataFrame)colData (DataFrame)None (intervals only)
Multi-Assay CapabilityRestricted (requires custom environments)Seamless (e.g., counts, tpm, logcounts)Not applicable
Coordinate QueryingImpossible without external tablesNative overlap queries (subsetByOverlaps)High-performance range algebra (findOverlaps)
Bioconductor StatusLegacy / Deprecated for new packagesUniversal Standard (DESeq2, edgeR, scater)Universal Standard (ChIPpeakAnno, diffloop)

3. Visual Architecture Breakdown

Understanding how SummarizedExperiment coordinates dimensions between features and samples:

colData (Samples Metadata)
+--------------------------+
| Condition | Batch | Time |
| Control | B1 | 0h |
| Treated | B2 | 24h |
+--------------------------+
|
v
rowRanges (GRanges) assays(se) [features x samples]
+------------------------+ +--------------------------+
| seqnames | start | end | ------> | count matrix (raw reads) |
| chr1 | 1000 | 2500| | tpm matrix (normalized) |
| chr2 | 5400 | 7200| | weights matrix (QC) |
+------------------------+ +--------------------------+
^
|
rowData (Feature Annotations)
+------------------------+
| Gene_Symbol | Biotype |
| TP53 | protein |
| EGFR | protein |
+------------------------+

4. Migrating from ExpressionSet to SummarizedExperiment

If you are working with legacy GEO datasets or historical lab packages formatted as ExpressionSet, Bioconductor provides a 1-line native converter function:

# ==============================================================================
# Converting ExpressionSet to SummarizedExperiment in R
# ==============================================================================
library(Biobase)
library(SummarizedExperiment)
# 1. Simulate a legacy ExpressionSet object
expr_matrix <- matrix(rpois(500, lambda = 50), nrow = 50, ncol = 10)
rownames(expr_matrix) <- paste0("PROBE_", 1:50)
colnames(expr_matrix) <- paste0("PATIENT_", 1:10)
pheno <- data.frame(
diagnosis = rep(c("Control", "Tumor"), each = 5),
row.names = colnames(expr_matrix)
)
legacy_eset <- ExpressionSet(
assayData = expr_matrix,
phenoData = AnnotatedDataFrame(pheno)
)
# 2. Automated Migration to SummarizedExperiment
modern_se <- makeSummarizedExperimentFromExpressionSet(legacy_eset)
# Verify converted components
cat("Migrated Assays:\n")
print(assayNames(modern_se)) # Automatically names primary matrix "exprs"
cat("\nMigrated Sample Metadata (colData):\n")
print(head(colData(modern_se)))

5. Practical Implementation: GRanges & Multi-Assay SummarizedExperiment

The following script demonstrates constructing a GRanges object, assembling a RangedSummarizedExperiment with multiple simultaneous assay matrices (counts and TPM), and slicing data by clinical phenotype and genomic coordinate overlaps.

# ==============================================================================
# Hands-on GRanges & RangedSummarizedExperiment Workflow
# ==============================================================================
library(GenomicRanges)
library(SummarizedExperiment)
# ------------------------------------------------------------------------------
# Step 1: Constructing a GRanges Object
# ------------------------------------------------------------------------------
# Define chromosomal genomic intervals with metadata columns (mcols)
gr_features <- GRanges(
seqnames = c("chr1", "chr1", "chr2", "chrX"),
ranges = IRanges(
start = c(1000000, 2500000, 5000000, 15000000),
end = c(1050000, 2580000, 5040000, 15020000)
),
strand = c("+", "-", "+", "-"),
gene_id = c("ENSG000001", "ENSG000002", "ENSG000003", "ENSG000004"),
symbol = c("GENEA", "GENEB", "GENEC", "GENED")
)
print(gr_features)
# ------------------------------------------------------------------------------
# Step 2: Creating Multi-Layer Assay Matrices & Metadata
# ------------------------------------------------------------------------------
n_features <- length(gr_features)
n_samples <- 6
# Assay 1: Raw sequencing integer counts
raw_counts <- matrix(
rpois(n_features * n_samples, lambda = 120),
nrow = n_features,
ncol = n_samples,
dimnames = list(gr_features$gene_id, paste0("Sample_", 1:n_samples))
)
# Assay 2: Normalized Transcripts Per Million (TPM)
tpm_values <- matrix(
round(runif(n_features * n_samples, 5.0, 85.0), 2),
nrow = n_features,
ncol = n_samples,
dimnames = list(gr_features$gene_id, paste0("Sample_", 1:n_samples))
)
# Sample metadata (colData)
sample_metadata <- DataFrame(
condition = factor(rep(c("WildType", "Knockout"), each = 3)),
batch = factor(rep(c("A", "B", "A"), times = 2)),
row.names = colnames(raw_counts)
)
# ------------------------------------------------------------------------------
# Step 3: Instantiate RangedSummarizedExperiment
# ------------------------------------------------------------------------------
rse <- SummarizedExperiment(
assays = list(counts = raw_counts, tpm = tpm_values),
rowRanges = gr_features,
colData = sample_metadata
)
print(rse)
# Access individual assays
head(assay(rse, "counts"))
head(assay(rse, "tpm"))
# ------------------------------------------------------------------------------
# Step 4: Coordinated Subsetting & Genomic Range Overlaps
# ------------------------------------------------------------------------------
# 4A. Subset by sample phenotype (e.g. only Knockout samples)
ko_samples <- rse[, rse$condition == "Knockout"]
cat(paste("Retained samples in KO subset:", ncol(ko_samples), "\n"))
# 4B. Subset by genomic coordinates (e.g., query chromosome 1 intervals)
query_region <- GRanges("chr1:500000-3000000")
overlap_rse <- subsetByOverlaps(rse, query_region)
cat("\nGenes located inside chromosome 1 target region:\n")
print(rowRanges(overlap_rse)$symbol)

6. Decision Tree: Which Structure Should You Choose?

Do you only have genomic coordinates/intervals (e.g., ChIP-seq peaks, VCF intervals)?
├── YES ──> Use **GRanges**
└── NO ──> Do you have matrix data (samples x features) linked to metadata?
├── Is it legacy microarray code requiring Biobase exprs()?
│ └── YES ──> Use **ExpressionSet** (or convert via makeSummarizedExperimentFromExpressionSet)
└── Are you doing RNA-seq, scRNA-seq, or modern sequencing analysis?
└── YES ──> Use **SummarizedExperiment** (or RangedSummarizedExperiment)

7. Frequently Asked Questions (FAQ)

Why did SummarizedExperiment replace ExpressionSet in Bioconductor?

SummarizedExperiment was introduced to address the limitations of ExpressionSet in modern high-throughput sequencing. It natively supports multi-layered assays (such as raw counts, normalized counts, and TPM within a single object), seamlessly links genomic coordinates through rowRanges (GRanges), and provides coordinated 2-dimensional matrix slicing that keeps sample and feature metadata perfectly synchronized.

How do you convert an ExpressionSet into a SummarizedExperiment in R?

You can convert any legacy ExpressionSet object using the makeSummarizedExperimentFromExpressionSet() function from the SummarizedExperiment package. It automatically transfers assayData to assays, phenoData to colData, and featureData to rowData.

When should you use GRanges versus SummarizedExperiment?

Use GRanges when your analytical unit is purely chromosomal coordinates (e.g., genomic intervals, ChIP-seq binding peaks, variant positions, or BED files) without an associated expression or quantification matrix. Use SummarizedExperiment whenever you have experimental measurement matrices across multiple biological samples linked to phenotype data and feature annotations.

Topics Covered

summarizedexperiment vs expressionset rbioconductor data structuresgranges rmakeSummarizedExperimentFromExpressionSetrowRanges colData assays