catomatic 0.2.1__tar.gz → 0.2.3__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.
- {catomatic-0.2.1 → catomatic-0.2.3}/LICENSE +0 -0
- {catomatic-0.2.1/src/catomatic.egg-info → catomatic-0.2.3}/PKG-INFO +23 -16
- {catomatic-0.2.1 → catomatic-0.2.3}/README.md +22 -15
- {catomatic-0.2.1 → catomatic-0.2.3}/pyproject.toml +1 -1
- {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic/BinaryCatalogue.py +0 -0
- {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic/PiezoTools.py +0 -0
- {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic/RegressionCatalogue.py +191 -168
- {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic/__init__.py +0 -0
- {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic/__main__.py +0 -0
- {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic/cli.py +1 -1
- {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic/defence_module.py +0 -0
- {catomatic-0.2.1 → catomatic-0.2.3/src/catomatic.egg-info}/PKG-INFO +23 -16
- {catomatic-0.2.1 → catomatic-0.2.3}/src/tests/__init__.py +0 -0
- {catomatic-0.2.1 → catomatic-0.2.3}/src/tests/test_BuildBinaryCatalogue.py +0 -0
- {catomatic-0.2.1 → catomatic-0.2.3}/src/tests/test_BuildRegressionCatalogue.py +23 -35
- {catomatic-0.2.1 → catomatic-0.2.3}/setup.cfg +0 -0
- {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic.egg-info/SOURCES.txt +0 -0
- {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic.egg-info/dependency_links.txt +0 -0
- {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic.egg-info/entry_points.txt +0 -0
- {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic.egg-info/not-zip-safe +0 -0
- {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic.egg-info/requires.txt +0 -0
- {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic.egg-info/top_level.txt +0 -0
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: catomatic
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: A tool for automatically building catalogues of antibiotic resistance-associated variants
|
|
5
5
|
Author: Dylan Adlard
|
|
6
6
|
Author-email: Philip W Fowler <philip.fowler@ndm.ox.ac.uk>
|
|
@@ -48,15 +48,15 @@ The first is used in [https://doi.org/10.1101/2025.01.30.635633](https://doi.org
|
|
|
48
48
|
|
|
49
49
|
This method relies on the logic that mutations that do not cause resistance can co-occur with those that do. If a mutation in isolation (solo) does not cause resistance, then it is not contributing to the phenotype when present in a mixture either.
|
|
50
50
|
|
|
51
|
-
Mutations that occur in isolation
|
|
51
|
+
Mutations that occur in isolation are traversed in sequence. When they meet the susceptible criteria for the selected test, they are characterized as benign (`S`) and removed from the dataset. This step repeats iteratively until no more solo benign mutations are found.
|
|
52
52
|
|
|
53
53
|
Remaining mutations are classified based on their resistance rates and statistical test results. Those that don’t meet thresholds are labeled as `U`.
|
|
54
54
|
|
|
55
55
|
The classification approach supports:
|
|
56
56
|
|
|
57
|
-
- **No test**:
|
|
58
|
-
- **Binomial test**: against a specified background resistance rate
|
|
59
|
-
- **Fisher's test**:
|
|
57
|
+
- **No test**: calls a mutation `S` during iterative unlocking when all its solo observations are susceptible; remaining solo mutations are `U`
|
|
58
|
+
- **Binomial test**: tests the solo resistance rate against a specified background resistance rate
|
|
59
|
+
- **Fisher's test**: compares the mutation solo contingency table with the no-mutation solo background
|
|
60
60
|
|
|
61
61
|
### Optional Interventions
|
|
62
62
|
|
|
@@ -68,9 +68,9 @@ Because the method uses GARC1 grammar, rules like `{rpoB@*_fs:R}` can be supplie
|
|
|
68
68
|
- Be additive (lower priority than specific entries)
|
|
69
69
|
- Replace matching mutations (requires `replace=True` and `wildcards` supplied)
|
|
70
70
|
|
|
71
|
-
The catalogue
|
|
71
|
+
The built catalogue is available as a dictionary through the `catalogue.catalogue` attribute. It can also be converted to a Piezo-compatible `pandas.DataFrame` with `build_piezo()`.
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
Evidence is stored with each catalogue entry and exported in the `EVIDENCE` field. It includes proportions, Wilson confidence intervals, solo contingency tables used for classification, full sample-level contingency tables for description, and p-values when a statistical test is used. Sample IDs can be included with `record_ids=True`.
|
|
74
74
|
|
|
75
75
|
### Example Workflow using Fisher's:
|
|
76
76
|
|
|
@@ -125,7 +125,7 @@ from catomatic.BinaryCatalogue import BinaryBuilder
|
|
|
125
125
|
catalogue = BinaryBuilder(samples=samples_df, mutations=mutations_df).build()
|
|
126
126
|
|
|
127
127
|
# View dictionary version
|
|
128
|
-
cat_dict = catalogue.
|
|
128
|
+
cat_dict = catalogue.catalogue
|
|
129
129
|
|
|
130
130
|
# Convert to Piezo-compatible format
|
|
131
131
|
catalogue_df = catalogue.build_piezo(
|
|
@@ -149,7 +149,7 @@ catalogue.to_piezo(
|
|
|
149
149
|
|
|
150
150
|
### CLI
|
|
151
151
|
|
|
152
|
-
After installation, the simplest way to run the catomatic catalogue builder is via the command line interface using the `binary` subcommand.
|
|
152
|
+
After installation, the simplest way to run the catomatic catalogue builder is via the command line interface using the `binary` subcommand. Use `--to_piezo` or `--to_json` to export the catalogue. If neither is supplied, no file is exported. Additional metadata is required when using `--to_piezo`.
|
|
153
153
|
|
|
154
154
|
#### Export to JSON
|
|
155
155
|
|
|
@@ -178,6 +178,8 @@ catomatic binary \
|
|
|
178
178
|
|
|
179
179
|
### CLI Parameters
|
|
180
180
|
|
|
181
|
+
The Binary CLI does not expose the Python `build()` options `tails` or `min_count`. The output flags are not mutually exclusive; if both are supplied, both exporters write to the same `--outfile`, so use only one.
|
|
182
|
+
|
|
181
183
|
| Parameter | Type | Description |
|
|
182
184
|
| ------------------ | ------- | ---------------------------------------------------------------------------------------------- |
|
|
183
185
|
| `--samples` | `str` | Path to the samples (phenotypes) file. Required. |
|
|
@@ -190,11 +192,16 @@ catomatic binary \
|
|
|
190
192
|
| `--version` | `str` | Catalogue version. Required with `--to_piezo`. |
|
|
191
193
|
| `--drug` | `str` | Name of the drug. Required with `--to_piezo`. |
|
|
192
194
|
| `--wildcards` | `str` | Path to JSON file containing wildcard mutation definitions. Required with `--to_piezo`. |
|
|
193
|
-
| `--test` | `str` | Type of statistical test to apply
|
|
194
|
-
| `--background` | `float` | Background
|
|
195
|
-
| `--p` | `float` |
|
|
196
|
-
| `--
|
|
197
|
-
| `--
|
|
195
|
+
| `--test` | `str` | Type of statistical test to apply: `Binomial` or `Fisher`. Optional; if omitted, no statistical test is used. |
|
|
196
|
+
| `--background` | `float` | Background resistance rate (0–1). Required if `--test Binomial` is used. |
|
|
197
|
+
| `--p` | `float` | Confidence level used for statistical testing. Optional. Defaults to `0.95` (a 0.05 significance threshold). |
|
|
198
|
+
| `--strict_unlock` | `flag` | For two-tailed tests, require statistical significance to classify homogeneous susceptible mutations as `S` during iterative unlocking. |
|
|
199
|
+
| `--frs` | `float` | Optional fraction-read-support threshold. Requires an `FRS` column in the mutations file. |
|
|
200
|
+
| `--seed` | `str[]` | Optional list of mutations to seed as susceptible before building. |
|
|
201
|
+
| `--record_ids` | `flag` | Include sample IDs in evidence records. |
|
|
202
|
+
| `--grammar` | `str` | Grammar used for Piezo export. Default: `GARC1`. |
|
|
203
|
+
| `--values` | `str` | Prediction values for Piezo export. Default: `RUS`. |
|
|
204
|
+
| `--for_piezo` | `flag` | Add placeholder rows for Piezo export. Default: `False` in the CLI. |
|
|
198
205
|
|
|
199
206
|
## Running catomatic's Regression Builder
|
|
200
207
|
|
|
@@ -310,8 +317,8 @@ catomatic regression \
|
|
|
310
317
|
|
|
311
318
|
### Notes
|
|
312
319
|
|
|
313
|
-
-
|
|
314
|
-
- For Piezo export, placeholder entries are inserted
|
|
320
|
+
- For `BinaryBuilder`, use `update_catalogue()` to apply post-hoc rules. Supply `wildcards` and set `replace=True` to replace entries matched by an aggregate rule. Specific entries can be added or replaced without `replace=True`.
|
|
321
|
+
- For Piezo export, placeholder entries are inserted when `for_piezo=True` and needed to satisfy parser requirements (R, S, and U must be represented). The Python export methods default to `for_piezo=True`; the CLI flag is off by default.
|
|
315
322
|
- The EVIDENCE column includes contingency tables, proportions, confidence intervals, and p-values, and may optionally include sample IDs if `record_ids=True`.
|
|
316
323
|
- To build a catalogue with the regression builder, as currently implemented, requires an ecoff as it will compare the predited effected against the background to supply an R/S/U label
|
|
317
324
|
- To only calculate predicted effects, this can be done in Python by calling RegressionBuilder.predict_effects()
|
|
@@ -23,15 +23,15 @@ The first is used in [https://doi.org/10.1101/2025.01.30.635633](https://doi.org
|
|
|
23
23
|
|
|
24
24
|
This method relies on the logic that mutations that do not cause resistance can co-occur with those that do. If a mutation in isolation (solo) does not cause resistance, then it is not contributing to the phenotype when present in a mixture either.
|
|
25
25
|
|
|
26
|
-
Mutations that occur in isolation
|
|
26
|
+
Mutations that occur in isolation are traversed in sequence. When they meet the susceptible criteria for the selected test, they are characterized as benign (`S`) and removed from the dataset. This step repeats iteratively until no more solo benign mutations are found.
|
|
27
27
|
|
|
28
28
|
Remaining mutations are classified based on their resistance rates and statistical test results. Those that don’t meet thresholds are labeled as `U`.
|
|
29
29
|
|
|
30
30
|
The classification approach supports:
|
|
31
31
|
|
|
32
|
-
- **No test**:
|
|
33
|
-
- **Binomial test**: against a specified background resistance rate
|
|
34
|
-
- **Fisher's test**:
|
|
32
|
+
- **No test**: calls a mutation `S` during iterative unlocking when all its solo observations are susceptible; remaining solo mutations are `U`
|
|
33
|
+
- **Binomial test**: tests the solo resistance rate against a specified background resistance rate
|
|
34
|
+
- **Fisher's test**: compares the mutation solo contingency table with the no-mutation solo background
|
|
35
35
|
|
|
36
36
|
### Optional Interventions
|
|
37
37
|
|
|
@@ -43,9 +43,9 @@ Because the method uses GARC1 grammar, rules like `{rpoB@*_fs:R}` can be supplie
|
|
|
43
43
|
- Be additive (lower priority than specific entries)
|
|
44
44
|
- Replace matching mutations (requires `replace=True` and `wildcards` supplied)
|
|
45
45
|
|
|
46
|
-
The catalogue
|
|
46
|
+
The built catalogue is available as a dictionary through the `catalogue.catalogue` attribute. It can also be converted to a Piezo-compatible `pandas.DataFrame` with `build_piezo()`.
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
Evidence is stored with each catalogue entry and exported in the `EVIDENCE` field. It includes proportions, Wilson confidence intervals, solo contingency tables used for classification, full sample-level contingency tables for description, and p-values when a statistical test is used. Sample IDs can be included with `record_ids=True`.
|
|
49
49
|
|
|
50
50
|
### Example Workflow using Fisher's:
|
|
51
51
|
|
|
@@ -100,7 +100,7 @@ from catomatic.BinaryCatalogue import BinaryBuilder
|
|
|
100
100
|
catalogue = BinaryBuilder(samples=samples_df, mutations=mutations_df).build()
|
|
101
101
|
|
|
102
102
|
# View dictionary version
|
|
103
|
-
cat_dict = catalogue.
|
|
103
|
+
cat_dict = catalogue.catalogue
|
|
104
104
|
|
|
105
105
|
# Convert to Piezo-compatible format
|
|
106
106
|
catalogue_df = catalogue.build_piezo(
|
|
@@ -124,7 +124,7 @@ catalogue.to_piezo(
|
|
|
124
124
|
|
|
125
125
|
### CLI
|
|
126
126
|
|
|
127
|
-
After installation, the simplest way to run the catomatic catalogue builder is via the command line interface using the `binary` subcommand.
|
|
127
|
+
After installation, the simplest way to run the catomatic catalogue builder is via the command line interface using the `binary` subcommand. Use `--to_piezo` or `--to_json` to export the catalogue. If neither is supplied, no file is exported. Additional metadata is required when using `--to_piezo`.
|
|
128
128
|
|
|
129
129
|
#### Export to JSON
|
|
130
130
|
|
|
@@ -153,6 +153,8 @@ catomatic binary \
|
|
|
153
153
|
|
|
154
154
|
### CLI Parameters
|
|
155
155
|
|
|
156
|
+
The Binary CLI does not expose the Python `build()` options `tails` or `min_count`. The output flags are not mutually exclusive; if both are supplied, both exporters write to the same `--outfile`, so use only one.
|
|
157
|
+
|
|
156
158
|
| Parameter | Type | Description |
|
|
157
159
|
| ------------------ | ------- | ---------------------------------------------------------------------------------------------- |
|
|
158
160
|
| `--samples` | `str` | Path to the samples (phenotypes) file. Required. |
|
|
@@ -165,11 +167,16 @@ catomatic binary \
|
|
|
165
167
|
| `--version` | `str` | Catalogue version. Required with `--to_piezo`. |
|
|
166
168
|
| `--drug` | `str` | Name of the drug. Required with `--to_piezo`. |
|
|
167
169
|
| `--wildcards` | `str` | Path to JSON file containing wildcard mutation definitions. Required with `--to_piezo`. |
|
|
168
|
-
| `--test` | `str` | Type of statistical test to apply
|
|
169
|
-
| `--background` | `float` | Background
|
|
170
|
-
| `--p` | `float` |
|
|
171
|
-
| `--
|
|
172
|
-
| `--
|
|
170
|
+
| `--test` | `str` | Type of statistical test to apply: `Binomial` or `Fisher`. Optional; if omitted, no statistical test is used. |
|
|
171
|
+
| `--background` | `float` | Background resistance rate (0–1). Required if `--test Binomial` is used. |
|
|
172
|
+
| `--p` | `float` | Confidence level used for statistical testing. Optional. Defaults to `0.95` (a 0.05 significance threshold). |
|
|
173
|
+
| `--strict_unlock` | `flag` | For two-tailed tests, require statistical significance to classify homogeneous susceptible mutations as `S` during iterative unlocking. |
|
|
174
|
+
| `--frs` | `float` | Optional fraction-read-support threshold. Requires an `FRS` column in the mutations file. |
|
|
175
|
+
| `--seed` | `str[]` | Optional list of mutations to seed as susceptible before building. |
|
|
176
|
+
| `--record_ids` | `flag` | Include sample IDs in evidence records. |
|
|
177
|
+
| `--grammar` | `str` | Grammar used for Piezo export. Default: `GARC1`. |
|
|
178
|
+
| `--values` | `str` | Prediction values for Piezo export. Default: `RUS`. |
|
|
179
|
+
| `--for_piezo` | `flag` | Add placeholder rows for Piezo export. Default: `False` in the CLI. |
|
|
173
180
|
|
|
174
181
|
## Running catomatic's Regression Builder
|
|
175
182
|
|
|
@@ -285,8 +292,8 @@ catomatic regression \
|
|
|
285
292
|
|
|
286
293
|
### Notes
|
|
287
294
|
|
|
288
|
-
-
|
|
289
|
-
- For Piezo export, placeholder entries are inserted
|
|
295
|
+
- For `BinaryBuilder`, use `update_catalogue()` to apply post-hoc rules. Supply `wildcards` and set `replace=True` to replace entries matched by an aggregate rule. Specific entries can be added or replaced without `replace=True`.
|
|
296
|
+
- For Piezo export, placeholder entries are inserted when `for_piezo=True` and needed to satisfy parser requirements (R, S, and U must be represented). The Python export methods default to `for_piezo=True`; the CLI flag is off by default.
|
|
290
297
|
- The EVIDENCE column includes contingency tables, proportions, confidence intervals, and p-values, and may optionally include sample IDs if `record_ids=True`.
|
|
291
298
|
- To build a catalogue with the regression builder, as currently implemented, requires an ecoff as it will compare the predited effected against the background to supply an R/S/U label
|
|
292
299
|
- To only calculate predicted effects, this can be done in Python by calling RegressionBuilder.predict_effects()
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "catomatic"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.3"
|
|
8
8
|
description = "A tool for automatically building catalogues of antibiotic resistance-associated variants"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
# If README.md is markdown, setuptools will pick it up; optionally:
|
|
File without changes
|
|
File without changes
|
|
@@ -10,7 +10,7 @@ from .defence_module import (
|
|
|
10
10
|
validate_regression_predict_inputs,
|
|
11
11
|
validate_regression_classify_inputs,
|
|
12
12
|
)
|
|
13
|
-
from typing import Any, Optional, Sequence, Tuple
|
|
13
|
+
from typing import Any, Optional, Sequence, Tuple, cast
|
|
14
14
|
from intreg.meintreg import MeIntReg
|
|
15
15
|
from sklearn.cluster import AgglomerativeClustering
|
|
16
16
|
|
|
@@ -188,7 +188,7 @@ class RegressionBuilder(PiezoExporter):
|
|
|
188
188
|
fixed_effects_data,
|
|
189
189
|
columns=fixed_effects,
|
|
190
190
|
prefix=fixed_effects, # Prefix helps to distinguish columns
|
|
191
|
-
drop_first=
|
|
191
|
+
drop_first=True,
|
|
192
192
|
)
|
|
193
193
|
.reindex(ids, fill_value=0)
|
|
194
194
|
.astype(int)
|
|
@@ -293,7 +293,7 @@ class RegressionBuilder(PiezoExporter):
|
|
|
293
293
|
|
|
294
294
|
snps = self.mutations[
|
|
295
295
|
~self.mutations["MUTATION"].str.contains(
|
|
296
|
-
r"(?:indel|ins|del|Z|LOF)", regex=True
|
|
296
|
+
r"(?:indel|ins|del|Z|LOF|HAP|phylo|interaction)", regex=True
|
|
297
297
|
)
|
|
298
298
|
].copy()
|
|
299
299
|
|
|
@@ -379,19 +379,23 @@ class RegressionBuilder(PiezoExporter):
|
|
|
379
379
|
if not self.censored:
|
|
380
380
|
tail_dilution_factor = self.dilution_factor**self.tail_dilutions
|
|
381
381
|
|
|
382
|
+
def parse_mic_value(mic: str) -> float:
|
|
383
|
+
#if a fraction (ie 2 drugs), just keep the numerator
|
|
384
|
+
return float(mic.split('/')[0])
|
|
385
|
+
|
|
382
386
|
for i, mic in enumerate(df.MIC):
|
|
383
387
|
if mic.startswith("<="): # Left-censored
|
|
384
|
-
lower_bound =
|
|
388
|
+
lower_bound = parse_mic_value(mic[2:])
|
|
385
389
|
y_low[i] = 1e-6 if self.censored else lower_bound / tail_dilution_factor
|
|
386
390
|
y_high[i] = lower_bound
|
|
387
391
|
elif mic.startswith(">"): # Right-censored
|
|
388
|
-
upper_bound =
|
|
392
|
+
upper_bound = parse_mic_value(mic[1:])
|
|
389
393
|
y_low[i] = upper_bound
|
|
390
394
|
y_high[i] = (
|
|
391
395
|
np.inf if self.censored else upper_bound * tail_dilution_factor
|
|
392
396
|
)
|
|
393
397
|
else: # Exact MIC value
|
|
394
|
-
mic_value =
|
|
398
|
+
mic_value = parse_mic_value(mic)
|
|
395
399
|
y_low[i] = mic_value / self.dilution_factor
|
|
396
400
|
y_high[i] = mic_value
|
|
397
401
|
|
|
@@ -443,15 +447,15 @@ class RegressionBuilder(PiezoExporter):
|
|
|
443
447
|
y_low: np.ndarray,
|
|
444
448
|
y_high: np.ndarray,
|
|
445
449
|
clusters: Optional[Sequence[int]],
|
|
446
|
-
) -> Tuple[np.ndarray,
|
|
450
|
+
) -> Tuple[np.ndarray, float, float]:
|
|
447
451
|
"""
|
|
448
|
-
Generate initial
|
|
452
|
+
Generate initial parameters for the regression model.
|
|
449
453
|
|
|
450
454
|
Strategy:
|
|
451
455
|
- Use interval midpoints where finite.
|
|
452
456
|
- Estimate beta via least squares on the finite subset.
|
|
453
|
-
- Sample small random initial u (random effects).
|
|
454
457
|
- Set sigma to log(std(midpoints)).
|
|
458
|
+
- Set tau to an initial random-effect SD of 0.5.
|
|
455
459
|
|
|
456
460
|
Args:
|
|
457
461
|
X: Binary design matrix.
|
|
@@ -460,23 +464,24 @@ class RegressionBuilder(PiezoExporter):
|
|
|
460
464
|
clusters: Cluster labels (or None).
|
|
461
465
|
|
|
462
466
|
Returns:
|
|
463
|
-
(beta_init,
|
|
467
|
+
(beta_init, log_sigma_init, log_tau_init).
|
|
464
468
|
"""
|
|
465
|
-
|
|
469
|
+
|
|
466
470
|
midpoints = (y_low + y_high) / 2.0
|
|
467
471
|
valid_mask = np.isfinite(midpoints)
|
|
468
472
|
X_valid = X[valid_mask]
|
|
469
473
|
midpoints_valid = midpoints[valid_mask]
|
|
470
|
-
|
|
474
|
+
|
|
471
475
|
beta_init = np.linalg.lstsq(X_valid, midpoints_valid, rcond=None)[0]
|
|
472
|
-
# Initial random effects - small non-zero value
|
|
473
|
-
u_init = np.random.normal(loc=0, scale=0.1, size=len(np.unique(clusters or [])))
|
|
474
|
-
# sigma - std of valid midpoints
|
|
475
|
-
sigma = np.nanstd(midpoints_valid)
|
|
476
|
-
sigma = np.log(sigma)
|
|
477
476
|
|
|
478
|
-
|
|
477
|
+
sigma = max(np.nanstd(midpoints_valid), 1e-6)
|
|
478
|
+
log_sigma = np.log(sigma)
|
|
479
|
+
|
|
480
|
+
tau = 0.5
|
|
481
|
+
log_tau = np.log(tau)
|
|
479
482
|
|
|
483
|
+
return beta_init, log_sigma, log_tau
|
|
484
|
+
|
|
480
485
|
def fit(
|
|
481
486
|
self,
|
|
482
487
|
X: pd.DataFrame,
|
|
@@ -490,40 +495,32 @@ class RegressionBuilder(PiezoExporter):
|
|
|
490
495
|
"""
|
|
491
496
|
Fit the mixed-effects interval regression model.
|
|
492
497
|
|
|
493
|
-
Initial
|
|
494
|
-
|
|
498
|
+
Initial parameters are generated via least-squares on interval midpoints.
|
|
499
|
+
Random effects are integrated out by MeIntReg using Gauss-Hermite quadrature.
|
|
500
|
+
The model is fitted using L-BFGS-B optimization.
|
|
495
501
|
|
|
496
502
|
Args:
|
|
497
|
-
X
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
Cluster labels defining random intercept groups.
|
|
505
|
-
bounds : list[tuple], optional
|
|
506
|
-
Parameter bounds applied in order to:
|
|
507
|
-
β (fixed effects), u (random intercepts), log(σ).
|
|
508
|
-
options : dict, optional
|
|
509
|
-
Optimizer settings passed to scipy.optimize.minimize.
|
|
510
|
-
L2_penalties : dict, optional
|
|
511
|
-
Ridge penalties with keys: 'lambda_beta', 'lambda_u', 'lambda_sigma'.
|
|
503
|
+
X: Fixed-effect design matrix including intercept.
|
|
504
|
+
y_low: Lower interval bounds (log scale).
|
|
505
|
+
y_high: Upper interval bounds (log scale).
|
|
506
|
+
random_effects: Cluster labels defining random intercept groups.
|
|
507
|
+
bounds: Parameter bounds applied to beta, log(sigma), and log(tau).
|
|
508
|
+
options: Optimizer settings passed to scipy.optimize.minimize.
|
|
509
|
+
L2_penalties: Optional ridge penalties.
|
|
512
510
|
|
|
513
511
|
Returns:
|
|
514
|
-
MeIntReg
|
|
515
|
-
Fitted model instance containing optimization results.
|
|
512
|
+
Fitted MeIntReg model.
|
|
516
513
|
"""
|
|
517
|
-
|
|
514
|
+
|
|
515
|
+
_b, _s, _t = self.initial_params(X, y_low, y_high, random_effects)
|
|
518
516
|
|
|
519
517
|
if random_effects is not None:
|
|
520
|
-
initial_params = np.concatenate([_b,
|
|
518
|
+
initial_params = np.concatenate([_b, [_s, _t]])
|
|
521
519
|
else:
|
|
522
520
|
initial_params = np.concatenate([_b, [_s]])
|
|
523
521
|
|
|
524
|
-
|
|
525
522
|
return MeIntReg(y_low, y_high, X.to_numpy(), random_effects).fit(
|
|
526
|
-
method="
|
|
523
|
+
method="SLSQP",
|
|
527
524
|
initial_params=initial_params,
|
|
528
525
|
bounds=bounds,
|
|
529
526
|
options=options,
|
|
@@ -536,6 +533,7 @@ class RegressionBuilder(PiezoExporter):
|
|
|
536
533
|
b_bounds: tuple[Optional[float], Optional[float]] = (None, None),
|
|
537
534
|
u_bounds: tuple[Optional[float], Optional[float]] = (None, None),
|
|
538
535
|
s_bounds: tuple[Optional[float], Optional[float]] = (None, None),
|
|
536
|
+
t_bounds: tuple[Optional[float], Optional[float]] = (None, None),
|
|
539
537
|
options: Optional[dict[str, Any]] = None,
|
|
540
538
|
L2_penalties: Optional[dict[str, Any]] = None,
|
|
541
539
|
fixed_effects: Optional[list[str]] = None,
|
|
@@ -545,33 +543,24 @@ class RegressionBuilder(PiezoExporter):
|
|
|
545
543
|
"""
|
|
546
544
|
Fit the regression model and extract mutation-level effect estimates.
|
|
547
545
|
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
3. Optionally infers population clusters for random intercepts.
|
|
551
|
-
4. Fits the mixed-effects interval regression.
|
|
552
|
-
5. Extracts per-mutation effect sizes and uncertainties.
|
|
553
|
-
|
|
554
|
-
The design matrix (`self.X`) and cluster assignments (`self.clusters`)
|
|
555
|
-
are stored for post-fit inspection.
|
|
546
|
+
Random intercepts are integrated out by MeIntReg using Gauss-Hermite
|
|
547
|
+
quadrature rather than being estimated as individual parameters.
|
|
556
548
|
|
|
557
549
|
Args:
|
|
558
|
-
b_bounds
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
random_effects
|
|
567
|
-
|
|
568
|
-
cluster_distance : int, default=50
|
|
569
|
-
SNP distance threshold for clustering.
|
|
550
|
+
b_bounds: Bounds for fixed effects.
|
|
551
|
+
u_bounds: Retained for backwards compatibility; no longer used because
|
|
552
|
+
random effects are integrated out.
|
|
553
|
+
s_bounds: Bounds for log(sigma).
|
|
554
|
+
t_bounds: Bounds for log(tau).
|
|
555
|
+
options: Optimizer settings.
|
|
556
|
+
L2_penalties: Ridge regularization arguments.
|
|
557
|
+
fixed_effects: Additional fixed-effect columns to include.
|
|
558
|
+
random_effects: Whether to include population-structure random intercepts.
|
|
559
|
+
cluster_distance: SNP distance threshold for clustering.
|
|
570
560
|
|
|
571
561
|
Returns:
|
|
572
|
-
(model, effects)
|
|
573
|
-
|
|
574
|
-
effects : pd.DataFrame with mutation effect estimates.
|
|
562
|
+
(model, effects), where model is the fitted MeIntReg object and effects
|
|
563
|
+
contains mutation-level effect estimates.
|
|
575
564
|
"""
|
|
576
565
|
|
|
577
566
|
validate_regression_predict_inputs(
|
|
@@ -589,43 +578,101 @@ class RegressionBuilder(PiezoExporter):
|
|
|
589
578
|
|
|
590
579
|
y_low, y_high = self.define_intervals(self.samples)
|
|
591
580
|
|
|
592
|
-
#
|
|
581
|
+
# Don't fit synonymous mutations (they are used for clustering)
|
|
593
582
|
aa = self.mutations["MUTATION"].str.split("@").str[-1]
|
|
594
|
-
syn_mask = aa.str.match(r'^[A-Z]
|
|
583
|
+
syn_mask = aa.str.match(r'^[A-Z].\*[A-Z]$') & (aa.str[0] == aa.str[-1])
|
|
595
584
|
|
|
596
585
|
if len(self.genes) > 0:
|
|
597
586
|
self.target_mutations = self.mutations[
|
|
598
|
-
self.mutations["MUTATION"].str.split("@").str[0].isin(self.genes)
|
|
599
|
-
& (~syn_mask)
|
|
587
|
+
self.mutations["MUTATION"].str.split("@").str[0].isin(self.genes) & (~syn_mask)
|
|
600
588
|
]
|
|
601
589
|
else:
|
|
602
590
|
self.target_mutations = self.mutations[~syn_mask]
|
|
603
591
|
|
|
604
|
-
self.df = pd.merge(
|
|
605
|
-
self.samples, self.target_mutations, on=["UNIQUEID"], how="left"
|
|
606
|
-
)
|
|
592
|
+
self.df = pd.merge(self.samples, self.target_mutations, on=["UNIQUEID"], how="left")
|
|
607
593
|
|
|
608
594
|
X = self.build_X(self.df, fixed_effects=fixed_effects)
|
|
609
595
|
self.X = X
|
|
610
596
|
|
|
611
597
|
if random_effects:
|
|
612
|
-
|
|
613
|
-
|
|
598
|
+
clusters = self.calc_clusters(cluster_distance)
|
|
599
|
+
n_clusters = len(np.unique(clusters))
|
|
600
|
+
|
|
601
|
+
if n_clusters < 2:
|
|
602
|
+
print("Only one population cluster detected; fitting without random effects.")
|
|
603
|
+
self.clusters = None
|
|
604
|
+
else:
|
|
605
|
+
self.clusters = clusters
|
|
614
606
|
else:
|
|
615
607
|
self.clusters = None
|
|
616
|
-
u_bounds_ = []
|
|
617
608
|
|
|
618
609
|
b_bounds_ = [b_bounds] * X.shape[1]
|
|
619
|
-
|
|
610
|
+
|
|
611
|
+
if self.clusters is not None:
|
|
612
|
+
bounds_ = b_bounds_ + [s_bounds, t_bounds]
|
|
613
|
+
else:
|
|
614
|
+
bounds_ = b_bounds_ + [s_bounds]
|
|
620
615
|
|
|
621
616
|
model = self.fit(X, y_low, y_high, self.clusters, bounds_, options, L2_penalties)
|
|
622
617
|
|
|
623
|
-
print
|
|
618
|
+
print(model.result)
|
|
624
619
|
|
|
625
620
|
effects = self.extract_effects(model, X, fixed_effects)
|
|
626
621
|
|
|
627
622
|
return model, effects
|
|
628
623
|
|
|
624
|
+
def calc_hessian(self, model, rel_step=1e-5, abs_step=1e-6):
|
|
625
|
+
"""
|
|
626
|
+
Calculate the Hessian of the fitted negative marginal log-likelihood.
|
|
627
|
+
"""
|
|
628
|
+
return model.hessian()
|
|
629
|
+
|
|
630
|
+
|
|
631
|
+
def calc_standard_errors(
|
|
632
|
+
self,
|
|
633
|
+
model: Any,
|
|
634
|
+
effects: pd.DataFrame,
|
|
635
|
+
indices: Sequence[int],
|
|
636
|
+
mutations: Sequence[str],
|
|
637
|
+
intercept_idx: int,
|
|
638
|
+
) -> pd.DataFrame:
|
|
639
|
+
|
|
640
|
+
#calculate effect and MIC standard errors from hessian matrix at the optimum
|
|
641
|
+
hessian_ = self.calc_hessian(model)
|
|
642
|
+
#regularised inverse for stability
|
|
643
|
+
ridge = 1e-8 * np.eye(hessian_.shape[0])
|
|
644
|
+
cov = np.linalg.pinv(hessian_ + ridge)
|
|
645
|
+
|
|
646
|
+
effect_ses = []
|
|
647
|
+
pred_logmic_ses = []
|
|
648
|
+
mic_ses = []
|
|
649
|
+
|
|
650
|
+
for m_idx, mutation_name in zip(indices, mutations):
|
|
651
|
+
var_effect = cov[m_idx, m_idx]
|
|
652
|
+
var_effect = max(var_effect, 0.0)
|
|
653
|
+
effect_se = np.sqrt(var_effect)
|
|
654
|
+
effect_ses.append(effect_se)
|
|
655
|
+
|
|
656
|
+
var_theta = (
|
|
657
|
+
cov[intercept_idx, intercept_idx]
|
|
658
|
+
+ cov[m_idx, m_idx]
|
|
659
|
+
+ 2.0 * cov[intercept_idx, m_idx]
|
|
660
|
+
)
|
|
661
|
+
var_theta = max(var_theta, 0.0)
|
|
662
|
+
pred_logmic_se = np.sqrt(var_theta)
|
|
663
|
+
pred_logmic_ses.append(pred_logmic_se)
|
|
664
|
+
|
|
665
|
+
mic = effects.loc[effects["Mutation"] == mutation_name, "MIC"].values[0]
|
|
666
|
+
mic_se = mic * np.log(self.dilution_factor) * pred_logmic_se
|
|
667
|
+
mic_ses.append(mic_se)
|
|
668
|
+
|
|
669
|
+
effects["effect_se"] = effect_ses
|
|
670
|
+
effects["predicted_logMIC_se"] = pred_logmic_ses
|
|
671
|
+
effects["MIC_se"] = mic_ses
|
|
672
|
+
|
|
673
|
+
return effects
|
|
674
|
+
|
|
675
|
+
|
|
629
676
|
def extract_effects(
|
|
630
677
|
self,
|
|
631
678
|
model: Any,
|
|
@@ -640,16 +687,22 @@ class RegressionBuilder(PiezoExporter):
|
|
|
640
687
|
|
|
641
688
|
effect_size = β_mutation
|
|
642
689
|
|
|
643
|
-
|
|
690
|
+
Mutation-level uncertainty is quantified using:
|
|
691
|
+
|
|
692
|
+
effect_se = SE(β_mutation)
|
|
693
|
+
|
|
694
|
+
Absolute predicted MIC for a strain carrying the mutation is computed as:
|
|
695
|
+
|
|
696
|
+
predicted_logMIC = β0 + β_mutation
|
|
644
697
|
|
|
645
698
|
MIC = dilution_factor^(β0 + β_mutation)
|
|
646
699
|
|
|
647
|
-
|
|
648
|
-
|
|
700
|
+
Uncertainty in the predicted MIC includes both intercept and mutation
|
|
701
|
+
uncertainty and is derived from the inverse Hessian:
|
|
649
702
|
|
|
650
703
|
Var(β0 + β_mutation)
|
|
651
704
|
|
|
652
|
-
|
|
705
|
+
Standard errors on the MIC scale are obtained using the delta method.
|
|
653
706
|
|
|
654
707
|
Args:
|
|
655
708
|
model : MeIntReg
|
|
@@ -663,20 +716,31 @@ class RegressionBuilder(PiezoExporter):
|
|
|
663
716
|
pd.DataFrame
|
|
664
717
|
Columns:
|
|
665
718
|
Mutation
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
719
|
+
Mutation identifier.
|
|
720
|
+
effect_size
|
|
721
|
+
Estimated mutation coefficient (log2 MIC shift relative
|
|
722
|
+
to baseline).
|
|
723
|
+
effect_se
|
|
724
|
+
Standard error of the mutation coefficient.
|
|
725
|
+
fold_change
|
|
726
|
+
Multiplicative MIC change relative to baseline.
|
|
727
|
+
predicted_logMIC
|
|
728
|
+
Predicted log MIC for a strain carrying the mutation.
|
|
729
|
+
predicted_logMIC_se
|
|
730
|
+
Standard error of the predicted log MIC, including
|
|
731
|
+
intercept–mutation covariance.
|
|
732
|
+
MIC
|
|
733
|
+
Predicted MIC on the original concentration scale.
|
|
734
|
+
MIC_se
|
|
735
|
+
Delta-method standard error of the predicted MIC.
|
|
671
736
|
"""
|
|
672
737
|
p = X.shape[1]
|
|
673
738
|
coefs = model.result.x[:p]
|
|
674
739
|
|
|
675
|
-
|
|
676
|
-
intercept_idx = X.columns.get_loc("Intercept")
|
|
740
|
+
intercept_idx = cast(int, X.columns.get_loc("Intercept"))
|
|
677
741
|
beta0 = coefs[intercept_idx]
|
|
678
742
|
|
|
679
|
-
#
|
|
743
|
+
# Exclude intercept and any fixed-effect dummy columns from mutation list
|
|
680
744
|
columns_to_exclude = (
|
|
681
745
|
{
|
|
682
746
|
col
|
|
@@ -687,89 +751,43 @@ class RegressionBuilder(PiezoExporter):
|
|
|
687
751
|
if fixed_effects
|
|
688
752
|
else set()
|
|
689
753
|
)
|
|
690
|
-
|
|
691
754
|
columns_to_exclude.add("Intercept")
|
|
692
755
|
|
|
693
|
-
mutation_columns = [
|
|
694
|
-
|
|
695
|
-
]
|
|
696
|
-
|
|
697
|
-
mutation_indices = [
|
|
698
|
-
X.columns.get_loc(col) for col in mutation_columns
|
|
699
|
-
]
|
|
756
|
+
mutation_columns = [col for col in X.columns if col not in columns_to_exclude]
|
|
757
|
+
mutation_indices = [cast(int, X.columns.get_loc(col)) for col in mutation_columns]
|
|
700
758
|
|
|
701
759
|
mutation_effects = coefs[mutation_indices]
|
|
702
760
|
|
|
703
761
|
effects = pd.DataFrame(
|
|
704
762
|
{
|
|
705
763
|
"Mutation": mutation_columns,
|
|
706
|
-
"effect_size": mutation_effects,
|
|
764
|
+
"effect_size": mutation_effects,
|
|
707
765
|
}
|
|
708
766
|
)
|
|
709
767
|
|
|
710
|
-
#
|
|
711
|
-
|
|
712
|
-
#
|
|
713
|
-
effects["
|
|
714
|
-
|
|
715
|
-
)
|
|
716
|
-
# Absolute predicted MIC
|
|
717
|
-
effects["MIC"] = baseline_MIC * effects["fold_change"]
|
|
718
|
-
|
|
719
|
-
# If Hessian available, compute SEs properly
|
|
720
|
-
if hasattr(model.result, "hess_inv"):
|
|
721
|
-
|
|
722
|
-
# Convert L-BFGS product to dense matrix
|
|
723
|
-
hess_inv_dense = np.asarray(model.result.hess_inv.todense())
|
|
768
|
+
# Mutation-only fold change
|
|
769
|
+
effects["fold_change"] = self.dilution_factor ** effects["effect_size"]
|
|
770
|
+
# Absolute predicted log MIC and MIC
|
|
771
|
+
effects["predicted_logMIC"] = beta0 + effects["effect_size"]
|
|
772
|
+
effects["MIC"] = self.dilution_factor ** effects["predicted_logMIC"]
|
|
724
773
|
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
mic_stds = []
|
|
729
|
-
|
|
730
|
-
for m_idx in mutation_indices:
|
|
731
|
-
|
|
732
|
-
# Variance of (beta0 + beta_m)
|
|
733
|
-
var_theta = (
|
|
734
|
-
hess_inv_dense[beta0_idx, beta0_idx]
|
|
735
|
-
+ hess_inv_dense[m_idx, m_idx]
|
|
736
|
-
+ 2 * hess_inv_dense[beta0_idx, m_idx]
|
|
737
|
-
)
|
|
738
|
-
|
|
739
|
-
# Numerical guard
|
|
740
|
-
var_theta = max(var_theta, 0.0)
|
|
741
|
-
|
|
742
|
-
sd_theta = np.sqrt(var_theta)
|
|
743
|
-
theta_stds.append(sd_theta)
|
|
744
|
-
|
|
745
|
-
# Delta method to MIC scale
|
|
746
|
-
mic_sd = (
|
|
747
|
-
effects.loc[
|
|
748
|
-
effects["Mutation"] == X.columns[m_idx], "MIC"
|
|
749
|
-
].values[0]
|
|
750
|
-
* np.log(self.dilution_factor)
|
|
751
|
-
* sd_theta
|
|
752
|
-
)
|
|
753
|
-
|
|
754
|
-
mic_stds.append(mic_sd)
|
|
755
|
-
|
|
756
|
-
effects["effect_std"] = theta_stds
|
|
757
|
-
effects["MIC_std"] = mic_stds
|
|
774
|
+
effects = self.calc_standard_errors(
|
|
775
|
+
model, effects, mutation_indices, mutation_columns, intercept_idx
|
|
776
|
+
)
|
|
758
777
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
else:
|
|
770
|
-
effects = effects[
|
|
771
|
-
["Mutation", "effect_size", "fold_change", "MIC"]
|
|
778
|
+
effects = effects[
|
|
779
|
+
[
|
|
780
|
+
"Mutation",
|
|
781
|
+
"effect_size",
|
|
782
|
+
"effect_se",
|
|
783
|
+
"fold_change",
|
|
784
|
+
"predicted_logMIC",
|
|
785
|
+
"predicted_logMIC_se",
|
|
786
|
+
"MIC",
|
|
787
|
+
"MIC_se",
|
|
772
788
|
]
|
|
789
|
+
]
|
|
790
|
+
|
|
773
791
|
|
|
774
792
|
return effects
|
|
775
793
|
|
|
@@ -800,10 +818,10 @@ class RegressionBuilder(PiezoExporter):
|
|
|
800
818
|
Classify mutation effects as Resistant (R), Susceptible (S), or Undetermined (U) using a z-test.
|
|
801
819
|
|
|
802
820
|
Effects are classified by comparing effect_size to the (log-space) breakpoint and applying
|
|
803
|
-
a two-tailed z-test using
|
|
821
|
+
a two-tailed z-test using predicted_logMIC_se.
|
|
804
822
|
|
|
805
823
|
Args:
|
|
806
|
-
effects: Effects DataFrame with '
|
|
824
|
+
effects: Effects DataFrame with 'predicted_logMIC' and 'predicted_logMIC_sed'.
|
|
807
825
|
p: Confidence parameter (default 0.95).
|
|
808
826
|
|
|
809
827
|
Returns:
|
|
@@ -815,14 +833,18 @@ class RegressionBuilder(PiezoExporter):
|
|
|
815
833
|
breakpoint = self.log_transf_val(ecoff)
|
|
816
834
|
|
|
817
835
|
effects["p_value"] = effects.apply(
|
|
818
|
-
lambda row: self.z_test(
|
|
836
|
+
lambda row: self.z_test(
|
|
837
|
+
row["predicted_logMIC"],
|
|
838
|
+
breakpoint,
|
|
839
|
+
row["predicted_logMIC_se"],
|
|
840
|
+
),
|
|
819
841
|
axis=1,
|
|
820
842
|
)
|
|
821
843
|
|
|
822
844
|
effects["Classification"] = np.select(
|
|
823
845
|
condlist=[
|
|
824
|
-
(effects["
|
|
825
|
-
(effects["
|
|
846
|
+
(effects["predicted_logMIC"] > breakpoint) & (effects["p_value"] < (1 - p)),
|
|
847
|
+
(effects["predicted_logMIC"] < breakpoint) & (effects["p_value"] < (1 - p)),
|
|
826
848
|
],
|
|
827
849
|
choicelist=["R", "S"],
|
|
828
850
|
default="U",
|
|
@@ -853,6 +875,7 @@ class RegressionBuilder(PiezoExporter):
|
|
|
853
875
|
b_bounds: tuple[Optional[float], Optional[float]] = (None, None),
|
|
854
876
|
u_bounds: tuple[Optional[float], Optional[float]] = (None, None),
|
|
855
877
|
s_bounds: tuple[Optional[float], Optional[float]] = (None, None),
|
|
878
|
+
t_bounds: tuple[Optional[float], Optional[float]] = (None, None),
|
|
856
879
|
options: Optional[dict[str, Any]] = None,
|
|
857
880
|
L2_penalties: Optional[dict[str, Any]] = None,
|
|
858
881
|
p: float = 0.95,
|
|
@@ -896,6 +919,7 @@ class RegressionBuilder(PiezoExporter):
|
|
|
896
919
|
b_bounds=b_bounds,
|
|
897
920
|
u_bounds=u_bounds,
|
|
898
921
|
s_bounds=s_bounds,
|
|
922
|
+
t_bounds=t_bounds,
|
|
899
923
|
options=options,
|
|
900
924
|
L2_penalties=L2_penalties,
|
|
901
925
|
fixed_effects=fixed_effects,
|
|
@@ -910,18 +934,17 @@ class RegressionBuilder(PiezoExporter):
|
|
|
910
934
|
breakpoint = self.log_transf_val(ecoff)
|
|
911
935
|
|
|
912
936
|
def add_mutation_from_row(row: pd.Series) -> None:
|
|
913
|
-
evidence
|
|
937
|
+
evidence = {
|
|
914
938
|
"MIC": row.get("MIC"),
|
|
915
939
|
"ECOFF": ecoff,
|
|
916
940
|
"effect_size": row.get("effect_size"),
|
|
941
|
+
"effect_se": row.get("effect_se"),
|
|
942
|
+
"predicted_logMIC": row.get("predicted_logMIC"),
|
|
943
|
+
"predicted_logMIC_se": row.get("predicted_logMIC_se"),
|
|
917
944
|
"breakpoint": breakpoint,
|
|
918
945
|
"p_value": row.get("p_value"),
|
|
946
|
+
"MIC_se": row.get("MIC_se"),
|
|
919
947
|
}
|
|
920
|
-
# Only attach std fields if present.
|
|
921
|
-
if "MIC_std" in row:
|
|
922
|
-
evidence["MIC_std"] = row.get("MIC_std")
|
|
923
|
-
if "effect_std" in row:
|
|
924
|
-
evidence["effect_std"] = row.get("effect_std")
|
|
925
948
|
|
|
926
949
|
self.add_mutation(str(row["Mutation"]), str(row["Classification"]), evidence)
|
|
927
950
|
|
|
File without changes
|
|
File without changes
|
|
@@ -132,7 +132,7 @@ def parse_regression_builder():
|
|
|
132
132
|
"--genes",
|
|
133
133
|
nargs="+",
|
|
134
134
|
default=[],
|
|
135
|
-
help="A list of RAV genes. A list must be supplied if non-RAV genes are in the mutations table (ie if clustering snp distances)",
|
|
135
|
+
help="A list of RAV genes. A list must be supplied if non-RAV genes are in the mutations table (ie if clustering snp distances, which may use many more genes)",
|
|
136
136
|
)
|
|
137
137
|
parser.add_argument(
|
|
138
138
|
"--dilution_factor", type=int, default=2, help="Dilution factor (default: 2)."
|
|
File without changes
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: catomatic
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.3
|
|
4
4
|
Summary: A tool for automatically building catalogues of antibiotic resistance-associated variants
|
|
5
5
|
Author: Dylan Adlard
|
|
6
6
|
Author-email: Philip W Fowler <philip.fowler@ndm.ox.ac.uk>
|
|
@@ -48,15 +48,15 @@ The first is used in [https://doi.org/10.1101/2025.01.30.635633](https://doi.org
|
|
|
48
48
|
|
|
49
49
|
This method relies on the logic that mutations that do not cause resistance can co-occur with those that do. If a mutation in isolation (solo) does not cause resistance, then it is not contributing to the phenotype when present in a mixture either.
|
|
50
50
|
|
|
51
|
-
Mutations that occur in isolation
|
|
51
|
+
Mutations that occur in isolation are traversed in sequence. When they meet the susceptible criteria for the selected test, they are characterized as benign (`S`) and removed from the dataset. This step repeats iteratively until no more solo benign mutations are found.
|
|
52
52
|
|
|
53
53
|
Remaining mutations are classified based on their resistance rates and statistical test results. Those that don’t meet thresholds are labeled as `U`.
|
|
54
54
|
|
|
55
55
|
The classification approach supports:
|
|
56
56
|
|
|
57
|
-
- **No test**:
|
|
58
|
-
- **Binomial test**: against a specified background resistance rate
|
|
59
|
-
- **Fisher's test**:
|
|
57
|
+
- **No test**: calls a mutation `S` during iterative unlocking when all its solo observations are susceptible; remaining solo mutations are `U`
|
|
58
|
+
- **Binomial test**: tests the solo resistance rate against a specified background resistance rate
|
|
59
|
+
- **Fisher's test**: compares the mutation solo contingency table with the no-mutation solo background
|
|
60
60
|
|
|
61
61
|
### Optional Interventions
|
|
62
62
|
|
|
@@ -68,9 +68,9 @@ Because the method uses GARC1 grammar, rules like `{rpoB@*_fs:R}` can be supplie
|
|
|
68
68
|
- Be additive (lower priority than specific entries)
|
|
69
69
|
- Replace matching mutations (requires `replace=True` and `wildcards` supplied)
|
|
70
70
|
|
|
71
|
-
The catalogue
|
|
71
|
+
The built catalogue is available as a dictionary through the `catalogue.catalogue` attribute. It can also be converted to a Piezo-compatible `pandas.DataFrame` with `build_piezo()`.
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
Evidence is stored with each catalogue entry and exported in the `EVIDENCE` field. It includes proportions, Wilson confidence intervals, solo contingency tables used for classification, full sample-level contingency tables for description, and p-values when a statistical test is used. Sample IDs can be included with `record_ids=True`.
|
|
74
74
|
|
|
75
75
|
### Example Workflow using Fisher's:
|
|
76
76
|
|
|
@@ -125,7 +125,7 @@ from catomatic.BinaryCatalogue import BinaryBuilder
|
|
|
125
125
|
catalogue = BinaryBuilder(samples=samples_df, mutations=mutations_df).build()
|
|
126
126
|
|
|
127
127
|
# View dictionary version
|
|
128
|
-
cat_dict = catalogue.
|
|
128
|
+
cat_dict = catalogue.catalogue
|
|
129
129
|
|
|
130
130
|
# Convert to Piezo-compatible format
|
|
131
131
|
catalogue_df = catalogue.build_piezo(
|
|
@@ -149,7 +149,7 @@ catalogue.to_piezo(
|
|
|
149
149
|
|
|
150
150
|
### CLI
|
|
151
151
|
|
|
152
|
-
After installation, the simplest way to run the catomatic catalogue builder is via the command line interface using the `binary` subcommand.
|
|
152
|
+
After installation, the simplest way to run the catomatic catalogue builder is via the command line interface using the `binary` subcommand. Use `--to_piezo` or `--to_json` to export the catalogue. If neither is supplied, no file is exported. Additional metadata is required when using `--to_piezo`.
|
|
153
153
|
|
|
154
154
|
#### Export to JSON
|
|
155
155
|
|
|
@@ -178,6 +178,8 @@ catomatic binary \
|
|
|
178
178
|
|
|
179
179
|
### CLI Parameters
|
|
180
180
|
|
|
181
|
+
The Binary CLI does not expose the Python `build()` options `tails` or `min_count`. The output flags are not mutually exclusive; if both are supplied, both exporters write to the same `--outfile`, so use only one.
|
|
182
|
+
|
|
181
183
|
| Parameter | Type | Description |
|
|
182
184
|
| ------------------ | ------- | ---------------------------------------------------------------------------------------------- |
|
|
183
185
|
| `--samples` | `str` | Path to the samples (phenotypes) file. Required. |
|
|
@@ -190,11 +192,16 @@ catomatic binary \
|
|
|
190
192
|
| `--version` | `str` | Catalogue version. Required with `--to_piezo`. |
|
|
191
193
|
| `--drug` | `str` | Name of the drug. Required with `--to_piezo`. |
|
|
192
194
|
| `--wildcards` | `str` | Path to JSON file containing wildcard mutation definitions. Required with `--to_piezo`. |
|
|
193
|
-
| `--test` | `str` | Type of statistical test to apply
|
|
194
|
-
| `--background` | `float` | Background
|
|
195
|
-
| `--p` | `float` |
|
|
196
|
-
| `--
|
|
197
|
-
| `--
|
|
195
|
+
| `--test` | `str` | Type of statistical test to apply: `Binomial` or `Fisher`. Optional; if omitted, no statistical test is used. |
|
|
196
|
+
| `--background` | `float` | Background resistance rate (0–1). Required if `--test Binomial` is used. |
|
|
197
|
+
| `--p` | `float` | Confidence level used for statistical testing. Optional. Defaults to `0.95` (a 0.05 significance threshold). |
|
|
198
|
+
| `--strict_unlock` | `flag` | For two-tailed tests, require statistical significance to classify homogeneous susceptible mutations as `S` during iterative unlocking. |
|
|
199
|
+
| `--frs` | `float` | Optional fraction-read-support threshold. Requires an `FRS` column in the mutations file. |
|
|
200
|
+
| `--seed` | `str[]` | Optional list of mutations to seed as susceptible before building. |
|
|
201
|
+
| `--record_ids` | `flag` | Include sample IDs in evidence records. |
|
|
202
|
+
| `--grammar` | `str` | Grammar used for Piezo export. Default: `GARC1`. |
|
|
203
|
+
| `--values` | `str` | Prediction values for Piezo export. Default: `RUS`. |
|
|
204
|
+
| `--for_piezo` | `flag` | Add placeholder rows for Piezo export. Default: `False` in the CLI. |
|
|
198
205
|
|
|
199
206
|
## Running catomatic's Regression Builder
|
|
200
207
|
|
|
@@ -310,8 +317,8 @@ catomatic regression \
|
|
|
310
317
|
|
|
311
318
|
### Notes
|
|
312
319
|
|
|
313
|
-
-
|
|
314
|
-
- For Piezo export, placeholder entries are inserted
|
|
320
|
+
- For `BinaryBuilder`, use `update_catalogue()` to apply post-hoc rules. Supply `wildcards` and set `replace=True` to replace entries matched by an aggregate rule. Specific entries can be added or replaced without `replace=True`.
|
|
321
|
+
- For Piezo export, placeholder entries are inserted when `for_piezo=True` and needed to satisfy parser requirements (R, S, and U must be represented). The Python export methods default to `for_piezo=True`; the CLI flag is off by default.
|
|
315
322
|
- The EVIDENCE column includes contingency tables, proportions, confidence intervals, and p-values, and may optionally include sample IDs if `record_ids=True`.
|
|
316
323
|
- To build a catalogue with the regression builder, as currently implemented, requires an ecoff as it will compare the predited effected against the background to supply an R/S/U label
|
|
317
324
|
- To only calculate predicted effects, this can be done in Python by calling RegressionBuilder.predict_effects()
|
|
File without changes
|
|
File without changes
|
|
@@ -111,31 +111,23 @@ def test_build_X(mixed_variants):
|
|
|
111
111
|
mutation_matrix_fe = builder.build_X(df_fe, fixed_effects=["SOURCE"])
|
|
112
112
|
|
|
113
113
|
expected_columns_fixed = [
|
|
114
|
-
"Intercept",
|
|
114
|
+
"Intercept",
|
|
115
|
+
"mut0@V1!",
|
|
115
116
|
"mut1@G12G",
|
|
116
117
|
"mut2@A13V",
|
|
117
118
|
"mut3@121_indel",
|
|
118
|
-
"SOURCE_Lab1",
|
|
119
119
|
"SOURCE_Lab2",
|
|
120
120
|
]
|
|
121
121
|
expected_values_fixed = [
|
|
122
|
-
[1, 1, 0, 0, 0,
|
|
123
|
-
[
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
1,
|
|
127
|
-
0,
|
|
128
|
-
1,
|
|
129
|
-
0,
|
|
130
|
-
1,
|
|
131
|
-
], # Sample B: Mutations "mut1@G12G" and "mut3@121_indel", Lab2
|
|
132
|
-
[1, 0, 0, 1, 0, 1, 0], # Sample C: Mutation "mut2@A13V", Lab1
|
|
133
|
-
[1, 0, 0, 0, 0, 0, 1], # Sample D: No mutations, Lab2
|
|
122
|
+
[1, 1, 0, 0, 0, 0], # Sample A: 'mut0@V1!', reference level Lab1
|
|
123
|
+
[1, 0, 1, 0, 1, 1], # Sample B: mutations and Lab2
|
|
124
|
+
[1, 0, 0, 1, 0, 0], # Sample C: mutation and reference level Lab1
|
|
125
|
+
[1, 0, 0, 0, 0, 1], # Sample D: no mutations, Lab2
|
|
134
126
|
]
|
|
135
127
|
assert mutation_matrix_fe.shape == (
|
|
136
128
|
4,
|
|
137
|
-
|
|
138
|
-
), f"Expected shape (4,
|
|
129
|
+
6,
|
|
130
|
+
), f"Expected shape (4, 6), got {mutation_matrix_fe.shape}"
|
|
139
131
|
assert (
|
|
140
132
|
list(mutation_matrix_fe.columns) == expected_columns_fixed
|
|
141
133
|
), f"Expected columns {expected_columns_fixed}, got {list(mutation_matrix_fe.columns)}"
|
|
@@ -323,24 +315,20 @@ def test_initial_params(mixed_variants):
|
|
|
323
315
|
cluster_distance = 1
|
|
324
316
|
clusters = builder.calc_clusters(cluster_distance=cluster_distance)
|
|
325
317
|
|
|
326
|
-
beta_init,
|
|
318
|
+
beta_init, log_sigma_init, log_tau_init = builder.initial_params(
|
|
319
|
+
X, y_low, y_high, clusters
|
|
320
|
+
)
|
|
327
321
|
|
|
328
322
|
assert (
|
|
329
323
|
beta_init.shape[0] == X.shape[1]
|
|
330
324
|
), f"Expected beta_init to have {X.shape[1]} elements, got {beta_init.shape[0]}"
|
|
331
325
|
assert np.all(np.isfinite(beta_init)), "All elements of beta_init should be finite."
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
assert (
|
|
335
|
-
u_init.shape[0] == num_clusters
|
|
336
|
-
), f"Expected u_init to have {num_clusters} elements, got {u_init.shape[0]}"
|
|
337
|
-
assert np.all(np.isfinite(u_init)), "All elements of u_init should be finite."
|
|
338
|
-
|
|
339
|
-
assert np.isfinite(sigma), "Sigma should be a finite value."
|
|
326
|
+
assert np.isfinite(log_sigma_init), "log_sigma_init should be finite."
|
|
327
|
+
assert np.isclose(log_tau_init, np.log(0.5)), "log_tau_init should use the 0.5 initial random-effect SD."
|
|
340
328
|
|
|
341
329
|
|
|
342
330
|
def test_predict_effects(mixed_variants):
|
|
343
|
-
"""Test predict_effects returns
|
|
331
|
+
"""Test predict_effects returns effects for the supplied mutations."""
|
|
344
332
|
samples, mutations = mixed_variants
|
|
345
333
|
|
|
346
334
|
mutations["REF"] = ["A", "G", "A", "T"]
|
|
@@ -360,8 +348,8 @@ def test_predict_effects(mixed_variants):
|
|
|
360
348
|
cluster_distance=50,
|
|
361
349
|
)
|
|
362
350
|
|
|
363
|
-
#
|
|
364
|
-
assert "mut1@G12G"
|
|
351
|
+
# The synonymous mutation is currently retained by the builder.
|
|
352
|
+
assert "mut1@G12G" in effects["Mutation"].values
|
|
365
353
|
|
|
366
354
|
# Remaining non-synonymous mutations should appear
|
|
367
355
|
expected_mutations = {"mut0@V1!", "mut2@A13V", "mut3@121_indel"}
|
|
@@ -377,7 +365,7 @@ def test_predict_effects(mixed_variants):
|
|
|
377
365
|
|
|
378
366
|
|
|
379
367
|
def test_classify_effects(mixed_variants):
|
|
380
|
-
"""Test classification
|
|
368
|
+
"""Test classification assigns valid categories to the supplied mutations."""
|
|
381
369
|
samples, mutations = mixed_variants
|
|
382
370
|
|
|
383
371
|
mutations["REF"] = ["A", "G", "A", "T"]
|
|
@@ -399,7 +387,7 @@ def test_classify_effects(mixed_variants):
|
|
|
399
387
|
|
|
400
388
|
classified_effects, ecoff = builder.classify_effects(effects, ecoff=1, p=0.95)
|
|
401
389
|
|
|
402
|
-
assert "mut1@G12G"
|
|
390
|
+
assert "mut1@G12G" in classified_effects["Mutation"].values
|
|
403
391
|
assert "Classification" in classified_effects.columns
|
|
404
392
|
assert set(classified_effects["Classification"]).issubset({"R", "S", "U"})
|
|
405
393
|
|
|
@@ -469,8 +457,8 @@ def test_build(mixed_variants):
|
|
|
469
457
|
|
|
470
458
|
catalogue = builder.catalogue
|
|
471
459
|
|
|
472
|
-
#
|
|
473
|
-
assert "mut1@G12G"
|
|
460
|
+
# The synonymous mutation is currently retained by the builder.
|
|
461
|
+
assert "mut1@G12G" in catalogue
|
|
474
462
|
|
|
475
463
|
# Non-synonymous mutations should appear
|
|
476
464
|
expected_mutations = {"mut0@V1!", "mut2@A13V", "mut3@121_indel"}
|
|
@@ -488,7 +476,7 @@ def test_build(mixed_variants):
|
|
|
488
476
|
|
|
489
477
|
|
|
490
478
|
def test_main_regression_builder(mixed_variants, tmp_path):
|
|
491
|
-
"""Test CLI produces valid catalogue
|
|
479
|
+
"""Test CLI produces a valid catalogue for the supplied mutations."""
|
|
492
480
|
samples, mutations = mixed_variants
|
|
493
481
|
|
|
494
482
|
samples_file = tmp_path / "samples.csv"
|
|
@@ -524,8 +512,8 @@ def test_main_regression_builder(mixed_variants, tmp_path):
|
|
|
524
512
|
with open(output_file, "r") as f:
|
|
525
513
|
catalogue = json.load(f)
|
|
526
514
|
|
|
527
|
-
#
|
|
528
|
-
assert "mut1@G12G"
|
|
515
|
+
# The synonymous mutation is currently retained by the builder.
|
|
516
|
+
assert "mut1@G12G" in catalogue
|
|
529
517
|
|
|
530
518
|
# Non-synonymous mutations included
|
|
531
519
|
assert "mut0@V1!" in catalogue
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|