Before you start
- You understand RNA-seq experimental design; see Experimental Design for RNA-seq, and some basic R helps (R basics).
- Any term new? The Glossary has it.
Learning objectives
By the end of this lesson you will be able to: define a batch effect, prevent it through design, model a recorded batch in DESeq2 with a design like ~ batch + condition, use batch-corrected values for visualization only, and recognize when a batch is fully confounded with treatment and cannot be separated.
What a batch effect is
A batch effect is a systematic technical difference between groups of samples: they were processed on different days, run on different lanes, or prepared with different kits or by different people. The problem is that these differences can shift expression in ways that look exactly like real biology. If you do not account for them, a batch effect can either invent differences that are not real or hide ones that are.
1The best defense is design
The cheapest fix happens before any sequencing. Spread your conditions across batches so that each processing day, lane, or kit contains a mix of treated and control samples. Then a batch difference washes out instead of lining up with your comparison. This is the heart of good experimental design, and no statistical trick later is as good as getting the layout right up front.
2When a batch is unavoidable: model it
Sometimes you cannot avoid a batch, but as long as you recorded which batch each sample belongs to, you can handle it. Put the batch in the DESeq2 design: ~ batch + condition. The model then accounts for the batch differences while still testing the condition. Order matters by convention: the variable of interest goes last, so for the airway data with batch added it would read ~ batch + dex, where dex is the treatment column. You are not removing the batch from the data; you are letting the model subtract its effect.
Model, do not overwriteThe key distinction
For the differential test, add batch to the design and keep the raw counts untouched. Do not replace your counts with batch-corrected numbers and feed those to DESeq2. Modeling the batch is the statistically correct way; overwriting the data double-counts the correction and distorts the test.
3Batch-corrected values are for plots only
There is a legitimate place for explicitly removing a batch effect: visualization. For a clean PCA or heatmap you can apply limma::removeBatchEffect or ComBat from the sva package to your transformed (vst or rlog) values, so the plot shows biology rather than batch. But these adjusted values are for the eye only. Do not feed batch-corrected values back into DESeq2. The correct workflow is to model the batch in the design for testing, and to remove the batch only on a copy of the transformed data that you use for figures.
When the batch cannot be separated
One hard limit: if treatment is fully confounded with batch, meaning every treated sample is in one batch and every control in another, then no method can untangle them. The model cannot tell whether a difference came from the treatment or the batch, because they move together perfectly. There is nothing to subtract that does not also subtract the biology. This is why design comes first: confounding is a planning failure that statistics cannot repair.
| Situation | Can it be fixed? | How |
|---|---|---|
| Conditions spread across batches | Best case | Optionally model batch; design already protects you |
| Batch unavoidable but recorded | Yes | Add batch to the design, e.g. ~ batch + dex |
| Need a clean PCA or heatmap | Yes, for plots only | limma::removeBatchEffect or ComBat on transformed data |
| Treatment fully confounded with batch | No | Cannot be separated; only a new design fixes it |
One-line summary
Prevent batch effects by design. When a batch is unavoidable and recorded, model it with ~ batch + condition. Use removeBatchEffect or ComBat for visualization only, never as DESeq2 input. If treatment is fully confounded with batch, it cannot be separated.
A worked example
The example shows both halves: the batch goes into the design for the actual test, and a separate batch-corrected matrix is made only for plotting.
library(DESeq2)
library(limma)
# --- Correct way to TEST: model the batch in the design ---
dds <- DESeqDataSetFromMatrix(countData = counts_raw, # raw integers
colData = sample_info, # has 'batch' and 'dex'
design = ~ batch + dex) # variable of interest last
dds <- DESeq(dds)
res <- results(dds) # tests dex while accounting for batch
# --- Visualization ONLY: remove batch on transformed data ---
vsd <- vst(dds, blind = FALSE)
plotPCA(vsd, intgroup = c("batch", "dex")) # batch still visible here
mat <- assay(vsd)
# subtract batch for a cleaner plot; do NOT feed 'mat' back into DESeq2
mat <- limma::removeBatchEffect(mat, batch = vsd$batch)
assay(vsd) <- mat
plotPCA(vsd, intgroup = "dex") # now split by condition