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.
Files changed (22) hide show
  1. {catomatic-0.2.1 → catomatic-0.2.3}/LICENSE +0 -0
  2. {catomatic-0.2.1/src/catomatic.egg-info → catomatic-0.2.3}/PKG-INFO +23 -16
  3. {catomatic-0.2.1 → catomatic-0.2.3}/README.md +22 -15
  4. {catomatic-0.2.1 → catomatic-0.2.3}/pyproject.toml +1 -1
  5. {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic/BinaryCatalogue.py +0 -0
  6. {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic/PiezoTools.py +0 -0
  7. {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic/RegressionCatalogue.py +191 -168
  8. {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic/__init__.py +0 -0
  9. {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic/__main__.py +0 -0
  10. {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic/cli.py +1 -1
  11. {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic/defence_module.py +0 -0
  12. {catomatic-0.2.1 → catomatic-0.2.3/src/catomatic.egg-info}/PKG-INFO +23 -16
  13. {catomatic-0.2.1 → catomatic-0.2.3}/src/tests/__init__.py +0 -0
  14. {catomatic-0.2.1 → catomatic-0.2.3}/src/tests/test_BuildBinaryCatalogue.py +0 -0
  15. {catomatic-0.2.1 → catomatic-0.2.3}/src/tests/test_BuildRegressionCatalogue.py +23 -35
  16. {catomatic-0.2.1 → catomatic-0.2.3}/setup.cfg +0 -0
  17. {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic.egg-info/SOURCES.txt +0 -0
  18. {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic.egg-info/dependency_links.txt +0 -0
  19. {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic.egg-info/entry_points.txt +0 -0
  20. {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic.egg-info/not-zip-safe +0 -0
  21. {catomatic-0.2.1 → catomatic-0.2.3}/src/catomatic.egg-info/requires.txt +0 -0
  22. {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.1
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 across specified genes are traversed in sequence, and if their proportion of drug-susceptibility (vs. resistance) passes the specified statistical test, they are characterized as benign (susceptible) and removed from the dataset. This step repeats iteratively until no more solo benign mutations are found.
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**: assumes homogeneous susceptibility is sufficient for S
58
- - **Binomial test**: against a specified background resistance rate
59
- - **Fisher's test**: using a contingency background
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 can be returned as a dictionary or a Piezo-compatible `pandas.DataFrame`.
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
- Contingency tables, proportions, p-values, and Wilson confidence intervals are stored in the `EVIDENCE` field.
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.return_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. You must use either `--to_piezo` or `--to_json` to specify the output format. Additional metadata is required when using `--to_piezo`.
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. One of: `None`, `Binomial`, or `Fisher`. Optional. |
194
- | `--background` | `float` | Background mutation rate (0–1). Required if `--test Binomial` is used. |
195
- | `--p` | `float` | P-value threshold for statistical significance. Optional. Defaults to `0.95`. |
196
- | `--tails` | `str` | Tail type for statistical test. One of: `one`, `two`. Optional. Defaults to `two`. |
197
- | `--strict_unlock` | `flag` | If set, disables classification of susceptible (`S`) mutations unless statistically confident. |
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
- - When using post-hoc rule updates via .update(), you must provide wildcards and set replace=True if you intend to override existing entries.
314
- - For Piezo export, placeholder entries are inserted automatically if needed to satisfy parser requirements (R, S, and U must be represented).
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 across specified genes are traversed in sequence, and if their proportion of drug-susceptibility (vs. resistance) passes the specified statistical test, they are characterized as benign (susceptible) and removed from the dataset. This step repeats iteratively until no more solo benign mutations are found.
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**: assumes homogeneous susceptibility is sufficient for S
33
- - **Binomial test**: against a specified background resistance rate
34
- - **Fisher's test**: using a contingency background
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 can be returned as a dictionary or a Piezo-compatible `pandas.DataFrame`.
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
- Contingency tables, proportions, p-values, and Wilson confidence intervals are stored in the `EVIDENCE` field.
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.return_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. You must use either `--to_piezo` or `--to_json` to specify the output format. Additional metadata is required when using `--to_piezo`.
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. One of: `None`, `Binomial`, or `Fisher`. Optional. |
169
- | `--background` | `float` | Background mutation rate (0–1). Required if `--test Binomial` is used. |
170
- | `--p` | `float` | P-value threshold for statistical significance. Optional. Defaults to `0.95`. |
171
- | `--tails` | `str` | Tail type for statistical test. One of: `one`, `two`. Optional. Defaults to `two`. |
172
- | `--strict_unlock` | `flag` | If set, disables classification of susceptible (`S`) mutations unless statistically confident. |
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
- - When using post-hoc rule updates via .update(), you must provide wildcards and set replace=True if you intend to override existing entries.
289
- - For Piezo export, placeholder entries are inserted automatically if needed to satisfy parser requirements (R, S, and U must be represented).
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.1"
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:
@@ -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=False,
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 = float(mic[2:])
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 = float(mic[1:])
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 = float(mic)
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, np.ndarray, float]:
450
+ ) -> Tuple[np.ndarray, float, float]:
447
451
  """
448
- Generate initial Args for the regression model.
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, u_init, sigma_init) where sigma_init is on the log scale.
467
+ (beta_init, log_sigma_init, log_tau_init).
464
468
  """
465
- # Need to think about this a little more carefully - perhaps init params in meintreg could be improved?
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
- # Initial estimate of beta via linear regression
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
- return beta_init, u_init, sigma
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 Args are generated via least-squares on interval midpoints.
494
- The model is then fitted using L-BFGS-B optimization.
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 : pd.DataFrame
498
- Fixed-effect design matrix including intercept.
499
- y_low : np.ndarray
500
- Lower interval bounds (log scale).
501
- y_high : np.ndarray
502
- Upper interval bounds (log scale).
503
- random_effects : sequence[int], optional
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
- _b, _u, _s = self.initial_params(X, y_low, y_high, random_effects)
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, _u, [_s]])
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="L-BFGS-B",
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
- 1. Defines MIC intervals.
549
- 2. Builds the design matrix (including intercept and optional fixed effects).
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, u_bounds, s_bounds : tuple
559
- Bounds for fixed effects, random effects, and log(σ).
560
- options : dict, optional
561
- Optimizer settings.
562
- L2_penalties : dict, optional
563
- Ridge regularization Args.
564
- fixed_effects : list[str], optional
565
- Additional fixed-effect columns to include.
566
- random_effects : bool, default=True
567
- hether to include lineage random intercepts.
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) : tuple
573
- model : Fitted MeIntReg object.
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
- #don't fit synonymous mutations (theyre used for clustering)
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].*[A-Z]$') & (aa.str[0] == aa.str[-1])
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
- self.clusters = self.calc_clusters(cluster_distance)
613
- u_bounds_ = [u_bounds] * len(np.unique(self.clusters))
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
- bounds_ = b_bounds_ + u_bounds_ + [s_bounds]
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 (model.result)
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
- Absolute predicted MIC is computed as:
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
- Standard errors are derived from the inverse Hessian and include
648
- covariance between the intercept and mutation coefficient:
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
- Uncertainty is propagated to MIC scale via the delta method.
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
- effect_size (log2 shift)
667
- effect_std (log2 scale, if available)
668
- fold_change (relative to baseline)
669
- MIC (absolute predicted MIC)
670
- MIC_std (delta-method SE, if available)
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
- # Identify intercept
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
- # Identify fixed-effect dummy columns (to exclude from mutation list)
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
- col for col in X.columns if col not in columns_to_exclude
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, # log2 shift
764
+ "effect_size": mutation_effects,
707
765
  }
708
766
  )
709
767
 
710
- # Baseline MIC
711
- baseline_MIC = self.dilution_factor ** beta0
712
- # Fold change from mutation
713
- effects["fold_change"] = (
714
- self.dilution_factor ** effects["effect_size"]
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
- beta0_idx = intercept_idx
726
-
727
- theta_stds = []
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
- effects = effects[
760
- [
761
- "Mutation",
762
- "effect_size",
763
- "effect_std",
764
- "fold_change",
765
- "MIC",
766
- "MIC_std",
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 effect_std.
821
+ a two-tailed z-test using predicted_logMIC_se.
804
822
 
805
823
  Args:
806
- effects: Effects DataFrame with 'effect_size' and 'effect_std'.
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(row["effect_size"], breakpoint, row["effect_std"]),
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["effect_size"] > breakpoint) & (effects["p_value"] < (1 - p)),
825
- (effects["effect_size"] < breakpoint) & (effects["p_value"] < (1 - p)),
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: dict[str, Any] = {
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
 
@@ -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)."
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: catomatic
3
- Version: 0.2.1
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 across specified genes are traversed in sequence, and if their proportion of drug-susceptibility (vs. resistance) passes the specified statistical test, they are characterized as benign (susceptible) and removed from the dataset. This step repeats iteratively until no more solo benign mutations are found.
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**: assumes homogeneous susceptibility is sufficient for S
58
- - **Binomial test**: against a specified background resistance rate
59
- - **Fisher's test**: using a contingency background
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 can be returned as a dictionary or a Piezo-compatible `pandas.DataFrame`.
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
- Contingency tables, proportions, p-values, and Wilson confidence intervals are stored in the `EVIDENCE` field.
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.return_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. You must use either `--to_piezo` or `--to_json` to specify the output format. Additional metadata is required when using `--to_piezo`.
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. One of: `None`, `Binomial`, or `Fisher`. Optional. |
194
- | `--background` | `float` | Background mutation rate (0–1). Required if `--test Binomial` is used. |
195
- | `--p` | `float` | P-value threshold for statistical significance. Optional. Defaults to `0.95`. |
196
- | `--tails` | `str` | Tail type for statistical test. One of: `one`, `two`. Optional. Defaults to `two`. |
197
- | `--strict_unlock` | `flag` | If set, disables classification of susceptible (`S`) mutations unless statistically confident. |
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
- - When using post-hoc rule updates via .update(), you must provide wildcards and set replace=True if you intend to override existing entries.
314
- - For Piezo export, placeholder entries are inserted automatically if needed to satisfy parser requirements (R, S, and U must be represented).
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()
@@ -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", "mut0@V1!",
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, 1, 0], # Sample A: 'mut0@V1!', Lab1
123
- [
124
- 1,
125
- 0,
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
- 7,
138
- ), f"Expected shape (4, 7), got {mutation_matrix_fe.shape}"
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, u_init, sigma = builder.initial_params(X, y_low, y_high, clusters)
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
- num_clusters = len(np.unique(clusters))
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 valid mutation-level effects (excluding synonymous)."""
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
- # Synonymous mutation mut1@G12G should be excluded
364
- assert "mut1@G12G" not in effects["Mutation"].values
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 produces valid categories and excludes synonymous mutations."""
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" not in classified_effects["Mutation"].values
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
- # Synonymous mutation should not appear
473
- assert "mut1@G12G" not in catalogue
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 and excludes synonymous mutations."""
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
- # Synonymous mutation excluded
528
- assert "mut1@G12G" not in catalogue
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