protpy 1.4.0__tar.gz → 1.4.1__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.0/protpy.egg-info → protpy-1.4.1}/PKG-INFO +2 -1
- {protpy-1.4.0 → protpy-1.4.1}/docs/conf.py +1 -1
- {protpy-1.4.0 → protpy-1.4.1}/protpy/__init__.py +2 -2
- protpy-1.4.1/protpy/encode.py +624 -0
- {protpy-1.4.0 → protpy-1.4.1/protpy.egg-info}/PKG-INFO +2 -1
- {protpy-1.4.0 → protpy-1.4.1}/protpy.egg-info/requires.txt +1 -0
- {protpy-1.4.0 → protpy-1.4.1}/pyproject.toml +2 -1
- protpy-1.4.1/tests/test_encode.py +947 -0
- {protpy-1.4.0 → protpy-1.4.1}/tests/test_protpy.py +2 -2
- protpy-1.4.0/protpy/encode.py +0 -203
- protpy-1.4.0/tests/test_encode.py +0 -304
- {protpy-1.4.0 → protpy-1.4.1}/LICENSE +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/MANIFEST.in +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/README.md +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/protpy/_constants.py +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/protpy/autocorrelation.py +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/protpy/composition.py +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/protpy/conjoint_triad.py +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/protpy/ctd.py +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/protpy/data/README.md +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/protpy/data/grantham-physicochemical-distance-matrix.json +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/protpy/data/schneider-wrede-physicochemical-distance-matrix.json +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/protpy/sequence_order.py +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/protpy/utils.py +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/protpy.egg-info/SOURCES.txt +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/protpy.egg-info/dependency_links.txt +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/protpy.egg-info/top_level.txt +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/setup.cfg +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/tests/__init__.py +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/tests/test_autocorrelation.py +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/tests/test_composition.py +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/tests/test_conjoint_triad.py +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/tests/test_ctd.py +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/tests/test_sequence_order.py +0 -0
- {protpy-1.4.0 → protpy-1.4.1}/tests/test_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: protpy
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.1
|
|
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>
|
|
@@ -35,6 +35,7 @@ License-File: LICENSE
|
|
|
35
35
|
Requires-Dist: aaindex>=1.2.0
|
|
36
36
|
Requires-Dist: numpy>=2.4.4
|
|
37
37
|
Requires-Dist: pandas>=3.0.2
|
|
38
|
+
Requires-Dist: tqdm>=4.0.0
|
|
38
39
|
Requires-Dist: varname>=0.15.1
|
|
39
40
|
Provides-Extra: test
|
|
40
41
|
Requires-Dist: pytest; extra == "test"
|
|
@@ -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.
|
|
16
|
+
release = '1.4.1'
|
|
17
17
|
|
|
18
18
|
# -- General configuration ---------------------------------------------------
|
|
19
19
|
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
|
|
@@ -4,7 +4,7 @@ from .conjoint_triad import *
|
|
|
4
4
|
from .ctd import *
|
|
5
5
|
from .sequence_order import *
|
|
6
6
|
from .utils import get_descriptor_info, list_descriptors, print_descriptor_info
|
|
7
|
-
from .encode import all_descriptors
|
|
7
|
+
from .encode import all_descriptors, encode_dataset, read_sequences_from_file
|
|
8
8
|
|
|
9
9
|
#canonical amino acids available in the package
|
|
10
10
|
amino_acids = ["A", "C", "D", "E", "F", "G", "H", "I", "K", "L", "M", "N", "P",
|
|
@@ -12,7 +12,7 @@ amino_acids = ["A", "C", "D", "E", "F", "G", "H", "I", "K", "L", "M", "N", "P",
|
|
|
12
12
|
|
|
13
13
|
#protPy package metadata
|
|
14
14
|
__name__ = 'protpy'
|
|
15
|
-
__version__ = "1.4.
|
|
15
|
+
__version__ = "1.4.1"
|
|
16
16
|
__description__ = "Python package for generating various biochemical, physicochemical and structural descriptors/features of protein sequences."
|
|
17
17
|
__author__ = 'AJ McKenna: https://github.com/amckenna41'
|
|
18
18
|
__authorEmail__ = 'amckenna41@qub.ac.uk'
|
|
@@ -0,0 +1,624 @@
|
|
|
1
|
+
################################################################################
|
|
2
|
+
############# Encode ##############
|
|
3
|
+
################################################################################
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import os
|
|
8
|
+
import pandas as pd
|
|
9
|
+
from tqdm import tqdm as _tqdm
|
|
10
|
+
|
|
11
|
+
from .composition import (
|
|
12
|
+
amino_acid_composition,
|
|
13
|
+
dipeptide_composition,
|
|
14
|
+
tripeptide_composition,
|
|
15
|
+
gravy,
|
|
16
|
+
aromaticity,
|
|
17
|
+
instability_index,
|
|
18
|
+
isoelectric_point,
|
|
19
|
+
molecular_weight,
|
|
20
|
+
charge_distribution,
|
|
21
|
+
hydrophobic_polar_charged_composition,
|
|
22
|
+
secondary_structure_propensity,
|
|
23
|
+
kmer_composition,
|
|
24
|
+
reduced_alphabet_composition,
|
|
25
|
+
motif_composition,
|
|
26
|
+
amino_acid_pair_composition,
|
|
27
|
+
aliphatic_index,
|
|
28
|
+
extinction_coefficient,
|
|
29
|
+
boman_index,
|
|
30
|
+
aggregation_propensity,
|
|
31
|
+
hydrophobic_moment,
|
|
32
|
+
shannon_entropy,
|
|
33
|
+
pseudo_amino_acid_composition,
|
|
34
|
+
amphiphilic_pseudo_amino_acid_composition,
|
|
35
|
+
)
|
|
36
|
+
from .autocorrelation import (
|
|
37
|
+
moreaubroto_autocorrelation,
|
|
38
|
+
moran_autocorrelation,
|
|
39
|
+
geary_autocorrelation,
|
|
40
|
+
)
|
|
41
|
+
from .conjoint_triad import conjoint_triad as _conjoint_triad
|
|
42
|
+
from .ctd import ctd_
|
|
43
|
+
from .sequence_order import (
|
|
44
|
+
sequence_order_coupling_number_all,
|
|
45
|
+
quasi_sequence_order_all,
|
|
46
|
+
)
|
|
47
|
+
from ._constants import _validate_sequence
|
|
48
|
+
|
|
49
|
+
#descriptors that accept extra keyword arguments beyond sequence
|
|
50
|
+
_PARAMETERISED_DESCRIPTORS: frozenset[str] = frozenset({
|
|
51
|
+
"charge_distribution",
|
|
52
|
+
"kmer_composition",
|
|
53
|
+
"reduced_alphabet_composition",
|
|
54
|
+
"motif_composition",
|
|
55
|
+
"aggregation_propensity",
|
|
56
|
+
"hydrophobic_moment",
|
|
57
|
+
"pseudo_amino_acid_composition",
|
|
58
|
+
"amphiphilic_pseudo_amino_acid_composition",
|
|
59
|
+
"moreaubroto_autocorrelation",
|
|
60
|
+
"moran_autocorrelation",
|
|
61
|
+
"geary_autocorrelation",
|
|
62
|
+
"ctd_",
|
|
63
|
+
"sequence_order_coupling_number_all",
|
|
64
|
+
"quasi_sequence_order_all",
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def all_descriptors(
|
|
69
|
+
sequence: str,
|
|
70
|
+
params: dict[str, dict] | None = None,
|
|
71
|
+
show_progress: bool = False,
|
|
72
|
+
output_file: str | None = "descriptors.csv",
|
|
73
|
+
save_each_sequence: bool = True,
|
|
74
|
+
_show_descriptor_list: bool = True,
|
|
75
|
+
) -> pd.DataFrame:
|
|
76
|
+
"""
|
|
77
|
+
Calculate all available protpy descriptors for a protein sequence and
|
|
78
|
+
return them concatenated into a single DataFrame (1 x N_total).
|
|
79
|
+
|
|
80
|
+
The combined variants of multi-property descriptors are used to avoid
|
|
81
|
+
redundancy: ``ctd_`` is called with ``all_ctd=True`` (covers
|
|
82
|
+
ctd_composition, ctd_transition and ctd_distribution across all 7
|
|
83
|
+
properties), and ``sequence_order_coupling_number_all`` /
|
|
84
|
+
``quasi_sequence_order_all`` cover both the schneider-wrede and grantham
|
|
85
|
+
distance matrices.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
==========
|
|
89
|
+
:sequence: str
|
|
90
|
+
Protein sequence string (case-insensitive, canonical amino acids only).
|
|
91
|
+
Must be longer than 30 residues to satisfy the default lag requirements
|
|
92
|
+
of autocorrelation and sequence-order descriptors (unless overridden
|
|
93
|
+
via ``params``).
|
|
94
|
+
|
|
95
|
+
:params: dict[str, dict] | None (default=None)
|
|
96
|
+
Optional per-descriptor keyword arguments. Keys must be descriptor
|
|
97
|
+
function names; values are dicts of keyword arguments passed directly
|
|
98
|
+
to that descriptor. Unrecognised descriptor names raise a ``ValueError``.
|
|
99
|
+
Descriptors not present in ``params`` use their default parameter values.
|
|
100
|
+
|
|
101
|
+
Only the following descriptors accept extra parameters::
|
|
102
|
+
|
|
103
|
+
charge_distribution → ph
|
|
104
|
+
kmer_composition → k
|
|
105
|
+
reduced_alphabet_composition → alphabet_size
|
|
106
|
+
motif_composition → motifs
|
|
107
|
+
aggregation_propensity → window, hydrophobicity_threshold,
|
|
108
|
+
charge_threshold
|
|
109
|
+
hydrophobic_moment → window, angle
|
|
110
|
+
pseudo_amino_acid_composition → lamda, weight, properties
|
|
111
|
+
amphiphilic_pseudo_amino_acid_composition
|
|
112
|
+
→ lamda, weight, properties
|
|
113
|
+
moreaubroto_autocorrelation → lag, properties, normalize
|
|
114
|
+
moran_autocorrelation → lag, properties, normalize
|
|
115
|
+
geary_autocorrelation → lag, properties, normalize
|
|
116
|
+
ctd_ → all_ctd
|
|
117
|
+
sequence_order_coupling_number_all → lag
|
|
118
|
+
quasi_sequence_order_all → lag, weight
|
|
119
|
+
|
|
120
|
+
Example::
|
|
121
|
+
|
|
122
|
+
df = protpy.all_descriptors(seq, params={
|
|
123
|
+
'charge_distribution': {'ph': 5.5},
|
|
124
|
+
'kmer_composition': {'k': 3},
|
|
125
|
+
'pseudo_amino_acid_composition': {'lamda': 10, 'weight': 0.1},
|
|
126
|
+
'moreaubroto_autocorrelation': {'lag': 15, 'normalize': False},
|
|
127
|
+
'quasi_sequence_order_all': {'lag': 20, 'weight': 0.2},
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
:show_progress: bool (default=False)
|
|
131
|
+
When ``True``, display a ``tqdm`` progress bar that names the
|
|
132
|
+
descriptor currently being computed and tracks overall progress.
|
|
133
|
+
|
|
134
|
+
:output_file: str | None (default="descriptors.csv")
|
|
135
|
+
Path to the CSV file where results are written. Pass ``None`` to
|
|
136
|
+
disable all file output. When calling this function in a loop over
|
|
137
|
+
multiple sequences the rows are accumulated in the file across calls;
|
|
138
|
+
each call appends one row to any previously saved sequences.
|
|
139
|
+
|
|
140
|
+
:save_each_sequence: bool (default=True)
|
|
141
|
+
When ``True``, the output CSV is updated once after all descriptors
|
|
142
|
+
for the current sequence have been computed, appending a complete
|
|
143
|
+
(no-NaN) row. When calling this function in a loop over multiple
|
|
144
|
+
sequences, each completed sequence is written immediately so that
|
|
145
|
+
progress is preserved if the process is interrupted. When ``False``,
|
|
146
|
+
nothing is written to the CSV within this call. Has no effect when
|
|
147
|
+
``output_file`` is ``None``.
|
|
148
|
+
|
|
149
|
+
Returns
|
|
150
|
+
=======
|
|
151
|
+
:descriptors_df: pd.DataFrame
|
|
152
|
+
DataFrame of shape 1 x N containing all descriptor values, where N is
|
|
153
|
+
the total number of features across all descriptors.
|
|
154
|
+
|
|
155
|
+
Raises
|
|
156
|
+
======
|
|
157
|
+
TypeError
|
|
158
|
+
If ``sequence`` is not a string, or if ``params`` is not a dict.
|
|
159
|
+
ValueError
|
|
160
|
+
If ``sequence`` is empty or contains non-canonical amino acid characters,
|
|
161
|
+
or if a key in ``params`` is not a recognised descriptor name.
|
|
162
|
+
|
|
163
|
+
Examples
|
|
164
|
+
========
|
|
165
|
+
>>> import protpy
|
|
166
|
+
>>> seq = "MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGD"
|
|
167
|
+
>>> df = protpy.all_descriptors(seq)
|
|
168
|
+
>>> df.shape[0]
|
|
169
|
+
1
|
|
170
|
+
>>> df = protpy.all_descriptors(seq, show_progress=True) # with progress bar
|
|
171
|
+
>>> df = protpy.all_descriptors(seq, output_file="out.csv") # save to file
|
|
172
|
+
>>> df = protpy.all_descriptors(seq, output_file=None) # no file output
|
|
173
|
+
"""
|
|
174
|
+
sequence = _validate_sequence(sequence)
|
|
175
|
+
|
|
176
|
+
if params is None:
|
|
177
|
+
params = {}
|
|
178
|
+
|
|
179
|
+
if not isinstance(params, dict):
|
|
180
|
+
raise TypeError(f"params must be a dict, got {type(params)}.")
|
|
181
|
+
|
|
182
|
+
unknown = set(params) - _PARAMETERISED_DESCRIPTORS
|
|
183
|
+
if unknown:
|
|
184
|
+
raise ValueError(
|
|
185
|
+
f"Unknown or non-parameterisable descriptor(s) in params: {sorted(unknown)}. "
|
|
186
|
+
f"Valid keys are: {sorted(_PARAMETERISED_DESCRIPTORS)}."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
def _p(name: str) -> dict:
|
|
190
|
+
return params.get(name, {})
|
|
191
|
+
|
|
192
|
+
# k=2 duplicates dipeptide_composition; k=3 duplicates tripeptide_composition.
|
|
193
|
+
# Only include kmer_composition when the user requests a k value that is unique.
|
|
194
|
+
_kmer_k = _p("kmer_composition").get("k", 2)
|
|
195
|
+
|
|
196
|
+
steps: list[tuple[str, object]] = [
|
|
197
|
+
# ── Composition ────────────────────────────────────────────────────────
|
|
198
|
+
("[Composition] Amino acid composition", lambda: amino_acid_composition(sequence)),
|
|
199
|
+
("[Composition] Dipeptide composition", lambda: dipeptide_composition(sequence)),
|
|
200
|
+
("[Composition] Tripeptide composition", lambda: tripeptide_composition(sequence)),
|
|
201
|
+
("[Composition] GRAVY index", lambda: gravy(sequence)),
|
|
202
|
+
("[Composition] Aromaticity", lambda: aromaticity(sequence)),
|
|
203
|
+
("[Composition] Instability index", lambda: instability_index(sequence)),
|
|
204
|
+
("[Composition] Isoelectric point", lambda: isoelectric_point(sequence)),
|
|
205
|
+
("[Composition] Molecular weight", lambda: molecular_weight(sequence)),
|
|
206
|
+
("[Composition] Charge distribution", lambda: charge_distribution(sequence, **_p("charge_distribution"))),
|
|
207
|
+
("[Composition] Hydrophobic / polar / charged comp.", lambda: hydrophobic_polar_charged_composition(sequence)),
|
|
208
|
+
("[Composition] Secondary structure propensity", lambda: secondary_structure_propensity(sequence)),
|
|
209
|
+
*([("[Composition] K-mer composition", lambda: kmer_composition(sequence, **_p("kmer_composition")))]
|
|
210
|
+
if _kmer_k not in (2, 3) else []),
|
|
211
|
+
("[Composition] Reduced alphabet composition", lambda: reduced_alphabet_composition(sequence, **_p("reduced_alphabet_composition"))),
|
|
212
|
+
("[Composition] Motif composition", lambda: motif_composition(sequence, **_p("motif_composition"))),
|
|
213
|
+
("[Composition] Amino acid pair composition", lambda: amino_acid_pair_composition(sequence)),
|
|
214
|
+
("[Composition] Aliphatic index", lambda: aliphatic_index(sequence)),
|
|
215
|
+
("[Composition] Extinction coefficient", lambda: extinction_coefficient(sequence)),
|
|
216
|
+
("[Composition] Boman index", lambda: boman_index(sequence)),
|
|
217
|
+
("[Composition] Aggregation propensity", lambda: aggregation_propensity(sequence, **_p("aggregation_propensity"))),
|
|
218
|
+
("[Composition] Hydrophobic moment", lambda: hydrophobic_moment(sequence, **_p("hydrophobic_moment"))),
|
|
219
|
+
("[Composition] Shannon entropy", lambda: shannon_entropy(sequence)),
|
|
220
|
+
("[Composition] Pseudo amino acid composition", lambda: pseudo_amino_acid_composition(sequence, **_p("pseudo_amino_acid_composition"))),
|
|
221
|
+
("[Composition] Amphiphilic pseudo amino acid comp.", lambda: amphiphilic_pseudo_amino_acid_composition(sequence, **_p("amphiphilic_pseudo_amino_acid_composition"))),
|
|
222
|
+
# ── Autocorrelation ────────────────────────────────────────────────────
|
|
223
|
+
("[Autocorrelation] Moreaubroto autocorrelation", lambda: moreaubroto_autocorrelation(sequence, **_p("moreaubroto_autocorrelation"))),
|
|
224
|
+
("[Autocorrelation] Moran autocorrelation", lambda: moran_autocorrelation(sequence, **_p("moran_autocorrelation"))),
|
|
225
|
+
("[Autocorrelation] Geary autocorrelation", lambda: geary_autocorrelation(sequence, **_p("geary_autocorrelation"))),
|
|
226
|
+
# ── Conjoint Triad ─────────────────────────────────────────────────────
|
|
227
|
+
("[Conjoint Triad] Conjoint triad", lambda: _conjoint_triad(sequence)),
|
|
228
|
+
# ── CTD ────────────────────────────────────────────────────────────────
|
|
229
|
+
("[CTD] Composition, transition & distribution",lambda: ctd_(sequence, all_ctd=True, **_p("ctd_"))),
|
|
230
|
+
# ── Sequence Order ─────────────────────────────────────────────────────
|
|
231
|
+
("[Sequence Order] Sequence order coupling number", lambda: sequence_order_coupling_number_all(sequence, **_p("sequence_order_coupling_number_all"))),
|
|
232
|
+
("[Sequence Order] Quasi sequence order", lambda: quasi_sequence_order_all(sequence, **_p("quasi_sequence_order_all"))),
|
|
233
|
+
]
|
|
234
|
+
|
|
235
|
+
descriptor_names = [label.split("]", 1)[1].strip() for label, _ in steps]
|
|
236
|
+
if _show_descriptor_list:
|
|
237
|
+
print("Calculating descriptors: " + ", ".join(descriptor_names))
|
|
238
|
+
|
|
239
|
+
# Load any rows already saved from previous sequences in this run so they
|
|
240
|
+
# are preserved when we rewrite the file after each checkpoint.
|
|
241
|
+
existing_df: pd.DataFrame | None = None
|
|
242
|
+
if output_file is not None and os.path.isfile(output_file):
|
|
243
|
+
try:
|
|
244
|
+
existing_df = pd.read_csv(output_file)
|
|
245
|
+
except Exception:
|
|
246
|
+
existing_df = None
|
|
247
|
+
|
|
248
|
+
def _checkpoint(frames_so_far: list[pd.DataFrame]) -> None:
|
|
249
|
+
partial = pd.concat(frames_so_far, axis=1)
|
|
250
|
+
out = pd.concat([existing_df, partial], ignore_index=True) if existing_df is not None else partial
|
|
251
|
+
out.to_csv(output_file, index=False)
|
|
252
|
+
|
|
253
|
+
frames: list[pd.DataFrame] = []
|
|
254
|
+
|
|
255
|
+
if show_progress:
|
|
256
|
+
bar = _tqdm(
|
|
257
|
+
total=len(steps),
|
|
258
|
+
unit="descriptor",
|
|
259
|
+
dynamic_ncols=True,
|
|
260
|
+
bar_format="{desc} {percentage:3.0f}%|{bar:28}| {n:02d}/{total} [{elapsed}<{remaining}]",
|
|
261
|
+
)
|
|
262
|
+
for label, fn in steps:
|
|
263
|
+
bar.set_description(label)
|
|
264
|
+
frames.append(fn())
|
|
265
|
+
bar.update(1)
|
|
266
|
+
bar.set_description(f"All {len(steps)} descriptors computed")
|
|
267
|
+
bar.close()
|
|
268
|
+
else:
|
|
269
|
+
for label, fn in steps:
|
|
270
|
+
frames.append(fn())
|
|
271
|
+
|
|
272
|
+
result = pd.concat(frames, axis=1)
|
|
273
|
+
|
|
274
|
+
if output_file is not None and save_each_sequence:
|
|
275
|
+
_checkpoint(frames)
|
|
276
|
+
|
|
277
|
+
return result
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def encode_dataset(
|
|
281
|
+
sequences: "list[str] | pd.DataFrame",
|
|
282
|
+
seq_col: str = "sequence",
|
|
283
|
+
params: dict[str, dict] | None = None,
|
|
284
|
+
show_progress: bool = False,
|
|
285
|
+
output_file: str | None = "descriptors.csv",
|
|
286
|
+
save_each_sequence: bool = True,
|
|
287
|
+
strip_gaps: bool = False,
|
|
288
|
+
) -> pd.DataFrame:
|
|
289
|
+
"""
|
|
290
|
+
Compute all available descriptors for every sequence in a dataset and
|
|
291
|
+
return a DataFrame whose **first column is a unique sequence identifier**.
|
|
292
|
+
|
|
293
|
+
Parameters
|
|
294
|
+
==========
|
|
295
|
+
:sequences: list[str] | pd.DataFrame
|
|
296
|
+
Input sequences in one of two forms:
|
|
297
|
+
|
|
298
|
+
- ``list[str]`` — a plain list of protein sequences. Identifiers are
|
|
299
|
+
generated automatically as ``seq1, seq2, seq3, …`` and the
|
|
300
|
+
identifier column is named ``"sequence_id"``.
|
|
301
|
+
- ``pd.DataFrame`` — typically the output of
|
|
302
|
+
``read_sequences_from_file(..., id_col=…)``, which has two columns
|
|
303
|
+
``[id_col, seq_col]``. The first column is used as the identifier;
|
|
304
|
+
``seq_col`` selects the sequence data.
|
|
305
|
+
|
|
306
|
+
:seq_col: str (default="sequence")
|
|
307
|
+
Name of the column containing sequences when *sequences* is a
|
|
308
|
+
``pd.DataFrame``.
|
|
309
|
+
|
|
310
|
+
:params: dict[str, dict] | None (default=None)
|
|
311
|
+
Per-descriptor keyword arguments forwarded to each ``all_descriptors``
|
|
312
|
+
call. See ``all_descriptors`` for the full list of accepted keys.
|
|
313
|
+
|
|
314
|
+
:show_progress: bool (default=False)
|
|
315
|
+
When ``True``, a ``tqdm`` progress bar is displayed while computing
|
|
316
|
+
descriptors for each individual sequence.
|
|
317
|
+
|
|
318
|
+
:output_file: str | None (default="descriptors.csv")
|
|
319
|
+
Path to the CSV file where results are written. Each completed row
|
|
320
|
+
(identifier prepended) is flushed immediately after that sequence
|
|
321
|
+
finishes, so progress is preserved if the process is interrupted.
|
|
322
|
+
Rows already present in the file from a previous run are retained.
|
|
323
|
+
Pass ``None`` to disable all file output.
|
|
324
|
+
|
|
325
|
+
:save_each_sequence: bool (default=True)
|
|
326
|
+
When ``True``, the CSV is updated after each sequence completes.
|
|
327
|
+
When ``False``, the file is never written inside this call. Has no
|
|
328
|
+
effect when ``output_file`` is ``None``.
|
|
329
|
+
|
|
330
|
+
:strip_gaps: bool (default=False)
|
|
331
|
+
When ``True``, gap characters (``-``) are removed from each sequence
|
|
332
|
+
before encoding. Useful for sequences derived from multiple sequence
|
|
333
|
+
alignments (MSAs) where ``-`` denotes an alignment gap rather than an
|
|
334
|
+
amino acid. When ``False`` (default), a ``-`` in any sequence raises
|
|
335
|
+
a ``ValueError``.
|
|
336
|
+
|
|
337
|
+
Returns
|
|
338
|
+
=======
|
|
339
|
+
:result: pd.DataFrame
|
|
340
|
+
Shape ``(n_sequences, 1 + n_features)`` where the first column holds
|
|
341
|
+
the sequence identifier and the remaining columns are descriptor
|
|
342
|
+
values. Returns an empty DataFrame when *sequences* is empty.
|
|
343
|
+
|
|
344
|
+
Raises
|
|
345
|
+
======
|
|
346
|
+
TypeError
|
|
347
|
+
If *sequences* is neither a ``list`` nor a ``pd.DataFrame``.
|
|
348
|
+
ValueError
|
|
349
|
+
If *sequences* is a ``pd.DataFrame`` and *seq_col* is not present.
|
|
350
|
+
|
|
351
|
+
Examples
|
|
352
|
+
========
|
|
353
|
+
>>> import protpy
|
|
354
|
+
>>> # Plain list — identifiers auto-generated as seq1, seq2, …
|
|
355
|
+
>>> seqs = ["MKTAYIAKQRQISFVKSHFSRQLEERLGLIEVQAPILSRVGD",
|
|
356
|
+
... "ACDEFGHIKLMNPQRSTVWYACDEFGHIKLMNPQRSTVWY"]
|
|
357
|
+
>>> df = protpy.encode_dataset(seqs)
|
|
358
|
+
>>> df.columns[0]
|
|
359
|
+
'sequence_id'
|
|
360
|
+
>>> df["sequence_id"].tolist()
|
|
361
|
+
['seq1', 'seq2']
|
|
362
|
+
|
|
363
|
+
>>> # DataFrame with named identifiers (e.g. from thermostability.txt)
|
|
364
|
+
>>> data = protpy.read_sequences_from_file(
|
|
365
|
+
... "thermostability.txt", seq_col="sequence", id_col="name"
|
|
366
|
+
... )
|
|
367
|
+
>>> df = protpy.encode_dataset(data, seq_col="sequence")
|
|
368
|
+
>>> df.columns[0]
|
|
369
|
+
'name'
|
|
370
|
+
"""
|
|
371
|
+
if isinstance(sequences, list):
|
|
372
|
+
id_col_name = "sequence_id"
|
|
373
|
+
ids = [f"seq{i + 1}" for i in range(len(sequences))]
|
|
374
|
+
seqs = [str(s) for s in sequences]
|
|
375
|
+
elif isinstance(sequences, pd.DataFrame):
|
|
376
|
+
if sequences.empty:
|
|
377
|
+
return pd.DataFrame()
|
|
378
|
+
id_col_name = sequences.columns[0]
|
|
379
|
+
if seq_col not in sequences.columns:
|
|
380
|
+
raise ValueError(
|
|
381
|
+
f"seq_col '{seq_col}' not found in DataFrame. "
|
|
382
|
+
f"Available columns: {list(sequences.columns)}."
|
|
383
|
+
)
|
|
384
|
+
ids = sequences[id_col_name].tolist()
|
|
385
|
+
seqs = sequences[seq_col].tolist()
|
|
386
|
+
else:
|
|
387
|
+
raise TypeError(
|
|
388
|
+
f"sequences must be a list[str] or pd.DataFrame, got {type(sequences)}."
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
# Snapshot any previously saved rows so they are preserved on each flush.
|
|
392
|
+
initial_df: pd.DataFrame | None = None
|
|
393
|
+
if output_file is not None and os.path.isfile(output_file):
|
|
394
|
+
try:
|
|
395
|
+
initial_df = pd.read_csv(output_file)
|
|
396
|
+
except Exception:
|
|
397
|
+
pass
|
|
398
|
+
|
|
399
|
+
completed: list[pd.DataFrame] = []
|
|
400
|
+
total = len(seqs)
|
|
401
|
+
|
|
402
|
+
for idx, (seq_id, seq) in enumerate(zip(ids, seqs), start=1):
|
|
403
|
+
print(f"Encoding sequence {idx}/{total}: {seq_id}")
|
|
404
|
+
if strip_gaps:
|
|
405
|
+
seq = seq.replace("-", "")
|
|
406
|
+
elif "-" in seq:
|
|
407
|
+
raise ValueError(
|
|
408
|
+
f"Sequence '{seq_id}' contains gap characters ('-'). "
|
|
409
|
+
"If the sequences are from a multiple sequence alignment (MSA), "
|
|
410
|
+
"set strip_gaps=True to remove them before encoding."
|
|
411
|
+
)
|
|
412
|
+
row_df = all_descriptors(
|
|
413
|
+
seq,
|
|
414
|
+
params=params,
|
|
415
|
+
show_progress=show_progress,
|
|
416
|
+
output_file=None,
|
|
417
|
+
save_each_sequence=False,
|
|
418
|
+
_show_descriptor_list=(idx == 1),
|
|
419
|
+
)
|
|
420
|
+
row_df.insert(0, id_col_name, seq_id)
|
|
421
|
+
completed.append(row_df)
|
|
422
|
+
|
|
423
|
+
if output_file is not None and save_each_sequence:
|
|
424
|
+
new_rows = pd.concat(completed, ignore_index=True)
|
|
425
|
+
# Only prepend the initial snapshot when its columns exactly match the
|
|
426
|
+
# current run (mismatches arise from stale CSVs with mangled duplicate
|
|
427
|
+
# column names written by a prior buggy run).
|
|
428
|
+
if initial_df is not None and list(initial_df.columns) == list(new_rows.columns):
|
|
429
|
+
out = pd.concat([initial_df, new_rows], ignore_index=True)
|
|
430
|
+
else:
|
|
431
|
+
out = new_rows
|
|
432
|
+
out.to_csv(output_file, index=False)
|
|
433
|
+
|
|
434
|
+
if not completed:
|
|
435
|
+
return pd.DataFrame()
|
|
436
|
+
return pd.concat(completed, ignore_index=True)
|
|
437
|
+
|
|
438
|
+
|
|
439
|
+
def _parse_fasta(filepath: str) -> list[str]:
|
|
440
|
+
sequences: list[str] = []
|
|
441
|
+
current: list[str] = []
|
|
442
|
+
with open(filepath) as fh:
|
|
443
|
+
for line in fh:
|
|
444
|
+
line = line.strip()
|
|
445
|
+
if not line:
|
|
446
|
+
continue
|
|
447
|
+
if line.startswith(">"):
|
|
448
|
+
if current:
|
|
449
|
+
sequences.append("".join(current).upper())
|
|
450
|
+
current = []
|
|
451
|
+
else:
|
|
452
|
+
current.append(line)
|
|
453
|
+
if current:
|
|
454
|
+
sequences.append("".join(current).upper())
|
|
455
|
+
return sequences
|
|
456
|
+
|
|
457
|
+
|
|
458
|
+
def _parse_txt(filepath: str) -> list[str]:
|
|
459
|
+
sequences: list[str] = []
|
|
460
|
+
with open(filepath) as fh:
|
|
461
|
+
for line in fh:
|
|
462
|
+
seq = line.strip().upper()
|
|
463
|
+
if seq:
|
|
464
|
+
sequences.append(seq)
|
|
465
|
+
return sequences
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def _detect_sep(filepath: str) -> str:
|
|
469
|
+
with open(filepath) as fh:
|
|
470
|
+
first_line = fh.readline()
|
|
471
|
+
return "\t" if "\t" in first_line else ","
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
def _resolve_col(
|
|
475
|
+
df: pd.DataFrame,
|
|
476
|
+
col: str | int | None,
|
|
477
|
+
param_name: str,
|
|
478
|
+
) -> str:
|
|
479
|
+
if col is None:
|
|
480
|
+
return df.columns[0]
|
|
481
|
+
if isinstance(col, int):
|
|
482
|
+
try:
|
|
483
|
+
return df.columns[col]
|
|
484
|
+
except IndexError:
|
|
485
|
+
raise ValueError(
|
|
486
|
+
f"{param_name} index {col} is out of range for a file with "
|
|
487
|
+
f"{df.shape[1]} column(s)."
|
|
488
|
+
)
|
|
489
|
+
if isinstance(col, str):
|
|
490
|
+
if col not in df.columns:
|
|
491
|
+
raise ValueError(
|
|
492
|
+
f"{param_name} '{col}' not found. Available columns: {list(df.columns)}."
|
|
493
|
+
)
|
|
494
|
+
return col
|
|
495
|
+
raise TypeError(f"{param_name} must be a str, int, or None, got {type(col)}.")
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _parse_delimited(
|
|
499
|
+
filepath: str,
|
|
500
|
+
sep: str,
|
|
501
|
+
seq_col: str | int | None,
|
|
502
|
+
id_col: str | int | None,
|
|
503
|
+
has_header: bool,
|
|
504
|
+
) -> "list[str] | pd.DataFrame":
|
|
505
|
+
df = pd.read_csv(filepath, sep=sep, header=0 if has_header else None)
|
|
506
|
+
|
|
507
|
+
seq_label = _resolve_col(df, seq_col, "seq_col")
|
|
508
|
+
|
|
509
|
+
if id_col is None:
|
|
510
|
+
raw = df[seq_label]
|
|
511
|
+
return [str(s).strip().upper() for s in raw if pd.notna(s) and str(s).strip()]
|
|
512
|
+
|
|
513
|
+
id_label = _resolve_col(df, id_col, "id_col")
|
|
514
|
+
result = df[[id_label, seq_label]].copy()
|
|
515
|
+
result = result[result[seq_label].notna()].copy()
|
|
516
|
+
result[seq_label] = result[seq_label].astype(str).str.strip().str.upper()
|
|
517
|
+
result = result[result[seq_label].str.len() > 0].reset_index(drop=True)
|
|
518
|
+
return result
|
|
519
|
+
|
|
520
|
+
|
|
521
|
+
def read_sequences_from_file(
|
|
522
|
+
filepath: str | os.PathLike,
|
|
523
|
+
seq_col: str | int | None = None,
|
|
524
|
+
id_col: str | int | None = None,
|
|
525
|
+
has_header: bool = True,
|
|
526
|
+
) -> "list[str] | pd.DataFrame":
|
|
527
|
+
"""
|
|
528
|
+
Parse protein sequences from a FASTA, TXT, TSV, or CSV file.
|
|
529
|
+
|
|
530
|
+
The file format is inferred from the extension:
|
|
531
|
+
|
|
532
|
+
- ``.fasta`` / ``.fa`` — standard FASTA format; multi-line sequences are
|
|
533
|
+
joined automatically; every record in the file is returned as a
|
|
534
|
+
``list[str]``.
|
|
535
|
+
- ``.txt`` without ``seq_col`` — one raw sequence per non-blank line,
|
|
536
|
+
returned as a ``list[str]``.
|
|
537
|
+
- ``.txt`` with ``seq_col`` — treated as a delimited file; the separator
|
|
538
|
+
(``,`` or ``\\t``) is auto-detected from the first line.
|
|
539
|
+
- ``.csv`` — comma-separated values.
|
|
540
|
+
- ``.tsv`` — tab-separated values.
|
|
541
|
+
|
|
542
|
+
When ``id_col`` is provided the function returns a ``pd.DataFrame``
|
|
543
|
+
containing only the identifier and sequence columns; all other columns
|
|
544
|
+
are dropped and rows with blank sequences are removed. When ``id_col``
|
|
545
|
+
is omitted the function returns a plain ``list[str]`` of sequences.
|
|
546
|
+
|
|
547
|
+
Parameters
|
|
548
|
+
==========
|
|
549
|
+
:filepath: str | os.PathLike
|
|
550
|
+
Path to the input file.
|
|
551
|
+
|
|
552
|
+
:seq_col: str | int | None (default=None)
|
|
553
|
+
The column that contains sequences. Pass a column *name* (str) to
|
|
554
|
+
select by header, or a zero-based *index* (int) to select by
|
|
555
|
+
position. ``None`` selects the first column. Ignored for plain
|
|
556
|
+
FASTA and plain TXT files.
|
|
557
|
+
|
|
558
|
+
:id_col: str | int | None (default=None)
|
|
559
|
+
The column that identifies each sequence row (e.g. ``"name"``).
|
|
560
|
+
When provided, all other columns are dropped and a ``pd.DataFrame``
|
|
561
|
+
with columns ``[id_col, seq_col]`` is returned. ``None`` returns a
|
|
562
|
+
``list[str]`` of sequences only. Ignored for FASTA files.
|
|
563
|
+
|
|
564
|
+
:has_header: bool (default=True)
|
|
565
|
+
Whether the first row of a delimited file is a header. Set to
|
|
566
|
+
``False`` for headerless files (column names are then integers).
|
|
567
|
+
Ignored for FASTA and plain TXT files.
|
|
568
|
+
|
|
569
|
+
Returns
|
|
570
|
+
=======
|
|
571
|
+
:result: list[str] | pd.DataFrame
|
|
572
|
+
- ``list[str]`` when ``id_col`` is ``None``: uppercase sequences,
|
|
573
|
+
blank entries excluded.
|
|
574
|
+
- ``pd.DataFrame`` when ``id_col`` is given: two-column frame with
|
|
575
|
+
the identifier and (uppercased) sequence columns; rows with blank
|
|
576
|
+
sequences are excluded and the index is reset.
|
|
577
|
+
|
|
578
|
+
Raises
|
|
579
|
+
======
|
|
580
|
+
FileNotFoundError
|
|
581
|
+
If the file at ``filepath`` does not exist.
|
|
582
|
+
ValueError
|
|
583
|
+
If the extension is unsupported, or if a requested column is not
|
|
584
|
+
found in a delimited file.
|
|
585
|
+
TypeError
|
|
586
|
+
If ``seq_col`` or ``id_col`` is not a str, int, or None.
|
|
587
|
+
|
|
588
|
+
Examples
|
|
589
|
+
========
|
|
590
|
+
>>> import protpy
|
|
591
|
+
>>> # FASTA / plain TXT
|
|
592
|
+
>>> seqs = protpy.read_sequences_from_file("proteins.fasta")
|
|
593
|
+
>>> seqs = protpy.read_sequences_from_file("seqs.txt")
|
|
594
|
+
>>> # CSV / TSV — sequences only
|
|
595
|
+
>>> seqs = protpy.read_sequences_from_file("data.csv", seq_col="sequence")
|
|
596
|
+
>>> seqs = protpy.read_sequences_from_file("data.tsv", seq_col=1)
|
|
597
|
+
>>> # Multi-column TXT/CSV/TSV — keep id + sequence, drop everything else
|
|
598
|
+
>>> df = protpy.read_sequences_from_file(
|
|
599
|
+
... "thermostability.txt", seq_col="sequence", id_col="name"
|
|
600
|
+
... )
|
|
601
|
+
"""
|
|
602
|
+
filepath = str(filepath)
|
|
603
|
+
|
|
604
|
+
if not os.path.isfile(filepath):
|
|
605
|
+
raise FileNotFoundError(f"No such file: '{filepath}'")
|
|
606
|
+
|
|
607
|
+
ext = os.path.splitext(filepath)[1].lower()
|
|
608
|
+
|
|
609
|
+
if ext in (".fasta", ".fa"):
|
|
610
|
+
return _parse_fasta(filepath)
|
|
611
|
+
elif ext == ".txt":
|
|
612
|
+
if seq_col is not None:
|
|
613
|
+
sep = _detect_sep(filepath)
|
|
614
|
+
return _parse_delimited(filepath, sep=sep, seq_col=seq_col, id_col=id_col, has_header=has_header)
|
|
615
|
+
return _parse_txt(filepath)
|
|
616
|
+
elif ext == ".csv":
|
|
617
|
+
return _parse_delimited(filepath, sep=",", seq_col=seq_col, id_col=id_col, has_header=has_header)
|
|
618
|
+
elif ext == ".tsv":
|
|
619
|
+
return _parse_delimited(filepath, sep="\t", seq_col=seq_col, id_col=id_col, has_header=has_header)
|
|
620
|
+
else:
|
|
621
|
+
raise ValueError(
|
|
622
|
+
f"Unsupported file extension '{ext}'. "
|
|
623
|
+
"Expected one of: .fasta, .fa, .txt, .csv, .tsv."
|
|
624
|
+
)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: protpy
|
|
3
|
-
Version: 1.4.
|
|
3
|
+
Version: 1.4.1
|
|
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>
|
|
@@ -35,6 +35,7 @@ License-File: LICENSE
|
|
|
35
35
|
Requires-Dist: aaindex>=1.2.0
|
|
36
36
|
Requires-Dist: numpy>=2.4.4
|
|
37
37
|
Requires-Dist: pandas>=3.0.2
|
|
38
|
+
Requires-Dist: tqdm>=4.0.0
|
|
38
39
|
Requires-Dist: varname>=0.15.1
|
|
39
40
|
Provides-Extra: test
|
|
40
41
|
Requires-Dist: pytest; extra == "test"
|