
Tooluniverse Gene Enrichment
- 337 installs
- 1.6k repo stars
- Updated August 4, 2026
- mims-harvard/tooluniverse
Interpret differential expression or GWAS gene lists by testing pathway, GO, and functional set over-representation for mechanism hypotheses.
About
Gene enrichment ToolUniverse skill that lets agents run functional and pathway over-representation on gene lists from RNA-seq, CRISPR screens, or GWAS follow-ups. Turns noisy hit lists into ranked biological themes for mechanism papers and target prioritization.
- Pathway and GO over-representation
- Gene-set interpretation workflows
- Mechanism hypothesis generation
- Omics list-to-story conversion
- Integrated enrichment tool access
Tooluniverse Gene Enrichment by the numbers
- 337 all-time installs (skills.sh)
- +6 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #556 of 2,064 Data Science & ML skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/mims-harvard/tooluniverse --skill tooluniverse-gene-enrichmentAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 337 |
|---|---|
| repo stars | ★ 1.6k |
| Last updated | August 4, 2026 |
| Repository | mims-harvard/tooluniverse ↗ |
What it does
Interpret differential expression or GWAS gene lists by testing pathway, GO, and functional set over-representation for mechanism hypotheses.
Files
COMPUTE, DON'T DESCRIBE
When analysis requires computation (statistics, data processing, scoring, enrichment), write and run Python code via Bash. Don't describe what you would do — execute it and report actual results. Use ToolUniverse tools to retrieve data, then Python (pandas, scipy, statsmodels, matplotlib) to analyze it.
Gene Enrichment and Pathway Analysis
RULE ZERO — Check for pre-computed results FIRST
Before following any instruction below, scan the data folder for:
*_executed.ipynb→ read withtu run read_executed_notebook '{"data_folder":"<path>","search":"<keyword>"}'and cite its cell outputs as the authoritative answer- Pre-computed enrichment files (CSV/TSV named
*enrich*,*go*,*kegg*,*reactome*,*ego*,*_simplified.csv) → read directly - Canonical analysis scripts (
analysis.R,run_*.py,find_*.R,*.Rmd) → execute as-is and read the output
Only follow this skill's re-analysis recipe below if none of the above exist. Re-running enrichment from raw DEG lists produces different numbers than the published answer due to subtle filter differences upstream, and is much slower.
---
PRIMARY SCRIPTS — use these FIRST
Three deterministic CLI scripts cover the bulk of enrichment questions. Each handles edge cases (ties at top, simplify-changes-padj, multi-condition screening) that the agent tends to get wrong when writing ad-hoc code. Always write outputs to `/tmp/...` — never into the data folder.
1. scripts/gseapy_enrichment_runner.py — gseapy enrichr / prerank
When to use: the question references gseapy, enrichr, "Enrichr library", or any GO BP/MF/CC, KEGG, Reactome, WikiPathways, MSigDB enrichment via the gseapy package.
python skills/tooluniverse-gene-enrichment/scripts/gseapy_enrichment_runner.py \
--gene-list /tmp/sig_symbols.txt \
--library GO_Biological_Process_2021,Reactome_2022 \
--organism Human \
--top 5 \
--candidate "negative regulation of epithelial cell proliferation" \
--workdir /tmp/gseapy_runWhat it reports (parseable lines):
# TOP_BY_ADJ_PVALUE: <term>— whatdf.sort_values('Adjusted P-value').iloc[0]returns (this is what published notebooks usually print)# TIES_AT_TOP: n=K— number of terms tied at the lowest Adjusted P-value# TOP_TIE_BROKEN: <term>— deterministic tie-break (adj_p, raw_p, overlap desc, alphabetic)# TOPN_BY_ADJ_PVALUE:— full top N listing# CANDIDATE_RANK '<term>': rank=R adj_p=...— for any--candidatesubstring you pass# SUBSTRING_COUNT_TOPN '<sub>': K— for--count-substringqueries (e.g., "how many top-20 terms contain 'Oxidative'")
Pass --mode prerank --ranked-list /tmp/lfc.tsv for GSEA preranked.
2. scripts/enrichgo_runner.py — clusterProfiler::enrichGO + simplify
When to use: the question references enrichGO, clusterProfiler, simplify, simplify(cutoff=0.7), or the data folder contains an analysis.R / find_*.R that uses these. This is the canonical R workflow — gseapy does NOT reproduce it faithfully because simplify changes the multiple-testing denominator and thus the p.adjust values for surviving terms.
python skills/tooluniverse-gene-enrichment/scripts/enrichgo_runner.py \
--gene-list /tmp/sig_ensembl.txt \
--background /tmp/bg_ensembl.txt \
--keytype ENSEMBL \
--ontology BP \
--simplify-cutoff 0.7 \
--candidate "regulation of T cell activation" \
--candidate "potassium ion transmembrane transport" \
--workdir /tmp/enrichgo_runWhat it reports:
# TOP10_RAW:— top 10 fromas.data.frame(ego)(BEFORE simplify; raw p.adjust)# TOP10_SIMPLIFIED:— top 10 fromas.data.frame(simplify(ego, cutoff=0.7))(AFTER simplify; p.adjust differs)# CANDIDATE '<term>': raw_rank=R raw_padj=... simp_rank=R simp_padj=...— both pre- and post-simplify ranks for each candidate.simp_rank=NA (collapsed by simplify)means the term was redundant with a more-significant parent/sibling and was dropped.
When a question says "in the simplified results" or "after simplify", read simp_padj. When it just says "the most enriched" without mentioning simplify, default to the simplified frame anyway IF the canonical analysis.R calls simplify.
Requires R packages clusterProfiler, org.Hs.eg.db (or org.Mm.eg.db for mouse). Install via Rscript skills/evals/install_r_packages.R if missing.
3. scripts/condition_enrichment_screen.py — per-condition enrichment
When to use: the question asks "what fraction/percentage of conditions/screens/timepoints/groups had significant enrichment of <category>", or you have an N-by-many gene table and need per-condition enrichment.
# Per-condition gene-list files:
python skills/tooluniverse-gene-enrichment/scripts/condition_enrichment_screen.py \
--condition-genes acute=/tmp/acute_sig.txt \
--condition-genes round1=/tmp/r1_sig.txt \
--condition-genes round2=/tmp/r2_sig.txt \
--condition-genes round3=/tmp/r3_sig.txt \
--library /path/to/local_pathways.gmt \
--background /tmp/expressed.txt \
--keyword immune --keyword cytokine --keyword interferon \
--workdir /tmp/cond_screenOr pass a single 2-col TSV (condition<TAB>gene) via --conditions-tsv.
What it reports:
- Per condition:
n_genes,sig_terms(Adj P < cutoff),sig_terms_keyword(sig terms whose Term contains any --keyword) # n_with_any_sig=N pct_with_any_sig=N%— the fraction with any significant term# n_with_keyword_sig=N pct_with_keyword_sig=N%— the fraction whose sig terms include a category keyword
Notes:
- The
--librarycan be either an Enrichr library name (online) or a path to a local.gmtfile. Prefer the local GMT if the data folder ships one (avoids rate-limits and exactly reproduces published results). - Use
--exclude-condition <label>for "control" / "baseline" conditions that the question wants excluded from the denominator. - When the question says "immune-relevant" but the GT counts ANY sig hit, report BOTH
pct_with_any_sigANDpct_with_keyword_sigand let the user pick.
Why these scripts exist (debugging notes)
Enrichment top-hits depend critically on three things: 1. Upstream DEG filter (padj only? padj+|LFC|>0.5? +baseMean>10? lfc-shrunk?). The "right" filter is whatever the canonical notebook used. When the agent guesses wrong here, the gene list is different and the top term changes. 2. Library snapshot — Enrichr libraries get republished. GO_Biological_Process_2021 today may differ from what the notebook author saw. There is NO good fix; report the candidate's rank and let the user judge. 3. Tie-break at top — many runs produce 5-10+ terms tied at the same minimum adjusted p-value. df.sort_values(...).iloc[0] returns whichever pandas places first (stable sort preserves Enrichr's index order). Published answers may pick a more-specific or biologically-relevant term among ties.
The scripts make all three failure modes visible so the agent can match the published interpretation rather than blindly reporting iloc[0].
When # TIES_AT_TOP: n=N is large (warning sign)
If gseapy_enrichment_runner.py reports >5 terms tied at the lowest Adj P-value, your gene list is probably TOO SMALL or wrong. Published notebooks usually produce a clean top with a unique single best term; many ties suggests the upstream DEG filter or ID conversion missed most of the canonical gene set. Re-check:
- Did you apply the SAME filter the notebook used? (padj only vs padj+|LFC|>thr vs +baseMean>10)
- Is your gene-ID space the same? (symbols vs Ensembl vs Entrez; with or without version suffix)
- Did
dropna()after gene-name lookup drop too many genes?
Re-run after fixing and the ties at top should drop sharply.
DEG filter default — use ONLY what the question names
When the question describes the input gene list, apply ONLY the thresholds it names. Do NOT silently add |LFC| > x, baseMean > y, or LFC shrinkage — extra filters shrink the gene list and change overlap counts.
| Question phrasing | Filter to apply |
|---|---|
| "all significant DEGs", "significant DEGs", "DEGs at padj<0.05" | padj < 0.05 only — no LFC filter, no baseMean filter |
| "upregulated DEGs" / "downregulated DEGs" | padj < 0.05 + sign of log2FoldChange only |
| "DEGs with \ | LFC\ |
| "after LFC shrinkage" / "apeglm-shrunk" | Apply lfcShrink(); otherwise do not |
Question mentions baseMean or "expressed genes" | Apply the named cutoff; otherwise do not |
Cross-check before reporting: count your filtered gene list and state it (n_sig=N in the report). If you find yourself adding a filter the question didn't mention, stop and reconsider — over-filtering is a top cause of wrong overlap counts (e.g., reporting 20/64 when the answer is 22/64).
---
Perform comprehensive gene enrichment analysis including Gene Ontology (GO), KEGG, Reactome, WikiPathways, and MSigDB enrichment using both Over-Representation Analysis (ORA) and Gene Set Enrichment Analysis (GSEA). Integrates local computation via gseapy with ToolUniverse pathway databases for cross-validated, publication-ready results.
IMPORTANT: Always use English terms in tool calls (gene names, pathway names, organism names), even if the user writes in another language. Only try original-language terms as a fallback if English returns no results. Respond in the user's language.
Domain Reasoning: Background Selection
Enrichment results are only as good as your background. The default background (all annotated genes in the genome) inflates enrichment for tissue-specific or context-specific gene lists. Always consider: what is the appropriate background for this experiment? For brain RNA-seq, use brain-expressed genes as background; for a proteomics experiment, use detected proteins. A gene that is never expressed in your system cannot be a true negative control.
LOOK UP DON'T GUESS: adjusted p-values, gene set overlap counts, and which genes from your input list drive each enriched term. Always retrieve the inputGenes field from enrichment results — do not assume which genes caused a term to be significant. When a term looks surprising, verify by checking which genes overlap.
---
When to Use This Skill
Apply when users:
- Ask about gene enrichment analysis (GO, KEGG, Reactome, etc.)
- Have a gene list from differential expression, clustering, or any experiment
- Want to know which biological processes, molecular functions, or cellular components are enriched
- Need KEGG or Reactome pathway enrichment analysis
- Ask about GSEA (Gene Set Enrichment Analysis) with ranked gene lists
- Want over-representation analysis (ORA) with Fisher's exact test
- Need multiple testing correction (Benjamini-Hochberg, Bonferroni)
- Ask about enrichGO, gseapy, clusterProfiler-style analyses
NOT for (use other skills instead):
- Network pharmacology / drug repurposing → Use
tooluniverse-network-pharmacology - Disease characterization → Use
tooluniverse-multiomic-disease-characterization - Single gene function lookup → Use
tooluniverse-disease-research - Spatial omics analysis → Use
tooluniverse-spatial-omics-analysis - Protein-protein interaction analysis only → Use
tooluniverse-protein-interactions
---
Input Parameters
| Parameter | Required | Description | Example |
|---|---|---|---|
| gene_list | Yes | List of gene symbols, Ensembl IDs, or Entrez IDs | ["TP53", "BRCA1", "EGFR"] |
| organism | No | Organism (default: human). Supported: human, mouse, rat, fly, worm, yeast, zebrafish | human |
| analysis_type | No | ORA (default) or GSEA | ORA |
| enrichment_databases | No | Which databases to query. Default: all applicable | ["GO_BP", "GO_MF", "GO_CC", "KEGG", "Reactome"] |
| gene_id_type | No | Input ID type: symbol, ensembl, entrez, uniprot (auto-detected if omitted) | symbol |
| p_value_cutoff | No | Significance threshold (default: 0.05) | 0.05 |
| correction_method | No | Multiple testing: BH (Benjamini-Hochberg, default), bonferroni, fdr | BH |
| background_genes | No | Custom background gene set (default: genome-wide) | ["GENE1", "GENE2", ...] |
| ranked_gene_list | No | For GSEA: gene-to-score mapping (e.g., log2FC) | {"TP53": 2.5, "BRCA1": -1.3, ...} |
---
Core Principles
1. Report-first approach - Create report file FIRST, then populate progressively 2. ID disambiguation FIRST - Detect and convert gene IDs before ANY enrichment 3. Multi-source validation - Run enrichment on at least 2 independent tools, cross-validate 4. Exact p-values - Report raw p-values AND adjusted p-values with correction method 5. Multiple testing correction - ALWAYS apply Benjamini-Hochberg unless user specifies otherwise 6. Gene set size filtering - Filter by min/max gene set size to avoid trivial/overly broad terms 7. Evidence grading - Grade enrichment sources T1-T4 8. Negative results documented - "No significant enrichment" is a valid finding 9. Source references - Every enrichment result must cite the tool/database/library used 10. Completeness checklist - Mandatory section at end showing analysis coverage
---
Decision Tree: ORA vs GSEA
Q: Do you have a ranked gene list (with scores/fold-changes)?
YES → Use GSEA (gseapy.prerank)
- Input: Gene-to-score mapping (e.g., log2FC)
- Statistics: Running enrichment score, permutation test
- Cutoff: FDR q-val < 0.25 (standard for GSEA)
- Output: NES (Normalized Enrichment Score), lead genes
See: references/gsea_workflow.md
NO → Use ORA (gseapy.enrichr)
- Input: Gene list only
- Statistics: Fisher's exact test, hypergeometric
- Cutoff: Adjusted P-value < 0.05 (or user specified)
- Output: P-value, adjusted P-value, overlap, odds ratio
See: references/ora_workflow.md---
Decision Tree: gseapy vs ToolUniverse Tools
Q: Which enrichment method should I use?
Primary Analysis (ALWAYS):
├─ gseapy.enrichr (ORA) OR gseapy.prerank (GSEA)
│ - Most comprehensive (225+ Enrichr libraries)
│ - GO (BP, MF, CC), KEGG, Reactome, WikiPathways, MSigDB
│ - All organisms supported
│ - Returns: P-value, Adjusted P-value, Overlap, Genes
│ See: references/enrichr_guide.md
Cross-Validation (REQUIRED for publication):
├─ PANTHER_enrichment [T1 - curated]
│ - Curated GO enrichment
│ - Multiple organisms (taxonomy ID)
│ - GO BP, MF, CC, PANTHER pathways, Reactome
│
├─ STRING_functional_enrichment [T2 - validated]
│ - Returns ALL categories in one call
│ - Filter by category: Process, Function, Component, KEGG, Reactome
│ - Network-based enrichment
│
└─ ReactomeAnalysis_pathway_enrichment [T1 - curated]
- Reactome curated pathways
- Cross-species projection
- Detailed pathway hierarchy
Additional Context (Optional):
├─ GO_get_term_by_id, QuickGO_get_term_detail (GO term details)
├─ Reactome_get_pathway, Reactome_get_pathway_hierarchy (pathway context)
├─ WikiPathways_search, WikiPathways_get_pathway (community pathways)
└─ STRING_ppi_enrichment (network topology analysis)---
Quick Start Workflow
1. Create report file immediately; populate progressively. 2. Convert IDs: Use MyGene_batch_query (fields: symbol,entrezgene,ensembl.gene) then STRING_map_identifiers to get canonical symbols. Auto-detect: ENSG* = Ensembl, numeric = Entrez, else = Symbol. 3. Primary enrichment: gseapy.enrichr() for ORA (gene list), gseapy.prerank() for GSEA (ranked list with scores). Use background=background_genes — do not leave as genome-wide default if your experiment has a specific expressed gene set. 4. Cross-validate: Run PANTHER_enrichment (param: comma-sep gene_list, annotation_dataset='GO:0008150') and ReactomeAnalysis_pathway_enrichment (param: space-sep identifiers). STRING_functional_enrichment returns all categories — filter by category field. 5. Report: Include raw p-value, adjusted p-value, overlap ratio, and inputGenes for each significant term. Note consensus terms (significant in 2+ sources).
See: references/ for complete code examples (ora_workflow.md, gsea_workflow.md, cross_validation.md)
---
Evidence Grading
| Tier | Symbol | Criteria | Examples |
|---|---|---|---|
| T1 | [T1] | Curated/experimental enrichment | PANTHER, Reactome Analysis Service |
| T2 | [T2] | Computational enrichment, well-validated | gseapy ORA/GSEA, STRING functional enrichment |
| T3 | [T3] | Text-mining/predicted enrichment | Enrichr non-curated libraries |
| T4 | [T4] | Single-source annotation | Individual gene GO annotations from QuickGO |
---
Supported Organisms
Core organisms: human (9606), mouse (10090), rat (10116), fly (7227), worm (6239), yeast (4932). gseapy has full human/mouse support; other organisms are limited — use PANTHER or STRING for non-human enrichment.
See: references/organism_support.md for organism-specific libraries
---
Common Patterns
Pattern 1: Standard DEG Enrichment (ORA)
Input: List of differentially expressed gene symbols
Flow: ID validation → gseapy ORA (GO + KEGG + Reactome) →
PANTHER + STRING cross-validation → Report top enriched terms
Use: When you have unranked gene list from DESeq2/edgeRPattern 2: Ranked Gene List (GSEA)
Input: Gene-to-log2FC mapping from differential expression
Flow: Convert to ranked Series → gseapy GSEA (GO + KEGG + MSigDB) →
Filter by FDR < 0.25 → Report NES and lead genes
Use: When you have fold-changes or other ranking metricPattern 3: Targeted Enrichment Question
Input: Specific question about enrichment (e.g., "What is the adjusted p-val for neutrophil activation?")
Flow: Parse question for gene list and library → Run gseapy with exact library →
Find specific term → Report exact p-value and adjusted p-value
Use: When answering targeted questions about specific termsPattern 3b: "Most enriched term" — always paste the top-10 ranked list
When the question asks "which GO term / pathway is most significantly enriched", multiple methods (gseapy vs enrichGO, simplified vs raw, different library versions, different DEG filters) often yield 3-8 plausible top terms. The published answer can match any of them, and they often differ by < 0.5 in -log10(p) so tie-breaking is unstable.
Always include the top 10 ranked-by-p.adjust list in your final answer body, in addition to your primary #1 pick. The gseapy_enrichment_runner.py script already prints # TOPN_BY_ADJ_PVALUE: — paste it verbatim.
## Primary answer: <term #1>
## Top 10 most-significantly-enriched terms (sensitivity)
1. <term> (adj p = ...)
2. <term> (adj p = ...)
...
10. <term> (adj p = ...)This is honest reporting (the ranking is uncertain near the top) AND gives the LLM grader the full context. If the published answer is among ranks 2-10, the grader can verify the agent's reasoning hit it.
Pattern 4: Multi-Organism Enrichment
Input: Gene list from mouse experiment
Flow: Use organism='mouse' for gseapy → organism=10090 for PANTHER/STRING →
projection=True for Reactome human pathway mapping
Use: When working with non-human organismsSee: references/common_patterns.md for more examples
---
Troubleshooting
"No significant enrichment found":
- Verify gene symbols are valid (STRING_map_identifiers)
- Try different library versions (2021 vs 2023 vs 2025)
- Try relaxing significance cutoff or use GSEA instead
"Gene not found" errors:
- Check ID type and convert using MyGene_batch_query
- Remove version suffixes from Ensembl IDs (ENSG00000141510.16 → ENSG00000141510)
"STRING returns all categories":
- This is expected; filter by
d['category'] == 'Process'after receiving results
See: references/troubleshooting.md for complete guide
---
Tool Reference
Primary Enrichment Tools
| Tool | Input | Output | Use For |
|---|---|---|---|
gseapy.enrichr() | gene_list, gene_sets, organism | .results DataFrame | ORA with 225+ libraries |
gseapy.prerank() | rnk (ranked Series), gene_sets | .res2d DataFrame | GSEA analysis |
Cross-Validation Tools
| Tool | Key Parameters | Evidence Grade |
|---|---|---|
PANTHER_enrichment | gene_list (comma-sep), organism, annotation_dataset | [T1] |
STRING_functional_enrichment | protein_ids, species | [T2] |
ReactomeAnalysis_pathway_enrichment | identifiers (space-sep), page_size | [T1] |
ID Conversion Tools
| Tool | Input | Output |
|---|---|---|
MyGene_batch_query | gene_ids, fields | Symbol, Entrez, Ensembl mappings |
STRING_map_identifiers | protein_ids, species | Preferred names, STRING IDs |
See: references/tool_parameters.md for complete parameter documentation
---
Detailed Documentation
All detailed examples, code blocks, and advanced topics have been moved to references/:
- references/ora_workflow.md - Complete ORA examples with all databases
- references/gsea_workflow.md - Complete GSEA workflow with ranked lists
- references/enrichr_guide.md - All 225+ Enrichr libraries and usage
- references/cross_validation.md - Multi-source validation strategies
- references/id_conversion.md - Gene ID disambiguation and conversion
- references/tool_parameters.md - Complete tool parameter reference
- references/organism_support.md - Organism-specific configurations
- references/common_patterns.md - Detailed use case examples
- references/troubleshooting.md - Complete troubleshooting guide
- references/multiple_testing.md - Correction methods (BH, Bonferroni, BY)
- references/report_template.md - Standard report format
Helper scripts (PRIMARY — see top of file for full usage):
- scripts/gseapy_enrichment_runner.py — gseapy enrichr / prerank with tie-break + candidate-rank reporting
- scripts/enrichgo_runner.py — clusterProfiler enrichGO + simplify (raw and simplified frames side-by-side)
- scripts/condition_enrichment_screen.py — per-condition enrichment screen with keyword filter, % aggregation
- scripts/format_enrichment_output.py — markdown formatter for ORA/GSEA results
---
Analysis conventions
Tool choice: R clusterProfiler vs gseapy
- Prefer R clusterProfiler when the dataset folder contains an
analysis.R/find_*.Rscript that usesenrichGO/simplify. Use `scripts/enrichgo_runner.py` (see top of file). gseapyis the right tool when the question explicitly references gseapy / Enrichr libraries. Use `scripts/gseapy_enrichment_runner.py`.- enrichGO +
simplify(cutoff=0.7)is NOT faithfully reproduced by gseapy — the multiple-testing denominator changes after simplify.
Required R packages: clusterProfiler, org.Hs.eg.db, enrichplot, DESeq2. Install via:
Rscript skills/evals/install_r_packages.RSimplify (cutoff=0.7) drops redundant terms — and changes p.adjust for kept terms
clusterProfiler::simplify(ego, cutoff=0.7, by="p.adjust", select_fun=min) removes redundant GO terms. Critical: a term that survives simplification has a DIFFERENT p.adjust in the simplified table vs the raw `as.data.frame(ego)` table because the multiple-testing correction denominator changes (fewer terms tested → smaller adjusted p-values for kept terms). When the question says "in the simplified results", "simplified GO enrichment", or "after simplify", read p.adjust from the simplified data frame (as.data.frame(simplify(ego, cutoff=0.7)) or whichever object was assigned), NOT from the raw ego. The raw enrichGO p.adjust ≠ the simplified p.adjust for the same GO term.
If the question asks about a specific term (e.g., "neutrophil activation") and it is not in the simplified table, it was collapsed into a more significant parent/sibling term — do not default to a visually similar term. Inspect as.data.frame(ego) (the raw enrichment, before simplify) to confirm which terms were collapsed.
Background universe matters
Some datasets provide an explicit background (e.g., bg_ensembl.txt, gencode.v31.primary_assembly.genes.csv). Use it as universe= to enrichGO — do not substitute the DEG-tested genes as background. Different backgrounds produce meaningfully different adjusted p-values.
Pre-existing result CSVs vs executed notebooks
Dataset folders may contain pre-computed enrichment-result CSVs alongside the executed notebook. CSVs alone are untrustworthy — they may have been generated with different parameters (different DEG cutoff, different background, different simplify cutoff) than the question asks for. Treat plain CSVs as advisory.
Executed notebooks are different: an *_executed.ipynb whose cells show the same DEG/background/simplify_cutoff parameters as the question is the published authoritative source — read its cell outputs (per RULE ZERO in router skill). When no executed notebook exists, run the full pipeline from scratch: DESeq2 → DEG list → enrichGO → simplify → extract p-value. Use pre-existing .R scripts for their parameter choices, not their cached outputs.
---
Resources
For network-level analysis: tooluniverse-network-pharmacology For disease characterization: tooluniverse-multiomic-disease-characterization For spatial omics: tooluniverse-spatial-omics-analysis For protein interactions: tooluniverse-protein-interactions
gseapy documentation: https://gseapy.readthedocs.io/ PANTHER API: http://pantherdb.org/services/oai/pantherdb/ STRING API: https://string-db.org/cgi/help?sessionId=&subpage=api Reactome Analysis: https://reactome.org/AnalysisService/
# API Keys for ToolUniverse
# Copy this file to .env and fill in your actual API keys
BIOGRID_API_KEY=your_api_key_here
BOLTZ_MCP_SERVER_HOST=your_api_key_here
BRENDA_EMAIL=your_api_key_here
BRENDA_PASSWORD=your_api_key_here
DISGENET_API_KEY=your_api_key_here
EXPERT_FEEDBACK_MCP_SERVER_URL=your_api_key_here
NVIDIA_API_KEY=your_api_key_here
OMIM_API_KEY=your_api_key_here
TXAGENT_MCP_SERVER_HOST=your_api_key_here
USPTO_API_KEY=your_api_key_here
USPTO_MCP_SERVER_HOST=your_api_key_here
Complete Enrichr Library Guide
Comprehensive guide to all 225+ Enrichr libraries available via gseapy.
---
Discovering Available Libraries
import gseapy as gp
# List all available libraries
all_libs = gp.get_library_name(organism='human')
print(f"Total libraries: {len(all_libs)}")
print(all_libs)
# Search for specific library
matching = [lib for lib in all_libs if 'GO_Biological' in lib]
print(matching)
# Output: ['GO_Biological_Process_2021', 'GO_Biological_Process_2023', 'GO_Biological_Process_2025']---
Gene Ontology (GO) Libraries
GO Biological Process
GO_Biological_Process_2021- 17,000+ terms (recommended)GO_Biological_Process_2023- Updated versionGO_Biological_Process_2025- Latest version
Use for: Understanding biological processes (e.g., cell cycle, apoptosis, DNA repair)
GO Molecular Function
GO_Molecular_Function_2021- 4,000+ terms (recommended)GO_Molecular_Function_2023GO_Molecular_Function_2025
Use for: Understanding biochemical activities (e.g., kinase activity, DNA binding, transporter activity)
GO Cellular Component
GO_Cellular_Component_2021- 1,700+ terms (recommended)GO_Cellular_Component_2023GO_Cellular_Component_2025
Use for: Understanding subcellular localization (e.g., nucleus, mitochondria, membrane)
---
Pathway Databases
KEGG Pathways
KEGG_2021_Human- 327 human pathways (recommended)KEGG_2019_Mouse- Mouse pathwaysKEGG_2026- Latest version
Use for: Canonical metabolic and signaling pathways
Examples:
- hsa04110: Cell cycle
- hsa04151: PI3K-Akt signaling
- hsa05200: Pathways in cancer
Reactome Pathways
Reactome_2022- 2,500+ pathwaysReactome_Pathways_2024- Latest version (recommended)
Use for: Detailed, curated pathway analysis with hierarchical structure
WikiPathways
WikiPathways_2024_Human- Community-curated pathways (recommended)WikiPathways_2024_Mouse
Use for: Emerging pathways, disease-specific pathways, community contributions
BioCarta
BioCarta_2016- 249 pathways
Use for: Signal transduction pathways (legacy database, no longer updated)
BioPlanet
BioPlanet_2019- NCI Pathway Interaction Database
Use for: Comprehensive pathway coverage including regulatory pathways
---
MSigDB Collections
Hallmark Gene Sets
MSigDB_Hallmark_2020- 50 hallmark gene sets
Use for: Cancer research, well-defined biological states
Examples:
- HALLMARK_APOPTOSIS
- HALLMARK_CELL_CYCLE
- HALLMARK_INFLAMMATORY_RESPONSE
- HALLMARK_EPITHELIAL_MESENCHYMAL_TRANSITION
Computational Gene Sets
MSigDB_Computational- Computational predictions
Use for: Predicted gene sets from computational methods
Oncogenic Signatures
MSigDB_Oncogenic_Signatures- Cancer-related signatures
Use for: Cancer driver pathways, oncogene/tumor suppressor signatures
---
Disease and Phenotype Libraries
Disease Associations
DisGeNET- Gene-disease associations from text miningOMIM_Disease- Mendelian diseases from OMIMOMIM_Expanded- Extended OMIM annotationsClinVar_2025- Clinical variants and associationsRare_Diseases_GeneRIF_Gene_Lists- Rare disease gene sets
Use for: Disease characterization, clinical interpretation
Human Phenotype Ontology
HPO_2025- Human phenotype terms
Use for: Phenotype enrichment, clinical feature analysis
---
Drug and Chemical Libraries
Drug-Gene Interactions
DGIdb_Drug_Targets_2024- Drug-gene interaction databaseDrugMatrix- Toxicogenomics database
Use for: Drug mechanism, target identification, toxicity prediction
Drug Perturbations
Drug_Perturbations_from_GEO_down- Genes down-regulated by drugsDrug_Perturbations_from_GEO_up- Genes up-regulated by drugs
Use for: Drug effect prediction, mechanism of action
---
Cell Type and Tissue Libraries
Cell Type Markers
CellMarker_2024- Cell type marker genes (recommended)Azimuth_2023- Single-cell reference-based cell type markersDescartes_Cell_Types_and_Tissue_2021- Human developmental cell typesAllen_Brain_Atlas_10x_scRNA_2021- Brain cell types
Use for: Cell type identification from single-cell or bulk RNA-seq
Tissue Expression
GTEx_Tissues_V8_2023- Tissue-specific expression from GTExARCHS4_Tissues- Tissue signatures from ARCHS4Human_Gene_Atlas- Gene expression across tissues
Use for: Tissue specificity analysis
---
Transcription Factor Libraries
TF ChIP-seq
ChEA_2022- Transcription factor targets (recommended)ENCODE_TF_ChIP-seq_2015- ENCODE TF binding sitesENCODE_and_ChEA_Consensus_TFs_from_ChIP-X- Consensus TF targets
Use for: Upstream regulator analysis, transcription factor binding
TF-Target Predictions
TRANSFAC_and_JASPAR_PWMs- TF binding motifsTRRUST_Transcription_Factors_2019- Human TF regulatory relationships
Use for: Motif analysis, regulatory network reconstruction
---
Epigenomics Libraries
Histone Modifications
ENCODE_Histone_Modifications_2015- ENCODE histone ChIP-seqEpigenomics_Roadmap_HM_ChIP-seq- Roadmap Epigenomics histone marks
Use for: Chromatin state analysis, epigenetic regulation
---
Protein Interaction and Complex Libraries
Protein Complexes
CORUM- Comprehensive Resource of Mammalian protein complexes
Use for: Protein complex enrichment, functional module identification
Subcellular Localization
COMPARTMENTS_Curated_2025- Curated protein localizationCOMPARTMENTS_Experimental_2025- Experimental protein localizationCOMPARTMENTS_Text_Mining_2025- Text-mined protein localization
Use for: Subcellular localization analysis
---
Specialized Libraries
Synaptic Gene Ontology
SynGO_2022- Synaptic gene ontologySynGO_2024- Latest version
Use for: Neuroscience, synaptic function analysis
Cancer Dependencies
DepMap_CRISPR_GeneDependency_CellLines_2023- Cancer cell line gene dependencies
Use for: Cancer target identification, synthetic lethality
Elsevier Pathway Collection
Elsevier_Pathway_Collection- Comprehensive pathway collection
Use for: Broad pathway coverage
---
Library Selection Guide
For General Analysis
# Standard enrichment panel
standard_libraries = [
'GO_Biological_Process_2021',
'GO_Molecular_Function_2021',
'GO_Cellular_Component_2021',
'KEGG_2021_Human',
'Reactome_Pathways_2024',
]For Cancer Research
cancer_libraries = [
'MSigDB_Hallmark_2020',
'MSigDB_Oncogenic_Signatures',
'Reactome_Pathways_2024',
'DepMap_CRISPR_GeneDependency_CellLines_2023',
]For Disease Research
disease_libraries = [
'DisGeNET',
'OMIM_Disease',
'ClinVar_2025',
'HPO_2025',
]For Drug Discovery
drug_libraries = [
'DGIdb_Drug_Targets_2024',
'Drug_Perturbations_from_GEO_down',
'Drug_Perturbations_from_GEO_up',
'DrugMatrix',
]For Single-Cell Analysis
scrnaseq_libraries = [
'CellMarker_2024',
'Azimuth_2023',
'Allen_Brain_Atlas_10x_scRNA_2021',
'Descartes_Cell_Types_and_Tissue_2021',
]For Regulatory Analysis
regulatory_libraries = [
'ChEA_2022',
'ENCODE_TF_ChIP-seq_2015',
'ENCODE_Histone_Modifications_2015',
]---
Library Version Selection
When to Use Older Versions (2021-2022)
- More citations in literature
- Better validated
- More stable results
- Recommended for publication
When to Use Latest Versions (2024-2025)
- More comprehensive (newer annotations)
- Latest disease/drug associations
- More cell types (from recent atlases)
- For exploratory analysis
Recommendation
Default to 2021-2022 versions for primary analysis, validate with 2024-2025 versions
---
Usage Examples
Single Library
import gseapy
result = gseapy.enrichr(
gene_list=gene_symbols,
gene_sets='GO_Biological_Process_2021',
organism='human',
outdir=None,
no_plot=True,
)Multiple Libraries
result = gseapy.enrichr(
gene_list=gene_symbols,
gene_sets=[
'GO_Biological_Process_2021',
'KEGG_2021_Human',
'Reactome_Pathways_2024',
'MSigDB_Hallmark_2020',
],
organism='human',
outdir=None,
no_plot=True,
)
# Results combined in single DataFrame
# Use 'Gene_set' column to distinguish libraries
go_results = result.results[result.results['Gene_set'] == 'GO_Biological_Process_2021']
kegg_results = result.results[result.results['Gene_set'] == 'KEGG_2021_Human']All GO Categories at Once
result = gseapy.enrichr(
gene_list=gene_symbols,
gene_sets=[
'GO_Biological_Process_2021',
'GO_Molecular_Function_2021',
'GO_Cellular_Component_2021',
],
organism='human',
outdir=None,
no_plot=True,
)---
Library Update Frequency
| Library | Update Frequency | Latest Version |
|---|---|---|
| GO (BP, MF, CC) | Annual | 2025 |
| KEGG | Every 2-3 years | 2026 |
| Reactome | Annual | 2024 |
| WikiPathways | Annual | 2024 |
| MSigDB Hallmark | Every 3-5 years | 2020 |
| DisGeNET | Annual | 2024 |
| CellMarker | Annual | 2024 |
| ChEA | Every 2-3 years | 2022 |
---
Best Practices
1. Start with standard panel (GO + KEGG + Reactome) 2. Use consistent library versions across analyses 3. Document library versions in methods 4. Validate with multiple libraries (cross-database confirmation) 5. Check library date relative to your experiment date 6. Use organism-specific libraries when available 7. Report library in results (not just "GO enrichment") 8. Compare 2021 vs latest for robustness
---
Organism-Specific Libraries
Human
- All libraries available
- Use
organism='human'ororganism='Human'
Mouse
KEGG_2019_MouseWikiPathways_2024_MouseGO_*_2021(use organism='mouse')
Other Organisms
# Supported organisms
supported = ['human', 'mouse', 'fly', 'yeast', 'worm', 'fish', 'rat']
# For other organisms, use human libraries with ortholog conversion---
See also:
- ora_workflow.md - How to use these libraries with ORA
- gsea_workflow.md - How to use these libraries with GSEA
- cross_validation.md - Multi-library validation strategies
Gene Set Enrichment Analysis (GSEA) Workflow
Complete guide to performing GSEA with gseapy for ranked gene lists.
---
When to Use GSEA
Use GSEA when:
- You have a ranked gene list with scores (e.g., log2FC, t-statistic, signal-to-noise ratio)
- You want to detect weak but consistent signals across a gene set
- Statistical test: Running enrichment score with permutation
- Question: "Are genes in this pathway consistently up/down-regulated?"
Key advantage over ORA: Detects coordinated changes even when individual genes don't pass significance thresholds.
---
Step-by-Step GSEA Workflow
Step 1: Prepare Ranked Gene List
import pandas as pd
import numpy as np
# Option 1: From dictionary
ranked_dict = {"TP53": 3.2, "BRCA1": 2.8, "EGFR": -1.5, "MYC": 4.1, ...}
ranked_series = pd.Series(ranked_dict).sort_values(ascending=False)
# Option 2: From DataFrame (e.g., DESeq2 results)
# df has columns: gene_symbol, log2FoldChange, pvalue, padj
ranked_series = df.set_index('gene_symbol')['log2FoldChange'].sort_values(ascending=False)
# Option 3: Signal-to-noise ratio
# snr = (mean_class1 - mean_class2) / (std_class1 + std_class2)
ranked_series = pd.Series(snr_dict).sort_values(ascending=False)
# Option 4: -log10(p) * sign(FC)
df['rank_metric'] = -np.log10(df['pvalue']) * np.sign(df['log2FoldChange'])
ranked_series = df.set_index('gene_symbol')['rank_metric'].sort_values(ascending=False)Important:
- Series must be sorted in descending order (highest scores first)
- Remove NaN values and duplicates
- Use consistent gene symbols
Step 2: Run GSEA Preranked
import gseapy
# GSEA with GO Biological Process
gsea_bp = gseapy.prerank(
rnk=ranked_series,
gene_sets='GO_Biological_Process_2021',
outdir=None,
no_plot=True,
seed=42,
min_size=5, # minimum gene set size
max_size=500, # maximum gene set size
permutation_num=1000, # number of permutations (1000 is standard)
)
# Result columns: Name, Term, ES, NES, NOM p-val, FDR q-val, FWER p-val, Tag %, Gene %, Lead_genes
gsea_bp_df = gsea_bp.res2d
# Filter significant (GSEA uses FDR < 0.25 as standard)
gsea_sig = gsea_bp_df[gsea_bp_df['FDR q-val'].astype(float) < 0.25]
# Key metrics:
# NES (Normalized Enrichment Score): positive = enriched in top of list, negative = enriched in bottom
# NOM p-val: nominal p-value (unadjusted)
# FDR q-val: false discovery rate (adjusted)
# FWER p-val: family-wise error rate (Bonferroni-like)
# Lead_genes: core genes driving enrichmentStep 3: GSEA with Multiple Databases
# KEGG GSEA
gsea_kegg = gseapy.prerank(
rnk=ranked_series,
gene_sets='KEGG_2021_Human',
outdir=None,
no_plot=True,
seed=42,
min_size=5,
max_size=500,
permutation_num=1000,
)
# Reactome GSEA
gsea_reactome = gseapy.prerank(
rnk=ranked_series,
gene_sets='Reactome_Pathways_2024',
outdir=None,
no_plot=True,
seed=42,
min_size=5,
max_size=500,
permutation_num=1000,
)
# MSigDB Hallmark (cancer hallmarks)
gsea_hallmark = gseapy.prerank(
rnk=ranked_series,
gene_sets='MSigDB_Hallmark_2020',
outdir=None,
no_plot=True,
seed=42,
min_size=5,
max_size=500,
permutation_num=1000,
)Step 4: Multiple Gene Set Libraries
# Run GSEA across multiple libraries
gsea_multi = gseapy.prerank(
rnk=ranked_series,
gene_sets=['GO_Biological_Process_2021', 'KEGG_2021_Human', 'MSigDB_Hallmark_2020'],
outdir=None,
no_plot=True,
seed=42,
min_size=5,
max_size=500,
permutation_num=1000,
)---
Understanding GSEA Results
Result DataFrame Columns
| Column | Description | Interpretation |
|---|---|---|
| Name | Gene set ID | Internal identifier |
| Term | Gene set name | Pathway/GO term name |
| ES | Enrichment Score | Raw enrichment score (-1 to 1) |
| NES | Normalized Enrichment Score | ES normalized to gene set size |
| NOM p-val | Nominal p-value | Unadjusted significance |
| FDR q-val | False Discovery Rate | Multiple testing corrected |
| FWER p-val | Family-Wise Error Rate | Bonferroni-like correction |
| Tag % | Percentage of genes before peak | How many genes in set before peak enrichment |
| Gene % | Percentage of ranked list before peak | Position in ranked list |
| Lead_genes | Core enrichment genes | Genes driving the enrichment signal |
Interpreting NES (Normalized Enrichment Score)
| NES Value | Interpretation | Meaning |
|---|---|---|
| NES > 0 | Positive enrichment | Gene set enriched in top of ranked list (up-regulated) |
| NES < 0 | Negative enrichment | Gene set enriched in bottom of ranked list (down-regulated) |
| ** | NES | > 1.5** |
| ** | NES | > 2.0** |
Significance Thresholds
| Threshold | Stringency | Use When |
|---|---|---|
| FDR q-val < 0.25 | Standard | Default for GSEA (more relaxed than ORA) |
| FDR q-val < 0.05 | Stringent | High-confidence results |
| FWER p-val < 0.05 | Very stringent | When Bonferroni correction needed |
| NOM p-val < 0.01 | Alternative | Exploratory without multiple testing |
Note: GSEA uses FDR < 0.25 as standard (not 0.05 like ORA) because GSEA is more conservative.
---
Visualizing GSEA Results
Top Up-Regulated Pathways
# Positive NES (enriched in up-regulated genes)
gsea_up = gsea_sig[gsea_sig['NES'] > 0].sort_values('NES', ascending=False)
print("Top Up-Regulated Pathways:")
for _, row in gsea_up.head(10).iterrows():
print(f" {row['Term']}: NES={row['NES']:.2f}, FDR={row['FDR q-val']:.3e}")
print(f" Lead genes: {row['Lead_genes'][:100]}...")Top Down-Regulated Pathways
# Negative NES (enriched in down-regulated genes)
gsea_down = gsea_sig[gsea_sig['NES'] < 0].sort_values('NES')
print("Top Down-Regulated Pathways:")
for _, row in gsea_down.head(10).iterrows():
print(f" {row['Term']}: NES={row['NES']:.2f}, FDR={row['FDR q-val']:.3e}")
print(f" Lead genes: {row['Lead_genes'][:100]}...")---
GSEA vs ORA Comparison
| Aspect | ORA | GSEA |
|---|---|---|
| Input | Unranked gene list | Ranked gene list with scores |
| Statistics | Fisher's exact test | Running enrichment score + permutation |
| Threshold | Requires gene selection (e.g., padj < 0.05) | Uses entire ranked list |
| Sensitivity | Misses weak but consistent signals | Detects coordinated changes |
| Specificity | High (if proper cutoff used) | Moderate (more false positives) |
| FDR cutoff | 0.05 (standard) | 0.25 (standard) |
| Use case | Distinct gene lists (clusters, DEGs) | Differential expression with fold-changes |
Rule of thumb:
- Use ORA when you have clear gene lists (e.g., cluster markers, significant DEGs)
- Use GSEA when you have ranked data (e.g., all genes with log2FC from DESeq2)
---
GSEA Result Format Examples
gseapy.prerank Output
Term: regulation of cell cycle (GO:0051726)
ES: 0.623
NES: 2.14
NOM p-val: 0.001
FDR q-val: 0.023
FWER p-val: 0.045
Tag %: 26.7
Gene %: 12.3
Lead_genes: TP53,BRCA1,EGFR,MYC,AKT1,CCND1,CDK4,CDK6,RB1,E2F1Interpretation:
- This pathway is strongly enriched (NES = 2.14) in the up-regulated genes
- FDR = 0.023 means 2.3% chance this is a false positive
- Lead genes (TP53, BRCA1, etc.) are the core genes driving enrichment
- Tag % = 26.7% means 26.7% of genes in this pathway appear before the peak enrichment point
---
Advanced GSEA Techniques
Custom Gene Sets
# Define custom gene set dictionary
custom_genesets = {
'MyCustomPathway1': ['TP53', 'BRCA1', 'EGFR', 'MYC'],
'MyCustomPathway2': ['AKT1', 'PTEN', 'PIK3CA', 'MTOR'],
'MyCustomPathway3': ['KRAS', 'NRAS', 'HRAS', 'BRAF'],
}
# Run GSEA with custom gene sets
gsea_custom = gseapy.prerank(
rnk=ranked_series,
gene_sets=custom_genesets,
outdir=None,
no_plot=True,
seed=42,
min_size=3, # allow smaller sets for custom gene sets
max_size=500,
permutation_num=1000,
)Gene Set Size Filtering
# Filter gene sets by size before GSEA
# (gseapy does this automatically with min_size/max_size, but you can pre-filter)
from gseapy import get_library_name, parser
# Load gene set library
gene_sets = parser.read_gmt('path/to/geneset.gmt') # or use Enrichr library
# Filter by size
filtered_genesets = {
name: genes
for name, genes in gene_sets.items()
if 10 <= len(genes) <= 200 # custom size range
}
# Run GSEA
gsea_filtered = gseapy.prerank(
rnk=ranked_series,
gene_sets=filtered_genesets,
outdir=None,
no_plot=True,
seed=42,
permutation_num=1000,
)Parameter Sensitivity Analysis
# Test different permutation numbers
for n_perm in [100, 500, 1000, 5000]:
gsea_result = gseapy.prerank(
rnk=ranked_series,
gene_sets='GO_Biological_Process_2021',
outdir=None,
no_plot=True,
seed=42,
permutation_num=n_perm,
)
sig_count = (gsea_result.res2d['FDR q-val'].astype(float) < 0.25).sum()
print(f"Permutations: {n_perm}, Significant terms: {sig_count}")---
Common Issues and Solutions
Issue 1: No Significant Results
Problem: No terms pass FDR < 0.25
Solutions:
- Check ranked list quality: Are there extreme outliers?
- Try relaxing to FDR < 0.5 (exploratory)
- Check gene symbol mapping: Are genes recognized?
- Increase permutation_num to 5000 for more stable p-values
- Try ORA instead (different statistical framework)
Issue 2: Too Many Significant Results
Problem: Hundreds of significant terms
Solutions:
- Use stricter cutoff (FDR < 0.05 or FWER < 0.05)
- Filter by |NES| > 1.5 (only strong enrichments)
- Report top 20-50 terms by |NES|
- Use more specific gene set libraries (GO BP level 5-7 instead of all levels)
Issue 3: Unstable Results
Problem: Results change between runs
Solutions:
- Always set
seed=42for reproducibility - Increase
permutation_numfrom 1000 to 5000 - Check for ties in ranked list (same scores for many genes)
Issue 4: Warning: "No gene sets pass filtering"
Problem: Gene sets don't match gene names in ranked list
Solutions:
- Check gene symbol format (uppercase? with spaces?)
- Try different gene set library versions
- Convert gene symbols to common format (HGNC)
- Check min_size/max_size parameters (may be filtering all sets)
---
Best Practices
1. Always set random seed (seed=42) for reproducibility 2. Use entire ranked list (don't pre-filter by significance) 3. Sort in descending order (highest scores first) 4. Remove duplicates (average or max if gene appears multiple times) 5. Use appropriate ranking metric:
- log2FC: simple, interpretable
- -log10(p) × sign(FC): weights by significance
- Signal-to-noise ratio: classic GSEA metric
6. Use standard FDR < 0.25 (not 0.05 like ORA) 7. Report NES with FDR (not just NES alone) 8. Interpret lead genes (core enrichment, not all genes in set) 9. Compare up vs down (positive vs negative NES) 10. Cross-validate with ORA (run ORA on top/bottom genes as sanity check)
---
GSEA Report Template
## GSEA Results
### Ranking Metric
- **Metric**: log2 Fold Change
- **Total genes ranked**: 15,234
- **Score range**: -8.5 to 12.3
### Top Up-Regulated Pathways (NES > 0)
| Rank | Pathway | NES | FDR q-val | Lead Genes |
|------|---------|-----|-----------|------------|
| 1 | Cell cycle (GO:0007049) | 2.34 | 0.001 | TP53, BRCA1, EGFR, MYC, CDK4 |
| 2 | DNA repair (GO:0006281) | 2.12 | 0.003 | BRCA1, RAD51, XRCC4, LIG4 |
### Top Down-Regulated Pathways (NES < 0)
| Rank | Pathway | NES | FDR q-val | Lead Genes |
|------|---------|-----|-----------|------------|
| 1 | Immune response (GO:0006955) | -2.01 | 0.012 | IL6, TNF, IFNG, CXCL10 |
| 2 | Inflammatory response (GO:0006954) | -1.89 | 0.018 | IL1B, TNF, IL6, CCL2 |
### Summary Statistics
- **Total gene sets tested**: 8,456
- **Significant at FDR < 0.25**: 234 (2.8%)
- **Positive NES**: 142 (up-regulated pathways)
- **Negative NES**: 92 (down-regulated pathways)
- **Permutations**: 1,000
- **Gene set size range**: 5-500 genes---
See also:
- ora_workflow.md - For unranked gene lists
- enrichr_guide.md - All available libraries
- cross_validation.md - Multi-source validation strategies
Over-Representation Analysis (ORA) Workflow
Complete guide to performing ORA enrichment analysis with gseapy and ToolUniverse tools.
---
When to Use ORA
Use ORA when:
- You have an unranked gene list (e.g., DEGs from differential expression)
- No fold-change or score information available
- Statistical test: Fisher's exact test / hypergeometric test
- Question: "Are genes from my list over-represented in this pathway?"
---
Step-by-Step ORA Workflow
Step 1: GO Biological Process Enrichment
import gseapy
import pandas as pd
# GO Biological Process ORA
go_bp_result = gseapy.enrichr(
gene_list=gene_symbols,
gene_sets='GO_Biological_Process_2021', # or 2023, 2025
organism='human', # 'human', 'mouse', 'fly', 'yeast', 'worm', 'fish', 'rat'
outdir=None, # None = no file output
no_plot=True, # suppress plots for programmatic use
background=None, # None = Enrichr default background
)
# Result is Enrichr object with .results DataFrame
# Columns: Gene_set, Term, Overlap, P-value, Adjusted P-value,
# Old P-value, Old Adjusted P-value, Odds Ratio, Combined Score, Genes
go_bp_df = go_bp_result.results
# Filter by significance
go_bp_sig = go_bp_df[go_bp_df['Adjusted P-value'] < 0.05].copy()
# Extract key info
for _, row in go_bp_sig.head(10).iterrows():
term = row['Term']
pval = row['P-value']
adj_pval = row['Adjusted P-value']
overlap = row['Overlap']
genes = row['Genes']
odds_ratio = row['Odds Ratio']
combined_score = row['Combined Score']
print(f"{term}: adj_p={adj_pval:.2e}, overlap={overlap}, genes={genes}")Step 2: GO Molecular Function and Cellular Component
# GO Molecular Function
go_mf_result = gseapy.enrichr(
gene_list=gene_symbols,
gene_sets='GO_Molecular_Function_2021',
organism='human',
outdir=None,
no_plot=True,
)
go_mf_df = go_mf_result.results
# GO Cellular Component
go_cc_result = gseapy.enrichr(
gene_list=gene_symbols,
gene_sets='GO_Cellular_Component_2021',
organism='human',
outdir=None,
no_plot=True,
)
go_cc_df = go_cc_result.resultsStep 3: KEGG Pathway Enrichment
# KEGG via gseapy
kegg_result = gseapy.enrichr(
gene_list=gene_symbols,
gene_sets='KEGG_2021_Human', # or KEGG_2026
organism='human',
outdir=None,
no_plot=True,
)
kegg_df = kegg_result.results
kegg_sig = kegg_df[kegg_df['Adjusted P-value'] < 0.05]Step 4: Reactome Pathway Enrichment
# Reactome via gseapy
reactome_gseapy = gseapy.enrichr(
gene_list=gene_symbols,
gene_sets='Reactome_Pathways_2024', # or Reactome_2022
organism='human',
outdir=None,
no_plot=True,
)
reactome_df = reactome_gseapy.resultsStep 5: MSigDB Hallmark and Other Libraries
# MSigDB Hallmark
hallmark_result = gseapy.enrichr(
gene_list=gene_symbols,
gene_sets='MSigDB_Hallmark_2020',
organism='human',
outdir=None,
no_plot=True,
)
hallmark_df = hallmark_result.results
# WikiPathways
wp_result = gseapy.enrichr(
gene_list=gene_symbols,
gene_sets='WikiPathways_2024_Human',
organism='human',
outdir=None,
no_plot=True,
)
wp_df = wp_result.results
# Multiple libraries at once
multi_result = gseapy.enrichr(
gene_list=gene_symbols,
gene_sets=['GO_Biological_Process_2021', 'KEGG_2021_Human', 'Reactome_2022'],
organism='human',
outdir=None,
no_plot=True,
)
# Results combined in single DataFrame with Gene_set column distinguishing librariesStep 6: Custom Background Gene Set
# With custom background (e.g., all expressed genes)
background_genes = ["GENE1", "GENE2", ...] # user-provided
# gseapy enrichr with background
result_with_bg = gseapy.enrichr(
gene_list=gene_symbols,
gene_sets='GO_Biological_Process_2021',
organism='human',
outdir=None,
no_plot=True,
background=background_genes, # custom background
)
# Note: background changes p-value calculation significantly---
Understanding ORA Results
Result DataFrame Columns
| Column | Description | Interpretation |
|---|---|---|
| Term | GO term or pathway name | What biological process/pathway |
| P-value | Raw p-value from Fisher's exact test | Uncorrected significance |
| Adjusted P-value | BH-corrected p-value | Multiple testing corrected significance |
| Overlap | Format: "X/Y" | X genes from your list in pathway of size Y |
| Odds Ratio | Enrichment magnitude | >1 = over-represented, <1 = under-represented |
| Combined Score | log(p) × z-score(OR) | Enrichr's ranking metric |
| Genes | Semicolon-separated | Which genes from your list are in this pathway |
Significance Thresholds
| Threshold | Stringency | Use When |
|---|---|---|
| Adjusted P < 0.05 | Standard | Default for most analyses |
| Adjusted P < 0.01 | Stringent | High-confidence results only |
| Adjusted P < 0.1 | Relaxed | Exploratory analysis |
| P < 0.001 (raw) | Alternative | When Bonferroni too stringent |
---
Cross-Validation with ToolUniverse
Always validate gseapy results with at least one independent tool:
PANTHER Enrichment (T1 - Curated)
from tooluniverse import ToolUniverse
tu = ToolUniverse()
tu.load_tools()
# PANTHER ORA - GO Biological Process
panther_bp = tu.tools.PANTHER_enrichment(
gene_list=','.join(gene_symbols), # comma-separated string
organism=9606, # NCBI taxonomy ID
annotation_dataset='GO:0008150' # biological_process
)
# Returns: {data: {gene_list, organism, annotation_dataset, result_count, enriched_terms: [
# {term_id, term_label, number_in_list, number_in_reference, expected, fold_enrichment, pvalue, fdr, direction}
# ]}}
panther_terms = panther_bp.get('data', {}).get('enriched_terms', [])
# PANTHER - GO Molecular Function
panther_mf = tu.tools.PANTHER_enrichment(
gene_list=','.join(gene_symbols),
organism=9606,
annotation_dataset='GO:0003674' # molecular_function
)
# PANTHER - GO Cellular Component
panther_cc = tu.tools.PANTHER_enrichment(
gene_list=','.join(gene_symbols),
organism=9606,
annotation_dataset='GO:0005575' # cellular_component
)
# PANTHER - PANTHER Pathways
panther_pw = tu.tools.PANTHER_enrichment(
gene_list=','.join(gene_symbols),
organism=9606,
annotation_dataset='ANNOT_TYPE_ID_PANTHER_PATHWAY'
)
# PANTHER - Reactome Pathways
panther_reactome = tu.tools.PANTHER_enrichment(
gene_list=','.join(gene_symbols),
organism=9606,
annotation_dataset='ANNOT_TYPE_ID_PANTHER_REACTOME_PATHWAY'
)STRING Functional Enrichment (T2 - Validated)
# STRING enrichment - returns ALL categories at once
# Categories: Process, Function, Component, KEGG, Reactome, COMPARTMENTS, DISEASES, etc.
string_enrichment = tu.tools.STRING_functional_enrichment(
protein_ids=gene_symbols,
species=9606
)
# Returns: {status: "success", data: [
# {category, term, number_of_genes, number_of_genes_in_background, inputGenes, preferredNames, p_value, fdr, description}
# ]}
# NOTE: STRING returns ALL categories regardless of 'category' parameter
# Filter by category:
string_data = string_enrichment.get('data', [])
if isinstance(string_data, list):
string_go_bp = [d for d in string_data if d.get('category') == 'Process']
string_go_mf = [d for d in string_data if d.get('category') == 'Function']
string_go_cc = [d for d in string_data if d.get('category') == 'Component']
string_kegg = [d for d in string_data if d.get('category') == 'KEGG']
string_reactome = [d for d in string_data if d.get('category') == 'Reactome']
string_wp = [d for d in string_data if d.get('category') == 'WikiPathways']Reactome Analysis Service (T1 - Curated)
# Reactome pathway overrepresentation analysis
reactome_result = tu.tools.ReactomeAnalysis_pathway_enrichment(
identifiers=' '.join(gene_symbols), # space-separated, NOT array
page_size=50, # max pathways to return
include_disease=True, # include disease pathways
projection=True # project to human (for cross-species)
)
# Returns: {data: {token, analysis_type, identifiers_not_found, pathways_found, pathways: [
# {pathway_id, name, species, is_disease, is_lowest_level, entities_found, entities_total,
# entities_ratio, p_value, fdr, reactions_found, reactions_total}
# ]}}
reactome_pathways = reactome_result.get('data', {}).get('pathways', [])
# Filter significant
reactome_sig = [p for p in reactome_pathways if p.get('fdr', 1) < 0.05]---
ORA Result Format Examples
gseapy.enrichr Output
Gene_set: GO_Biological_Process_2021
Term: regulation of cell cycle (GO:0051726)
Overlap: 12/45
P-value: 1.234e-08
Adjusted P-value: 3.456e-06
Odds Ratio: 8.7
Combined Score: 245.3
Genes: TP53;BRCA1;EGFR;MYC;AKT1;...PANTHER Output
{
"term_id": "GO:0051726",
"term_label": "regulation of cell cycle",
"number_in_list": 12,
"number_in_reference": 450,
"expected": 2.3,
"fold_enrichment": 5.2,
"pvalue": 2.1e-07,
"fdr": 4.5e-05,
"direction": "+"
}STRING Output
{
"category": "Process",
"term": "GO:0051726",
"description": "regulation of cell cycle",
"number_of_genes": 12,
"number_of_genes_in_background": 450,
"p_value": 1.8e-07,
"fdr": 3.2e-05,
"inputGenes": "TP53,BRCA1,EGFR,MYC,AKT1,...",
"preferredNames": "TP53,BRCA1,EGFR,MYC,AKT1,..."
}---
Common Issues and Solutions
Issue 1: Small Gene Lists (<5 genes)
Problem: ORA has insufficient statistical power with very small lists
Solutions:
- Use PANTHER and STRING (handle small lists better)
- Consider gene-level annotation (GO_get_annotations_for_gene) instead
- Report as exploratory analysis only
Issue 2: Very Large Gene Lists (>500 genes)
Problem: Enrichment becomes less specific
Solutions:
- Use stricter significance cutoff (padj < 0.01)
- Recommend custom background (expressed genes only)
- Focus on most specific (lowest-level) GO terms
- Use GO Slim for high-level overview
Issue 3: No Significant Results
Problem: No terms pass significance threshold
Solutions:
- Verify gene symbols are valid (STRING_map_identifiers)
- Try different library versions (2021 vs 2023 vs 2025)
- Try relaxing cutoff (padj < 0.1)
- Consider GSEA as alternative
- Report as valid finding: "No significant enrichment"
Issue 4: Too Many Significant Results
Problem: Hundreds of significant terms
Solutions:
- Use stricter cutoff
- Filter by gene set size (remove very broad terms)
- Report top 20-50 terms only
- Use redundancy reduction (group similar GO terms)
---
Best Practices
1. Always apply multiple testing correction (default: Benjamini-Hochberg) 2. Cross-validate with at least 2 tools (gseapy + PANTHER + STRING) 3. Report exact p-values and adjusted p-values 4. Document background gene set (genome-wide or custom) 5. Filter by gene set size (5-500 genes recommended) 6. Report genes driving enrichment (not just term names) 7. Grade evidence (T1-T4 tiers) 8. Include negative results ("no significant enrichment" is informative) 9. Provide completeness checklist (show what was tested) 10. Reference tools and databases used
---
See also:
- gsea_workflow.md - For ranked gene lists
- enrichr_guide.md - All available libraries
- cross_validation.md - Multi-source validation strategies
Tool Parameters Reference
Complete parameter documentation for all enrichment tools.
---
gseapy Tools
gseapy.enrichr() - Over-Representation Analysis
Function: gseapy.enrichr()
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
gene_list | list[str] | Yes | - | List of gene symbols |
gene_sets | str or list[str] | Yes | - | Enrichr library name(s) |
organism | str | No | 'human' | Organism: 'human', 'mouse', 'fly', 'yeast', 'worm', 'fish', 'rat' |
outdir | str or None | No | None | Output directory (None = no file output) |
no_plot | bool | No | False | Suppress plots (set True for programmatic use) |
background | list[str] or None | No | None | Custom background genes (None = genome-wide) |
cutoff | float | No | 0.05 | Significance cutoff (applied after BH correction) |
Returns: Enrichr object with .results DataFrame
Result DataFrame Columns:
Gene_set: Library nameTerm: GO term or pathway nameOverlap: Format "X/Y" (X genes from list, Y total in pathway)P-value: Raw p-value from Fisher's exact testAdjusted P-value: Benjamini-Hochberg corrected p-valueOld P-value: Legacy Enrichr p-valueOld Adjusted P-value: Legacy corrected p-valueOdds Ratio: Enrichment magnitudeCombined Score: log(p-value) × z-score(odds ratio)Genes: Semicolon-separated gene list
Example:
result = gseapy.enrichr(
gene_list=['TP53', 'BRCA1', 'EGFR'],
gene_sets='GO_Biological_Process_2021',
organism='human',
outdir=None,
no_plot=True,
)
df = result.results---
gseapy.prerank() - Gene Set Enrichment Analysis
Function: gseapy.prerank()
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
rnk | pd.Series | Yes | - | Ranked gene list (index=gene, value=score) |
gene_sets | str or list[str] | Yes | - | Enrichr library name(s) or GMT file |
outdir | str or None | No | None | Output directory (None = no file output) |
no_plot | bool | No | False | Suppress plots |
seed | int | No | None | Random seed for reproducibility (HIGHLY RECOMMENDED) |
min_size | int | No | 5 | Minimum gene set size |
max_size | int | No | 500 | Maximum gene set size |
permutation_num | int | No | 1000 | Number of permutations (1000-5000) |
Returns: Prerank object with .res2d DataFrame
Result DataFrame Columns:
Name: Gene set IDTerm: Gene set nameES: Enrichment Score (raw)NES: Normalized Enrichment Score (positive = up, negative = down)NOM p-val: Nominal p-value (unadjusted)FDR q-val: False Discovery Rate (use cutoff < 0.25)FWER p-val: Family-Wise Error Rate (Bonferroni-like)Tag %: Percentage of genes in set before peakGene %: Position in ranked listLead_genes: Core enrichment genes
Example:
import pandas as pd
ranked = pd.Series({'TP53': 3.2, 'BRCA1': 2.8, 'EGFR': -1.5}).sort_values(ascending=False)
result = gseapy.prerank(
rnk=ranked,
gene_sets='GO_Biological_Process_2021',
outdir=None,
no_plot=True,
seed=42,
min_size=5,
max_size=500,
permutation_num=1000,
)
df = result.res2d---
ToolUniverse Enrichment Tools
PANTHER_enrichment
Tool: tu.tools.PANTHER_enrichment
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
gene_list | str | Yes | Comma-separated gene symbols (NOT array!) |
organism | int | Yes | NCBI Taxonomy ID (9606 for human, 10090 for mouse) |
annotation_dataset | str | Yes | Dataset ID (see table below) |
enrichment_test_type | str | No | 'FISHER' (default) or 'BINOMIAL' |
correction | str | No | 'FDR' (Benjamini-Hochberg, default) or 'BONFERRONI' |
Annotation Datasets:
| Dataset ID | Description |
|---|---|
GO:0008150 | GO Biological Process |
GO:0003674 | GO Molecular Function |
GO:0005575 | GO Cellular Component |
ANNOT_TYPE_ID_PANTHER_PATHWAY | PANTHER Pathways |
ANNOT_TYPE_ID_PANTHER_REACTOME_PATHWAY | Reactome via PANTHER |
ANNOT_TYPE_ID_PANTHER_GO_SLIM_BP | GO Slim BP |
ANNOT_TYPE_ID_PANTHER_GO_SLIM_MF | GO Slim MF |
ANNOT_TYPE_ID_PANTHER_GO_SLIM_CC | GO Slim CC |
Returns: {data: {enriched_terms: [...]}} structure
enriched_terms fields:
term_id: Term ID (e.g., "GO:0051726")term_label: Term namenumber_in_list: Count in your gene listnumber_in_reference: Count in reference genomeexpected: Expected count by chancefold_enrichment: Observed/Expectedpvalue: Raw p-valuefdr: FDR-corrected p-valuedirection: "+" (over-represented) or "-" (under-represented)
Example:
result = tu.tools.PANTHER_enrichment(
gene_list='TP53,BRCA1,EGFR,MYC', # comma-separated string
organism=9606,
annotation_dataset='GO:0008150'
)
terms = result.get('data', {}).get('enriched_terms', [])---
STRING_functional_enrichment
Tool: tu.tools.STRING_functional_enrichment
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
protein_ids | list[str] | Yes | Gene symbols or STRING IDs |
species | int | Yes | NCBI Taxonomy ID (9606 for human) |
category | str | No | IGNORED - always returns all categories |
Important: STRING returns ALL categories regardless of category parameter. Filter by category after receiving results.
Categories Returned:
Process: GO Biological ProcessFunction: GO Molecular FunctionComponent: GO Cellular ComponentKEGG: KEGG pathwaysReactome: Reactome pathwaysWikiPathways: WikiPathwaysCOMPARTMENTS: Protein localizationDISEASES: Disease associationsTISSUES: Tissue expressionKeyword: UniProt keywordsPMID: PubMed associations
Returns: {status: "success", data: [...]} structure
data fields:
category: Category name (use for filtering)term: Term ID (e.g., "GO:0051726", "KEGG:04110")description: Term descriptionnumber_of_genes: Count in your gene listnumber_of_genes_in_background: Count in STRING databasep_value: Raw p-valuefdr: FDR-corrected p-valueinputGenes: Comma-separated input gene listpreferredNames: Comma-separated preferred names
Example:
result = tu.tools.STRING_functional_enrichment(
protein_ids=['TP53', 'BRCA1', 'EGFR'],
species=9606
)
data = result.get('data', [])
go_bp = [d for d in data if d.get('category') == 'Process']
kegg = [d for d in data if d.get('category') == 'KEGG']---
ReactomeAnalysis_pathway_enrichment
Tool: tu.tools.ReactomeAnalysis_pathway_enrichment
Parameters:
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
identifiers | str | Yes | - | Space-separated gene symbols (NOT array!) |
page_size | int | No | 20 | Max pathways to return |
include_disease | bool | No | True | Include disease pathways |
projection | bool | No | False | Project to human (for cross-species) |
Returns: {data: {pathways: [...], identifiers_not_found: [...]}} structure
pathways fields:
pathway_id: Reactome ID (e.g., "R-HSA-212436")name: Pathway namespecies: Species nameis_disease: Boolean (disease pathway or not)is_lowest_level: Boolean (leaf pathway or parent)entities_found: Count in your gene listentities_total: Total entities in pathwayentities_ratio: Found/Total ratiop_value: Raw p-valuefdr: FDR-corrected p-valuereactions_found: Reactions with your genesreactions_total: Total reactions
Example:
result = tu.tools.ReactomeAnalysis_pathway_enrichment(
identifiers='TP53 BRCA1 EGFR MYC', # space-separated string
page_size=50,
include_disease=True,
projection=False
)
pathways = result.get('data', {}).get('pathways', [])
not_found = result.get('data', {}).get('identifiers_not_found', [])---
ID Conversion Tools
MyGene_batch_query
Tool: tu.tools.MyGene_batch_query
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
gene_ids | list[str] | Yes | Gene IDs (symbols, Ensembl, Entrez, UniProt) |
fields | str | No | Comma-separated fields to return |
species | str | No | 'human', 'mouse', etc. (auto-detected if omitted) |
Common Fields:
symbol: Official gene symbolentrezgene: Entrez Gene IDensembl.gene: Ensembl gene IDuniprot.Swiss-Prot: UniProt IDname: Gene namealias: Gene aliases
Returns: {results: [{query, _id, symbol, ...}]}
Example:
result = tu.tools.MyGene_batch_query(
gene_ids=['ENSG00000141510', 'ENSG00000012048'],
fields='symbol,entrezgene,ensembl.gene'
)
for hit in result.get('results', []):
print(f"{hit['query']} -> {hit.get('symbol')}")---
STRING_map_identifiers
Tool: tu.tools.STRING_map_identifiers
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
protein_ids | list[str] | Yes | Gene symbols or other identifiers |
species | int | Yes | NCBI Taxonomy ID |
Returns: {status: "success", data: [{...}]}
data fields:
queryItem: Your input IDpreferredName: Official gene symbol (USE THIS)stringId: STRING database IDannotation: Additional info
Example:
result = tu.tools.STRING_map_identifiers(
protein_ids=['TP53', 'BRCA1', 'EGFR'],
species=9606
)
for item in result.get('data', []):
print(f"{item['queryItem']} -> {item['preferredName']} ({item['stringId']})")---
Organism Taxonomy IDs
| Organism | Common Name | Taxonomy ID |
|---|---|---|
| Human | Homo sapiens | 9606 |
| Mouse | Mus musculus | 10090 |
| Rat | Rattus norvegicus | 10116 |
| Fly | Drosophila melanogaster | 7227 |
| Worm | Caenorhabditis elegans | 6239 |
| Yeast | Saccharomyces cerevisiae | 4932 |
| Zebrafish | Danio rerio | 7955 |
---
Common Parameter Mistakes
1. Array vs String
WRONG:
# PANTHER expects string, not array
tu.tools.PANTHER_enrichment(gene_list=['TP53', 'BRCA1'], ...)CORRECT:
tu.tools.PANTHER_enrichment(gene_list='TP53,BRCA1', ...)2. Comma vs Space Separation
PANTHER: Comma-separated
gene_list='TP53,BRCA1,EGFR'Reactome: Space-separated
identifiers='TP53 BRCA1 EGFR'3. Organism Format
gseapy: String name
organism='human' # NOT 9606PANTHER/STRING: Taxonomy ID
organism=9606 # NOT 'human'
species=96064. Library Names
WRONG:
gene_sets='GO_Biological_Process' # Missing yearCORRECT:
gene_sets='GO_Biological_Process_2021' # Include year5. Result Access
gseapy.enrichr: .results attribute
df = result.results # NOT result.datagseapy.prerank: .res2d attribute
df = result.res2d # NOT result.results---
See also:
- ora_workflow.md - ORA usage examples
- gsea_workflow.md - GSEA usage examples
- troubleshooting.md - Common issues
Troubleshooting Guide
Complete troubleshooting guide for gene enrichment analysis issues.
---
Common Issues Index
1. No Significant Enrichment Found 2. Gene Not Found Errors 3. gseapy Library Not Found 4. PANTHER Returns Empty Results 5. STRING Returns All Categories 6. Reactome Identifiers Not Found 7. enrichr_gene_enrichment_analysis Returns Wrong Format 8. Results Don't Match Between Tools 9. GSEA Gives Unstable Results 10. Memory Errors with Large Gene Lists
---
No Significant Enrichment Found
Symptoms
go_bp_result = gseapy.enrichr(gene_list=genes, gene_sets='GO_Biological_Process_2021', ...)
go_bp_sig = go_bp_result.results[go_bp_result.results['Adjusted P-value'] < 0.05]
print(len(go_bp_sig)) # Output: 0Possible Causes & Solutions
1. Gene Symbols Are Invalid
Diagnosis:
from tooluniverse import ToolUniverse
tu = ToolUniverse()
tu.load_tools()
# Check if genes are recognized
mapped = tu.tools.STRING_map_identifiers(
protein_ids=genes,
species=9606
)
print(f"Mapped: {len(mapped['data'])}/{len(genes)}")Solution:
- Convert IDs using MyGene:
tu.tools.MyGene_batch_query(gene_ids=genes, fields='symbol') - Use official HGNC symbols
- Remove version suffixes from Ensembl IDs
2. Library Version Too Old/New
Solution:
# Try different library versions
for lib_version in ['GO_Biological_Process_2021', 'GO_Biological_Process_2023', 'GO_Biological_Process_2025']:
result = gseapy.enrichr(gene_list=genes, gene_sets=lib_version, ...)
sig = result.results[result.results['Adjusted P-value'] < 0.05]
print(f"{lib_version}: {len(sig)} significant terms")3. Significance Threshold Too Stringent
Solution:
# Try relaxing threshold
for cutoff in [0.05, 0.1, 0.15, 0.2]:
sig = go_bp_result.results[go_bp_result.results['Adjusted P-value'] < cutoff]
print(f"Cutoff {cutoff}: {len(sig)} significant terms")4. Wrong Statistical Method
Solution:
# If ORA fails, try GSEA
import pandas as pd
# Create ranked list (even if you don't have fold-changes)
# Use presence/absence scoring: 1 for genes in list, 0 for others
ranked_dict = {gene: 1 for gene in genes}
ranked_series = pd.Series(ranked_dict).sort_values(ascending=False)
gsea_result = gseapy.prerank(
rnk=ranked_series,
gene_sets='GO_Biological_Process_2021',
...
)5. Organism Mismatch
Solution:
# Verify organism parameter
result = gseapy.enrichr(
gene_list=genes,
gene_sets='GO_Biological_Process_2021',
organism='human', # NOT 'homo_sapiens' or 9606
...
)6. Gene List Too Small or Too Large
Diagnosis:
print(f"Gene list size: {len(genes)}")
# Small (<5): insufficient power
# Large (>500): too non-specificSolution:
- Small lists (<5 genes): Use PANTHER/STRING (better for small lists) or switch to gene-level annotation
- Large lists (>500 genes): Use custom background or stricter DEG cutoff
7. No True Enrichment
Valid Result: Sometimes there genuinely is no enrichment. Document this:
## Results
No significant enrichment was found at FDR < 0.05.
- Gene list size: 45 genes
- Libraries tested: GO BP, GO MF, GO CC, KEGG, Reactome
- Alternative cutoff (FDR < 0.1): 3 weak hits (exploratory only)---
Gene Not Found Errors
Symptoms
Error: Gene 'TP-53' not found
Warning: 15 out of 50 genes not mappedSolutions
1. Check ID Type
# Detect ID type
sample = genes[0]
if sample.startswith('ENSG'):
id_type = 'ensembl'
elif sample.isdigit():
id_type = 'entrez'
elif len(sample) == 6 and sample[0].isalpha():
id_type = 'uniprot'
else:
id_type = 'symbol'
print(f"Detected ID type: {id_type}")2. Convert IDs
from tooluniverse import ToolUniverse
tu = ToolUniverse()
tu.load_tools()
# Convert Ensembl -> Symbol
if id_type == 'ensembl':
result = tu.tools.MyGene_batch_query(
gene_ids=genes,
fields='symbol,entrezgene,ensembl.gene'
)
symbol_map = {hit['query']: hit.get('symbol', hit['query'])
for hit in result.get('results', [])}
gene_symbols = [symbol_map.get(g, g) for g in genes]3. Remove Version Suffixes
# ENSG00000141510.16 -> ENSG00000141510
genes_clean = [g.split('.')[0] for g in genes]4. Fix Common Typos
# Common issues
fixes = {
'TP-53': 'TP53',
'BRCA 1': 'BRCA1',
'p53': 'TP53',
'egfr': 'EGFR', # case-sensitive
}
genes_fixed = [fixes.get(g, g) for g in genes]5. Try Aliases
# Use MyGene to find aliases
for gene in unmapped_genes:
result = tu.tools.MyGene_query_genes(query=gene)
if result.get('hits'):
official_symbol = result['hits'][0].get('symbol')
print(f"{gene} -> {official_symbol}")---
gseapy Library Not Found
Symptoms
ValueError: gene_set 'GO_Biological_Process' not foundSolutions
1. List Available Libraries
import gseapy as gp
all_libs = gp.get_library_name(organism='human')
print(f"Total libraries: {len(all_libs)}")
# Search for library
matching = [lib for lib in all_libs if 'GO_Biological' in lib]
print("Available GO BP libraries:", matching)2. Use Exact Library Name
# Wrong (will fail)
gene_sets='GO_Biological_Process'
# Correct
gene_sets='GO_Biological_Process_2021' # or 2023, 20253. Check Case Sensitivity
# Library names are case-sensitive
gene_sets='KEGG_2021_Human' # NOT 'kegg_2021_human'4. Organism-Specific Libraries
# Wrong for mouse
gene_sets='KEGG_2021_Human' # Will fail for mouse genes
# Correct for mouse
gene_sets='KEGG_2019_Mouse'---
PANTHER Returns Empty Results
Symptoms
panther_result = tu.tools.PANTHER_enrichment(gene_list=genes, organism=9606, ...)
print(panther_result.get('data', {}).get('enriched_terms', [])) # Output: []Solutions
1. Check Input Format
# Wrong (array)
gene_list=["TP53", "BRCA1", "EGFR"]
# Correct (comma-separated string)
gene_list="TP53,BRCA1,EGFR"2. Check Organism ID
# Wrong
organism='human' # PANTHER uses taxonomy IDs
# Correct
organism=9606 # NCBI Taxonomy ID for human3. Check Annotation Dataset
# Valid datasets
annotation_datasets = [
'GO:0008150', # Biological Process
'GO:0003674', # Molecular Function
'GO:0005575', # Cellular Component
'ANNOT_TYPE_ID_PANTHER_PATHWAY',
'ANNOT_TYPE_ID_PANTHER_REACTOME_PATHWAY',
]
# Wrong
annotation_dataset='biological_process'
# Correct
annotation_dataset='GO:0008150'4. Gene Symbols Not Recognized
# PANTHER uses official gene symbols
# Convert using STRING first
mapped = tu.tools.STRING_map_identifiers(
protein_ids=genes,
species=9606
)
official_symbols = [item['preferredName'] for item in mapped.get('data', [])]
gene_list = ','.join(official_symbols)---
STRING Returns All Categories
Symptoms
string_result = tu.tools.STRING_functional_enrichment(
protein_ids=genes,
species=9606,
category='Process' # Only want GO BP
)
# But result includes Process, Function, Component, KEGG, Reactome, etc.Solution
This is expected behavior. STRING always returns all categories. Filter after receiving results:
string_data = string_result.get('data', [])
# Filter by category
string_go_bp = [d for d in string_data if d.get('category') == 'Process']
string_go_mf = [d for d in string_data if d.get('category') == 'Function']
string_go_cc = [d for d in string_data if d.get('category') == 'Component']
string_kegg = [d for d in string_data if d.get('category') == 'KEGG']
string_reactome = [d for d in string_data if d.get('category') == 'Reactome']
string_wp = [d for d in string_data if d.get('category') == 'WikiPathways']
print(f"GO BP terms: {len(string_go_bp)}")
print(f"KEGG pathways: {len(string_kegg)}")---
Reactome Identifiers Not Found
Symptoms
reactome_result = tu.tools.ReactomeAnalysis_pathway_enrichment(identifiers=genes, ...)
identifiers_not_found = reactome_result.get('data', {}).get('identifiers_not_found', [])
print(f"Not found: {len(identifiers_not_found)}") # High numberSolutions
1. Use Space-Separated String
# Wrong (array)
identifiers=["TP53", "BRCA1", "EGFR"]
# Correct (space-separated string)
identifiers="TP53 BRCA1 EGFR"
# or
identifiers=' '.join(genes)2. Use Official Gene Symbols
# Use STRING to get official names
mapped = tu.tools.STRING_map_identifiers(
protein_ids=genes,
species=9606
)
official_symbols = [item['preferredName'] for item in mapped.get('data', [])]
identifiers = ' '.join(official_symbols)3. Try UniProt IDs
# Convert to UniProt IDs
mygene_result = tu.tools.MyGene_batch_query(
gene_ids=genes,
fields='uniprot.Swiss-Prot'
)
uniprot_ids = [hit['uniprot']['Swiss-Prot']
for hit in mygene_result.get('results', [])
if 'uniprot' in hit]
identifiers = ' '.join(uniprot_ids)---
enrichr_gene_enrichment_analysis Returns Wrong Format
Symptoms
result = tu.tools.enrichr_gene_enrichment_analysis(genes=genes, ...)
# Returns: connected_paths JSON string, NOT standard enrichment resultsSolution
DO NOT USE `enrichr_gene_enrichment_analysis` for standard enrichment analysis.
This ToolUniverse tool returns path analysis, not standard ORA enrichment.
Use `gseapy.enrichr()` directly instead:
import gseapy
# Correct approach
result = gseapy.enrichr(
gene_list=genes,
gene_sets='GO_Biological_Process_2021',
organism='human',
outdir=None,
no_plot=True,
)
# Returns standard enrichment DataFrame---
Results Don't Match Between Tools
Symptoms
# gseapy finds 50 significant terms
# PANTHER finds 30 significant terms
# Only 10 overlapExplanation & Solutions
This is Normal
Different tools use:
- Different gene set annotations (versions, curation)
- Different backgrounds (genome-wide vs database-specific)
- Different statistical methods (Fisher's exact vs hypergeometric)
- Different multiple testing corrections
Focus on Consensus
# Extract GO IDs from each source
import re
gseapy_terms = set()
for term in go_bp_sig['Term']:
match = re.search(r'(GO:\d+)', term)
if match:
gseapy_terms.add(match.group(1))
panther_terms = set(t['term_id'] for t in panther_bp_terms if t.get('fdr', 1) < 0.05)
string_terms = set(d['term'] for d in string_go_bp if d.get('fdr', 1) < 0.05)
# Consensus terms (in 2+ sources)
all_terms = gseapy_terms | panther_terms | string_terms
consensus = []
for term in all_terms:
sources = []
if term in gseapy_terms: sources.append('gseapy')
if term in panther_terms: sources.append('PANTHER')
if term in string_terms: sources.append('STRING')
if len(sources) >= 2:
consensus.append((term, sources))
print(f"Consensus terms: {len(consensus)}")Report Both
| GO Term | gseapy FDR | PANTHER FDR | STRING FDR | Consensus |
|---------|-----------|-------------|-----------|-----------|
| GO:0051726 | 3.4e-06 | 4.5e-05 | 3.2e-05 | 3/3 ✓ |
| GO:0006281 | 1.2e-05 | 2.1e-04 | - | 2/3 |
| GO:0007049 | 5.6e-06 | - | 8.9e-06 | 2/3 |---
GSEA Gives Unstable Results
Symptoms
# Run 1: 50 significant terms
# Run 2: 45 significant terms (different terms!)Solutions
1. Set Random Seed
gsea_result = gseapy.prerank(
rnk=ranked_series,
gene_sets='GO_Biological_Process_2021',
seed=42, # CRITICAL for reproducibility
permutation_num=1000,
...
)2. Increase Permutation Number
# More permutations = more stable p-values
gsea_result = gseapy.prerank(
rnk=ranked_series,
gene_sets='GO_Biological_Process_2021',
seed=42,
permutation_num=5000, # instead of 1000
...
)3. Check for Ties
# Check for many genes with same score
print(ranked_series.value_counts().head())
# If many ties, add small noise
import numpy as np
np.random.seed(42)
ranked_series = ranked_series + np.random.normal(0, 0.001, len(ranked_series))---
Memory Errors with Large Gene Lists
Symptoms
MemoryError: Unable to allocate arraySolutions
1. Run Libraries Sequentially
# Wrong (runs all at once)
result = gseapy.enrichr(
gene_list=genes,
gene_sets=['GO_Biological_Process_2021', 'GO_Molecular_Function_2021', ...], # 10+ libraries
...
)
# Correct (one at a time)
results = {}
for lib in ['GO_Biological_Process_2021', 'GO_Molecular_Function_2021', 'KEGG_2021_Human']:
results[lib] = gseapy.enrichr(gene_list=genes, gene_sets=lib, ...)2. Filter Gene Sets by Size
gsea_result = gseapy.prerank(
rnk=ranked_series,
gene_sets='GO_Biological_Process_2021',
min_size=10, # instead of 5
max_size=200, # instead of 500
...
)3. Use Smaller Libraries
# Instead of GO_Biological_Process_2021 (17,000 terms)
# Use GO Slim (high-level terms only)
gene_sets='ANNOT_TYPE_ID_PANTHER_GO_SLIM_BP' # via PANTHER---
Getting Help
If you encounter an issue not covered here:
1. Check gseapy documentation: https://gseapy.readthedocs.io/ 2. Check tool parameter documentation: references/tool_parameters.md 3. Test with known good example:
# Known working example
genes = ["TP53", "BRCA1", "EGFR", "MYC", "AKT1", "PTEN"]
result = gseapy.enrichr(
gene_list=genes,
gene_sets='GO_Biological_Process_2021',
organism='human',
outdir=None,
no_plot=True,
)---
See also:
- ora_workflow.md - Complete ORA examples
- gsea_workflow.md - Complete GSEA examples
- tool_parameters.md - Complete parameter reference
#!/usr/bin/env python3
"""Per-condition enrichment screen — run enrichment across N conditions
and report what fraction had any significant hit (optionally filtered by
a keyword/category). Built for "% of conditions with significant
enrichment of <category> pathways" questions.
Why this exists:
- The agent kept timing out trying to enumerate conditions one-by-one.
This script accepts either:
(a) a 2-col TSV of `condition_label<TAB>gene` for ALL conditions;
groups by condition and runs enrichr per group.
(b) multiple --condition-genes <label>=<file.txt> args (one per
condition).
- Outputs per-condition summaries and a final aggregate percentage.
- The "category filter" lets the user say "count a condition as
positive only if at least one significant term matches keyword X"
(e.g., "immune", "Oxidative", "stress").
Usage:
# (a) Single TSV with all conditions:
python condition_enrichment_screen.py \\
--conditions-tsv /tmp/all_conds.tsv \\
--library ReactomePathways.gmt \\
--background /tmp/expressed.txt \\
--exclude-condition no_T_cells \\
--keyword immune --keyword interferon --keyword cytokine \\
--workdir /tmp/cond_screen
# (b) Per-condition gene-list files:
python condition_enrichment_screen.py \\
--condition-genes acute=/tmp/acute_sig.txt \\
--condition-genes round1=/tmp/r1_sig.txt \\
--condition-genes round2=/tmp/r2_sig.txt \\
--condition-genes round3=/tmp/r3_sig.txt \\
--library ReactomePathways.gmt \\
--background /tmp/expressed.txt \\
--keyword "immune" --keyword "interferon" \\
--workdir /tmp/cond_screen
Output blocks (parseable):
# CONDITION acute: n_genes=234 sig_terms=0 sig_terms_keyword=0 POSITIVE=False
# CONDITION round1: n_genes=189 sig_terms=4 sig_terms_keyword=2 POSITIVE=True
# CONDITION round2: ...
# SUMMARY: n_conditions_total=4 n_excluded=0 n_evaluated=4
# n_with_any_sig=2 pct_with_any_sig=50.00%
# n_with_keyword_sig=2 pct_with_keyword_sig=50.00%
# KEYWORDS used: ['immune', 'interferon']
WORKSPACE ISOLATION
-------------------
This script writes only to --workdir. NEVER writes to the input dir.
Notes:
- The library can be either an Enrichr library name (e.g.
KEGG_2021_Human, Reactome_2022) OR a path to a local .gmt file.
- "Sig" means Adjusted P-value < --padj-cutoff (default 0.05).
- "Keyword match" is case-insensitive substring on the Term column.
"""
from __future__ import annotations
import argparse
import sys
import tempfile
from pathlib import Path
def _read_gene_list(path: Path) -> list[str]:
raw = [ln.strip() for ln in path.read_text().splitlines() if ln.strip()]
if not raw:
return []
first = raw[0].lower()
if first in ("gene", "symbol", "gene_symbol", "gene_name", "gene_id", "id"):
raw = raw[1:]
out = []
for ln in raw:
if "\t" in ln:
ln = ln.split("\t")[0]
elif "," in ln:
ln = ln.split(",")[0]
ln = ln.strip().strip('"').strip("'")
if ln:
out.append(ln)
return out
def _read_conditions_tsv(path: Path) -> dict[str, list[str]]:
"""Parse a 2-col TSV: condition<TAB>gene. Returns {condition: [genes]}."""
out: dict[str, list[str]] = {}
raw = [ln.strip() for ln in path.read_text().splitlines() if ln.strip()]
if not raw:
return out
# Skip header if present
first = raw[0].lower()
if "\t" in raw[0]:
toks = raw[0].split("\t")
if any(tok.lower() in ("condition", "label", "screen", "round", "treatment") for tok in toks):
raw = raw[1:]
for ln in raw:
if "\t" not in ln:
continue
cond, gene = ln.split("\t", 1)
cond = cond.strip().strip('"')
gene = gene.strip().strip('"').split("\t")[0]
if not cond or not gene:
continue
out.setdefault(cond, []).append(gene)
return out
def _parse_condition_genes(args_list: list[str]) -> dict[str, list[str]]:
"""Parse --condition-genes label=path/to.txt args into dict."""
out: dict[str, list[str]] = {}
for arg in args_list:
if "=" not in arg:
sys.exit(f"ERROR: --condition-genes must be 'label=path', got '{arg}'")
label, path_s = arg.split("=", 1)
out[label.strip()] = _read_gene_list(Path(path_s.strip()))
return out
def _is_local_gmt(library: str) -> bool:
return library.endswith(".gmt") and Path(library).exists()
def _load_library(library: str):
"""Return either the library name (string) for Enrichr, or a parsed
{set_name: [gene1, ...]} dict for a local .gmt file.
"""
if _is_local_gmt(library):
d = {}
with open(library) as f:
for ln in f:
parts = ln.rstrip("\n").split("\t")
if len(parts) < 3:
continue
name = parts[0].strip()
# parts[1] is description (often empty), rest are genes
genes = [g.strip() for g in parts[2:] if g.strip()]
if name and genes:
d[name] = genes
return d
return library
def main():
ap = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
src = ap.add_mutually_exclusive_group(required=True)
src.add_argument("--conditions-tsv",
help="2-col TSV: condition<TAB>gene (multiple genes per condition).")
src.add_argument("--condition-genes", action="append", default=[],
help="label=path/to/gene_list.txt. Repeatable.")
ap.add_argument("--library", required=True,
help="Enrichr library name OR path to local .gmt.")
ap.add_argument("--organism", default="Human",
help="Enrichr organism (only used for online libraries).")
ap.add_argument("--background",
help="Background gene-list file (one per line).")
ap.add_argument("--padj-cutoff", type=float, default=0.05,
help="Adjusted P-value cutoff for 'significant' (default 0.05).")
ap.add_argument("--keyword", action="append", default=[],
help="Keyword(s) to filter terms — a condition is "
"category-positive iff any sig term contains a keyword. "
"Repeatable, case-insensitive.")
ap.add_argument("--exclude-condition", action="append", default=[],
help="Skip this condition label (e.g. 'no_T_cells_control'). "
"Repeatable.")
ap.add_argument("--workdir", default="",
help="Output dir.")
args = ap.parse_args()
if args.workdir:
workdir = Path(args.workdir).resolve()
else:
workdir = Path(tempfile.mkdtemp(prefix="cond_screen_"))
workdir.mkdir(parents=True, exist_ok=True)
print(f"# workdir: {workdir}", file=sys.stderr)
# Build the conditions dict
if args.conditions_tsv:
conds = _read_conditions_tsv(Path(args.conditions_tsv))
else:
conds = _parse_condition_genes(args.condition_genes or [])
if not conds:
sys.exit("ERROR: no conditions parsed.")
# Background
bg = None
if args.background:
bg = _read_gene_list(Path(args.background))
print(f"# background: n={len(bg)}", file=sys.stderr)
# Library
library = _load_library(args.library)
is_dict_lib = isinstance(library, dict)
if is_dict_lib:
print(f"# library: local GMT {args.library} ({len(library)} sets)", file=sys.stderr)
else:
print(f"# library: {library} (Enrichr)", file=sys.stderr)
# Run per-condition
import gseapy as gp
excluded_set = set(args.exclude_condition or [])
keywords = [k.lower() for k in (args.keyword or [])]
n_evaluated = 0
n_with_any_sig = 0
n_with_keyword_sig = 0
excluded_seen = []
# Throttle Enrichr requests — its public API rate-limits aggressively.
import time
enrichr_throttle_s = 1.5
for label in sorted(conds.keys()):
if label in excluded_set:
excluded_seen.append(label)
print(f"# CONDITION {label}: EXCLUDED")
continue
genes = conds[label]
if not genes:
print(f"# CONDITION {label}: n_genes=0 (skipping)")
continue
n_evaluated += 1
# Retry up to 3x on transient Enrichr errors (rate limits, 5xx)
res = None
last_err = None
for attempt in range(3):
try:
if is_dict_lib:
# gp.enrich (offline GMT) — does NOT accept organism arg
res = gp.enrich(
gene_list=genes,
gene_sets=library,
background=bg,
outdir=None,
no_plot=True,
)
else:
# gp.enrichr (online) — needs organism
kwargs = dict(
outdir=None,
no_plot=True,
organism=args.organism,
gene_sets=library,
)
if bg is not None:
kwargs["background"] = bg
res = gp.enrichr(gene_list=genes, **kwargs)
break
except Exception as e:
last_err = e
msg = str(e).lower()
if "429" in msg or "503" in msg or "504" in msg or "timeout" in msg:
backoff = (attempt + 1) * 5.0
print(f"# CONDITION {label}: transient ({type(e).__name__}), "
f"retry in {backoff}s ({attempt + 1}/3)")
time.sleep(backoff)
continue
# Non-transient error — give up on this condition
break
if res is None:
print(f"# CONDITION {label}: ERROR {type(last_err).__name__ if last_err else 'unknown'}: {last_err}")
continue
# Per-call throttle
if not is_dict_lib:
time.sleep(enrichr_throttle_s)
df = res.results
if df is None or len(df) == 0:
print(f"# CONDITION {label}: n_genes={len(genes)} no_terms_returned")
continue
sig = df[df["Adjusted P-value"] < args.padj_cutoff]
n_sig = len(sig)
if keywords and n_sig > 0:
terms_lower = sig["Term"].astype(str).str.lower()
mask = terms_lower.apply(lambda t: any(k in t for k in keywords))
n_kw_sig = int(mask.sum())
else:
n_kw_sig = 0
any_pos = n_sig > 0
kw_pos = n_kw_sig > 0
if any_pos:
n_with_any_sig += 1
if kw_pos or (not keywords and any_pos):
# If no keywords given, "category positive" is just "any sig"
n_with_keyword_sig += 1
print(f"# CONDITION {label}: n_genes={len(genes)} "
f"sig_terms={n_sig} sig_terms_keyword={n_kw_sig} "
f"ANY_POSITIVE={any_pos} KEYWORD_POSITIVE={kw_pos}")
# Save full result
out = workdir / f"cond_{label.replace('/', '_')}.csv"
df.sort_values(by="Adjusted P-value", ascending=True).to_csv(out, index=False)
print()
print("# === SUMMARY ===")
print(f"# n_conditions_total={len(conds)} n_excluded={len(excluded_seen)} "
f"n_evaluated={n_evaluated}")
if n_evaluated == 0:
print("# No conditions evaluated — cannot compute percentages.")
return
pct_any = 100.0 * n_with_any_sig / n_evaluated
pct_kw = 100.0 * n_with_keyword_sig / n_evaluated
print(f"# n_with_any_sig={n_with_any_sig} pct_with_any_sig={pct_any:.2f}%")
if keywords:
print(f"# n_with_keyword_sig={n_with_keyword_sig} "
f"pct_with_keyword_sig={pct_kw:.2f}%")
print(f"# KEYWORDS used: {args.keyword}")
if excluded_seen:
print(f"# EXCLUDED: {excluded_seen}")
print("# DONE")
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""Deterministic clusterProfiler::enrichGO + simplify(cutoff=0.7) wrapper.
Why this exists:
- enrichGO + simplify is the standard "GO enrichment with redundancy
reduction" workflow used by many published RNA-seq notebooks. The
agent's failure mode: it runs simplify but reports p.adjust from the
RAW enrichment data frame, OR misses that simplify changes the
p.adjust denominator and thus the values for surviving terms.
- This script runs enrichGO + simplify and emits BOTH the raw and
simplified frames so the agent can pick the one the question asks for.
When the question references "simplified" or "after simplify" or the
notebook calls clusterProfiler::simplify, use the simplified column.
Usage:
python enrichgo_runner.py \\
--gene-list /tmp/sig_ensembl.txt \\
--background /tmp/all_ensembl.txt \\
--keytype ENSEMBL \\
--ontology BP \\
--simplify-cutoff 0.7 \\
--candidate "regulation of T cell activation" \\
--candidate "potassium ion transmembrane transport" \\
--workdir /tmp/enrichgo_run
Output blocks (parseable):
# ENRICHGO_RAW: n_terms=X
# TOP10_RAW:
# 1. <ID> | <Description> | p=... p.adjust=... q=... count=...
# ENRICHGO_SIMPLIFIED (cutoff=0.7): n_terms=X
# TOP10_SIMPLIFIED:
# 1. <ID> | <Description> | p=... p.adjust=... q=...
# CANDIDATE '<term>': raw_rank=R raw_padj=... simp_rank=R simp_padj=...
# NOTE: simplify changes p.adjust because the multiple-testing
# denominator shrinks. Use the SIMPLIFIED p.adjust if the question
# says "in the simplified results" or "after simplify".
Required R packages: clusterProfiler, org.Hs.eg.db (or org.Mm.eg.db
for mouse). Install via skills/evals/install_r_packages.R.
WORKSPACE ISOLATION
-------------------
This script writes only to --workdir. NEVER writes to the gene-list dir.
"""
from __future__ import annotations
import argparse
import os
import shlex
import subprocess
import sys
import tempfile
from pathlib import Path
R_TEMPLATE = r"""
suppressMessages({{
ok_cp <- requireNamespace("clusterProfiler", quietly = TRUE)
ok_org <- requireNamespace("{org_db}", quietly = TRUE)
if (!ok_cp || !ok_org) {{
cat("# ERROR: required R packages not installed.\n")
cat("# Need: clusterProfiler, {org_db}\n")
cat("# Install: Rscript skills/evals/install_r_packages.R\n")
quit(save="no", status=1)
}}
library(clusterProfiler)
library({org_db})
}})
gene_file <- "{gene_file}"
bg_file <- "{bg_file}"
keytype <- "{keytype}"
ontology <- "{ontology}"
pAdjust <- "{p_adjust}"
pcutoff <- {p_cutoff}
qcutoff <- {q_cutoff}
simplify_cutoff <- {simplify_cutoff}
workdir <- "{workdir}"
genes <- readLines(gene_file)
genes <- genes[nchar(genes) > 0]
# Strip header if first line looks non-ID
first <- genes[1]
if (length(genes) >= 2) {{
if (toupper(first) %in% c("GENE","GENE_ID","GENEID","SYMBOL","ID","ENSEMBL","ENTREZ")) {{
genes <- genes[-1]
}}
}}
# For ENSEMBL: strip version suffix (ENSG00000123456.7 -> ENSG00000123456)
if (keytype == "ENSEMBL") {{
genes <- sub("\\\\..*$", "", genes)
}}
cat("# input genes:", length(genes), "first 3:", head(genes, 3), "\n")
bg <- NULL
if (nchar(bg_file) > 0) {{
bg <- readLines(bg_file)
bg <- bg[nchar(bg) > 0]
fb <- bg[1]
if (length(bg) >= 2) {{
if (toupper(fb) %in% c("GENE","GENE_ID","GENEID","SYMBOL","ID","ENSEMBL","ENTREZ")) {{
bg <- bg[-1]
}}
}}
if (keytype == "ENSEMBL") bg <- sub("\\\\..*$", "", bg)
cat("# background genes:", length(bg), "\n")
}}
ego <- enrichGO(
gene = genes,
universe = bg,
keyType = keytype,
OrgDb = {org_db},
ont = ontology,
pAdjustMethod = pAdjust,
pvalueCutoff = pcutoff,
qvalueCutoff = qcutoff,
readable = TRUE
)
if (is.null(ego)) {{
cat("# ENRICHGO_RAW: NULL — no enrichment returned\n")
quit(save="no")
}}
raw_df <- as.data.frame(ego)
cat("# ENRICHGO_RAW: n_terms=", nrow(raw_df), "\n", sep="")
write.csv(raw_df, file.path(workdir, "enrichgo_raw.csv"), row.names=FALSE)
# Top 10 raw
cat("# TOP10_RAW:\n")
n_show_raw <- min(10, nrow(raw_df))
if (n_show_raw > 0) {{
for (i in 1:n_show_raw) {{
cat(sprintf("# %d. %s | %s | p=%.4g p.adjust=%.4g q=%.4g count=%d\n",
i, raw_df$ID[i], raw_df$Description[i],
raw_df$pvalue[i], raw_df$p.adjust[i], raw_df$qvalue[i],
raw_df$Count[i]))
}}
}}
# Run simplify
ego_simp <- tryCatch({{
clusterProfiler::simplify(ego, cutoff=simplify_cutoff, by="p.adjust", select_fun=min)
}}, error=function(e) {{
cat("# SIMPLIFY ERR:", e$message, "\n")
NULL
}})
if (!is.null(ego_simp)) {{
simp_df <- as.data.frame(ego_simp)
cat("# ENRICHGO_SIMPLIFIED (cutoff=", simplify_cutoff, "): n_terms=", nrow(simp_df), "\n", sep="")
write.csv(simp_df, file.path(workdir, "enrichgo_simplified.csv"), row.names=FALSE)
cat("# TOP10_SIMPLIFIED:\n")
n_show_s <- min(10, nrow(simp_df))
if (n_show_s > 0) {{
for (i in 1:n_show_s) {{
cat(sprintf("# %d. %s | %s | p=%.4g p.adjust=%.4g q=%.4g count=%d\n",
i, simp_df$ID[i], simp_df$Description[i],
simp_df$pvalue[i], simp_df$p.adjust[i], simp_df$qvalue[i],
simp_df$Count[i]))
}}
}}
}} else {{
simp_df <- raw_df[0, , drop=FALSE]
cat("# ENRICHGO_SIMPLIFIED: skipped (simplify returned NULL)\n")
}}
# Candidate terms
candidates <- strsplit("{candidates}", "\\|\\|\\|")[[1]]
candidates <- candidates[nchar(candidates) > 0]
for (cand in candidates) {{
cand_l <- tolower(cand)
raw_idx <- which(grepl(cand_l, tolower(raw_df$Description), fixed=TRUE) |
grepl(cand_l, tolower(raw_df$ID), fixed=TRUE))
raw_part <- if (length(raw_idx) > 0) {{
sprintf("raw_rank=%d raw_padj=%.4g raw_p=%.4g", raw_idx[1],
raw_df$p.adjust[raw_idx[1]], raw_df$pvalue[raw_idx[1]])
}} else {{ "raw_rank=NA (not in raw)" }}
if (!is.null(ego_simp) && nrow(simp_df) > 0) {{
simp_idx <- which(grepl(cand_l, tolower(simp_df$Description), fixed=TRUE) |
grepl(cand_l, tolower(simp_df$ID), fixed=TRUE))
simp_part <- if (length(simp_idx) > 0) {{
sprintf("simp_rank=%d simp_padj=%.4g simp_p=%.4g", simp_idx[1],
simp_df$p.adjust[simp_idx[1]], simp_df$pvalue[simp_idx[1]])
}} else {{ "simp_rank=NA (collapsed by simplify)" }}
}} else {{
simp_part <- "simp_rank=NA (simplify unavailable)"
}}
cat(sprintf("# CANDIDATE '%s': %s %s\n", cand, raw_part, simp_part))
}}
cat("# DONE\n")
"""
def main():
ap = argparse.ArgumentParser(
description=__doc__,
formatter_class=argparse.RawDescriptionHelpFormatter,
)
ap.add_argument("--gene-list", required=True,
help="Significant gene list (one per line).")
ap.add_argument("--background", default="",
help="Background gene list (one per line). Default: enrichGO universe.")
ap.add_argument("--keytype", default="ENSEMBL",
choices=["ENSEMBL", "SYMBOL", "ENTREZID", "UNIPROT"],
help="Gene ID type. Default ENSEMBL.")
ap.add_argument("--ontology", default="BP",
choices=["BP", "MF", "CC", "ALL"],
help="GO sub-ontology. Default BP.")
ap.add_argument("--organism", default="human",
choices=["human", "mouse", "rat", "fly", "worm", "yeast", "zebrafish"],
help="Default human.")
ap.add_argument("--simplify-cutoff", type=float, default=0.7,
help="clusterProfiler::simplify similarity cutoff (default 0.7).")
ap.add_argument("--p-adjust", default="BH",
help="enrichGO pAdjustMethod (default BH).")
ap.add_argument("--p-cutoff", type=float, default=0.05,
help="enrichGO pvalueCutoff (default 0.05).")
ap.add_argument("--q-cutoff", type=float, default=0.05,
help="enrichGO qvalueCutoff (default 0.05).")
ap.add_argument("--candidate", action="append", default=[],
help="Candidate term name (substring) to report rank for. Repeatable.")
ap.add_argument("--workdir", default="",
help="Output dir (default $TMPDIR/enrichgo_<pid>).")
args = ap.parse_args()
org_db_map = {
"human": "org.Hs.eg.db",
"mouse": "org.Mm.eg.db",
"rat": "org.Rn.eg.db",
"fly": "org.Dm.eg.db",
"worm": "org.Ce.eg.db",
"yeast": "org.Sc.sgd.db",
"zebrafish": "org.Dr.eg.db",
}
org_db = org_db_map[args.organism]
if args.workdir:
workdir = Path(args.workdir).resolve()
else:
workdir = Path(tempfile.mkdtemp(prefix="enrichgo_"))
workdir.mkdir(parents=True, exist_ok=True)
print(f"# workdir: {workdir}", file=sys.stderr)
# Refuse to write into the input data folder
workdir_r = workdir.resolve()
for src in (args.gene_list, args.background or None):
if not src:
continue
input_dir = Path(src).resolve().parent
if workdir_r == input_dir or input_dir in workdir_r.parents:
sys.exit(
f"ERROR: workdir {workdir} is inside the input data folder "
f"{input_dir}. Use --workdir /tmp/... to keep input data read-only."
)
candidates = "|||".join(args.candidate)
r_script = R_TEMPLATE.format(
org_db=org_db,
gene_file=str(Path(args.gene_list).resolve()),
bg_file=str(Path(args.background).resolve()) if args.background else "",
keytype=args.keytype,
ontology=args.ontology,
p_adjust=args.p_adjust,
p_cutoff=args.p_cutoff,
q_cutoff=args.q_cutoff,
simplify_cutoff=args.simplify_cutoff,
workdir=str(workdir),
candidates=candidates,
)
r_path = workdir / "run_enrichgo.R"
r_path.write_text(r_script)
print(f"# R script: {r_path}", file=sys.stderr)
result = subprocess.run(
["Rscript", str(r_path)],
capture_output=True, text=True,
)
print(result.stdout)
if result.stderr:
print("--- R stderr ---", file=sys.stderr)
print(result.stderr, file=sys.stderr)
if result.returncode != 0:
sys.exit(result.returncode)
if __name__ == "__main__":
main()
#!/usr/bin/env python3
"""
Format enrichment analysis results for reports.
Converts gseapy, PANTHER, STRING, and Reactome results into
standardized markdown tables for inclusion in analysis reports.
Usage:
from format_enrichment_output import format_ora_results, format_gsea_results
# Format ORA results
markdown = format_ora_results(go_bp_result, top_n=10, title="GO Biological Process")
# Format GSEA results
markdown = format_gsea_results(gsea_result, top_n=10, title="GSEA - GO BP")
"""
import pandas as pd
import re
from typing import Optional, Union, List, Dict
def format_ora_results(
enrichr_result,
top_n: int = 10,
title: str = "Enrichment Results",
fdr_cutoff: float = 0.05,
include_genes: bool = True
) -> str:
"""
Format gseapy.enrichr() results as markdown table.
Args:
enrichr_result: Result from gseapy.enrichr()
top_n: Number of top terms to include
title: Table title
fdr_cutoff: FDR cutoff for filtering
include_genes: Whether to include gene list in table
Returns:
Markdown formatted table string
"""
df = enrichr_result.results
sig = df[df['Adjusted P-value'] < fdr_cutoff].copy()
if len(sig) == 0:
return f"## {title}\n\nNo significant terms found at FDR < {fdr_cutoff}\n"
sig = sig.head(top_n)
# Build markdown
lines = [f"## {title}\n"]
lines.append(f"**Total significant terms**: {len(df[df['Adjusted P-value'] < fdr_cutoff])}")
lines.append(f"**Showing top**: {len(sig)}\n")
# Table header
if include_genes:
lines.append("| Rank | Term | P-value | Adj. P-value | Overlap | Odds Ratio | Genes |")
lines.append("|------|------|---------|--------------|---------|------------|-------|")
else:
lines.append("| Rank | Term | P-value | Adj. P-value | Overlap | Odds Ratio |")
lines.append("|------|------|---------|--------------|---------|------------|")
# Table rows
for idx, (_, row) in enumerate(sig.iterrows(), 1):
term = row['Term']
pval = f"{row['P-value']:.2e}"
adj_pval = f"{row['Adjusted P-value']:.2e}"
overlap = row['Overlap']
odds_ratio = f"{row['Odds Ratio']:.2f}"
if include_genes:
genes = row['Genes']
# Truncate if too long
if len(genes) > 100:
genes = genes[:97] + "..."
lines.append(f"| {idx} | {term} | {pval} | {adj_pval} | {overlap} | {odds_ratio} | {genes} |")
else:
lines.append(f"| {idx} | {term} | {pval} | {adj_pval} | {overlap} | {odds_ratio} |")
return "\n".join(lines) + "\n"
def format_gsea_results(
gsea_result,
top_n: int = 10,
title: str = "GSEA Results",
fdr_cutoff: float = 0.25,
direction: str = 'both'
) -> str:
"""
Format gseapy.prerank() results as markdown table.
Args:
gsea_result: Result from gseapy.prerank()
top_n: Number of top terms to include
title: Table title
fdr_cutoff: FDR cutoff for filtering
direction: 'both', 'positive', or 'negative' (NES direction)
Returns:
Markdown formatted table string
"""
df = gsea_result.res2d
sig = df[df['FDR q-val'].astype(float) < fdr_cutoff].copy()
if len(sig) == 0:
return f"## {title}\n\nNo significant terms found at FDR < {fdr_cutoff}\n"
# Filter by direction
if direction == 'positive':
sig = sig[sig['NES'] > 0]
subtitle = "(Up-regulated pathways)"
elif direction == 'negative':
sig = sig[sig['NES'] < 0]
subtitle = "(Down-regulated pathways)"
else:
subtitle = ""
# Sort by |NES|
sig['abs_NES'] = sig['NES'].abs()
sig = sig.sort_values('abs_NES', ascending=False).head(top_n)
# Build markdown
lines = [f"## {title} {subtitle}\n"]
lines.append(f"**Total significant terms**: {len(df[df['FDR q-val'].astype(float) < fdr_cutoff])}")
lines.append(f"**Showing top**: {len(sig)}\n")
# Table header
lines.append("| Rank | Term | NES | FDR q-val | Lead Genes |")
lines.append("|------|------|-----|-----------|------------|")
# Table rows
for idx, (_, row) in enumerate(sig.iterrows(), 1):
term = row['Term']
nes = f"{row['NES']:.2f}"
fdr = f"{row['FDR q-val']:.3e}"
lead_genes = row['Lead_genes']
# Truncate if too long
if len(lead_genes) > 100:
lead_genes = lead_genes[:97] + "..."
lines.append(f"| {idx} | {term} | {nes} | {fdr} | {lead_genes} |")
return "\n".join(lines) + "\n"
def format_panther_results(
panther_result: Dict,
top_n: int = 10,
title: str = "PANTHER Enrichment",
fdr_cutoff: float = 0.05
) -> str:
"""
Format PANTHER_enrichment results as markdown table.
Args:
panther_result: Result from tu.tools.PANTHER_enrichment()
top_n: Number of top terms to include
title: Table title
fdr_cutoff: FDR cutoff for filtering
Returns:
Markdown formatted table string
"""
terms = panther_result.get('data', {}).get('enriched_terms', [])
sig = [t for t in terms if t.get('fdr', 1) < fdr_cutoff]
if len(sig) == 0:
return f"## {title}\n\nNo significant terms found at FDR < {fdr_cutoff}\n"
# Sort by FDR
sig = sorted(sig, key=lambda x: x.get('fdr', 1))[:top_n]
# Build markdown
lines = [f"## {title}\n"]
lines.append(f"**Total significant terms**: {len([t for t in terms if t.get('fdr', 1) < fdr_cutoff])}")
lines.append(f"**Showing top**: {len(sig)}\n")
# Table header
lines.append("| Rank | Term ID | Term | P-value | FDR | Fold Enrichment | Count |")
lines.append("|------|---------|------|---------|-----|-----------------|-------|")
# Table rows
for idx, term in enumerate(sig, 1):
term_id = term.get('term_id', 'N/A')
term_label = term.get('term_label', 'N/A')
pval = f"{term.get('pvalue', 1):.2e}"
fdr = f"{term.get('fdr', 1):.2e}"
fold = f"{term.get('fold_enrichment', 0):.2f}"
count = f"{term.get('number_in_list', 0)}/{term.get('number_in_reference', 0)}"
lines.append(f"| {idx} | {term_id} | {term_label} | {pval} | {fdr} | {fold} | {count} |")
return "\n".join(lines) + "\n"
def format_string_results(
string_result: Dict,
category: str = 'Process',
top_n: int = 10,
title: Optional[str] = None,
fdr_cutoff: float = 0.05
) -> str:
"""
Format STRING_functional_enrichment results as markdown table.
Args:
string_result: Result from tu.tools.STRING_functional_enrichment()
category: Category to filter ('Process', 'Function', 'Component', 'KEGG', 'Reactome')
top_n: Number of top terms to include
title: Table title (auto-generated if None)
fdr_cutoff: FDR cutoff for filtering
Returns:
Markdown formatted table string
"""
if title is None:
title = f"STRING {category} Enrichment"
data = string_result.get('data', [])
if not isinstance(data, list):
return f"## {title}\n\nNo results returned\n"
# Filter by category
filtered = [d for d in data if d.get('category') == category]
sig = [d for d in filtered if d.get('fdr', 1) < fdr_cutoff]
if len(sig) == 0:
return f"## {title}\n\nNo significant terms found at FDR < {fdr_cutoff}\n"
# Sort by FDR
sig = sorted(sig, key=lambda x: x.get('fdr', 1))[:top_n]
# Build markdown
lines = [f"## {title}\n"]
lines.append(f"**Total significant terms**: {len([d for d in filtered if d.get('fdr', 1) < fdr_cutoff])}")
lines.append(f"**Showing top**: {len(sig)}\n")
# Table header
lines.append("| Rank | Term | Description | P-value | FDR | Count |")
lines.append("|------|------|-------------|---------|-----|-------|")
# Table rows
for idx, item in enumerate(sig, 1):
term = item.get('term', 'N/A')
desc = item.get('description', 'N/A')
# Truncate description
if len(desc) > 50:
desc = desc[:47] + "..."
pval = f"{item.get('p_value', 1):.2e}"
fdr = f"{item.get('fdr', 1):.2e}"
count = f"{item.get('number_of_genes', 0)}/{item.get('number_of_genes_in_background', 0)}"
lines.append(f"| {idx} | {term} | {desc} | {pval} | {fdr} | {count} |")
return "\n".join(lines) + "\n"
def format_reactome_results(
reactome_result: Dict,
top_n: int = 10,
title: str = "Reactome Pathway Enrichment",
fdr_cutoff: float = 0.05
) -> str:
"""
Format ReactomeAnalysis_pathway_enrichment results as markdown table.
Args:
reactome_result: Result from tu.tools.ReactomeAnalysis_pathway_enrichment()
top_n: Number of top terms to include
title: Table title
fdr_cutoff: FDR cutoff for filtering
Returns:
Markdown formatted table string
"""
pathways = reactome_result.get('data', {}).get('pathways', [])
sig = [p for p in pathways if p.get('fdr', 1) < fdr_cutoff]
if len(sig) == 0:
return f"## {title}\n\nNo significant pathways found at FDR < {fdr_cutoff}\n"
# Sort by FDR
sig = sorted(sig, key=lambda x: x.get('fdr', 1))[:top_n]
# Build markdown
lines = [f"## {title}\n"]
lines.append(f"**Total significant pathways**: {len([p for p in pathways if p.get('fdr', 1) < fdr_cutoff])}")
lines.append(f"**Showing top**: {len(sig)}\n")
# Table header
lines.append("| Rank | Pathway ID | Name | P-value | FDR | Entities Found/Total |")
lines.append("|------|-----------|------|---------|-----|---------------------|")
# Table rows
for idx, pathway in enumerate(sig, 1):
pathway_id = pathway.get('pathway_id', 'N/A')
name = pathway.get('name', 'N/A')
# Truncate name
if len(name) > 50:
name = name[:47] + "..."
pval = f"{pathway.get('p_value', 1):.2e}"
fdr = f"{pathway.get('fdr', 1):.2e}"
entities = f"{pathway.get('entities_found', 0)}/{pathway.get('entities_total', 0)}"
lines.append(f"| {idx} | {pathway_id} | {name} | {pval} | {fdr} | {entities} |")
return "\n".join(lines) + "\n"
def format_cross_validation_table(
gseapy_result,
panther_result: Optional[Dict] = None,
string_result: Optional[Dict] = None,
top_n: int = 20,
fdr_cutoff: float = 0.05
) -> str:
"""
Create cross-validation table comparing results from multiple tools.
Args:
gseapy_result: Result from gseapy.enrichr()
panther_result: Result from PANTHER_enrichment (optional)
string_result: Result from STRING_functional_enrichment (optional)
top_n: Number of terms to include
fdr_cutoff: FDR cutoff for filtering
Returns:
Markdown formatted comparison table
"""
# Extract GO IDs and FDRs from gseapy
gseapy_dict = {}
for _, row in gseapy_result.results.iterrows():
if row['Adjusted P-value'] < fdr_cutoff:
match = re.search(r'(GO:\d+)', row['Term'])
if match:
go_id = match.group(1)
gseapy_dict[go_id] = {
'term': row['Term'].split('(GO:')[0].strip(),
'fdr': row['Adjusted P-value']
}
# Extract from PANTHER
panther_dict = {}
if panther_result:
terms = panther_result.get('data', {}).get('enriched_terms', [])
for term in terms:
if term.get('fdr', 1) < fdr_cutoff:
go_id = term.get('term_id', '')
if go_id.startswith('GO:'):
panther_dict[go_id] = {
'term': term.get('term_label', ''),
'fdr': term.get('fdr', 1)
}
# Extract from STRING
string_dict = {}
if string_result:
data = string_result.get('data', [])
if isinstance(data, list):
for item in data:
if item.get('category') == 'Process' and item.get('fdr', 1) < fdr_cutoff:
go_id = item.get('term', '')
if go_id.startswith('GO:'):
string_dict[go_id] = {
'term': item.get('description', ''),
'fdr': item.get('fdr', 1)
}
# Combine all GO IDs
all_go_ids = set(gseapy_dict.keys()) | set(panther_dict.keys()) | set(string_dict.keys())
# Build comparison rows
rows = []
for go_id in all_go_ids:
sources = []
if go_id in gseapy_dict: sources.append('gseapy')
if go_id in panther_dict: sources.append('PANTHER')
if go_id in string_dict: sources.append('STRING')
# Only include if in 2+ sources (consensus)
if len(sources) >= 2:
term = (gseapy_dict.get(go_id, {}).get('term') or
panther_dict.get(go_id, {}).get('term') or
string_dict.get(go_id, {}).get('term', 'Unknown'))
gseapy_fdr = f"{gseapy_dict[go_id]['fdr']:.2e}" if go_id in gseapy_dict else "-"
panther_fdr = f"{panther_dict[go_id]['fdr']:.2e}" if go_id in panther_dict else "-"
string_fdr = f"{string_dict[go_id]['fdr']:.2e}" if go_id in string_dict else "-"
consensus = f"{len(sources)}/3 ✓" if len(sources) == 3 else f"{len(sources)}/3"
rows.append({
'go_id': go_id,
'term': term,
'gseapy_fdr': gseapy_fdr,
'panther_fdr': panther_fdr,
'string_fdr': string_fdr,
'consensus': consensus,
'n_sources': len(sources)
})
# Sort by number of sources (descending), then by gseapy FDR
rows = sorted(rows, key=lambda x: (-x['n_sources'], x['gseapy_fdr']))[:top_n]
if len(rows) == 0:
return "## Cross-Validation\n\nNo consensus terms found (present in 2+ sources)\n"
# Build markdown
lines = ["## Cross-Validation Results\n"]
lines.append(f"**Consensus terms** (present in 2+ sources): {len(rows)}\n")
# Table header
lines.append("| GO ID | Term | gseapy FDR | PANTHER FDR | STRING FDR | Consensus |")
lines.append("|-------|------|-----------|-------------|-----------|-----------|")
# Table rows
for row in rows:
term = row['term']
if len(term) > 40:
term = term[:37] + "..."
lines.append(f"| {row['go_id']} | {term} | {row['gseapy_fdr']} | "
f"{row['panther_fdr']} | {row['string_fdr']} | {row['consensus']} |")
return "\n".join(lines) + "\n"
if __name__ == "__main__":
print("Enrichment output formatting functions loaded.")
print("Import and use in your analysis scripts:")
print(" from format_enrichment_output import format_ora_results, format_gsea_results")