Skill Tracer
Skills › Scientific Agent Skills › pathway-enrichment › gseapy.md

gseapy.md

195 lines · skills/pathway-enrichment/references/gseapy.md at 5f2bc03

Open on GitHub
1# GSEApy reference (tested 1.3.1)
2
3Python/Rust implementation, BSD-3-Clause. These patterns require your own validated
4inputs; offline numerical/plotting APIs were exercised with synthetic data. See
5[verified-api.md](verified-api.md) for online verification and service limitations.
6

GSEApy reference (tested 1.3.1)

Python/Rust implementation, BSD-3-Clause. These patterns require your own validated inputs; offline numerical/plotting APIs were exercised with synthetic data. See verified-api.md for online verification and service limitations.

Raw lines 1 to 6
7## ORA: online and local
8
9```python
10import gseapy as gp
11# genes and tested_genes must already use the library's namespace/species.
12enr = gp.enrichr(
13 gene_list=genes, gene_sets=["MSigDB_Hallmark_2020"], organism="human",
14 background=tested_genes, outdir=None,
16results = enr.results # all submitted libraries
17```
19In 1.3.1, named libraries with a nonempty iterable background use **Speedrichr**.
20Without that background, the standard Enrichr service supplies its default.
21The public wrapper docstring still says online backgrounds are ignored; released
22`enrich_online()` code contradicts that and was tested with mocked transport.
23A numeric background count does not select the custom-background route.
24Speedrichr's base URL is not organism-specific; use local GMT for custom
25backgrounds with fly/fish/worm/yeast rather than assuming the routing is correct.
27For offline analysis, pass a GMT/dict **and explicit gene universe**:
29```python
30sets = gp.read_gmt("pathways.symbols.gmt")
31assert set(genes) <= set(tested_genes)
32enr = gp.enrich(gene_list=genes, gene_sets=sets,
33 background=tested_genes, outdir=None)
34```
36The local one-sided hypergeometric test intersects each set and query with the
37background. `Overlap` is hits / set size after intersection. It returns only
38terms with at least one hit and applies BH over those returned terms, not every
39zero-hit term in the GMT. If the planned testing family includes all eligible
40terms, retain zero-hit tests with p=1 and recompute BH over that full family.
41Its `Odds Ratio` includes a 0.5 continuity correction in all four cells; this
42need not equal the online service's odds ratio. `Combined Score` is a ranking
43heuristic, not an adjusted p-value or biological effect size.
45`enr.results` can remain a list when no terms match. Check that it is a DataFrame
46before filtering. Standard export, Speedrichr and local results have different
47columns: Speedrichr JSON does not include `Overlap`; do not require it universally.
48`outdir=None` suppresses result files, but online library downloads may still cache.

ORA: online and local

import gseapy as gp
# genes and tested_genes must already use the library's namespace/species.
enr = gp.enrichr(
    gene_list=genes, gene_sets=["MSigDB_Hallmark_2020"], organism="human",
    background=tested_genes, outdir=None,
)
results = enr.results                  # all submitted libraries

In 1.3.1, named libraries with a nonempty iterable background use Speedrichr. Without that background, the standard Enrichr service supplies its default. The public wrapper docstring still says online backgrounds are ignored; released enrich_online() code contradicts that and was tested with mocked transport. A numeric background count does not select the custom-background route. Speedrichr's base URL is not organism-specific; use local GMT for custom backgrounds with fly/fish/worm/yeast rather than assuming the routing is correct.

For offline analysis, pass a GMT/dict and explicit gene universe:

sets = gp.read_gmt("pathways.symbols.gmt")
assert set(genes) <= set(tested_genes)
enr = gp.enrich(gene_list=genes, gene_sets=sets,
                background=tested_genes, outdir=None)

