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 Dimension | ExpressionSet (Biobase) | SummarizedExperiment (SummarizedExperiment) | GRanges (GenomicRanges) |
|---|---|---|---|
| Era & Focus | Legacy Microarray era (~2004) | Modern High-Throughput Sequencing (~2015+) | Universal Genomic Interval Engine (~2010+) |
| Primary Data Stored | Single primary matrix (exprs) | Multiple named assay matrices (assays) | 1-based genomic coordinates (chr, start, end, strand) |
| Feature Annotations | featureData (AnnotatedDataFrame) | rowData (DataFrame) | Metadata columns (mcols) |
| Genomic Coordinates | None (requires external lookups) | Native rowRanges (GRanges or GRangesList) | Core foundation (seqnames, ranges, strand) |
| Sample Metadata | phenoData (AnnotatedDataFrame) | colData (DataFrame) | None (intervals only) |
| Multi-Assay Capability | Restricted (requires custom environments) | Seamless (e.g., counts, tpm, logcounts) | Not applicable |
| Coordinate Querying | Impossible without external tables | Native overlap queries (subsetByOverlaps) | High-performance range algebra (findOverlaps) |
| Bioconductor Status | Legacy / Deprecated for new packages | Universal 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 objectexpr_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 SummarizedExperimentmodern_se <- makeSummarizedExperimentFromExpressionSet(legacy_eset)
# Verify converted componentscat("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 countsraw_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 assayshead(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.