[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M73GD652SNW9RENVZXA6XMX6":3,"skill-same:01M73GD652SNW9RENVZXA6XMX6":48},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":24,"commitSha":25,"skillDirectory":26,"forksCount":27,"repositoryHtmlUrl":28,"repositoryHomepageUrl":29,"ownerHtmlUrl":30,"ownerBlogUrl":31,"ownerDescription":32,"updatedAt":33,"install":34,"resources":47},"01M73GD652SNW9RENVZXA6XMX6","deseq2-differential-expression",null,"Bulk RNA-seq DE with R\u002FBioconductor DESeq2. Negative binomial GLM, empirical Bayes shrinkage, Wald\u002FLRT tests, multi-factor designs, Salmon tximeta import, apeglm LFC shrinkage, MA\u002Fvolcano\u002Fheatmap viz. R gold standard. Use pydeseq2-differential-expression for Python; use edgeR for TMM normalization.","bg-szy\u002FTOP-SKILLS","bg-szy","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F106576190?v=4","01M73A4E8KPV67SFXRY8XZGNBX","ShuZhiyong","01M73A4E96MQ294MHM74TZBVQD",6,"LGPL-3.0","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fawesome-skills\u002Fdeseq2-differential-expression\u002FSKILL.md",[],[],"2026-11-11 15:11:14",false,"---\nname: \"deseq2-differential-expression\"\ndescription: \"Bulk RNA-seq DE with R\u002FBioconductor DESeq2. Negative binomial GLM, empirical Bayes shrinkage, Wald\u002FLRT tests, multi-factor designs, Salmon tximeta import, apeglm LFC shrinkage, MA\u002Fvolcano\u002Fheatmap viz. R gold standard. Use pydeseq2-differential-expression for Python; use edgeR for TMM normalization.\"\nlicense: \"LGPL-3.0\"\n---\n\n# DESeq2 Differential Expression Analysis (R\u002FBioconductor)\n\n## Overview\n\nDESeq2 is the Bioconductor R package for differential gene expression analysis from bulk RNA-seq count data. It fits a negative binomial generalized linear model per gene, estimates dispersion parameters using empirical Bayes shrinkage across genes, and tests differential expression using Wald tests (two-group) or likelihood ratio tests (complex designs). DESeq2 is the R gold standard for RNA-seq DE analysis, with native Bioconductor integration for seamless import from Salmon (tximeta\u002Ftximport), featureCounts, or HTSeq.\n\n## When to Use\n\n- Identifying differentially expressed genes between two experimental conditions (treated vs. control, disease vs. healthy) from bulk RNA-seq count data\n- Analyzing multi-factor designs that account for batch effects or covariates (e.g., `~ batch + condition`)\n- Testing complex hypotheses with interaction terms (e.g., time × treatment) or reduced models using likelihood ratio tests (LRT)\n- Importing Salmon pseudoalignment output via tximeta or tximport for transcript-level uncertainty propagation\n- Performing LFC shrinkage with apeglm for ranked gene lists, volcano plots, and downstream pathway analysis\n- Conducting time-series experiments or any design with more than two levels requiring model comparison\n- Working in an R\u002FBioconductor ecosystem where integration with SummarizedExperiment, clusterProfiler, or EnhancedVolcano is needed\n- Use **pydeseq2-differential-expression** instead for Python-based pipelines with the same statistical model\n- Use **edgeR** for negative binomial DE with TMM normalization, quasi-likelihood F-tests, or TREAT testing\n- Use **omics-plotting** SKILL after DE and for publication-quality plots of DESeq2 results\n- Use **gseapy-gene-enrichment** SKILL after DE to interpret results at the pathway level\n\n## Prerequisites\n\n- **R packages**: `DESeq2` (Bioconductor), `tximeta` or `tximport` (Salmon import), `apeglm` (LFC shrinkage), `pheatmap`, `ggplot2`, `EnhancedVolcano`\n- **Data requirements**: Raw (unnormalized) integer count matrix — gene rows × sample columns — plus a sample metadata data frame with matching column names. If using Salmon: per-sample `quant.sf` files and a transcript-to-gene mapping\n- **Environment**: R ≥ 4.2, Bioconductor ≥ 3.16\n\n```r\n# Install Bioconductor packages\nif (!require(\"BiocManager\", quietly = TRUE))\n    install.packages(\"BiocManager\")\n\nBiocManager::install(c(\"DESeq2\", \"tximeta\", \"tximport\", \"apeglm\",\n                        \"EnhancedVolcano\"))\ninstall.packages(c(\"pheatmap\", \"ggplot2\", \"dplyr\"))\n```\n\n## Pre-flight Interview\n\nSettle these with the user before writing any analysis code.\n\n```yaml\ndecisions:\n  - id: D1\n    param: design\n    kind: required\n    source: data\n    ask: \"Which column of the sample sheet separates the groups you want to compare?\"\n    default: null\n\n  - id: D2\n    param: contrast\n    kind: required\n    source: user\n    depends_on: [D1]\n    ask: \"Which two groups should be compared, and which group should have positive log2 fold changes?\"\n    default: null\n\n  - id: D3\n    param: design\n    kind: required\n    source: data\n    depends_on: [D1]\n    ask: \"Which available variables should be included to adjust for nuisance variation (batch, donor, sex, sequencing run)?\"\n    default: \"none\"\n    skip_if: \"sample sheet carries no column besides the grouping variable\"\n\n  - id: D4\n    param: test\n    kind: required\n    source: user\n    depends_on: [D1, D2]\n    ask: \"Is the primary hypothesis a named comparison, or an overall effect of this factor across its levels?\"\n    default: \"named comparison (Wald test)\"\n\n  - id: D5\n    param: alpha\n    kind: optional\n    source: user\n    ask: \"How strong must the evidence be before a gene counts as changed (false-discovery rate)?\"\n    default: 0.05\n\n  - id: D6\n    param: lfcThreshold\n    kind: optional\n    source: user\n    ask: \"Test against a zero change, or against a minimum fold change worth caring about?\"\n    default: 0\n\n  - id: D7\n    param: lfcShrink.type\n    kind: derived\n    source: upstream\n    ask: \"Shrink fold-change estimates so low-count genes stop dominating the rankings and plots?\"\n    default: \"apeglm, applied for ranking and visualization\"\n\n  - id: D8\n    param: blind (vst\u002Frlog)\n    kind: derived\n    source: upstream\n    ask: \"Should the variance-stabilizing transform ignore the design?\"\n    default: \"TRUE for QC\u002Fclustering, FALSE for design-aware downstream uses\"\n\n  - id: D9\n    param: independentFilter\n    kind: optional\n    source: user\n    ask: \"Should DESeq2 use its adaptive mean-count filter to maximize discoveries at the chosen FDR threshold?\"\n    default: true\n```\n\n`design` appears twice because the formula is assembled from both D1 and D3;\nD2 defines the direction of the reported effect. The pipeline explicitly passes\n`alpha: 0.05` to `results()` so independent filtering is optimized for the\nreported FDR threshold rather than DESeq2's default of `0.1`.\n\n## Quick Start\n\nComplete two-group comparison from a count matrix in under 20 lines.\n\n```r\nlibrary(DESeq2)\n\n# Load count matrix (genes x samples) and metadata\ncounts \u003C- as.matrix(read.csv(\"counts.csv\", row.names = 1))\ncoldata \u003C- read.csv(\"metadata.csv\", row.names = 1)\ncoldata$condition \u003C- factor(coldata$condition)\n\n# Build DESeqDataSet, run full pipeline\ndds \u003C- DESeqDataSetFromMatrix(countData = counts,\n                               colData   = coldata,\n                               design    = ~ condition)\ndds \u003C- dds[rowSums(counts(dds)) >= 10, ]   # pre-filter\ndds \u003C- DESeq(dds)\n\n# Extract results (treated vs. control)\nres \u003C- results(dds, contrast = c(\"condition\", \"treated\", \"control\"),\n               alpha = 0.05)\nsummary(res)\n# LFC shrinkage for visualization\nres_shrunk \u003C- lfcShrink(dds, contrast = c(\"condition\", \"treated\", \"control\"),\n                         type = \"apeglm\", coef = 2)\n```\n\n## Workflow\n\n### Step 1: Prepare Count Matrix and Sample Metadata\n\nBuild a `DESeqDataSet` from a gene × sample count matrix and a colData data frame. Column names of the count matrix must match row names of colData.\n\n```r\nlibrary(DESeq2)\nlibrary(dplyr)\n\n# Load raw count matrix (genes as rows, samples as columns)\ncounts \u003C- as.matrix(read.csv(\"featureCounts_matrix.csv\", row.names = 1))\n# Or from featureCounts tab-delimited output — skip first comment line\n# fc \u003C- read.table(\"featurecounts_output.txt\", header = TRUE, skip = 1, row.names = 1)\n# counts \u003C- as.matrix(fc[, 7:ncol(fc)])  # columns 7+ are sample counts\n\n# Load sample metadata\ncoldata \u003C- data.frame(\n    condition = factor(c(\"control\", \"control\", \"control\",\n                          \"treated\", \"treated\", \"treated\")),\n    batch     = factor(c(\"A\", \"A\", \"B\", \"A\", \"B\", \"B\")),\n    row.names = colnames(counts)\n)\n\n# Build DESeqDataSet\ndds \u003C- DESeqDataSetFromMatrix(countData = counts,\n                               colData   = coldata,\n                               design    = ~ condition)\n\ncat(\"Samples:\", ncol(dds), \"\\n\")\ncat(\"Genes:\", nrow(dds), \"\\n\")\ncat(\"Condition levels:\", levels(coldata$condition), \"\\n\")\n```\n\n### Step 2: Import from Salmon via tximeta\n\nWhen reads were quantified with Salmon, use `tximeta` to import transcript-level estimates with proper offset correction that accounts for transcript length and GC bias.\n\n```r\nlibrary(tximeta)\n\n# Build a coldata with a \"files\" column pointing to quant.sf files\nquant_dirs \u003C- file.path(\"salmon_output\", coldata$sample_id, \"quant.sf\")\ncoldata$files \u003C- quant_dirs\ncoldata$names \u003C- coldata$sample_id\n\n# Import with tximeta — automatically fetches transcript metadata from Ensembl\nse \u003C- tximeta(coldata)           # SummarizedExperiment with transcript-level data\n\n# Summarize to gene level (requires Ensembl or custom txdb)\ngse \u003C- summarizeToGene(se)       # gene-level SummarizedExperiment\n\n# Build DESeqDataSet from the SummarizedExperiment\ndds \u003C- DESeqDataSet(gse, design = ~ condition)\n\ncat(\"Gene-level SE dimensions:\", dim(gse), \"\\n\")\ncat(\"Assay names:\", assayNames(gse), \"\\n\")\n# Assay \"counts\" = estimated counts; \"abundance\" = TPM; \"length\" = eff. lengths\n```\n\n### Step 3: Pre-Filtering and Quality Control\n\nRemove genes with very low counts to improve statistical power and reduce the multiple testing burden. Explore sample quality with PCA on variance-stabilized counts.\n\n```r\nlibrary(ggplot2)\n\n# Pre-filter: keep genes with at least 10 reads total\nkeep \u003C- rowSums(counts(dds)) >= 10\ndds \u003C- dds[keep, ]\ncat(\"Genes after pre-filtering:\", nrow(dds), \"\\n\")\n\n# Variance-stabilizing transformation for QC visualization\nvsd \u003C- vst(dds, blind = TRUE)    # blind = TRUE for exploratory QC\n\n# PCA plot\npca_data \u003C- plotPCA(vsd, intgroup = c(\"condition\", \"batch\"), returnData = TRUE)\npercent_var \u003C- round(100 * attr(pca_data, \"percentVar\"))\n\nggplot(pca_data, aes(PC1, PC2, color = condition, shape = batch)) +\n    geom_point(size = 3) +\n    xlab(paste0(\"PC1: \", percent_var[1], \"% variance\")) +\n    ylab(paste0(\"PC2: \", percent_var[2], \"% variance\")) +\n    ggtitle(\"PCA — Variance-Stabilized Counts\") +\n    theme_bw()\n\nggsave(\"pca_plot.pdf\", width = 6, height = 5)\ncat(\"Saved pca_plot.pdf\\n\")\n```\n\n### Step 4: Run DESeq() — Normalization, Dispersion, and Model Fitting\n\n`DESeq()` runs three sequential steps: (1) median-of-ratios size factor estimation, (2) gene-wise and shrunken dispersion estimation, (3) negative binomial GLM fitting and Wald statistics.\n\n```r\n# Run full DESeq2 pipeline\ndds \u003C- DESeq(dds)\n\n# Inspect size factors (library normalization)\ncat(\"Size factors:\\n\")\nprint(sizeFactors(dds))\n# Expected range: 0.3–3.0; outliers indicate library quality issues\n\n# Dispersion plot — should show decreasing dispersion trend with mean\nplotDispEsts(dds, main = \"Dispersion Estimates\")\n\n# Inspect fitted model coefficients\nresultsNames(dds)\n# [1] \"Intercept\" \"condition_treated_vs_control\"\n```\n\n### Step 5: Extract Results and Apply FDR Correction\n\nUse `results()` to extract Wald test statistics. Specify the contrast explicitly for clarity. Independent filtering maximizes the number of detectable genes.\n\n```r\n# Extract results for the contrast of interest\nres \u003C- results(dds,\n               contrast          = c(\"condition\", \"treated\", \"control\"),\n               alpha             = 0.05,   # FDR threshold for independent filtering\n               lfcThreshold      = 0,      # Test H0: |LFC| = 0 (Wald test)\n               independentFilter = TRUE)   # Maximize power via adaptive filtering\n\n# Summary: how many genes are up\u002Fdown\u002FNA\nsummary(res)\n# out of N with nonzero total read count:\n# adjusted p-value \u003C 0.05: ...\n# LFC > 0 (up): ...\n# LFC \u003C 0 (down): ...\n# outliers [Cook's distance]: ...\n# low counts [below independent filtering threshold]: ...\n\n# Convert to data frame and sort by adjusted p-value\nres_df \u003C- as.data.frame(res)\nres_df \u003C- res_df[order(res_df$padj, na.last = TRUE), ]\n\ncat(\"Significant genes (padj \u003C 0.05):\", sum(res_df$padj \u003C 0.05, na.rm = TRUE), \"\\n\")\ncat(\"Upregulated:\", sum(res_df$padj \u003C 0.05 & res_df$log2FoldChange > 0, na.rm = TRUE), \"\\n\")\ncat(\"Downregulated:\", sum(res_df$padj \u003C 0.05 & res_df$log2FoldChange \u003C 0, na.rm = TRUE), \"\\n\")\n\nwrite.csv(res_df, \"deseq2_all_results.csv\")\n```\n\n### Step 6: LFC Shrinkage with apeglm\n\nShrink log2 fold changes using the adaptive t prior (apeglm). Use shrunk LFCs for MA plots, volcano plots, and gene ranking — NOT for significance calling (padj comes from the unshrunk Wald test).\n\n```r\nlibrary(apeglm)\n\n# apeglm shrinkage — specify coefficient name from resultsNames(dds)\nres_shrunk \u003C- lfcShrink(dds,\n                         coef = \"condition_treated_vs_control\",\n                         type = \"apeglm\",\n                         res  = res)    # pass original results for speed\n\n# Compare shrunk vs. unshrunk LFC\ncat(\"Max |LFC| unshrunk:\", max(abs(res$log2FoldChange), na.rm = TRUE), \"\\n\")\ncat(\"Max |LFC| shrunk:  \", max(abs(res_shrunk$log2FoldChange), na.rm = TRUE), \"\\n\")\n\n# Export shrunk results\nres_shrunk_df \u003C- as.data.frame(res_shrunk)\nres_shrunk_df \u003C- res_shrunk_df[order(res_shrunk_df$padj, na.last = TRUE), ]\nwrite.csv(res_shrunk_df, \"deseq2_shrunk_results.csv\")\n\n# Filter significant with effect size threshold\nsig_genes \u003C- res_shrunk_df[\n    !is.na(res_shrunk_df$padj) &\n    res_shrunk_df$padj \u003C 0.05 &\n    abs(res_shrunk_df$log2FoldChange) > 1, ]\ncat(\"Significant (padj\u003C0.05, |LFC|>1):\", nrow(sig_genes), \"\\n\")\nwrite.csv(sig_genes, \"deseq2_significant.csv\")\n```\n\n### Step 7: Visualize — MA Plot, Volcano Plot, and Heatmap\n\n> **Compute\u002Fviz separation (required):** Do not combine DESeq2 computation and plotting in the same code block. Step 6 (or Step 5) saves the results CSV; this step loads that CSV and produces plots only. This way plots can be regenerated or restyled without re-running the expensive computation.\n> **Plotting:** for the volcano, **read `skills\u002Fdata-visualization\u002Fomics-plotting\u002FSKILL.md` and follow its \"Volcano\" recipe** on the exported DEG CSV (→ `figures\u002Fvolcano_plot.pdf`). The heatmap below stays native R (`pheatmap`).\n\n```r\n# Volcano: use the omics-plotting SKILL (`skills\u002Fdata-visualization\u002Fomics-plotting\u002FSKILL.md`) \"Volcano\" recipe on the exported DEG CSV -> figures\u002Fvolcano_plot.pdf\n\nlibrary(pheatmap)\n# ---Heatmap of top 50 DE genes ---\ntop50 \u003C- head(rownames(sig_genes[order(sig_genes$padj), ]), 50)\nmat   \u003C- assay(vsd)[top50, ]            # variance-stabilized expression\nmat   \u003C- mat - rowMeans(mat)            # center by gene mean\n\nannotation_col \u003C- data.frame(\n    Condition = coldata$condition,\n    Batch     = coldata$batch,\n    row.names = colnames(mat)\n)\n# Use the code in the omics-plotting SKILL (`skills\u002Fdata-visualization\u002Fomics-plotting\u002FSKILL.md`) for publication-quality heatmap plots or use below code to generate a heatmap\npheatmap(mat,\n         annotation_col  = annotation_col,\n         cluster_rows    = TRUE,\n         cluster_cols    = TRUE,\n         show_rownames   = TRUE,\n         fontsize_row    = 6,\n         filename        = \"figures\u002Fheatmap_top50.pdf\",\n         width           = 8,\n         height          = 10)\n\n```\n\n### Step 8: Multi-Factor Design and Interaction Terms\n\nFor designs with batch correction, paired samples, or interaction effects between two variables.\n\n```r\n# --- Batch-corrected two-group comparison ---\ndds_batch \u003C- DESeqDataSetFromMatrix(countData = counts,\n                                     colData   = coldata,\n                                     design    = ~ batch + condition)\ndds_batch \u003C- dds_batch[rowSums(counts(dds_batch)) >= 10, ]\ndds_batch \u003C- DESeq(dds_batch)\n\nres_batch \u003C- results(dds_batch,\n                      contrast = c(\"condition\", \"treated\", \"control\"),\n                      alpha    = 0.05)\ncat(\"Significant (batch-corrected):\", sum(res_batch$padj \u003C 0.05, na.rm = TRUE), \"\\n\")\n\n# --- Interaction design: test whether treatment effect differs between genotypes ---\n# H0: the effect of treatment is the same in WT and KO\ncoldata$genotype \u003C- factor(coldata$genotype)  # \"WT\" or \"KO\"\n\ndds_int \u003C- DESeqDataSetFromMatrix(\n    countData = counts, colData = coldata,\n    design = ~ genotype + condition + genotype:condition\n)\ndds_int \u003C- DESeq(dds_int)\nresultsNames(dds_int)\n# Interaction coefficient: \"genotypeKO.conditiontreated\"\n\n# Extract interaction term — genes where treatment effect differs by genotype\nres_int \u003C- results(dds_int, name = \"genotypeKO.conditiontreated\", alpha = 0.05)\ncat(\"Genes with significant interaction:\", sum(res_int$padj \u003C 0.05, na.rm = TRUE), \"\\n\")\n\n# --- Likelihood Ratio Test for complex designs (e.g., time series) ---\n# LRT compares full model vs. reduced model\ndds_lrt \u003C- DESeq(dds_int, test = \"LRT\", reduced = ~ genotype + condition)\nres_lrt  \u003C- results(dds_lrt)\ncat(\"Genes with genotype:condition interaction (LRT):\",\n    sum(res_lrt$padj \u003C 0.05, na.rm = TRUE), \"\\n\")\n```\n\n## Key Parameters\n\n| Parameter | Function | Default | Range \u002F Options | Effect |\n|-----------|----------|---------|-----------------|--------|\n| `design` | `DESeqDataSetFromMatrix()` | (required) | `~ var` or `~ cov + var` | Specifies the GLM formula; put batch\u002Fcovariates before variable of interest |\n| `contrast` | `results()` | `NULL` | `c(var, level1, level2)` or coefficient name | Defines the comparison; always specify explicitly for clarity |\n| `alpha` | `results()` | `0.1` | `0.01`–`0.10` | FDR threshold used for independent filtering; does not change padj values |\n| `lfcThreshold` | `results()` | `0` | `0`–`2` | Test H0: |LFC| ≤ threshold (TREAT-style test, more stringent) |\n| `independentFilter` | `results()` | `TRUE` | `TRUE`\u002F`FALSE` | Adaptive mean-count filtering to maximize the number of rejectible hypotheses |\n| `type` | `lfcShrink()` | `\"apeglm\"` | `\"apeglm\"`, `\"ashr\"`, `\"normal\"` | Shrinkage prior; apeglm is fastest and best calibrated |\n| `coef` | `lfcShrink()` | (required) | coefficient name from `resultsNames()` | Which model coefficient to shrink; must match `resultsNames()` output |\n| `test` | `DESeq()` | `\"Wald\"` | `\"Wald\"`, `\"LRT\"` | Wald: two-group comparisons; LRT: model comparison for complex designs |\n| `reduced` | `DESeq()` | `NULL` | formula without the term to test | Required for LRT; specifies the null model |\n| `blind` | `vst()` \u002F `rlog()` | `TRUE` | `TRUE`\u002F`FALSE` | `TRUE` for QC\u002Fexploration; `FALSE` for downstream analysis using dispersion info |\n\n## Key Concepts\n\n### Negative Binomial Model and Dispersion\n\nDESeq2 models each gene's count as Negative Binomial(mean = µ, dispersion = α), where mean µ depends on the experimental design through a log-linear model. The key innovation is **dispersion shrinkage**: gene-wise dispersion estimates are shrunk toward a fitted trend across all genes using an empirical Bayes approach, borrowing information across genes to stabilize estimates from small sample sizes.\n\n### Size Factor Normalization\n\nDESeq2 uses the **median-of-ratios** method: for each sample, compute the ratio of each gene's count to the geometric mean of that gene across all samples, then take the median ratio across all genes as the size factor. This is robust to differentially expressed genes and does not require any assumptions about the fraction of DE genes.\n\n### Wald Test vs. Likelihood Ratio Test\n\n- **Wald test** (default): tests a single coefficient `β = 0`; fast and appropriate for two-group comparisons and specific contrasts in multi-factor designs\n- **LRT**: compares the full model to a `reduced` model by the likelihood ratio; appropriate for testing whether any level of a multi-level factor (e.g., 5 time points) has an effect, or for testing interaction terms as a group\n\n### Cook's Distance Outlier Detection\n\nDESeq2 computes Cook's distance per gene per sample to flag potential expression outliers. Genes where any sample has a Cook's distance above a threshold are automatically replaced with `NA` in the results. With `refit = TRUE` (default), outlier samples are excluded and the model is refit for that gene.\n\n## Common Recipes\n\n### Recipe: Multiple Contrasts from One Fitted Model\n\nEfficient when comparing multiple treatment groups — fit `DESeq()` once, extract multiple contrasts.\n\n```r\n# Fit once\ndds_multi \u003C- DESeqDataSetFromMatrix(counts, coldata, design = ~ condition)\ndds_multi \u003C- dds_multi[rowSums(counts(dds_multi)) >= 10, ]\ndds_multi \u003C- DESeq(dds_multi)\n\n# Extract contrasts — all vs. \"control\"\ncontrasts_list \u003C- list(\n    A_vs_ctrl = c(\"condition\", \"treatment_A\", \"control\"),\n    B_vs_ctrl = c(\"condition\", \"treatment_B\", \"control\"),\n    C_vs_ctrl = c(\"condition\", \"treatment_C\", \"control\")\n)\n\nresults_list \u003C- lapply(names(contrasts_list), function(nm) {\n    res \u003C- results(dds_multi, contrast = contrasts_list[[nm]], alpha = 0.05)\n    res_shrunk \u003C- lfcShrink(dds_multi, contrast = contrasts_list[[nm]],\n                             type = \"apeglm\", res = res)\n    write.csv(as.data.frame(res_shrunk), paste0(\"results_\", nm, \".csv\"))\n    cat(nm, \"— significant:\", sum(res$padj \u003C 0.05, na.rm = TRUE), \"\\n\")\n    as.data.frame(res_shrunk)\n})\nnames(results_list) \u003C- names(contrasts_list)\n```\n\n### Recipe: Paired Sample Design\n\nEliminate within-subject variability by including subject as a blocking factor.\n\n```r\n# Paired design: each subject has pre- and post-treatment samples\ncoldata$subject   \u003C- factor(c(\"S1\",\"S1\",\"S2\",\"S2\",\"S3\",\"S3\"))\ncoldata$timepoint \u003C- factor(c(\"pre\",\"post\",\"pre\",\"post\",\"pre\",\"post\"))\n\ndds_paired \u003C- DESeqDataSetFromMatrix(counts, coldata,\n                                      design = ~ subject + timepoint)\ndds_paired \u003C- dds_paired[rowSums(counts(dds_paired)) >= 10, ]\ndds_paired \u003C- DESeq(dds_paired)\n\nres_paired \u003C- results(dds_paired,\n                       contrast = c(\"timepoint\", \"post\", \"pre\"),\n                       alpha    = 0.05)\ncat(\"Significant (paired):\", sum(res_paired$padj \u003C 0.05, na.rm = TRUE), \"\\n\")\n```\n\n### Recipe: Rank Genes for GSEA Using Shrunk LFC × -log10(pvalue)\n\nCreate a pre-ranked gene list for input to GSEA or gseapy's `prerank` function.\n\n```r\n# Build ranking metric: signed -log10(pvalue) with LFC direction\nres_rank \u003C- as.data.frame(res_shrunk)\nres_rank \u003C- res_rank[!is.na(res_rank$pvalue) & !is.na(res_rank$log2FoldChange), ]\nres_rank$rank_metric \u003C- sign(res_rank$log2FoldChange) *\n                         (-log10(res_rank$pvalue + 1e-300))\nres_rank \u003C- res_rank[order(res_rank$rank_metric, decreasing = TRUE), ]\n\n# Export as two-column TSv for fgsea or gseapy prerank\nwrite.table(data.frame(gene  = rownames(res_rank),\n                        score = res_rank$rank_metric),\n            \"gsea_ranked_list.rnk\",\n            sep = \"\\t\", quote = FALSE, row.names = FALSE, col.names = FALSE)\ncat(\"Ranked gene list saved:\", nrow(res_rank), \"genes\\n\")\n```\n\n### Recipe: Save and Reload Fitted DESeq2 Object\n\nAvoid re-running the expensive `DESeq()` step in subsequent sessions.\n\n```r\n# Save fitted DDS object\nsaveRDS(dds, \"dds_fitted.rds\")\ncat(\"Saved fitted DESeqDataSet to dds_fitted.rds\\n\")\n\n# Reload in a new session\ndds_loaded \u003C- readRDS(\"dds_fitted.rds\")\n# Continue with results(), lfcShrink(), etc.\nres_reloaded \u003C- results(dds_loaded, contrast = c(\"condition\", \"treated\", \"control\"))\n```\n\n## Expected Outputs\n\n| File | Description |\n|------|-------------|\n| `deseq2_all_results.csv` | Full results table: baseMean, log2FoldChange, lfcSE, stat, pvalue, padj for all genes |\n| `deseq2_shrunk_results.csv` | Results with apeglm-shrunk LFC; use for ranking and visualization |\n| `deseq2_significant.csv` | Filtered results: padj \u003C 0.05 and |LFC| > 1 |\n| `pca_plot.pdf` | PCA of variance-stabilized counts; used to check batch structure and outliers |\n| `dispersion_plot.pdf` | Gene-wise vs. fitted dispersions; should show tight cloud around trend |\n| `figures\u002Fvolcano_plot.pdf` | Volcano plot with significance and LFC thresholds labeled |\n| `figures\u002Fheatmap_top50.pdf` | Hierarchically clustered heatmap of top 50 DE genes |\n| `gsea_ranked_list.rnk` | Pre-ranked gene list for pathway enrichment (fgsea\u002Fgseapy) |\n| `dds_fitted.rds` | Serialized DESeqDataSet for checkpoint\u002Fresume |\n\n## Troubleshooting\n\n| Problem | Cause | Solution |\n|---------|-------|----------|\n| `Error: design contains one or more variables with all samples having the same value` | A design factor has only one level | Check `table(coldata$condition)`; ensure all factor levels are present |\n| `Model matrix not full rank` | Confounded design (e.g., batch perfectly correlated with condition) | Run `table(coldata$batch, coldata$condition)`; drop the confounded variable or collect more samples |\n| All or most `padj = NA` | Genes flagged by independent filtering (low mean count) or Cook's outliers | Check `summary(res)` for count of filtered genes; relax `alpha` or verify data quality |\n| No significant genes despite strong biology | Under-powered experiment or high within-group variability | Verify n ≥ 3 per group; check PCA for outliers; inspect p-value histogram (should show spike near 0) |\n| `Error in lfcShrink`: coefficient not found | `coef` name does not match `resultsNames(dds)` | Run `resultsNames(dds)` and copy the exact coefficient string into `coef =` |\n| `apeglm` fails with convergence warning | Extreme LFC estimates (sparse data or complete separation) | Switch to `type = \"ashr\"` which is more robust to these cases |\n| Very large size factors (> 5) | Extremely different library sizes, or normalized counts accidentally used | Check `colSums(counts(dds))`; ensure input is raw integer counts from aligner\u002Fcounter |\n| Dispersion plot shows outlier cloud far from trend | Noisy or failed libraries; incorrect metadata grouping | Examine PCA; remove outlier samples; check that `design` formula matches biological groups |\n\n## References\n\n- [DESeq2 Bioconductor page](https:\u002F\u002Fbioconductor.org\u002Fpackages\u002FDESeq2) — Package home, vignette, and reference manual\n- [DESeq2 vignette](https:\u002F\u002Fbioconductor.org\u002Fpackages\u002Frelease\u002Fbioc\u002Fvignettes\u002FDESeq2\u002Finst\u002Fdoc\u002FDESeq2.html) — Comprehensive worked example covering all design types\n- [Love et al. (2014) Genome Biology](https:\u002F\u002Fdoi.org\u002F10.1186\u002Fs13059-014-0550-8) — Original DESeq2 paper: moderated estimation of fold change and dispersion for RNA-seq data\n- [Zhu et al. (2019) Bioinformatics](https:\u002F\u002Fdoi.org\u002F10.1093\u002Fbioinformatics\u002Fbty895) — apeglm LFC shrinkage for DESeq2\n- [tximeta Bioconductor page](https:\u002F\u002Fbioconductor.org\u002Fpackages\u002Ftximeta) — Salmon import with transcript-level metadata\n- [EnhancedVolcano Bioconductor page](https:\u002F\u002Fbioconductor.org\u002Fpackages\u002FEnhancedVolcano) — Publication-quality volcano plots for DE results\n","# DESeq2 Differential Expression Analysis (R\u002FBioconductor)\n\n## Overview\n\nDESeq2 is the Bioconductor R package for differential gene expression analysis from bulk RNA-seq count data. It fits a negative binomial generalized linear model per gene, estimates dispersion parameters using empirical Bayes shrinkage across genes, and tests differential expression using Wald tests (two-group) or likelihood ratio tests (complex designs). DESeq2 is the R gold standard for RNA-seq DE analysis, with native Bioconductor integration for seamless import from Salmon (tximeta\u002Ftximport), featureCounts, or HTSeq.\n\n## When to Use\n\n- Identifying differentially expressed genes between two experimental conditions (treated vs. control, disease vs. healthy) from bulk RNA-seq count data\n- Analyzing multi-factor designs that account for batch effects or covariates (e.g., `~ batch + condition`)\n- Testing complex hypotheses with interaction terms (e.g., time × treatment) or reduced models using likelihood ratio tests (LRT)\n- Importing Salmon pseudoalignment output via tximeta or tximport for transcript-level uncertainty propagation\n- Performing LFC shrinkage with apeglm for ranked gene lists, volcano plots, and downstream pathway analysis\n- Conducting time-series experiments or any design with more than two levels requiring model comparison\n- Working in an R\u002FBioconductor ecosystem where integration with SummarizedExperiment, clusterProfiler, or EnhancedVolcano is needed\n- Use **pydeseq2-differential-expression** instead for Python-based pipelines with the same statistical model\n- Use **edgeR** for negative binomial DE with TMM normalization, quasi-likelihood F-tests, or TREAT testing\n- Use **omics-plotting** SKILL after DE and for publication-quality plots of DESeq2 results\n- Use **gseapy-gene-enrichment** SKILL after DE to interpret results at the pathway level\n\n## Prerequisites\n\n- **R packages**: `DESeq2` (Bioconductor), `tximeta` or `tximport` (Salmon import), `apeglm` (LFC shrinkage), `pheatmap`, `ggplot2`, `EnhancedVolcano`\n- **Data requirements**: Raw (unnormalized) integer count matrix — gene rows × sample columns — plus a sample metadata data frame with matching column names. If using Salmon: per-sample `quant.sf` files and a transcript-to-gene mapping\n- **Environment**: R ≥ 4.2, Bioconductor ≥ 3.16\n\n```r\n# Install Bioconductor packages\nif (!require(\"BiocManager\", quietly = TRUE))\n    install.packages(\"BiocManager\")\n\nBiocManager::install(c(\"DESeq2\", \"tximeta\", \"tximport\", \"apeglm\",\n                        \"EnhancedVolcano\"))\ninstall.packages(c(\"pheatmap\", \"ggplot2\", \"dplyr\"))\n```\n\n## Pre-flight Interview\n\nSettle these with the user before writing any analysis code.\n\n```yaml\ndecisions:\n  - id: D1\n    param: design\n    kind: required\n    source: data\n    ask: \"Which column of the sample sheet separates the groups you want to compare?\"\n    default: null\n\n  - id: D2\n    param: contrast\n    kind: required\n    source: user\n    depends_on: [D1]\n    ask: \"Which two groups should be compared, and which group should have positive log2 fold changes?\"\n    default: null\n\n  - id: D3\n    param: design\n    kind: required\n    source: data\n    depends_on: [D1]\n    ask: \"Which available variables should be included to adjust for nuisance variation (batch, donor, sex, sequencing run)?\"\n    default: \"none\"\n    skip_if: \"sample sheet carries no column besides the grouping variable\"\n\n  - id: D4\n    param: test\n    kind: required\n    source: user\n    depends_on: [D1, D2]\n    ask: \"Is the primary hypothesis a named comparison, or an overall effect of this factor across its levels?\"\n    default: \"named comparison (Wald test)\"\n\n  - id: D5\n    param: alpha\n    kind: optional\n    source: user\n    ask: \"How strong must the evidence be before a gene counts as changed (false-discovery rate)?\"\n    default: 0.05\n\n  - id: D6\n    param: lfcThreshold\n    kind: optional\n    source: user\n    ask: \"Test against a zero change, or against a minimum fold change worth caring about?\"\n    default: 0\n\n  - id: D7\n    param: lfcShrink.type\n    kind: derived\n    source: upstream\n    ask: \"Shrink fold-change estimates so low-count genes stop dominating the rankings and plots?\"\n    default: \"apeglm, applied for ranking and visualization\"\n\n  - id: D8\n    param: blind (vst\u002Frlog)\n    kind: derived\n    source: upstream\n    ask: \"Should the variance-stabilizing transform ignore the design?\"\n    default: \"TRUE for QC\u002Fclustering, FALSE for design-aware downstream uses\"\n\n  - id: D9\n    param: independentFilter\n    kind: optional\n    source: user\n    ask: \"Should DESeq2 use its adaptive mean-count filter to maximize discoveries at the chosen FDR threshold?\"\n    default: true\n```\n\n`design` appears twice because the formula is assembled from both D1 and D3;\nD2 defines the direction of the reported effect. The pipeline explicitly passes\n`alpha: 0.05` to `results()` so independent filtering is optimized for the\nreported FDR threshold rather than DESeq2's default of `0.1`.\n\n## Quick Start\n\nComplete two-group comparison from a count matrix in under 20 lines.\n\n```r\nlibrary(DESeq2)\n\n# Load count matrix (genes x samples) and metadata\ncounts \u003C- as.matrix(read.csv(\"counts.csv\", row.names = 1))\ncoldata \u003C- read.csv(\"metadata.csv\", row.names = 1)\ncoldata$condition \u003C- factor(coldata$condition)\n\n# Build DESeqDataSet, run full pipeline\ndds \u003C- DESeqDataSetFromMatrix(countData = counts,\n                               colData   = coldata,\n                               design    = ~ condition)\ndds \u003C- dds[rowSums(counts(dds)) >= 10, ]   # pre-filter\ndds \u003C- DESeq(dds)\n\n# Extract results (treated vs. control)\nres \u003C- results(dds, contrast = c(\"condition\", \"treated\", \"control\"),\n               alpha = 0.05)\nsummary(res)\n# LFC shrinkage for visualization\nres_shrunk \u003C- lfcShrink(dds, contrast = c(\"condition\", \"treated\", \"control\"),\n                         type = \"apeglm\", coef = 2)\n```\n\n## Workflow\n\n### Step 1: Prepare Count Matrix and Sample Metadata\n\nBuild a `DESeqDataSet` from a gene × sample count matrix and a colData data frame. Column names of the count matrix must match row names of colData.\n\n```r\nlibrary(DESeq2)\nlibrary(dplyr)\n\n# Load raw count matrix (genes as rows, samples as columns)\ncounts \u003C- as.matrix(read.csv(\"featureCounts_matrix.csv\", row.names = 1))\n# Or from featureCounts tab-delimited output — skip first comment line\n# fc \u003C- read.table(\"featurecounts_output.txt\", header = TRUE, skip = 1, row.names = 1)\n# counts \u003C- as.matrix(fc[, 7:ncol(fc)])  # columns 7+ are sample counts\n\n# Load sample metadata\ncoldata \u003C- data.frame(\n    condition = factor(c(\"control\", \"control\", \"control\",\n                          \"treated\", \"treated\", \"treated\")),\n    batch     = factor(c(\"A\", \"A\", \"B\", \"A\", \"B\", \"B\")),\n    row.names = colnames(counts)\n)\n\n# Build DESeqDataSet\ndds \u003C- DESeqDataSetFromMatrix(countData = counts,\n                               colData   = coldata,\n                               design    = ~ condition)\n\ncat(\"Samples:\", ncol(dds), \"\\n\")\ncat(\"Genes:\", nrow(dds), \"\\n\")\ncat(\"Condition levels:\", levels(coldata$condition), \"\\n\")\n```\n\n### Step 2: Import from Salmon via tximeta\n\nWhen reads were quantified with Salmon, use `tximeta` to import transcript-level estimates with proper offset correction that accounts for transcript length and GC bias.\n\n```r\nlibrary(tximeta)\n\n# Build a coldata with a \"files\" column pointing to quant.sf files\nquant_dirs \u003C- file.path(\"salmon_output\", coldata$sample_id, \"quant.sf\")\ncoldata$files \u003C- quant_dirs\ncoldata$names \u003C- coldata$sample_id\n\n# Import with tximeta — automatically fetches transcript metadata from Ensembl\nse \u003C- tximeta(coldata)           # SummarizedExperiment with transcript-level data\n\n# Summarize to gene level (requires Ensembl or custom txdb)\ngse \u003C- summarizeToGene(se)       # gene-level SummarizedExperiment\n\n# Build DESeqDataSet from the SummarizedExperiment\ndds \u003C- DESeqDataSet(gse, design = ~ condition)\n\ncat(\"Gene-level SE dimensions:\", dim(gse), \"\\n\")\ncat(\"Assay names:\", assayNames(gse), \"\\n\")\n# Assay \"counts\" = estimated counts; \"abundance\" = TPM; \"length\" = eff. lengths\n```\n\n### Step 3: Pre-Filtering and Quality Control\n\nRemove genes with very low counts to improve statistical power and reduce the multiple testing burden. Explore sample quality with PCA on variance-stabilized counts.\n\n```r\nlibrary(ggplot2)\n\n# Pre-filter: keep genes with at least 10 reads total\nkeep \u003C- rowSums(counts(dds)) >= 10\ndds \u003C- dds[keep, ]\ncat(\"Genes after pre-filtering:\", nrow(dds), \"\\n\")\n\n# Variance-stabilizing transformation for QC visualization\nvsd \u003C- vst(dds, blind = TRUE)    # blind = TRUE for exploratory QC\n\n# PCA plot\npca_data \u003C- plotPCA(vsd, intgroup = c(\"condition\", \"batch\"), returnData = TRUE)\npercent_var \u003C- round(100 * attr(pca_data, \"percentVar\"))\n\nggplot(pca_data, aes(PC1, PC2, color = condition, shape = batch)) +\n    geom_point(size = 3) +\n    xlab(paste0(\"PC1: \", percent_var[1], \"% variance\")) +\n    ylab(paste0(\"PC2: \", percent_var[2], \"% variance\")) +\n    ggtitle(\"PCA — Variance-Stabilized Counts\") +\n    theme_bw()\n\nggsave(\"pca_plot.pdf\", width = 6, height = 5)\ncat(\"Saved pca_plot.pdf\\n\")\n```\n\n### Step 4: Run DESeq() — Normalization, Dispersion, and Model Fitting\n\n`DESeq()` runs three sequential steps: (1) median-of-ratios size factor estimation, (2) gene-wise and shrunken dispersion estimation, (3) negative binomial GLM fitting and Wald statistics.\n\n```r\n# Run full DESeq2 pipeline\ndds \u003C- DESeq(dds)\n\n# Inspect size factors (library normalization)\ncat(\"Size factors:\\n\")\nprint(sizeFactors(dds))\n# Expected range: 0.3–3.0; outliers indicate library quality issues\n\n# Dispersion plot — should show decreasing dispersion trend with mean\nplotDispEsts(dds, main = \"Dispersion Estimates\")\n\n# Inspect fitted model coefficients\nresultsNames(dds)\n# [1] \"Intercept\" \"condition_treated_vs_control\"\n```\n\n### Step 5: Extract Results and Apply FDR Correction\n\nUse `results()` to extract Wald test statistics. Specify the contrast explicitly for clarity. Independent filtering maximizes the number of detectable genes.\n\n```r\n# Extract results for the contrast of interest\nres \u003C- results(dds,\n               contrast          = c(\"condition\", \"treated\", \"control\"),\n               alpha             = 0.05,   # FDR threshold for independent filtering\n               lfcThreshold      = 0,      # Test H0: |LFC| = 0 (Wald test)\n               independentFilter = TRUE)   # Maximize power via adaptive filtering\n\n# Summary: how many genes are up\u002Fdown\u002FNA\nsummary(res)\n# out of N with nonzero total read count:\n# adjusted p-value \u003C 0.05: ...\n# LFC > 0 (up): ...\n# LFC \u003C 0 (down): ...\n# outliers [Cook's distance]: ...\n# low counts [below independent filtering threshold]: ...\n\n# Convert to data frame and sort by adjusted p-value\nres_df \u003C- as.data.frame(res)\nres_df \u003C- res_df[order(res_df$padj, na.last = TRUE), ]\n\ncat(\"Significant genes (padj \u003C 0.05):\", sum(res_df$padj \u003C 0.05, na.rm = TRUE), \"\\n\")\ncat(\"Upregulated:\", sum(res_df$padj \u003C 0.05 & res_df$log2FoldChange > 0, na.rm = TRUE), \"\\n\")\ncat(\"Downregulated:\", sum(res_df$padj \u003C 0.05 & res_df$log2FoldChange \u003C 0, na.rm = TRUE), \"\\n\")\n\nwrite.csv(res_df, \"deseq2_all_results.csv\")\n```\n\n### Step 6: LFC Shrinkage with apeglm\n\nShrink log2 fold changes using the adaptive t prior (apeglm). Use shrunk LFCs for MA plots, volcano plots, and gene ranking — NOT for significance calling (padj comes from the unshrunk Wald test).\n\n```r\nlibrary(apeglm)\n\n# apeglm shrinkage — specify coefficient name from resultsNames(dds)\nres_shrunk \u003C- lfcShrink(dds,\n                         coef = \"condition_treated_vs_control\",\n                         type = \"apeglm\",\n                         res  = res)    # pass original results for speed\n\n# Compare shrunk vs. unshrunk LFC\ncat(\"Max |LFC| unshrunk:\", max(abs(res$log2FoldChange), na.rm = TRUE), \"\\n\")\ncat(\"Max |LFC| shrunk:  \", max(abs(res_shrunk$log2FoldChange), na.rm = TRUE), \"\\n\")\n\n# Export shrunk results\nres_shrunk_df \u003C- as.data.frame(res_shrunk)\nres_shrunk_df \u003C- res_shrunk_df[order(res_shrunk_df$padj, na.last = TRUE), ]\nwrite.csv(res_shrunk_df, \"deseq2_shrunk_results.csv\")\n\n# Filter significant with effect size threshold\nsig_genes \u003C- res_shrunk_df[\n    !is.na(res_shrunk_df$padj) &\n    res_shrunk_df$padj \u003C 0.05 &\n    abs(res_shrunk_df$log2FoldChange) > 1, ]\ncat(\"Significant (padj\u003C0.05, |LFC|>1):\", nrow(sig_genes), \"\\n\")\nwrite.csv(sig_genes, \"deseq2_significant.csv\")\n```\n\n### Step 7: Visualize — MA Plot, Volcano Plot, and Heatmap\n\n> **Compute\u002Fviz separation (required):** Do not combine DESeq2 computation and plotting in the same code block. Step 6 (or Step 5) saves the results CSV; this step loads that CSV and produces plots only. This way plots can be regenerated or restyled without re-running the expensive computation.\n> **Plotting:** for the volcano, **read `skills\u002Fdata-visualization\u002Fomics-plotting\u002FSKILL.md` and follow its \"Volcano\" recipe** on the exported DEG CSV (→ `figures\u002Fvolcano_plot.pdf`). The heatmap below stays native R (`pheatmap`).\n\n```r\n# Volcano: use the omics-plotting SKILL (`skills\u002Fdata-visualization\u002Fomics-plotting\u002FSKILL.md`) \"Volcano\" recipe on the exported DEG CSV -> figures\u002Fvolcano_plot.pdf\n\nlibrary(pheatmap)\n# ---Heatmap of top 50 DE genes ---\ntop50 \u003C- head(rownames(sig_genes[order(sig_genes$padj), ]), 50)\nmat   \u003C- assay(vsd)[top50, ]            # variance-stabilized expression\nmat   \u003C- mat - rowMeans(mat)            # center by gene mean\n\nannotation_col \u003C- data.frame(\n    Condition = coldata$condition,\n    Batch     = coldata$batch,\n    row.names = colnames(mat)\n)\n# Use the code in the omics-plotting SKILL (`skills\u002Fdata-visualization\u002Fomics-plotting\u002FSKILL.md`) for publication-quality heatmap plots or use below code to generate a heatmap\npheatmap(mat,\n         annotation_col  = annotation_col,\n         cluster_rows    = TRUE,\n         cluster_cols    = TRUE,\n         show_rownames   = TRUE,\n         fontsize_row    = 6,\n         filename        = \"figures\u002Fheatmap_top50.pdf\",\n         width           = 8,\n         height          = 10)\n\n```\n\n### Step 8: Multi-Factor Design and Interaction Terms\n\nFor designs with batch correction, paired samples, or interaction effects between two variables.\n\n```r\n# --- Batch-corrected two-group comparison ---\ndds_batch \u003C- DESeqDataSetFromMatrix(countData = counts,\n                                     colData   = coldata,\n                                     design    = ~ batch + condition)\ndds_batch \u003C- dds_batch[rowSums(counts(dds_batch)) >= 10, ]\ndds_batch \u003C- DESeq(dds_batch)\n\nres_batch \u003C- results(dds_batch,\n                      contrast = c(\"condition\", \"treated\", \"control\"),\n                      alpha    = 0.05)\ncat(\"Significant (batch-corrected):\", sum(res_batch$padj \u003C 0.05, na.rm = TRUE), \"\\n\")\n\n# --- Interaction design: test whether treatment effect differs between genotypes ---\n# H0: the effect of treatment is the same in WT and KO\ncoldata$genotype \u003C- factor(coldata$genotype)  # \"WT\" or \"KO\"\n\ndds_int \u003C- DESeqDataSetFromMatrix(\n    countData = counts, colData = coldata,\n    design = ~ genotype + condition + genotype:condition\n)\ndds_int \u003C- DESeq(dds_int)\nresultsNames(dds_int)\n# Interaction coefficient: \"genotypeKO.conditiontreated\"\n\n# Extract interaction term — genes where treatment effect differs by genotype\nres_int \u003C- results(dds_int, name = \"genotypeKO.conditiontreated\", alpha = 0.05)\ncat(\"Genes with significant interaction:\", sum(res_int$padj \u003C 0.05, na.rm = TRUE), \"\\n\")\n\n# --- Likelihood Ratio Test for complex designs (e.g., time series) ---\n# LRT compares full model vs. reduced model\ndds_lrt \u003C- DESeq(dds_int, test = \"LRT\", reduced = ~ genotype + condition)\nres_lrt  \u003C- results(dds_lrt)\ncat(\"Genes with genotype:condition interaction (LRT):\",\n    sum(res_lrt$padj \u003C 0.05, na.rm = TRUE), \"\\n\")\n```\n\n## Key Parameters\n\n| Parameter | Function | Default | Range \u002F Options | Effect |\n|-----------|----------|---------|-----------------|--------|\n| `design` | `DESeqDataSetFromMatrix()` | (required) | `~ var` or `~ cov + var` | Specifies the GLM formula; put batch\u002Fcovariates before variable of interest |\n| `contrast` | `results()` | `NULL` | `c(var, level1, level2)` or coefficient name | Defines the comparison; always specify explicitly for clarity |\n| `alpha` | `results()` | `0.1` | `0.01`–`0.10` | FDR threshold used for independent filtering; does not change padj values |\n| `lfcThreshold` | `results()` | `0` | `0`–`2` | Test H0: |LFC| ≤ threshold (TREAT-style test, more stringent) |\n| `independentFilter` | `results()` | `TRUE` | `TRUE`\u002F`FALSE` | Adaptive mean-count filtering to maximize the number of rejectible hypotheses |\n| `type` | `lfcShrink()` | `\"apeglm\"` | `\"apeglm\"`, `\"ashr\"`, `\"normal\"` | Shrinkage prior; apeglm is fastest and best calibrated |\n| `coef` | `lfcShrink()` | (required) | coefficient name from `resultsNames()` | Which model coefficient to shrink; must match `resultsNames()` output |\n| `test` | `DESeq()` | `\"Wald\"` | `\"Wald\"`, `\"LRT\"` | Wald: two-group comparisons; LRT: model comparison for complex designs |\n| `reduced` | `DESeq()` | `NULL` | formula without the term to test | Required for LRT; specifies the null model |\n| `blind` | `vst()` \u002F `rlog()` | `TRUE` | `TRUE`\u002F`FALSE` | `TRUE` for QC\u002Fexploration; `FALSE` for downstream analysis using dispersion info |\n\n## Key Concepts\n\n### Negative Binomial Model and Dispersion\n\nDESeq2 models each gene's count as Negative Binomial(mean = µ, dispersion = α), where mean µ depends on the experimental design through a log-linear model. The key innovation is **dispersion shrinkage**: gene-wise dispersion estimates are shrunk toward a fitted trend across all genes using an empirical Bayes approach, borrowing information across genes to stabilize estimates from small sample sizes.\n\n### Size Factor Normalization\n\nDESeq2 uses the **median-of-ratios** method: for each sample, compute the ratio of each gene's count to the geometric mean of that gene across all samples, then take the median ratio across all genes as the size factor. This is robust to differentially expressed genes and does not require any assumptions about the fraction of DE genes.\n\n### Wald Test vs. Likelihood Ratio Test\n\n- **Wald test** (default): tests a single coefficient `β = 0`; fast and appropriate for two-group comparisons and specific contrasts in multi-factor designs\n- **LRT**: compares the full model to a `reduced` model by the likelihood ratio; appropriate for testing whether any level of a multi-level factor (e.g., 5 time points) has an effect, or for testing interaction terms as a group\n\n### Cook's Distance Outlier Detection\n\nDESeq2 computes Cook's distance per gene per sample to flag potential expression outliers. Genes where any sample has a Cook's distance above a threshold are automatically replaced with `NA` in the results. With `refit = TRUE` (default), outlier samples are excluded and the model is refit for that gene.\n\n## Common Recipes\n\n### Recipe: Multiple Contrasts from One Fitted Model\n\nEfficient when comparing multiple treatment groups — fit `DESeq()` once, extract multiple contrasts.\n\n```r\n# Fit once\ndds_multi \u003C- DESeqDataSetFromMatrix(counts, coldata, design = ~ condition)\ndds_multi \u003C- dds_multi[rowSums(counts(dds_multi)) >= 10, ]\ndds_multi \u003C- DESeq(dds_multi)\n\n# Extract contrasts — all vs. \"control\"\ncontrasts_list \u003C- list(\n    A_vs_ctrl = c(\"condition\", \"treatment_A\", \"control\"),\n    B_vs_ctrl = c(\"condition\", \"treatment_B\", \"control\"),\n    C_vs_ctrl = c(\"condition\", \"treatment_C\", \"control\")\n)\n\nresults_list \u003C- lapply(names(contrasts_list), function(nm) {\n    res \u003C- results(dds_multi, contrast = contrasts_list[[nm]], alpha = 0.05)\n    res_shrunk \u003C- lfcShrink(dds_multi, contrast = contrasts_list[[nm]],\n                             type = \"apeglm\", res = res)\n    write.csv(as.data.frame(res_shrunk), paste0(\"results_\", nm, \".csv\"))\n    cat(nm, \"— significant:\", sum(res$padj \u003C 0.05, na.rm = TRUE), \"\\n\")\n    as.data.frame(res_shrunk)\n})\nnames(results_list) \u003C- names(contrasts_list)\n```\n\n### Recipe: Paired Sample Design\n\nEliminate within-subject variability by including subject as a blocking factor.\n\n```r\n# Paired design: each subject has pre- and post-treatment samples\ncoldata$subject   \u003C- factor(c(\"S1\",\"S1\",\"S2\",\"S2\",\"S3\",\"S3\"))\ncoldata$timepoint \u003C- factor(c(\"pre\",\"post\",\"pre\",\"post\",\"pre\",\"post\"))\n\ndds_paired \u003C- DESeqDataSetFromMatrix(counts, coldata,\n                                      design = ~ subject + timepoint)\ndds_paired \u003C- dds_paired[rowSums(counts(dds_paired)) >= 10, ]\ndds_paired \u003C- DESeq(dds_paired)\n\nres_paired \u003C- results(dds_paired,\n                       contrast = c(\"timepoint\", \"post\", \"pre\"),\n                       alpha    = 0.05)\ncat(\"Significant (paired):\", sum(res_paired$padj \u003C 0.05, na.rm = TRUE), \"\\n\")\n```\n\n### Recipe: Rank Genes for GSEA Using Shrunk LFC × -log10(pvalue)\n\nCreate a pre-ranked gene list for input to GSEA or gseapy's `prerank` function.\n\n```r\n# Build ranking metric: signed -log10(pvalue) with LFC direction\nres_rank \u003C- as.data.frame(res_shrunk)\nres_rank \u003C- res_rank[!is.na(res_rank$pvalue) & !is.na(res_rank$log2FoldChange), ]\nres_rank$rank_metric \u003C- sign(res_rank$log2FoldChange) *\n                         (-log10(res_rank$pvalue + 1e-300))\nres_rank \u003C- res_rank[order(res_rank$rank_metric, decreasing = TRUE), ]\n\n# Export as two-column TSv for fgsea or gseapy prerank\nwrite.table(data.frame(gene  = rownames(res_rank),\n                        score = res_rank$rank_metric),\n            \"gsea_ranked_list.rnk\",\n            sep = \"\\t\", quote = FALSE, row.names = FALSE, col.names = FALSE)\ncat(\"Ranked gene list saved:\", nrow(res_rank), \"genes\\n\")\n```\n\n### Recipe: Save and Reload Fitted DESeq2 Object\n\nAvoid re-running the expensive `DESeq()` step in subsequent sessions.\n\n```r\n# Save fitted DDS object\nsaveRDS(dds, \"dds_fitted.rds\")\ncat(\"Saved fitted DESeqDataSet to dds_fitted.rds\\n\")\n\n# Reload in a new session\ndds_loaded \u003C- readRDS(\"dds_fitted.rds\")\n# Continue with results(), lfcShrink(), etc.\nres_reloaded \u003C- results(dds_loaded, contrast = c(\"condition\", \"treated\", \"control\"))\n```\n\n## Expected Outputs\n\n| File | Description |\n|------|-------------|\n| `deseq2_all_results.csv` | Full results table: baseMean, log2FoldChange, lfcSE, stat, pvalue, padj for all genes |\n| `deseq2_shrunk_results.csv` | Results with apeglm-shrunk LFC; use for ranking and visualization |\n| `deseq2_significant.csv` | Filtered results: padj \u003C 0.05 and |LFC| > 1 |\n| `pca_plot.pdf` | PCA of variance-stabilized counts; used to check batch structure and outliers |\n| `dispersion_plot.pdf` | Gene-wise vs. fitted dispersions; should show tight cloud around trend |\n| `figures\u002Fvolcano_plot.pdf` | Volcano plot with significance and LFC thresholds labeled |\n| `figures\u002Fheatmap_top50.pdf` | Hierarchically clustered heatmap of top 50 DE genes |\n| `gsea_ranked_list.rnk` | Pre-ranked gene list for pathway enrichment (fgsea\u002Fgseapy) |\n| `dds_fitted.rds` | Serialized DESeqDataSet for checkpoint\u002Fresume |\n\n## Troubleshooting\n\n| Problem | Cause | Solution |\n|---------|-------|----------|\n| `Error: design contains one or more variables with all samples having the same value` | A design factor has only one level | Check `table(coldata$condition)`; ensure all factor levels are present |\n| `Model matrix not full rank` | Confounded design (e.g., batch perfectly correlated with condition) | Run `table(coldata$batch, coldata$condition)`; drop the confounded variable or collect more samples |\n| All or most `padj = NA` | Genes flagged by independent filtering (low mean count) or Cook's outliers | Check `summary(res)` for count of filtered genes; relax `alpha` or verify data quality |\n| No significant genes despite strong biology | Under-powered experiment or high within-group variability | Verify n ≥ 3 per group; check PCA for outliers; inspect p-value histogram (should show spike near 0) |\n| `Error in lfcShrink`: coefficient not found | `coef` name does not match `resultsNames(dds)` | Run `resultsNames(dds)` and copy the exact coefficient string into `coef =` |\n| `apeglm` fails with convergence warning | Extreme LFC estimates (sparse data or complete separation) | Switch to `type = \"ashr\"` which is more robust to these cases |\n| Very large size factors (> 5) | Extremely different library sizes, or normalized counts accidentally used | Check `colSums(counts(dds))`; ensure input is raw integer counts from aligner\u002Fcounter |\n| Dispersion plot shows outlier cloud far from trend | Noisy or failed libraries; incorrect metadata grouping | Examine PCA; remove outlier samples; check that `design` formula matches biological groups |\n\n## References\n\n- [DESeq2 Bioconductor page](https:\u002F\u002Fbioconductor.org\u002Fpackages\u002FDESeq2) — Package home, vignette, and reference manual\n- [DESeq2 vignette](https:\u002F\u002Fbioconductor.org\u002Fpackages\u002Frelease\u002Fbioc\u002Fvignettes\u002FDESeq2\u002Finst\u002Fdoc\u002FDESeq2.html) — Comprehensive worked example covering all design types\n- [Love et al. (2014) Genome Biology](https:\u002F\u002Fdoi.org\u002F10.1186\u002Fs13059-014-0550-8) — Original DESeq2 paper: moderated estimation of fold change and dispersion for RNA-seq data\n- [Zhu et al. (2019) Bioinformatics](https:\u002F\u002Fdoi.org\u002F10.1093\u002Fbioinformatics\u002Fbty895) — apeglm LFC shrinkage for DESeq2\n- [tximeta Bioconductor page](https:\u002F\u002Fbioconductor.org\u002Fpackages\u002Ftximeta) — Salmon import with transcript-level metadata\n- [EnhancedVolcano Bioconductor page](https:\u002F\u002Fbioconductor.org\u002Fpackages\u002FEnhancedVolcano) — Publication-quality volcano plots for DE results\n",{"name":5,"license":15,"description":7},"master","fd76f882a04e445e899d5cfb2732f2105ea054ee","skills\u002Fawesome-skills\u002Fdeseq2-differential-expression",0,"https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS","https:\u002F\u002Fbg-szy.github.io\u002FTOP-SKILLS\u002F","https:\u002F\u002Fgithub.com\u002Fbg-szy","https:\u002F\u002Fwww.ecust.edu.cn\u002F","a student of East China University of Science and Technology","2026-11-12 05:55:03",{"source":8,"skillName":5,"commands":35,"prompt":45,"zipAvailable":46},[36,39,42],{"tool":37,"command":38},"npx","npx skills add bg-szy\u002FTOP-SKILLS --skill deseq2-differential-expression",{"tool":40,"command":41},"bunx","bunx skills add bg-szy\u002FTOP-SKILLS --skill deseq2-differential-expression",{"tool":43,"command":44},"pnpm","pnpm dlx skills add bg-szy\u002FTOP-SKILLS --skill deseq2-differential-expression","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fawesome-skills\u002Fdeseq2-differential-expression\u002FSKILL.md",true,[],[49,57,64,73,81,89],{"skillNo":50,"name":51,"nameZh":6,"description":52,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":53,"licenseFileUrl":6,"sourceHtmlUrl":54,"categories":55,"keywords":56,"publishedAt":19},"01M748C5DS5BQBVBMG22FCGG34","branding","When the user wants to define, audit, or apply brand strategy—purpose, values, positioning, storytelling, voice, narrative (not only visuals). Also use when the user mentions \"brand strategy\", \"brand story\", \"brand storytelling\" or \"brand voice\". For typography, colors, design tokens, and frontend visuals, use brand-visual-generator.","MIT","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fsuperskills\u002Fbranding\u002FSKILL.md",[],[],{"skillNo":58,"name":59,"nameZh":6,"description":60,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":53,"licenseFileUrl":6,"sourceHtmlUrl":61,"categories":62,"keywords":63,"publishedAt":19},"01M748C1GXJ9WRKP18X45DKP6G","brand-visual-generator","When the user wants to define, audit, or apply visual identity (typography, colors, spacing, design tokens, frontend aesthetics). Also use when the user mentions \"brand style guide\", \"visual identity\", \"design system\" or \"typography\". For brand story, positioning, and voice, use branding.","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fsuperskills\u002Fbrand-visual-generator\u002FSKILL.md",[],[],{"skillNo":65,"name":66,"nameZh":6,"description":67,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":68,"licenseFileUrl":69,"sourceHtmlUrl":70,"categories":71,"keywords":72,"publishedAt":19},"01M747T565TFEP3RAVWM0Y899Y","mcp-builder","Guide for creating high-quality MCP (Model Context Protocol) servers that enable LLMs to interact with external services through well-designed tools. Use when building MCP servers to integrate external APIs or services, whether in Python (FastMCP) or Node\u002FTypeScript (MCP SDK).","Apache-2.0","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fskills\u002Fmcp-builder\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fskills\u002Fmcp-builder\u002FSKILL.md",[],[],{"skillNo":74,"name":75,"nameZh":6,"description":76,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":68,"licenseFileUrl":77,"sourceHtmlUrl":78,"categories":79,"keywords":80,"publishedAt":19},"01M747RAJGMBZ2QN4563STB5SH","frontend-design","Guidance for distinctive, intentional visual design when building new UI or reshaping an existing one. Helps with aesthetic direction, typography, and making choices that don't read as templated defaults.","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fskills\u002Ffrontend-design\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fskills\u002Ffrontend-design\u002FSKILL.md",[],[],{"skillNo":82,"name":83,"nameZh":6,"description":84,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":68,"licenseFileUrl":85,"sourceHtmlUrl":86,"categories":87,"keywords":88,"publishedAt":19},"01M747NHAY5BE9GXEB31H255R1","claude-api","Reference for the Claude API \u002F Anthropic SDK — model ids, pricing, params, streaming, tool use, MCP, agents, caching, token counting, model migration.\nTRIGGER — read BEFORE opening the target file; don't skip because it \"looks like a one-liner\" — whenever: the prompt names Claude\u002FAnthropic in any form (Claude, Anthropic, Fable, Opus, Sonnet, Haiku, `anthropic`, `@anthropic-ai`, `claude-*`, `us.anthropic.*`, `[1m]`); the user asks about an LLM (pricing\u002Fmodel choice\u002Flimits\u002Fcaching) — never answer from memory; OR the task is LLM-shaped with provider unstated (agent\u002FMCP\u002Ftool-definition\u002Fmulti-agent\u002FRAG\u002FLLM-judge\u002Fcomputer-use; generate\u002Fsummarize\u002Fextract\u002Fclassify\u002Frewrite\u002Fconverse over NL; debugging refusals\u002Fcutoffs\u002Fstreaming\u002Ftool-calls\u002Ftokens).\nSKIP only when another provider is being worked on (overrides all triggers): OpenAI\u002FGPT\u002FGemini\u002FLlama\u002FMistral\u002FCohere\u002FOllama named in the query; OR `grep -rE 'openai|langchain_openai|google.generativeai|genai|mistralai|cohere|ollama'` over the project hits (run this grep FIRST if no provider named — don't Read the file).","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fskills\u002Fclaude-api\u002FLICENSE.txt","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fskills\u002Fclaude-api\u002FSKILL.md",[],[],{"skillNo":90,"name":91,"nameZh":6,"description":92,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":53,"licenseFileUrl":93,"sourceHtmlUrl":94,"categories":95,"keywords":96,"publishedAt":19},"01M73WM4TPHEJ57D7G2AZ0ABVT","inbox-guardian-for-gmail","Review a personal Gmail inbox with local, owner-approved spam rules. Use for audit-first quarantine, Trash, repeated-evidence sender learning, and header review. Do not use for automatic unsubscribe requests or unreviewed mailbox actions.","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fmarketplace\u002Finbox-guardian-for-gmail\u002FLICENSE","https:\u002F\u002Fgithub.com\u002Fbg-szy\u002FTOP-SKILLS\u002Fblob\u002Fmaster\u002Fskills\u002Fmarketplace\u002Finbox-guardian-for-gmail\u002FSKILL.md",[],[]]