The local one-sided hypergeometric test intersects each set and query with the background. Overlap is hits / set size after intersection. It returns only terms with at least one hit and applies BH over those returned terms, not every zero-hit term in the GMT. If the planned testing family includes all eligible terms, retain zero-hit tests with p=1 and recompute BH over that full family. Its Odds Ratio includes a 0.5 continuity correction in all four cells; this need not equal the online service's odds ratio. Combined Score is a ranking heuristic, not an adjusted p-value or biological effect size.

enr.results can remain a list when no terms match. Check that it is a DataFrame before filtering. Standard export, Speedrichr and local results have different columns: Speedrichr JSON does not include Overlap; do not require it universally. outdir=None suppresses result files, but online library downloads may still cache.

Raw lines 7 to 49
50## Preranked GSEA
52```python
53pre = gp.prerank(
54 rnk=rnk, # unique IDs, finite signed scores, all tested genes
55 gene_sets="pathways.symbols.gmt",
56 organism="human", # only routes named Enrichr libraries
57 min_size=15, max_size=500, # matched size after intersection with rank
58 method="permutation", permutation_num=1000,
59 weight=1.0, seed=123, threads=4, ascending=None, outdir=None,
61results = pre.res2d
62```
64Sort `rnk` descending first; `ascending=None` preserves that order, including ties.
65The helper uses a stable sort and reports ties. GSEApy itself can uppercase
66inputs when sampled gene sets are uppercase; this heuristic is not orthology.
67Verify matching IDs beforehand and inspect matched/leading genes afterward. A seed cannot make ambiguous
68identifier mapping or arbitrary tied ranks scientifically valid. Check sensitivity
69when many ranks tie. Never let upstream duplicate renaming (`GENE_1`) stand in for
70resolving multiple probes/transcripts. Do not feed an unsigned DESeq2 LRT statistic
71when interpreting positive/negative enrichment; use the signed contrast statistic.
73Classic permutation results include `ES`, `NES`, `NOM p-val`, `FDR q-val`,
74`FWER p-val`, `Tag %`, `Gene %`, `Lead_genes`. FDR is based on NES nulls and is
75scoped by the prefix before `__` in 1.3.1. GSEApy prefixes combined libraries;
76avoid `__` inside custom term names unless that grouping is intentional. A single
77dict without such prefixes defines one family. The hosted API prose describing
78pooled FDR across all named libraries is stale relative to the 1.2.1+ source.
80`method="multilevel"` is a separate, optional backend in 1.3.x: supports one
81ranked list, `sample_size=101`, `eps=1e-50`, plus `permutation_num` for simple-null
82normalization. It estimates smaller tail p-values, uses fgsea-style NES, applies
83**BH across all tested terms**, reports `log2err`, and omits `FWER p-val`.
84Do not mix its q-values/NES with the classic method without identifying the
85backend. The bundled helper deliberately uses `method="permutation"`. In a 1.3.1 native
86smoke, extreme sets still returned nominal p=0 despite release notes describing
87a p-value floor. Interpret zero as no observed exceedances at the chosen
88permutation resolution, never as a mathematically zero probability.

Preranked GSEA

pre = gp.prerank(
    rnk=rnk,                          # unique IDs, finite signed scores, all tested genes
    gene_sets="pathways.symbols.gmt",
    organism="human",                # only routes named Enrichr libraries
    min_size=15, max_size=500,        # matched size after intersection with rank
    method="permutation", permutation_num=1000,
    weight=1.0, seed=123, threads=4, ascending=None, outdir=None,
)
results = pre.res2d

Sort rnk descending first; ascending=None preserves that order, including ties. The helper uses a stable sort and reports ties. GSEApy itself can uppercase inputs when sampled gene sets are uppercase; this heuristic is not orthology. Verify matching IDs beforehand and inspect matched/leading genes afterward. A seed cannot make ambiguous identifier mapping or arbitrary tied ranks scientifically valid. Check sensitivity when many ranks tie. Never let upstream duplicate renaming (GENE_1) stand in for resolving multiple probes/transcripts. Do not feed an unsigned DESeq2 LRT statistic when interpreting positive/negative enrichment; use the signed contrast statistic.

