omics.co.in logo omics.co.in

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 chr1 or 1; divergent naming causes silent alignment drops.
Was this guide helpful to your research?
Help us maintain 100% accurate, peer-reviewed bioinformatics standards.
📝 Propose Edit on GitHub ↗