diff --git a/DESCRIPTION b/DESCRIPTION index 2c35b4b43..436912be3 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: AMR -Version: 2.1.1.9154 -Date: 2025-02-22 +Version: 2.1.1.9155 +Date: 2025-02-23 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/NAMESPACE b/NAMESPACE index 5b2dcbec3..fa2e38634 100644 --- a/NAMESPACE +++ b/NAMESPACE @@ -251,6 +251,8 @@ export(mdr_cmi2012) export(mdr_tb) export(mdro) export(mean_amr_distance) +export(mic_p50) +export(mic_p90) export(mo_authors) export(mo_class) export(mo_cleaning_regex) diff --git a/NEWS.md b/NEWS.md index ee37d9fab..434953dbb 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,4 +1,4 @@ -# AMR 2.1.1.9154 +# AMR 2.1.1.9155 *(this beta version will eventually become v3.0. We're happy to reach a new major milestone soon, which will be all about the new One Health support! Install this beta using [the instructions here](https://msberends.github.io/AMR/#latest-development-version).)* @@ -38,7 +38,7 @@ This package now supports not only tools for AMR data analysis in clinical setti * **Other** * New function `top_n_microorganisms()` to filter a data set to the top *n* of any taxonomic property, e.g., filter to the top 3 species, filter to any species in the top 5 genera, or filter to the top 3 species in each of the top 5 genera * New function `mo_group_members()` to retrieve the member microorganisms of a microorganism group. For example, `mo_group_members("Strep group C")` returns a vector of all microorganisms that belong to that group. - + * New functions `mic_p50()` and `mic_p90()` to retrieve the 50th and 90th percentile of MIC values. ## Changed * SIR interpretation diff --git a/PythonPackage/AMR/AMR.egg-info/PKG-INFO b/PythonPackage/AMR/AMR.egg-info/PKG-INFO index 4583ab3ba..7e2f2d1fd 100644 --- a/PythonPackage/AMR/AMR.egg-info/PKG-INFO +++ b/PythonPackage/AMR/AMR.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.2 Name: AMR -Version: 2.1.1.9154 +Version: 2.1.1.9155 Summary: A Python wrapper for the AMR R package Home-page: https://github.com/msberends/AMR Author: Matthijs Berends diff --git a/PythonPackage/AMR/AMR/__init__.py b/PythonPackage/AMR/AMR/__init__.py index 4175abe4d..21e55385b 100644 --- a/PythonPackage/AMR/AMR/__init__.py +++ b/PythonPackage/AMR/AMR/__init__.py @@ -73,6 +73,8 @@ from .functions import is_disk from .functions import as_mic from .functions import is_mic from .functions import rescale_mic +from .functions import mic_p50 +from .functions import mic_p90 from .functions import as_mo from .functions import is_mo from .functions import mo_uncertainties diff --git a/PythonPackage/AMR/AMR/functions.py b/PythonPackage/AMR/AMR/functions.py index 6963838c7..6e91b0583 100644 --- a/PythonPackage/AMR/AMR/functions.py +++ b/PythonPackage/AMR/AMR/functions.py @@ -249,6 +249,12 @@ def is_mic(x): def rescale_mic(x, *args, **kwargs): """See our website of the R package for the manual: https://msberends.github.io/AMR/index.html""" return convert_to_python(amr_r.rescale_mic(x, *args, **kwargs)) +def mic_p50(x, *args, **kwargs): + """See our website of the R package for the manual: https://msberends.github.io/AMR/index.html""" + return convert_to_python(amr_r.mic_p50(x, *args, **kwargs)) +def mic_p90(x, *args, **kwargs): + """See our website of the R package for the manual: https://msberends.github.io/AMR/index.html""" + return convert_to_python(amr_r.mic_p90(x, *args, **kwargs)) def as_mo(x, *args, **kwargs): """See our website of the R package for the manual: https://msberends.github.io/AMR/index.html""" return convert_to_python(amr_r.as_mo(x, *args, **kwargs)) diff --git a/PythonPackage/AMR/dist/AMR-2.1.1.9154-py3-none-any.whl b/PythonPackage/AMR/dist/AMR-2.1.1.9154-py3-none-any.whl deleted file mode 100644 index bd4abbb05..000000000 Binary files a/PythonPackage/AMR/dist/AMR-2.1.1.9154-py3-none-any.whl and /dev/null differ diff --git a/PythonPackage/AMR/dist/AMR-2.1.1.9155-py3-none-any.whl b/PythonPackage/AMR/dist/AMR-2.1.1.9155-py3-none-any.whl new file mode 100644 index 000000000..42baddfc3 Binary files /dev/null and b/PythonPackage/AMR/dist/AMR-2.1.1.9155-py3-none-any.whl differ diff --git a/PythonPackage/AMR/dist/amr-2.1.1.9154.tar.gz b/PythonPackage/AMR/dist/amr-2.1.1.9154.tar.gz deleted file mode 100644 index a168b57e9..000000000 Binary files a/PythonPackage/AMR/dist/amr-2.1.1.9154.tar.gz and /dev/null differ diff --git a/PythonPackage/AMR/dist/amr-2.1.1.9155.tar.gz b/PythonPackage/AMR/dist/amr-2.1.1.9155.tar.gz new file mode 100644 index 000000000..5655fa851 Binary files /dev/null and b/PythonPackage/AMR/dist/amr-2.1.1.9155.tar.gz differ diff --git a/PythonPackage/AMR/setup.py b/PythonPackage/AMR/setup.py index f7fc7dc48..7caaa3f0a 100644 --- a/PythonPackage/AMR/setup.py +++ b/PythonPackage/AMR/setup.py @@ -2,7 +2,7 @@ from setuptools import setup, find_packages setup( name='AMR', - version='2.1.1.9154', + version='2.1.1.9155', packages=find_packages(), install_requires=[ 'rpy2', diff --git a/R/aa_helper_functions.R b/R/aa_helper_functions.R index 2378f68eb..a007be4dc 100644 --- a/R/aa_helper_functions.R +++ b/R/aa_helper_functions.R @@ -1390,6 +1390,9 @@ as_original_data_class <- function(df, old_class = NULL, extra_class = NULL) { fn <- function(x) base::as.data.frame(df, stringsAsFactors = FALSE) } out <- fn(df) + # don't keep row names + rownames(out) <- NULL + # add additional class if needed if (!is.null(extra_class)) { class(out) <- c(extra_class, class(out)) } diff --git a/R/antibiogram.R b/R/antibiogram.R index 0de1d7a3d..b9087166c 100755 --- a/R/antibiogram.R +++ b/R/antibiogram.R @@ -923,19 +923,16 @@ antibiogram.default <- function(x, } } - out <- as_original_data_class(new_df, class(x), extra_class = "antibiogram") + out <- structure(as_original_data_class(new_df, class(x), extra_class = "antibiogram"), + has_syndromic_group = has_syndromic_group, + combine_SI = combine_SI, + wisca = wisca, + conf_interval = conf_interval, + formatting_type = formatting_type, + wisca_parameters = as_original_data_class(wisca_parameters, class(x)), + long_numeric = as_original_data_class(long_numeric, class(x))) rownames(out) <- NULL - rownames(wisca_parameters) <- NULL - rownames(long_numeric) <- NULL - structure(out, - has_syndromic_group = has_syndromic_group, - combine_SI = combine_SI, - wisca = wisca, - conf_interval = conf_interval, - formatting_type = formatting_type, - wisca_parameters = as_original_data_class(wisca_parameters, class(x)), - long_numeric = as_original_data_class(long_numeric, class(x)) - ) + out } #' @method antibiogram grouped_df @@ -1041,14 +1038,16 @@ antibiogram.grouped_df <- function(x, close(progress) - structure(as_original_data_class(out, class(x), extra_class = "antibiogram"), - has_syndromic_group = FALSE, - combine_SI = isTRUE(combine_SI), - wisca = isTRUE(wisca), - conf_interval = conf_interval, - formatting_type = formatting_type, - wisca_parameters = as_original_data_class(wisca_parameters, class(x)), - long_numeric = as_original_data_class(long_numeric, class(x))) + out <- structure(as_original_data_class(out, class(x), extra_class = "antibiogram"), + has_syndromic_group = FALSE, + combine_SI = isTRUE(combine_SI), + wisca = isTRUE(wisca), + conf_interval = conf_interval, + formatting_type = formatting_type, + wisca_parameters = as_original_data_class(wisca_parameters, class(x)), + long_numeric = as_original_data_class(long_numeric, class(x))) + rownames(out) <- NULL + out } #' @export diff --git a/R/bug_drug_combinations.R b/R/bug_drug_combinations.R index 2d8b0c6fd..067868a11 100755 --- a/R/bug_drug_combinations.R +++ b/R/bug_drug_combinations.R @@ -185,8 +185,9 @@ bug_drug_combinations <- function(x, out <- as_original_data_class(out, class(x.bak)) # will remove tibble groups out <- out %pm>% pm_arrange(mo, ab) + class(out) <- c("bug_drug_combinations", if(data_has_groups) "grouped" else NULL, class(out)) rownames(out) <- NULL - structure(out, class = c("bug_drug_combinations", if(data_has_groups) "grouped" else NULL, class(out))) + out } #' @method format bug_drug_combinations diff --git a/R/mic.R b/R/mic.R index 1a9a54562..c460f110b 100644 --- a/R/mic.R +++ b/R/mic.R @@ -346,6 +346,21 @@ rescale_mic <- function(x, mic_range, keep_operators = "edges", as.mic = TRUE) { out } +#' @rdname as.mic +#' @details Use [mic_p50()] and [mic_p90()] to get the 50th and 90th percentile of MIC values. They return 'normal' [numeric] values. +#' @export +mic_p50 <- function(x, na.rm = FALSE, ...) { + x <- as.mic(x) + as.double(stats::quantile(x, probs = 0.5, na.rm = na.rm)) +} + +#' @rdname as.mic +#' @export +mic_p90 <- function(x, na.rm = FALSE, ...) { + x <- as.mic(x) + as.double(stats::quantile(x, probs = 0.9, na.rm = na.rm)) +} + #' @method as.double mic #' @export #' @noRd diff --git a/R/plotting.R b/R/plotting.R index 1787cf54f..9e7156f23 100755 --- a/R/plotting.R +++ b/R/plotting.R @@ -186,7 +186,7 @@ #' scale_colour_sir(language = "pt", #' name = "Support in 20 languages") #' } -#' +#' } #' #' # Plotting using base R's plot() --------------------------------------- #' diff --git a/R/sir.R b/R/sir.R index 737b82601..e0d2cb72a 100755 --- a/R/sir.R +++ b/R/sir.R @@ -1544,7 +1544,7 @@ sir_interpretation_history <- function(clean = FALSE) { if (pkg_is_available("tibble")) { out <- import_fn("as_tibble", "tibble")(out) } - structure(out, class = c("sir_log", class(out))) + as_original_data_class(out, class(out), extra_class = "sir_log") } #' @method print sir_log diff --git a/R/sir_calc.R b/R/sir_calc.R index d7de6d89f..9b3c130ca 100755 --- a/R/sir_calc.R +++ b/R/sir_calc.R @@ -383,8 +383,6 @@ sir_calc_df <- function(type, # "proportion", "count" or "both" # remove redundant columns out <- subset(out, select = -c(ci_min, ci_max, isolates)) } - - rownames(out) <- NULL - out <- as_original_data_class(out, class(data.bak)) # will remove tibble groups - structure(out, class = c("sir_df", class(out))) + + as_original_data_class(out, class(data.bak), extra_class = "sir_df") # will remove tibble groups } diff --git a/data-raw/gpt_training_text_v2.1.1.9154.txt b/data-raw/gpt_training_text_v2.1.1.9155.txt similarity index 98% rename from data-raw/gpt_training_text_v2.1.1.9154.txt rename to data-raw/gpt_training_text_v2.1.1.9155.txt index 6d9990a71..ccdfd9f03 100644 --- a/data-raw/gpt_training_text_v2.1.1.9154.txt +++ b/data-raw/gpt_training_text_v2.1.1.9155.txt @@ -1,6 +1,6 @@ This knowledge base contains all context you must know about the AMR package for R. You are a GPT trained to be an assistant for the AMR package in R. You are an incredible R specialist, especially trained in this package and in the tidyverse. -First and foremost, you are trained on version 2.1.1.9154. Remember this whenever someone asks which AMR package version you’re at. +First and foremost, you are trained on version 2.1.1.9155. Remember this whenever someone asks which AMR package version you’re at. Below are the contents of the file, the file, and all the files (documentation) in the package. Every file content is split using 100 hypens. ---------------------------------------------------------------------------------------------------- @@ -262,6 +262,8 @@ export(mdr_cmi2012) export(mdr_tb) export(mdro) export(mean_amr_distance) +export(mic_p50) +export(mic_p90) export(mo_authors) export(mo_class) export(mo_cleaning_regex) @@ -656,30 +658,18 @@ It will be downloaded and installed automatically. For RStudio, click on the men #### Latest development version -[![check-old](https://github.com/msberends/AMR/actions/workflows/check-old.yaml/badge.svg?branch=main)](https://github.com/msberends/AMR/actions/workflows/check-old.yaml?query=branch%3Amain) -[![check-recent](https://github.com/msberends/AMR/actions/workflows/check-recent.yaml/badge.svg?branch=main)](https://github.com/msberends/AMR/actions/workflows/check-recent.yaml?query=branch%3Amain) +[![check-old](https://github.com/msberends/AMR/actions/workflows/check-old-tinytest.yaml/badge.svg?branch=main)](https://github.com/msberends/AMR/actions/workflows/check-old-tinytest.yaml) +[![check-recent](https://github.com/msberends/AMR/actions/workflows/check-current-testthat.yaml/badge.svg?branch=main)](https://github.com/msberends/AMR/actions/workflows/check-current-testthat.yaml) [![CodeFactor](https://www.codefactor.io/repository/github/msberends/amr/badge)](https://www.codefactor.io/repository/github/msberends/amr) [![Codecov](https://codecov.io/gh/msberends/AMR/branch/main/graph/badge.svg)](https://codecov.io/gh/msberends/AMR?branch=main) Please read our [Developer Guideline here](https://github.com/msberends/AMR/wiki/Developer-Guideline). -The latest and unpublished development version can be installed from GitHub in two ways: +The latest and unpublished development version can be installed from the [rOpenSci R-universe platform](https://msberends.r-universe.dev/AMR): -1. Manually, using: - - ```r - install.packages("remotes") # if you haven't already - remotes::install_github("msberends/AMR") - ``` - -2. Automatically, using the [rOpenSci R-universe platform](https://ropensci.org/r-universe/), by adding [our R-universe address](https://msberends.r-universe.dev) to your list of repositories ('repos'): - - ```r - options(repos = c(getOption("repos"), - msberends = "https://msberends.r-universe.dev")) - ``` - - After this, you can install and update this `AMR` package like any official release (e.g., using `install.packages("AMR")` or in RStudio via *Tools* > *Check for Package Updates...*). +```r +install.packages("AMR", repos = "https://msberends.r-universe.dev") +``` ### Get started @@ -2873,6 +2863,8 @@ THE PART HEREAFTER CONTAINS CONTENTS FROM FILE 'man/as.mic.Rd': \alias{is.mic} \alias{NA_mic_} \alias{rescale_mic} +\alias{mic_p50} +\alias{mic_p90} \alias{droplevels.mic} \title{Transform Input to Minimum Inhibitory Concentrations (MIC)} \usage{ @@ -2884,6 +2876,10 @@ NA_mic_ rescale_mic(x, mic_range, keep_operators = "edges", as.mic = TRUE) +mic_p50(x, na.rm = FALSE, ...) + +mic_p90(x, na.rm = FALSE, ...) + \method{droplevels}{mic}(x, as.mic = FALSE, ...) } \arguments{ @@ -2953,6 +2949,8 @@ With \code{\link[=rescale_mic]{rescale_mic()}}, existing MIC ranges can be limit For \code{ggplot2}, use one of the \code{\link[=scale_x_mic]{scale_*_mic()}} functions to plot MIC values. They allows custom MIC ranges and to plot intermediate log2 levels for missing MIC values. \code{NA_mic_} is a missing value of the new \code{mic} class, analogous to e.g. base \R's \code{\link[base:NA]{NA_character_}}. + +Use \code{\link[=mic_p50]{mic_p50()}} and \code{\link[=mic_p90]{mic_p90()}} to get the 50th and 90th percentile of MIC values. They return 'normal' \link{numeric} values. } \examples{ mic_data <- as.mic(c(">=32", "1.0", "1", "1.00", 8, "<=0.128", "8", "16", "16")) @@ -7540,6 +7538,131 @@ The interpretation of "I" will be named "Increased exposure" for all EUCAST guid For interpreting MIC values as well as disk diffusion diameters, the default guideline is EUCAST 2024, unless the package option \code{\link[=AMR-options]{AMR_guideline}} is set. See \code{\link[=as.sir]{as.sir()}} for more information. } } +\examples{ +some_mic_values <- random_mic(size = 100) +some_disk_values <- random_disk(size = 100, mo = "Escherichia coli", ab = "cipro") +some_sir_values <- random_sir(50, prob_SIR = c(0.55, 0.05, 0.30)) + + +\donttest{ +# Plotting using ggplot2's autoplot() for MIC, disk, and SIR ----------- +if (require("ggplot2")) { + autoplot(some_mic_values) +} +if (require("ggplot2")) { + # when providing the microorganism and antibiotic, colours will show interpretations: + autoplot(some_mic_values, mo = "Escherichia coli", ab = "cipro") +} +if (require("ggplot2")) { + # support for 20 languages, various guidelines, and many options + autoplot(some_disk_values, mo = "Escherichia coli", ab = "cipro", + guideline = "CLSI 2024", language = "no", + title = "Disk diffusion from the North") +} + + +# Plotting using scale_x_mic() ----------------------------------------- +if (require("ggplot2")) { + mic_plot <- ggplot(data.frame(mics = as.mic(c(0.25, "<=4", 4, 8, 32, ">=32")), + counts = c(1, 1, 2, 2, 3, 3)), + aes(mics, counts)) + + geom_col() + mic_plot + + labs(title = "without scale_x_mic()") +} +if (require("ggplot2")) { + mic_plot + + scale_x_mic() + + labs(title = "with scale_x_mic()") +} +if (require("ggplot2")) { + mic_plot + + scale_x_mic(keep_operators = "all") + + labs(title = "with scale_x_mic() keeping all operators") +} +if (require("ggplot2")) { + mic_plot + + scale_x_mic(mic_range = c(1, 16)) + + labs(title = "with scale_x_mic() using a manual 'within' range") +} +if (require("ggplot2")) { + mic_plot + + scale_x_mic(mic_range = c(0.032, 256)) + + labs(title = "with scale_x_mic() using a manual 'outside' range") +} + + +# Plotting using scale_y_mic() ----------------------------------------- +some_groups <- sample(LETTERS[1:5], 20, replace = TRUE) + +if (require("ggplot2")) { + ggplot(data.frame(mic = some_mic_values, + group = some_groups), + aes(group, mic)) + + geom_boxplot() + + geom_violin(linetype = 2, colour = "grey", fill = NA) + + scale_y_mic() +} +if (require("ggplot2")) { + ggplot(data.frame(mic = some_mic_values, + group = some_groups), + aes(group, mic)) + + geom_boxplot() + + geom_violin(linetype = 2, colour = "grey", fill = NA) + + scale_y_mic(mic_range = c(NA, 0.25)) +} + + +# Plotting using scale_x_sir() ----------------------------------------- +if (require("ggplot2")) { + ggplot(data.frame(x = c("I", "R", "S"), + y = c(45,323, 573)), + aes(x, y)) + + geom_col() + + scale_x_sir() +} + + +# Plotting using scale_y_mic() and scale_colour_sir() ------------------ +if (require("ggplot2")) { + plain <- ggplot(data.frame(mic = some_mic_values, + group = some_groups, + sir = as.sir(some_mic_values, + mo = "E. coli", + ab = "cipro")), + aes(x = group, y = mic, colour = sir)) + + theme_minimal() + + geom_boxplot(fill = NA, colour = "grey") + + geom_jitter(width = 0.25) + + plain +} +if (require("ggplot2")) { + # and now with our MIC and SIR scale functions: + plain + + scale_y_mic() + + scale_colour_sir() +} +if (require("ggplot2")) { + plain + + scale_y_mic(mic_range = c(0.005, 32), name = "Our MICs!") + + scale_colour_sir(language = "pt", + name = "Support in 20 languages") +} +} + +# Plotting using base R's plot() --------------------------------------- + +plot(some_mic_values) +# when providing the microorganism and antibiotic, colours will show interpretations: +plot(some_mic_values, mo = "S. aureus", ab = "ampicillin") + +plot(some_disk_values) +plot(some_disk_values, mo = "Escherichia coli", ab = "cipro") +plot(some_disk_values, mo = "Escherichia coli", ab = "cipro", language = "nl") + +plot(some_sir_values) +} @@ -8516,8 +8639,10 @@ antibiogram(example_isolates, To create a combined antibiogram, use antibiotic codes or names with a plus `+` character like this: ```{r comb} -antibiogram(example_isolates, - antibiotics = c("TZP", "TZP+TOB", "TZP+GEN")) +combined_ab <- antibiogram(example_isolates, + antibiotics = c("TZP", "TZP+TOB", "TZP+GEN"), + ab_transform = NULL) +combined_ab ``` ### Syndromic Antibiogram @@ -8532,17 +8657,26 @@ antibiogram(example_isolates, ### Weighted-Incidence Syndromic Combination Antibiogram (WISCA) -To create a WISCA, you must state combination therapy in the `antibiotics` argument (similar to the Combination Antibiogram), define a syndromic group with the `syndromic_group` argument (similar to the Syndromic Antibiogram) in which cases are predefined based on clinical or demographic characteristics (e.g., endocarditis in 75+ females). This next example is a simplification without clinical characteristics, but just gives an idea of how a WISCA can be created: +To create a **Weighted-Incidence Syndromic Combination Antibiogram (WISCA)**, simply set `wisca = TRUE` in the `antibiogram()` function, or use the dedicated `wisca()` function. Unlike traditional antibiograms, WISCA provides syndrome-based susceptibility estimates, weighted by pathogen incidence and antimicrobial susceptibility patterns. ```{r wisca} -wisca <- antibiogram(example_isolates, - antibiotics = c("AMC", "AMC+CIP", "TZP", "TZP+TOB"), - mo_transform = "gramstain", - minimum = 10, # this should be >= 30, but now just as example - syndromic_group = ifelse(example_isolates$age >= 65 & - example_isolates$gender == "M", - "WISCA Group 1", "WISCA Group 2")) -wisca +example_isolates %>% + wisca(antibiotics = c("TZP", "TZP+TOB", "TZP+GEN"), + minimum = 10) # Recommended threshold: ≥30 +``` + +WISCA uses a **Bayesian decision model** to integrate data from multiple pathogens, improving empirical therapy guidance, especially for low-incidence infections. It is **pathogen-agnostic**, meaning results are syndrome-based rather than stratified by microorganism. + +For reliable results, ensure your data includes **only first isolates** (use `first_isolate()`) and consider filtering for **the top *n* species** (use `top_n_microorganisms()`), as WISCA outcomes are most meaningful when based on robust incidence estimates. + +For **patient- or syndrome-specific WISCA**, run the function on a grouped `tibble`, i.e., using `group_by()` first: + +```{r wisca_grouped} +example_isolates %>% + top_n_microorganisms(n = 10) %>% + group_by(age_group = age_groups(age, c(25, 50, 75)), + gender) %>% + wisca(antibiotics = c("TZP", "TZP+TOB", "TZP+GEN")) ``` ### Plotting antibiograms @@ -8550,7 +8684,7 @@ wisca Antibiograms can be plotted using `autoplot()` from the `ggplot2` packages, since this `AMR` package provides an extension to that function: ```{r} -autoplot(wisca) +autoplot(combined_ab) ``` To calculate antimicrobial resistance in a more sensible way, also by correcting for too few results, we use the `resistance()` and `susceptibility()` functions. @@ -8575,9 +8709,54 @@ our_data_1st %>% summarise(amoxicillin = resistance(AMX)) ``` +## Interpreting MIC and Disk Diffusion Values + +Minimal inhibitory concentration (MIC) values and disk diffusion diameters can be interpreted into clinical breakpoints (SIR) using `as.sir()`. Here’s an example with randomly generated MIC values for *Klebsiella pneumoniae* and ciprofloxacin: + +```{r mic_interpretation} +set.seed(123) +mic_values <- random_mic(100) +sir_values <- as.sir(mic_values, mo = "K. pneumoniae", ab = "cipro", guideline = "EUCAST 2024") + +my_data <- tibble(MIC = mic_values, SIR = sir_values) +my_data +``` + +This allows direct interpretation according to EUCAST or CLSI breakpoints, facilitating automated AMR data processing. + +## Plotting MIC and SIR Interpretations + +We can visualise MIC distributions and their SIR interpretations using `ggplot2`, using the new `scale_y_mic()` for the y-axis and `scale_colour_sir()` to colour-code SIR categories. + +```{r mic_plot} +# add a group +my_data$group <- rep(c("A", "B", "C", "D"), each = 25) + +ggplot(my_data, + aes(x = group, y = MIC, colour = SIR)) + + geom_jitter(width = 0.2, size = 2) + + geom_boxplot(fill = NA, colour = "grey40") + + scale_y_mic() + + scale_colour_sir() + + labs(title = "MIC Distribution and SIR Interpretation", + x = "Sample Groups", + y = "MIC (mg/L)") +``` + +This plot provides an intuitive way to assess susceptibility patterns across different groups while incorporating clinical breakpoints. + +For a more straightforward and less manual approach, `ggplot2`'s function `autoplot()` has been extended by this package to directly plot MIC and disk diffusion values: + +```{r autoplot} +autoplot(mic_values) + +# by providing `mo` and `ab`, colours will indicate the SIR interpretation: +autoplot(mic_values, mo = "K. pneumoniae", ab = "cipro", guideline = "EUCAST 2024") +``` + ---- -*Author: Dr. Matthijs Berends, 26th Feb 2023* +*Author: Dr. Matthijs Berends, 23rd Feb 2025* diff --git a/index.md b/index.md index 810324f3b..1dfddd85f 100644 --- a/index.md +++ b/index.md @@ -266,30 +266,18 @@ It will be downloaded and installed automatically. For RStudio, click on the men #### Latest development version -[![check-old](https://github.com/msberends/AMR/actions/workflows/check-old.yaml/badge.svg?branch=main)](https://github.com/msberends/AMR/actions/workflows/check-old.yaml?query=branch%3Amain) -[![check-recent](https://github.com/msberends/AMR/actions/workflows/check-recent.yaml/badge.svg?branch=main)](https://github.com/msberends/AMR/actions/workflows/check-recent.yaml?query=branch%3Amain) +[![check-old](https://github.com/msberends/AMR/actions/workflows/check-old-tinytest.yaml/badge.svg?branch=main)](https://github.com/msberends/AMR/actions/workflows/check-old-tinytest.yaml) +[![check-recent](https://github.com/msberends/AMR/actions/workflows/check-current-testthat.yaml/badge.svg?branch=main)](https://github.com/msberends/AMR/actions/workflows/check-current-testthat.yaml) [![CodeFactor](https://www.codefactor.io/repository/github/msberends/amr/badge)](https://www.codefactor.io/repository/github/msberends/amr) [![Codecov](https://codecov.io/gh/msberends/AMR/branch/main/graph/badge.svg)](https://codecov.io/gh/msberends/AMR?branch=main) Please read our [Developer Guideline here](https://github.com/msberends/AMR/wiki/Developer-Guideline). -The latest and unpublished development version can be installed from GitHub in two ways: +The latest and unpublished development version can be installed from the [rOpenSci R-universe platform](https://msberends.r-universe.dev/AMR): -1. Manually, using: - - ```r - install.packages("remotes") # if you haven't already - remotes::install_github("msberends/AMR") - ``` - -2. Automatically, using the [rOpenSci R-universe platform](https://ropensci.org/r-universe/), by adding [our R-universe address](https://msberends.r-universe.dev) to your list of repositories ('repos'): - - ```r - options(repos = c(getOption("repos"), - msberends = "https://msberends.r-universe.dev")) - ``` - - After this, you can install and update this `AMR` package like any official release (e.g., using `install.packages("AMR")` or in RStudio via *Tools* > *Check for Package Updates...*). +```r +install.packages("AMR", repos = "https://msberends.r-universe.dev") +``` ### Get started diff --git a/man/as.mic.Rd b/man/as.mic.Rd index 5d8c6dbe2..6f6c03bb0 100644 --- a/man/as.mic.Rd +++ b/man/as.mic.Rd @@ -7,6 +7,8 @@ \alias{is.mic} \alias{NA_mic_} \alias{rescale_mic} +\alias{mic_p50} +\alias{mic_p90} \alias{droplevels.mic} \title{Transform Input to Minimum Inhibitory Concentrations (MIC)} \usage{ @@ -18,6 +20,10 @@ NA_mic_ rescale_mic(x, mic_range, keep_operators = "edges", as.mic = TRUE) +mic_p50(x, na.rm = FALSE, ...) + +mic_p90(x, na.rm = FALSE, ...) + \method{droplevels}{mic}(x, as.mic = FALSE, ...) } \arguments{ @@ -87,6 +93,8 @@ With \code{\link[=rescale_mic]{rescale_mic()}}, existing MIC ranges can be limit For \code{ggplot2}, use one of the \code{\link[=scale_x_mic]{scale_*_mic()}} functions to plot MIC values. They allows custom MIC ranges and to plot intermediate log2 levels for missing MIC values. \code{NA_mic_} is a missing value of the new \code{mic} class, analogous to e.g. base \R's \code{\link[base:NA]{NA_character_}}. + +Use \code{\link[=mic_p50]{mic_p50()}} and \code{\link[=mic_p90]{mic_p90()}} to get the 50th and 90th percentile of MIC values. They return 'normal' \link{numeric} values. } \examples{ mic_data <- as.mic(c(">=32", "1.0", "1", "1.00", 8, "<=0.128", "8", "16", "16")) diff --git a/man/plot.Rd b/man/plot.Rd index c3e175cd2..f7d74aaf5 100644 --- a/man/plot.Rd +++ b/man/plot.Rd @@ -196,3 +196,128 @@ The interpretation of "I" will be named "Increased exposure" for all EUCAST guid For interpreting MIC values as well as disk diffusion diameters, the default guideline is EUCAST 2024, unless the package option \code{\link[=AMR-options]{AMR_guideline}} is set. See \code{\link[=as.sir]{as.sir()}} for more information. } } +\examples{ +some_mic_values <- random_mic(size = 100) +some_disk_values <- random_disk(size = 100, mo = "Escherichia coli", ab = "cipro") +some_sir_values <- random_sir(50, prob_SIR = c(0.55, 0.05, 0.30)) + + +\donttest{ +# Plotting using ggplot2's autoplot() for MIC, disk, and SIR ----------- +if (require("ggplot2")) { + autoplot(some_mic_values) +} +if (require("ggplot2")) { + # when providing the microorganism and antibiotic, colours will show interpretations: + autoplot(some_mic_values, mo = "Escherichia coli", ab = "cipro") +} +if (require("ggplot2")) { + # support for 20 languages, various guidelines, and many options + autoplot(some_disk_values, mo = "Escherichia coli", ab = "cipro", + guideline = "CLSI 2024", language = "no", + title = "Disk diffusion from the North") +} + + +# Plotting using scale_x_mic() ----------------------------------------- +if (require("ggplot2")) { + mic_plot <- ggplot(data.frame(mics = as.mic(c(0.25, "<=4", 4, 8, 32, ">=32")), + counts = c(1, 1, 2, 2, 3, 3)), + aes(mics, counts)) + + geom_col() + mic_plot + + labs(title = "without scale_x_mic()") +} +if (require("ggplot2")) { + mic_plot + + scale_x_mic() + + labs(title = "with scale_x_mic()") +} +if (require("ggplot2")) { + mic_plot + + scale_x_mic(keep_operators = "all") + + labs(title = "with scale_x_mic() keeping all operators") +} +if (require("ggplot2")) { + mic_plot + + scale_x_mic(mic_range = c(1, 16)) + + labs(title = "with scale_x_mic() using a manual 'within' range") +} +if (require("ggplot2")) { + mic_plot + + scale_x_mic(mic_range = c(0.032, 256)) + + labs(title = "with scale_x_mic() using a manual 'outside' range") +} + + +# Plotting using scale_y_mic() ----------------------------------------- +some_groups <- sample(LETTERS[1:5], 20, replace = TRUE) + +if (require("ggplot2")) { + ggplot(data.frame(mic = some_mic_values, + group = some_groups), + aes(group, mic)) + + geom_boxplot() + + geom_violin(linetype = 2, colour = "grey", fill = NA) + + scale_y_mic() +} +if (require("ggplot2")) { + ggplot(data.frame(mic = some_mic_values, + group = some_groups), + aes(group, mic)) + + geom_boxplot() + + geom_violin(linetype = 2, colour = "grey", fill = NA) + + scale_y_mic(mic_range = c(NA, 0.25)) +} + + +# Plotting using scale_x_sir() ----------------------------------------- +if (require("ggplot2")) { + ggplot(data.frame(x = c("I", "R", "S"), + y = c(45,323, 573)), + aes(x, y)) + + geom_col() + + scale_x_sir() +} + + +# Plotting using scale_y_mic() and scale_colour_sir() ------------------ +if (require("ggplot2")) { + plain <- ggplot(data.frame(mic = some_mic_values, + group = some_groups, + sir = as.sir(some_mic_values, + mo = "E. coli", + ab = "cipro")), + aes(x = group, y = mic, colour = sir)) + + theme_minimal() + + geom_boxplot(fill = NA, colour = "grey") + + geom_jitter(width = 0.25) + + plain +} +if (require("ggplot2")) { + # and now with our MIC and SIR scale functions: + plain + + scale_y_mic() + + scale_colour_sir() +} +if (require("ggplot2")) { + plain + + scale_y_mic(mic_range = c(0.005, 32), name = "Our MICs!") + + scale_colour_sir(language = "pt", + name = "Support in 20 languages") +} +} + +# Plotting using base R's plot() --------------------------------------- + +plot(some_mic_values) +# when providing the microorganism and antibiotic, colours will show interpretations: +plot(some_mic_values, mo = "S. aureus", ab = "ampicillin") + +plot(some_disk_values) +plot(some_disk_values, mo = "Escherichia coli", ab = "cipro") +plot(some_disk_values, mo = "Escherichia coli", ab = "cipro", language = "nl") + +plot(some_sir_values) +} diff --git a/tests/testthat/test-mic.R b/tests/testthat/test-mic.R index fcbee903c..cabe6f81c 100755 --- a/tests/testthat/test-mic.R +++ b/tests/testthat/test-mic.R @@ -28,9 +28,9 @@ # ==================================================================== # # used in multiple functions, also in plotting -expect_true(all(as.mic(COMMON_MIC_VALUES) %in% VALID_MIC_LEVELS)) -expect_true(all(paste0("<=", as.mic(COMMON_MIC_VALUES)) %in% VALID_MIC_LEVELS)) -expect_true(all(paste0(">=", as.mic(COMMON_MIC_VALUES)) %in% VALID_MIC_LEVELS)) +expect_true(all(as.mic(AMR:::COMMON_MIC_VALUES) %in% AMR:::VALID_MIC_LEVELS)) +expect_true(all(paste0("<=", as.mic(AMR:::COMMON_MIC_VALUES)) %in% AMR:::VALID_MIC_LEVELS)) +expect_true(all(paste0(">=", as.mic(AMR:::COMMON_MIC_VALUES)) %in% AMR:::VALID_MIC_LEVELS)) expect_true(as.mic(8) == as.mic("8")) expect_true(as.mic("1") > as.mic("<=0.0625")) diff --git a/vignettes/AMR.Rmd b/vignettes/AMR.Rmd index bcc4e80cb..390557c1f 100755 --- a/vignettes/AMR.Rmd +++ b/vignettes/AMR.Rmd @@ -288,8 +288,10 @@ antibiogram(example_isolates, To create a combined antibiogram, use antibiotic codes or names with a plus `+` character like this: ```{r comb} -antibiogram(example_isolates, - antibiotics = c("TZP", "TZP+TOB", "TZP+GEN")) +combined_ab <- antibiogram(example_isolates, + antibiotics = c("TZP", "TZP+TOB", "TZP+GEN"), + ab_transform = NULL) +combined_ab ``` ### Syndromic Antibiogram @@ -304,17 +306,26 @@ antibiogram(example_isolates, ### Weighted-Incidence Syndromic Combination Antibiogram (WISCA) -To create a WISCA, you must state combination therapy in the `antibiotics` argument (similar to the Combination Antibiogram), define a syndromic group with the `syndromic_group` argument (similar to the Syndromic Antibiogram) in which cases are predefined based on clinical or demographic characteristics (e.g., endocarditis in 75+ females). This next example is a simplification without clinical characteristics, but just gives an idea of how a WISCA can be created: +To create a **Weighted-Incidence Syndromic Combination Antibiogram (WISCA)**, simply set `wisca = TRUE` in the `antibiogram()` function, or use the dedicated `wisca()` function. Unlike traditional antibiograms, WISCA provides syndrome-based susceptibility estimates, weighted by pathogen incidence and antimicrobial susceptibility patterns. ```{r wisca} -wisca <- antibiogram(example_isolates, - antibiotics = c("AMC", "AMC+CIP", "TZP", "TZP+TOB"), - mo_transform = "gramstain", - minimum = 10, # this should be >= 30, but now just as example - syndromic_group = ifelse(example_isolates$age >= 65 & - example_isolates$gender == "M", - "WISCA Group 1", "WISCA Group 2")) -wisca +example_isolates %>% + wisca(antibiotics = c("TZP", "TZP+TOB", "TZP+GEN"), + minimum = 10) # Recommended threshold: ≥30 +``` + +WISCA uses a **Bayesian decision model** to integrate data from multiple pathogens, improving empirical therapy guidance, especially for low-incidence infections. It is **pathogen-agnostic**, meaning results are syndrome-based rather than stratified by microorganism. + +For reliable results, ensure your data includes **only first isolates** (use `first_isolate()`) and consider filtering for **the top *n* species** (use `top_n_microorganisms()`), as WISCA outcomes are most meaningful when based on robust incidence estimates. + +For **patient- or syndrome-specific WISCA**, run the function on a grouped `tibble`, i.e., using `group_by()` first: + +```{r wisca_grouped} +example_isolates %>% + top_n_microorganisms(n = 10) %>% + group_by(age_group = age_groups(age, c(25, 50, 75)), + gender) %>% + wisca(antibiotics = c("TZP", "TZP+TOB", "TZP+GEN")) ``` ### Plotting antibiograms @@ -322,7 +333,7 @@ wisca Antibiograms can be plotted using `autoplot()` from the `ggplot2` packages, since this `AMR` package provides an extension to that function: ```{r} -autoplot(wisca) +autoplot(combined_ab) ``` To calculate antimicrobial resistance in a more sensible way, also by correcting for too few results, we use the `resistance()` and `susceptibility()` functions. @@ -347,6 +358,51 @@ our_data_1st %>% summarise(amoxicillin = resistance(AMX)) ``` +## Interpreting MIC and Disk Diffusion Values + +Minimal inhibitory concentration (MIC) values and disk diffusion diameters can be interpreted into clinical breakpoints (SIR) using `as.sir()`. Here’s an example with randomly generated MIC values for *Klebsiella pneumoniae* and ciprofloxacin: + +```{r mic_interpretation} +set.seed(123) +mic_values <- random_mic(100) +sir_values <- as.sir(mic_values, mo = "K. pneumoniae", ab = "cipro", guideline = "EUCAST 2024") + +my_data <- tibble(MIC = mic_values, SIR = sir_values) +my_data +``` + +This allows direct interpretation according to EUCAST or CLSI breakpoints, facilitating automated AMR data processing. + +## Plotting MIC and SIR Interpretations + +We can visualise MIC distributions and their SIR interpretations using `ggplot2`, using the new `scale_y_mic()` for the y-axis and `scale_colour_sir()` to colour-code SIR categories. + +```{r mic_plot} +# add a group +my_data$group <- rep(c("A", "B", "C", "D"), each = 25) + +ggplot(my_data, + aes(x = group, y = MIC, colour = SIR)) + + geom_jitter(width = 0.2, size = 2) + + geom_boxplot(fill = NA, colour = "grey40") + + scale_y_mic() + + scale_colour_sir() + + labs(title = "MIC Distribution and SIR Interpretation", + x = "Sample Groups", + y = "MIC (mg/L)") +``` + +This plot provides an intuitive way to assess susceptibility patterns across different groups while incorporating clinical breakpoints. + +For a more straightforward and less manual approach, `ggplot2`'s function `autoplot()` has been extended by this package to directly plot MIC and disk diffusion values: + +```{r autoplot} +autoplot(mic_values) + +# by providing `mo` and `ab`, colours will indicate the SIR interpretation: +autoplot(mic_values, mo = "K. pneumoniae", ab = "cipro", guideline = "EUCAST 2024") +``` + ---- -*Author: Dr. Matthijs Berends, 26th Feb 2023* +*Author: Dr. Matthijs Berends, 23rd Feb 2025*