phasepred 0.9.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.
- phasepred/__init__.py +3 -0
- phasepred/cli.py +1053 -0
- phasepred/data/models/PdPS/8f_model_0.joblib +0 -0
- phasepred/data/models/PdPS/8f_model_1.joblib +0 -0
- phasepred/data/models/PdPS/8f_model_2.joblib +0 -0
- phasepred/data/models/PdPS/8f_model_3.joblib +0 -0
- phasepred/data/models/PdPS/8f_model_4.joblib +0 -0
- phasepred/data/models/PdPS/8f_model_5.joblib +0 -0
- phasepred/data/models/PdPS/8f_model_6.joblib +0 -0
- phasepred/data/models/PdPS/8f_model_7.joblib +0 -0
- phasepred/data/models/PdPS/8f_model_8.joblib +0 -0
- phasepred/data/models/PdPS/8f_model_9.joblib +0 -0
- phasepred/data/models/PdPS/manifest.json +25 -0
- phasepred/data/models/SaPS/8f_model_0.joblib +0 -0
- phasepred/data/models/SaPS/8f_model_1.joblib +0 -0
- phasepred/data/models/SaPS/8f_model_2.joblib +0 -0
- phasepred/data/models/SaPS/8f_model_3.joblib +0 -0
- phasepred/data/models/SaPS/8f_model_4.joblib +0 -0
- phasepred/data/models/SaPS/8f_model_5.joblib +0 -0
- phasepred/data/models/SaPS/8f_model_6.joblib +0 -0
- phasepred/data/models/SaPS/8f_model_7.joblib +0 -0
- phasepred/data/models/SaPS/8f_model_8.joblib +0 -0
- phasepred/data/models/SaPS/8f_model_9.joblib +0 -0
- phasepred/data/models/SaPS/manifest.json +25 -0
- phasepred/data/models/hPdPS/8f_model_0.joblib +0 -0
- phasepred/data/models/hPdPS/8f_model_1.joblib +0 -0
- phasepred/data/models/hPdPS/8f_model_2.joblib +0 -0
- phasepred/data/models/hPdPS/8f_model_3.joblib +0 -0
- phasepred/data/models/hPdPS/8f_model_4.joblib +0 -0
- phasepred/data/models/hPdPS/8f_model_5.joblib +0 -0
- phasepred/data/models/hPdPS/8f_model_6.joblib +0 -0
- phasepred/data/models/hPdPS/8f_model_7.joblib +0 -0
- phasepred/data/models/hPdPS/8f_model_8.joblib +0 -0
- phasepred/data/models/hPdPS/8f_model_9.joblib +0 -0
- phasepred/data/models/hPdPS/manifest.json +27 -0
- phasepred/data/models/hSaPS/8f_model_0.joblib +0 -0
- phasepred/data/models/hSaPS/8f_model_1.joblib +0 -0
- phasepred/data/models/hSaPS/8f_model_2.joblib +0 -0
- phasepred/data/models/hSaPS/8f_model_3.joblib +0 -0
- phasepred/data/models/hSaPS/8f_model_4.joblib +0 -0
- phasepred/data/models/hSaPS/8f_model_5.joblib +0 -0
- phasepred/data/models/hSaPS/8f_model_6.joblib +0 -0
- phasepred/data/models/hSaPS/8f_model_7.joblib +0 -0
- phasepred/data/models/hSaPS/8f_model_8.joblib +0 -0
- phasepred/data/models/hSaPS/8f_model_9.joblib +0 -0
- phasepred/data/models/hSaPS/manifest.json +27 -0
- phasepred/data.py +65 -0
- phasepred/espritz.py +192 -0
- phasepred/features.py +158 -0
- phasepred/legacy_features.py +224 -0
- phasepred/models.py +141 -0
- phasepred/paper.py +160 -0
- phasepred/predictor.py +215 -0
- phasepred/sequences.py +220 -0
- phasepred/tool_features.py +218 -0
- phasepred/tool_paths.py +429 -0
- phasepred/tools.py +602 -0
- phasepred/updated_data.py +349 -0
- phasepred-0.9.0.dist-info/METADATA +283 -0
- phasepred-0.9.0.dist-info/RECORD +64 -0
- phasepred-0.9.0.dist-info/WHEEL +5 -0
- phasepred-0.9.0.dist-info/entry_points.txt +2 -0
- phasepred-0.9.0.dist-info/licenses/LICENSE +35 -0
- phasepred-0.9.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import gzip
|
|
4
|
+
import subprocess
|
|
5
|
+
import tempfile
|
|
6
|
+
from dataclasses import asdict, dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
import pandas as pd
|
|
11
|
+
|
|
12
|
+
from phasepred.data import data_path
|
|
13
|
+
from phasepred.features import compute_native_features
|
|
14
|
+
from phasepred.tool_paths import (
|
|
15
|
+
PhaSePredToolNotFound,
|
|
16
|
+
find_tool_entry,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LegacyFeatureError(RuntimeError):
|
|
21
|
+
"""Raised when a legacy feature adapter cannot compute a value."""
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class ScalarFeature:
|
|
26
|
+
accession: str
|
|
27
|
+
value: float
|
|
28
|
+
source: str
|
|
29
|
+
|
|
30
|
+
def to_json(self) -> dict[str, object]:
|
|
31
|
+
return asdict(self)
|
|
32
|
+
|
|
33
|
+
@classmethod
|
|
34
|
+
def from_json(cls, payload: dict[str, Any]) -> ScalarFeature:
|
|
35
|
+
return cls(
|
|
36
|
+
accession=str(payload["accession"]),
|
|
37
|
+
value=float(payload["value"]),
|
|
38
|
+
source=str(payload["source"]),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def compute_lcr_features(sequence_rows: list[dict[str, object]]) -> list[ScalarFeature]:
|
|
43
|
+
try:
|
|
44
|
+
seg_entry = find_tool_entry("seg")
|
|
45
|
+
except PhaSePredToolNotFound as e:
|
|
46
|
+
raise LegacyFeatureError(str(e)) from e
|
|
47
|
+
fasta = _write_temp_fasta(sequence_rows)
|
|
48
|
+
try:
|
|
49
|
+
result = subprocess.run(
|
|
50
|
+
[str(seg_entry), str(fasta), "12", "2.2", "2.5", "-x"],
|
|
51
|
+
check=True,
|
|
52
|
+
capture_output=True,
|
|
53
|
+
text=True,
|
|
54
|
+
)
|
|
55
|
+
finally:
|
|
56
|
+
fasta.unlink(missing_ok=True)
|
|
57
|
+
masked = _parse_fasta_text(result.stdout)
|
|
58
|
+
features = []
|
|
59
|
+
for row in sequence_rows:
|
|
60
|
+
accession = str(row["UniprotEntry"])
|
|
61
|
+
sequence = masked.get(accession, "")
|
|
62
|
+
if not sequence:
|
|
63
|
+
continue
|
|
64
|
+
low_complexity = sum(1 for residue in sequence if residue == "x" or residue.islower())
|
|
65
|
+
features.append(
|
|
66
|
+
ScalarFeature(
|
|
67
|
+
accession=accession,
|
|
68
|
+
value=low_complexity / len(sequence),
|
|
69
|
+
source="seg_12_2.2_2.5_x",
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
return features
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def compute_pscore_features(sequence_rows: list[dict[str, object]]) -> list[ScalarFeature]:
|
|
76
|
+
try:
|
|
77
|
+
pscore_entry = find_tool_entry("pscore")
|
|
78
|
+
except PhaSePredToolNotFound as e:
|
|
79
|
+
raise LegacyFeatureError(str(e)) from e
|
|
80
|
+
with tempfile.TemporaryDirectory(prefix="phasepred_pscore_") as tmp:
|
|
81
|
+
workdir = Path(tmp)
|
|
82
|
+
fasta = workdir / "input.fasta"
|
|
83
|
+
output = workdir / "pscore.tsv"
|
|
84
|
+
fasta.write_text(_fasta_text(sequence_rows), encoding="utf-8")
|
|
85
|
+
subprocess.run(
|
|
86
|
+
[
|
|
87
|
+
str(pscore_entry),
|
|
88
|
+
str(fasta),
|
|
89
|
+
"-output",
|
|
90
|
+
str(output),
|
|
91
|
+
"-overwrite",
|
|
92
|
+
"-mute",
|
|
93
|
+
],
|
|
94
|
+
check=True,
|
|
95
|
+
capture_output=True,
|
|
96
|
+
text=True,
|
|
97
|
+
)
|
|
98
|
+
return parse_pscore_output(output)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def parse_pscore_output(path: str | Path) -> list[ScalarFeature]:
|
|
102
|
+
rows = []
|
|
103
|
+
for line in Path(path).read_text(encoding="utf-8", errors="replace").splitlines():
|
|
104
|
+
parts = line.split()
|
|
105
|
+
if len(parts) >= 3 and parts[0].startswith("PScore"):
|
|
106
|
+
accession = parts[-1].lstrip(">")
|
|
107
|
+
rows.append(ScalarFeature(accession, float(parts[1]), "pscore_elife_sourcecode2"))
|
|
108
|
+
return rows
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def compute_plaac_features(sequence_rows: list[dict[str, object]]) -> list[ScalarFeature]:
|
|
112
|
+
try:
|
|
113
|
+
plaac_runner = find_tool_entry("plaac")
|
|
114
|
+
except PhaSePredToolNotFound as e:
|
|
115
|
+
raise LegacyFeatureError(str(e)) from e
|
|
116
|
+
with tempfile.TemporaryDirectory(prefix="phasepred_plaac_") as tmp:
|
|
117
|
+
fasta = Path(tmp) / "input.fasta"
|
|
118
|
+
fasta.write_text(_fasta_text(sequence_rows), encoding="utf-8")
|
|
119
|
+
result = subprocess.run(
|
|
120
|
+
[str(plaac_runner), "-i", str(fasta)],
|
|
121
|
+
cwd=str(data_path()),
|
|
122
|
+
check=True,
|
|
123
|
+
capture_output=True,
|
|
124
|
+
text=True,
|
|
125
|
+
)
|
|
126
|
+
return parse_plaac_output(result.stdout)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def parse_plaac_output(text: str) -> list[ScalarFeature]:
|
|
130
|
+
lines = [line for line in text.splitlines() if line.strip()]
|
|
131
|
+
header = None
|
|
132
|
+
rows = []
|
|
133
|
+
for line in lines:
|
|
134
|
+
if line.startswith("SEQid\t"):
|
|
135
|
+
header = line.split("\t")
|
|
136
|
+
continue
|
|
137
|
+
if header is None or line.startswith("#"):
|
|
138
|
+
continue
|
|
139
|
+
parts = line.split("\t")
|
|
140
|
+
if len(parts) != len(header):
|
|
141
|
+
continue
|
|
142
|
+
record = dict(zip(header, parts, strict=False))
|
|
143
|
+
if "SEQid" in record and "NLLR" in record:
|
|
144
|
+
rows.append(ScalarFeature(record["SEQid"], float(record["NLLR"]), "plaac_nllr"))
|
|
145
|
+
return rows
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def compute_phosphosite_frequencies(
|
|
149
|
+
sequence_rows: list[dict[str, object]],
|
|
150
|
+
phosphosite_path: str | Path,
|
|
151
|
+
) -> list[ScalarFeature]:
|
|
152
|
+
lengths = {str(row["UniprotEntry"]): len(str(row["sequence"])) for row in sequence_rows}
|
|
153
|
+
counts = dict.fromkeys(lengths, 0)
|
|
154
|
+
with gzip.open(phosphosite_path, "rt", encoding="utf-8", errors="replace") as handle:
|
|
155
|
+
header = None
|
|
156
|
+
for line in handle:
|
|
157
|
+
if line.startswith("GENE\t"):
|
|
158
|
+
header = line.rstrip("\n").split("\t")
|
|
159
|
+
continue
|
|
160
|
+
if header is None or not line.strip() or line.startswith("PhosphoSitePlus"):
|
|
161
|
+
continue
|
|
162
|
+
record = dict(zip(header, line.rstrip("\n").split("\t"), strict=False))
|
|
163
|
+
accession = record.get("ACC_ID", "")
|
|
164
|
+
if accession in counts and record.get("ORGANISM", "").lower() == "human":
|
|
165
|
+
counts[accession] += 1
|
|
166
|
+
return [
|
|
167
|
+
ScalarFeature(accession, count / lengths[accession], "phosphositeplus_sites_per_residue")
|
|
168
|
+
for accession, count in counts.items()
|
|
169
|
+
if lengths[accession] > 0
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def load_deepphase_scores(path: str | Path) -> pd.DataFrame:
|
|
174
|
+
frame = pd.read_excel(path, sheet_name="tableS3")
|
|
175
|
+
required = {"Swiss-Prot ID", "DeepPhase_score"}
|
|
176
|
+
missing = required - set(frame.columns)
|
|
177
|
+
if missing:
|
|
178
|
+
raise LegacyFeatureError(f"DeepPhase table missing columns: {', '.join(sorted(missing))}")
|
|
179
|
+
return frame[["Swiss-Prot ID", "DeepPhase_score"]].rename(
|
|
180
|
+
columns={"Swiss-Prot ID": "UniprotEntry", "DeepPhase_score": "DeepPhase"}
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def native_feature_frame(sequence_rows: list[dict[str, object]]) -> pd.DataFrame:
|
|
185
|
+
rows = []
|
|
186
|
+
errors = []
|
|
187
|
+
for row in sequence_rows:
|
|
188
|
+
accession = str(row["UniprotEntry"])
|
|
189
|
+
try:
|
|
190
|
+
features = compute_native_features(str(row["sequence"]))
|
|
191
|
+
except ValueError as exc:
|
|
192
|
+
errors.append({"UniprotEntry": accession, "error": str(exc)})
|
|
193
|
+
features = {}
|
|
194
|
+
rows.append({"UniprotEntry": accession, **features})
|
|
195
|
+
frame = pd.DataFrame(rows)
|
|
196
|
+
if errors:
|
|
197
|
+
frame.attrs["errors"] = errors
|
|
198
|
+
return frame
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _write_temp_fasta(sequence_rows: list[dict[str, object]]) -> Path:
|
|
202
|
+
handle = tempfile.NamedTemporaryFile("w", suffix=".fasta", delete=False, encoding="utf-8")
|
|
203
|
+
with handle:
|
|
204
|
+
handle.write(_fasta_text(sequence_rows))
|
|
205
|
+
return Path(handle.name)
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _fasta_text(sequence_rows: list[dict[str, object]]) -> str:
|
|
209
|
+
return "".join(
|
|
210
|
+
f">{str(row['UniprotEntry']).strip()}\n{''.join(str(row['sequence']).split()).upper()}\n"
|
|
211
|
+
for row in sequence_rows
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _parse_fasta_text(text: str) -> dict[str, str]:
|
|
216
|
+
records: dict[str, list[str]] = {}
|
|
217
|
+
current = None
|
|
218
|
+
for line in text.splitlines():
|
|
219
|
+
if line.startswith(">"):
|
|
220
|
+
current = line[1:].split()[0]
|
|
221
|
+
records[current] = []
|
|
222
|
+
elif current is not None:
|
|
223
|
+
records[current].append(line.strip())
|
|
224
|
+
return {accession: "".join(parts) for accession, parts in records.items()}
|
phasepred/models.py
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import joblib
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from sklearn.impute import SimpleImputer
|
|
10
|
+
from sklearn.linear_model import LogisticRegression
|
|
11
|
+
from sklearn.pipeline import Pipeline
|
|
12
|
+
from sklearn.preprocessing import StandardScaler
|
|
13
|
+
|
|
14
|
+
from phasepred.features import (
|
|
15
|
+
BASE_FEATURE_COLUMNS,
|
|
16
|
+
HUMAN_FEATURE_COLUMNS,
|
|
17
|
+
validate_feature_columns,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
FEATURES_BY_TASK = {
|
|
21
|
+
"SaPS": BASE_FEATURE_COLUMNS,
|
|
22
|
+
"PdPS": BASE_FEATURE_COLUMNS,
|
|
23
|
+
"hSaPS": HUMAN_FEATURE_COLUMNS,
|
|
24
|
+
"hPdPS": HUMAN_FEATURE_COLUMNS,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
# The v2022 feature-definitions version that the code implements
|
|
28
|
+
# (docs/FEATURES.md). Model artifacts tag their training definitions with
|
|
29
|
+
# this value; mismatches trigger the stale warning in `predict`.
|
|
30
|
+
FEATURE_DEFINITIONS_VERSION = "v2022"
|
|
31
|
+
|
|
32
|
+
MANIFEST_FILENAME = "manifest.json"
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class ModelManifestError(RuntimeError):
|
|
36
|
+
"""Raised when a model directory manifest is unreadable."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@dataclass(frozen=True)
|
|
40
|
+
class ModelManifest:
|
|
41
|
+
feature_definitions_version: str
|
|
42
|
+
raw: dict[str, Any]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def load_model_manifest(model_dir: Path) -> ModelManifest | None:
|
|
46
|
+
"""Read ``manifest.json`` from a model directory, if present.
|
|
47
|
+
|
|
48
|
+
Returns ``None`` for pre-0.9.0 artifacts that carry no manifest. A
|
|
49
|
+
malformed manifest raises :class:`ModelManifestError`.
|
|
50
|
+
"""
|
|
51
|
+
manifest_path = model_dir / MANIFEST_FILENAME
|
|
52
|
+
if not manifest_path.is_file():
|
|
53
|
+
return None
|
|
54
|
+
try:
|
|
55
|
+
import json
|
|
56
|
+
|
|
57
|
+
raw = json.loads(manifest_path.read_text(encoding="utf-8"))
|
|
58
|
+
except (OSError, ValueError) as exc:
|
|
59
|
+
raise ModelManifestError(f"Unreadable model manifest at {manifest_path}: {exc}") from exc
|
|
60
|
+
if not isinstance(raw, dict):
|
|
61
|
+
raise ModelManifestError(f"Model manifest at {manifest_path} is not a JSON object")
|
|
62
|
+
version = raw.get("feature_definitions_version")
|
|
63
|
+
if not isinstance(version, str) or not version:
|
|
64
|
+
raise ModelManifestError(
|
|
65
|
+
f"Model manifest at {manifest_path} is missing string "
|
|
66
|
+
"feature_definitions_version"
|
|
67
|
+
)
|
|
68
|
+
return ModelManifest(feature_definitions_version=version, raw=raw)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def model_dir_is_stale(model_dir: Path) -> tuple[bool, str]:
|
|
72
|
+
"""Check whether a model directory binds an out-of-date feature set.
|
|
73
|
+
|
|
74
|
+
Returns ``(is_stale, reason)``. Legacy artifacts without a manifest, and
|
|
75
|
+
artifacts whose ``feature_definitions_version`` differs from the current
|
|
76
|
+
``FEATURE_DEFINITIONS_VERSION``, are stale.
|
|
77
|
+
"""
|
|
78
|
+
manifest = load_model_manifest(model_dir)
|
|
79
|
+
if manifest is None:
|
|
80
|
+
return True, (
|
|
81
|
+
f"model directory {model_dir} has no {MANIFEST_FILENAME} "
|
|
82
|
+
"(pre-0.9.0 artifact trained before feature_definitions_version "
|
|
83
|
+
f"tagging; current version {FEATURE_DEFINITIONS_VERSION})"
|
|
84
|
+
)
|
|
85
|
+
if manifest.feature_definitions_version != FEATURE_DEFINITIONS_VERSION:
|
|
86
|
+
return True, (
|
|
87
|
+
f"model directory {model_dir} binds feature_definitions_version "
|
|
88
|
+
f"{manifest.feature_definitions_version!r} but the code implements "
|
|
89
|
+
f"{FEATURE_DEFINITIONS_VERSION!r}"
|
|
90
|
+
)
|
|
91
|
+
return False, ""
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@dataclass
|
|
95
|
+
class ModelArtifact:
|
|
96
|
+
task: str
|
|
97
|
+
feature_columns: list[str]
|
|
98
|
+
estimator: Pipeline
|
|
99
|
+
|
|
100
|
+
def dump(self, path: str | Path) -> None:
|
|
101
|
+
path = Path(path)
|
|
102
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
103
|
+
joblib.dump(self, path)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def train_model_artifact(
|
|
107
|
+
frame: pd.DataFrame,
|
|
108
|
+
*,
|
|
109
|
+
label_column: str,
|
|
110
|
+
task: str,
|
|
111
|
+
feature_columns: list[str] | None = None,
|
|
112
|
+
) -> ModelArtifact:
|
|
113
|
+
if label_column not in frame.columns:
|
|
114
|
+
raise ValueError(f"Label column not found: {label_column}")
|
|
115
|
+
columns = feature_columns or list(FEATURES_BY_TASK[task])
|
|
116
|
+
validate_feature_columns(frame, columns)
|
|
117
|
+
estimator = Pipeline(
|
|
118
|
+
steps=[
|
|
119
|
+
("impute", SimpleImputer(strategy="median")),
|
|
120
|
+
("scale", StandardScaler()),
|
|
121
|
+
("classifier", LogisticRegression(max_iter=2000, random_state=42)),
|
|
122
|
+
]
|
|
123
|
+
)
|
|
124
|
+
estimator.fit(frame[columns], frame[label_column].astype(int))
|
|
125
|
+
return ModelArtifact(task=task, feature_columns=columns, estimator=estimator)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def load_model_artifact(path: str | Path) -> ModelArtifact:
|
|
129
|
+
artifact = joblib.load(path)
|
|
130
|
+
if not isinstance(artifact, ModelArtifact):
|
|
131
|
+
raise TypeError(f"Unexpected model artifact type: {type(artifact)!r}")
|
|
132
|
+
return artifact
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def predict_with_artifact(artifact: ModelArtifact, frame: pd.DataFrame) -> pd.DataFrame:
|
|
136
|
+
validate_feature_columns(frame, artifact.feature_columns)
|
|
137
|
+
scores = artifact.estimator.predict_proba(frame[artifact.feature_columns])[:, 1]
|
|
138
|
+
output = pd.DataFrame({"score": scores})
|
|
139
|
+
if "UniprotEntry" in frame.columns:
|
|
140
|
+
output.insert(0, "UniprotEntry", frame["UniprotEntry"].to_numpy())
|
|
141
|
+
return output
|
phasepred/paper.py
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class PaperSplitError(ValueError):
|
|
10
|
+
"""Raised when paper supplemental split tables cannot be parsed."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class PaperSplit:
|
|
15
|
+
train: pd.DataFrame
|
|
16
|
+
test: pd.DataFrame
|
|
17
|
+
|
|
18
|
+
@property
|
|
19
|
+
def all_rows(self) -> pd.DataFrame:
|
|
20
|
+
return pd.concat([self.train, self.test], ignore_index=True)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class PaperFeatures:
|
|
25
|
+
train: pd.DataFrame
|
|
26
|
+
test: pd.DataFrame
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def all_rows(self) -> pd.DataFrame:
|
|
30
|
+
return pd.concat([self.train, self.test], ignore_index=True)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
TRAIN_SHEETS: dict[str, tuple[str, str]] = {
|
|
34
|
+
"SaPS": ("SaPS", "NoPS"),
|
|
35
|
+
"PdPS": ("PdPS", "NoPS"),
|
|
36
|
+
"hSaPS": ("hSaPS", "hNoPS"),
|
|
37
|
+
"hPdPS": ("hPdPS", "hNoPS"),
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
TEST_SHEETS: dict[str, tuple[tuple[str, ...], str]] = {
|
|
41
|
+
"SaPS": (("SaPS-test", "PS-test"), "NoPS-test"),
|
|
42
|
+
"PdPS": (("PdPS-test", "PS-test"), "NoPS-test"),
|
|
43
|
+
"hSaPS": (("hSaPS-test", "hPS-test"), "hNoPS-test"),
|
|
44
|
+
"hPdPS": (("hPdPS-test", "hPS-test"), "hNoPS-test"),
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def load_paper_split(s2_path: str | Path, s3_path: str | Path) -> PaperSplit:
|
|
49
|
+
s2 = Path(s2_path)
|
|
50
|
+
s3 = Path(s3_path)
|
|
51
|
+
if not s2.exists():
|
|
52
|
+
raise PaperSplitError(f"Dataset S2 workbook not found: {s2}")
|
|
53
|
+
if not s3.exists():
|
|
54
|
+
raise PaperSplitError(f"Dataset S3 workbook not found: {s3}")
|
|
55
|
+
|
|
56
|
+
train_rows: list[dict[str, object]] = []
|
|
57
|
+
for task, (positive_sheet, negative_sheet) in TRAIN_SHEETS.items():
|
|
58
|
+
train_rows.extend(_rows_for_sheet(s2, positive_sheet, task, "train", 1))
|
|
59
|
+
train_rows.extend(_rows_for_sheet(s2, negative_sheet, task, "train", 0))
|
|
60
|
+
|
|
61
|
+
test_rows: list[dict[str, object]] = []
|
|
62
|
+
for task, (positive_sheets, negative_sheet) in TEST_SHEETS.items():
|
|
63
|
+
for positive_sheet in positive_sheets:
|
|
64
|
+
test_rows.extend(_rows_for_sheet(s3, positive_sheet, task, "test", 1))
|
|
65
|
+
test_rows.extend(_rows_for_sheet(s3, negative_sheet, task, "test", 0))
|
|
66
|
+
|
|
67
|
+
return PaperSplit(
|
|
68
|
+
train=pd.DataFrame(train_rows, columns=_split_columns()),
|
|
69
|
+
test=pd.DataFrame(test_rows, columns=_split_columns()),
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def load_paper_features(s2_path: str | Path, s3_path: str | Path) -> PaperFeatures:
|
|
74
|
+
s2 = Path(s2_path)
|
|
75
|
+
s3 = Path(s3_path)
|
|
76
|
+
if not s2.exists():
|
|
77
|
+
raise PaperSplitError(f"Dataset S2 workbook not found: {s2}")
|
|
78
|
+
if not s3.exists():
|
|
79
|
+
raise PaperSplitError(f"Dataset S3 workbook not found: {s3}")
|
|
80
|
+
|
|
81
|
+
train_rows: list[dict[str, object]] = []
|
|
82
|
+
for task, (positive_sheet, negative_sheet) in TRAIN_SHEETS.items():
|
|
83
|
+
train_rows.extend(_feature_rows_for_sheet(s2, positive_sheet, task, "train", 1))
|
|
84
|
+
train_rows.extend(_feature_rows_for_sheet(s2, negative_sheet, task, "train", 0))
|
|
85
|
+
|
|
86
|
+
test_rows: list[dict[str, object]] = []
|
|
87
|
+
for task, (positive_sheets, negative_sheet) in TEST_SHEETS.items():
|
|
88
|
+
for positive_sheet in positive_sheets:
|
|
89
|
+
test_rows.extend(_feature_rows_for_sheet(s3, positive_sheet, task, "test", 1))
|
|
90
|
+
test_rows.extend(_feature_rows_for_sheet(s3, negative_sheet, task, "test", 0))
|
|
91
|
+
|
|
92
|
+
return PaperFeatures(
|
|
93
|
+
train=pd.DataFrame(train_rows),
|
|
94
|
+
test=pd.DataFrame(test_rows),
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _split_columns() -> list[str]:
|
|
99
|
+
return ["task", "split", "label", "UniprotEntry", "source_sheet"]
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _rows_for_sheet(
|
|
103
|
+
workbook: Path,
|
|
104
|
+
sheet_name: str,
|
|
105
|
+
task: str,
|
|
106
|
+
split: str,
|
|
107
|
+
label: int,
|
|
108
|
+
) -> list[dict[str, object]]:
|
|
109
|
+
frame = _read_sheet(workbook, sheet_name)
|
|
110
|
+
return [
|
|
111
|
+
{
|
|
112
|
+
"task": task,
|
|
113
|
+
"split": split,
|
|
114
|
+
"label": label,
|
|
115
|
+
"UniprotEntry": accession,
|
|
116
|
+
"source_sheet": sheet_name,
|
|
117
|
+
}
|
|
118
|
+
for accession in (frame["UniprotEntry"].dropna().astype(str).str.strip().tolist())
|
|
119
|
+
if accession
|
|
120
|
+
]
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _feature_rows_for_sheet(
|
|
124
|
+
workbook: Path,
|
|
125
|
+
sheet_name: str,
|
|
126
|
+
task: str,
|
|
127
|
+
split: str,
|
|
128
|
+
label: int,
|
|
129
|
+
) -> list[dict[str, object]]:
|
|
130
|
+
frame = _read_sheet(workbook, sheet_name)
|
|
131
|
+
records = frame.to_dict(orient="records")
|
|
132
|
+
rows: list[dict[str, object]] = []
|
|
133
|
+
for record in records:
|
|
134
|
+
accession = str(record.get("UniprotEntry") or "").strip()
|
|
135
|
+
if not accession:
|
|
136
|
+
continue
|
|
137
|
+
row = dict(record)
|
|
138
|
+
row.update(
|
|
139
|
+
{
|
|
140
|
+
"task": task,
|
|
141
|
+
"split": split,
|
|
142
|
+
"label": label,
|
|
143
|
+
"UniprotEntry": accession,
|
|
144
|
+
"source_sheet": sheet_name,
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
rows.append(row)
|
|
148
|
+
return rows
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _read_sheet(workbook: Path, sheet_name: str) -> pd.DataFrame:
|
|
152
|
+
try:
|
|
153
|
+
frame = pd.read_excel(workbook, sheet_name=sheet_name)
|
|
154
|
+
except ValueError as exc:
|
|
155
|
+
raise PaperSplitError(f"Required sheet {sheet_name!r} not found in {workbook}") from exc
|
|
156
|
+
if "UniprotEntry" not in frame.columns:
|
|
157
|
+
frame = pd.read_excel(workbook, sheet_name=sheet_name, header=1)
|
|
158
|
+
if "UniprotEntry" not in frame.columns:
|
|
159
|
+
raise PaperSplitError(f"Sheet {sheet_name!r} in {workbook} lacks UniprotEntry column")
|
|
160
|
+
return frame
|