Classic permutation results include ES, NES, NOM p-val, FDR q-val, FWER p-val, Tag %, Gene %, Lead_genes. FDR is based on NES nulls and is scoped by the prefix before __ in 1.3.1. GSEApy prefixes combined libraries; avoid __ inside custom term names unless that grouping is intentional. A single dict without such prefixes defines one family. The hosted API prose describing pooled FDR across all named libraries is stale relative to the 1.2.1+ source.

method="multilevel" is a separate, optional backend in 1.3.x: supports one ranked list, sample_size=101, eps=1e-50, plus permutation_num for simple-null normalization. It estimates smaller tail p-values, uses fgsea-style NES, applies BH across all tested terms, reports log2err, and omits FWER p-val. Do not mix its q-values/NES with the classic method without identifying the backend. The bundled helper deliberately uses method="permutation". In a 1.3.1 native smoke, extreme sets still returned nominal p=0 despite release notes describing a p-value floor. Interpret zero as no observed exceedances at the chosen permutation resolution, never as a mathematically zero probability.

Raw lines 50 to 89
90## Standard GSEA (matrix and independent class labels)
92```python
93result = gp.gsea(
94 data=expr_df, gene_sets="pathways.symbols.gmt", # genes x samples
95 cls=["A"] * 7 + ["B"] * 7, # aligned with columns
96 permutation_type="phenotype", method="signal_to_noise",
97 min_size=15, max_size=500, permutation_num=1000,
98 seed=123, threads=4, outdir=None,
100```
102Phenotype permutation requires exchangeable independent biological samples;
103paired/block/confounded designs need a design-aware analysis upstream, not
104unrestricted label shuffling. Signal-to-noise and t-test metrics need at least
105three samples per phenotype; the GSEA guide recommends at least seven for
106phenotype permutation. For smaller groups, gene-set permutations change the
107null and fail to preserve gene correlation; disclose that limitation. Do not
108pretend thousands of cells are independent biological replicates.

Standard GSEA (matrix and independent class labels)

result = gp.gsea(
    data=expr_df, gene_sets="pathways.symbols.gmt",  # genes x samples
    cls=["A"] * 7 + ["B"] * 7,                   # aligned with columns
    permutation_type="phenotype", method="signal_to_noise",
    min_size=15, max_size=500, permutation_num=1000,
    seed=123, threads=4, outdir=None,
)

Phenotype permutation requires exchangeable independent biological samples; paired/block/confounded designs need a design-aware analysis upstream, not unrestricted label shuffling. Signal-to-noise and t-test metrics need at least three samples per phenotype; the GSEA guide recommends at least seven for phenotype permutation. For smaller groups, gene-set permutations change the null and fail to preserve gene correlation; disclose that limitation. Do not pretend thousands of cells are independent biological replicates.

Raw lines 90 to 109
110## ssGSEA and GSVA
112```python
113ss = gp.ssgsea(data=expr_df, gene_sets="pathways.symbols.gmt",
114 sample_norm_method="rank", min_size=15, max_size=500,
115 permutation_num=0, outdir=None, threads=4)
116scores = ss.res2d.pivot(index="Term", columns="Name", values="NES")
117gsva = gp.gsva(data=expr_df, gene_sets="pathways.symbols.gmt",
118 kcdf="Gaussian", min_size=15, max_size=500, outdir=None)
119```
121These are sample-level scores, not differential-enrichment p-values. ssGSEA NES
122normalization depends on the submitted score range, so separately processed
123cohorts need not be comparable. Choose GSVA `kcdf` for the supplied scale:
124Gaussian for continuous/log expression, Poisson for nonnegative counts, or None
125for direct ECDF. Do not call pathway-membership scores evidence of activation;
126use signed responsive signatures and a study design when that is the question.

ssGSEA and GSVA

