protpy 1.4.2__tar.gz → 1.5.0__tar.gz
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.
- {protpy-1.4.2/protpy.egg-info → protpy-1.5.0}/PKG-INFO +36 -1
- {protpy-1.4.2 → protpy-1.5.0}/README.md +35 -0
- {protpy-1.4.2 → protpy-1.5.0}/docs/conf.py +20 -1
- {protpy-1.4.2 → protpy-1.5.0}/protpy/__init__.py +17 -2
- {protpy-1.4.2 → protpy-1.5.0}/protpy/_constants.py +34 -0
- protpy-1.5.0/protpy/_helpers.py +331 -0
- {protpy-1.4.2 → protpy-1.5.0}/protpy/autocorrelation.py +77 -152
- protpy-1.5.0/protpy/complexity.py +219 -0
- {protpy-1.4.2 → protpy-1.5.0}/protpy/composition.py +707 -106
- {protpy-1.4.2 → protpy-1.5.0}/protpy/conjoint_triad.py +28 -8
- {protpy-1.4.2 → protpy-1.5.0}/protpy/ctd.py +50 -18
- protpy-1.5.0/protpy/data/README.md +16 -0
- protpy-1.5.0/protpy/data/aa_scales.json +1874 -0
- protpy-1.5.0/protpy/data/reduced_alphabets.json +151 -0
- protpy-1.5.0/protpy/data/residue_tables.json +351 -0
- protpy-1.5.0/protpy/data/structural_params.json +135 -0
- {protpy-1.4.2 → protpy-1.5.0}/protpy/encode.py +192 -77
- protpy-1.5.0/protpy/physicochemical.py +590 -0
- protpy-1.5.0/protpy/scales.py +574 -0
- {protpy-1.4.2 → protpy-1.5.0}/protpy/sequence_order.py +22 -46
- protpy-1.5.0/protpy/signal.py +155 -0
- protpy-1.5.0/protpy/structural.py +641 -0
- protpy-1.5.0/protpy/utils.py +1530 -0
- {protpy-1.4.2 → protpy-1.5.0/protpy.egg-info}/PKG-INFO +36 -1
- {protpy-1.4.2 → protpy-1.5.0}/protpy.egg-info/SOURCES.txt +21 -0
- {protpy-1.4.2 → protpy-1.5.0}/protpy.egg-info/top_level.txt +1 -0
- {protpy-1.4.2 → protpy-1.5.0}/pyproject.toml +3 -2
- {protpy-1.4.2 → protpy-1.5.0}/tests/test_autocorrelation.py +1 -1
- protpy-1.5.0/tests/test_backwards_compat.py +84 -0
- protpy-1.5.0/tests/test_complexity.py +67 -0
- {protpy-1.4.2 → protpy-1.5.0}/tests/test_composition.py +30 -17
- protpy-1.5.0/tests/test_composition_extended.py +183 -0
- {protpy-1.4.2 → protpy-1.5.0}/tests/test_conjoint_triad.py +1 -1
- protpy-1.5.0/tests/test_descriptor_sets.py +99 -0
- protpy-1.5.0/tests/test_helpers.py +116 -0
- protpy-1.5.0/tests/test_performance.py +25 -0
- protpy-1.5.0/tests/test_physicochemical.py +182 -0
- {protpy-1.4.2 → protpy-1.5.0}/tests/test_protpy.py +9 -4
- protpy-1.5.0/tests/test_regressions.py +84 -0
- protpy-1.5.0/tests/test_scales.py +103 -0
- protpy-1.5.0/tests/test_signal.py +67 -0
- protpy-1.5.0/tests/test_structural.py +118 -0
- {protpy-1.4.2 → protpy-1.5.0}/tests/test_utils.py +2 -1
- protpy-1.4.2/protpy/data/README.md +0 -9
- protpy-1.4.2/protpy/utils.py +0 -740
- {protpy-1.4.2 → protpy-1.5.0}/LICENSE +0 -0
- {protpy-1.4.2 → protpy-1.5.0}/MANIFEST.in +0 -0
- {protpy-1.4.2 → protpy-1.5.0}/protpy/data/grantham-physicochemical-distance-matrix.json +0 -0
- {protpy-1.4.2 → protpy-1.5.0}/protpy/data/schneider-wrede-physicochemical-distance-matrix.json +0 -0
- {protpy-1.4.2 → protpy-1.5.0}/protpy.egg-info/dependency_links.txt +0 -0
- {protpy-1.4.2 → protpy-1.5.0}/protpy.egg-info/requires.txt +0 -0
- {protpy-1.4.2 → protpy-1.5.0}/setup.cfg +0 -0
- {protpy-1.4.2 → protpy-1.5.0}/tests/__init__.py +0 -0
- {protpy-1.4.2 → protpy-1.5.0}/tests/test_ctd.py +0 -0
- {protpy-1.4.2 → protpy-1.5.0}/tests/test_encode.py +0 -0
- {protpy-1.4.2 → protpy-1.5.0}/tests/test_sequence_order.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: protpy
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.5.0
|
|
4
4
|
Summary: Python package for generating various biochemical, physicochemical and structural descriptors/features of protein sequences.
|
|
5
5
|
Author-email: AJ McKenna <amckenna41@qub.ac.uk>
|
|
6
6
|
Maintainer-email: AJ McKenna <amckenna41@qub.ac.uk>
|
|
@@ -160,6 +160,24 @@ The descriptors available in `protpy` include:
|
|
|
160
160
|
|
|
161
161
|
</details>
|
|
162
162
|
|
|
163
|
+
### New in v1.5.0
|
|
164
|
+
|
|
165
|
+
| Category | New descriptors |
|
|
166
|
+
|---|---|
|
|
167
|
+
| Composition | `cksaap`, `cksaagp`, `dde`, `asdc`, `distance_pair`, `natural_vector`, `atomic_composition` |
|
|
168
|
+
| Scales | `z_scales_5`, `vhse`, `atchley_factors`, `t_scales`, `st_scales`, `protfp`, `fasgai`, `blosum_indices`, `ms_whim`, `auto_cross_covariance`, `physicochemical_distance_transformation` |
|
|
169
|
+
| Physicochemical | `charge_fractions`, `ncpr`, `kappa`, `sequence_charge_decoration`, `charge_density`, `hydrophobicity_profile`, `hydrophobic_ratio`, `half_life`, `ivywrel_index`, `cvp_bias`, `flexibility` |
|
|
170
|
+
| Complexity | `wootton_federhen_complexity`, `linguistic_complexity`, `lempel_ziv_complexity` |
|
|
171
|
+
| Signal | `fft_descriptors`, `dct_descriptors` |
|
|
172
|
+
| Structural | `aggrescan`, `uversky_charge_hydropathy`, `fold_index`, `disorder_propensity`, `transmembrane_features`, `signal_peptide_features`, `functional_site_counts`, `plaac`, `antigenicity_surface`, `cysteine_features` |
|
|
173
|
+
| Extensions | GAAC/GDPC/GTPC (`reduced_alphabet_composition(alphabet="ifeature5", k=...)`), KSCTriad (`conjoint_triad(gap=...)`), 13-property CTD (`ctd_(property_set="ifeature13")`), split AAC/DPC (`region=`), `pka_scale=` for pI/charge, `scale=` for GRAVY/hydrophobic moment, A280 absorbance |
|
|
174
|
+
|
|
175
|
+
`all_descriptors` and `encode_dataset` take `descriptor_set="classic"` (default, unchanged output), `"extended"` or `"all"`, plus `include=` / `exclude=` lists:
|
|
176
|
+
|
|
177
|
+
```python
|
|
178
|
+
df = protpy.all_descriptors(protein_seq, descriptor_set="extended")
|
|
179
|
+
```
|
|
180
|
+
|
|
163
181
|
More detail of each descriptor is listed in the markdown file: [`DESCRIPTORS.md`](https://github.com/amckenna41/protPy/blob/main/DESCRIPTORS.md)
|
|
164
182
|
|
|
165
183
|
|
|
@@ -482,6 +500,23 @@ qso = protpy.quasi_sequence_order(protein_seq, lag=10, weight=0.2, distance_matr
|
|
|
482
500
|
|
|
483
501
|
</details>
|
|
484
502
|
|
|
503
|
+
### Scaling features for machine learning
|
|
504
|
+
|
|
505
|
+
protpy returns each descriptor on its natural scale (percentages, counts, indices, pH, Da, ...). If your classifier needs features on a common scale such as [0, 1], fit a scaler on your **training** data and reuse it for test and new sequences. This scales every column to the range seen in your data without leaking information from the test set:
|
|
506
|
+
|
|
507
|
+
```python
|
|
508
|
+
from sklearn.preprocessing import MinMaxScaler
|
|
509
|
+
|
|
510
|
+
X_train = protpy.encode_dataset(train_seqs, output_file=None, verbose=False).set_index("sequence_id")
|
|
511
|
+
X_test = protpy.encode_dataset(test_seqs, output_file=None, verbose=False).set_index("sequence_id")
|
|
512
|
+
|
|
513
|
+
scaler = MinMaxScaler(clip=True).fit(X_train) # fit on training data only
|
|
514
|
+
X_train_scaled = scaler.transform(X_train) # every column in [0, 1]
|
|
515
|
+
X_test_scaled = scaler.transform(X_test) # clip=True keeps unseen extremes in [0, 1]
|
|
516
|
+
```
|
|
517
|
+
|
|
518
|
+
`StandardScaler` is a common alternative, and tree-based models don't need scaling. When cross-validating, put the scaler in a scikit-learn `Pipeline` so it is re-fitted on each training fold. Impute the few `NaN` values that `descriptor_set="extended"` can produce (e.g. with `SimpleImputer`) before scaling.
|
|
519
|
+
|
|
485
520
|
Documentation
|
|
486
521
|
-------------
|
|
487
522
|
The documentation for `protpy` is hosted on **ReadTheDocs** and is available [here][readthedocs].
|
|
@@ -111,6 +111,24 @@ The descriptors available in `protpy` include:
|
|
|
111
111
|
|
|
112
112
|
</details>
|
|
113
113
|
|
|
114
|
+
### New in v1.5.0
|
|
115
|
+
|
|
116
|
+
| Category | New descriptors |
|
|
117
|
+
|---|---|
|
|
118
|
+
| Composition | `cksaap`, `cksaagp`, `dde`, `asdc`, `distance_pair`, `natural_vector`, `atomic_composition` |
|
|
119
|
+
| Scales | `z_scales_5`, `vhse`, `atchley_factors`, `t_scales`, `st_scales`, `protfp`, `fasgai`, `blosum_indices`, `ms_whim`, `auto_cross_covariance`, `physicochemical_distance_transformation` |
|
|
120
|
+
| Physicochemical | `charge_fractions`, `ncpr`, `kappa`, `sequence_charge_decoration`, `charge_density`, `hydrophobicity_profile`, `hydrophobic_ratio`, `half_life`, `ivywrel_index`, `cvp_bias`, `flexibility` |
|
|
121
|
+
| Complexity | `wootton_federhen_complexity`, `linguistic_complexity`, `lempel_ziv_complexity` |
|
|
122
|
+
| Signal | `fft_descriptors`, `dct_descriptors` |
|
|
123
|
+
| Structural | `aggrescan`, `uversky_charge_hydropathy`, `fold_index`, `disorder_propensity`, `transmembrane_features`, `signal_peptide_features`, `functional_site_counts`, `plaac`, `antigenicity_surface`, `cysteine_features` |
|
|
124
|
+
| Extensions | GAAC/GDPC/GTPC (`reduced_alphabet_composition(alphabet="ifeature5", k=...)`), KSCTriad (`conjoint_triad(gap=...)`), 13-property CTD (`ctd_(property_set="ifeature13")`), split AAC/DPC (`region=`), `pka_scale=` for pI/charge, `scale=` for GRAVY/hydrophobic moment, A280 absorbance |
|
|
125
|
+
|
|
126
|
+
`all_descriptors` and `encode_dataset` take `descriptor_set="classic"` (default, unchanged output), `"extended"` or `"all"`, plus `include=` / `exclude=` lists:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
df = protpy.all_descriptors(protein_seq, descriptor_set="extended")
|
|
130
|
+
```
|
|
131
|
+
|
|
114
132
|
More detail of each descriptor is listed in the markdown file: [`DESCRIPTORS.md`](https://github.com/amckenna41/protPy/blob/main/DESCRIPTORS.md)
|
|
115
133
|
|
|
116
134
|
|
|
@@ -433,6 +451,23 @@ qso = protpy.quasi_sequence_order(protein_seq, lag=10, weight=0.2, distance_matr
|
|
|
433
451
|
|
|
434
452
|
</details>
|
|
435
453
|
|
|
454
|
+
### Scaling features for machine learning
|
|
455
|
+
|
|
456
|
+
protpy returns each descriptor on its natural scale (percentages, counts, indices, pH, Da, ...). If your classifier needs features on a common scale such as [0, 1], fit a scaler on your **training** data and reuse it for test and new sequences. This scales every column to the range seen in your data without leaking information from the test set:
|
|
457
|
+
|
|
458
|
+
```python
|
|
459
|
+
from sklearn.preprocessing import MinMaxScaler
|
|
460
|
+
|
|
461
|
+
X_train = protpy.encode_dataset(train_seqs, output_file=None, verbose=False).set_index("sequence_id")
|
|
462
|
+
X_test = protpy.encode_dataset(test_seqs, output_file=None, verbose=False).set_index("sequence_id")
|
|
463
|
+
|
|
464
|
+
scaler = MinMaxScaler(clip=True).fit(X_train) # fit on training data only
|
|
465
|
+
X_train_scaled = scaler.transform(X_train) # every column in [0, 1]
|
|
466
|
+
X_test_scaled = scaler.transform(X_test) # clip=True keeps unseen extremes in [0, 1]
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
`StandardScaler` is a common alternative, and tree-based models don't need scaling. When cross-validating, put the scaler in a scikit-learn `Pipeline` so it is re-fitted on each training fold. Impute the few `NaN` values that `descriptor_set="extended"` can produce (e.g. with `SimpleImputer`) before scaling.
|
|
470
|
+
|
|
436
471
|
Documentation
|
|
437
472
|
-------------
|
|
438
473
|
The documentation for `protpy` is hosted on **ReadTheDocs** and is available [here][readthedocs].
|
|
@@ -13,7 +13,7 @@ sys.path.insert(0, os.path.abspath('..'))
|
|
|
13
13
|
project = 'protpy'
|
|
14
14
|
copyright = '2026, AJ McKenna'
|
|
15
15
|
author = 'AJ McKenna'
|
|
16
|
-
release = '1.
|
|
16
|
+
release = '1.5.0'
|
|
17
17
|
|
|
18
18
|
# -- General configuration ---------------------------------------------------
|
|
19
19
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
|
@@ -39,3 +39,22 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
|
|
|
39
39
|
# html_theme = 'alabaster'
|
|
40
40
|
html_theme = 'sphinx_rtd_theme'
|
|
41
41
|
html_static_path = ['_static']
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
#protpy docstrings write parameters as ":name: type" under a "Parameters" heading; napoleon
|
|
45
|
+
#expects numpy's "name : type", so rewrite those lines before napoleon (priority 500) runs
|
|
46
|
+
import re as _re
|
|
47
|
+
_FIELD = _re.compile(r"^(\s*):(\w+):\s*(.*)$")
|
|
48
|
+
|
|
49
|
+
def _house_style_to_numpy(app, what, name, obj, options, lines):
|
|
50
|
+
section = None
|
|
51
|
+
for i, line in enumerate(lines):
|
|
52
|
+
stripped = line.strip()
|
|
53
|
+
if stripped in ("Parameters", "Returns", "Raises") and i + 1 < len(lines) and set(lines[i + 1].strip()) <= {"=", "-"}:
|
|
54
|
+
section = stripped
|
|
55
|
+
match = _FIELD.match(line)
|
|
56
|
+
if section in ("Parameters", "Returns") and match:
|
|
57
|
+
lines[i] = f"{match.group(1)}{match.group(2)} : {match.group(3)}"
|
|
58
|
+
|
|
59
|
+
def setup(app):
|
|
60
|
+
app.connect("autodoc-process-docstring", _house_style_to_numpy, priority=400)
|
|
@@ -3,6 +3,11 @@ from .composition import *
|
|
|
3
3
|
from .conjoint_triad import *
|
|
4
4
|
from .ctd import *
|
|
5
5
|
from .sequence_order import *
|
|
6
|
+
from .scales import *
|
|
7
|
+
from .physicochemical import *
|
|
8
|
+
from .complexity import *
|
|
9
|
+
from .signal import *
|
|
10
|
+
from .structural import *
|
|
6
11
|
from .utils import get_descriptor_info, list_descriptors, print_descriptor_info
|
|
7
12
|
from .encode import all_descriptors, encode_dataset, read_sequences_from_file
|
|
8
13
|
|
|
@@ -12,7 +17,7 @@ amino_acids = ["A", "C", "D", "E", "F", "G", "H", "I", "K", "L", "M", "N", "P",
|
|
|
12
17
|
|
|
13
18
|
#protPy package metadata
|
|
14
19
|
__name__ = 'protpy'
|
|
15
|
-
__version__ = "1.
|
|
20
|
+
__version__ = "1.5.0"
|
|
16
21
|
__description__ = "Python package for generating various biochemical, physicochemical and structural descriptors/features of protein sequences."
|
|
17
22
|
__author__ = 'AJ McKenna: https://github.com/amckenna41'
|
|
18
23
|
__authorEmail__ = 'amckenna41@qub.ac.uk'
|
|
@@ -39,5 +44,15 @@ descriptor_names = ["amino_acid_composition", "dipeptide_composition", "tripepti
|
|
|
39
44
|
"conjoint_triad", "ctd_composition", "ctd_transition", "ctd_distribution", "ctd_",
|
|
40
45
|
"sequence_order_coupling_number_", "sequence_order_coupling_number",
|
|
41
46
|
"sequence_order_coupling_number_all", "quasi_sequence_order", "quasi_sequence_order_all",
|
|
42
|
-
"shannon_entropy"
|
|
47
|
+
"shannon_entropy",
|
|
48
|
+
#v1.5.0
|
|
49
|
+
"cksaap", "cksaagp", "dde", "asdc", "distance_pair", "natural_vector", "atomic_composition",
|
|
50
|
+
"z_scales_5", "vhse", "atchley_factors", "t_scales", "st_scales", "protfp", "fasgai",
|
|
51
|
+
"blosum_indices", "ms_whim", "auto_cross_covariance", "physicochemical_distance_transformation",
|
|
52
|
+
"charge_fractions", "ncpr", "kappa", "sequence_charge_decoration", "charge_density",
|
|
53
|
+
"hydrophobicity_profile", "hydrophobic_ratio", "half_life", "ivywrel_index", "cvp_bias", "flexibility",
|
|
54
|
+
"wootton_federhen_complexity", "linguistic_complexity", "lempel_ziv_complexity",
|
|
55
|
+
"fft_descriptors", "dct_descriptors",
|
|
56
|
+
"aggrescan", "uversky_charge_hydropathy", "fold_index", "disorder_propensity", "transmembrane_features",
|
|
57
|
+
"signal_peptide_features", "functional_site_counts", "plaac", "antigenicity_surface", "cysteine_features",
|
|
43
58
|
]
|
|
@@ -43,3 +43,37 @@ def _validate_sequence(sequence: str) -> str:
|
|
|
43
43
|
f"Invalid amino acid found in protein sequence: {aa}."
|
|
44
44
|
)
|
|
45
45
|
return sequence
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def _validate_int(name: str, value, minimum: int = 0) -> int:
|
|
49
|
+
"""
|
|
50
|
+
Parse ``value`` as an int and check it is >= ``minimum``.
|
|
51
|
+
|
|
52
|
+
Raises
|
|
53
|
+
======
|
|
54
|
+
ValueError
|
|
55
|
+
if ``value`` cannot be parsed as an int or is below ``minimum``.
|
|
56
|
+
"""
|
|
57
|
+
if isinstance(value, bool):
|
|
58
|
+
raise ValueError(f"Invalid {name} value input: {value}.")
|
|
59
|
+
try:
|
|
60
|
+
value = int(value)
|
|
61
|
+
except (ValueError, TypeError):
|
|
62
|
+
raise ValueError(f"Invalid {name} value input: {value}.")
|
|
63
|
+
if value < minimum:
|
|
64
|
+
raise ValueError(f"{name} must be >= {minimum}, got {value}.")
|
|
65
|
+
return value
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _validate_weight(weight) -> float:
|
|
69
|
+
"""
|
|
70
|
+
Check that ``weight`` is a non-negative number.
|
|
71
|
+
|
|
72
|
+
Raises
|
|
73
|
+
======
|
|
74
|
+
ValueError
|
|
75
|
+
if ``weight`` is not a number or is negative.
|
|
76
|
+
"""
|
|
77
|
+
if isinstance(weight, bool) or not isinstance(weight, (int, float)) or weight < 0:
|
|
78
|
+
raise ValueError(f"weight must be a non-negative number, got {weight}.")
|
|
79
|
+
return weight
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
############# Internal Helpers ##############
|
|
3
|
+
################################################################################
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
Internal engines shared by the descriptor modules added in v1.5.0. Nothing in
|
|
9
|
+
this module is exported from the protpy namespace.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
import copy
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
from collections import Counter
|
|
16
|
+
from functools import lru_cache
|
|
17
|
+
|
|
18
|
+
import numpy as np
|
|
19
|
+
import pandas as pd
|
|
20
|
+
|
|
21
|
+
from ._constants import amino_acids
|
|
22
|
+
|
|
23
|
+
_DATA_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "data")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@lru_cache(maxsize=None)
|
|
27
|
+
def _read_json(filename: str) -> dict:
|
|
28
|
+
"""
|
|
29
|
+
Read a JSON data file from the protpy/data directory. Files are cached
|
|
30
|
+
after the first read.
|
|
31
|
+
|
|
32
|
+
Parameters
|
|
33
|
+
==========
|
|
34
|
+
:filename: str
|
|
35
|
+
name of the JSON file in protpy/data.
|
|
36
|
+
|
|
37
|
+
Returns
|
|
38
|
+
=======
|
|
39
|
+
:tables: dict
|
|
40
|
+
contents of the JSON file.
|
|
41
|
+
"""
|
|
42
|
+
with open(os.path.join(_DATA_DIR, filename)) as f:
|
|
43
|
+
return json.load(f)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _load_json_table(filename: str, key: str) -> dict:
|
|
47
|
+
"""
|
|
48
|
+
Get a copy of a data table from a JSON file in the protpy/data directory.
|
|
49
|
+
Each table has the keys name, values, reference, doi and source_note. A copy
|
|
50
|
+
is returned so the cached table can't be modified.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
==========
|
|
54
|
+
:filename: str
|
|
55
|
+
name of the JSON file in protpy/data.
|
|
56
|
+
:key: str
|
|
57
|
+
name of the table in the file.
|
|
58
|
+
|
|
59
|
+
Returns
|
|
60
|
+
=======
|
|
61
|
+
:table: dict
|
|
62
|
+
copy of the data table.
|
|
63
|
+
|
|
64
|
+
Raises
|
|
65
|
+
======
|
|
66
|
+
KeyError
|
|
67
|
+
if the table is not in the file; the message lists the available tables.
|
|
68
|
+
"""
|
|
69
|
+
tables = _read_json(filename)
|
|
70
|
+
if key not in tables:
|
|
71
|
+
raise KeyError(f"Table '{key}' not found in {filename}. Valid tables are: {sorted(tables)}.")
|
|
72
|
+
#copy so callers cannot mutate the cached table
|
|
73
|
+
return copy.deepcopy(tables[key])
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _scale_values(scale: str | dict) -> dict:
|
|
77
|
+
"""
|
|
78
|
+
Get the values of an amino acid scale for each amino acid. The scale can be a
|
|
79
|
+
table name in the aa_scales.json data file, an AAindex accession number or a
|
|
80
|
+
dict of amino acid values.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
==========
|
|
84
|
+
:scale: str | dict
|
|
85
|
+
scale name, AAindex accession number or dict of amino acid values.
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
=======
|
|
89
|
+
:values: dict
|
|
90
|
+
dict of scale values (a float or a list of floats) for each amino acid.
|
|
91
|
+
|
|
92
|
+
Raises
|
|
93
|
+
======
|
|
94
|
+
ValueError
|
|
95
|
+
if the scale is not a protpy scale or an AAindex accession number.
|
|
96
|
+
"""
|
|
97
|
+
if isinstance(scale, dict):
|
|
98
|
+
return scale
|
|
99
|
+
tables = _read_json("aa_scales.json")
|
|
100
|
+
if scale in tables:
|
|
101
|
+
return tables[scale]["values"]
|
|
102
|
+
from .autocorrelation import _get_properties
|
|
103
|
+
try:
|
|
104
|
+
return _get_properties(scale, normalize=False)[scale]
|
|
105
|
+
except ValueError:
|
|
106
|
+
raise ValueError(f"Unknown scale '{scale}'. Use a protpy scale name {sorted(tables)}, "
|
|
107
|
+
"an AAindex1 accession code, or a {amino_acid: value} dict.") from None
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _property_matrix(sequence: str, scales: str | list[str] | dict, standardize: bool = False) -> np.ndarray:
|
|
111
|
+
"""
|
|
112
|
+
Convert a protein sequence into a matrix of amino acid property values, with
|
|
113
|
+
one row per residue and one column per property dimension. The scales can be a
|
|
114
|
+
factor-scale family or single-value scale from the aa_scales.json data file,
|
|
115
|
+
an AAindex accession number, a list of AAindex accession numbers or a dict of
|
|
116
|
+
amino acid values (a float or a list of floats per amino acid).
|
|
117
|
+
|
|
118
|
+
Parameters
|
|
119
|
+
==========
|
|
120
|
+
:sequence: str
|
|
121
|
+
protein sequence.
|
|
122
|
+
:scales: str | list[str] | dict
|
|
123
|
+
scale name, AAindex accession number, list of accession numbers or dict of
|
|
124
|
+
amino acid values.
|
|
125
|
+
:standardize: bool (default=False)
|
|
126
|
+
standardise each dimension to zero mean and unit standard deviation over
|
|
127
|
+
the 20 amino acids.
|
|
128
|
+
|
|
129
|
+
Returns
|
|
130
|
+
=======
|
|
131
|
+
:matrix: np.ndarray
|
|
132
|
+
property values of the sequence, of the shape L x d, where L is the length
|
|
133
|
+
of the sequence and d is the number of property dimensions.
|
|
134
|
+
|
|
135
|
+
Raises
|
|
136
|
+
======
|
|
137
|
+
ValueError
|
|
138
|
+
if the scale is missing values for any of the 20 amino acids.
|
|
139
|
+
"""
|
|
140
|
+
if isinstance(scales, (list, tuple)):
|
|
141
|
+
tables = [_scale_values(s) for s in scales]
|
|
142
|
+
table = {aa: [t[aa] for t in tables] for aa in amino_acids}
|
|
143
|
+
else:
|
|
144
|
+
table = _scale_values(scales)
|
|
145
|
+
missing = [aa for aa in amino_acids if aa not in table]
|
|
146
|
+
if missing:
|
|
147
|
+
raise ValueError(f"Scale is missing values for amino acids: {missing}.")
|
|
148
|
+
#rows ordered by amino_acids so standardisation is over exactly the 20 residues
|
|
149
|
+
aa_matrix = np.array([np.atleast_1d(np.asarray(table[aa], dtype=float)) for aa in amino_acids])
|
|
150
|
+
if standardize:
|
|
151
|
+
std = aa_matrix.std(axis=0)
|
|
152
|
+
aa_matrix = np.divide(aa_matrix - aa_matrix.mean(axis=0), std,
|
|
153
|
+
out=np.zeros_like(aa_matrix), where=std != 0)
|
|
154
|
+
index = {aa: i for i, aa in enumerate(amino_acids)}
|
|
155
|
+
return aa_matrix[[index[aa] for aa in sequence]]
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def _sliding_windows(values: np.ndarray, window: int, edge: float = 1.0) -> np.ndarray:
|
|
159
|
+
"""
|
|
160
|
+
Calculate the weighted mean of a profile over a sliding window. With edge=1.0
|
|
161
|
+
the weights are all 1 (a plain mean); with edge < 1 the weights fall linearly
|
|
162
|
+
from 1 at the centre of the window to edge at its ends, as in ExPASy ProtScale
|
|
163
|
+
and Biopython's protein_scale. A window longer than the profile uses the whole
|
|
164
|
+
profile.
|
|
165
|
+
|
|
166
|
+
Parameters
|
|
167
|
+
==========
|
|
168
|
+
:values: np.ndarray
|
|
169
|
+
profile values along the sequence.
|
|
170
|
+
:window: int
|
|
171
|
+
length of the sliding window.
|
|
172
|
+
:edge: float (default=1.0)
|
|
173
|
+
relative weight of the ends of the window, between 0 and 1.
|
|
174
|
+
|
|
175
|
+
Returns
|
|
176
|
+
=======
|
|
177
|
+
:windows: np.ndarray
|
|
178
|
+
weighted mean of each window, of length L - window + 1.
|
|
179
|
+
"""
|
|
180
|
+
values = np.asarray(values, dtype=float)
|
|
181
|
+
window = min(window, len(values))
|
|
182
|
+
half = (window - 1) / 2
|
|
183
|
+
dist = np.minimum(np.arange(window), np.arange(window)[::-1])
|
|
184
|
+
weights = edge + (1 - edge) * dist / half if half else np.ones(window)
|
|
185
|
+
weights = np.minimum(weights, 1.0)
|
|
186
|
+
return np.convolve(values, weights[::-1], mode="valid") / weights.sum()
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def _profile_stats(profile: np.ndarray, prefix: str, stats=("mean", "max", "min", "std"),
|
|
190
|
+
threshold: float | None = None) -> dict:
|
|
191
|
+
"""
|
|
192
|
+
Summarise a profile by its mean, maximum, minimum and standard deviation, and
|
|
193
|
+
optionally the fraction of values above a threshold.
|
|
194
|
+
|
|
195
|
+
Parameters
|
|
196
|
+
==========
|
|
197
|
+
:profile: np.ndarray
|
|
198
|
+
profile values.
|
|
199
|
+
:prefix: str
|
|
200
|
+
prefix for the names of the statistics.
|
|
201
|
+
:stats: tuple (default=("mean", "max", "min", "std"))
|
|
202
|
+
statistics to calculate.
|
|
203
|
+
:threshold: float | None (default=None)
|
|
204
|
+
if set, also calculate the fraction of values above it (frac_above).
|
|
205
|
+
|
|
206
|
+
Returns
|
|
207
|
+
=======
|
|
208
|
+
:stats: dict
|
|
209
|
+
dict of statistic names (prefix_stat) and values.
|
|
210
|
+
"""
|
|
211
|
+
profile = np.asarray(profile, dtype=float)
|
|
212
|
+
funcs = {"mean": np.mean, "max": np.max, "min": np.min, "std": np.std}
|
|
213
|
+
out = {f"{prefix}_{s}": float(funcs[s](profile)) for s in stats}
|
|
214
|
+
if threshold is not None:
|
|
215
|
+
out[f"{prefix}_frac_above"] = float(np.mean(profile > threshold))
|
|
216
|
+
return out
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _gapped_pair_counts(sequence: str, gap: int, alphabet: dict[str, str] | None = None) -> Counter:
|
|
220
|
+
"""
|
|
221
|
+
Count the pairs of residues (s_i, s_i+gap+1) in a protein sequence, with gap
|
|
222
|
+
residues between them. The residues can first be mapped to groups using a
|
|
223
|
+
reduced alphabet.
|
|
224
|
+
|
|
225
|
+
Parameters
|
|
226
|
+
==========
|
|
227
|
+
:sequence: str
|
|
228
|
+
protein sequence.
|
|
229
|
+
:gap: int
|
|
230
|
+
number of residues between the two residues of a pair.
|
|
231
|
+
:alphabet: dict | None (default=None)
|
|
232
|
+
dict mapping each amino acid to its group; None counts the amino acids.
|
|
233
|
+
|
|
234
|
+
Returns
|
|
235
|
+
=======
|
|
236
|
+
:counts: Counter
|
|
237
|
+
count of each pair, keyed by a tuple of the two residues or groups.
|
|
238
|
+
"""
|
|
239
|
+
seq = [alphabet[aa] for aa in sequence] if alphabet else list(sequence)
|
|
240
|
+
step = gap + 1
|
|
241
|
+
return Counter((seq[i], seq[i + step]) for i in range(len(seq) - step))
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _kmer_counts(sequence: str, k: int, alphabet: dict[str, str] | None = None) -> Counter:
|
|
245
|
+
"""
|
|
246
|
+
Count the overlapping k-mers of a protein sequence. The residues can first be
|
|
247
|
+
mapped to groups using a reduced alphabet.
|
|
248
|
+
|
|
249
|
+
Parameters
|
|
250
|
+
==========
|
|
251
|
+
:sequence: str
|
|
252
|
+
protein sequence.
|
|
253
|
+
:k: int
|
|
254
|
+
length of the k-mers.
|
|
255
|
+
:alphabet: dict | None (default=None)
|
|
256
|
+
dict mapping each amino acid to its group; None counts the amino acids.
|
|
257
|
+
|
|
258
|
+
Returns
|
|
259
|
+
=======
|
|
260
|
+
:counts: Counter
|
|
261
|
+
count of each k-mer, keyed by a tuple of its residues or groups.
|
|
262
|
+
"""
|
|
263
|
+
seq = [alphabet[aa] for aa in sequence] if alphabet else list(sequence)
|
|
264
|
+
return Counter(tuple(seq[i:i + k]) for i in range(len(seq) - k + 1))
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def _charge_vector(sequence: str, his: bool = False) -> np.ndarray:
|
|
268
|
+
"""
|
|
269
|
+
Get the charge of each residue of a protein sequence: +1 for K and R, -1 for
|
|
270
|
+
D and E and 0 otherwise. His is neutral unless his=True.
|
|
271
|
+
|
|
272
|
+
Parameters
|
|
273
|
+
==========
|
|
274
|
+
:sequence: str
|
|
275
|
+
protein sequence.
|
|
276
|
+
:his: bool (default=False)
|
|
277
|
+
treat His as positive.
|
|
278
|
+
|
|
279
|
+
Returns
|
|
280
|
+
=======
|
|
281
|
+
:charges: np.ndarray
|
|
282
|
+
charge of each residue.
|
|
283
|
+
"""
|
|
284
|
+
positive = "KRH" if his else "KR"
|
|
285
|
+
return np.array([1 if aa in positive else -1 if aa in "DE" else 0 for aa in sequence], dtype=float)
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
def _runs(mask: np.ndarray, min_len: int = 1) -> list[tuple[int, int]]:
|
|
289
|
+
"""
|
|
290
|
+
Find the runs of consecutive True values in a boolean array, e.g. the
|
|
291
|
+
segments of a profile above a threshold.
|
|
292
|
+
|
|
293
|
+
Parameters
|
|
294
|
+
==========
|
|
295
|
+
:mask: np.ndarray
|
|
296
|
+
boolean values along the sequence.
|
|
297
|
+
:min_len: int (default=1)
|
|
298
|
+
minimum length of a run.
|
|
299
|
+
|
|
300
|
+
Returns
|
|
301
|
+
=======
|
|
302
|
+
:runs: list
|
|
303
|
+
list of (start, end) indices of each run, where end is exclusive.
|
|
304
|
+
"""
|
|
305
|
+
mask = np.concatenate(([False], np.asarray(mask, dtype=bool), [False]))
|
|
306
|
+
edges = np.flatnonzero(np.diff(mask.astype(int)))
|
|
307
|
+
return [(int(s), int(e)) for s, e in zip(edges[::2], edges[1::2]) if e - s >= min_len]
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def _to_df(values: list, columns: list[str]) -> pd.DataFrame:
|
|
311
|
+
"""
|
|
312
|
+
Convert a list of descriptor values into a single-row dataframe. Floats are
|
|
313
|
+
rounded to 3 decimal places and booleans and numpy integers are converted to
|
|
314
|
+
ints.
|
|
315
|
+
|
|
316
|
+
Parameters
|
|
317
|
+
==========
|
|
318
|
+
:values: list
|
|
319
|
+
descriptor values.
|
|
320
|
+
:columns: list[str]
|
|
321
|
+
column names.
|
|
322
|
+
|
|
323
|
+
Returns
|
|
324
|
+
=======
|
|
325
|
+
:descriptor_df: pd.DataFrame
|
|
326
|
+
pandas dataframe of the descriptor values, of the shape 1 x N, where N is
|
|
327
|
+
the number of values.
|
|
328
|
+
"""
|
|
329
|
+
values = [round(float(v), 3) if isinstance(v, (float, np.floating)) else
|
|
330
|
+
int(v) if isinstance(v, (bool, np.bool_, np.integer)) else v for v in values]
|
|
331
|
+
return pd.DataFrame([values], columns=columns)
|