mirror of
https://github.com/msberends/AMR.git
synced 2026-03-11 17:47:50 +01:00
mdro(): infer base drug resistance from drug+inhibitor combination co… (#263)
* mdro(): infer base drug resistance from drug+inhibitor combination columns (#209) When a base beta-lactam column (e.g., piperacillin/PIP) is absent but a corresponding drug+inhibitor combination (e.g., piperacillin/tazobactam/TZP) is present and resistant, resistance in the base drug is now correctly inferred. This is clinically sound: resistance in a combination implies the inhibitor provided no benefit, so the base drug is also resistant. Susceptibility in a combination is NOT propagated to the base drug (the inhibitor may be responsible for susceptibility), so only R values are inferred; missing base drugs remain NA otherwise. Implementation details: - Uses AB_BETALACTAMS_WITH_INHIBITOR to identify all beta-lactam+inhibitor combinations present in the user's data - Derives base drug AB codes by stripping the "/inhibitor" part from names - Creates synthetic proxy columns (.sir_proxy_<AB>) in x, set to "R" when any matching combination is R, otherwise NA - Proxy columns are added to cols_ab before drug variable assignment, so all existing guideline logic benefits without any changes - Multiple combos for the same base drug are OR-ed (any R → R) - Adds internal ab_without_inhibitor() helper for the name->base mapping - Verbose mode reports which combinations are used for inference Bumps version: 3.0.1.9028 -> 3.0.1.9029 https://claude.ai/code/session_01Cp154UtssHg84bw38xiiTG * Add sir.R/mic.R fixes and mdro() unit tests; bump to 3.0.1.9030 R/sir.R (line 571): Guard purely numeric strings (e.g. "1", "8") from the Unicode letter filter. Values matching the broad SIR regex but consisting only of digits must not be stripped; add `x %unlike% "^[0-9+]$"` predicate. R/mic.R (lines 220-222): Preserve the letter 'e' during Unicode-letter removal so that MIC values in scientific notation (e.g. "1e-3", "2.5e-2") survive the cleaning step. - Line 220: [\\p{L}] → [^e\\P{L}] (remove all letters except 'e') - Line 222: [^0-9.><= -]+ → [^0-9e.><= -]+ (allow 'e' in whitelist) tests/testthat/test-mdro.R: New tests for the drug+inhibitor inference added in the previous commit (issue #209): - TZP=R with no PIP column → PIP inferred R → MDRO class elevated - TZP=S with no PIP column → proxy col is NA (not S) → class lower - verbose mode emits "Inferring resistance" message - AMC=R with no AMX column runs without error (Enterococcus faecium) https://claude.ai/code/session_01Cp154UtssHg84bw38xiiTG * Fix version to single bump (9029) and update CLAUDE.md versioning rules CLAUDE.md: Rewrite the "Version and date bump" subsection to document that: - Exactly ONE version bump is allowed per PR (PRs are squash-merged into one commit on the default branch, so one commit = one version increment) - The correct version is computed from git history: currentversion="${currenttag}.$((commits_since_tag + 9001 + 1))" with the +1 accounting for the PR's own squash commit not yet on the default branch - Fall back to incrementing DESCRIPTION's version by 1 if git describe fails - The Date: field tracks the date of the *last* PR commit (updated each time) DESCRIPTION / NEWS.md: Correct the version from 3.0.1.9030 back to 3.0.1.9029. Two version bumps were made across two commits in this PR; since it will be squash-merged as one commit only one bump is correct. Also update Date to today (2026-03-07). https://claude.ai/code/session_01Cp154UtssHg84bw38xiiTG * Fix stats::setNames, test accessor bug, and version script verification R/mdro.R: Qualify setNames() as stats::setNames() in the drug+inhibitor inference block to satisfy R CMD CHECK's global-function checks. tests/testthat/test-mdro.R: mdro() with verbose=FALSE returns an atomic ordered factor, not a data.frame. Fix three test errors introduced in the previous commit: - Line 320: result_no_pip$MDRO -> result_no_pip (factor, no $ accessor) - Line 328: result_tzp_s$MDRO / result_no_pip$MDRO -> direct factor refs - Line 347: expect_inherits(..., "data.frame") -> c("factor","ordered") Also fix the comment on line 347 to match the actual return type. Version: confirmed at 3.0.1.9029 (no further bump; one bump already made this PR). git describe failed (no tags in dev environment) — fallback applies. The +1 in CLAUDE.md's formula is correct for tagged repos: currentcommit + 9001 + 1 = 27 + 9001 + 1 = 9029 ✓ https://claude.ai/code/session_01Cp154UtssHg84bw38xiiTG * Fix unit tests: use mrgn guideline and expect_message() for proxy tests Three failures corrected: 1. Classification tests (lines 321, 329): The EUCAST guideline for P. aeruginosa already has OR logic (PIP OR TZP), so TZP=R alone satisfies it regardless of whether the PIP proxy exists. Switch to guideline="mrgn": the MRGN 4MRGN criterion for P. aeruginosa requires PIP=R explicitly (lines 1488-1496 of mdro.R), with no TZP fallback. Without the proxy: PIP missing -> not 4MRGN -> level 1. With the proxy (TZP=R infers PIP=R): 4MRGN reached -> level 3. The TZP=S case leaves proxy=NA, so PIP is still absent effectively -> level 1, which is < level 3 as expected. 2. Verbose/message test (line 335): message_() routes through message() to stderr, not cat() to stdout. expect_output() only captures stdout so it always saw nothing. Fix: use expect_message() instead, and remove the inner suppressMessages() that was swallowing the message before expect_message() could capture it. Also trim two stale lines left over from the old expect_output block. https://claude.ai/code/session_01Cp154UtssHg84bw38xiiTG --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
54
R/mdro.R
54
R/mdro.R
@@ -480,6 +480,50 @@ mdro <- function(x = NULL,
|
||||
}
|
||||
cols_ab <- cols_ab[!duplicated(cols_ab)]
|
||||
|
||||
# Infer resistance for missing base drugs from available drug+inhibitor combination columns.
|
||||
# Clinical principle: resistance in drug+inhibitor (e.g., piperacillin/tazobactam = R)
|
||||
# always implies resistance in the base drug (e.g., piperacillin = R), because the
|
||||
# enzyme inhibitor adds nothing when the organism is truly resistant to the base drug.
|
||||
# NOTE: susceptibility in a combination does NOT imply susceptibility in the base drug
|
||||
# (the inhibitor may be responsible), so synthetic proxy columns only propagate R, not S/I.
|
||||
.combos_in_data <- AB_BETALACTAMS_WITH_INHIBITOR[AB_BETALACTAMS_WITH_INHIBITOR %in% names(cols_ab)]
|
||||
if (length(.combos_in_data) > 0) {
|
||||
.base_drugs <- suppressMessages(
|
||||
as.ab(gsub("/.*", "", ab_name(as.character(.combos_in_data), language = NULL)))
|
||||
)
|
||||
.unique_bases <- unique(.base_drugs[!is.na(.base_drugs)])
|
||||
for (.base in .unique_bases) {
|
||||
.base_code <- as.character(.base)
|
||||
if (!.base_code %in% names(cols_ab)) {
|
||||
# Base drug column absent; find all available combo columns for this base drug
|
||||
.combos <- .combos_in_data[!is.na(.base_drugs) & as.character(.base_drugs) == .base_code]
|
||||
.combo_cols <- unname(cols_ab[as.character(.combos)])
|
||||
.combo_cols <- .combo_cols[!is.na(.combo_cols)]
|
||||
if (length(.combo_cols) > 0) {
|
||||
# Vectorised: if ANY combination is R, infer base drug as R; otherwise NA
|
||||
.sir_chars <- as.data.frame(
|
||||
lapply(x[, .combo_cols, drop = FALSE], function(col) as.character(as.sir(col))),
|
||||
stringsAsFactors = FALSE
|
||||
)
|
||||
.new_col <- paste0(".sir_proxy_", .base_code)
|
||||
x[[.new_col]] <- ifelse(rowSums(.sir_chars == "R", na.rm = TRUE) > 0L, "R", NA_character_)
|
||||
cols_ab <- c(cols_ab, stats::setNames(.new_col, .base_code))
|
||||
if (isTRUE(verbose)) {
|
||||
message_(
|
||||
"Inferring resistance for ", ab_name(.base_code, language = NULL),
|
||||
" from available drug+inhibitor combination(s): ",
|
||||
paste(ab_name(as.character(.combos), language = NULL), collapse = ", "),
|
||||
" (resistance in a combination always implies resistance in the base drug)",
|
||||
add_fn = font_blue
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
cols_ab <- cols_ab[!duplicated(names(cols_ab))]
|
||||
}
|
||||
rm(list = intersect(ls(), c(".combos_in_data", ".base_drugs", ".unique_bases", ".base", ".base_code", ".combos", ".combo_cols", ".sir_chars", ".new_col")))
|
||||
|
||||
# nolint start
|
||||
AMC <- cols_ab["AMC"]
|
||||
AMK <- cols_ab["AMK"]
|
||||
@@ -674,6 +718,16 @@ mdro <- function(x = NULL,
|
||||
x
|
||||
}
|
||||
|
||||
ab_without_inhibitor <- function(ab_codes) {
|
||||
# Get the base drug AB code from a drug+inhibitor combination.
|
||||
# e.g., AMC (amoxicillin/clavulanic acid) -> AMX (amoxicillin)
|
||||
# TZP (piperacillin/tazobactam) -> PIP (piperacillin)
|
||||
# SAM (ampicillin/sulbactam) -> AMP (ampicillin)
|
||||
combo_names <- ab_name(ab_codes, language = NULL)
|
||||
base_names <- gsub("/.*", "", combo_names)
|
||||
suppressMessages(as.ab(base_names))
|
||||
}
|
||||
|
||||
# antimicrobial classes
|
||||
# nolint start
|
||||
aminoglycosides <- c(TOB, GEN)
|
||||
|
||||
Reference in New Issue
Block a user