# SummarizedExperiment subsetting: keep assays, rowData, and colData aligned
When you filter samples or features in a Bioconductor `SummarizedExperiment`, subset the
container with `[i, j]` -- never the assay matrix directly. The `[` method subsets every assay,
`rowData`/`rowRanges`, and `colData` in one operation, so the pieces cannot drift out of sync.
The official vignette's motivation: "Improperly accounting for metadata and observational data
has resulted in a number of incorrect results and retractions."
## The one rule
Rows are features (genes, transcripts, exons). Columns are samples. Always do
`se_subset <- se[i, j]` and only then extract data with `assay(se_subset)`, `colData(se_subset)`,
`rowData(se_subset)`.
The anti-pattern that corrupts analyses silently:
```r
library(SummarizedExperiment)
# WRONG: subsetting the bare matrix breaks the link to colData.
# Downstream code that pairs assay columns with colData rows by POSITION
# now attributes each sample's data to the wrong sample.
counts <- assay(se)
counts <- counts[, se$dex == "trt"]
stopifnot(ncol(counts) == nrow(colData(se))) # fails -- desynced
# RIGHT: subset the container; assays and colData move together.
se_trt <- se[, se$dex == "trt"]
stopifnot(ncol(assay(se_trt)) == nrow(colData(se_trt))) # always true
```
Positional desync is the worst failure mode because nothing errors: a design matrix built from
the full `colData` applied to a filtered assay silently swaps sample labels.
## `drop` is ignored -- the container never collapses
Per the method documentation, the `drop` argument is "ignored by these methods."
`x[i, j, drop=TRUE]` always returns a `SummarizedExperiment`, even for a single row or column.
This differs from base R, where `m[, 1]` becomes a vector:
```r
sub <- se[, 1] # still a SummarizedExperiment, dim: NFEATURES x 1
class(sub) # "SummarizedExperiment"
dim(sub) # e.g. 200 1
# The bare matrix follows base R rules and drops:
vec <- assay(se)[, 1] # a vector, dimnames lost
```
If you need a single sample's values as a vector, be explicit: `assay(se[, 1])[, 1]`.
If you need a colData column as a vector, use the `[[` accessor (note: `se[[1]]` is the
FIRST colData COLUMN, not the first assay row): `se[[TREATMENT_COL]]` or `se$treatment`.
## `[, j]` keeps all features; `[i, ]` keeps all samples
`i` and `j` may each be numeric, logical, character, or missing. Missing means "keep all":
```r
se_treated <- se[, se$dex == "trt"] # sample filter; every feature kept
se_sig <- se[rowSums(assay(se)) > 10, ] # feature filter; every sample kept
se_both <- se[rowSums(assay(se)) > 10, se$dex == "trt"]
se_by_name <- se[c("ENSG00000000003", "ENSG00000000005"), ] # character i
```
## Logical indexing: NA creates rows, it does not drop them
Base R subsetting semantics apply inside the container: a logical vector containing NA
produces NA placeholder rows in assays, rowData, and colData -- consistently, so alignment
holds, but the object now contains all-NA rows/columns. Clean your index first:
```r
keep <- se$dex == "trt"
keep[is.na(keep)] <- FALSE # decide: NA samples are out, explicitly
se <- se[, keep]
```
`subset(x, subset, select)` evaluates `subset` in the context of `rowData(x)` (or
`rowRanges(x)` for a RangedSummarizedExperiment) and `select` in the context of
`colData(x)`; missing values in the logical result are taken as false there, which is safer:
```r
se_trt <- subset(se, select = dex == "trt")
```
## Character indexing and duplicated names
Character `i`/`j` values are matched against `rownames(x)`/`colnames(x)` with bounds
checking -- an unknown name errors. But if dimnames contain DUPLICATES, matching takes the
first occurrence silently. Guarantee unique names before character subsetting, or prefer
logical/numeric indices:
```r
stopifnot(!anyDuplicated(rownames(se)), !anyDuplicated(colnames(se)))
se_sel <- se[SAMPLE_IDS_OF_INTEREST, ]
```
## Assay dimnames are overlaid on extraction
`assay(x)` copies the top-level dimnames of the SE onto the returned matrix by default.
The stored matrix may have different (or NULL) dimnames underneath. Two consequences:
1. `assay(se)` and the matrix you supplied at construction can LOOK identical while the
stored object differs -- use `assay(se, withDimnames = FALSE)` to see (or pass to
performance-critical code) the matrix as stored.
2. At construction, assay dimnames must be NULL or identical to the object's dimnames, and
the constructor validates counts: "nb of cols in 'assay' must equal nb of rows in
'colData'". If your counts and metadata came from different files, reorder first --
order matters, the constructor does not reorder for you:
```r
counts <- counts[, rownames(coldata)] # align columns to colData order
rowranges <- rowranges[rownames(counts), ] # align rows to assay order
stopifnot(identical(rownames(coldata), colnames(counts)))
se <- SummarizedExperiment(assays = list(counts = counts),
rowRanges = rowranges, colData = coldata)
```
## metadata() does not subset
`metadata()` is a plain list describing the whole experiment (design formulas, processing
parameters). It is NOT subset-aware: any per-sample or per-feature vector you stash there
goes stale the moment you subset. Keep per-feature data in `rowData`, per-sample data in
`colData`, experiment-wide data in `metadata`.
## Replacement and combining
- `x[i, j] <- value` requires `value` to be a SummarizedExperiment with dimensions,
dimnames, and assays consistent with the replaced region. You cannot inject a bare matrix.
- `cbind(...)` combines samples: duplicate `rowData` columns across objects must contain
identical data; assays combine by name matching. `rbind(...)` combines features with the
mirror constraint on `colData`. `metadata` from all objects is concatenated into a list
with no name checking -- conflicting entries are silently stacked.
## Quick alignment audit
After any chain of manipulations, run this before modeling:
```r
audit_se <- function(x) {
stopifnot(
all(vapply(assays(x), function(a) identical(dim(a), dim(x)), logical(1))),
nrow(colData(x)) == ncol(x),
nrow(rowData(x)) == nrow(x),
is.null(colnames(x)) || identical(rownames(colData(x)), colnames(x)) || is.null(rownames(colData(x)))
)
invisible(TRUE)
}
audit_se(se)
```