ptmreloc 0.1.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.
- ptmreloc/__init__.py +50 -0
- ptmreloc/api.py +268 -0
- ptmreloc/cli.py +199 -0
- ptmreloc/database.py +320 -0
- ptmreloc/defaults.py +26 -0
- ptmreloc/engine.py +82 -0
- ptmreloc/features.py +263 -0
- ptmreloc/metrics.py +104 -0
- ptmreloc/optimize.py +216 -0
- ptmreloc/peptide.py +85 -0
- ptmreloc/reassign.py +132 -0
- ptmreloc/reference.py +155 -0
- ptmreloc-0.1.0.dist-info/METADATA +244 -0
- ptmreloc-0.1.0.dist-info/RECORD +18 -0
- ptmreloc-0.1.0.dist-info/WHEEL +5 -0
- ptmreloc-0.1.0.dist-info/entry_points.txt +2 -0
- ptmreloc-0.1.0.dist-info/licenses/LICENSE +21 -0
- ptmreloc-0.1.0.dist-info/top_level.txt +1 -0
ptmreloc/__init__.py
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""ptmreloc — prior-knowledge guided relocalization of low-confidence PTM sites.
|
|
2
|
+
|
|
3
|
+
Quick start. The human reference database (PhosphoSitePlus, Gold Standard, Functional
|
|
4
|
+
Landscape, UniProt FASTA) is downloaded once on first use, unless you pass your own tables::
|
|
5
|
+
|
|
6
|
+
import ptmreloc as pr
|
|
7
|
+
|
|
8
|
+
out = pr.reassign(peptides) # default weights, default PSP
|
|
9
|
+
res = pr.optimize(peptides, out_dir="opt/") # optimize against default GS/FUN
|
|
10
|
+
out = pr.reassign(peptides, weights=res) # or weights="opt/weights.json"
|
|
11
|
+
rep = pr.evaluate(peptides, weights=res); print(rep.summary())
|
|
12
|
+
|
|
13
|
+
# own data: any of psp= / gold= / fun= / fasta= (DataFrame or file path)
|
|
14
|
+
out = pr.reassign(peptides, psp="my_psp.tsv")
|
|
15
|
+
|
|
16
|
+
The lower-level building blocks (``build_candidate_table``, ``choose``,
|
|
17
|
+
``evaluate_selection``, ``optimize_weights``, ``Reassigner``) are exported too.
|
|
18
|
+
"""
|
|
19
|
+
from .api import EvaluationReport, compare_weights, evaluate, load_reference, optimize, reassign, resolve_weights
|
|
20
|
+
from .database import (DATABASE_FILES, annotate_residues, cache_dir, database_dir, database_file,
|
|
21
|
+
database_info, default_proteome, default_reference, download_database, read_fasta)
|
|
22
|
+
from .defaults import DEFAULT_THRESHOLD, DEFAULT_WEIGHTS, WEIGHT_PRESETS
|
|
23
|
+
from .engine import choose, site_scores
|
|
24
|
+
from .features import FEATURE_NAMES, WEIGHT_NAMES, CandidateTable, FeatureConfig, build_candidate_table
|
|
25
|
+
from .metrics import composite_score, evaluate_by_ascore, trial_relative_composite
|
|
26
|
+
from .metrics import evaluate as evaluate_selection
|
|
27
|
+
from .optimize import OptimizationResult, normalize_weights, optimize_weights
|
|
28
|
+
from .peptide import format_residue, parse_peptide, parse_residue, relabel
|
|
29
|
+
from .reassign import Reassigner, apply_selection, load_weights
|
|
30
|
+
from .reference import ReferenceDB, read_table, write_table
|
|
31
|
+
|
|
32
|
+
__version__ = "0.1.0"
|
|
33
|
+
|
|
34
|
+
__all__ = [
|
|
35
|
+
# high-level API
|
|
36
|
+
"optimize", "reassign", "evaluate", "compare_weights", "load_reference", "resolve_weights",
|
|
37
|
+
"EvaluationReport", "OptimizationResult",
|
|
38
|
+
# reference database
|
|
39
|
+
"DATABASE_FILES", "download_database", "cache_dir", "database_dir", "database_file", "database_info",
|
|
40
|
+
"default_reference",
|
|
41
|
+
"default_proteome", "read_fasta", "annotate_residues",
|
|
42
|
+
# defaults
|
|
43
|
+
"DEFAULT_WEIGHTS", "DEFAULT_THRESHOLD", "WEIGHT_PRESETS",
|
|
44
|
+
# building blocks
|
|
45
|
+
"ReferenceDB", "read_table", "write_table", "FeatureConfig", "CandidateTable", "build_candidate_table",
|
|
46
|
+
"FEATURE_NAMES", "WEIGHT_NAMES", "choose", "site_scores", "evaluate_selection", "evaluate_by_ascore",
|
|
47
|
+
"composite_score", "trial_relative_composite", "optimize_weights", "normalize_weights",
|
|
48
|
+
"Reassigner", "apply_selection", "load_weights", "parse_peptide", "parse_residue", "format_residue",
|
|
49
|
+
"relabel",
|
|
50
|
+
]
|
ptmreloc/api.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""High-level library API: one call each for optimize / reassign / evaluate.
|
|
2
|
+
|
|
3
|
+
Reference data defaults to the human reference database (PhosphoSitePlus,
|
|
4
|
+
Gold Standard, Functional Landscape, UniProt FASTA), downloaded once on first use. To use your own data, pass
|
|
5
|
+
DataFrames or file paths (pkl/csv/tsv/parquet/xlsx) as ``psp=``, ``gold=``, ``fun=``,
|
|
6
|
+
``fasta=``. Anything left as ``None`` falls back to the default file; ``False``
|
|
7
|
+
disables an optional table. A prebuilt :class:`ReferenceDB` can be passed as ``psp``
|
|
8
|
+
(its own GS/FUN sites are used unless ``gold``/``fun`` are given explicitly).
|
|
9
|
+
|
|
10
|
+
If the peptide table has no residue column, residues are annotated from the
|
|
11
|
+
protein sequences (``fasta=``, the default human proteome).
|
|
12
|
+
|
|
13
|
+
Weights (``weights=``) may be any of:
|
|
14
|
+
|
|
15
|
+
* ``None`` – the package defaults (:data:`ptmreloc.DEFAULT_WEIGHTS`)
|
|
16
|
+
* a preset name – ``"medians"``, ``"fun_medians"``, ``"gs_medians"``, ``"htp_only"``
|
|
17
|
+
* a path to a ``weights.json`` written by :func:`optimize` (its threshold and
|
|
18
|
+
feature settings are used too, unless overridden)
|
|
19
|
+
* an :class:`OptimizationResult`
|
|
20
|
+
* a mapping ``{"w_H": ..., "w_L": ..., "w_D": ...}`` or a sequence ``(w_H, w_L, w_D)``
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import copy
|
|
25
|
+
import os
|
|
26
|
+
from dataclasses import dataclass, field
|
|
27
|
+
from typing import Dict, Mapping, Optional, Sequence, Tuple, Union
|
|
28
|
+
|
|
29
|
+
import numpy as np
|
|
30
|
+
import pandas as pd
|
|
31
|
+
|
|
32
|
+
from .database import annotate_residues, database_dir, database_file, default_reference
|
|
33
|
+
from .defaults import DEFAULT_THRESHOLD, DEFAULT_WEIGHTS, WEIGHT_PRESETS
|
|
34
|
+
from .engine import as_weight_vector, choose
|
|
35
|
+
from .features import DEFAULT_COLUMNS, WEIGHT_NAMES, FeatureConfig, build_candidate_table
|
|
36
|
+
from .metrics import DEFAULT_ASCORE_BINS, evaluate as _evaluate_selection, evaluate_by_ascore
|
|
37
|
+
from .optimize import OptimizationResult, optimize_weights
|
|
38
|
+
from .reassign import Reassigner, load_weights
|
|
39
|
+
from .reference import ReferenceDB, TableLike, read_table, write_table
|
|
40
|
+
|
|
41
|
+
RefSpec = Union[None, TableLike, ReferenceDB]
|
|
42
|
+
OptTable = Union[None, bool, TableLike]
|
|
43
|
+
WeightsSpec = Union[None, str, os.PathLike, OptimizationResult, Mapping[str, float], Sequence[float], np.ndarray]
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ------------------------------------------------------------------ helpers
|
|
47
|
+
def resolve_weights(weights: WeightsSpec = None) -> Tuple[Dict[str, float], Optional[float], Optional[FeatureConfig]]:
|
|
48
|
+
"""Turn any accepted weights spec into ``(weights, threshold or None, FeatureConfig or None)``."""
|
|
49
|
+
if weights is None:
|
|
50
|
+
return dict(DEFAULT_WEIGHTS), None, None
|
|
51
|
+
if isinstance(weights, OptimizationResult):
|
|
52
|
+
return dict(weights.weights), weights.threshold, weights.config
|
|
53
|
+
if isinstance(weights, (str, os.PathLike)):
|
|
54
|
+
key = os.fspath(weights)
|
|
55
|
+
if key.lower() in WEIGHT_PRESETS:
|
|
56
|
+
return dict(WEIGHT_PRESETS[key.lower()]), None, None
|
|
57
|
+
if os.path.isfile(key):
|
|
58
|
+
payload = load_weights(key)
|
|
59
|
+
cfg = payload.get("feature_config")
|
|
60
|
+
return dict(payload["weights"]), payload.get("threshold"), FeatureConfig(**cfg) if cfg else None
|
|
61
|
+
raise ValueError(f"{key!r} is neither a weights file nor a preset {sorted(WEIGHT_PRESETS)}")
|
|
62
|
+
w = as_weight_vector(weights)
|
|
63
|
+
return dict(zip(WEIGHT_NAMES, map(float, w))), None, None
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def load_reference(
|
|
67
|
+
psp: RefSpec = None,
|
|
68
|
+
gold: OptTable = None,
|
|
69
|
+
fun: OptTable = None,
|
|
70
|
+
organism: Optional[str] = None,
|
|
71
|
+
database: Optional[Union[str, os.PathLike]] = None,
|
|
72
|
+
) -> ReferenceDB:
|
|
73
|
+
"""Build the reference used for scoring (PSP) and for evaluation (GS, FUN).
|
|
74
|
+
|
|
75
|
+
Each argument may be a DataFrame or file path (your own data), ``None`` (the
|
|
76
|
+
default dataset from ``database`` / the default database folder) or ``False``
|
|
77
|
+
(``gold``/``fun`` only: do not load). If ``psp`` is a :class:`ReferenceDB`, it is
|
|
78
|
+
returned as is, or as a copy extended with explicitly given ``gold``/``fun`` tables.
|
|
79
|
+
|
|
80
|
+
>>> ref = load_reference() # all default
|
|
81
|
+
>>> ref = load_reference(psp="my_psp.tsv") # own PSP, default GS/FUN
|
|
82
|
+
>>> ref = load_reference(gold=False, fun=False) # default PSP only
|
|
83
|
+
"""
|
|
84
|
+
if isinstance(psp, ReferenceDB):
|
|
85
|
+
if gold is None and fun is None or gold is False and fun is False:
|
|
86
|
+
return psp
|
|
87
|
+
ref = copy.copy(psp)
|
|
88
|
+
if gold is not None and gold is not False:
|
|
89
|
+
ref.gold = set(psp.gold)
|
|
90
|
+
ref.load_gold(read_table(gold))
|
|
91
|
+
if fun is not None and fun is not False:
|
|
92
|
+
ref.fun = dict(psp.fun)
|
|
93
|
+
ref.load_fun(read_table(fun))
|
|
94
|
+
return ref
|
|
95
|
+
|
|
96
|
+
if psp is None and (gold is None or gold is False) and (fun is None or fun is False):
|
|
97
|
+
return default_reference(gold=gold is None, fun=fun is None, organism=organism, path=database)
|
|
98
|
+
|
|
99
|
+
def pick(x, name):
|
|
100
|
+
if x is False:
|
|
101
|
+
return None
|
|
102
|
+
return database_file(name, database) if x is None else x
|
|
103
|
+
|
|
104
|
+
return ReferenceDB.from_sources(pick(psp, "psp"), pick(gold, "gold"), pick(fun, "fun"), organism=organism)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _settings(weights, threshold, config):
|
|
108
|
+
w, thr_w, cfg_w = resolve_weights(weights)
|
|
109
|
+
thr = threshold if threshold is not None else (thr_w if thr_w is not None else DEFAULT_THRESHOLD)
|
|
110
|
+
cfg = config if config is not None else (cfg_w if cfg_w is not None else FeatureConfig())
|
|
111
|
+
return w, float(thr), cfg
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _peptides(peptides: TableLike, columns=None, fasta=None, config: Optional[FeatureConfig] = None) -> pd.DataFrame:
|
|
115
|
+
df = read_table(peptides)
|
|
116
|
+
res_col = {**DEFAULT_COLUMNS, **(columns or {})}["residue"]
|
|
117
|
+
if res_col not in df.columns:
|
|
118
|
+
label = config.ptm_label if config is not None else "*"
|
|
119
|
+
df = annotate_residues(df, fasta, columns, label)
|
|
120
|
+
return df
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
# ------------------------------------------------------------------ optimize
|
|
124
|
+
def optimize(
|
|
125
|
+
peptides: TableLike,
|
|
126
|
+
psp: RefSpec = None,
|
|
127
|
+
gold: OptTable = None,
|
|
128
|
+
fun: OptTable = None,
|
|
129
|
+
threshold: float = DEFAULT_THRESHOLD,
|
|
130
|
+
n_samples: int = 2000,
|
|
131
|
+
config: Optional[FeatureConfig] = None,
|
|
132
|
+
columns: Optional[Mapping[str, str]] = None,
|
|
133
|
+
out_dir: Optional[str] = None,
|
|
134
|
+
organism: Optional[str] = None,
|
|
135
|
+
fasta: Optional[Union[TableLike, Mapping[str, str]]] = None,
|
|
136
|
+
**kwargs,
|
|
137
|
+
) -> OptimizationResult:
|
|
138
|
+
"""Optimize (w_H, w_L, w_D) against Gold Standard / Functional Landscape sites.
|
|
139
|
+
|
|
140
|
+
Extra keyword arguments go to :func:`ptmreloc.optimize_weights` (``refine``,
|
|
141
|
+
``holdout_frac``, ``top_frac``, ``gs_weight``, ``log10_bounds``, ``seed``, ``verbose`` ...).
|
|
142
|
+
If ``out_dir`` is given, ``weights.json`` and the trial/evaluation tables are written there.
|
|
143
|
+
|
|
144
|
+
>>> res = ptmreloc.optimize("peptides.tsv") # default reference data
|
|
145
|
+
>>> res = ptmreloc.optimize(df, psp="my_psp.tsv", gold="my_gs.tsv") # own data (FUN default)
|
|
146
|
+
>>> res.weights, res.summary()
|
|
147
|
+
"""
|
|
148
|
+
ref = load_reference(psp, gold, fun, organism)
|
|
149
|
+
if not ref.has_targets:
|
|
150
|
+
raise ValueError("optimize needs Gold Standard and/or Functional Landscape sites (gold=, fun=)")
|
|
151
|
+
cfg = config or FeatureConfig()
|
|
152
|
+
table = build_candidate_table(_peptides(peptides, columns, fasta, cfg), ref, cfg, columns)
|
|
153
|
+
res = optimize_weights(table, threshold=threshold, n_samples=n_samples, **kwargs)
|
|
154
|
+
if out_dir:
|
|
155
|
+
res.save(out_dir)
|
|
156
|
+
return res
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ------------------------------------------------------------------ reassign
|
|
160
|
+
def reassign(
|
|
161
|
+
peptides: TableLike,
|
|
162
|
+
psp: RefSpec = None,
|
|
163
|
+
weights: WeightsSpec = None,
|
|
164
|
+
threshold: Optional[float] = None,
|
|
165
|
+
config: Optional[FeatureConfig] = None,
|
|
166
|
+
columns: Optional[Mapping[str, str]] = None,
|
|
167
|
+
include_candidates: bool = True,
|
|
168
|
+
output: Optional[str] = None,
|
|
169
|
+
organism: Optional[str] = None,
|
|
170
|
+
fasta: Optional[Union[TableLike, Mapping[str, str]]] = None,
|
|
171
|
+
) -> pd.DataFrame:
|
|
172
|
+
"""Relocalize sites of peptides with AScore <= threshold. Only PSP is required.
|
|
173
|
+
|
|
174
|
+
Without ``weights`` the package defaults are used (threshold 30); without
|
|
175
|
+
``psp`` the default PhosphoSitePlus table is used.
|
|
176
|
+
Returns the input table plus ``NewPeptide``, ``NewResidue``, ``Reassigned``,
|
|
177
|
+
``ReassignStatus``, ``OriginalSiteProb``, ``NewSiteProb`` (and ``CandidateSiteProbs``).
|
|
178
|
+
|
|
179
|
+
>>> out = ptmreloc.reassign(df) # default PSP, default weights
|
|
180
|
+
>>> out = ptmreloc.reassign(df, psp="my_psp.tsv", weights="opt/weights.json")
|
|
181
|
+
"""
|
|
182
|
+
w, thr, cfg = _settings(weights, threshold, config)
|
|
183
|
+
ref = load_reference(psp, gold=False, fun=False, organism=organism)
|
|
184
|
+
df = _peptides(peptides, columns, fasta, cfg)
|
|
185
|
+
out = Reassigner(ref, w, thr, cfg, columns).reassign(df, include_candidates)
|
|
186
|
+
if output:
|
|
187
|
+
write_table(out, output)
|
|
188
|
+
return out
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
# ------------------------------------------------------------------ evaluate
|
|
192
|
+
@dataclass
|
|
193
|
+
class EvaluationReport:
|
|
194
|
+
weights: Dict[str, float]
|
|
195
|
+
threshold: float
|
|
196
|
+
overall: Dict[str, float] # metrics on rows with AScore <= threshold
|
|
197
|
+
by_ascore: pd.DataFrame # metrics per AScore bin (selection applied to every bin)
|
|
198
|
+
config: FeatureConfig = field(default_factory=FeatureConfig)
|
|
199
|
+
|
|
200
|
+
def summary(self) -> str:
|
|
201
|
+
keys = ["n_rows", "n_rows_changed", "delta_gs", "random_delta_gs", "oracle_delta_gs", "delta_fun",
|
|
202
|
+
"random_delta_fun", "oracle_delta_fun", "gs_gain", "fun_gain", "composite"]
|
|
203
|
+
head = "\n".join(f" {k:<18}{self.overall[k]:.4g}" for k in keys)
|
|
204
|
+
cols = ["ascore_bin", "n_rows", "frac_rows_changed", "delta_gs", "random_delta_gs", "delta_fun",
|
|
205
|
+
"random_delta_fun", "gs_gain", "fun_gain", "composite"]
|
|
206
|
+
with pd.option_context("display.width", 200, "display.max_columns", 20, "display.precision", 4):
|
|
207
|
+
return (f"weights {self.weights}, AScore <= {self.threshold:g}\n{head}\n\n"
|
|
208
|
+
f"{self.by_ascore[cols].to_string(index=False)}")
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def evaluate(
|
|
212
|
+
peptides: TableLike,
|
|
213
|
+
psp: RefSpec = None,
|
|
214
|
+
gold: OptTable = None,
|
|
215
|
+
fun: OptTable = None,
|
|
216
|
+
weights: WeightsSpec = None,
|
|
217
|
+
threshold: Optional[float] = None,
|
|
218
|
+
bins: Sequence[float] = DEFAULT_ASCORE_BINS,
|
|
219
|
+
config: Optional[FeatureConfig] = None,
|
|
220
|
+
columns: Optional[Mapping[str, str]] = None,
|
|
221
|
+
gs_weight: float = 0.5,
|
|
222
|
+
organism: Optional[str] = None,
|
|
223
|
+
fasta: Optional[Union[TableLike, Mapping[str, str]]] = None,
|
|
224
|
+
) -> EvaluationReport:
|
|
225
|
+
"""Score a weight set against GS/FUN sites, overall and per AScore bin.
|
|
226
|
+
|
|
227
|
+
>>> rep = ptmreloc.evaluate(df) # default data, default weights
|
|
228
|
+
>>> print(rep.summary())
|
|
229
|
+
"""
|
|
230
|
+
w, thr, cfg = _settings(weights, threshold, config)
|
|
231
|
+
ref = load_reference(psp, gold, fun, organism)
|
|
232
|
+
if not ref.has_targets:
|
|
233
|
+
raise ValueError("evaluate needs Gold Standard and/or Functional Landscape sites (gold=, fun=)")
|
|
234
|
+
table = build_candidate_table(_peptides(peptides, columns, fasta, cfg), ref, cfg, columns)
|
|
235
|
+
sel = choose(table, w)
|
|
236
|
+
overall = _evaluate_selection(table, sel, table.r_ascore <= thr, gs_weight)
|
|
237
|
+
by_bin = evaluate_by_ascore(table, sel, bins=bins, gs_weight=gs_weight)
|
|
238
|
+
return EvaluationReport(w, thr, overall, by_bin, cfg)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def compare_weights(
|
|
242
|
+
peptides: TableLike,
|
|
243
|
+
psp: RefSpec = None,
|
|
244
|
+
gold: OptTable = None,
|
|
245
|
+
fun: OptTable = None,
|
|
246
|
+
weight_sets: Optional[Mapping[str, WeightsSpec]] = None,
|
|
247
|
+
threshold: float = DEFAULT_THRESHOLD,
|
|
248
|
+
config: Optional[FeatureConfig] = None,
|
|
249
|
+
columns: Optional[Mapping[str, str]] = None,
|
|
250
|
+
gs_weight: float = 0.5,
|
|
251
|
+
organism: Optional[str] = None,
|
|
252
|
+
fasta: Optional[Union[TableLike, Mapping[str, str]]] = None,
|
|
253
|
+
) -> pd.DataFrame:
|
|
254
|
+
"""Evaluate several weight sets on the same data (features built once).
|
|
255
|
+
|
|
256
|
+
``weight_sets`` defaults to all presets. Returns one row per weight set.
|
|
257
|
+
All sets share ``threshold`` and ``config``.
|
|
258
|
+
"""
|
|
259
|
+
weight_sets = weight_sets if weight_sets is not None else {k: k for k in WEIGHT_PRESETS}
|
|
260
|
+
ref = load_reference(psp, gold, fun, organism)
|
|
261
|
+
cfg = config or FeatureConfig()
|
|
262
|
+
table = build_candidate_table(_peptides(peptides, columns, fasta, cfg), ref, cfg, columns)
|
|
263
|
+
mask = table.r_ascore <= threshold
|
|
264
|
+
recs = []
|
|
265
|
+
for name, spec in weight_sets.items():
|
|
266
|
+
w, _, _ = resolve_weights(spec)
|
|
267
|
+
recs.append({"weights": name, **w, **_evaluate_selection(table, choose(table, w), mask, gs_weight)})
|
|
268
|
+
return pd.DataFrame(recs)
|
ptmreloc/cli.py
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Command-line interface: ``ptmreloc optimize | reassign | evaluate``."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import argparse
|
|
5
|
+
import sys
|
|
6
|
+
import time
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from . import api
|
|
11
|
+
from .defaults import DEFAULT_THRESHOLD, DEFAULT_WEIGHTS, WEIGHT_PRESETS
|
|
12
|
+
from .features import FeatureConfig
|
|
13
|
+
from .reference import read_table, write_table
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _add_common(p: argparse.ArgumentParser, targets: bool) -> None:
|
|
17
|
+
p.add_argument("-i", "--input", required=True, help="peptide table (pkl/csv/tsv/parquet/xlsx)")
|
|
18
|
+
g = p.add_argument_group("reference data (default: human reference database, downloaded on first use)")
|
|
19
|
+
g.add_argument("--psp", default=None, help="own PhosphoSitePlus site table")
|
|
20
|
+
if targets:
|
|
21
|
+
g.add_argument("--gold", default=None, help="own Gold Standard site table ('none' to disable)")
|
|
22
|
+
g.add_argument("--fun", default=None, help="own Functional Landscape site table ('none' to disable)")
|
|
23
|
+
g.add_argument("--fasta", default=None,
|
|
24
|
+
help="own protein FASTA, used only when the input has no residue column")
|
|
25
|
+
g.add_argument("--database", default=None, help="folder with reference files (same names) to use instead")
|
|
26
|
+
g.add_argument("--organism", default=None, help="filter own PSP by ORGANISM (e.g. human)")
|
|
27
|
+
p.add_argument("--sheet", default=None, help="Excel sheet name for --input")
|
|
28
|
+
g = p.add_argument_group("input columns")
|
|
29
|
+
g.add_argument("--col-uniprot", default="UniProt")
|
|
30
|
+
g.add_argument("--col-peptide", default="Peptide")
|
|
31
|
+
g.add_argument("--col-residue", default="Residue")
|
|
32
|
+
g.add_argument("--col-ascore", default="AScore")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _add_features(p: argparse.ArgumentParser, inherit: bool) -> None:
|
|
36
|
+
g = p.add_argument_group("features" + (" (default: from --weights file, else legacy)" if inherit else ""))
|
|
37
|
+
d = None if inherit else argparse.SUPPRESS
|
|
38
|
+
g.add_argument("--ptm-label", default=d)
|
|
39
|
+
g.add_argument("--candidate-residues", default=d)
|
|
40
|
+
g.add_argument("--evidence-transform", choices=["legacy", "log", "raw"], default=d)
|
|
41
|
+
g.add_argument("--distance-mode", choices=["legacy", "inverse"], default=d)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _config(a, base: FeatureConfig = FeatureConfig()):
|
|
45
|
+
"""Explicit feature flags override ``base``; returns None when nothing was given and base is None."""
|
|
46
|
+
over = {k: getattr(a, k, None) for k in ("ptm_label", "candidate_residues", "evidence_transform", "distance_mode")}
|
|
47
|
+
over = {k: v for k, v in over.items() if v is not None}
|
|
48
|
+
return FeatureConfig(**{**base.to_dict(), **over})
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _opt_table(x):
|
|
52
|
+
return False if isinstance(x, str) and x.lower() == "none" else x
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _ref(a, targets: bool):
|
|
56
|
+
gold = _opt_table(a.gold) if targets else False
|
|
57
|
+
fun = _opt_table(a.fun) if targets else False
|
|
58
|
+
return api.load_reference(a.psp, gold, fun, organism=a.organism, database=a.database)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _columns(a):
|
|
62
|
+
return {"uniprot": a.col_uniprot, "peptide": a.col_peptide, "residue": a.col_residue, "ascore": a.col_ascore}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _read_input(a):
|
|
66
|
+
kw = {"sheet_name": a.sheet} if a.sheet else {}
|
|
67
|
+
return read_table(a.input, **kw)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _weights_spec(a):
|
|
71
|
+
if a.w is not None:
|
|
72
|
+
return list(a.w)
|
|
73
|
+
return a.weights # file, preset name, or None -> defaults
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _resolved(a):
|
|
77
|
+
w, thr, cfg = api.resolve_weights(_weights_spec(a))
|
|
78
|
+
thr = a.threshold if a.threshold is not None else (thr if thr is not None else DEFAULT_THRESHOLD)
|
|
79
|
+
return w, thr, _config(a, cfg or FeatureConfig())
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def cmd_optimize(a) -> int:
|
|
83
|
+
t0 = time.time()
|
|
84
|
+
res = api.optimize(
|
|
85
|
+
_read_input(a), _ref(a, True), fasta=a.fasta, threshold=a.threshold, n_samples=a.n_samples,
|
|
86
|
+
config=_config(a), columns=_columns(a), out_dir=a.out, organism=a.organism,
|
|
87
|
+
log10_bounds=(a.log10_low, a.log10_high), refine=not a.no_refine, refine_maxiter=a.refine_maxiter,
|
|
88
|
+
top_frac=a.top_frac, holdout_frac=a.holdout, gs_weight=a.gs_weight, seed=a.seed, verbose=True,
|
|
89
|
+
)
|
|
90
|
+
print(res.summary())
|
|
91
|
+
print(f"\nwrote {a.out}/weights.json, trials.csv, evaluation.csv, metrics_by_ascore.csv "
|
|
92
|
+
f"({time.time() - t0:.1f}s)")
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def cmd_reassign(a) -> int:
|
|
97
|
+
w, thr, cfg = _resolved(a)
|
|
98
|
+
out = api.reassign(_read_input(a), _ref(a, False), fasta=a.fasta, weights=w, threshold=thr, config=cfg, columns=_columns(a),
|
|
99
|
+
include_candidates=not a.no_candidates, organism=a.organism)
|
|
100
|
+
write_table(out, a.output)
|
|
101
|
+
n_elig = int((out["ReassignStatus"] == "eligible").sum())
|
|
102
|
+
print(f"weights {w}, AScore <= {thr:g}")
|
|
103
|
+
print(f"{len(out)} rows; {n_elig} eligible; {int(out['Reassigned'].sum())} relocalized; "
|
|
104
|
+
f"{int((out['ReassignStatus'] == 'unparsed').sum())} unparsed -> {a.output}")
|
|
105
|
+
return 0
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def cmd_evaluate(a) -> int:
|
|
109
|
+
w, thr, cfg = _resolved(a)
|
|
110
|
+
bins = [-np.inf] + [float(x) for x in a.bins.split(",")]
|
|
111
|
+
rep = api.evaluate(_read_input(a), _ref(a, True), fasta=a.fasta, weights=w, threshold=thr, bins=bins,
|
|
112
|
+
config=cfg, columns=_columns(a), organism=a.organism)
|
|
113
|
+
print(rep.summary())
|
|
114
|
+
if a.output:
|
|
115
|
+
write_table(rep.by_ascore, a.output)
|
|
116
|
+
return 0
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def cmd_info(a) -> int:
|
|
120
|
+
from .database import _database_url, cache_dir, database_info
|
|
121
|
+
|
|
122
|
+
with __import__("pandas").option_context("display.width", 200, "display.max_colwidth", 120):
|
|
123
|
+
print(database_info(a.database).to_string(index=False))
|
|
124
|
+
print(f"\ndownload cache: {cache_dir()}\ndownload URL: {_database_url() or '(not configured)'}")
|
|
125
|
+
print(f"\ndefault weights: {DEFAULT_WEIGHTS}, AScore threshold {DEFAULT_THRESHOLD:g}")
|
|
126
|
+
print(f"presets: {', '.join(WEIGHT_PRESETS)}")
|
|
127
|
+
return 0
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def cmd_download(a) -> int:
|
|
131
|
+
from .database import download_database
|
|
132
|
+
|
|
133
|
+
dest = download_database(url=a.url, dest=a.dest, force=a.force, verify=not a.no_verify)
|
|
134
|
+
print(f"reference data ready in {dest}")
|
|
135
|
+
return 0
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
139
|
+
ap = argparse.ArgumentParser(prog="ptmreloc", description="Prior-knowledge guided PTM site relocalization")
|
|
140
|
+
sub = ap.add_subparsers(dest="cmd", required=True)
|
|
141
|
+
|
|
142
|
+
p = sub.add_parser("info", help="show the reference data files in use and the default weights")
|
|
143
|
+
p.add_argument("--database", default=None)
|
|
144
|
+
p.set_defaults(func=cmd_info)
|
|
145
|
+
|
|
146
|
+
p = sub.add_parser("download", help="download the reference data (otherwise fetched on first use)")
|
|
147
|
+
p.add_argument("--url", default=None, help="base URL of the data files (default: the package's)")
|
|
148
|
+
p.add_argument("--dest", default=None, help="target folder (default: the per-user cache)")
|
|
149
|
+
p.add_argument("--force", action="store_true", help="download again even if present")
|
|
150
|
+
p.add_argument("--no-verify", action="store_true", help="skip checksum verification")
|
|
151
|
+
p.set_defaults(func=cmd_download)
|
|
152
|
+
|
|
153
|
+
p = sub.add_parser("optimize", help="optimize weights against Gold Standard / Functional sites")
|
|
154
|
+
_add_common(p, targets=True)
|
|
155
|
+
_add_features(p, inherit=True)
|
|
156
|
+
p.add_argument("-o", "--out", required=True, help="output directory")
|
|
157
|
+
p.add_argument("--threshold", type=float, default=DEFAULT_THRESHOLD,
|
|
158
|
+
help="AScore cutoff (reassign AScore <= cutoff)")
|
|
159
|
+
p.add_argument("--n-samples", type=int, default=2000)
|
|
160
|
+
p.add_argument("--log10-low", type=float, default=-3.0)
|
|
161
|
+
p.add_argument("--log10-high", type=float, default=0.0)
|
|
162
|
+
p.add_argument("--no-refine", action="store_true")
|
|
163
|
+
p.add_argument("--refine-maxiter", type=int, default=30)
|
|
164
|
+
p.add_argument("--top-frac", type=float, default=0.05)
|
|
165
|
+
p.add_argument("--holdout", type=float, default=0.2, help="fraction of proteins held out")
|
|
166
|
+
p.add_argument("--gs-weight", type=float, default=0.5, help="GS weight in the composite score")
|
|
167
|
+
p.add_argument("--seed", type=int, default=0)
|
|
168
|
+
p.set_defaults(func=cmd_optimize)
|
|
169
|
+
|
|
170
|
+
wh = (f"weights.json from `ptmreloc optimize`, or a preset ({', '.join(WEIGHT_PRESETS)}). "
|
|
171
|
+
f"Default: {DEFAULT_WEIGHTS}")
|
|
172
|
+
for name, func, targets, help_ in [
|
|
173
|
+
("reassign", cmd_reassign, False, "relocalize sites (default weights unless --weights/--w)"),
|
|
174
|
+
("evaluate", cmd_evaluate, True, "score weights against GS/FUN, per AScore bin"),
|
|
175
|
+
]:
|
|
176
|
+
p = sub.add_parser(name, help=help_)
|
|
177
|
+
_add_common(p, targets=targets)
|
|
178
|
+
_add_features(p, inherit=True)
|
|
179
|
+
p.add_argument("--weights", default=None, help=wh)
|
|
180
|
+
p.add_argument("--w", nargs=3, type=float, metavar=("W_H", "W_L", "W_D"), help="explicit weights")
|
|
181
|
+
p.add_argument("--threshold", type=float, default=None,
|
|
182
|
+
help=f"AScore cutoff (default: from weights file, else {DEFAULT_THRESHOLD:g})")
|
|
183
|
+
if name == "reassign":
|
|
184
|
+
p.add_argument("-o", "--output", required=True, help="output table (.tsv/.csv/.pkl/.parquet/.xlsx)")
|
|
185
|
+
p.add_argument("--no-candidates", action="store_true", help="omit CandidateSiteProbs column")
|
|
186
|
+
else:
|
|
187
|
+
p.add_argument("--bins", default="0,10,20,30,50,100,inf", help="AScore bin edges")
|
|
188
|
+
p.add_argument("-o", "--output", default=None, help="write per-bin metrics table")
|
|
189
|
+
p.set_defaults(func=func)
|
|
190
|
+
return ap
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def main(argv=None) -> int:
|
|
194
|
+
a = build_parser().parse_args(argv)
|
|
195
|
+
return a.func(a)
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
if __name__ == "__main__":
|
|
199
|
+
sys.exit(main())
|