ss = gp.ssgsea(data=expr_df, gene_sets="pathways.symbols.gmt",
               sample_norm_method="rank", min_size=15, max_size=500,
               permutation_num=0, outdir=None, threads=4)
scores = ss.res2d.pivot(index="Term", columns="Name", values="NES")
gsva = gp.gsva(data=expr_df, gene_sets="pathways.symbols.gmt",
               kcdf="Gaussian", min_size=15, max_size=500, outdir=None)

These are sample-level scores, not differential-enrichment p-values. ssGSEA NES normalization depends on the submitted score range, so separately processed cohorts need not be comparable. Choose GSVA kcdf for the supplied scale: Gaussian for continuous/log expression, Poisson for nonnegative counts, or None for direct ECDF. Do not call pathway-membership scores evidence of activation; use signed responsive signatures and a study design when that is the question.

Raw lines 110 to 127
128## Library discovery and GMTs
130```python
131names = gp.get_library_name(organism="human")
132lib = gp.get_library("MSigDB_Hallmark_2020", organism="human")
133sets = gp.read_gmt("pathways.symbols.gmt")
134```
136`get_library` defaults to `min_size=0, max_size=2000`, so it can omit large terms;
137choose and record these limits deliberately. The 2026-10-01 human catalog included
138`GO_Biological_Process_2026`, `KEGG_2026`, `Reactome_Pathways_2024`,
139`WikiPathways_2024_Human`, plus older versioned names. Names with a year do not
140replace storing a downloaded GMT and its hash. Discovery verifies availability,
141not species compatibility or curation quality.
143```python
144# lxml is needed by pandas.read_html used for the MSigDB directory listings.
145versions = gp.Msigdb.list_dbver() # DataFrame, not a plain list
146categories = gp.Msigdb.list_category(dbver="2026.1.Hs")
147hallmark = gp.Msigdb.get_gmt(category="h.all", dbver="2026.1.Hs")
148assert hallmark and len(hallmark) == 50
149# Mouse collection codes differ: use mh.all / m* categories from its own catalog.
150```
152`entrez=True` fetches Entrez rather than symbols. Directory listings can contain
153non-release entries (currently `msigdb_releases.json`); inspect `Name` values,
154not just the last row. MSigDB's website requests registration and license
155compliance; public SDK-accessible GMTs do not waive those terms.

Library discovery and GMTs

names = gp.get_library_name(organism="human")
lib = gp.get_library("MSigDB_Hallmark_2020", organism="human")
sets = gp.read_gmt("pathways.symbols.gmt")

get_library defaults to min_size=0, max_size=2000, so it can omit large terms; choose and record these limits deliberately. The 2026-10-01 human catalog included GO_Biological_Process_2026, KEGG_2026, Reactome_Pathways_2024, WikiPathways_2024_Human, plus older versioned names. Names with a year do not replace storing a downloaded GMT and its hash. Discovery verifies availability, not species compatibility or curation quality.

# lxml is needed by pandas.read_html used for the MSigDB directory listings.
versions = gp.Msigdb.list_dbver()           # DataFrame, not a plain list
categories = gp.Msigdb.list_category(dbver="2026.1.Hs")
hallmark = gp.Msigdb.get_gmt(category="h.all", dbver="2026.1.Hs")
assert hallmark and len(hallmark) == 50
# Mouse collection codes differ: use mh.all / m* categories from its own catalog.

entrez=True fetches Entrez rather than symbols. Directory listings can contain non-release entries (currently msigdb_releases.json); inspect Name values, not just the last row. MSigDB's website requests registration and license compliance; public SDK-accessible GMTs do not waive those terms.

