From 3b8b2136162c50d38399a2d13c0cf08f784a721b Mon Sep 17 00:00:00 2001 From: Matthijs Berends Date: Tue, 3 Nov 2026 23:51:37 +0100 Subject: [PATCH] Return the serovar in mo_shortname() for Salmonella, keep Salmonella groups apart from streptococci, abbreviate all Lancefield groups, and apply the Typhi rule in mdro() (#341) --- DESCRIPTION | 4 ++-- NEWS.md | 7 ++++++- R/mdro.R | 6 +++--- R/mo_property.R | 10 ++++++++-- man/mo_property.Rd | 2 +- tests/testthat/test-mdro.R | 21 +++++++++++++++++++++ tests/testthat/test-mo_property.R | 19 +++++++++++++++++++ 7 files changed, 60 insertions(+), 9 deletions(-) diff --git a/DESCRIPTION b/DESCRIPTION index 5efd865cc..6ee63de10 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: AMR -Version: 3.1.0.9014 -Date: 2026-10-30 +Version: 3.1.0.9015 +Date: 2026-11-04 Title: Antimicrobial Resistance Data Analysis Description: Functions to simplify and standardise antimicrobial resistance (AMR) data analysis and to work with microbial and antimicrobial properties by diff --git a/NEWS.md b/NEWS.md index c2c9c35d3..e9ebf0bbd 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,7 +1,12 @@ -# AMR 3.1.0.9014 +# AMR 3.1.0.9015 This version fixes a critical bug in WISCA that biased all coverage estimates of `wisca()` and `antibiogram(..., wisca = TRUE)` downwards since AMR 3.0.0. Users are strongly advised to re-run their WISCA analyses. +### Breaking +* `mo_shortname()` now returns the serovar for *Salmonella* serovars, e.g., `"S. Typhi"` and `"S. Goldcoast"` instead of `"S. enterica"`. As this is the default for `bug_drug_combinations()` and short names in `antibiogram()`, serovars are no longer merged in their results. Use `FUN = function(x) paste(mo_genus(x), mo_species(x))` in `bug_drug_combinations()` to group them by species (#341) +* `mo_shortname()` now returns e.g. `"S. Group B"` for *Salmonella* groups instead of the abbreviations of streptococci, such as `"GBS"`, so that they are no longer merged with streptococci in `bug_drug_combinations()` and `antibiogram()`. *Streptococcus* groups E, I, J and L are now also abbreviated, e.g., `"GES"`, and `"B_STRPT_ABCG"` now returns `"S. Group A, B, C, G"` instead of the malformed `"GA, B, C, GS"` (#341) +* `mdro()` now applies the rule for *Salmonella* Typhi in the EUCAST guidelines, i.e., resistance to carbapenems (and fluoroquinolones in `"EUCAST3.1"`). This rule never applied before, so these isolates were wrongly classified as negative (#341) + ### Changed * WISCA (`wisca()` and `antibiogram(..., wisca = TRUE)`) * Fixed a critical bug that systematically underestimated the coverage of every WISCA since AMR 3.0.0. Pathogens absent from a syndromic group, and pathogens without any test result for a regimen, were still included in the model with only prior information: each added a prior weight of 1 to the pathogen incidence, and untested pathogens kept their full incidence weight while counting as about 50% susceptible. The bias was largest for stratified analyses (e.g., up to 14 percentage points for meropenem in random strata of `example_isolates`). Now, only pathogens that occur in a group and have at least one test result for the regimen are included. Please re-run all WISCA analyses (#337) diff --git a/R/mdro.R b/R/mdro.R index 4f389b6e3..e012867c4 100755 --- a/R/mdro.R +++ b/R/mdro.R @@ -1156,7 +1156,7 @@ mdro <- function(x = NULL, ) trans_tbl( 3, - which(x$genus == "Salmonella" & x$species == "Typhi"), + which(x$genus == "Salmonella" & x$subspecies == "Typhi"), c(carbapenems, fluoroquinolones), "any" ) @@ -1251,7 +1251,7 @@ mdro <- function(x = NULL, ) trans_tbl( 3, - which(x$genus == "Salmonella" & x$species == "Typhi"), + which(x$genus == "Salmonella" & x$subspecies == "Typhi"), c(carbapenems), "any" ) @@ -1353,7 +1353,7 @@ mdro <- function(x = NULL, ) trans_tbl( 3, - which(x$genus == "Salmonella" & x$species == "Typhi"), + which(x$genus == "Salmonella" & x$subspecies == "Typhi"), c(carbapenems), "any" ) diff --git a/R/mo_property.R b/R/mo_property.R index e51afa06e..3885d776d 100755 --- a/R/mo_property.R +++ b/R/mo_property.R @@ -44,7 +44,7 @@ #' #' [mo_ref()] returns the abbreviated authority of the nomenclatural act that created the queried name combination. When `keep_synonyms = FALSE` (default), this is the authority of the currently accepted name. When `keep_synonyms = TRUE`, this is the authority under which the queried (possibly outdated) name was published. Emendations (changes to the species description without a name change) are not reflected; only the combination or original description authority is returned. #' -#' The short name ([mo_shortname()]) returns the first character of the genus and the full species, such as `"E. coli"`, for species and subspecies. Exceptions are abbreviations of staphylococci (such as *"CoNS"*, Coagulase-Negative Staphylococci) and beta-haemolytic streptococci (such as *"GBS"*, Group B Streptococci). Please bear in mind that e.g. *E. coli* could mean *Escherichia coli* (domain of Bacteria) as well as *Entamoeba coli* (domain of Protozoa). Returning to the full name will be done using [as.mo()] internally, giving priority to bacteria and human pathogens, i.e. `"E. coli"` will always be considered *Escherichia coli*. As a result, `mo_fullname(mo_shortname("Entamoeba coli"))` returns `"Escherichia coli"`. +#' The short name ([mo_shortname()]) returns the first character of the genus and the full species, such as `"E. coli"`, for species and subspecies. Exceptions are *Salmonella* serovars, which return the serovar (such as `"S. Typhi"`), and abbreviations of staphylococci (such as *"CoNS"*, Coagulase-Negative Staphylococci) and beta-haemolytic streptococci (such as *"GBS"*, Group B Streptococci). Please bear in mind that e.g. *E. coli* could mean *Escherichia coli* (domain of Bacteria) as well as *Entamoeba coli* (domain of Protozoa). Returning to the full name will be done using [as.mo()] internally, giving priority to bacteria and human pathogens, i.e. `"E. coli"` will always be considered *Escherichia coli*. As a result, `mo_fullname(mo_shortname("Entamoeba coli"))` returns `"Escherichia coli"`. #' #' Following the formal introduction of the new kingdom rank into prokaryotic nomenclature in 2024 (\doi{10.1099/ijsem.0.006242}), [mo_kingdom()] and [mo_domain()] return different results for bacteria and archaea: [mo_kingdom()] returns the new formal kingdom (e.g. "Pseudomonadati", "Bacillati"), while [mo_domain()] returns the new domain (e.g. "Bacteria", "Archaea"). For non-prokaryotic organisms, both functions return identical results. #' @@ -262,13 +262,19 @@ mo_shortname <- function(x, language = get_AMR_locale(), keep_synonyms = getOpti genera <- mo_genus(x.mo, language = NULL, keep_synonyms = keep_synonyms, ...) shortnames <- paste0(substr(genera, 1, 1), ". ", replace_empty(mo_species(x.mo, language = NULL, keep_synonyms = keep_synonyms, ...))) + # exceptions for Salmonella serovars, that are stored as subspecies of S. enterica: return e.g. "S. Typhi" + subspecies <- mo_subspecies(x.mo, language = NULL, keep_synonyms = keep_synonyms, ...) + serovars <- which(genera == "Salmonella" & subspecies %like_case% "^[A-Z]") + shortnames[serovars] <- paste0("S. ", subspecies[serovars]) + # exceptions for where no species is known shortnames[shortnames %like% ".[.] spp[.]"] <- genera[shortnames %like% ".[.] spp[.]"] # exceptions for staphylococci shortnames[shortnames == "S. coagulase-negative"] <- "CoNS" shortnames[shortnames == "S. coagulase-positive"] <- "CoPS" # exceptions for streptococci: Group A Streptococcus -> GAS - shortnames[shortnames %like_case% "S. Group [ABCDFGHK]"] <- paste0("G", gsub("S. Group ([ABCDFGHK])", "\\1", shortnames[shortnames %like_case% "S. Group [ABCDFGHK]"], perl = TRUE), "S") + streptococci_groups <- which(genera == "Streptococcus" & shortnames %like_case% "^S[.] Group [A-L]$") + shortnames[streptococci_groups] <- paste0("G", gsub("S. Group ([A-L])", "\\1", shortnames[streptococci_groups], perl = TRUE), "S") # unknown species etc. shortnames[shortnames %like% "unknown"] <- paste0("(", trimws2(gsub("[^a-zA-Z -]", "", shortnames[shortnames %like% "unknown"], perl = TRUE)), ")") diff --git a/man/mo_property.Rd b/man/mo_property.Rd index 6bf90268f..5be482075 100644 --- a/man/mo_property.Rd +++ b/man/mo_property.Rd @@ -349,7 +349,7 @@ All functions will, at default, \strong{not} keep old taxonomic properties, as s \code{\link[=mo_ref]{mo_ref()}} returns the abbreviated authority of the nomenclatural act that created the queried name combination. When \code{keep_synonyms = FALSE} (default), this is the authority of the currently accepted name. When \code{keep_synonyms = TRUE}, this is the authority under which the queried (possibly outdated) name was published. Emendations (changes to the species description without a name change) are not reflected; only the combination or original description authority is returned. -The short name (\code{\link[=mo_shortname]{mo_shortname()}}) returns the first character of the genus and the full species, such as \code{"E. coli"}, for species and subspecies. Exceptions are abbreviations of staphylococci (such as \emph{"CoNS"}, Coagulase-Negative Staphylococci) and beta-haemolytic streptococci (such as \emph{"GBS"}, Group B Streptococci). Please bear in mind that e.g. \emph{E. coli} could mean \emph{Escherichia coli} (domain of Bacteria) as well as \emph{Entamoeba coli} (domain of Protozoa). Returning to the full name will be done using \code{\link[=as.mo]{as.mo()}} internally, giving priority to bacteria and human pathogens, i.e. \code{"E. coli"} will always be considered \emph{Escherichia coli}. As a result, \code{mo_fullname(mo_shortname("Entamoeba coli"))} returns \code{"Escherichia coli"}. +The short name (\code{\link[=mo_shortname]{mo_shortname()}}) returns the first character of the genus and the full species, such as \code{"E. coli"}, for species and subspecies. Exceptions are \emph{Salmonella} serovars, which return the serovar (such as \code{"S. Typhi"}), and abbreviations of staphylococci (such as \emph{"CoNS"}, Coagulase-Negative Staphylococci) and beta-haemolytic streptococci (such as \emph{"GBS"}, Group B Streptococci). Please bear in mind that e.g. \emph{E. coli} could mean \emph{Escherichia coli} (domain of Bacteria) as well as \emph{Entamoeba coli} (domain of Protozoa). Returning to the full name will be done using \code{\link[=as.mo]{as.mo()}} internally, giving priority to bacteria and human pathogens, i.e. \code{"E. coli"} will always be considered \emph{Escherichia coli}. As a result, \code{mo_fullname(mo_shortname("Entamoeba coli"))} returns \code{"Escherichia coli"}. Following the formal introduction of the new kingdom rank into prokaryotic nomenclature in 2024 (\doi{10.1099/ijsem.0.006242}), \code{\link[=mo_kingdom]{mo_kingdom()}} and \code{\link[=mo_domain]{mo_domain()}} return different results for bacteria and archaea: \code{\link[=mo_kingdom]{mo_kingdom()}} returns the new formal kingdom (e.g. "Pseudomonadati", "Bacillati"), while \code{\link[=mo_domain]{mo_domain()}} returns the new domain (e.g. "Bacteria", "Archaea"). For non-prokaryotic organisms, both functions return identical results. diff --git a/tests/testthat/test-mdro.R b/tests/testthat/test-mdro.R index 7e003c695..3162ce0df 100755 --- a/tests/testthat/test-mdro.R +++ b/tests/testthat/test-mdro.R @@ -56,6 +56,27 @@ test_that("test-mdro.R", { mdro(example_isolates, guideline = "BRMO", info = FALSE) ) + # test EUCAST rule for Salmonella Typhi, which is stored as subspecies of S. enterica + typhi <- data.frame( + mo = as.mo(rep("Salmonella Typhi", 2)), + CIP = as.sir(c("R", "S")), + IPM = as.sir(c("S", "S")), + MEM = as.sir(c("S", "S")), + AMX = as.sir(c("S", "S")) + ) + expect_equal( + as.character(suppressWarnings(mdro(typhi, guideline = "EUCAST3.1", col_mo = "mo", info = FALSE))), + c("Positive", "Negative") + ) + typhi$CIP <- as.sir(c("S", "S")) + typhi$MEM <- as.sir(c("R", "S")) + for (gl in c("EUCAST3.2", "EUCAST3.3")) { + expect_equal( + as.character(suppressWarnings(mdro(typhi, guideline = gl, col_mo = "mo", info = FALSE))), + c("Positive", "Negative") + ) + } + # test Dutch P. aeruginosa MDRO expect_equal( as.character(mdro( diff --git a/tests/testthat/test-mo_property.R b/tests/testthat/test-mo_property.R index 0cc323668..b8e48bee6 100644 --- a/tests/testthat/test-mo_property.R +++ b/tests/testthat/test-mo_property.R @@ -45,6 +45,25 @@ test_that("test-mo_property.R", { expect_equal(mo_shortname("Staphylococcus aureus", Becker = "all", language = "en"), "CoPS") expect_equal(mo_shortname("Streptococcus agalactiae"), "S. agalactiae") expect_equal(mo_shortname("Streptococcus agalactiae", Lancefield = TRUE), "GBS") + # Salmonella serovars return the serovar, and all stay distinct + expect_equal( + mo_shortname(c("Salmonella Typhi", "Salmonella Goldcoast", "Salmonella Paratyphi A", "Salmonella enterica", "Salmonella enterica enterica", NA), language = NULL), + c("S. Typhi", "S. Goldcoast", "S. Paratyphi A", "S. enterica", "S. enterica", NA) + ) + # Salmonella groups must not be abbreviated as streptococci + expect_equal( + mo_shortname(c("Salmonella group B", "Streptococcus group B"), language = NULL), + c("S. Group B", "GBS") + ) + # all Lancefield groups are abbreviated, also when they share a letter with a Salmonella group + expect_equal( + mo_shortname(paste0("B_STRPT_GRP", LETTERS[1:12]), language = NULL), + paste0("G", LETTERS[1:12], "S") + ) + expect_equal(mo_shortname("B_SLMNL_GRPE", language = NULL), "S. Group E") + expect_equal(mo_shortname("B_STRPT_ABCG", language = NULL), "S. Group A, B, C, G") + serovars <- microorganisms$mo[microorganisms$genus == "Salmonella" & microorganisms$subspecies %like_case% "^[A-Z]" & microorganisms$status == "accepted"] + expect_false(anyDuplicated(mo_shortname(serovars, language = NULL)) > 0) # check gram stain determination, to prevent we lag after a taxonomic renaming current_grampos_phyla <- c(