Source: https://github.com/aipoch/medical-research-skills
When to Use
Use this skill when you need to run DESeq2-style differential expression in Python, especially in these scenarios:
- Case vs control bulk RNA-seq from a raw integer count matrix (e.g., treated vs control).
- Multi-factor designs to adjust for batch effects or covariates (e.g.,
~ batch + condition, ~ age + condition).
- DESeq2 migration when converting an R DESeq2 workflow into a Python pipeline.
- Pipeline integration where results must stay in Python objects (pandas/AnnData) for downstream QC, plots, or reporting.
- Requests mentioning “DESeq2”, “differential expression”, “Wald test”, “FDR/padj”, “volcano plot”, “MA plot”, or “PyDESeq2”.
Key Features
- End-to-end DESeq2-like workflow: normalization (size factors), dispersion estimation/shrinkage, LFC fitting, outlier handling.
- Wald tests for differential expression with Benjamini–Hochberg FDR (
padj).
- Design formulas in Wilkinson/R-style notation (single-factor and multi-factor).
- Contrast-based comparisons:
[variable, test_group, reference_group].
- Optional Cook’s distance outlier filtering and refitting.
- Optional LFC shrinkage (apeGLM-style) for visualization/ranking.
- Works naturally with pandas and can interoperate with AnnData.
Dependencies
Minimum environment (as documented in the source material):
- Python 3.10–3.11
pydeseq2 (install via pip/uv)
pandas >= 1.4.3
numpy >= 1.23.0
scipy >= 1.11.0
scikit-learn >= 1.1.1
anndata >= 0.8.0 (optional, for AnnData I/O)
Optional plotting:
matplotlib (recommended)
seaborn (optional)
Installation:
uv pip install pydeseq2
Example Usage
The following script is a complete, runnable example for a standard treated-vs-control analysis.
import pandas as pd
import numpy as np
from pydeseq2.dds import DeseqDataSet
from pydeseq2.ds import DeseqStats
# -----------------------------
# 1) Load inputs
# -----------------------------
# counts.csv is commonly stored as genes x samples; transpose to samples x genes.
counts_df = pd.read_csv("counts.csv", index_col=0).T
metadata = pd.read_csv("metadata.csv", index_col=0)
# Ensure sample alignment
common = counts_df.index.intersection(metadata.index)
counts_df = counts_df.loc[common]
metadata = metadata.loc[common]
# -----------------------------
# 2) Basic filtering
# -----------------------------
# Remove genes with very low total counts
min_total_counts = 10
genes_to_keep = counts_df.columns[counts_df.sum(axis=0) >= min_total_counts]
counts_df = counts_df[genes_to_keep]
# Drop samples with missing condition
metadata = metadata.dropna(subset=["condition"])
counts_df = counts_df.loc[metadata.index]
# -----------------------------
# 3) Fit DESeq2 model
# -----------------------------
dds = DeseqDataSet(
counts=counts_df,
metadata=metadata,
design="~ condition",
refit_cooks=True,
n_cpus=1,
)
dds.deseq2()
# -----------------------------
# 4) Wald test with contrast
# -----------------------------
ds = DeseqStats(
dds,
contrast=["condition", "treated", "control"],
alpha=0.05,
cooks_filter=True,
independent_filter=True,
)
ds.summary()
# -----------------------------
# 5) Results + optional shrinkage
# -----------------------------
res = ds.results_df.copy()
sig = res[res["padj"] < 0.05].sort_values("padj")
print(f"Significant genes (padj < 0.05): {len(sig)}")
# Optional: shrink LFC for visualization/ranking (p-values do not change)
ds.lfc_shrink()
res_shrunk = ds.results_df.copy()
# Export
res.to_csv("deseq2_results.csv")
res_shrunk.to_csv("deseq2_results_shrunk_lfc.csv")
sig.to_csv("significant_genes.csv")
# -----------------------------
# 6) Minimal volcano plot (optional)
# -----------------------------
try:
import matplotlib.pyplot as plt
plot_df = res.copy()
plot_df["neglog10_padj"] = -np.log10(plot_df["padj"].clip(lower=1e-300))
is_sig = plot_df["padj"] < 0.05
plt.figure(figsize=(9, 5))
plt.scatter(
plot_df.loc[~is_sig, "log2FoldChange"],
plot_df.loc[~is_sig, "neglog10_padj"],
s=10,
alpha=0.3,
c="gray",
label="Not significant",
)
plt.scatter(
plot_df.loc[is_sig, "log2FoldChange"],
plot_df.loc[is_sig, "neglog10_padj"],
s=10,
alpha=0.6,
c="red",
label="padj < 0.05",
)
plt.axhline(-np.log10(0.05), linestyle="--", color="blue", alpha=0.5)
plt.xlabel("Log2 Fold Change")
plt.ylabel("-Log10(adjusted p-value)")
plt.title("Volcano Plot")
plt.legend()
plt.tight_layout()
plt.savefig("volcano_plot.png", dpi=300)
except ImportError:
pass
Implementation Details
Inputs and orientation
- Counts matrix must be samples × genes with non-negative integer counts.
- Many files are stored as genes × samples; transpose with
.T after loading.
Design formula (Wilkinson/R-style)
- Use strings like:
~ condition (single factor)
~ batch + condition (batch-adjusted)
~ age + condition (continuous covariate)
~ group + condition + group:condition (interaction)
- Put adjustment variables first (e.g.,
~ batch + condition) so the primary effect is interpreted cleanly.
What dds.deseq2() does (high level)
The fitting pipeline typically includes:
- Size factor estimation (library-size normalization)
- Gene-wise dispersion estimation
- Dispersion trend fitting and prior estimation
- MAP dispersion shrinkage
- Log2 fold change fitting under the specified design
- Cook’s distance outlier detection
- Optional refitting after outlier handling (
refit_cooks=True)
Statistical testing and multiple testing correction
DeseqStats(...).summary() runs Wald tests for the requested coefficient/contrast.
- Output columns commonly include:
baseMean: mean normalized expression
log2FoldChange, lfcSE, stat
pvalue: raw p-value
padj: Benjamini–Hochberg FDR adjusted p-value
- Use
padj < alpha (commonly 0.05) for significance.
Contrast specification
- Format:
contrast=["variable", "test_group", "reference_group"]
- Example:
["condition", "treated", "control"] tests treated relative to control.
LFC shrinkage (optional)
ds.lfc_shrink() applies shrinkage to log2FoldChange for more stable ranking/plots.
- Shrinkage is intended for visualization and prioritization; statistical significance is still based on the (unshrunken) Wald test p-values.
Notes on bundled references/scripts
If your repository includes them, use:
references/api_reference.md for parameter/object details.
references/workflow_guide.md for extended workflows and troubleshooting.
scripts/run_deseq2_analysis.py for a CLI-style batch workflow (counts/metadata/design/contrast/output, optional plots).
1---2name: pydeseq3description: Differential gene expression analysis for bulk RNA-seq count matrices using a DESeq2-like workflow in Python; use when you need Wald tests, FDR correction, and optional LFC shrinkage for condition/batch/covariate designs.4license: MIT5---6> **Source**: [https://github.com/aipoch/medical-research-skills](https://github.com/aipoch/medical-research-skills)
7
8## When to Use
9
10Use this skill when you need to run DESeq2-style differential expression in Python, especially in these scenarios:
11
121. **Case vs control bulk RNA-seq** from a raw integer count matrix (e.g., treated vs control).
132. **Multi-factor designs** to adjust for batch effects or covariates (e.g., `~ batch + condition`, `~ age + condition`).
143. **DESeq2 migration** when converting an R DESeq2 workflow into a Python pipeline.
154. **Pipeline integration** where results must stay in Python objects (pandas/AnnData) for downstream QC, plots, or reporting.
165. **Requests mentioning** “DESeq2”, “differential expression”, “Wald test”, “FDR/padj”, “volcano plot”, “MA plot”, or “PyDESeq2”.
17
18## Key Features
19
20- End-to-end DESeq2-like workflow: normalization (size factors), dispersion estimation/shrinkage, LFC fitting, outlier handling.
21- **Wald tests** for differential expression with **Benjamini–Hochberg FDR** (`padj`).
22- **Design formulas** in Wilkinson/R-style notation (single-factor and multi-factor).
23- **Contrast-based comparisons**: `[variable, test_group, reference_group]`.
24- Optional **Cook’s distance** outlier filtering and refitting.
25- Optional **LFC shrinkage** (apeGLM-style) for visualization/ranking.
26- Works naturally with **pandas** and can interoperate with **AnnData**.
27
28## Dependencies
29
30Minimum environment (as documented in the source material):
31
32- Python **3.10–3.11**
33- `pydeseq2` (install via pip/uv)
34- `pandas` **>= 1.4.3**
35- `numpy` **>= 1.23.0**
36- `scipy` **>= 1.11.0**
37- `scikit-learn` **>= 1.1.1**
38- `anndata` **>= 0.8.0** (optional, for AnnData I/O)
39
40Optional plotting:
41
42- `matplotlib` (recommended)
43- `seaborn` (optional)
44
45Installation:
46
47```bash
48uv pip install pydeseq2
49```
50
51## Example Usage
52
53The following script is a complete, runnable example for a standard treated-vs-control analysis.
54
55```python
56import pandas as pd
57import numpy as np
58
59from pydeseq2.dds import DeseqDataSet
60from pydeseq2.ds import DeseqStats
61
62# -----------------------------
63# 1) Load inputs
64# -----------------------------
65# counts.csv is commonly stored as genes x samples; transpose to samples x genes.
66counts_df = pd.read_csv("counts.csv", index_col=0).T
67metadata = pd.read_csv("metadata.csv", index_col=0)
68
69# Ensure sample alignment
70common = counts_df.index.intersection(metadata.index)
71counts_df = counts_df.loc[common]
72metadata = metadata.loc[common]
73
74# -----------------------------
75# 2) Basic filtering
76# -----------------------------
77# Remove genes with very low total counts
78min_total_counts = 10
79genes_to_keep = counts_df.columns[counts_df.sum(axis=0) >= min_total_counts]
80counts_df = counts_df[genes_to_keep]
81
82# Drop samples with missing condition
83metadata = metadata.dropna(subset=["condition"])
84counts_df = counts_df.loc[metadata.index]
85
86# -----------------------------
87# 3) Fit DESeq2 model
88# -----------------------------
89dds = DeseqDataSet(
90 counts=counts_df,
91 metadata=metadata,
92 design="~ condition",
93 refit_cooks=True,
94 n_cpus=1,
95)
96dds.deseq2()
97
98# -----------------------------
99# 4) Wald test with contrast
100# -----------------------------
101ds = DeseqStats(
102 dds,
103 contrast=["condition", "treated", "control"],
104 alpha=0.05,
105 cooks_filter=True,
106 independent_filter=True,
107)
108ds.summary()
109
110# -----------------------------
111# 5) Results + optional shrinkage
112# -----------------------------
113res = ds.results_df.copy()
114sig = res[res["padj"] < 0.05].sort_values("padj")
115print(f"Significant genes (padj < 0.05): {len(sig)}")
116
117# Optional: shrink LFC for visualization/ranking (p-values do not change)
118ds.lfc_shrink()
119res_shrunk = ds.results_df.copy()
120
121# Export
122res.to_csv("deseq2_results.csv")
123res_shrunk.to_csv("deseq2_results_shrunk_lfc.csv")
124sig.to_csv("significant_genes.csv")
125
126# -----------------------------
127# 6) Minimal volcano plot (optional)
128# -----------------------------
129try:
130 import matplotlib.pyplot as plt
131
132 plot_df = res.copy()
133 plot_df["neglog10_padj"] = -np.log10(plot_df["padj"].clip(lower=1e-300))
134 is_sig = plot_df["padj"] < 0.05
135
136 plt.figure(figsize=(9, 5))
137 plt.scatter(
138 plot_df.loc[~is_sig, "log2FoldChange"],
139 plot_df.loc[~is_sig, "neglog10_padj"],
140 s=10,
141 alpha=0.3,
142 c="gray",
143 label="Not significant",
144 )
145 plt.scatter(
146 plot_df.loc[is_sig, "log2FoldChange"],
147 plot_df.loc[is_sig, "neglog10_padj"],
148 s=10,
149 alpha=0.6,
150 c="red",
151 label="padj < 0.05",
152 )
153 plt.axhline(-np.log10(0.05), linestyle="--", color="blue", alpha=0.5)
154 plt.xlabel("Log2 Fold Change")
155 plt.ylabel("-Log10(adjusted p-value)")
156 plt.title("Volcano Plot")
157 plt.legend()
158 plt.tight_layout()
159 plt.savefig("volcano_plot.png", dpi=300)
160except ImportError:
161 pass
162```
163
164## Implementation Details
165
166### Inputs and orientation
167
168- **Counts matrix** must be **samples × genes** with **non-negative integer** counts.
169- Many files are stored as **genes × samples**; transpose with `.T` after loading.
170
171### Design formula (Wilkinson/R-style)
172
173- Use strings like:
174 - `~ condition` (single factor)
175 - `~ batch + condition` (batch-adjusted)
176 - `~ age + condition` (continuous covariate)
177 - `~ group + condition + group:condition` (interaction)
178- Put **adjustment variables first** (e.g., `~ batch + condition`) so the primary effect is interpreted cleanly.
179
180### What `dds.deseq2()` does (high level)
181
182The fitting pipeline typically includes:
183
1841. **Size factor** estimation (library-size normalization)
1852. **Gene-wise dispersion** estimation
1863. Dispersion **trend** fitting and **prior** estimation
1874. **MAP dispersion** shrinkage
1885. **Log2 fold change** fitting under the specified design
1896. **Cook’s distance** outlier detection
1907. Optional **refitting** after outlier handling (`refit_cooks=True`)
191
192### Statistical testing and multiple testing correction
193
194- `DeseqStats(...).summary()` runs **Wald tests** for the requested coefficient/contrast.
195- Output columns commonly include:
196 - `baseMean`: mean normalized expression
197 - `log2FoldChange`, `lfcSE`, `stat`
198 - `pvalue`: raw p-value
199 - `padj`: **Benjamini–Hochberg FDR** adjusted p-value
200- Use `padj < alpha` (commonly 0.05) for significance.
201
202### Contrast specification
203
204- Format: `contrast=["variable", "test_group", "reference_group"]`
205- Example: `["condition", "treated", "control"]` tests treated relative to control.
206
207### LFC shrinkage (optional)
208
209- `ds.lfc_shrink()` applies shrinkage to **log2FoldChange** for more stable ranking/plots.
210- Shrinkage is intended for **visualization and prioritization**; statistical significance is still based on the (unshrunken) Wald test p-values.
211
212### Notes on bundled references/scripts
213
214If your repository includes them, use:
215- `references/api_reference.md` for parameter/object details.
216- `references/workflow_guide.md` for extended workflows and troubleshooting.
217- `scripts/run_deseq2_analysis.py` for a CLI-style batch workflow (counts/metadata/design/contrast/output, optional plots).