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.
Files changed (56) hide show
  1. {protpy-1.4.2/protpy.egg-info → protpy-1.5.0}/PKG-INFO +36 -1
  2. {protpy-1.4.2 → protpy-1.5.0}/README.md +35 -0
  3. {protpy-1.4.2 → protpy-1.5.0}/docs/conf.py +20 -1
  4. {protpy-1.4.2 → protpy-1.5.0}/protpy/__init__.py +17 -2
  5. {protpy-1.4.2 → protpy-1.5.0}/protpy/_constants.py +34 -0
  6. protpy-1.5.0/protpy/_helpers.py +331 -0
  7. {protpy-1.4.2 → protpy-1.5.0}/protpy/autocorrelation.py +77 -152
  8. protpy-1.5.0/protpy/complexity.py +219 -0
  9. {protpy-1.4.2 → protpy-1.5.0}/protpy/composition.py +707 -106
  10. {protpy-1.4.2 → protpy-1.5.0}/protpy/conjoint_triad.py +28 -8
  11. {protpy-1.4.2 → protpy-1.5.0}/protpy/ctd.py +50 -18
  12. protpy-1.5.0/protpy/data/README.md +16 -0
  13. protpy-1.5.0/protpy/data/aa_scales.json +1874 -0
  14. protpy-1.5.0/protpy/data/reduced_alphabets.json +151 -0
  15. protpy-1.5.0/protpy/data/residue_tables.json +351 -0
  16. protpy-1.5.0/protpy/data/structural_params.json +135 -0
  17. {protpy-1.4.2 → protpy-1.5.0}/protpy/encode.py +192 -77
  18. protpy-1.5.0/protpy/physicochemical.py +590 -0
  19. protpy-1.5.0/protpy/scales.py +574 -0
  20. {protpy-1.4.2 → protpy-1.5.0}/protpy/sequence_order.py +22 -46
  21. protpy-1.5.0/protpy/signal.py +155 -0
  22. protpy-1.5.0/protpy/structural.py +641 -0
  23. protpy-1.5.0/protpy/utils.py +1530 -0
  24. {protpy-1.4.2 → protpy-1.5.0/protpy.egg-info}/PKG-INFO +36 -1
  25. {protpy-1.4.2 → protpy-1.5.0}/protpy.egg-info/SOURCES.txt +21 -0
  26. {protpy-1.4.2 → protpy-1.5.0}/protpy.egg-info/top_level.txt +1 -0
  27. {protpy-1.4.2 → protpy-1.5.0}/pyproject.toml +3 -2
  28. {protpy-1.4.2 → protpy-1.5.0}/tests/test_autocorrelation.py +1 -1
  29. protpy-1.5.0/tests/test_backwards_compat.py +84 -0
  30. protpy-1.5.0/tests/test_complexity.py +67 -0
  31. {protpy-1.4.2 → protpy-1.5.0}/tests/test_composition.py +30 -17
  32. protpy-1.5.0/tests/test_composition_extended.py +183 -0
  33. {protpy-1.4.2 → protpy-1.5.0}/tests/test_conjoint_triad.py +1 -1
  34. protpy-1.5.0/tests/test_descriptor_sets.py +99 -0
  35. protpy-1.5.0/tests/test_helpers.py +116 -0
  36. protpy-1.5.0/tests/test_performance.py +25 -0
  37. protpy-1.5.0/tests/test_physicochemical.py +182 -0
  38. {protpy-1.4.2 → protpy-1.5.0}/tests/test_protpy.py +9 -4
  39. protpy-1.5.0/tests/test_regressions.py +84 -0
  40. protpy-1.5.0/tests/test_scales.py +103 -0
  41. protpy-1.5.0/tests/test_signal.py +67 -0
  42. protpy-1.5.0/tests/test_structural.py +118 -0
  43. {protpy-1.4.2 → protpy-1.5.0}/tests/test_utils.py +2 -1
  44. protpy-1.4.2/protpy/data/README.md +0 -9
  45. protpy-1.4.2/protpy/utils.py +0 -740
  46. {protpy-1.4.2 → protpy-1.5.0}/LICENSE +0 -0
  47. {protpy-1.4.2 → protpy-1.5.0}/MANIFEST.in +0 -0
  48. {protpy-1.4.2 → protpy-1.5.0}/protpy/data/grantham-physicochemical-distance-matrix.json +0 -0
  49. {protpy-1.4.2 → protpy-1.5.0}/protpy/data/schneider-wrede-physicochemical-distance-matrix.json +0 -0
  50. {protpy-1.4.2 → protpy-1.5.0}/protpy.egg-info/dependency_links.txt +0 -0
  51. {protpy-1.4.2 → protpy-1.5.0}/protpy.egg-info/requires.txt +0 -0
  52. {protpy-1.4.2 → protpy-1.5.0}/setup.cfg +0 -0
  53. {protpy-1.4.2 → protpy-1.5.0}/tests/__init__.py +0 -0
  54. {protpy-1.4.2 → protpy-1.5.0}/tests/test_ctd.py +0 -0
  55. {protpy-1.4.2 → protpy-1.5.0}/tests/test_encode.py +0 -0
  56. {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.4.2
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.4.2'
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.4.2"
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)