Raw lines 128 to 156
157## BioMart identifiers
159```python
160bm = gp.Biomart()
161attrs = ["ensembl_gene_id", "external_gene_name"]
162conv = bm.query(dataset="hsapiens_gene_ensembl", attributes=attrs,
163 filters={"ensembl_gene_id": ensembl_ids})
164if conv is None or list(conv.columns) != attrs:
165 raise RuntimeError("BioMart did not return the requested mapping schema")
166# Inspect empty symbols, duplicate source IDs, and one-to-many mappings here.
167```
169The default host returned service-unavailable HTML parsed as a one-column table
170during review. HTTP success/nonempty output alone is insufficient. Pin an archive
171host and release when available; do not accept a different species/assembly just
172to obtain a response. g:Profiler/MyGene are alternatives with different mapping
173semantics, described in [databases-and-gene-sets.md](databases-and-gene-sets.md).

BioMart identifiers

bm = gp.Biomart()
attrs = ["ensembl_gene_id", "external_gene_name"]
conv = bm.query(dataset="hsapiens_gene_ensembl", attributes=attrs,
                filters={"ensembl_gene_id": ensembl_ids})
if conv is None or list(conv.columns) != attrs:
    raise RuntimeError("BioMart did not return the requested mapping schema")
# Inspect empty symbols, duplicate source IDs, and one-to-many mappings here.

The default host returned service-unavailable HTML parsed as a one-column table during review. HTTP success/nonempty output alone is insufficient. Pin an archive host and release when available; do not accept a different species/assembly just to obtain a response. g:Profiler/MyGene are alternatives with different mapping semantics, described in databases-and-gene-sets.md.

Raw lines 157 to 174
175## Plotting and outputs
177```python
178ax = gp.dotplot(enr.results, column="Adjusted P-value", top_term=15, cutoff=0.05)
179ax.get_figure().savefig("ora.png", dpi=200, bbox_inches="tight")
180gp.barplot(enr.results, column="Adjusted P-value", ofname="bar.png")
181gp.dotplot(pre.res2d, column="FDR q-val", ofname="gsea.png")
182term = pre.res2d.Term.iloc[0]
183gp.gseaplot(term=term, rank_metric=pre.ranking, ofname="running.png",
184 **pre.results[term])
185nodes, edges = gp.enrichment_map(pre.res2d, column="FDR q-val", cutoff=0.05)
186```
188`enrichment_map` returns **two DataFrames**, not a NetworkX graph. Use
189`networkx.from_pandas_edgelist(edges, source="src_idx", target="targ_idx", edge_attr=True)`
190and attach term labels from `nodes`. Without `ofname`, dotplot/barplot return
191Axes; with `ofname` they save and may return None. Use `dotplot(show_ring=True)`;
192there is no top-level `gp.ringplot` in 1.3.1. `gseaplot2` takes lists of terms,
193hit-index lists and running-score vectors; `heatmap` takes a numeric DataFrame.
194A cutoff with no matching terms can raise; report no significant result instead
195of silently plotting nonsignificant terms as discoveries.

Plotting and outputs

ax = gp.dotplot(enr.results, column="Adjusted P-value", top_term=15, cutoff=0.05)
ax.get_figure().savefig("ora.png", dpi=200, bbox_inches="tight")
gp.barplot(enr.results, column="Adjusted P-value", ofname="bar.png")
gp.dotplot(pre.res2d, column="FDR q-val", ofname="gsea.png")
term = pre.res2d.Term.iloc[0]
gp.gseaplot(term=term, rank_metric=pre.ranking, ofname="running.png",
            **pre.results[term])
nodes, edges = gp.enrichment_map(pre.res2d, column="FDR q-val", cutoff=0.05)

enrichment_map returns two DataFrames, not a NetworkX graph. Use networkx.from_pandas_edgelist(edges, source="src_idx", target="targ_idx", edge_attr=True) and attach term labels from nodes. Without ofname, dotplot/barplot return Axes; with ofname they save and may return None. Use dotplot(show_ring=True); there is no top-level gp.ringplot in 1.3.1. gseaplot2 takes lists of terms, hit-index lists and running-score vectors; heatmap takes a numeric DataFrame. A cutoff with no matching terms can raise; report no significant result instead of silently plotting nonsignificant terms as discoveries.

Raw lines 175 to 195