(v3.1.0.9010) Fix the systematic underestimation of WISCA coverage by only including pathogens that occur in a group and were tested for the regimen, and warn when the prior weight of rare pathogens is large (fixes #337)

This commit is contained in:
dr. M.S. (Matthijs) Berends committed 2026-10-30 12:35:12 +01:00
1 parent 92bb8931c5
commit fdfdadae14
6 files changed
+93 -13

No files matched your search

+1 -1
View File
@@ -1,5 +1,5 @@
Package: AMR
Version: 3.1.0.9009
Version: 3.1.0.9010
Date: 2026-10-30
Title: Antimicrobial Resistance Data Analysis
Description: Functions to simplify and standardise antimicrobial resistance (AMR)
+7 -1
View File
@@ -1,6 +1,12 @@
# AMR 3.1.0.9009
# AMR 3.1.0.9010
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.
### 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)
* `retrieve_wisca_parameters()` now returns only the parameters actually used in the model, and `n_total` in the `long_numeric` attribute now counts only isolates of the pathogens included for that regimen
* A new warning is given when the prior weight of 1 per pathogen (Bielicki *et al.*, 2016) accounts for more than 5% of the pathogen incidence, as many rare pathogens can then strongly lower the estimated coverage; consider `top_n_microorganisms()` in that case
* Microorganism coercion (`as.mo()`)
* Fixed the misaligned arrows in the message on taxonomically renamed microorganisms in colour-supporting consoles, for old names whose author reference lacks "*et al.*" (#336)
+24 -2
View File
@@ -156,6 +156,10 @@
#'
#' **Prior distributions:** Pathogen incidence uses a non-informative \eqn{Dirichlet(1, 1, \ldots, 1)} prior. Susceptibility proportions use the Jeffreys prior, \eqn{\beta(0.5, 0.5)}, except for pathogen-drug combinations with known intrinsic resistance, which use a strongly informative \eqn{\beta(1, 9999)} prior that forces near-zero susceptibility regardless of observed data. Intrinsic resistance is determined using the [intrinsic_resistant] data set, which is based on `r format_eucast_version_nr(names(EUCAST_VERSION_EXPECTED_PHENOTYPES[1]))`.
#'
#' **Pathogens included in the model:** For each regimen (and group), only pathogens that occur in the data and have at least one test result for the regimen are included. Pathogens without any test result for a regimen do not contribute to its coverage, nor to its `n_total` in `long_numeric`. Use [retrieve_wisca_parameters()] to get the parameters that were used.
#'
#' **Rare pathogens:** Since every pathogen receives a prior weight of 1 in the Dirichlet distribution, pathogens found only once or a few times are given relatively much weight, which pulls the incidence towards a uniform distribution. Their very uncertain susceptibility can then strongly lower the estimated coverage, without reflecting the clinical expectation. A warning is therefore given when this prior weight accounts for more than 5% of the pathogen incidence in any regimen (or group). Consider limiting the analysis to the most common pathogens using [top_n_microorganisms()], or grouping rare pathogens before running WISCA.
#'
#' **Interpreting the output:** Overlapping credible intervals between regimens indicate no significant difference in coverage; if a narrower-spectrum regimen overlaps with a broader one, the narrower-spectrum option may be preferred on stewardship grounds. Non-overlapping intervals indicate a clinically meaningful difference. For small sample sizes, consider pooling data from multiple sites to improve precision, provided pathogen distributions are sufficiently similar (Bielicki *et al.*, 2016).
#'
#' For the full mathematical derivation and worked examples, see the [WISCA vignette](https://amr-for-r.org/articles/WISCA.html).
@@ -744,7 +748,8 @@ antibiogram.default <- function(x,
coverage = NA_real_,
lower_ci = NA_real_,
upper_ci = NA_real_,
n_total = sum(n_total, na.rm = TRUE),
# only pathogens with test results for the regimen enter the WISCA model, see below
n_total = sum(n_total[which(n_tested > 0)]),
n_tested = sum(n_tested, na.rm = TRUE),
n_susceptible = sum(n_susceptible, na.rm = TRUE)
)
@@ -761,7 +766,24 @@ antibiogram.default <- function(x,
out_wisca$group <- out_wisca$ab
}
wisca_parameters <- out
# only pathogens that occur in the group (n_total > 0) and have at least one test result for the regimen (n_tested > 0) may contribute to WISCA; otherwise, absent or untested pathogens would enter with prior-only parameters and bias the coverage downwards (#337)
wisca_parameters <- out[which(out$n_total > 0 & out$n_tested > 0), , drop = FALSE]
# the Dirichlet prior of 1 per pathogen (Bielicki et al., 2016) weighs heavily when many pathogens are rare
prior_share <- vapply(
split(wisca_parameters$n_total, as.character(wisca_parameters$group)),
function(n) length(n) / (length(n) + sum(n)),
FUN.VALUE = numeric(1)
)
if (any(prior_share > 0.05)) {
warning_(
"In ", sum(prior_share > 0.05), " of ", length(prior_share), " WISCA regimen(s) or group(s), the prior weight of 1 per pathogen accounts for more than 5% of the pathogen incidence (up to ", round(max(prior_share) * 100, 1), "%), since many pathogens occur only rarely. ",
"Because WISCA gives every pathogen a prior weight of 1 (Bielicki et al., 2016), such rare pathogens can strongly lower the estimated coverage without reflecting the clinical expectation. ",
"Consider limiting the analysis to the most common pathogens with {.help [{.fun top_n_microorganisms}](AMR::top_n_microorganisms)}, or grouping rare pathogens before running WISCA.",
call = FALSE
)
}
wisca_draws <- list()
wisca_components <- list()
+15 -8
View File
@@ -59,8 +59,8 @@ times](https://scholar.google.com/citations?view_op=view_citation&hl=en&citation
in scientific research.
After installing this package, R knows [**~103 000 distinct microbial
species**](./reference/microorganisms.html) (updated April 2026) and all
[**~630 antimicrobial and antiviral
species**](./reference/microorganisms.html) (updated October 2026) and
all [**~630 antimicrobial and antiviral
drugs**](./reference/antimicrobials.html) by name and code (including
ATC, EARS-Net, ASIARS-Net, PubChem, LOINC and SNOMED CT), and knows all
about valid SIR and MIC values. The integral clinical breakpoint
@@ -224,7 +224,7 @@ wisca(example_isolates,
| Piperacillin/tazobactam | Piperacillin/tazobactam + Gentamicin | Piperacillin/tazobactam + Tobramycin |
|:------------------------|:-------------------------------------|:-------------------------------------|
| 70.2% (65-75.4%) | 93.6% (92-95.1%) | 89.9% (87-92.5%) |
| 72.5% (67.7-77.1%) | 96% (95.1-96.9%) | 92.1% (89.3-94.4%) |
WISCA supports stratification by any clinical variable, so you can
generate syndrome-specific or ward-specific coverage estimates:
@@ -234,13 +234,19 @@ wisca(example_isolates,
antimicrobials = c("TZP", "TZP+TOB", "TZP+GEN"),
syndromic_group = "ward",
minimum = 10) # Recommended threshold: >=30
#> Warning: In 6 of 9 WISCA regimen(s) or group(s), the prior weight of 1 per pathogen accounts for more than
#> 5% of the pathogen incidence (up to 13.3%), since many pathogens occur only rarely. Because WISCA
#> gives every pathogen a prior weight of 1 (Bielicki et al., 2016), such rare pathogens can strongly
#> lower the estimated coverage without reflecting the clinical expectation. Consider limiting the
#> analysis to the most common pathogens with `top_n_microorganisms()`, or grouping rare pathogens
#> before running WISCA.
```
| Syndromic Group | Piperacillin/tazobactam | Piperacillin/tazobactam + Gentamicin | Piperacillin/tazobactam + Tobramycin |
|:----------------|:------------------------|:-------------------------------------|:-------------------------------------|
| Clinical | 74.8% (69-80.3%) | 93.7% (92.1-95.1%) | 90.4% (86.8-93.3%) |
| ICU | 56.7% (48.2-65.4%) | 86.6% (83.3-89.6%) | 83.1% (78.2-87.5%) |
| Outpatient | 57.6% (46-69%) | 76.4% (69.8-81.7%) | 68% (57.7-77.5%) |
| Clinical | 78% (72.7-83.4%) | 96.3% (95.1-97.3%) | 93% (89.4-95.7%) |
| ICU | 59.7% (53.5-67.9%) | 92.2% (90-94.3%) | 88.2% (83.4-92.2%) |
| Outpatient | 67.9% (51.9-84.7%) | 92.8% (88.2-96.6%) | 79.8% (64.9-92.9%) |
**For AMR surveillance**, traditional antibiograms remain the right tool
for tracking resistance per species over time:
@@ -427,7 +433,7 @@ data analysis. This package can be used for:
organisms (MDRO) ([tutorial](./reference/mdro.html))
- Calculating (empirical) susceptibility of both mono therapy and
combination therapies ([tutorial](./articles/AMR.html))
- Apply AMR functions in predictive modelling
- Applying AMR functions in predictive modelling
([tutorial](./articles/AMR_with_tidymodels.html))
- Getting properties for any microorganism (like Gram stain, species,
genus or family) ([manual](./reference/mo_property.html))
@@ -435,7 +441,8 @@ data analysis. This package can be used for:
EARS-Net/ATC/LOINC/PubChem, defined daily dose or trade name)
([manual](./reference/ab_property.html))
- Plotting antimicrobial resistance ([tutorial](./articles/AMR.html))
- Applying EUCAST expert rules ([manual](./reference/eucast_rules.html))
- Applying EUCAST and CLSI interpretive rules
([manual](./reference/interpretive_rules.html))
- Getting SNOMED codes of a microorganism, or getting properties of a
microorganism based on a SNOMED code
([manual](./reference/mo_property.html))
+4
View File
@@ -268,6 +268,10 @@ It does so by combining two quantities: the relative incidence of each pathogen
\strong{Prior distributions:} Pathogen incidence uses a non-informative \eqn{Dirichlet(1, 1, \ldots, 1)} prior. Susceptibility proportions use the Jeffreys prior, \eqn{\beta(0.5, 0.5)}, except for pathogen-drug combinations with known intrinsic resistance, which use a strongly informative \eqn{\beta(1, 9999)} prior that forces near-zero susceptibility regardless of observed data. Intrinsic resistance is determined using the \link{intrinsic_resistant} data set, which is based on \href{https://www.eucast.org/bacteria/important-additional-information/expert-rules/}{'EUCAST Expected Resistant Phenotypes' v1.2} (2023).
\strong{Pathogens included in the model:} For each regimen (and group), only pathogens that occur in the data and have at least one test result for the regimen are included. Pathogens without any test result for a regimen do not contribute to its coverage, nor to its \code{n_total} in \code{long_numeric}. Use \code{\link[=retrieve_wisca_parameters]{retrieve_wisca_parameters()}} to get the parameters that were used.
\strong{Rare pathogens:} Since every pathogen receives a prior weight of 1 in the Dirichlet distribution, pathogens found only once or a few times are given relatively much weight, which pulls the incidence towards a uniform distribution. Their very uncertain susceptibility can then strongly lower the estimated coverage, without reflecting the clinical expectation. A warning is therefore given when this prior weight accounts for more than 5\% of the pathogen incidence in any regimen (or group). Consider limiting the analysis to the most common pathogens using \code{\link[=top_n_microorganisms]{top_n_microorganisms()}}, or grouping rare pathogens before running WISCA.
\strong{Interpreting the output:} Overlapping credible intervals between regimens indicate no significant difference in coverage; if a narrower-spectrum regimen overlaps with a broader one, the narrower-spectrum option may be preferred on stewardship grounds. Non-overlapping intervals indicate a clinically meaningful difference. For small sample sizes, consider pooling data from multiple sites to improve precision, provided pathogen distributions are sufficiently similar (Bielicki \emph{et al.}, 2016).
For the full mathematical derivation and worked examples, see the \href{https://amr-for-r.org/articles/WISCA.html}{WISCA vignette}.
+42 -1
View File
@@ -47,6 +47,35 @@ test_that("test-antibiogram.R", {
wisca_known <- suppressMessages(wisca(example_isolates[example_isolates$mo != "UNKNOWN", ], antimicrobials = "GEN", simulations = 100, info = FALSE, guideline = "EUCAST"))
expect_true("UNKNOWN" %in% attributes(wisca_all)$wisca_parameters$mo)
expect_false(identical(as.data.frame(wisca_all), as.data.frame(wisca_known)))
# WISCA only includes pathogens that occur in a group and were tested for the regimen (#337)
set.seed(337)
wisca_data <- example_isolates[mo_domain(example_isolates$mo) == "Bacteria", ]
wisca_data$grp <- sample(c("A", "B", "C"), nrow(wisca_data), replace = TRUE)
wisca_strat <- suppressWarnings(wisca(wisca_data, antimicrobials = "MEM", syndromic_group = "grp", simulations = 5000, info = FALSE))
wisca_params <- retrieve_wisca_parameters(wisca_strat)
# no absent or untested pathogens
expect_true(all(wisca_params$n_total > 0 & wisca_params$n_tested > 0))
# each stratum equals the unstratified WISCA of its subset
for (g in c("A", "B", "C")) {
wisca_subset <- suppressWarnings(wisca(wisca_data[wisca_data$grp == g, ], antimicrobials = "MEM", simulations = 5000, info = FALSE))
cov_strat <- attributes(wisca_strat)$long_numeric$coverage[attributes(wisca_strat)$long_numeric$syndromic_group == g]
expect_true(abs(cov_strat - attributes(wisca_subset)$long_numeric$coverage) < 0.01)
expect_identical(attributes(wisca_strat)$long_numeric$n_total[attributes(wisca_strat)$long_numeric$syndromic_group == g], attributes(wisca_subset)$long_numeric$n_total)
}
# untested pathogens have no influence
wisca_untested <- wisca_data[1:20, ]
wisca_untested$mo <- as.mo("Brucella melitensis")
wisca_untested$MEM <- NA_sir_
set.seed(337)
wisca_without <- suppressWarnings(wisca(wisca_data, antimicrobials = "MEM", simulations = 1000, info = FALSE))
set.seed(337)
wisca_with <- suppressWarnings(wisca(rbind(wisca_data, wisca_untested), antimicrobials = "MEM", simulations = 1000, info = FALSE))
expect_identical(attributes(wisca_with)$long_numeric, attributes(wisca_without)$long_numeric)
expect_false("B_BRCLL_MLTN" %in% retrieve_wisca_parameters(wisca_with)$mo)
# warning for a large prior weight of rare pathogens
expect_warning(wisca(wisca_data, antimicrobials = "MEM", syndromic_group = "grp", simulations = 100, info = FALSE), "prior weight")
expect_warning(wisca(example_isolates, antimicrobials = "MEM", simulations = 100, info = FALSE), NA)
if (AMR:::pkg_is_available("dplyr", min_version = "1.0.0", also_load = TRUE)) {
ab_grouped <- tryCatch(
# pathogen names are translated, so test in English, which must also hold on systems in another language
@@ -230,7 +259,19 @@ test_that("test-antibiogram.R", {
expect_equal(nrow(ab_grp_seq), nrow(ab_grp_par))
}
# 6. parallel = TRUE without a plan raises an informative error
# 6. parallel and sequential use the same WISCA parameters and give the same coverage (#337)
set.seed(337)
wisca_data <- example_isolates[mo_domain(example_isolates$mo) == "Bacteria", ]
wisca_data$grp <- sample(c("A", "B", "C"), nrow(wisca_data), replace = TRUE)
future::plan(future::sequential)
wisca_strat_seq <- suppressWarnings(wisca(wisca_data, antimicrobials = "MEM", syndromic_group = "grp", simulations = 5000, info = FALSE))
future::plan(future::multicore)
wisca_strat_par <- suppressWarnings(wisca(wisca_data, antimicrobials = "MEM", syndromic_group = "grp", simulations = 5000, parallel = TRUE, info = FALSE))
expect_identical(retrieve_wisca_parameters(wisca_strat_par), retrieve_wisca_parameters(wisca_strat_seq))
expect_true(all(retrieve_wisca_parameters(wisca_strat_par)$n_tested > 0))
expect_true(all(abs(attributes(wisca_strat_par)$long_numeric$coverage - attributes(wisca_strat_seq)$long_numeric$coverage) < 0.01))
# 7. parallel = TRUE without a plan raises an informative error
future::plan(future::sequential)
expect_error(
suppressWarnings(wisca(example_isolates, antimicrobials = "TZP", parallel = TRUE, info = FALSE)),