gp3sequencespy 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gp3sequencespy/__init__.py +220 -0
- gp3sequencespy/_advanced.py +441 -0
- gp3sequencespy/_exceptions.py +14 -0
- gp3sequencespy/_types.py +189 -0
- gp3sequencespy/adapters.py +256 -0
- gp3sequencespy/analysis_audit.py +462 -0
- gp3sequencespy/capabilities.py +102 -0
- gp3sequencespy/consensus.py +394 -0
- gp3sequencespy/covariate_hmm.py +576 -0
- gp3sequencespy/data.py +979 -0
- gp3sequencespy/distances.py +878 -0
- gp3sequencespy/hmm.py +644 -0
- gp3sequencespy/inference.py +342 -0
- gp3sequencespy/motif_visualisation.py +550 -0
- gp3sequencespy/motifs.py +517 -0
- gp3sequencespy/multichannel_hmm.py +425 -0
- gp3sequencespy/networks.py +514 -0
- gp3sequencespy/panel.py +282 -0
- gp3sequencespy/py.typed +0 -0
- gp3sequencespy/subsequences.py +347 -0
- gp3sequencespy/summaries.py +441 -0
- gp3sequencespy/time_models.py +450 -0
- gp3sequencespy/visualisations.py +420 -0
- gp3sequencespy-0.1.0.dist-info/METADATA +103 -0
- gp3sequencespy-0.1.0.dist-info/RECORD +27 -0
- gp3sequencespy-0.1.0.dist-info/WHEEL +4 -0
- gp3sequencespy-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""gp3sequencespy: transparent ordered categorical sequence analysis."""
|
|
2
|
+
|
|
3
|
+
from ._exceptions import GP3SequencesError, ModelFitError, ParityError, ValidationError
|
|
4
|
+
from .adapters import (
|
|
5
|
+
ArulesSequenceAdapter,
|
|
6
|
+
GrpStringInput,
|
|
7
|
+
WideSequenceAdapter,
|
|
8
|
+
as_arules_sequences,
|
|
9
|
+
as_grpstring_data,
|
|
10
|
+
as_igraph_transition_network,
|
|
11
|
+
as_seqhmm_sequences,
|
|
12
|
+
as_traminer_sequences,
|
|
13
|
+
prepare_gp3tools_sequences,
|
|
14
|
+
)
|
|
15
|
+
from .analysis_audit import (
|
|
16
|
+
SequenceAnalysisAudit,
|
|
17
|
+
SequenceAnalysisComparison,
|
|
18
|
+
audit_sequence_analysis,
|
|
19
|
+
compare_sequence_analysis_results,
|
|
20
|
+
)
|
|
21
|
+
from .capabilities import sequence_capabilities
|
|
22
|
+
from .consensus import (
|
|
23
|
+
compare_sequence_groups,
|
|
24
|
+
create_consensus_sequence,
|
|
25
|
+
format_consensus_sequence,
|
|
26
|
+
summarise_consensus_agreement,
|
|
27
|
+
)
|
|
28
|
+
from .covariate_hmm import (
|
|
29
|
+
CovariateSequenceHMM,
|
|
30
|
+
decode_covariate_sequence_states,
|
|
31
|
+
fit_covariate_sequence_hmm,
|
|
32
|
+
predict_covariate_transition_probabilities,
|
|
33
|
+
summarise_covariate_sequence_hmm,
|
|
34
|
+
)
|
|
35
|
+
from .data import audit_sequence_data, prepare_sequence_data, validate_sequence_data
|
|
36
|
+
from .distances import (
|
|
37
|
+
bootstrap_sequence_clusters,
|
|
38
|
+
cluster_sequences,
|
|
39
|
+
compute_sequence_distance,
|
|
40
|
+
create_sequence_cluster_ensemble,
|
|
41
|
+
extract_representative_sequences,
|
|
42
|
+
summarise_sequence_cluster_stability,
|
|
43
|
+
summarise_sequence_distance,
|
|
44
|
+
validate_sequence_clusters,
|
|
45
|
+
)
|
|
46
|
+
from .hmm import (
|
|
47
|
+
compare_sequence_hmms,
|
|
48
|
+
decode_sequence_states,
|
|
49
|
+
fit_sequence_hmm,
|
|
50
|
+
fit_sequence_hmm_mixture,
|
|
51
|
+
summarise_sequence_hmm,
|
|
52
|
+
)
|
|
53
|
+
from .inference import (
|
|
54
|
+
bootstrap_sequence_group_difference,
|
|
55
|
+
declare_sequence_comparison_design,
|
|
56
|
+
plot_sequence_group_inference,
|
|
57
|
+
summarise_sequence_group_inference,
|
|
58
|
+
test_sequence_group_difference,
|
|
59
|
+
)
|
|
60
|
+
from .motif_visualisation import (
|
|
61
|
+
MotifPositionResult,
|
|
62
|
+
format_sequence_motif_positions,
|
|
63
|
+
plot_sequence_motif_positions,
|
|
64
|
+
plot_sequence_motifs,
|
|
65
|
+
summarise_sequence_motif_positions,
|
|
66
|
+
)
|
|
67
|
+
from .motifs import (
|
|
68
|
+
extract_sequence_ngrams,
|
|
69
|
+
filter_sequence_motifs,
|
|
70
|
+
format_sequence_motifs,
|
|
71
|
+
summarise_sequence_motifs,
|
|
72
|
+
)
|
|
73
|
+
from .multichannel_hmm import (
|
|
74
|
+
decode_multichannel_sequence_states,
|
|
75
|
+
fit_multichannel_sequence_hmm,
|
|
76
|
+
plot_multichannel_sequence_hmm,
|
|
77
|
+
summarise_multichannel_sequence_hmm,
|
|
78
|
+
)
|
|
79
|
+
from .networks import (
|
|
80
|
+
bootstrap_transition_network,
|
|
81
|
+
create_transition_network,
|
|
82
|
+
detect_transition_communities,
|
|
83
|
+
fit_higher_order_transition_model,
|
|
84
|
+
predict_next_state,
|
|
85
|
+
summarise_transition_centrality,
|
|
86
|
+
)
|
|
87
|
+
from .panel import (
|
|
88
|
+
compare_sequence_panel_changes,
|
|
89
|
+
plot_sequence_panel_changes,
|
|
90
|
+
prepare_sequence_panel,
|
|
91
|
+
summarise_sequence_panel,
|
|
92
|
+
)
|
|
93
|
+
from .subsequences import (
|
|
94
|
+
compare_sequence_subsequences,
|
|
95
|
+
extract_sequence_subsequences,
|
|
96
|
+
filter_sequence_subsequences,
|
|
97
|
+
plot_sequence_subsequences,
|
|
98
|
+
summarise_sequence_subsequences,
|
|
99
|
+
)
|
|
100
|
+
from .summaries import (
|
|
101
|
+
encode_sequence_data,
|
|
102
|
+
format_sequence_paths,
|
|
103
|
+
summarise_sequence_states,
|
|
104
|
+
summarise_sequence_transitions,
|
|
105
|
+
)
|
|
106
|
+
from .time_models import (
|
|
107
|
+
TimeVaryingSequenceModel,
|
|
108
|
+
fit_time_varying_sequence_model,
|
|
109
|
+
plot_time_varying_sequence_model,
|
|
110
|
+
predict_time_varying_sequence_model,
|
|
111
|
+
summarise_time_varying_sequence_model,
|
|
112
|
+
)
|
|
113
|
+
from .visualisations import (
|
|
114
|
+
plot_consensus_sequence,
|
|
115
|
+
plot_sequence_cluster_silhouette,
|
|
116
|
+
plot_sequence_distance_heatmap,
|
|
117
|
+
plot_sequence_entropy,
|
|
118
|
+
plot_sequence_group_comparison,
|
|
119
|
+
plot_sequence_index,
|
|
120
|
+
plot_sequence_state_distribution,
|
|
121
|
+
plot_transition_network,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
__version__ = "0.1.0"
|
|
125
|
+
|
|
126
|
+
__all__ = [
|
|
127
|
+
"GP3SequencesError",
|
|
128
|
+
"ValidationError",
|
|
129
|
+
"ModelFitError",
|
|
130
|
+
"ParityError",
|
|
131
|
+
"CovariateSequenceHMM",
|
|
132
|
+
"TimeVaryingSequenceModel",
|
|
133
|
+
"MotifPositionResult",
|
|
134
|
+
"WideSequenceAdapter",
|
|
135
|
+
"ArulesSequenceAdapter",
|
|
136
|
+
"GrpStringInput",
|
|
137
|
+
"SequenceAnalysisAudit",
|
|
138
|
+
"SequenceAnalysisComparison",
|
|
139
|
+
"as_traminer_sequences",
|
|
140
|
+
"as_arules_sequences",
|
|
141
|
+
"as_grpstring_data",
|
|
142
|
+
"as_seqhmm_sequences",
|
|
143
|
+
"as_igraph_transition_network",
|
|
144
|
+
"prepare_gp3tools_sequences",
|
|
145
|
+
"sequence_capabilities",
|
|
146
|
+
"audit_sequence_analysis",
|
|
147
|
+
"compare_sequence_analysis_results",
|
|
148
|
+
"plot_consensus_sequence",
|
|
149
|
+
"plot_sequence_group_comparison",
|
|
150
|
+
"plot_sequence_index",
|
|
151
|
+
"plot_sequence_state_distribution",
|
|
152
|
+
"plot_sequence_entropy",
|
|
153
|
+
"plot_sequence_distance_heatmap",
|
|
154
|
+
"plot_transition_network",
|
|
155
|
+
"plot_sequence_cluster_silhouette",
|
|
156
|
+
"audit_sequence_data",
|
|
157
|
+
"validate_sequence_data",
|
|
158
|
+
"prepare_sequence_data",
|
|
159
|
+
"encode_sequence_data",
|
|
160
|
+
"summarise_sequence_states",
|
|
161
|
+
"summarise_sequence_transitions",
|
|
162
|
+
"format_sequence_paths",
|
|
163
|
+
"extract_sequence_ngrams",
|
|
164
|
+
"summarise_sequence_motifs",
|
|
165
|
+
"filter_sequence_motifs",
|
|
166
|
+
"format_sequence_motifs",
|
|
167
|
+
"summarise_sequence_motif_positions",
|
|
168
|
+
"format_sequence_motif_positions",
|
|
169
|
+
"plot_sequence_motifs",
|
|
170
|
+
"plot_sequence_motif_positions",
|
|
171
|
+
"create_consensus_sequence",
|
|
172
|
+
"summarise_consensus_agreement",
|
|
173
|
+
"format_consensus_sequence",
|
|
174
|
+
"compare_sequence_groups",
|
|
175
|
+
"compute_sequence_distance",
|
|
176
|
+
"summarise_sequence_distance",
|
|
177
|
+
"cluster_sequences",
|
|
178
|
+
"validate_sequence_clusters",
|
|
179
|
+
"extract_representative_sequences",
|
|
180
|
+
"bootstrap_sequence_clusters",
|
|
181
|
+
"summarise_sequence_cluster_stability",
|
|
182
|
+
"create_sequence_cluster_ensemble",
|
|
183
|
+
"create_transition_network",
|
|
184
|
+
"summarise_transition_centrality",
|
|
185
|
+
"detect_transition_communities",
|
|
186
|
+
"fit_higher_order_transition_model",
|
|
187
|
+
"predict_next_state",
|
|
188
|
+
"bootstrap_transition_network",
|
|
189
|
+
"prepare_sequence_panel",
|
|
190
|
+
"summarise_sequence_panel",
|
|
191
|
+
"compare_sequence_panel_changes",
|
|
192
|
+
"plot_sequence_panel_changes",
|
|
193
|
+
"extract_sequence_subsequences",
|
|
194
|
+
"summarise_sequence_subsequences",
|
|
195
|
+
"filter_sequence_subsequences",
|
|
196
|
+
"compare_sequence_subsequences",
|
|
197
|
+
"plot_sequence_subsequences",
|
|
198
|
+
"fit_sequence_hmm",
|
|
199
|
+
"fit_sequence_hmm_mixture",
|
|
200
|
+
"decode_sequence_states",
|
|
201
|
+
"summarise_sequence_hmm",
|
|
202
|
+
"compare_sequence_hmms",
|
|
203
|
+
"declare_sequence_comparison_design",
|
|
204
|
+
"test_sequence_group_difference",
|
|
205
|
+
"bootstrap_sequence_group_difference",
|
|
206
|
+
"summarise_sequence_group_inference",
|
|
207
|
+
"plot_sequence_group_inference",
|
|
208
|
+
"fit_multichannel_sequence_hmm",
|
|
209
|
+
"decode_multichannel_sequence_states",
|
|
210
|
+
"summarise_multichannel_sequence_hmm",
|
|
211
|
+
"plot_multichannel_sequence_hmm",
|
|
212
|
+
"fit_covariate_sequence_hmm",
|
|
213
|
+
"predict_covariate_transition_probabilities",
|
|
214
|
+
"decode_covariate_sequence_states",
|
|
215
|
+
"summarise_covariate_sequence_hmm",
|
|
216
|
+
"fit_time_varying_sequence_model",
|
|
217
|
+
"predict_time_varying_sequence_model",
|
|
218
|
+
"summarise_time_varying_sequence_model",
|
|
219
|
+
"plot_time_varying_sequence_model",
|
|
220
|
+
]
|
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
from ._exceptions import ValidationError
|
|
10
|
+
from ._types import PrepareResult
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def scalar_character(x: Any, argument: str, allow_none: bool = False) -> None:
|
|
14
|
+
if allow_none and x is None:
|
|
15
|
+
return
|
|
16
|
+
if not isinstance(x, str) or not x:
|
|
17
|
+
raise ValidationError(f"`{argument}` must be a single non-missing character value.")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def scalar_number(
|
|
21
|
+
x: Any, argument: str, lower: float = -np.inf, upper: float = np.inf, integer: bool = False
|
|
22
|
+
) -> None:
|
|
23
|
+
valid = (
|
|
24
|
+
isinstance(x, (int, float, np.integer, np.floating))
|
|
25
|
+
and not isinstance(x, (bool, np.bool_))
|
|
26
|
+
and np.isfinite(x)
|
|
27
|
+
)
|
|
28
|
+
invalid_integer = (
|
|
29
|
+
integer
|
|
30
|
+
and valid
|
|
31
|
+
and (int(x) != x or x < -np.iinfo(np.int32).max or x > np.iinfo(np.int32).max)
|
|
32
|
+
)
|
|
33
|
+
if not valid or x < lower or x > upper or invalid_integer:
|
|
34
|
+
raise ValidationError(f"`{argument}` has an invalid numeric value.")
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def scalar_logical(x: Any, argument: str) -> None:
|
|
38
|
+
if not isinstance(x, (bool, np.bool_)):
|
|
39
|
+
raise ValidationError(f"`{argument}` must be a single non-missing logical value.")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def match_cols(
|
|
43
|
+
data: pd.DataFrame, columns: Sequence[str] | str | None, argument: str, allow_none: bool = True
|
|
44
|
+
) -> list[str]:
|
|
45
|
+
if allow_none and columns is None:
|
|
46
|
+
return []
|
|
47
|
+
if isinstance(columns, str):
|
|
48
|
+
columns = [columns]
|
|
49
|
+
if columns is None or not isinstance(columns, Sequence):
|
|
50
|
+
raise ValidationError(f"`{argument}` must contain unique, non-missing column names.")
|
|
51
|
+
cols = list(columns)
|
|
52
|
+
if any(not isinstance(c, str) or not c for c in cols) or len(set(cols)) != len(cols):
|
|
53
|
+
raise ValidationError(f"`{argument}` must contain unique, non-missing column names.")
|
|
54
|
+
missing = [c for c in cols if c not in data.columns]
|
|
55
|
+
if missing:
|
|
56
|
+
raise ValidationError(f"Missing columns in `{argument}`: {', '.join(missing)}.")
|
|
57
|
+
return cols
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def adv_data(
|
|
61
|
+
data: pd.DataFrame | PrepareResult | Any,
|
|
62
|
+
sequence_id_col: str = "sequence_id",
|
|
63
|
+
order_col: str = "sequence_order",
|
|
64
|
+
state_col: str = "state",
|
|
65
|
+
metadata_cols: Sequence[str] | str | None = None,
|
|
66
|
+
missing_state_policy: str = "error",
|
|
67
|
+
missing_state_label: str = "<MISSING>",
|
|
68
|
+
) -> dict[str, Any]:
|
|
69
|
+
if missing_state_policy not in {"error", "drop", "state"}:
|
|
70
|
+
raise ValidationError("Invalid missing-state policy.")
|
|
71
|
+
if missing_state_policy == "state":
|
|
72
|
+
scalar_character(missing_state_label, "missing_state_label")
|
|
73
|
+
if isinstance(data, PrepareResult):
|
|
74
|
+
data = data.data
|
|
75
|
+
elif (
|
|
76
|
+
not isinstance(data, pd.DataFrame)
|
|
77
|
+
and hasattr(data, "data")
|
|
78
|
+
and isinstance(data.data, pd.DataFrame)
|
|
79
|
+
):
|
|
80
|
+
data = data.data
|
|
81
|
+
if not isinstance(data, pd.DataFrame):
|
|
82
|
+
raise ValidationError(
|
|
83
|
+
"`data` must be a data frame or an object with a data-frame `data` component."
|
|
84
|
+
)
|
|
85
|
+
scalar_character(sequence_id_col, "sequence_id_col")
|
|
86
|
+
scalar_character(order_col, "order_col")
|
|
87
|
+
scalar_character(state_col, "state_col")
|
|
88
|
+
metadata = match_cols(data, metadata_cols, "metadata_cols")
|
|
89
|
+
required = [sequence_id_col, order_col, state_col]
|
|
90
|
+
overlap = [c for c in metadata if c in required]
|
|
91
|
+
if overlap:
|
|
92
|
+
raise ValidationError(
|
|
93
|
+
"`metadata_cols` must not repeat core sequence columns: " + ", ".join(overlap) + "."
|
|
94
|
+
)
|
|
95
|
+
missing = [c for c in required if c not in data.columns]
|
|
96
|
+
if missing:
|
|
97
|
+
raise ValidationError("Missing required sequence columns: " + ", ".join(missing) + ".")
|
|
98
|
+
if len(data) == 0:
|
|
99
|
+
raise ValidationError("`data` must contain at least one sequence row.")
|
|
100
|
+
if data.columns.duplicated().any():
|
|
101
|
+
raise ValidationError("`data` contains duplicated column names.")
|
|
102
|
+
ident = data[sequence_id_col]
|
|
103
|
+
order = data[order_col]
|
|
104
|
+
state = data[state_col]
|
|
105
|
+
if (
|
|
106
|
+
ident.map(
|
|
107
|
+
lambda x: (
|
|
108
|
+
isinstance(x, (list, dict, set, tuple, np.ndarray)) if x is not None else False
|
|
109
|
+
)
|
|
110
|
+
).any()
|
|
111
|
+
or state.map(
|
|
112
|
+
lambda x: (
|
|
113
|
+
isinstance(x, (list, dict, set, tuple, np.ndarray)) if x is not None else False
|
|
114
|
+
)
|
|
115
|
+
).any()
|
|
116
|
+
):
|
|
117
|
+
raise ValidationError("Sequence identifiers and states must be atomic vectors.")
|
|
118
|
+
if (
|
|
119
|
+
not pd.api.types.is_numeric_dtype(order.dtype)
|
|
120
|
+
or order.isna().any()
|
|
121
|
+
or not np.isfinite(pd.to_numeric(order)).all()
|
|
122
|
+
):
|
|
123
|
+
raise ValidationError(
|
|
124
|
+
"The sequence order column must contain finite, non-missing numeric values."
|
|
125
|
+
)
|
|
126
|
+
id_text = ident.astype("string")
|
|
127
|
+
state_text = state.astype("string")
|
|
128
|
+
missing_id = ident.isna() | id_text.str.strip().eq("").fillna(False)
|
|
129
|
+
if missing_id.any():
|
|
130
|
+
raise ValidationError("Sequence identifiers must not be missing or blank.")
|
|
131
|
+
missing_state = state.isna() | state_text.str.strip().eq("").fillna(False)
|
|
132
|
+
if missing_state_policy == "error" and missing_state.any():
|
|
133
|
+
raise ValidationError(
|
|
134
|
+
"Missing states were found. Select an explicit non-error policy to continue."
|
|
135
|
+
)
|
|
136
|
+
working = data.copy()
|
|
137
|
+
working[".gp3_adv_original_row"] = np.arange(1, len(working) + 1)
|
|
138
|
+
if missing_state_policy == "drop":
|
|
139
|
+
working = working.loc[~missing_state].copy()
|
|
140
|
+
elif missing_state_policy == "state":
|
|
141
|
+
vals = working[state_col].astype("string").copy()
|
|
142
|
+
vals.loc[missing_state] = missing_state_label
|
|
143
|
+
working[state_col] = vals.astype(object)
|
|
144
|
+
if len(working) == 0:
|
|
145
|
+
raise ValidationError("No sequence rows remain after applying the missing-state policy.")
|
|
146
|
+
key = (
|
|
147
|
+
working[sequence_id_col].astype(str)
|
|
148
|
+
+ "\x1c"
|
|
149
|
+
+ working[order_col].map(lambda v: format(float(v), ".17g"))
|
|
150
|
+
)
|
|
151
|
+
if key.duplicated().any():
|
|
152
|
+
raise ValidationError(
|
|
153
|
+
"Duplicated sequence positions were found. Prepare the data before advanced analysis."
|
|
154
|
+
)
|
|
155
|
+
working = working.sort_values(
|
|
156
|
+
[sequence_id_col, order_col, ".gp3_adv_original_row"], kind="stable"
|
|
157
|
+
).reset_index(drop=True)
|
|
158
|
+
seq_ids = list(dict.fromkeys(working[sequence_id_col].astype(str).tolist()))
|
|
159
|
+
sequences = {
|
|
160
|
+
sid: working.loc[working[sequence_id_col].astype(str) == sid, state_col]
|
|
161
|
+
.astype(str)
|
|
162
|
+
.tolist()
|
|
163
|
+
for sid in seq_ids
|
|
164
|
+
}
|
|
165
|
+
orders = {
|
|
166
|
+
sid: working.loc[working[sequence_id_col].astype(str) == sid, order_col].tolist()
|
|
167
|
+
for sid in seq_ids
|
|
168
|
+
}
|
|
169
|
+
observed = list(dict.fromkeys(working[state_col].astype(str).tolist()))
|
|
170
|
+
if isinstance(working[state_col].dtype, pd.CategoricalDtype):
|
|
171
|
+
state_levels = [str(x) for x in working[state_col].cat.categories if str(x) in observed]
|
|
172
|
+
else:
|
|
173
|
+
state_levels = sorted(observed)
|
|
174
|
+
metadata_df = None
|
|
175
|
+
if metadata:
|
|
176
|
+
rows = []
|
|
177
|
+
for sid in seq_ids:
|
|
178
|
+
part = working.loc[working[sequence_id_col].astype(str) == sid]
|
|
179
|
+
for c in metadata:
|
|
180
|
+
vals = part[c].astype("string").fillna("<NA>")
|
|
181
|
+
if vals.nunique(dropna=False) > 1:
|
|
182
|
+
raise ValidationError(f"Metadata vary within sequence `{sid}`: {c}.")
|
|
183
|
+
rec = {sequence_id_col: sid, **{c: part.iloc[0][c] for c in metadata}}
|
|
184
|
+
rows.append(rec)
|
|
185
|
+
metadata_df = pd.DataFrame(rows)
|
|
186
|
+
return {
|
|
187
|
+
"data": working,
|
|
188
|
+
"sequences": sequences,
|
|
189
|
+
"orders": orders,
|
|
190
|
+
"sequence_ids": seq_ids,
|
|
191
|
+
"state_levels": state_levels,
|
|
192
|
+
"metadata": metadata_df,
|
|
193
|
+
"columns": {
|
|
194
|
+
"sequence_id": sequence_id_col,
|
|
195
|
+
"order": order_col,
|
|
196
|
+
"state": state_col,
|
|
197
|
+
"metadata": metadata,
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def group_key(data: pd.DataFrame, group_cols: Sequence[str]) -> pd.Series:
|
|
203
|
+
if not group_cols:
|
|
204
|
+
return pd.Series(["__all__"] * len(data), index=data.index, dtype=object)
|
|
205
|
+
vals = []
|
|
206
|
+
for c in group_cols:
|
|
207
|
+
vals.append(data[c].astype("string").fillna("<NA>"))
|
|
208
|
+
out = vals[0].astype(str)
|
|
209
|
+
for v in vals[1:]:
|
|
210
|
+
out = out + "\x1d" + v.astype(str)
|
|
211
|
+
return out
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def state_order(
|
|
215
|
+
values: Sequence[Any] | pd.Series, state_levels: Sequence[Any] | None = None
|
|
216
|
+
) -> list[str]:
|
|
217
|
+
vals = pd.Series(list(values))
|
|
218
|
+
observed = list(dict.fromkeys(vals.dropna().astype(str).tolist()))
|
|
219
|
+
if state_levels is None:
|
|
220
|
+
return sorted(observed)
|
|
221
|
+
levels = [str(x) for x in state_levels]
|
|
222
|
+
if any(not x.strip() for x in levels) or len(set(levels)) != len(levels):
|
|
223
|
+
raise ValidationError("`state_levels` must contain unique, non-missing, non-blank values.")
|
|
224
|
+
return [x for x in levels if x in observed] + sorted([x for x in observed if x not in levels])
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def tie(
|
|
228
|
+
states: Sequence[str], weights: Sequence[float], levels: Sequence[str], tie_method: str
|
|
229
|
+
) -> dict[str, Any]:
|
|
230
|
+
totals: dict[str, float] = {}
|
|
231
|
+
for s, w in zip(states, weights, strict=True):
|
|
232
|
+
totals[s] = totals.get(s, 0.0) + float(w)
|
|
233
|
+
max_total = max(totals.values())
|
|
234
|
+
eps = np.sqrt(np.finfo(float).eps)
|
|
235
|
+
tied = [s for s, v in totals.items() if abs(v - max_total) <= eps]
|
|
236
|
+
ordered = state_order(tied, levels)
|
|
237
|
+
selected: str | None
|
|
238
|
+
if tie_method == "first":
|
|
239
|
+
selected = ordered[0]
|
|
240
|
+
elif tie_method == "last":
|
|
241
|
+
selected = ordered[-1]
|
|
242
|
+
elif tie_method == "missing":
|
|
243
|
+
selected = ordered[0] if len(ordered) == 1 else None
|
|
244
|
+
elif tie_method == "all":
|
|
245
|
+
selected = " | ".join(ordered)
|
|
246
|
+
else:
|
|
247
|
+
raise ValidationError("Invalid tie method.")
|
|
248
|
+
return {
|
|
249
|
+
"selected": selected,
|
|
250
|
+
"tied": ordered,
|
|
251
|
+
"total": max_total,
|
|
252
|
+
"agreement": max_total / sum(weights),
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def edit_distance(
|
|
257
|
+
a: Sequence[str],
|
|
258
|
+
b: Sequence[str],
|
|
259
|
+
indel_cost: float = 1,
|
|
260
|
+
substitution_cost: float = 1,
|
|
261
|
+
substitution_matrix: pd.DataFrame | np.ndarray | None = None,
|
|
262
|
+
state_labels: Sequence[str] | None = None,
|
|
263
|
+
) -> float:
|
|
264
|
+
n, m = len(a), len(b)
|
|
265
|
+
d = np.zeros((n + 1, m + 1), float)
|
|
266
|
+
d[:, 0] = np.arange(n + 1) * indel_cost
|
|
267
|
+
d[0, :] = np.arange(m + 1) * indel_cost
|
|
268
|
+
if isinstance(substitution_matrix, np.ndarray):
|
|
269
|
+
if state_labels is None:
|
|
270
|
+
raise ValidationError("State labels are required with an unnamed substitution matrix.")
|
|
271
|
+
substitution_matrix = pd.DataFrame(
|
|
272
|
+
substitution_matrix, index=state_labels, columns=state_labels
|
|
273
|
+
)
|
|
274
|
+
for i in range(n):
|
|
275
|
+
for j in range(m):
|
|
276
|
+
if a[i] == b[j]:
|
|
277
|
+
cost = 0.0
|
|
278
|
+
elif substitution_matrix is None:
|
|
279
|
+
cost = float(substitution_cost)
|
|
280
|
+
else:
|
|
281
|
+
if a[i] not in substitution_matrix.index or b[j] not in substitution_matrix.columns:
|
|
282
|
+
raise ValidationError(
|
|
283
|
+
"The substitution matrix does not cover all observed states."
|
|
284
|
+
)
|
|
285
|
+
cost = float(substitution_matrix.loc[a[i], b[j]])
|
|
286
|
+
d[i + 1, j + 1] = min(
|
|
287
|
+
d[i, j + 1] + indel_cost, d[i + 1, j] + indel_cost, d[i, j] + cost
|
|
288
|
+
)
|
|
289
|
+
return float(d[n, m])
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def lcs_length(a: Sequence[str], b: Sequence[str]) -> int:
|
|
293
|
+
m = len(b)
|
|
294
|
+
prev = np.zeros(m + 1, dtype=int)
|
|
295
|
+
for x in a:
|
|
296
|
+
cur = np.zeros(m + 1, dtype=int)
|
|
297
|
+
for j, y in enumerate(b, 1):
|
|
298
|
+
cur[j] = prev[j - 1] + 1 if x == y else max(prev[j], cur[j - 1])
|
|
299
|
+
prev = cur
|
|
300
|
+
return int(prev[m])
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def transition_profile(
|
|
304
|
+
sequence: Sequence[str], states: Sequence[str], smoothing: float = 0
|
|
305
|
+
) -> np.ndarray:
|
|
306
|
+
p = len(states)
|
|
307
|
+
idx = {s: i for i, s in enumerate(states)}
|
|
308
|
+
out = np.full((p, p), float(smoothing))
|
|
309
|
+
for a, b in zip(sequence[:-1], sequence[1:], strict=True):
|
|
310
|
+
out[idx[a], idx[b]] += 1
|
|
311
|
+
totals = out.sum(axis=1)
|
|
312
|
+
nz = totals > 0
|
|
313
|
+
out[nz] = out[nz] / totals[nz, None]
|
|
314
|
+
return out.ravel()
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def validate_distance_matrix(x: Any) -> tuple[np.ndarray, list[str]]:
|
|
318
|
+
labels: list[str]
|
|
319
|
+
if hasattr(x, "matrix") and hasattr(x, "labels"):
|
|
320
|
+
mat = np.asarray(x.matrix, float)
|
|
321
|
+
labels = list(x.labels)
|
|
322
|
+
elif isinstance(x, pd.DataFrame):
|
|
323
|
+
mat = x.to_numpy(float)
|
|
324
|
+
labels = [str(v) for v in x.index]
|
|
325
|
+
else:
|
|
326
|
+
mat = np.asarray(x, float)
|
|
327
|
+
labels = [str(i + 1) for i in range(mat.shape[0])] if mat.ndim == 2 else []
|
|
328
|
+
if mat.ndim != 2 or mat.shape[0] != mat.shape[1]:
|
|
329
|
+
raise ValidationError("A square distance matrix is required.")
|
|
330
|
+
if mat.shape[0] == 0:
|
|
331
|
+
raise ValidationError("The distance object must contain at least one sequence.")
|
|
332
|
+
if not np.isfinite(mat).all() or (mat < 0).any():
|
|
333
|
+
raise ValidationError("Sequence distances must be finite, non-missing, and non-negative.")
|
|
334
|
+
tol = np.sqrt(np.finfo(float).eps)
|
|
335
|
+
if np.max(np.abs(mat - mat.T)) > tol:
|
|
336
|
+
raise ValidationError("The distance matrix must be symmetric.")
|
|
337
|
+
if np.max(np.abs(np.diag(mat))) > tol:
|
|
338
|
+
raise ValidationError("The distance-matrix diagonal must be zero.")
|
|
339
|
+
if len(set(labels)) != len(labels) or any(not s for s in labels):
|
|
340
|
+
raise ValidationError(
|
|
341
|
+
"Distance rows and columns must have identical, unique sequence identifiers."
|
|
342
|
+
)
|
|
343
|
+
return mat, labels
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
def silhouette(assignments: Sequence[int], distance_matrix: np.ndarray) -> np.ndarray:
|
|
347
|
+
labels = np.asarray(assignments)
|
|
348
|
+
out = np.zeros(len(labels))
|
|
349
|
+
clusters = sorted(set(labels.tolist()))
|
|
350
|
+
for i, own in enumerate(labels):
|
|
351
|
+
same = np.flatnonzero(labels == own)
|
|
352
|
+
same = same[same != i]
|
|
353
|
+
if len(same) == 0:
|
|
354
|
+
out[i] = 0
|
|
355
|
+
continue
|
|
356
|
+
a = float(distance_matrix[i, same].mean())
|
|
357
|
+
others = [float(distance_matrix[i, labels == k].mean()) for k in clusters if k != own]
|
|
358
|
+
b = min(others) if others else 0
|
|
359
|
+
out[i] = 0 if max(a, b) == 0 else (b - a) / max(a, b)
|
|
360
|
+
return out
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
def row_normalise(x: np.ndarray, pseudocount: float = 0) -> np.ndarray:
|
|
364
|
+
x = np.asarray(x, float) + pseudocount
|
|
365
|
+
totals = x.sum(axis=1)
|
|
366
|
+
bad = (totals <= 0) | (~np.isfinite(totals))
|
|
367
|
+
x[bad] = 1
|
|
368
|
+
return x / x.sum(axis=1, keepdims=True)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def vector_normalise(x: np.ndarray, pseudocount: float = 0) -> np.ndarray:
|
|
372
|
+
x = np.asarray(x, float) + pseudocount
|
|
373
|
+
total = x.sum()
|
|
374
|
+
if not np.isfinite(total) or total <= 0:
|
|
375
|
+
x = np.ones_like(x)
|
|
376
|
+
return x / x.sum()
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
def forward_backward(
|
|
380
|
+
obs: np.ndarray, initial: np.ndarray, transition: np.ndarray, emission: np.ndarray
|
|
381
|
+
) -> dict[str, Any]:
|
|
382
|
+
n_states = len(initial)
|
|
383
|
+
n = len(obs)
|
|
384
|
+
alpha = np.zeros((n, n_states))
|
|
385
|
+
scales = np.zeros(n)
|
|
386
|
+
alpha[0] = initial * emission[:, obs[0]]
|
|
387
|
+
scales[0] = alpha[0].sum()
|
|
388
|
+
scales[0] = np.finfo(float).tiny if not np.isfinite(scales[0]) or scales[0] <= 0 else scales[0]
|
|
389
|
+
alpha[0] /= scales[0]
|
|
390
|
+
for t in range(1, n):
|
|
391
|
+
alpha[t] = (alpha[t - 1] @ transition) * emission[:, obs[t]]
|
|
392
|
+
scales[t] = alpha[t].sum()
|
|
393
|
+
scales[t] = (
|
|
394
|
+
np.finfo(float).tiny if not np.isfinite(scales[t]) or scales[t] <= 0 else scales[t]
|
|
395
|
+
)
|
|
396
|
+
alpha[t] /= scales[t]
|
|
397
|
+
beta = np.ones((n, n_states))
|
|
398
|
+
for t in range(n - 2, -1, -1):
|
|
399
|
+
beta[t] = transition @ (emission[:, obs[t + 1]] * beta[t + 1])
|
|
400
|
+
beta[t] /= scales[t + 1]
|
|
401
|
+
gamma = alpha * beta
|
|
402
|
+
gt = gamma.sum(axis=1)
|
|
403
|
+
bad = (~np.isfinite(gt)) | (gt <= 0)
|
|
404
|
+
gamma[bad] = 1 / n_states
|
|
405
|
+
gt[bad] = 1
|
|
406
|
+
gamma /= gt[:, None]
|
|
407
|
+
xi = np.zeros((max(n - 1, 0), n_states, n_states))
|
|
408
|
+
for t in range(n - 1):
|
|
409
|
+
cur = np.outer(alpha[t], emission[:, obs[t + 1]] * beta[t + 1]) * transition
|
|
410
|
+
total = cur.sum()
|
|
411
|
+
xi[t] = cur / total if total > 0 else cur
|
|
412
|
+
return {
|
|
413
|
+
"alpha": alpha,
|
|
414
|
+
"beta": beta,
|
|
415
|
+
"gamma": gamma,
|
|
416
|
+
"xi": xi,
|
|
417
|
+
"log_likelihood": float(np.log(scales).sum()),
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
def viterbi(
|
|
422
|
+
obs: np.ndarray, initial: np.ndarray, transition: np.ndarray, emission: np.ndarray
|
|
423
|
+
) -> dict[str, Any]:
|
|
424
|
+
ni = np.log(np.maximum(initial, np.finfo(float).tiny))
|
|
425
|
+
nt = np.log(np.maximum(transition, np.finfo(float).tiny))
|
|
426
|
+
ne = np.log(np.maximum(emission, np.finfo(float).tiny))
|
|
427
|
+
n = len(obs)
|
|
428
|
+
k = len(initial)
|
|
429
|
+
delta = np.full((n, k), -np.inf)
|
|
430
|
+
psi = np.zeros((n, k), int)
|
|
431
|
+
delta[0] = ni + ne[:, obs[0]]
|
|
432
|
+
for t in range(1, n):
|
|
433
|
+
for j in range(k):
|
|
434
|
+
cand = delta[t - 1] + nt[:, j]
|
|
435
|
+
psi[t, j] = int(np.argmax(cand))
|
|
436
|
+
delta[t, j] = cand[psi[t, j]] + ne[j, obs[t]]
|
|
437
|
+
path = np.zeros(n, int)
|
|
438
|
+
path[-1] = int(np.argmax(delta[-1]))
|
|
439
|
+
for t in range(n - 2, -1, -1):
|
|
440
|
+
path[t] = psi[t + 1, path[t + 1]]
|
|
441
|
+
return {"path": path, "log_probability": float(delta[-1, path[-1]])}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class GP3SequencesError(Exception):
|
|
2
|
+
"""Base exception for gp3sequencespy."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ValidationError(GP3SequencesError, ValueError):
|
|
6
|
+
"""Raised when an input violates a sequence-data or analysis contract."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class ModelFitError(GP3SequencesError, RuntimeError):
|
|
10
|
+
"""Raised when a model cannot be fitted under its declared contract."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ParityError(GP3SequencesError, AssertionError):
|
|
14
|
+
"""Raised when an R/Python parity assertion fails."""
|