update MIC comparisons

This commit is contained in:
dr. M.S. (Matthijs) Berends 2024-04-05 16:44:43 +02:00
parent 4170def0ec
commit 0039cb05d6
13 changed files with 170 additions and 120 deletions

View File

@ -1,6 +1,6 @@
Package: AMR
Version: 2.1.1.9015
Date: 2024-03-09
Version: 2.1.1.9016
Date: 2024-04-05
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

View File

@ -1,4 +1,4 @@
# AMR 2.1.1.9015
# AMR 2.1.1.9016
*(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!)*
@ -10,7 +10,7 @@ This package now supports not only tools for AMR data analysis in clinical setti
## New
* One Health implementation
* Function `as.sir()` now supports animal breakpoints from CLSI. Use `breakpoint_type = "animal"` and set the `host` argument to a variable that contains animal species names.
* Function `as.sir()` now has extensive support for animal breakpoints from CLSI. Use `breakpoint_type = "animal"` and set the `host` argument to a variable that contains animal species names.
* The `clinical_breakpoints` data set contains all these breakpoints, and can be downloaded on our [download page](https://msberends.github.io/AMR/articles/datasets.html).
* The `antibiotics` data set contains all veterinary antibiotics, such as pradofloxacin and enrofloxacin. All WHOCC codes for veterinary use have been added as well.
* `ab_atc()` now supports ATC codes of veterinary antibiotics (that all start with "Q")
@ -22,8 +22,9 @@ This package now supports not only tools for AMR data analysis in clinical setti
* For MICs:
* Added as valid levels: 4096, 6 powers of 0.0625, and 5 powers of 192 (192, 384, 576, 768, 960)
* Added new argument `keep_operators` to `as.mic()`. This can be `"all"` (default), `"none"`, or `"edges"`. This argument is also available in the new `limit_mic_range()` and `scale_*_mic()` functions.
* Comparisons of MIC values are now more strict. For example, `>32` is higher than (and never equal to) `32`. Thus, `as.mic(">32") == as.mic(32)` now returns `FALSE`, and `as.mic(">32") > as.mic(32)` now returns `TRUE`.
* Updated `italicise_taxonomy()` to support HTML
* Greatly improved `vctrs` integration, a Tidyverse package working in the background for many Tidyverse functions. For users, this means that `dplyr::rowwise()` and `dplyr::c_across()` are now supported for e.g. columns of class `mic`. Despite this, this `AMR` package is still zero-dependent on any other package, including `dplyr` and `vctrs`.
* Greatly improved `vctrs` integration, a Tidyverse package working in the background for many Tidyverse functions. For users, this means that functions such as `dplyr`'s `bind_rows()`, `rowwise()` and `c_across()` are now supported for e.g. columns of class `mic`. Despite this, this `AMR` package is still zero-dependent on any other package, including `dplyr` and `vctrs`.
* Updated all ATC codes from WHOCC
* Updated all antibiotic DDDs from WHOCC

View File

@ -875,7 +875,7 @@ meet_criteria <- function(object, # can be literally `list(...)` for `allow_argu
if ("logical" %in% allow_class) {
is_in <- is_in[!is_in %in% c("TRUE", "FALSE")]
}
or_values <- vector_or(is_in, quotes = !isTRUE(any(c("double", "numeric", "integer") %in% allow_class)))
or_values <- vector_or(is_in, quotes = !isTRUE(any(c("numeric", "integer") %in% allow_class)))
if ("logical" %in% allow_class) {
or_values <- paste0(or_values, ", or TRUE or FALSE")
}

View File

@ -165,11 +165,6 @@ pillar_shaft.disk <- function(x, ...) {
create_pillar_column(out, align = "right", width = 2)
}
# will be exported using s3_register() in R/zzz.R
type_sum.disk <- function(x, ...) {
"disk"
}
#' @method print disk
#' @export
#' @noRd

View File

@ -175,8 +175,8 @@ mean_amr_distance.data.frame <- function(x, ..., combine_SI = TRUE) {
#' @param row an index, such as a row number
#' @export
amr_distance_from_row <- function(amr_distance, row) {
meet_criteria(amr_distance, allow_class = c("double", "numeric"), is_finite = TRUE)
meet_criteria(row, allow_class = c("logical", "double", "numeric"))
meet_criteria(amr_distance, allow_class = "numeric", is_finite = TRUE)
meet_criteria(row, allow_class = c("logical", "numeric", "integer"))
if (is.logical(row)) {
row <- which(row)
}

199
R/mic.R
View File

@ -154,99 +154,103 @@ as.mic <- function(x, na.rm = FALSE, keep_operators = "all") {
}
if (is.mic(x) && (keep_operators == "all" || !any(x %like% "[>=<]", na.rm = TRUE))) {
x
if (!identical(levels(x), VALID_MIC_LEVELS)) {
# from an older AMR version - just update MIC factor levels
x <- set_clean_class(factor(as.character(x), levels = VALID_MIC_LEVELS, ordered = TRUE),
new_class = c("mic", "ordered", "factor"))
}
return(x)
}
x.bak <- NULL
if (is.numeric(x)) {
x.bak <- format(x, scientific = FALSE)
# MICs never have more than 9 decimals, so:
x <- format(round(x, 9), scientific = FALSE)
} else {
x.bak <- NULL
if (is.numeric(x)) {
x.bak <- format(x, scientific = FALSE)
# MICs never have more than 9 decimals, so:
x <- format(round(x, 9), scientific = FALSE)
} else {
x <- as.character(unlist(x))
}
if (isTRUE(na.rm)) {
x <- x[!is.na(x)]
}
x <- trimws2(x)
x[x == ""] <- NA
if (is.null(x.bak)) {
x.bak <- x
}
# comma to period
x <- gsub(",", ".", x, fixed = TRUE)
# transform scientific notation
x[x %like% "[-]?[0-9]+([.][0-9]+)?e[-]?[0-9]+"] <- as.double(x[x %like% "[-]?[0-9]+([.][0-9]+)?e[-]?[0-9]+"])
# transform Unicode for >= and <=
x <- gsub("\u2264", "<=", x, fixed = TRUE)
x <- gsub("\u2265", ">=", x, fixed = TRUE)
# remove other invalid characters
x <- gsub("[^a-zA-Z0-9.><= ]+", "", x, perl = TRUE)
# remove space between operator and number ("<= 0.002" -> "<=0.002")
x <- gsub("(<|=|>) +", "\\1", x, perl = TRUE)
# transform => to >= and =< to <=
x <- gsub("=<", "<=", x, fixed = TRUE)
x <- gsub("=>", ">=", x, fixed = TRUE)
# dots without a leading zero must start with 0
x <- gsub("([^0-9]|^)[.]", "\\10.", x, perl = TRUE)
# values like "<=0.2560.512" should be 0.512
x <- gsub(".*[.].*[.]", "0.", x, perl = TRUE)
# remove ending .0
x <- gsub("[.]+0$", "", x, perl = TRUE)
# remove all after last digit
x <- gsub("[^0-9]+$", "", x, perl = TRUE)
# keep only one zero before dot
x <- gsub("0+[.]", "0.", x, perl = TRUE)
# starting 00 is probably 0.0 if there's no dot yet
x[x %unlike% "[.]"] <- gsub("^00", "0.0", x[!x %like% "[.]"])
# remove last zeroes
x <- gsub("([.].?)0+$", "\\1", x, perl = TRUE)
x <- gsub("(.*[.])0+$", "\\10", x, perl = TRUE)
# remove ending .0 again
x[x %like% "[.]"] <- gsub("0+$", "", x[x %like% "[.]"])
# never end with dot
x <- gsub("[.]$", "", x, perl = TRUE)
# trim it
x <- trimws2(x)
## previously unempty values now empty - should return a warning later on
x[x.bak != "" & x == ""] <- "invalid"
na_before <- x[is.na(x) | x == ""] %pm>% length()
x[!x %in% VALID_MIC_LEVELS] <- NA
na_after <- x[is.na(x) | x == ""] %pm>% length()
if (na_before != na_after) {
list_missing <- x.bak[is.na(x) & !is.na(x.bak) & x.bak != ""] %pm>%
unique() %pm>%
sort() %pm>%
vector_and(quotes = TRUE)
cur_col <- get_current_column()
warning_("in `as.mic()`: ", na_after - na_before, " result",
ifelse(na_after - na_before > 1, "s", ""),
ifelse(is.null(cur_col), "", paste0(" in column '", cur_col, "'")),
" truncated (",
round(((na_after - na_before) / length(x)) * 100),
"%) that were invalid MICs: ",
list_missing,
call = FALSE
)
}
if (keep_operators == "none" && !all(is.na(x))) {
x <- gsub("[>=<]", "", x)
} else if (keep_operators == "edges" && !all(is.na(x))) {
dbls <- as.double(gsub("[>=<]", "", x))
x[dbls == min(dbls, na.rm = TRUE)] <- paste0("<=", min(dbls, na.rm = TRUE))
x[dbls == max(dbls, na.rm = TRUE)] <- paste0(">=", max(dbls, na.rm = TRUE))
keep <- x[dbls == max(dbls, na.rm = TRUE) | dbls == min(dbls, na.rm = TRUE)]
x[!x %in% keep] <- gsub("[>=<]", "", x[!x %in% keep])
}
set_clean_class(factor(x, levels = VALID_MIC_LEVELS, ordered = TRUE),
new_class = c("mic", "ordered", "factor")
x <- as.character(unlist(x))
}
if (isTRUE(na.rm)) {
x <- x[!is.na(x)]
}
x <- trimws2(x)
x[x == ""] <- NA
if (is.null(x.bak)) {
x.bak <- x
}
# comma to period
x <- gsub(",", ".", x, fixed = TRUE)
# transform scientific notation
x[x %like% "[-]?[0-9]+([.][0-9]+)?e[-]?[0-9]+"] <- as.double(x[x %like% "[-]?[0-9]+([.][0-9]+)?e[-]?[0-9]+"])
# transform Unicode for >= and <=
x <- gsub("\u2264", "<=", x, fixed = TRUE)
x <- gsub("\u2265", ">=", x, fixed = TRUE)
# remove other invalid characters
x <- gsub("[^a-zA-Z0-9.><= ]+", "", x, perl = TRUE)
# remove space between operator and number ("<= 0.002" -> "<=0.002")
x <- gsub("(<|=|>) +", "\\1", x, perl = TRUE)
# transform => to >= and =< to <=
x <- gsub("=<", "<=", x, fixed = TRUE)
x <- gsub("=>", ">=", x, fixed = TRUE)
# dots without a leading zero must start with 0
x <- gsub("([^0-9]|^)[.]", "\\10.", x, perl = TRUE)
# values like "<=0.2560.512" should be 0.512
x <- gsub(".*[.].*[.]", "0.", x, perl = TRUE)
# remove ending .0
x <- gsub("[.]+0$", "", x, perl = TRUE)
# remove all after last digit
x <- gsub("[^0-9]+$", "", x, perl = TRUE)
# keep only one zero before dot
x <- gsub("0+[.]", "0.", x, perl = TRUE)
# starting 00 is probably 0.0 if there's no dot yet
x[x %unlike% "[.]"] <- gsub("^00", "0.0", x[!x %like% "[.]"])
# remove last zeroes
x <- gsub("([.].?)0+$", "\\1", x, perl = TRUE)
x <- gsub("(.*[.])0+$", "\\10", x, perl = TRUE)
# remove ending .0 again
x[x %like% "[.]"] <- gsub("0+$", "", x[x %like% "[.]"])
# never end with dot
x <- gsub("[.]$", "", x, perl = TRUE)
# trim it
x <- trimws2(x)
## previously unempty values now empty - should return a warning later on
x[x.bak != "" & x == ""] <- "invalid"
na_before <- x[is.na(x) | x == ""] %pm>% length()
x[!x %in% VALID_MIC_LEVELS] <- NA
na_after <- x[is.na(x) | x == ""] %pm>% length()
if (na_before != na_after) {
list_missing <- x.bak[is.na(x) & !is.na(x.bak) & x.bak != ""] %pm>%
unique() %pm>%
sort() %pm>%
vector_and(quotes = TRUE)
cur_col <- get_current_column()
warning_("in `as.mic()`: ", na_after - na_before, " result",
ifelse(na_after - na_before > 1, "s", ""),
ifelse(is.null(cur_col), "", paste0(" in column '", cur_col, "'")),
" truncated (",
round(((na_after - na_before) / length(x)) * 100),
"%) that were invalid MICs: ",
list_missing,
call = FALSE
)
}
if (keep_operators == "none" && !all(is.na(x))) {
x <- gsub("[>=<]", "", x)
} else if (keep_operators == "edges" && !all(is.na(x))) {
dbls <- as.double(gsub("[>=<]", "", x))
x[dbls == min(dbls, na.rm = TRUE)] <- paste0("<=", min(dbls, na.rm = TRUE))
x[dbls == max(dbls, na.rm = TRUE)] <- paste0(">=", max(dbls, na.rm = TRUE))
keep <- x[dbls == max(dbls, na.rm = TRUE) | dbls == min(dbls, na.rm = TRUE)]
x[!x %in% keep] <- gsub("[>=<]", "", x[!x %in% keep])
}
set_clean_class(factor(x, levels = VALID_MIC_LEVELS, ordered = TRUE),
new_class = c("mic", "ordered", "factor"))
}
#' @rdname as.mic
@ -375,7 +379,7 @@ type_sum.mic <- function(x, ...) {
#' @noRd
print.mic <- function(x, ...) {
cat("Class 'mic'",
ifelse(length(levels(x)) < length(VALID_MIC_LEVELS), font_red(" with dropped levels"), ""),
ifelse(!identical(levels(x), VALID_MIC_LEVELS), font_red(" with outdated structure - convert with `as.mic()` to update"), ""),
"\n",
sep = ""
)
@ -533,12 +537,23 @@ Math.mic <- function(x, ...) {
#' @export
Ops.mic <- function(e1, e2) {
e1_chr <- as.character(e1)
e2_chr <- character(0)
e1 <- as.double(e1)
if (!missing(e2)) {
# when e1 is `!`, e2 is missing
e2_chr <- as.character(e2)
e2 <- as.double(e2)
}
# set class to numeric, because otherwise NextMethod will be factor (since mic is a factor)
if (as.character(.Generic) %in% c("<", "<=", "==", "!=", ">", ">=")) {
# make sure that <0.002 is lower than 0.002
# and that >32 is higher than 32, but equal to >=32
e1[e1_chr %like% "<" & e1_chr %unlike% "="] <- e1[e1_chr %like% "<" & e1_chr %unlike% "="] - 0.000001
e1[e1_chr %like% ">" & e1_chr %unlike% "="] <- e1[e1_chr %like% ">" & e1_chr %unlike% "="] + 0.000001
e2[e2_chr %like% "<" & e2_chr %unlike% "="] <- e2[e2_chr %like% "<" & e2_chr %unlike% "="] - 0.000001
e2[e2_chr %like% ">" & e2_chr %unlike% "="] <- e2[e2_chr %like% ">" & e2_chr %unlike% "="] + 0.000001
}
# set .Class to numeric, because otherwise NextMethod will be factor (since mic is a factor)
.Class <- class(e1)
NextMethod(.Generic)
}

View File

@ -294,7 +294,7 @@ sir_confidence_interval <- function(...,
# this applies the Clopper-Pearson method
out <- stats::binom.test(x = x, n = n, conf.level = confidence_level)$conf.int
out <- set_clean_class(out, "double")
out <- set_clean_class(out, "numeric")
if (side %in% c("left", "l", "lower", "lowest", "less", "min")) {
out <- out[1]

View File

@ -29,7 +29,7 @@
#' Translate MIC and Disk Diffusion to SIR, or Clean Existing SIR Data
#'
#' @description Clean up existing SIR values, or interpret minimum inhibitory concentration (MIC) values and disk diffusion diameters according to EUCAST or CLSI. [as.sir()] transforms the input to a new class [`sir`], which is an ordered [factor] with levels `S < I < R`.
#' @description Clean up existing SIR values, or interpret minimum inhibitory concentration (MIC) values and disk diffusion diameters according to EUCAST or CLSI. [as.sir()] transforms the input to a new class [`sir`], which is an ordered [factor].
#'
#' Currently breakpoints are available:
#' - For **clinical microbiology** from EUCAST `r min(as.integer(gsub("[^0-9]", "", subset(AMR::clinical_breakpoints, guideline %like% "EUCAST" & type == "human")$guideline)))`-`r max(as.integer(gsub("[^0-9]", "", subset(AMR::clinical_breakpoints, guideline %like% "EUCAST" & type == "human")$guideline)))` and CLSI `r min(as.integer(gsub("[^0-9]", "", subset(AMR::clinical_breakpoints, guideline %like% "CLSI" & type == "human")$guideline)))`-`r max(as.integer(gsub("[^0-9]", "", subset(AMR::clinical_breakpoints, guideline %like% "CLSI" & type == "human")$guideline)))`;
@ -326,7 +326,7 @@ as.sir.default <- function(x, ...) {
x.bak <- x
x <- as.character(x) # this is needed to prevent the vctrs pkg from throwing an error
if (inherits(x.bak, c("integer", "numeric", "double")) && all(x %in% c(1:3, NA))) {
if (inherits(x.bak, c("numeric", "integer")) && all(x %in% c(1:3, NA))) {
# support haven package for importing e.g., from SPSS - it adds the 'labels' attribute
lbls <- attributes(x.bak)$labels
if (!is.null(lbls) && all(c("S", "I", "R") %in% names(lbls)) && all(c(1:3) %in% lbls)) {

View File

@ -102,6 +102,12 @@ vec_cast.mo.character <- function(x, to, ...) {
}
# S3: disk ----
vec_ptype_full.disk <- function(x, ...) {
"disk"
}
vec_ptype_abbr.disk <- function(x, ...) {
"dsk"
}
vec_ptype2.disk.default <- function (x, y, ..., x_arg = "", y_arg = "") {
x
}
@ -129,10 +135,16 @@ vec_cast.disk.character <- function(x, to, ...) {
# S3: mic ----
vec_ptype2.mic.default <- function (x, y, ..., x_arg = "", y_arg = "") {
x
# this will make sure that currently implemented MIC levels are returned
as.mic(x)
}
vec_ptype2.mic.mic <- function(x, y, ...) {
x
# this will make sure that currently implemented MIC levels are returned
as.mic(x)
}
vec_cast.mic.mic <- function(x, to, ...) {
# this will make sure that currently implemented MIC levels are returned
as.mic(x)
}
vec_cast.character.mic <- function(x, to, ...) {
as.character(x)

View File

@ -106,7 +106,6 @@ if (pkg_is_available("cli")) {
s3_register("pillar::type_sum", "mo")
s3_register("pillar::type_sum", "sir")
s3_register("pillar::type_sum", "mic")
s3_register("pillar::type_sum", "disk")
# Support for frequency tables from the cleaner package
s3_register("cleaner::freq", "mo")
s3_register("cleaner::freq", "sir")
@ -156,6 +155,8 @@ if (pkg_is_available("cli")) {
s3_register("vctrs::vec_cast", "character.mo")
s3_register("vctrs::vec_cast", "mo.character")
# S3: disk
s3_register("vctrs::vec_ptype_full", "disk") # returns "disk"
s3_register("vctrs::vec_ptype_abbr", "disk") # returns "dsk"
s3_register("vctrs::vec_ptype2", "disk.default")
s3_register("vctrs::vec_ptype2", "disk.disk")
s3_register("vctrs::vec_cast", "integer.disk")
@ -173,6 +174,7 @@ if (pkg_is_available("cli")) {
s3_register("vctrs::vec_cast", "mic.character")
s3_register("vctrs::vec_cast", "mic.double")
s3_register("vctrs::vec_cast", "mic.integer")
s3_register("vctrs::vec_cast", "mic.mic")
s3_register("vctrs::vec_math", "mic")
s3_register("vctrs::vec_arith", "mic")
# S3: sir

View File

@ -2,7 +2,7 @@
# For editing this EUCAST reference file, these values can all be used for targeting antibiotics:
# aminoglycosides, aminopenicillins, antifungals, antimycobacterials, betalactams, carbapenems, cephalosporins, cephalosporins_1st, cephalosporins_2nd, cephalosporins_3rd, cephalosporins_4th, cephalosporins_5th, cephalosporins_except_CAZ, fluoroquinolones, glycopeptides, glycopeptides_except_lipo, lincosamides, lipoglycopeptides, macrolides, oxazolidinones, penicillins, polymyxins, quinolones, streptogramins, tetracyclines, tetracyclines_except_TGC, trimethoprims, ureidopenicillins
# and all separate EARS-Net letter codes such as AMC. They can be separated by comma: 'AMC, fluoroquinolones'.
# The 'if_mo_property' column can be any column name from the AMR::microorganisms data set, or "genus_species" or "gramstain".
# The 'if_mo_property' column can be any column name from the AMR::microorganisms data set, or 'genus_species' or 'gramstain'.
# The like.is.one_of column must be 'like' or 'is' or 'one_of' ('like' will read the 'this_value' column as regular expression)
# The EUCAST guideline contains references to the 'Burkholderia cepacia complex'. All species in this group are noted on the 'B.cepacia' sheet of the EUCAST Clinical Breakpoint v.10.0 Excel file of 2020 and later
# >>>>> IF YOU WANT TO IMPORT THIS FILE INTO YOUR OWN SOFTWARE, HAVE THE FIRST 9 LINES SKIPPED <<<<<

Can't render this file because it contains an unexpected character in line 5 and column 96.

View File

@ -142,10 +142,35 @@ suppressWarnings(expect_identical(el1^el2, el1_double^el2_double))
suppressWarnings(expect_identical(el1 %% el2, el1_double %% el2_double))
suppressWarnings(expect_identical(el1 %/% el2, el1_double %/% el2_double))
suppressWarnings(expect_identical(el1 & el2, el1_double & el2_double))
suppressWarnings(expect_identical(el1 | el2, el1_double | el2_double))
suppressWarnings(expect_identical(el1 == el2, el1_double == el2_double))
suppressWarnings(expect_identical(el1 != el2, el1_double != el2_double))
suppressWarnings(expect_identical(el1 < el2, el1_double < el2_double))
suppressWarnings(expect_identical(el1 <= el2, el1_double <= el2_double))
suppressWarnings(expect_identical(el1 >= el2, el1_double >= el2_double))
suppressWarnings(expect_identical(el1 > el2, el1_double > el2_double))
# for comparison operators, be more strict:
expect_true(as.mic(">32") > as.mic(32))
expect_true(as.mic(">32") >= as.mic(32))
expect_true(as.mic(">32") >= as.mic("<32"))
expect_true(as.mic(">32") >= as.mic("<=32"))
expect_true(as.mic(">32") > as.mic("<=32"))
expect_false(as.mic("32") > as.mic(32))
expect_true(as.mic("32") >= as.mic(32))
expect_true(as.mic("32") >= as.mic("<32"))
expect_true(as.mic("32") >= as.mic("<=32"))
expect_false(as.mic("32") > as.mic("<=32"))
expect_true(as.mic("32") == as.mic(32))
expect_true(as.mic("32") == as.mic(32))
expect_false(as.mic("32") == as.mic("<32"))
expect_true(as.mic("32") == as.mic("<=32"))
expect_true(as.mic("32") == as.mic("<=32"))
expect_false(as.mic(">32") < as.mic(32))
expect_false(as.mic(">32") <= as.mic(32))
expect_false(as.mic(">32") <= as.mic("<32"))
expect_false(as.mic(">32") <= as.mic("<=32"))
expect_false(as.mic(">32") < as.mic("<=32"))
expect_false(as.mic("32") < as.mic(32))
expect_true(as.mic("32") <= as.mic(32))
expect_false(as.mic("32") <= as.mic("<32"))
expect_true(as.mic("32") <= as.mic("<=32"))
expect_false(as.mic("32") < as.mic("<=32"))

View File

@ -115,7 +115,7 @@ sir_interpretation_history(clean = FALSE)
Ordered \link{factor} with new class \code{sir}
}
\description{
Clean up existing SIR values, or interpret minimum inhibitory concentration (MIC) values and disk diffusion diameters according to EUCAST or CLSI. \code{\link[=as.sir]{as.sir()}} transforms the input to a new class \code{\link{sir}}, which is an ordered \link{factor} with levels \verb{S < I < R}.
Clean up existing SIR values, or interpret minimum inhibitory concentration (MIC) values and disk diffusion diameters according to EUCAST or CLSI. \code{\link[=as.sir]{as.sir()}} transforms the input to a new class \code{\link{sir}}, which is an ordered \link{factor}.
Currently breakpoints are available:
\itemize{