amrpredict 0.3.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.
- amrpredict/__init__.py +259 -0
- amrpredict/_paths.py +11 -0
- amrpredict/amr_constants.py +202 -0
- amrpredict/cli.py +125 -0
- amrpredict/genome.py +496 -0
- amrpredict/lgbm.py +561 -0
- amrpredict/models/ab_rate_full.joblib +0 -0
- amrpredict/models/amr_lgbm_final_model.txt +10634 -0
- amrpredict/models/genome/feature_meta.json +536 -0
- amrpredict/models/genome/genome_metrics.json +52 -0
- amrpredict/models/genome/model.txt +10704 -0
- amrpredict/models/genome_genes/feature_meta.json +811 -0
- amrpredict/models/genome_genes/genome_metrics.json +52 -0
- amrpredict/models/genome_genes/model.txt +10961 -0
- amrpredict/models/genome_genes/species_profiles.npz +0 -0
- amrpredict/models/genus_ab_rate_full.joblib +0 -0
- amrpredict/models/lgbm_meta.joblib +0 -0
- amrpredict/models/lgbm_metrics.json +97 -0
- amrpredict/models/rl_agent/policy.npz +0 -0
- amrpredict/models/rl_agent/rl_agent.json +22 -0
- amrpredict/models/taxon_ab_rate_full.joblib +0 -0
- amrpredict/models/taxon_species.csv +13059 -0
- amrpredict/models/timeline_calibration.json +21 -0
- amrpredict/models/timeline_ml/model.txt +1683 -0
- amrpredict/models/timeline_ml/timeline_metrics.json +145 -0
- amrpredict/models/timeline_ml/timeline_ml.json +1 -0
- amrpredict/registry.py +67 -0
- amrpredict/rl_agent.py +177 -0
- amrpredict/timeline.py +275 -0
- amrpredict/timeline_ml.py +174 -0
- amrpredict-0.3.0.dist-info/METADATA +304 -0
- amrpredict-0.3.0.dist-info/RECORD +35 -0
- amrpredict-0.3.0.dist-info/WHEEL +4 -0
- amrpredict-0.3.0.dist-info/entry_points.txt +2 -0
- amrpredict-0.3.0.dist-info/licenses/LICENSE +21 -0
amrpredict/__init__.py
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
"""amrpredict — antimicrobial resistance prediction from genomic data.
|
|
2
|
+
|
|
3
|
+
The models the project's web app serves, behind one flat API:
|
|
4
|
+
|
|
5
|
+
>>> import amrpredict
|
|
6
|
+
>>> amrpredict.forecast('ciprofloxacin', taxon_id=562, mic_value=4)['prediction']
|
|
7
|
+
'Susceptible'
|
|
8
|
+
|
|
9
|
+
Trained models:
|
|
10
|
+
|
|
11
|
+
* ``forecast`` — resistance from metadata and an MIC value (LightGBM).
|
|
12
|
+
* ``predict_fasta`` — resistance from a complete genome: the gene model
|
|
13
|
+
(LightGBM on AMRFinderPlus genes) when AMRFinderPlus is installed, the k-mer
|
|
14
|
+
model (LightGBM on 4-mer frequencies) otherwise.
|
|
15
|
+
* ``forecast_trend`` — the yearly % resistant of a genus and antibiotic, and its
|
|
16
|
+
forecast for the coming years (LightGBM on BV-BRC laboratory results).
|
|
17
|
+
* ``plan_treatment`` — which drug to give each week, from an RL agent (PPO)
|
|
18
|
+
trained on the simulation, against fixed schedules.
|
|
19
|
+
|
|
20
|
+
``simulate_timeline`` projects week-by-week resistance under treatment and is
|
|
21
|
+
a biological simulation, not a trained model.
|
|
22
|
+
"""
|
|
23
|
+
from . import registry
|
|
24
|
+
from ._paths import default_model_dir
|
|
25
|
+
|
|
26
|
+
__version__ = '0.3.0'
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
'forecast',
|
|
30
|
+
'predict_fasta',
|
|
31
|
+
'identify_species',
|
|
32
|
+
'forecast_trend',
|
|
33
|
+
'trend_series',
|
|
34
|
+
'plan_treatment',
|
|
35
|
+
'simulate_timeline',
|
|
36
|
+
'antibiotics',
|
|
37
|
+
'status',
|
|
38
|
+
'registry',
|
|
39
|
+
'default_model_dir',
|
|
40
|
+
'LGBMResistancePredictor',
|
|
41
|
+
'GenomeModelPredictor',
|
|
42
|
+
'TimelineMLForecaster',
|
|
43
|
+
'RLAgent',
|
|
44
|
+
'MutationTimelinePredictor',
|
|
45
|
+
'__version__',
|
|
46
|
+
]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def forecast(antibiotic, *, taxon_id=None, mic_value=None, mic_sign=None,
|
|
50
|
+
genus='unknown', species='unknown', threshold=None, model_dir=None):
|
|
51
|
+
"""Forecast resistance from genome metadata and (optionally) an MIC value.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
antibiotic: Antibiotic name, case-insensitive (e.g. ``'ciprofloxacin'``).
|
|
55
|
+
taxon_id: NCBI taxonomy ID of the organism, e.g. ``562`` for *E. coli*.
|
|
56
|
+
mic_value: Minimum inhibitory concentration in mg/L.
|
|
57
|
+
mic_sign: Comparator recorded with the MIC — ``'='``, ``'>'``, ``'<='`` …
|
|
58
|
+
genus: Genus name, e.g. ``'Escherichia'``.
|
|
59
|
+
species: Species name, e.g. ``'coli'``.
|
|
60
|
+
threshold: Probability at or above which the call is ``'Resistant'``.
|
|
61
|
+
Default: the threshold the bundled model was promoted with
|
|
62
|
+
(``status()['lgbm_forecasting']['default_threshold']``).
|
|
63
|
+
model_dir: Override the bundled artifacts.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
dict with ``prediction``, ``probability`` (calibrated), ``confidence``,
|
|
67
|
+
``drug_class``, ``model_run``, ``evidence`` (which inputs the model
|
|
68
|
+
recognised) and related fields.
|
|
69
|
+
"""
|
|
70
|
+
return registry.lgbm(model_dir).predict(
|
|
71
|
+
antibiotic, taxon_id=taxon_id, mic_value=mic_value, mic_sign=mic_sign,
|
|
72
|
+
genus=genus, species=species, threshold=threshold,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def predict_fasta(fasta_text, antibiotic, *, threshold=None, genus=None, species=None,
|
|
77
|
+
use_genes=None, model_dir=None):
|
|
78
|
+
"""Predict resistance from a complete genome assembly (at least 100,000 bp).
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
fasta_text: FASTA contents as a string; every contig is read.
|
|
82
|
+
antibiotic: Antibiotic name, case-insensitive.
|
|
83
|
+
threshold: Probability at or above which the call is ``'Resistant'``.
|
|
84
|
+
Default: the model's own threshold, chosen on validation genomes.
|
|
85
|
+
genus, species: The organism, if known (the k-mer model uses them).
|
|
86
|
+
use_genes: ``None`` (default) uses the gene model when AMRFinderPlus is
|
|
87
|
+
installed and the k-mer model otherwise; ``False`` always uses the
|
|
88
|
+
k-mer model; ``True`` requires the gene model.
|
|
89
|
+
model_dir: Override the bundled artifacts.
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
dict with ``prediction``, ``probability``, ``confidence``,
|
|
93
|
+
``gc_content``, ``sequence_length``, ``model_used``; ``top_kmers`` from
|
|
94
|
+
the k-mer model; ``genes_found`` and ``species_detected`` from the gene
|
|
95
|
+
model. Bad input gives a dict with ``error`` instead of raising.
|
|
96
|
+
"""
|
|
97
|
+
if use_genes is False:
|
|
98
|
+
model = registry.genome_kmers(model_dir)
|
|
99
|
+
else:
|
|
100
|
+
model = registry.genome(model_dir)
|
|
101
|
+
if use_genes and not model.uses_genes:
|
|
102
|
+
return {'error': 'the gene model needs AMRFinderPlus (the `amrfinder` program), '
|
|
103
|
+
'which was not found; pass use_genes=None to use the k-mer model'}
|
|
104
|
+
return model.predict(fasta_text, antibiotic, threshold, genus, species)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def identify_species(fasta_text, *, model_dir=None):
|
|
108
|
+
"""Species and genus of a complete genome from its 6-mer profile.
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
dict with ``species``, ``genus`` (``None`` when no reference is close)
|
|
112
|
+
and ``distance``; or a dict with ``error``.
|
|
113
|
+
"""
|
|
114
|
+
from .genome import GENE_FOLDER, GenomeModelPredictor, SpeciesProfiles, genome_counts
|
|
115
|
+
import os
|
|
116
|
+
path = os.path.join(model_dir or default_model_dir(), GENE_FOLDER, 'species_profiles.npz')
|
|
117
|
+
counts, _, length = genome_counts(fasta_text)
|
|
118
|
+
if length < GenomeModelPredictor.MIN_BP:
|
|
119
|
+
return {'error': f'genome too short ({length:,} bp): upload a complete assembly',
|
|
120
|
+
'sequence_length': length}
|
|
121
|
+
found = SpeciesProfiles(path).identify(counts) or {'species': None, 'genus': None, 'distance': None}
|
|
122
|
+
return {k: (str(v) if isinstance(v, str) else v) for k, v in found.items()}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def forecast_trend(genus, antibiotic, *, years_ahead=5, model_dir=None):
|
|
126
|
+
"""Yearly % resistant of a genus and antibiotic, and the timeline ML model's forecast.
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
genus: Genus name, case-insensitive (e.g. ``'Salmonella'``);
|
|
130
|
+
``trend_series()`` lists those with enough history.
|
|
131
|
+
antibiotic: Antibiotic name, case-insensitive.
|
|
132
|
+
years_ahead: Years to forecast, 1 to 10. The 80% band widens with each.
|
|
133
|
+
model_dir: Override the bundled artifacts.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
dict with ``history`` (year, resistant_pct, n lab results),
|
|
137
|
+
``forecast`` (year, resistant_pct, lo, hi), the model's held-out
|
|
138
|
+
``metrics`` against the baselines and a ``label``; or a dict with
|
|
139
|
+
``error`` when the pair has too little history.
|
|
140
|
+
"""
|
|
141
|
+
years_ahead = int(years_ahead)
|
|
142
|
+
if not 1 <= years_ahead <= 10:
|
|
143
|
+
return {'error': 'years_ahead must be from 1 to 10'}
|
|
144
|
+
model = registry.trend(model_dir)
|
|
145
|
+
if not model.is_trained:
|
|
146
|
+
return {'error': 'the timeline ML model is not bundled'}
|
|
147
|
+
result = model.forecast(genus, antibiotic, years_ahead=years_ahead)
|
|
148
|
+
return result or {'error': f'{genus} has too little yearly lab history for {antibiotic} to forecast'}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def trend_series(model_dir=None):
|
|
152
|
+
"""``{genus: [antibiotic, ...]}`` that ``forecast_trend`` can forecast."""
|
|
153
|
+
return registry.trend(model_dir).series
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def plan_treatment(antibiotic, *, n_weeks=52, seed=42, model_dir=None):
|
|
157
|
+
"""Which drug to give each week: the RL agent against fixed schedules.
|
|
158
|
+
|
|
159
|
+
The agent (PPO) was trained on the cycling simulation for ciprofloxacin,
|
|
160
|
+
gentamicin and imipenem; ``antibiotic`` is the first drug, the other two
|
|
161
|
+
are the alternatives. It learned from simulated episodes, not patients.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
antibiotic: One of the agent's drugs, case-insensitive.
|
|
165
|
+
n_weeks: Weeks to plan, 1 to 104 (its training horizon).
|
|
166
|
+
seed: Seed of the simulated episode.
|
|
167
|
+
model_dir: Override the bundled artifacts.
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
dict with ``drugs``, ``policies`` (the agent and each baseline: the drug
|
|
171
|
+
per week, resistance curves, ``failure_week``, ``total_reward``) and
|
|
172
|
+
``best``; or a dict with ``error``.
|
|
173
|
+
"""
|
|
174
|
+
from .amr_constants import normalize_antibiotic
|
|
175
|
+
agent = registry.rl(model_dir)
|
|
176
|
+
if agent is None:
|
|
177
|
+
return {'error': 'the RL agent is not bundled'}
|
|
178
|
+
n_weeks = int(n_weeks)
|
|
179
|
+
if not 1 <= n_weeks <= agent.train_weeks:
|
|
180
|
+
return {'error': f'n_weeks must be from 1 to {agent.train_weeks}'}
|
|
181
|
+
block = agent.block(normalize_antibiotic(antibiotic), n_weeks, seed=seed)
|
|
182
|
+
return block or {'error': f'the RL agent was trained on {", ".join(agent.drugs)} only'}
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def simulate_timeline(fasta_text, antibiotic, *, n_weeks=8, seed=42, model_dir=None):
|
|
186
|
+
"""Simulate week-by-week resistance evolution under antibiotic pressure.
|
|
187
|
+
|
|
188
|
+
This is a biological simulation, not a trained model — it carries no AUC
|
|
189
|
+
or accuracy score. Treat its output as illustrative. The same inputs and
|
|
190
|
+
seed give the same output, and the three shares sum to 100 every week.
|
|
191
|
+
For a trained forecast of yearly resistance, use ``forecast_trend``.
|
|
192
|
+
|
|
193
|
+
Args:
|
|
194
|
+
fasta_text: FASTA contents as a string.
|
|
195
|
+
antibiotic: Antibiotic name, case-insensitive.
|
|
196
|
+
n_weeks: Number of weeks to project.
|
|
197
|
+
seed: Seed for the random parts (mutation counts and types).
|
|
198
|
+
model_dir: Override the bundled artifacts.
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
dict with a ``weeks`` series, mutation events and gene activations.
|
|
202
|
+
"""
|
|
203
|
+
return registry.timeline(model_dir).predict(fasta_text, antibiotic, n_weeks, seed=seed)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def antibiotics(model='genome', model_dir=None):
|
|
207
|
+
"""Sorted antibiotic names a model was trained on.
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
model: ``'genome'`` (``predict_fasta``), ``'forecast'`` (``forecast``),
|
|
211
|
+
``'trend'`` (``forecast_trend``) or ``'rl'`` (``plan_treatment``).
|
|
212
|
+
"""
|
|
213
|
+
if model == 'genome':
|
|
214
|
+
return registry.genome_kmers(model_dir).ab_list
|
|
215
|
+
if model == 'forecast':
|
|
216
|
+
return sorted(registry.lgbm(model_dir).vocabulary.get('antibiotics') or [])
|
|
217
|
+
if model == 'trend':
|
|
218
|
+
return sorted({ab for drugs in trend_series(model_dir).values() for ab in drugs})
|
|
219
|
+
if model == 'rl':
|
|
220
|
+
agent = registry.rl(model_dir)
|
|
221
|
+
return sorted(agent.drugs) if agent else []
|
|
222
|
+
raise ValueError("model must be 'genome', 'forecast', 'trend' or 'rl'")
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
def status(model_dir=None):
|
|
226
|
+
"""Report which models loaded, keyed by model name, with their measured
|
|
227
|
+
test metrics where they have them."""
|
|
228
|
+
agent = registry.rl(model_dir)
|
|
229
|
+
return {
|
|
230
|
+
'version': __version__,
|
|
231
|
+
'lgbm_forecasting': registry.lgbm(model_dir).status,
|
|
232
|
+
'genome_resistance': registry.genome(model_dir).status,
|
|
233
|
+
'trend_forecast': registry.trend(model_dir).status,
|
|
234
|
+
'rl_agent': {'trained': agent is not None,
|
|
235
|
+
'model_type': agent.label if agent else None,
|
|
236
|
+
'drugs': agent.drugs if agent else [],
|
|
237
|
+
'description': 'Plans which drug to give each week; trained on the simulation'},
|
|
238
|
+
'mutation_timeline': registry.timeline(model_dir).status,
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def __getattr__(name):
|
|
243
|
+
# Re-export the classes without importing numpy/pandas/lightgbm at import time.
|
|
244
|
+
if name == 'LGBMResistancePredictor':
|
|
245
|
+
from .lgbm import LGBMResistancePredictor
|
|
246
|
+
return LGBMResistancePredictor
|
|
247
|
+
if name == 'GenomeModelPredictor':
|
|
248
|
+
from .genome import GenomeModelPredictor
|
|
249
|
+
return GenomeModelPredictor
|
|
250
|
+
if name == 'TimelineMLForecaster':
|
|
251
|
+
from .timeline_ml import TimelineMLForecaster
|
|
252
|
+
return TimelineMLForecaster
|
|
253
|
+
if name == 'RLAgent':
|
|
254
|
+
from .rl_agent import RLAgent
|
|
255
|
+
return RLAgent
|
|
256
|
+
if name == 'MutationTimelinePredictor':
|
|
257
|
+
from .timeline import MutationTimelinePredictor
|
|
258
|
+
return MutationTimelinePredictor
|
|
259
|
+
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')
|
amrpredict/_paths.py
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Locating the model artifacts bundled inside the installed package."""
|
|
2
|
+
from importlib.resources import files
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def default_model_dir():
|
|
6
|
+
"""Absolute path to the models shipped with this package.
|
|
7
|
+
|
|
8
|
+
Used whenever a predictor is constructed without an explicit ``model_dir``,
|
|
9
|
+
so that ``LGBMForecaster()`` works straight after ``pip install``.
|
|
10
|
+
"""
|
|
11
|
+
return str(files(__package__) / 'models')
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Antibiotic names and label maps: the library's copy of backend/amr_constants.py.
|
|
3
|
+
|
|
4
|
+
The package installs without backend/, so it carries this copy. Do not edit it
|
|
5
|
+
here: edit backend/amr_constants.py, then copy the data over (everything above
|
|
6
|
+
FRONTEND_COPY); backend/tests/test_amr_constants.py fails while they differ.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Drug class per canonical antibiotic name. Anything missing is 'other'.
|
|
10
|
+
DRUG_CLASS_MAP = {
|
|
11
|
+
'ampicillin': 'beta_lactam', 'amoxicillin': 'beta_lactam',
|
|
12
|
+
'amoxicillin/clavulanic acid': 'beta_lactam', 'piperacillin': 'beta_lactam',
|
|
13
|
+
'piperacillin/tazobactam': 'beta_lactam', 'oxacillin': 'beta_lactam',
|
|
14
|
+
'ampicillin/sulbactam': 'beta_lactam', 'penicillin': 'beta_lactam',
|
|
15
|
+
'methicillin': 'beta_lactam', 'carbenicillin': 'beta_lactam',
|
|
16
|
+
'cefazolin': 'beta_lactam', 'cefoxitin': 'beta_lactam', 'cefotaxime': 'beta_lactam',
|
|
17
|
+
'ceftazidime': 'beta_lactam', 'ceftriaxone': 'beta_lactam', 'cefepime': 'beta_lactam',
|
|
18
|
+
'cefuroxime': 'beta_lactam', 'cephalothin': 'beta_lactam', 'cefalothin': 'beta_lactam',
|
|
19
|
+
'ceftazidime/avibactam': 'beta_lactam', 'ceftolozane/tazobactam': 'beta_lactam',
|
|
20
|
+
'ceftiofur': 'beta_lactam', 'cefpodoxime': 'beta_lactam',
|
|
21
|
+
'imipenem': 'carbapenem', 'meropenem': 'carbapenem', 'ertapenem': 'carbapenem',
|
|
22
|
+
'doripenem': 'carbapenem', 'aztreonam': 'monobactam',
|
|
23
|
+
'ciprofloxacin': 'fluoroquinolone', 'levofloxacin': 'fluoroquinolone',
|
|
24
|
+
'norfloxacin': 'fluoroquinolone', 'nalidixic acid': 'fluoroquinolone',
|
|
25
|
+
'ofloxacin': 'fluoroquinolone', 'moxifloxacin': 'fluoroquinolone',
|
|
26
|
+
'pefloxacin': 'fluoroquinolone', 'trovafloxacin': 'fluoroquinolone',
|
|
27
|
+
'gentamicin': 'aminoglycoside', 'tobramycin': 'aminoglycoside',
|
|
28
|
+
'amikacin': 'aminoglycoside', 'streptomycin': 'aminoglycoside',
|
|
29
|
+
'neomycin': 'aminoglycoside', 'kanamycin': 'aminoglycoside',
|
|
30
|
+
'spectinomycin': 'aminoglycoside', 'capreomycin': 'aminoglycoside',
|
|
31
|
+
'tetracycline': 'tetracycline', 'doxycycline': 'tetracycline',
|
|
32
|
+
'minocycline': 'tetracycline', 'tigecycline': 'tetracycline',
|
|
33
|
+
'sulfamethoxazole': 'sulfonamide', 'trimethoprim': 'sulfonamide',
|
|
34
|
+
'trimethoprim/sulfamethoxazole': 'sulfonamide', 'sulfisoxazole': 'sulfonamide',
|
|
35
|
+
'chloramphenicol': 'phenicol',
|
|
36
|
+
'azithromycin': 'macrolide', 'erythromycin': 'macrolide',
|
|
37
|
+
'telithromycin': 'macrolide', 'clarithromycin': 'macrolide',
|
|
38
|
+
'colistin': 'polymyxin', 'polymyxin b': 'polymyxin',
|
|
39
|
+
'vancomycin': 'glycopeptide', 'teicoplanin': 'glycopeptide',
|
|
40
|
+
'clindamycin': 'lincosamide', 'nitrofurantoin': 'nitrofuran',
|
|
41
|
+
'rifampicin': 'rifamycin', 'rifampin': 'rifamycin', 'rifabutin': 'rifamycin',
|
|
42
|
+
'linezolid': 'oxazolidinone', 'daptomycin': 'lipopeptide',
|
|
43
|
+
'fusidic acid': 'fusidane', 'quinupristin/dalfopristin': 'streptogramin',
|
|
44
|
+
'pristinamycin': 'streptogramin', 'cephalexin': 'beta_lactam',
|
|
45
|
+
'cefpodoxime/clavulanic acid': 'beta_lactam', 'ticarcillin/clavulanic acid': 'beta_lactam',
|
|
46
|
+
'isoniazid': 'antitubercular', 'ethambutol': 'antitubercular',
|
|
47
|
+
'pyrazinamide': 'antitubercular', 'ethionamide': 'antitubercular',
|
|
48
|
+
'prothionamide': 'antitubercular', 'cycloserine': 'antitubercular',
|
|
49
|
+
'para-aminosalicylic acid': 'antitubercular', 'clofazimine': 'antitubercular',
|
|
50
|
+
'delamanid': 'antitubercular', 'bedaquiline': 'antitubercular',
|
|
51
|
+
# v4: drugs in the export (or the UI list) that fell into 'other'.
|
|
52
|
+
# Only azidothymidine (an antiviral) is left there on purpose.
|
|
53
|
+
'cefixime': 'beta_lactam', 'cefpirome': 'beta_lactam', 'cefoperazone': 'beta_lactam',
|
|
54
|
+
'cefotetan': 'beta_lactam', 'cefiderocol': 'beta_lactam', 'ceftaroline': 'beta_lactam',
|
|
55
|
+
'cefovecin': 'beta_lactam', 'cefamandole': 'beta_lactam', 'cefaclor': 'beta_lactam',
|
|
56
|
+
'cefmetazole': 'beta_lactam', 'ceftizoxime': 'beta_lactam', 'cefdinir': 'beta_lactam',
|
|
57
|
+
'cefozopran': 'beta_lactam', 'ceftobiprole': 'beta_lactam', 'ceftibuten': 'beta_lactam',
|
|
58
|
+
'ceftriaxone/cefpodoxime': 'beta_lactam', 'cefotaxime/clavulanic acid': 'beta_lactam',
|
|
59
|
+
'ceftazidime/clavulanic acid': 'beta_lactam', 'cefoperazone/sulbactam': 'beta_lactam',
|
|
60
|
+
'cefepime/taniborbactam': 'beta_lactam', 'temocillin': 'beta_lactam',
|
|
61
|
+
'ticarcillin': 'beta_lactam', 'mecillinam': 'beta_lactam', 'sulbactam': 'beta_lactam',
|
|
62
|
+
'imipenem/relebactam': 'carbapenem',
|
|
63
|
+
'netilmicin': 'aminoglycoside', 'plazomicin': 'aminoglycoside', 'apramycin': 'aminoglycoside',
|
|
64
|
+
'gatifloxacin': 'fluoroquinolone', 'enrofloxacin': 'fluoroquinolone',
|
|
65
|
+
'danofloxacin': 'fluoroquinolone', 'sparfloxacin': 'fluoroquinolone',
|
|
66
|
+
'pradofloxacin': 'fluoroquinolone', 'delafloxacin': 'fluoroquinolone',
|
|
67
|
+
'chlortetracycline': 'tetracycline', 'oxytetracycline': 'tetracycline',
|
|
68
|
+
'eravacycline': 'tetracycline', 'omadacycline': 'tetracycline',
|
|
69
|
+
'sulfathiazole': 'sulfonamide', 'sulfamethazine': 'sulfonamide',
|
|
70
|
+
'trimethoprim/sulfobactam': 'sulfonamide',
|
|
71
|
+
'tylosin': 'macrolide', 'tulathromycin': 'macrolide', 'spiramycin': 'macrolide',
|
|
72
|
+
'florfenicol': 'phenicol', 'lincomycin': 'lincosamide', 'virginiamycin': 'streptogramin',
|
|
73
|
+
'furazolidone': 'nitrofuran', 'metronidazole': 'nitroimidazole',
|
|
74
|
+
'fosfomycin': 'phosphonic_acid', 'mupirocin': 'pseudomonic_acid',
|
|
75
|
+
'zoliflodacin': 'spiropyrimidinetrione', 'avilamycin': 'orthosomycin',
|
|
76
|
+
'nicotinamide': 'antitubercular', # all 229 rows are Mycobacterium
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
# Spelling variants, typos and non-drugs found in the raw Antibiotic column.
|
|
80
|
+
# Left-hand side is what appears in the export; right-hand side is canonical.
|
|
81
|
+
# None means "drop these rows", a drug class is not a drug.
|
|
82
|
+
ANTIBIOTIC_ALIASES = {
|
|
83
|
+
'ampicillin-sulbactam': 'ampicillin/sulbactam',
|
|
84
|
+
'ampicillin_clavulanic_acid': 'amoxicillin/clavulanic acid',
|
|
85
|
+
'amoxicillin-clavulanic acid': 'amoxicillin/clavulanic acid',
|
|
86
|
+
'piperacillin-tazobactam': 'piperacillin/tazobactam',
|
|
87
|
+
'trimethoprim-sulfamethoxazole': 'trimethoprim/sulfamethoxazole',
|
|
88
|
+
'sulfamethoxazole/trimethoprim': 'trimethoprim/sulfamethoxazole',
|
|
89
|
+
'co_trimoxazole': 'trimethoprim/sulfamethoxazole',
|
|
90
|
+
'co-trimoxazole': 'trimethoprim/sulfamethoxazole',
|
|
91
|
+
'geamycin': 'gentamicin',
|
|
92
|
+
'trimotheprim': 'trimethoprim',
|
|
93
|
+
'cefalothin': 'cephalothin',
|
|
94
|
+
'rifampin': 'rifampicin',
|
|
95
|
+
# Separator variants of combination drugs
|
|
96
|
+
'tazobactam_piperacillin': 'piperacillin/tazobactam',
|
|
97
|
+
'ceftazidime_avibactam': 'ceftazidime/avibactam',
|
|
98
|
+
'ceftolozane_tazobactam': 'ceftolozane/tazobactam',
|
|
99
|
+
'ticarcillin_clavulanate': 'ticarcillin/clavulanic acid',
|
|
100
|
+
'cefpodoxime_clavulanic_acid': 'cefpodoxime/clavulanic acid',
|
|
101
|
+
'trimethoprim_sulfobactam': 'trimethoprim/sulfobactam',
|
|
102
|
+
'para_aminosalicylic_acid': 'para-aminosalicylic acid',
|
|
103
|
+
'cefepime_taniborbactam': 'cefepime/taniborbactam',
|
|
104
|
+
'amoxicillin_clavulanat': 'amoxicillin/clavulanic acid',
|
|
105
|
+
'polymyxin_b': 'polymyxin b',
|
|
106
|
+
'ceftazidime-avibactam': 'ceftazidime/avibactam',
|
|
107
|
+
'ceftolozane-tazobactam': 'ceftolozane/tazobactam',
|
|
108
|
+
'imipenem-relebactam': 'imipenem/relebactam',
|
|
109
|
+
# Typos and a broken character encoding
|
|
110
|
+
'amipicillin_sulbactam': 'ampicillin/sulbactam',
|
|
111
|
+
'tgecycline': 'tigecycline',
|
|
112
|
+
'cefuroxim\u00e2': 'cefuroxime',
|
|
113
|
+
'pristimycin': 'pristinamycin',
|
|
114
|
+
'cefotaxime/clavulanic acid\u00e2': 'cefotaxime/clavulanic acid',
|
|
115
|
+
# Other-language spellings, found by Suleman in BVBRC_genome_amr.csv
|
|
116
|
+
'tigecyklin': 'tigecycline',
|
|
117
|
+
'tetracyklin': 'tetracycline',
|
|
118
|
+
'cefpirom': 'cefpirome',
|
|
119
|
+
# Shigella panel; never on the same genome as nitrofurantoin
|
|
120
|
+
'strofurantoin': 'nitrofurantoin',
|
|
121
|
+
# Alternative names for the same drug
|
|
122
|
+
'cefuroxime_sodium': 'cefuroxime',
|
|
123
|
+
'cefalotin': 'cephalothin',
|
|
124
|
+
'cefalexin': 'cephalexin',
|
|
125
|
+
'synercid': 'quinupristin/dalfopristin',
|
|
126
|
+
'phosphomycin': 'fosfomycin',
|
|
127
|
+
'benzylpenicillin': 'penicillin',
|
|
128
|
+
# Not a single drug: drug classes, a phenotype, and a lost drug name
|
|
129
|
+
# ('instrument' is 593 C. difficile lab rows with the drug name missing)
|
|
130
|
+
'carbapenem': None,
|
|
131
|
+
'beta-lactam': None,
|
|
132
|
+
'cephalosporin': None,
|
|
133
|
+
'fluoroquinolones': None,
|
|
134
|
+
'aminogycosides': None,
|
|
135
|
+
'macrolides': None,
|
|
136
|
+
'sulfonamides': None,
|
|
137
|
+
'sulfa': None,
|
|
138
|
+
'extended spectrum beta lactamase': None,
|
|
139
|
+
'instrument': None,
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
# Resistant Phenotype -> label. 0 = susceptible, 1 = resistant.
|
|
143
|
+
PHENOTYPE_MAP = {
|
|
144
|
+
'Susceptible': 0,
|
|
145
|
+
'Susceptible-dose dependent': 0,
|
|
146
|
+
'Resistant': 1,
|
|
147
|
+
'Intermediate': 1,
|
|
148
|
+
'Nonsusceptible': 1,
|
|
149
|
+
'Reduced Susceptibility': 1,
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
# Phenotypes each trainer keeps; the rest of its rows are dropped. They
|
|
153
|
+
# differ on purpose, to keep the served models' training data as it was.
|
|
154
|
+
LGBM_TRAIN_PHENOTYPES = ('Susceptible', 'Resistant', 'Intermediate', 'Nonsusceptible')
|
|
155
|
+
KMER_TRAIN_PHENOTYPES = ('Susceptible', 'Resistant', 'Intermediate')
|
|
156
|
+
|
|
157
|
+
# MIC comparators a user may type that the forecaster has no category for,
|
|
158
|
+
# mapped onto the nearest sign it learned.
|
|
159
|
+
MIC_SIGN_ALIASES = {'>=': '>', '\u2265': '>', '\u2264': '<=', '=<': '<=', '=>': '>'}
|
|
160
|
+
|
|
161
|
+
# Antibiotics offered in the dropdowns, grouped by class. The pages show only
|
|
162
|
+
# the names the loaded model knows; this list is the fallback when the backend
|
|
163
|
+
# is down, and what GET /api/antibiotics/ returns.
|
|
164
|
+
UI_ANTIBIOTICS = [
|
|
165
|
+
'penicillin', 'ampicillin', 'ampicillin/sulbactam', 'amoxicillin',
|
|
166
|
+
'amoxicillin/clavulanic acid', 'piperacillin', 'piperacillin/tazobactam',
|
|
167
|
+
'oxacillin', 'temocillin', 'carbenicillin', 'ticarcillin/clavulanic acid',
|
|
168
|
+
'cefazolin', 'cefoxitin', 'cefotetan', 'cefmetazole', 'cefotaxime',
|
|
169
|
+
'cefotaxime/clavulanic acid', 'ceftazidime', 'ceftazidime/avibactam',
|
|
170
|
+
'ceftazidime/clavulanic acid', 'ceftolozane/tazobactam', 'ceftriaxone',
|
|
171
|
+
'cefepime', 'cefepime/taniborbactam', 'cefuroxime', 'cephalothin',
|
|
172
|
+
'cefixime', 'cefpodoxime', 'cefpodoxime/clavulanic acid', 'ceftibuten',
|
|
173
|
+
'cefoperazone/sulbactam', 'ceftiofur', 'cefpirome', 'cefozopran',
|
|
174
|
+
'ceftaroline', 'ceftobiprole',
|
|
175
|
+
'imipenem', 'imipenem/relebactam', 'meropenem', 'ertapenem', 'doripenem',
|
|
176
|
+
'aztreonam', 'ciprofloxacin', 'levofloxacin', 'norfloxacin',
|
|
177
|
+
'nalidixic acid', 'ofloxacin', 'moxifloxacin', 'pefloxacin',
|
|
178
|
+
'delafloxacin',
|
|
179
|
+
'gentamicin', 'tobramycin', 'amikacin', 'streptomycin', 'neomycin',
|
|
180
|
+
'kanamycin', 'spectinomycin', 'apramycin',
|
|
181
|
+
'tetracycline', 'oxytetracycline', 'doxycycline', 'minocycline',
|
|
182
|
+
'tigecycline',
|
|
183
|
+
'sulfamethoxazole', 'sulfisoxazole', 'trimethoprim',
|
|
184
|
+
'trimethoprim/sulfamethoxazole',
|
|
185
|
+
'chloramphenicol', 'florfenicol',
|
|
186
|
+
'azithromycin', 'erythromycin', 'clarithromycin', 'telithromycin',
|
|
187
|
+
'colistin', 'polymyxin b', 'vancomycin', 'teicoplanin',
|
|
188
|
+
'clindamycin', 'lincomycin', 'nitrofurantoin', 'fosfomycin', 'rifampicin',
|
|
189
|
+
]
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def normalize_antibiotic(name):
|
|
193
|
+
"""Lower-case, strip, and map a spelling variant to its canonical name.
|
|
194
|
+
|
|
195
|
+
Names the alias table drops from training (drug classes such as
|
|
196
|
+
'carbapenem') are returned unchanged; a model then reports them as
|
|
197
|
+
unrecognised instead of silently answering for a different drug.
|
|
198
|
+
"""
|
|
199
|
+
if name in (None, ''):
|
|
200
|
+
return name
|
|
201
|
+
ab = ' '.join(str(name).split()).lower()
|
|
202
|
+
return ANTIBIOTIC_ALIASES.get(ab) or ab
|
amrpredict/cli.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""Command-line interface: ``amrpredict <command> [options]``."""
|
|
2
|
+
import argparse
|
|
3
|
+
import json
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _read_fasta(path):
|
|
8
|
+
if path == '-':
|
|
9
|
+
return sys.stdin.read()
|
|
10
|
+
with open(path, 'r', encoding='utf-8', errors='replace') as fh:
|
|
11
|
+
return fh.read()
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _emit(payload, pretty):
|
|
15
|
+
print(json.dumps(payload, indent=2 if pretty else None, default=str))
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def build_parser():
|
|
19
|
+
from . import __version__
|
|
20
|
+
|
|
21
|
+
parser = argparse.ArgumentParser(
|
|
22
|
+
prog='amrpredict',
|
|
23
|
+
description='Predict antimicrobial resistance from genomic data.',
|
|
24
|
+
)
|
|
25
|
+
parser.add_argument('--version', action='version', version=f'amrpredict {__version__}')
|
|
26
|
+
parser.add_argument('--compact', action='store_true', help='emit single-line JSON')
|
|
27
|
+
parser.add_argument('--model-dir', default=None, help='use artifacts from this directory')
|
|
28
|
+
sub = parser.add_subparsers(dest='command', required=True)
|
|
29
|
+
|
|
30
|
+
p = sub.add_parser('forecast', help='predict from metadata + MIC value')
|
|
31
|
+
p.add_argument('antibiotic')
|
|
32
|
+
p.add_argument('--taxon-id', type=int, default=None, help='NCBI taxonomy ID, e.g. 562')
|
|
33
|
+
p.add_argument('--mic-value', type=float, default=None, help='MIC in mg/L')
|
|
34
|
+
p.add_argument('--mic-sign', default=None, help="MIC comparator, e.g. '=' or '>'")
|
|
35
|
+
p.add_argument('--genus', default='unknown')
|
|
36
|
+
p.add_argument('--species', default='unknown')
|
|
37
|
+
p.add_argument('--threshold', type=float, default=None,
|
|
38
|
+
help="default: the bundled model's own threshold")
|
|
39
|
+
|
|
40
|
+
p = sub.add_parser('predict', help='predict from a complete genome FASTA')
|
|
41
|
+
p.add_argument('fasta', help="path to a FASTA file, or '-' for stdin")
|
|
42
|
+
p.add_argument('antibiotic')
|
|
43
|
+
p.add_argument('--threshold', type=float, default=None,
|
|
44
|
+
help="default: the model's own threshold")
|
|
45
|
+
p.add_argument('--genus', default=None)
|
|
46
|
+
p.add_argument('--species', default=None)
|
|
47
|
+
genes = p.add_mutually_exclusive_group()
|
|
48
|
+
genes.add_argument('--kmers', action='store_true', help='always use the k-mer model')
|
|
49
|
+
genes.add_argument('--genes', action='store_true',
|
|
50
|
+
help='require the gene model (needs AMRFinderPlus)')
|
|
51
|
+
|
|
52
|
+
p = sub.add_parser('species', help='identify the species of a complete genome')
|
|
53
|
+
p.add_argument('fasta', help="path to a FASTA file, or '-' for stdin")
|
|
54
|
+
|
|
55
|
+
p = sub.add_parser('trend', help='yearly resistance of a genus and drug, and its forecast')
|
|
56
|
+
p.add_argument('genus')
|
|
57
|
+
p.add_argument('antibiotic')
|
|
58
|
+
p.add_argument('--years', type=int, default=5, help='years ahead, 1 to 10')
|
|
59
|
+
|
|
60
|
+
p = sub.add_parser('plan', help='drug per week: the RL agent against fixed schedules')
|
|
61
|
+
p.add_argument('antibiotic', help='ciprofloxacin, gentamicin or imipenem')
|
|
62
|
+
p.add_argument('--weeks', type=int, default=52, help='1 to 104')
|
|
63
|
+
p.add_argument('--seed', type=int, default=42)
|
|
64
|
+
|
|
65
|
+
p = sub.add_parser('timeline', help='simulate resistance evolution (not a trained model)')
|
|
66
|
+
p.add_argument('fasta', help="path to a FASTA file, or '-' for stdin")
|
|
67
|
+
p.add_argument('antibiotic')
|
|
68
|
+
p.add_argument('--weeks', type=int, default=8)
|
|
69
|
+
|
|
70
|
+
p = sub.add_parser('antibiotics', help='list the antibiotics a model knows')
|
|
71
|
+
p.add_argument('--model', default='genome', choices=['genome', 'forecast', 'trend', 'rl'])
|
|
72
|
+
sub.add_parser('status', help='report which models loaded')
|
|
73
|
+
return parser
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def main(argv=None):
|
|
77
|
+
import amrpredict
|
|
78
|
+
|
|
79
|
+
args = build_parser().parse_args(argv)
|
|
80
|
+
pretty = not args.compact
|
|
81
|
+
md = args.model_dir
|
|
82
|
+
|
|
83
|
+
try:
|
|
84
|
+
if args.command == 'forecast':
|
|
85
|
+
result = amrpredict.forecast(
|
|
86
|
+
args.antibiotic, taxon_id=args.taxon_id, mic_value=args.mic_value,
|
|
87
|
+
mic_sign=args.mic_sign, genus=args.genus, species=args.species,
|
|
88
|
+
threshold=args.threshold, model_dir=md,
|
|
89
|
+
)
|
|
90
|
+
elif args.command == 'predict':
|
|
91
|
+
result = amrpredict.predict_fasta(
|
|
92
|
+
_read_fasta(args.fasta), args.antibiotic, threshold=args.threshold,
|
|
93
|
+
genus=args.genus, species=args.species,
|
|
94
|
+
use_genes=False if args.kmers else (True if args.genes else None), model_dir=md,
|
|
95
|
+
)
|
|
96
|
+
elif args.command == 'species':
|
|
97
|
+
result = amrpredict.identify_species(_read_fasta(args.fasta), model_dir=md)
|
|
98
|
+
elif args.command == 'trend':
|
|
99
|
+
result = amrpredict.forecast_trend(args.genus, args.antibiotic,
|
|
100
|
+
years_ahead=args.years, model_dir=md)
|
|
101
|
+
elif args.command == 'plan':
|
|
102
|
+
result = amrpredict.plan_treatment(args.antibiotic, n_weeks=args.weeks,
|
|
103
|
+
seed=args.seed, model_dir=md)
|
|
104
|
+
elif args.command == 'timeline':
|
|
105
|
+
result = amrpredict.simulate_timeline(
|
|
106
|
+
_read_fasta(args.fasta), args.antibiotic,
|
|
107
|
+
n_weeks=args.weeks, model_dir=md,
|
|
108
|
+
)
|
|
109
|
+
elif args.command == 'antibiotics':
|
|
110
|
+
result = amrpredict.antibiotics(args.model, model_dir=md)
|
|
111
|
+
else:
|
|
112
|
+
result = amrpredict.status(model_dir=md)
|
|
113
|
+
except FileNotFoundError as exc:
|
|
114
|
+
print(f'amrpredict: {exc}', file=sys.stderr)
|
|
115
|
+
return 2
|
|
116
|
+
except ValueError as exc:
|
|
117
|
+
print(f'amrpredict: {exc}', file=sys.stderr)
|
|
118
|
+
return 1
|
|
119
|
+
|
|
120
|
+
_emit(result, pretty)
|
|
121
|
+
return 0
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
if __name__ == '__main__':
|
|
125
|
+
raise SystemExit(main())
|