gpath2vec 3.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gpath2vec/__init__.py +10 -0
- gpath2vec/aucell.py +274 -0
- gpath2vec/cli.py +602 -0
- gpath2vec/compare.py +86 -0
- gpath2vec/ea.py +201 -0
- gpath2vec/embedder.py +554 -0
- gpath2vec/net.py +140 -0
- gpath2vec/utils.py +124 -0
- gpath2vec-3.0.0.dist-info/METADATA +342 -0
- gpath2vec-3.0.0.dist-info/RECORD +14 -0
- gpath2vec-3.0.0.dist-info/WHEEL +5 -0
- gpath2vec-3.0.0.dist-info/entry_points.txt +2 -0
- gpath2vec-3.0.0.dist-info/licenses/LICENSE +21 -0
- gpath2vec-3.0.0.dist-info/top_level.txt +1 -0
gpath2vec/ea.py
ADDED
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
"""pathway enrichment via fisher's exact test with fdr correction."""
|
|
2
|
+
|
|
3
|
+
import io
|
|
4
|
+
import tarfile
|
|
5
|
+
import zipfile
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import pandas as pd
|
|
9
|
+
import scipy.stats as stats
|
|
10
|
+
import statsmodels.stats.multitest as multi
|
|
11
|
+
|
|
12
|
+
from . import utils
|
|
13
|
+
|
|
14
|
+
np.random.seed(1234)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def gene_mappings():
|
|
18
|
+
"""stId -> name + gene list from ReactomePathways.gmt.zip"""
|
|
19
|
+
raw = utils.fetch("ReactomePathways.gmt.zip",
|
|
20
|
+
"https://reactome.org/download/current/ReactomePathways.gmt.zip",
|
|
21
|
+
binary=True)
|
|
22
|
+
if raw is None:
|
|
23
|
+
return []
|
|
24
|
+
with zipfile.ZipFile(io.BytesIO(raw)) as zf:
|
|
25
|
+
lines = zf.open(zf.infolist()[0]).readlines()
|
|
26
|
+
return [
|
|
27
|
+
dict(name=(p := line.decode("utf8").strip().split("\t"))[0],
|
|
28
|
+
stId=p[1], genes=p[2:])
|
|
29
|
+
for line in lines
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def ehld_stids():
|
|
34
|
+
"""high-level pathway stIds (ehld)."""
|
|
35
|
+
text = utils.fetch("svgsummary.txt",
|
|
36
|
+
"https://reactome.org/download/current/ehld/svgsummary.txt")
|
|
37
|
+
return [s for s in (text or "").splitlines() if "R-" in s]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def sbgn_stids():
|
|
41
|
+
"""low-level pathway stIds (sbgn minus ehld)."""
|
|
42
|
+
raw = utils.fetch("homo_sapiens.sbgn.tar.gz",
|
|
43
|
+
"https://reactome.org/download/current/homo_sapiens.sbgn.tar.gz",
|
|
44
|
+
binary=True)
|
|
45
|
+
if raw is None:
|
|
46
|
+
return []
|
|
47
|
+
names = tarfile.open(fileobj=io.BytesIO(raw)).getnames()
|
|
48
|
+
ehlds = set(ehld_stids())
|
|
49
|
+
return [n.replace(".sbgn", "") for n in names if n.replace(".sbgn", "") not in ehlds]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def level_stids(level):
|
|
53
|
+
"""return set of stIds for a given level: high, mid, low, or all."""
|
|
54
|
+
if level == "all":
|
|
55
|
+
return None
|
|
56
|
+
if level == "high":
|
|
57
|
+
return set(ehld_stids())
|
|
58
|
+
if level == "low":
|
|
59
|
+
return set(sbgn_stids())
|
|
60
|
+
if level == "mid":
|
|
61
|
+
all_stids = set(r["stId"] for r in gene_mappings())
|
|
62
|
+
return all_stids - set(ehld_stids()) - set(sbgn_stids())
|
|
63
|
+
raise ValueError(f"unknown level: {level}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def filter_pathways(level="all", gene_filter=None, min_genes=3):
|
|
67
|
+
"""
|
|
68
|
+
filter pathway universe before EA.
|
|
69
|
+
|
|
70
|
+
level: high/mid/low/all
|
|
71
|
+
gene_filter: optional set of genes to restrict which pathways are included
|
|
72
|
+
min_genes: minimum pathway size
|
|
73
|
+
returns: filtered gene_mappings dataframe
|
|
74
|
+
"""
|
|
75
|
+
mappings = gene_mappings()
|
|
76
|
+
if not mappings:
|
|
77
|
+
return pd.DataFrame(columns=["name", "stId", "genes"])
|
|
78
|
+
gm = pd.json_normalize(mappings)
|
|
79
|
+
|
|
80
|
+
stids = level_stids(level)
|
|
81
|
+
if stids is not None:
|
|
82
|
+
gm = gm[gm.stId.isin(stids)]
|
|
83
|
+
|
|
84
|
+
if gene_filter is not None:
|
|
85
|
+
gf = set(gene_filter)
|
|
86
|
+
gm = gm[gm.genes.apply(lambda g: bool(set(g) & gf))]
|
|
87
|
+
|
|
88
|
+
gm = gm[gm.genes.apply(len) > min_genes].copy()
|
|
89
|
+
return gm
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _enrich_chunk(items, u_set, pw_genes, n_universe, n_pw):
|
|
93
|
+
"""fisher + per-cluster BH-FDR for a chunk of (cname, cgenes). pure;
|
|
94
|
+
output order == input order, so parallel == serial bit-identically."""
|
|
95
|
+
out = []
|
|
96
|
+
for cname, cgenes in items:
|
|
97
|
+
cset = set(cgenes) & u_set
|
|
98
|
+
c_in = len(cset)
|
|
99
|
+
c_out = n_universe - c_in
|
|
100
|
+
|
|
101
|
+
rows = []
|
|
102
|
+
for stid, pset in pw_genes.items():
|
|
103
|
+
a = len(cset & pset)
|
|
104
|
+
b = len(pset) - a
|
|
105
|
+
c = c_in - a
|
|
106
|
+
d = c_out - b
|
|
107
|
+
oddsratio, pvalue = stats.fisher_exact([[a, b], [c, d]], alternative="greater")
|
|
108
|
+
rows.append(dict(cluster=str(cname), stId=stid,
|
|
109
|
+
oddsratio=oddsratio, pvalue=pvalue))
|
|
110
|
+
|
|
111
|
+
edf = pd.DataFrame(rows)
|
|
112
|
+
corrected = multi.multipletests(edf.pvalue, alpha=0.05, method="fdr_bh",
|
|
113
|
+
is_sorted=False, returnsorted=False)
|
|
114
|
+
edf["fdr_bh"] = corrected[1]
|
|
115
|
+
edf["sig_pathway"] = corrected[0]
|
|
116
|
+
edf["pathway_adjpvalue"] = -np.log10(edf.fdr_bh) / n_pw
|
|
117
|
+
edf.fillna(value={"pathway_adjpvalue": 1}, inplace=True)
|
|
118
|
+
out.append(edf)
|
|
119
|
+
return out
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def enrich(gene_sets, level="low", gene_filter=None, min_genes=3, n_jobs=1):
|
|
123
|
+
"""
|
|
124
|
+
fisher's exact test per gene-set/pathway pair with fdr correction.
|
|
125
|
+
|
|
126
|
+
gene_sets: {name: [genes]}
|
|
127
|
+
level: high/mid/low/all
|
|
128
|
+
gene_filter: optional set of genes to restrict pathway universe
|
|
129
|
+
min_genes: minimum pathway size to include
|
|
130
|
+
n_jobs: 1 = serial (default, unchanged behavior); >1 or -1 parallelizes
|
|
131
|
+
the per-cluster loop. order-preserving, so the result is
|
|
132
|
+
bit-identical to the serial path.
|
|
133
|
+
returns: dataframe with cluster, stId, name, oddsratio, pvalue, fdr_bh
|
|
134
|
+
"""
|
|
135
|
+
gm = filter_pathways(level=level, gene_filter=gene_filter, min_genes=min_genes)
|
|
136
|
+
if gm.empty:
|
|
137
|
+
return pd.DataFrame()
|
|
138
|
+
|
|
139
|
+
universe = sorted(set(g for genes in gm.genes for g in genes))
|
|
140
|
+
u_set = set(universe)
|
|
141
|
+
pw_genes = {row.stId: set(row.genes) & u_set for _, row in gm.iterrows()}
|
|
142
|
+
n_pw = len(pw_genes)
|
|
143
|
+
n_universe = len(universe)
|
|
144
|
+
|
|
145
|
+
items = list(gene_sets.items())
|
|
146
|
+
if n_jobs == 1 or len(items) <= 1:
|
|
147
|
+
results = _enrich_chunk(items, u_set, pw_genes, n_universe, n_pw)
|
|
148
|
+
else:
|
|
149
|
+
from joblib import Parallel, delayed, cpu_count
|
|
150
|
+
nj = cpu_count() if n_jobs == -1 else n_jobs
|
|
151
|
+
k = max(1, -(-len(items) // (nj * 4))) # ~4 chunks/worker
|
|
152
|
+
chunks = [items[i:i + k] for i in range(0, len(items), k)]
|
|
153
|
+
nested = Parallel(n_jobs=n_jobs)(
|
|
154
|
+
delayed(_enrich_chunk)(ch, u_set, pw_genes, n_universe, n_pw)
|
|
155
|
+
for ch in chunks)
|
|
156
|
+
results = [edf for sub in nested for edf in sub]
|
|
157
|
+
|
|
158
|
+
ea_df = pd.concat(results, ignore_index=True)
|
|
159
|
+
ea_df["name"] = ea_df.stId.map(gm.set_index("stId")["name"])
|
|
160
|
+
return ea_df
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def aggregate_min_fdr(ea_records):
|
|
164
|
+
"""min-FDR per pathway across all niches; the correct sig/notsig basis.
|
|
165
|
+
|
|
166
|
+
accepts records in either shape:
|
|
167
|
+
- {"stId": ..., "fdr_bh": ...} (long-form ea_df rows)
|
|
168
|
+
- {"stId": ..., "entities": {"fdr": ...}} (Net.enrichment format)
|
|
169
|
+
pandas Series rows (from ea_df.iterrows()) also work via .get().
|
|
170
|
+
returns records in Net.enrichment format.
|
|
171
|
+
"""
|
|
172
|
+
best = {}
|
|
173
|
+
for r in ea_records:
|
|
174
|
+
stid = r["stId"]
|
|
175
|
+
fdr = r.get("fdr_bh", r.get("entities", {}).get("fdr", 1.0))
|
|
176
|
+
if stid not in best or fdr < best[stid]:
|
|
177
|
+
best[stid] = fdr
|
|
178
|
+
return [{"stId": s, "entities": {"fdr": f}} for s, f in best.items()]
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def ea_matrix(ea_df, weight="fdr", sig_only=True):
|
|
182
|
+
"""
|
|
183
|
+
pivot enrichment results into cluster x pathway matrix.
|
|
184
|
+
|
|
185
|
+
weight: "fdr" (uses 1 - fdr_bh) or "oddsratio"
|
|
186
|
+
sig_only: if true, zero out non-significant entries
|
|
187
|
+
returns: dataframe with clusters as rows, pathway stIds as columns
|
|
188
|
+
"""
|
|
189
|
+
df = ea_df.copy()
|
|
190
|
+
if weight == "fdr":
|
|
191
|
+
df["value"] = 1 - df.fdr_bh
|
|
192
|
+
elif weight == "oddsratio":
|
|
193
|
+
df["value"] = df.oddsratio
|
|
194
|
+
else:
|
|
195
|
+
raise ValueError(f"unknown weight: {weight}, expected fdr/oddsratio")
|
|
196
|
+
|
|
197
|
+
if sig_only:
|
|
198
|
+
df.loc[~df.sig_pathway, "value"] = 0.0
|
|
199
|
+
|
|
200
|
+
matrix = df.pivot_table(index="cluster", columns="stId", values="value", fill_value=0.0)
|
|
201
|
+
return matrix
|