Module 9: Differential Gene Expression Analysis with DESeq2 in R
Tested Stacks:
GATK 4.6.1
Samtools 1.21
Nextflow 24.10
Seurat v5.1
Python 3.12
Prerequisites: Basic Linux CLI & Sequencing Formats
Module 09 • DESeq2 Analysis
Track 18 • Practical Transcriptomics
Hands-On Code Included
Module 9: Differential Gene Expression Analysis with DESeq2 in R
Mathematical foundations of count data modeling: Negative Binomial distribution, size factor median-of-ratios normalization, empirical Bayes dispersion shrinkage, Wald testing, and log2 fold-change shrinkage (apeglm).
9. Differential Gene Expression Analysis with DESeq2 in R
RNA-seq read counts exhibit overdispersion—the variance across biological replicates exceeds the mean ($\sigma^2 > \mu$). Therefore, standard Poisson models underestimate false discovery rates. DESeq2 models count data using the Negative Binomial Generalized Linear Model (GLM) with Empirical Bayes shrinkage:
$$K_{ij} \sim \text{NB}(\text{mean} = \mu_{ij}, \text{dispersion} = \alpha_i)$$
$$\mu_{ij} = s_{ij} q_{ij}$$
$$\text{Var}(K_{ij}) = \mu_{ij} + \alpha_i \mu_{ij}^2$$
Production R Script: Complete DESeq2 Pipeline
library(DESeq2)
library(tidyverse)
# 1. Load Count Matrix and Sample Metadata Table
counts <- read.table("05_counts/counts_matrix_clean.tsv", header = TRUE, row.names = 1, sep = "\t")
colData <- read.csv("sample_metadata.csv", row.names = 1)
# Ensure sample names match exactly
all(colnames(counts) == rownames(colData))
# Set reference baseline condition (e.g. "Control")
colData$Condition <- factor(colData$Condition, levels = c("Control", "Treated"))
# 2. Construct DESeqDataSet
dds <- DESeqDataSetFromMatrix(
countData = counts,
colData = colData,
design = ~ Batch + Condition # Control for batch effects + main treatment
)
# 3. Filter ultra-low count genes (<10 reads across samples)
keep <- rowSums(counts(dds)) >= 10
dds <- dds[keep, ]
# 4. Execute Differential Expression Analysis
# Performs: Size factor estimation -> Dispersion estimation -> GLM Wald testing
dds <- DESeq(dds)
# 5. Extract Results with apeglm Log2 Fold Change Shrinkage
res <- results(dds, contrast = c("Condition", "Treated", "Control"), alpha = 0.05)
res_shrink <- lfcShrink(dds, coef = "Condition_Treated_vs_Control", type = "apeglm")
# Export Full Results to CSV
write.csv(as.data.frame(res_shrink), file = "06_deseq2/DEG_Treated_vs_Control_apeglm.csv")
# Filter Significant DEGs: FDR < 0.05 & |log2FC| >= 1
sig_degs <- subset(res_shrink, padj < 0.05 & abs(log2FoldChange) >= 1)
message("Total Significant DEGs: ", nrow(sig_degs))
⚠️
Common Pitfalls & Troubleshooting Checklist
- Java Heap Exhaustion (GATK / Picard): If processes terminate with
java.lang.OutOfMemoryError, set your environment variable explicitly:export _JAVA_OPTIONS="-Xmx16g -Djava.io.tmpdir=/tmp". - Index Timestamp Mismatch: If tools complain that a BAM index is missing or out of date, regenerate with
samtools index -@ 4 sample.bam. - Conda Channel Conflicts: Avoid ABI incompatibilities between bioconda and conda-forge by setting strict priority:
conda config --set channel_priority strict. - Chromosome Naming Contamination: Always verify whether reference FASTA headers use
chr1or1; divergent naming causes silent alignment drops.
Was this guide helpful to your research?
Help us maintain 100% accurate, peer-reviewed bioinformatics standards.