1
0
mirror of https://github.com/msberends/AMR.git synced 2026-06-30 13:06:23 +02:00

31 Commits

Author SHA1 Message Date
e23d7b4c45 (v3.0.1.9081) fix mo 2026-06-27 15:33:08 +02:00
Matthijs Berends
518425311e (v3.0.1.9080) fix(as.mo): resolve abbreviated genus when species has subspecies (#288 follow-up) (#301)
When a genus+species abbreviation like "P. ovale" was used, the previous
bypass (Issue #288) checked sum(sp_exact) == 1, which failed if the species
also had subspecies sharing the epithet (ovale curtisi, ovale wallikeri).
The fix extends the bypass to fire whenever all exact species matches belong
to one genus, collapsing to the species-rank record (subspecies == "") for
genus+species queries and preserving the chosen row for explicit subspecies
queries.

Also extends the data-invariant test to cover all taxonomic rank columns
from domain to subspecies, not just the terminal three.


Claude-Session: https://claude.ai/code/session_01M4fqQYQYJ3drdudkDYNqAY

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-27 15:20:38 +02:00
Matthijs Berends
03be4b87fc (v3.0.1.9079) docs: rename remaining 'antibiotic selectors'/'AB selectors' to 'antimicrobial selectors'/'AMR selectors'
* docs: rename remaining 'antibiotic selectors'/'AB selectors' to 'antimicrobial selectors'/'AMR selectors'

Five leftover occurrences of the old terminology updated in the AMR
vignette (section heading + code comment), the amr_selectors test file,
and two roxygen example comments in amr_selectors.R. The auto-generated
man/antimicrobial_selectors.Rd will update on the next devtools::document() run.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0169LucQ7SHDLHdTnDs1ENhd

* fix

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-06-27 14:31:58 +02:00
Matthijs Berends
12cabca29d (v3.0.1.9076) fix: normalise year-qualified AMR_guideline option in all affected functions 2026-06-27 13:53:25 +02:00
Matthijs Berends
f7d353361c improve top_n_microorganisms(): add property_for_each, fix property=NULL, enforce rank order (#297) 2026-06-26 21:40:11 +02:00
Matthijs Berends
02bd9a71c1 (v3.0.1.9076) document Python installation channels and enforce_method in vignette (#296) 2026-06-26 15:03:07 +02:00
5f6372342e (v3.0.1.9075) another go 2026-06-26 12:06:12 +02:00
3c17679382 (v3.0.1.9074) You go girl 2026-06-26 11:58:38 +02:00
4ca7fdf3d4 (v3.0.1.9073) yet another go 2026-06-26 11:51:08 +02:00
6edae2037a (v3.0.1.9072) fix py again 2026-06-26 11:47:11 +02:00
61e1fbf1e0 (v3.0.1.9071) fix python wrapper 2026-06-26 11:35:30 +02:00
637ada920b (v3.0.1.9070) Fix #293, update EUCAST rules for PEF 2026-06-26 08:56:59 +02:00
4fac683fac (v3.0.1.9069) fix Python wrapper 2026-06-24 20:27:39 +02:00
b6c1c26a5d (v3.0.1.9068) todo tracker update 2026-06-24 20:01:25 +02:00
935071ae01 (v3.0.1.9067) todo tracker update 2026-06-24 19:43:02 +02:00
a88150ca4a (v3.0.1.9066) fix documentation 2026-06-24 19:34:47 +02:00
39b6a250de (v3.0.1.9065) fix example isolates data 2026-06-24 18:36:47 +02:00
bd63794136 (v3.0.1.9064) Documentation updates 2026-06-24 18:32:08 +02:00
c7b17e5833 (v3.0.1.9063) fix old R version 2026-06-24 10:22:56 +02:00
ab38fb911d (v3.0.1.9062) Fix older R versions 2026-06-24 10:19:15 +02:00
0c1709c999 (v3.0.1.9061) WISCA refix 2026-06-23 19:43:54 +02:00
fd90b2c250 (v3.0.1.9060) WISCA fix 2026-06-23 19:27:40 +02:00
9898b5df4b (v3.0.1.9059) Fix WISCA in vignette 2026-06-23 14:38:59 +02:00
Matthijs Berends
3f9f931777 (v3.0.1.9059) Update taxonomy of microorganisms 2026-06-23 01:38:13 +02:00
0af3f84655 (v3.0.1.9057) website fix 2026-05-02 14:56:26 +02:00
Matthijs Berends
64753e9a16 Revamp beta NEWS.md section: concise, user-focused, telegram style (#284)
* Revamp beta NEWS.md section: concise, user-focused, telegram style

https://claude.ai/code/session_01CnwgKsQqnKNdSD2KjwFAdH

* Loosen NEWS.md telegram style: add context words for clarity

https://claude.ai/code/session_01CnwgKsQqnKNdSD2KjwFAdH

* Add issue/PR references to beta NEWS.md entries

https://claude.ai/code/session_01CnwgKsQqnKNdSD2KjwFAdH

* Add antimicrobial codes to new antimicrobials list in NEWS.md

https://claude.ai/code/session_01CnwgKsQqnKNdSD2KjwFAdH

* Replace em dashes with natural English punctuation in NEWS.md

https://claude.ai/code/session_01CnwgKsQqnKNdSD2KjwFAdH

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-02 10:31:33 +01:00
Matthijs Berends
24f24ecaf8 Generalise interpretive rules for multi-guideline support (#268) (#283)
* Generalise interpretive rules for multi-guideline support (#268)

- Rename data-raw/eucast_rules.tsv → interpretive_rules.tsv; add rule.provider
  column (value: "EUCAST") to distinguish future CLSI rows
- Rename EUCAST_RULES_DF → INTERPRETIVE_RULES_DF in _pre_commit_checks.R;
  filter by rule.provider == guideline when applying rules in interpretive_rules()
- Rename custom_eucast_rules() → custom_interpretive_rules() with new S3 class
  "custom_interpretive_rules"; old function becomes a deprecated wrapper in
  zz_deprecated.R; backward-compat S3 dispatch shims added for old class
- Remove stop_if(guideline == "CLSI", ...) so clsi_rules() no longer errors
- Add .onLoad shim in zzz.R to create INTERPRETIVE_RULES_DF from EUCAST_RULES_DF
  for transitional compatibility until sysdata.rda is regenerated

https://claude.ai/code/session_01D46BTsfJSPo3HnLWp3PRkP

* Fix namespace load failure: remove assignInNamespace from .onLoad (#268)

assignInNamespace cannot add NEW bindings to a locked package namespace
(R locks namespace bindings before .onLoad runs). Replace the .onLoad
shim with a runtime fallback inside interpretive_rules(): if
INTERPRETIVE_RULES_DF is absent (pre-regeneration sysdata.rda), derive
it from EUCAST_RULES_DF by adding the rule.provider column. This also
fixes the screening_abx line to reuse the already-resolved
interpretive_rules_df_total instead of a bare INTERPRETIVE_RULES_DF
reference.

https://claude.ai/code/session_01D46BTsfJSPo3HnLWp3PRkP

* fixes

* fixes

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-01 18:38:51 +01:00
Matthijs Berends
f7e9294bea Add parallel computing support to antibiogram() and wisca() (#281) (#282)
* Add parallel computing support to antibiogram() and wisca() (#281)

For WISCA: simulations are distributed across (group, chunk) job pairs
via future.apply::future_lapply(), keeping all workers active even when
the regimen count is smaller than nbrOfWorkers(). Sequential fallback
with progress ticker is preserved when parallel = FALSE or workers = 1.

For grouped antibiograms: each group is processed by a separate worker,
mirroring the row-batch approach in as.sir().

Same gate pattern as as.sir() (PR #280): requires a non-sequential
future::plan() to be active; auto-upgrades to parallel = TRUE when a
parallel plan is detected; throws an informative error otherwise.

https://claude.ai/code/session_01FC43syPbzhGmKgrrVNHjnF

* Fix version to 3.0.1.9055 and update CLAUDE.md version formula

Uses origin/${defaultbranch} (with a fetch) instead of the local
branch ref so the commit count is never stale after a merge.

https://claude.ai/code/session_01FC43syPbzhGmKgrrVNHjnF

* Fix non-ASCII characters in antibiogram.R

Replace en/em dashes and non-breaking spaces with ASCII equivalents
to satisfy R CMD check portability requirement.

https://claude.ai/code/session_01FC43syPbzhGmKgrrVNHjnF

* Update auto-generated Rd files after documentation rebuild

https://claude.ai/code/session_01FC43syPbzhGmKgrrVNHjnF

* Move parallel gate to top of antibiogram.default() like sir.R

The gate was inside the wisca==TRUE block, so parallel=TRUE with a
sequential plan was silently ignored for non-WISCA antibiograms.
Now the gate runs unconditionally at the top of the function,
identical to the as.sir() pattern: error on explicit parallel=TRUE
with sequential plan, auto-upgrade when a non-sequential plan is
already active.

https://claude.ai/code/session_01FC43syPbzhGmKgrrVNHjnF

* Fix parallel WISCA returning all NA; strengthen tests; add sequential hint

Bug: lapply() over a factor yields length-1 factor elements (integer
codes), while for() over a factor yields character strings.  The job
list stored j\$group as a factor integer, but the reassembly loop
compared it with identical(j\$group, g) where g was character -- always
FALSE, so no simulation chunks were ever assembled and coverage stayed
NA throughout.

Fix: convert unique_groups to character before building jobs so both
the job list and the reassembly loop use the same type.

Tests: replaced na.rm = TRUE guards with explicit anyNA() checks so the
test suite would have caught the all-NA result immediately.

Also adds a sequential-mode performance hint (analogous to sir.R
lines 1116-1127) when simulations >= 500 and >= 3 regimens.

https://claude.ai/code/session_01FC43syPbzhGmKgrrVNHjnF

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-30 18:41:56 +01:00
Matthijs Berends
23beebc6c3 Migrate parallel computing in as.sir() from parallel:: to future/future.apply (#280)
* Migrate parallel computing in as.sir() from parallel:: to future/future.apply

Replace parallel::mclapply() and parallel::parLapply() with
future.apply::future_lapply(), enabling transparent support for any
future backend (multisession, multicore, mirai_multisession, cluster)
on all platforms including Windows.

When parallel = TRUE the function now: (1) respects an active
future::plan() set by the user without overriding it on exit, or
(2) sets a temporary multisession plan with parallelly::availableCores()
and tears it down on exit. The max_cores argument controls worker count
only when no user plan is active.

future and future.apply are added to Suggests in DESCRIPTION.

https://claude.ai/code/session_01M1Jvf2Miu6JL4TQrEh1wS8

* Require user plan() for parallel=TRUE; fix as_wt_nwt false-positive warnings

- parallel = TRUE now errors with a cli-styled message if no non-sequential
  future::plan() is active; users must call e.g. future::plan(future::multisession)
  before using parallel = TRUE (breaking change)
- Removed auto-setup/teardown of multisession plan inside as.sir(), which was
  slow and caused version-mismatch issues with load_all() workflows
- Added as_wt_nwt to the exclusion list in as_sir_method() to suppress
  false-positive "no longer used" warnings during parallel runs
- Fixed pieces_per_col row-batch calculation to use n_workers (total available
  workers from the active plan) instead of n_cores (workers clipped to n_cols),
  so row-batch mode activates correctly when n_cols < n_workers
- Updated @param parallel and @param max_cores roxygen docs; regenerated man/as.sir.Rd
- Updated sequential-mode hint to instruct users to set plan() first

https://claude.ai/code/session_01M1Jvf2Miu6JL4TQrEh1wS8

* fix parallel

* fix parallel

* unit tests

* unit tedts

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-04-30 08:57:19 +01:00
3f1b20c304 (v3.0.1.9052) fix NEWS 2026-04-25 16:21:31 +02:00
905dea2cf1 (v3.0.1.9051) fix NEWS 2026-04-25 16:20:34 +02:00
176 changed files with 126135 additions and 97186 deletions

View File

@@ -49,7 +49,7 @@ jobs:
# Test all old versions of R >= 3.0, we support them all!
# For these old versions, dependencies and vignettes will not be checked.
# For recent R versions, see check-recent.yaml (r-lib and tidyverse support the latest 5 major R releases).
- {os: ubuntu-latest, r: '3.6', allowfail: false}
# - {os: ubuntu-latest, r: '3.6', allowfail: false}
# - {os: windows-latest, r: '3.5', allowfail: false} # always fails, horrible with UTF-8
# - {os: ubuntu-latest, r: '3.4', allowfail: false} # 3.1-3.4 now always fails with Error in grep(warn_re, lines, invert = TRUE, value = TRUE) attempt to set index 46/46 in SET_STRING_ELT
# - {os: ubuntu-latest, r: '3.3', allowfail: false}

View File

@@ -29,7 +29,6 @@
on:
push:
# only on main
branches: "main"
name: Update TODO Tracker
@@ -40,40 +39,228 @@ jobs:
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # full history required for git blame
- name: Generate TODO list from R/
- name: Generate TODO report
env:
GH_TOKEN: ${{ secrets.GH_REPO_SCOPE }}
run: |
set -euo pipefail
export TZ=Europe/Amsterdam
last_updated=$(date +"%e %B %Y %H:%M:%S %Z" | sed 's/^ *//')
echo "## \`TODO\` Report" > todo.md
echo "" >> todo.md
echo "**Last Updated: ${last_updated}**" >> todo.md
echo "" >> todo.md
echo "_This overview is automatically updated on each push to \`main\`. It provides an automated overview of all mentions of the text \`TODO\`._" >> todo.md
echo "" >> todo.md
todos=$(grep -rn --include=\*.{R,Rmd,yaml,yml,md,css,js} --exclude={todo-tracker.yml,todo.md} "TODO" . || true)
if [ -z "$todos" ]; then
echo "✅ No TODOs found." >> todo.md
else
echo "$todos" | awk -F: -v repo="https://github.com/msberends/AMR/blob/main/" '
{
file = $1
gsub("^\\./", "", file) # remove leading ./ if present
line = $2
text = substr($0, index($0,$3))
if (file != last_file) {
if (last_file != "") print "```"
print ""
print "### [`" file "`](" repo file ")"
print "```r"
last_file = file
}
printf "L%s: %s\n", line, text
}
' >> todo.md
echo "\`\`\`" >> todo.md
REPO="msberends/AMR"
REPO_URL="https://github.com/$REPO/blob/main"
NOW=$(date +%s)
LAST_UPDATED=$(date +"%e %B %Y %H:%M:%S %Z" | sed 's/^ *//')
STALE_DAYS=180
# ── helper: human-readable age ──────────────────────────────
format_age() {
local d=$1
if [ "$d" -lt 0 ] 2>/dev/null; then echo "unknown"; return; fi
local y=$((d / 365)) m=$(( (d % 365) / 30 ))
if [ "$y" -gt 0 ] && [ "$m" -gt 0 ]; then echo "${y}y ${m}m"
elif [ "$y" -gt 0 ]; then echo "${y}y"
elif [ "$m" -gt 0 ]; then echo "${m}m"
else echo "${d}d"
fi
}
export -f format_age
# ── step 1: find all markers ────────────────────────────────
grep -rn \
--include='*.R' --include='*.Rmd' --include='*.yaml' \
--include='*.yml' --include='*.md' --include='*.css' \
--include='*.js' \
--exclude='todo-tracker.yml' --exclude='todo.md' \
-E '\b(TODO|FIXME|HACK|XXX)\b' . > /tmp/raw.txt || true
if [ ! -s /tmp/raw.txt ]; then
echo -e "## \`TODO\` Report\n\n**Last Updated: ${LAST_UPDATED}**\n\nNo markers found." > todo.md
exit 0
fi
# ── step 2: enrich with git blame & extract issue refs ──────
> /tmp/enriched.tsv
> /tmp/issues_seen.txt
while IFS= read -r match; do
clean=$(printf '%s\n' "$match" | sed 's|^\./||')
file=$(printf '%s\n' "$clean" | cut -d: -f1)
lineno=$(printf '%s\n' "$clean" | cut -d: -f2)
text=$(printf '%s\n' "$clean" | cut -d: -f3-)
# determine marker type (first match wins, TODO is default)
marker="TODO"
for m in FIXME HACK XXX; do
if printf '%s\n' "$text" | grep -qw "$m"; then marker="$m"; break; fi
done
# git blame timestamp
blame_ts=$(git blame -L "${lineno},${lineno}" --porcelain -- "$file" 2>/dev/null \
| awk '/^author-time/{print $2}' || echo "0")
blame_ts=${blame_ts:-0}
if [ "$blame_ts" -gt 0 ] 2>/dev/null; then
age_days=$(( (NOW - blame_ts) / 86400 ))
else
age_days=-1
fi
# extract issue references (#NNN)
issues=$(printf '%s\n' "$text" | grep -oE '#[0-9]+' | sed 's/#//' | tr '\n' ',' | sed 's/,$//' || true)
if [ -n "$issues" ]; then
for inum in $(echo "$issues" | tr ',' ' '); do
echo "$inum" >> /tmp/issues_seen.txt
done
fi
printf '%s\t%s\t%s\t%s\t%s\t%s\n' \
"$file" "$lineno" "$marker" "$age_days" "$issues" "$text" >> /tmp/enriched.tsv
done < /tmp/raw.txt
# ── step 3: query GitHub API for referenced issues ──────────
> /tmp/issue_info.tsv
if [ -s /tmp/issues_seen.txt ]; then
sort -un /tmp/issues_seen.txt | while read -r inum; do
info=$(gh api "/repos/$REPO/issues/$inum" \
--jq '"\(.state)\t\(.title)"' 2>/dev/null \
|| echo "unknown (could not fetch)")
printf '%s\t%s\n' "$inum" "$info" >> /tmp/issue_info.tsv
done
fi
# ── step 4: build the report ────────────────────────────────
{
# ── header ──
echo "## \`TODO\` Report"
echo ""
echo "**Last Updated: ${LAST_UPDATED}**"
echo ""
echo "_This overview is automatically updated on each push to \`main\`. It scans for \`TODO\`, \`FIXME\`, \`HACK\`, and \`XXX\` markers across the codebase._"
echo ""
# ── summary table ──
total=$(wc -l < /tmp/enriched.tsv | tr -d ' ')
files_affected=$(awk -F'\t' '{print $1}' /tmp/enriched.tsv | sort -u | wc -l | tr -d ' ')
todo_n=$(awk -F'\t' '$3=="TODO"' /tmp/enriched.tsv | wc -l | tr -d ' ')
fixme_n=$(awk -F'\t' '$3=="FIXME"' /tmp/enriched.tsv | wc -l | tr -d ' ')
hack_n=$(awk -F'\t' '$3=="HACK"' /tmp/enriched.tsv | wc -l | tr -d ' ')
xxx_n=$(awk -F'\t' '$3=="XXX"' /tmp/enriched.tsv | wc -l | tr -d ' ')
stale_n=$(awk -F'\t' -v s="$STALE_DAYS" '$4 > s' /tmp/enriched.tsv | wc -l | tr -d ' ')
linked_n=$(awk -F'\t' '$5 != ""' /tmp/enriched.tsv | wc -l | tr -d ' ')
unlinked_n=$(awk -F'\t' '$5 == ""' /tmp/enriched.tsv | wc -l | tr -d ' ')
# oldest marker
oldest_line=$(awk -F'\t' '$4 >= 0' /tmp/enriched.tsv | sort -t$'\t' -k4 -rn | head -1)
oldest_days=$(echo "$oldest_line" | cut -f4)
oldest_file=$(echo "$oldest_line" | cut -f1)
oldest_lineno=$(echo "$oldest_line" | cut -f2)
oldest_age=$(format_age "$oldest_days")
echo "### Summary"
echo ""
echo "| Metric | Value |"
echo "|:---|---:|"
echo "| Total markers | **${total}** |"
[ "$todo_n" -gt 0 ] && echo "| \`TODO\` | ${todo_n} |"
[ "$fixme_n" -gt 0 ] && echo "| \`FIXME\` | ${fixme_n} |"
[ "$hack_n" -gt 0 ] && echo "| \`HACK\` | ${hack_n} |"
[ "$xxx_n" -gt 0 ] && echo "| \`XXX\` | ${xxx_n} |"
echo "| Files affected | ${files_affected} |"
echo "| Stale (> 6 months) | ${stale_n} |"
echo "| Oldest marker | ${oldest_age}, \`${oldest_file}\` L${oldest_lineno} |"
echo "| Linked to issues | ${linked_n} |"
echo "| Unlinked (no issue ref) | ${unlinked_n} |"
echo ""
# ── by referenced issue ──
if [ -s /tmp/issue_info.tsv ]; then
echo "### By Referenced Issue"
echo ""
has_closed=false
while IFS=$'\t' read -r inum state title; do
count=$(awk -F'\t' -v n="$inum" '$5 ~ "(^|,)"n"(,|$)"' /tmp/enriched.tsv | wc -l | tr -d ' ')
[ "$state" = "closed" ] && has_closed=true
state_icon=""
[ "$state" = "closed" ] && state_icon=" :warning:"
echo "<details><summary><b>#${inum}</b> (${state}): <i>${title}</i> &mdash; ${count} marker(s)${state_icon}</summary>"
echo ""
awk -F'\t' -v n="$inum" '$5 ~ "(^|,)"n"(,|$)"' /tmp/enriched.tsv \
| while IFS=$'\t' read -r f l m d refs txt; do
age_str=$(format_age "$d")
flag=""
[ "$d" -gt "$STALE_DAYS" ] 2>/dev/null && flag=" :warning:"
# re-read the actual source line and trim leading/trailing whitespace
src_text=$(sed -n "${l}p" "$f" 2>/dev/null | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' || true)
echo "- [\`${f}\` L${l}](${REPO_URL}/${f}#L${l}) (${age_str} ago)${flag}"
[ -n "$src_text" ] && echo " \`${src_text}\`"
done
echo ""
echo "</details>"
echo ""
done < /tmp/issue_info.tsv
if [ "$has_closed" = true ]; then
echo "> **Warning:** some markers reference closed issues and may be stale."
echo ""
fi
fi
# ── by file ──
echo "### By File"
echo ""
prev_file=""
prev_lineno=-99
while IFS=$'\t' read -r file lineno marker age_days issues text; do
if [ "$file" != "$prev_file" ]; then
# close previous code block
if [ -n "$prev_file" ]; then
echo '```'
echo ""
fi
file_count=$(awk -F'\t' -v f="$file" '$1==f' /tmp/enriched.tsv | wc -l | tr -d ' ')
echo "#### [\`${file}\`](${REPO_URL}/${file}) &mdash; ${file_count} marker(s)"
echo '```r'
prev_lineno=-99
fi
# blank line between non-sequential lines (visual grouping)
if [ "$file" = "$prev_file" ] && [ $((lineno - prev_lineno)) -gt 1 ]; then
echo ""
fi
age_str=$(format_age "$age_days")
flag=""
[ "$age_days" -gt "$STALE_DAYS" ] 2>/dev/null && flag=" !!"
# re-read the actual source line to avoid TSV round-trip corruption
src_line=$(sed -n "${lineno}p" "$file" 2>/dev/null | sed 's/[[:space:]]*$//' || true)
printf 'L%s: %s ◁ %s ago%s\n' "$lineno" "$src_line" "$age_str" "$flag"
prev_file="$file"
prev_lineno="$lineno"
done < <(sort -t$'\t' -k1,1 -k2,2n /tmp/enriched.tsv)
# close final code block
if [ -n "$prev_file" ]; then
echo '```'
fi
} > todo.md
- name: Update GitHub issue
uses: peter-evans/create-or-update-comment@v4
with:

1
.gitignore vendored
View File

@@ -22,6 +22,7 @@ vignettes/*.R
^CRAN-RELEASE$
packrat/lib*/
packrat/src/
*~$*
data-raw/taxa.txt
data-raw/taxon.tab
data-raw/CLSI*.pdf

View File

@@ -2,7 +2,7 @@ Version: 1.0
ProjectId: 5128c748-a412-44db-a5fb-45c68c93dd10
RestoreWorkspace: No
SaveWorkspace: No
SaveWorkspace: Ask
AlwaysSaveHistory: Yes
EnableCodeIndexing: Yes

View File

@@ -85,6 +85,27 @@ _pkgdown.yml # pkgdown website configuration
- `translate.R` — 28-language translation system
- `ggplot_sir.R` / `ggplot_pca.R` / `plotting.R` — visualisation functions
## Code Style
Follow the [tidyverse style guide](https://style.tidyverse.org/) precisely. Key rules:
- 2-space indentation; no tabs
- `<-` for assignment, not `=`
- Spaces around all binary operators and after commas; no spaces inside parentheses
- When a function call must break across lines, place the first argument on a new line indented by 2 spaces, and put the closing `)` on its own line — **never align arguments to the opening parenthesis** (no hanging/forced mid-line indentation)
```r
# good
stop_(
"some long message part one ",
"part two"
)
# bad — forces indentation to match the opening parenthesis
stop_("some long message part one ",
"part two")
```
## Custom S3 Classes
The package defines five S3 classes with full print/format/plot/vctrs support:
@@ -167,7 +188,8 @@ Then run the following from the repo root to determine the version string to use
currenttag=$(git describe --tags --abbrev=0 | sed 's/v//')
currenttagfull=$(git describe --tags --abbrev=0)
defaultbranch=$(git branch | cut -c 3- | grep -E '^master$|^main$')
currentcommit=$(git rev-list --count ${currenttagfull}..${defaultbranch})
git fetch origin ${defaultbranch} --quiet
currentcommit=$(git rev-list --count ${currenttagfull}..origin/${defaultbranch})
currentversion="${currenttag}.$((currentcommit + 9001 + 1))"
echo "$currentversion"
```

View File

@@ -1,6 +1,6 @@
Package: AMR
Version: 3.0.1.9050
Date: 2026-04-25
Version: 3.0.1.9081
Date: 2026-06-27
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
@@ -44,10 +44,11 @@ Suggests:
curl,
data.table,
dplyr,
future,
future.apply,
ggplot2,
knitr,
openxlsx,
parallelly,
pillar,
progress,
readxl,
@@ -72,4 +73,4 @@ License: GPL-2 | file LICENSE
Encoding: UTF-8
LazyData: true
RoxygenNote: 7.3.3
Roxygen: list(markdown = TRUE, old_usage = TRUE)
Roxygen: list(markdown = TRUE)

View File

@@ -49,6 +49,7 @@ S3method(as.data.frame,mo)
S3method(as.double,mic)
S3method(as.double,sir)
S3method(as.list,custom_eucast_rules)
S3method(as.list,custom_interpretive_rules)
S3method(as.list,custom_mdro_guideline)
S3method(as.list,mic)
S3method(as.matrix,mic)
@@ -66,6 +67,7 @@ S3method(c,ab)
S3method(c,amr_selector)
S3method(c,av)
S3method(c,custom_eucast_rules)
S3method(c,custom_interpretive_rules)
S3method(c,custom_mdro_guideline)
S3method(c,disk)
S3method(c,mic)
@@ -96,6 +98,7 @@ S3method(print,amr_selector)
S3method(print,av)
S3method(print,bug_drug_combinations)
S3method(print,custom_eucast_rules)
S3method(print,custom_interpretive_rules)
S3method(print,custom_mdro_guideline)
S3method(print,deprecated_amr_dataset)
S3method(print,disk)
@@ -228,6 +231,7 @@ export(count_df)
export(count_resistant)
export(count_susceptible)
export(custom_eucast_rules)
export(custom_interpretive_rules)
export(custom_mdro_guideline)
export(eucast_dosage)
export(eucast_exceptional_phenotypes)
@@ -296,6 +300,7 @@ export(mo_is_yeast)
export(mo_kingdom)
export(mo_lpsn)
export(mo_matching_score)
export(mo_morphology)
export(mo_mycobank)
export(mo_name)
export(mo_order)
@@ -381,6 +386,7 @@ export(translate_AMR)
export(trimethoprims)
export(ureidopenicillins)
export(wisca)
export(wisca_plot)
if(getRversion() >= "3.0.0") S3method(cleaner::freq, mo)
if(getRversion() >= "3.0.0") S3method(cleaner::freq, sir)
if(getRversion() >= "3.0.0") S3method(ggplot2::autoplot, antibiogram)

109
NEWS.md
View File

@@ -1,63 +1,60 @@
# AMR 3.0.1.9050
# AMR 3.0.1.9081
Planned as v3.1.0, end of June 2026.
### Breaking Changes
* The former *kingdoms* Bacteria and Archaea are now each divided into four kingdoms with new top-level *domains* 'Bacteria' and 'Archaea' (Göker and Oren, 2024, DOI: 10.1099/ijsem.0.006242). Following this, a new `domain` column in the `microorganisms` data set was added, and more importantly, `mo_kingdom()` now returns the formal kingdom (e.g. `"Pseudomonadati"` instead of `"Bacteria"`). Use `mo_domain()` for the old behaviour. For non-prokaryotic kingdoms (Fungi, Protozoa, etc.), `kingdom` and `domain` are identical.
* Faster parallel computing via the `future` package for `as.sir()` and `wisca()`: a non-sequential plan (e.g. `future::plan(future::multisession)`) must be active before using `parallel = TRUE`.
### New
* Support for clinical breakpoints of 2026 of both CLSI and EUCAST, by adding all of their over 5,700 new clinical breakpoints to the `clinical_breakpoints` data set for usage in `as.sir()`. EUCAST 2026 is now the new default guideline for all MIC and disk diffusion interpretations.
* Integration with the **tidymodels** framework to allow seamless use of SIR, MIC and disk data in modelling pipelines via `recipes`
- `step_mic_log2()` to transform `<mic>` columns with log2, and `step_sir_numeric()` to convert `<sir>` columns to numeric
- New `tidyselect` helpers:
- `all_sir()`, `all_sir_predictors()`
- `all_mic()`, `all_mic_predictors()`
- `all_disk()`, `all_disk_predictors()`
* Data set `esbl_isolates` to practise with AMR modelling
* AMR selectors `ionophores()`, `peptides()`, `phosphonics()` and `spiropyrimidinetriones()`
* Support for Wildtype (WT) / Non-wildtype (NWT) in `as.sir()`, all plotting functions, and all susceptibility/resistance functions.
- `as.sir()` gained an argument `as_wt_nwt`, which defaults to `TRUE` only when `breakpoint_type = "ECOFF"` (#254)
- This transforms the output from S/R to WT/NWT
- Functions such as `susceptibility()` count WT as S and NWT as R
* Function `interpretive_rules()`, which allows future implementation of CLSI interpretive rules (#235)
- `eucast_rules()` has become a wrapper around that function
- Gained argument `add_if_missing` (default: `TRUE`). When set to `FALSE`, rules are only applied to cells that already contain an SIR value; `NA` cells are left untouched. This is useful with `overwrite = TRUE` to update reported results without imputing values for drugs that were not tested (#259)
* Function `amr_course()`, which allows for automated download and unpacking of a GitHub repository for e.g. webinar use
* Two new `NA` objects, `NA_ab_` and `NA_mo_`, analogous to base R's `NA_character_` and `NA_integer_`, for use in pipelines that require typed missing values
* EUCAST 2026 and CLSI 2026 breakpoints: over 5,700 new breakpoints added to the `clinical_breakpoints` data set; EUCAST 2026 is now the default for all MIC and disk diffusion interpretations
* Wildtype/Non-wildtype (WT/NWT) output when using ECOFF-based interpretation, by setting `breakpoint_type = "ECOFF"` in `as.sir()`; WT/NWT results are fully supported in all resistance/susceptibility functions and plots (#254)
* *tidymodels* integration for using SIR, MIC and disk data in modelling pipelines: `step_mic_log2()`, `step_sir_numeric()`, and new column selectors `all_sir()`, `all_mic()`, `all_disk()`
* New `esbl_isolates` data set for practising AMR modelling
* New antimicrobial selectors: `ionophores()`, `peptides()`, `phosphonics()`, `spiropyrimidinetriones()`
* New antimicrobials: cefepime/taniborbactam (`FTA`), ceftibuten/avibactam (`CTA`), clorobiocin (`CLB`), kasugamycin (`KAS`), ostreogrycin (`OST`), taniborbactam (`TAN`), thiostrepton (`THS`), xeruborbactam (`XER`), zorbamycin (`ZOR`)
* New `interpretive_rules()`, a unified function for EUCAST and CLSI interpretive rules; `eucast_rules()` is now a wrapper around it (#235, #259)
* New `morphology` column in the `microorganisms` data set and corresponding `mo_morphology()` function, returning the cell shape of bacteria. Data sourced from BacDive; values prefixed with "likely" are extrapolated from genus-level consensus. New `add_morphology` argument was added to `mo_gramstain()` to return combined results such as `"Gram-negative rods"`.
* New `amr_course()` to download and unpack course or webinar materials from GitHub in one call
* Typed missing value constants `NA_ab_` and `NA_mo_`, for use in pipelines that need missing values of a specific class
* New `wisca_plot()` to assess the susceptibility and incidence distributions from the Monte Carlo simulations
### Fixes
* `as.sir()` with `reference_data`: custom guideline names now correctly classify values as R using EUCAST convention (`> breakpoint_R` for MIC, `< breakpoint_R` for disk); custom breakpoints with `host = NA` now serve as a host-agnostic fallback when no host-specific row matches (fixes #239)
* Fixed multiple bugs in the `parallel = TRUE` mode of `as.sir()` for data frames: (1) PSOCK workers (Windows / R < 4.0) now correctly load the AMR package before processing, with a graceful fallback to sequential mode when the package cannot be loaded; (2) resolved stale-environment issue where the PSOCK path read a frozen copy of `AMR_env` instead of the live one, causing the wrong log entries to be captured; (3) fixed log-entry duplication in the fork-based path (`mclapply`) where pre-existing `sir_interpretation_history` rows were included in every worker's captured log; (4) removed use of non-exported internal functions (`%pm>%`, `pm_pull`, `as.sir.default`) from the worker closure, which made PSOCK workers fail; (5) suppressed per-column progress messages inside workers to prevent interleaved console output; (6) fixed a malformed Unicode escape `\u00a` (3 digits) in the "DONE" status message
* Fixed a bug in `as.sir()` where values that were purely numeric (e.g., `"1"`) and matched the broad SIR-matching regex would be incorrectly stripped of all content by the Unicode letter filter
* Fixed a bug in `as.mic()` where MIC values in scientific notation (e.g., `"1e-3"`) were incorrectly handled because the letter `e` was removed along with other Unicode letters; scientific notation `e` is now preserved
* Fixed a bug in `as.ab()` where certain AB codes containing "PH" or "TH" (such as `ETH`, `MTH`, `PHE`, `PHN`, `STH`, `THA`, `THI1`) would incorrectly return `NA` when combined in a vector with any untranslatable value (#245)
* Fixed a bug in `antibiogram()` for when no antimicrobials are set
* Fixed a bug in `as.sir()` where for numeric input the arguments `S`, `I`, and `R` would not be considered (#244)
* Fixed a bug in plotting MIC values when `keep_operators = "all"`
* Fixed some foreign translations of antimicrobial drugs
* Fixed a bug for printing column names to the console when using `mutate_at(vars(...), as.mic)` (#249)
* Fixed a bug to disregard `NI` for susceptibility proportion functions
* Fixed Italian translation of CoNS to Stafilococco coagulasi-negativo and CoPS to Stafilococco coagulasi-positivo (#256)
* Fixed SIR and MIC coercion of combined values, e.g. `as.sir("<= 0.002; S") ` or `as.mic("S; 0.002")` (#252)
* Fixed translation of foreign languages in `sir_df()` (#272)
* Fixed BRMO classification by including bacterial complexes (#275)
* Fixed `as.sir()` for data frames silently deleting columns whose AB class was already `<sir>` when called a second time (re-running on already-converted data) (#278)
* Fixed `as.sir()` for data frames incorrectly treating metadata columns (e.g. `patient`, `ward`) as antibiotic columns when their names coincidentally matched an antibiotic code; column content is now validated against AMR data patterns before inclusion
* Improved parallel computing in `as.sir()`: when the number of AB columns is smaller than the number of available cores, rows are now split into batches so all cores stay active (row-batch mode). Previously, a 6-column dataset on a 16-core machine would only use 6 cores; now all 16 are used, with each worker processing a smaller row slice (lower per-worker memory pressure)
* Fixed `as.sir()` ignoring `info = FALSE` for columns with no breakpoints (e.g. cefoxitin against *E. coli*): an operator-precedence bug (`&&`/`||`) caused the "Interpreting MIC values" intro message to fire unconditionally when `nrow(breakpoints) == 0`, regardless of `info`; the progress bar title was also not gated by `info`
### Updates
* Extensive `cli` integration for better message handling and clickable links in messages and warnings (#191, #265)
* `mdro()` now infers resistance for a _missing_ base drug column from an _available_ corresponding drug+inhibitor combination showing resistance (e.g., piperacillin is absent but required, while piperacillin/tazobactam available and resistant). Can be set with the new argument `infer_from_combinations`, which defaults to `TRUE` (#209). Note that this can yield a higher MDRO detection (which is a good thing as it has become more reliable).
* `susceptibility()` and `resistance()` gained the argument `guideline`, which defaults to EUCAST, for interpreting the 'I' category correctly.
* Added to the `antimicrobials` data set: cefepime/taniborbactam (`FTA`), ceftibuten/avibactam (`CTA`), clorobiocin (`CLB`), kasugamycin (`KAS`), ostreogrycin (`OST`), taniborbactam (`TAN`), thiostrepton (`THS`), xeruborbactam (`XER`), and zorbamycin (`ZOR`)
* `as.mic()` and `rescale_mic()` gained the argument `round_to_next_log2`, which can be set to `TRUE` to round all values up to the nearest next log2 level (#255)
* `antimicrobials$group` is now a `list` instead of a `character`, to contain any group the drug is in (#246)
* `ab_group()` gained an argument `all_groups` to return all groups the antimicrobial drug is in (#246)
* Added explaining message to `as.sir()` when interpreting numeric values (e.g., 1 for S, 2 for I, 3 for R) (#244)
* Updated handling of capped MIC values (`<`, `<=`, `>`, `>=`) in `as.sir()` in the argument `capped_mic_handling`: (#243)
* Introduced four clearly defined options: `"none"`, `"conservative"` (default), `"standard"`, and `"lenient"`
* Interpretation of capped MIC values now consistently returns `"NI"` (non-interpretable) when the true MIC could be at either side of a breakpoint, depending on the selected handling mode
* This results in more reliable behaviour compared to previous versions for capped MIC values
* Removed the `"inverse"` option, which has now become redundant
* `ab_group()` now returns values consist with the AMR selectors (#246)
* Added two new `NA` objects, `NA_ab_` and `NA_mo_`, analogous to base R's `NA_character_` and `NA_integer_`, for use in pipelines that require typed missing values
### Fixed
* Setting `options(AMR_guideline = "EUCAST 2012")` or any year-qualified value no longer causes errors or silent wrong behaviour in `interpretive_rules()`, `resistance()`, `susceptibility()`, `count_resistant()`, `count_susceptible()`, and SIR plotting/printing functions (#298)
* `as.sir()`
* On data frames: already-converted SIR columns no longer dropped on re-run (#278)
* Metadata columns (e.g. `patient`, `ward`) no longer misidentified as antibiotic columns
* `info = FALSE` now suppresses all messages, including for columns without breakpoints
* Assumption of disk zones are now preferred over MIC values when input is only whole numbers (#291)
* `as.mic()`: values in scientific notation (e.g. `1e-3`) now handled correctly
* `as.ab()`: codes containing "PH" or "TH" (e.g. `ETH`, `PHE`) no longer return `NA` when mixed with unrecognised input (#245)
* Combined MIC/SIR input values (e.g. `"<= 0.002; S"` or `"S; 0.002"`) now parsed correctly (#252)
* `as.mo()`:
* Input of the form `"X complex"` now falls back to `"X"` when the complex is not a distinct taxon in the database, preventing `NA` results for valid clinical descriptions such as `"Proteus vulgaris complex"` (#287)
* Abbreviated-genus input (e.g. `"S. apiospermum"`) now correctly ranks candidates whose species epithet exactly matches the input above more-prevalent organisms whose species does not match; fixes `"S. apiospermum"` resolving to *Staphylococcus* instead of *Scedosporium apiospermum* (#288)
* Abbreviated-genus input for species that have subspecies (e.g. `"P. ovale"`) now collapses to the species-rank record instead of incorrectly matching a more-prevalent organism; explicit subspecies queries (e.g. `"P. ovale curtisi"`) are preserved (#288)
* `get_author_year()` in the microorganism reproduction script now strips `emend.` and everything after it, so `ref` reflects the combination authority rather than the emendation author (e.g. *Rhodococcus equi* now returns "Goodfellow et al., 1977" instead of "Nouioui et al., 2018")
* BRMO classification now includes bacterial complexes (#275)
* Translation fixes for Italian CoNS/CoPS names (#256), Dutch antimicrobials, and `sir_df()` foreign-language output (#272)
* Fixed some EUCAST Expert Rules, mostly on *S. pneumoniae*
### Updated
* `top_n_microorganisms()`: new `property_for_each` argument for sub-grouping within top *n* groups; rank ordering enforced (only lower taxonomic ranks allowed); fixed `property = NULL` not being accepted; inner filter now tracks original row indices to prevent cross-group contamination
* Taxonomic update for all microorganisms, now updated to June 2026
* `mo_kingdom()` now returns the formal taxonomic kingdom; a one-time note per session explains the change when querying bacterial or archaeal records.
* `mo_taxonomy()` and `mo_info()` gained `domain` for the list output
* `antibiogram()` and `wisca()` now also support parallel computing via the argument `parallel = TRUE` (#281)
* `custom_eucast_rules()` renamed to `custom_interpretive_rules()`; old name deprecated but still works (#268)
* `mdro()` can now infer resistance from a drug+inhibitor combination when the base drug column is absent (e.g. piperacillin inferred from piperacillin/tazobactam); controlled via new `infer_from_combinations` argument (default `TRUE`) (#209)
* `wisca()` now more strictly follows Bielicki et al. (2016) by using $\text{Beta}(1, 9999)$ for intrinsically resistant pairs, forcing near-zero susceptibility regardless of observed data (based on EUCAST Expected Resistant Phenotypes)
* `susceptibility()` / `resistance()`: new `guideline` argument (default EUCAST) to ensure the 'I' category is interpreted correctly per guideline
* Capped MIC handling in `as.sir()` reworked into four clearly defined options: `"none"`, `"conservative"` (new default), `"standard"`, `"lenient"` (#243)
* `as.mic()` / `rescale_mic()`: new `round_to_next_log2` argument to round values up to the nearest log2 dilution level (#255)
* `antimicrobials$group` is now a `list`, so that drugs belonging to multiple groups are fully represented; use `ab_group(all_groups = TRUE)` to retrieve all groups for a drug (#246)
* Improved console messages with clickable links throughout, powered by `cli` if it is installed (#191, #265)
* `as.disk()`: input validation is now more strict, rejecting values that are not recognisable as a numeric disk zone diameter
* `as.sir()` gains an `enforce_method` argument (`"auto"`, `"mic"`, or `"disk"`) to force the interpretation method when S3 class information is lost, e.g. when called from Python (#291)
* `AMR for Python` vignette: added sections on installation channels (stable CRAN vs. development GitHub via `AMR.beta`) and on using `enforce_method` in `as_sir()` from Python
# AMR 3.0.1

View File

@@ -36,7 +36,7 @@
#'
#' This work was published in the Journal of Statistical Software (Volume 104(3); \doi{10.18637/jss.v104.i03}) and formed the basis of two PhD theses (\doi{10.33612/diss.177417131} and \doi{10.33612/diss.192486375}).
#'
#' After installing this package, R knows [**`r AMR:::format_included_data_number(AMR::microorganisms)` distinct microbial species**](https://amr-for-r.org/reference/microorganisms.html) (updated June 2024) and all [**`r AMR:::format_included_data_number(NROW(AMR::antimicrobials) + NROW(AMR::antivirals))` antimicrobial and antiviral drugs**](https://amr-for-r.org/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 guidelines from CLSI `r min(as.integer(gsub("[^0-9]", "", subset(AMR::clinical_breakpoints, grepl("CLSI", guideline))$guideline)))`-`r max(as.integer(gsub("[^0-9]", "", subset(AMR::clinical_breakpoints, grepl("CLSI", guideline))$guideline)))` and EUCAST `r min(as.integer(gsub("[^0-9]", "", subset(AMR::clinical_breakpoints, grepl("EUCAST", guideline))$guideline)))`-`r max(as.integer(gsub("[^0-9]", "", subset(AMR::clinical_breakpoints, grepl("EUCAST", guideline))$guideline)))` are included, even with epidemiological cut-off (ECOFF) values. It supports and can read any data format, including WHONET data. This package works on Windows, macOS and Linux with all versions of R since R-3.0 (April 2013). **It was designed to work in any setting, including those with very limited resources**. It was created for both routine data analysis and academic research at the Faculty of Medical Sciences of the [University of Groningen](https://www.rug.nl) and the [University Medical Center Groningen](https://www.umcg.nl).
#' After installing this package, R knows [**`r AMR:::format_included_data_number(AMR::microorganisms)` distinct microbial species**](https://amr-for-r.org/reference/microorganisms.html) (updated `r format(AMR:::TAXONOMY_VERSION$GBIF$accessed_date, "%B %Y")`) and all [**`r AMR:::format_included_data_number(NROW(AMR::antimicrobials) + NROW(AMR::antivirals))` antimicrobial and antiviral drugs**](https://amr-for-r.org/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 guidelines from CLSI `r min(as.integer(gsub("[^0-9]", "", subset(AMR::clinical_breakpoints, grepl("CLSI", guideline))$guideline)))`-`r max(as.integer(gsub("[^0-9]", "", subset(AMR::clinical_breakpoints, grepl("CLSI", guideline))$guideline)))` and EUCAST `r min(as.integer(gsub("[^0-9]", "", subset(AMR::clinical_breakpoints, grepl("EUCAST", guideline))$guideline)))`-`r max(as.integer(gsub("[^0-9]", "", subset(AMR::clinical_breakpoints, grepl("EUCAST", guideline))$guideline)))` are included, even with epidemiological cut-off (ECOFF) values. It supports and can read any data format, including WHONET data. This package works on Windows, macOS and Linux with all versions of R since R-3.0 (April 2013). **It was designed to work in any setting, including those with very limited resources**. It was created for both routine data analysis and academic research at the Faculty of Medical Sciences of the [University of Groningen](https://www.rug.nl) and the [University Medical Center Groningen](https://www.umcg.nl).
#'
#' The `AMR` package is available in `r vector_and(vapply(FUN.VALUE = character(1), LANGUAGES_SUPPORTED_NAMES, function(x) x$exonym), quotes = FALSE, sort = FALSE)`. Antimicrobial drug (group) names and colloquial microorganism names are provided in these languages.
#' @section Download Our Reference Data:
@@ -45,7 +45,7 @@
#' For maximum compatibility, we also provide machine-readable, tab-separated plain text files suitable for use in any software, including laboratory information systems.
#'
#' Visit [our website for direct download links](https://amr-for-r.org/articles/datasets.html), or explore the actual files in [our GitHub repository](https://github.com/msberends/AMR/tree/main/data-raw/datasets).
#' @source
#' @references
#' To cite AMR in publications use:
#'
#' Berends MS, Luz CF, Friedrich AW, Sinha BNM, Albers CJ, Glasner C (2022). "AMR: An R Package for Working with Antimicrobial Resistance Data." _Journal of Statistical Software_, *104*(3), 1-31. \doi{10.18637/jss.v104.i03}

View File

@@ -27,7 +27,7 @@
# how to conduct AMR data analysis: https://amr-for-r.org #
# ==================================================================== #
# add new version numbers here, and add the rules themselves to "data-raw/eucast_rules.tsv" and clinical_breakpoints
# add new version numbers here, and add the rules themselves to "data-raw/interpretive_rules.tsv" and clinical_breakpoints
# (sourcing "data-raw/_pre_commit_checks.R" will process the TSV file)
EUCAST_VERSION_BREAKPOINTS <- list(
"16.0" = list(
@@ -104,26 +104,27 @@ EUCAST_VERSION_EXPECTED_PHENOTYPES <- list(
TAXONOMY_VERSION <- list(
GBIF = list(
name = "Global Biodiversity Information Facility (GBIF)",
accessed_date = as.Date("2024-06-24"),
citation = "GBIF Secretariat (2023). GBIF Backbone Taxonomy. Checklist dataset \\doi{10.15468/39omei}.",
name = "Global Biodiversity Information Facility (GBIF), via Catalogue of Life (COL)",
accessed_date = as.Date("2026-05-07"),
# take the citation from https://www.gbif.org/dataset/7ddf754f-d193-4cc9-b351-99906754a03b#citation
citation = "Banki, O. *et al.* (2026). Catalogue of Life (2026-04-18 XR). Catalogue of Life Foundation, Amsterdam, Netherlands. \\doi{10.48580/dgxjw}.",
url = "https://www.gbif.org"
),
LPSN = list(
name = "List of Prokaryotic names with Standing in Nomenclature (LPSN)",
accessed_date = as.Date("2024-06-24"),
citation = "Parte, AC *et al.* (2020). **List of Prokaryotic names with Standing in Nomenclature (LPSN) moves to the DSMZ.** International Journal of Systematic and Evolutionary Microbiology, 70, 5607-5612; \\doi{10.1099/ijsem.0.004332}.",
accessed_date = as.Date("2026-05-07"),
citation = "Freese, HM *et al.* (2026). **TYGS and LPSN in 2025: a Global Core Biodata Resource for genome-based classification and nomenclature of prokaryotes within DSMZ Digital Diversity.** Nucleic Acids Research, 54, D884\u2013D891; \\doi{10.1093/nar/gkaf1110}.",
url = "https://lpsn.dsmz.de"
),
MycoBank = list(
name = "MycoBank",
accessed_date = as.Date("2024-06-24"),
accessed_date = as.Date("2026-05-07"),
citation = "Vincent, R *et al* (2013). **MycoBank gearing up for new horizons.** IMA Fungus, 4(2), 371-9; \\doi{10.5598/imafungus.2013.04.02.16}.",
url = "https://www.mycobank.org"
),
BacDive = list(
name = "BacDive",
accessed_date = as.Date("2024-07-16"),
accessed_date = as.Date("2026-05-07"),
citation = "Reimer, LC *et al.* (2022). ***BacDive* in 2022: the knowledge base for standardized bacterial and archaeal data.** Nucleic Acids Res., 50(D1):D741-D74; \\doi{10.1093/nar/gkab961}.",
url = "https://bacdive.dsmz.de"
),
@@ -148,10 +149,13 @@ TAXONOMY_VERSION <- list(
)
globalVariables(c(
".coverage",
".GenericCallEnv",
".lower",
".mo",
".rowid",
".syndromic_group",
".upper",
"ab",
"ab_txt",
"affect_ab_name",
@@ -187,6 +191,7 @@ globalVariables(c(
"hjust",
"host_index",
"host_match",
"incidence",
"input",
"input_given",
"intrinsic_resistant",
@@ -214,6 +219,7 @@ globalVariables(c(
"old",
"old_name",
"p_susceptible",
"pathogen",
"pattern",
"R",
"rank_index",
@@ -221,6 +227,8 @@ globalVariables(c(
"reference.rule",
"reference.rule_group",
"reference.version",
"regimen",
"rule.provider",
"rowid",
"rule_group",
"rule_name",

97
R/aa_helper_functions.R Normal file → Executable file
View File

@@ -489,7 +489,11 @@ cli_to_plain <- function(msg, envir = parent.frame()) {
if (length(m) >= 2L) m[2L] else paste0("?", resolve(c))
})
msg <- apply_sub(msg, "\\{\\.url (\\{[^}]+\\}|[^}]+)\\}", function(c) resolve(c))
msg <- apply_sub(msg, "\\{\\.href ([^}]+)\\}", function(c) strsplit(resolve(c), " ", fixed = TRUE)[[1L]][1L])
msg <- apply_sub(msg, "\\{\\.href ([^}]+)\\}", function(c) {
# Handle [display text](url) markdown link format: extract just the URL
m <- regmatches(c, regexec("^\\[.*\\]\\(([^)]+)\\)$", c))[[1L]]
if (length(m) >= 2L) m[2L] else resolve(c)
})
# bare {variable} or {expression} -> evaluate in caller's environment
while (grepl("\\{[^{}]+\\}", msg)) {
@@ -551,7 +555,7 @@ word_wrap <- function(...,
indentation <- 0L + extra_indent
}
if (indentation > 0L) {
wrapped <- gsub("\n", paste0("\n", strrep(" ", indentation)), wrapped, fixed = TRUE)
wrapped <- gsub("\n", paste0("\n", strrep("\u00a0", indentation)), wrapped, fixed = TRUE)
}
gsub("(\n| )+$", "", wrapped)
}
@@ -583,13 +587,27 @@ simplify_help_markup <- function(msg) {
message_ <- function(...,
appendLF = TRUE,
as_note = TRUE) {
as_note = TRUE,
as_check = FALSE,
extra_indent = 0,
with_bullet = FALSE) {
msg <- paste0(c(...), collapse = "")
if (with_bullet == TRUE) {
as_note <- FALSE
msg <- paste0(AMR_env$bullet_icon, "\u00a0", msg)
}
if (extra_indent > 0) {
msg <- paste0(strrep("\u00a0", extra_indent), msg)
}
if (has_cli_rlang()) {
msg <- paste0(c(...), collapse = "")
# prevent errors with single opening curly brackets, we don't evaluate cli's/glue's {} in AMR anyway
msg <- gsub("\\{(?!\\.)", "", msg, perl = TRUE)
if (!cli::ansi_has_hyperlink_support()) {
msg <- simplify_help_markup(msg)
}
if (isTRUE(as_note)) {
if (isTRUE(as_check)) {
cli::cli_inform(c("v" = msg), .envir = parent.frame())
} else if (isTRUE(as_note)) {
cli::cli_inform(c("i" = msg), .envir = parent.frame())
} else if (isTRUE(appendLF)) {
cli::cli_inform(msg, .envir = parent.frame())
@@ -598,22 +616,28 @@ message_ <- function(...,
cat(format_inline_(msg), file = stderr())
}
} else {
plain_msg <- cli_to_plain(paste0(c(...), collapse = ""), envir = parent.frame())
plain_msg <- cli_to_plain(msg, envir = parent.frame())
message(word_wrap(plain_msg, as_note = as_note), appendLF = appendLF)
}
}
warning_ <- function(...,
immediate = FALSE,
call = FALSE) {
call = FALSE,
extra_indent = 0) {
msg <- paste0(c(...), collapse = "")
if (extra_indent > 0) {
msg <- paste0(strrep("\u00a0", extra_indent), msg)
}
if (has_cli_rlang()) {
msg <- paste0(c(...), collapse = "")
# prevent errors with single opening curly brackets, we don't evaluate cli's/glue's {} in AMR anyway
msg <- gsub("\\{(?!\\.)", "", msg, perl = TRUE)
if (!cli::ansi_has_hyperlink_support()) {
msg <- simplify_help_markup(msg)
}
cli::cli_warn(msg, .envir = parent.frame())
} else {
plain_msg <- cli_to_plain(paste0(c(...), collapse = ""), envir = parent.frame())
plain_msg <- cli_to_plain(msg, envir = parent.frame())
warning(trimws2(word_wrap(plain_msg, as_note = FALSE)), immediate. = immediate, call. = call)
}
}
@@ -621,8 +645,15 @@ warning_ <- function(...,
# this alternative to the stop() function:
# - adds the function name where the error was thrown (plain-text fallback)
# - wraps text to never break lines within words (plain-text fallback)
stop_ <- function(..., call = TRUE) {
stop_ <- function(...,
call = TRUE,
extra_indent = 0) {
msg <- paste0(c(...), collapse = "")
if (extra_indent > 0) {
msg <- paste0(strrep("\u00a0", extra_indent), msg)
}
# prevent errors with single opening curly brackets, we don't evaluate cli's/glue's {} in AMR anyway
msg <- gsub("\\{(?!\\.)", "", msg, perl = TRUE)
if (!cli::ansi_has_hyperlink_support()) {
msg <- simplify_help_markup(msg)
}
@@ -677,7 +708,7 @@ stop_ifnot <- function(expr, ..., call = TRUE) {
}
}
"%or%" <- function(x, y) {
"%or_if_na%" <- function(x, y) {
if (is.null(x) || is.null(y)) {
if (is.null(x)) {
return(y)
@@ -727,7 +758,7 @@ documentation_date <- function(d) {
suffix[day %in% c(1, 21, 31)] <- "st"
suffix[day %in% c(2, 22)] <- "nd"
suffix[day %in% c(3, 23)] <- "rd"
paste0(month.name[as.integer(format(d, "%m"))], " ", day, suffix, ", ", format(d, "%Y"))
paste0(day, suffix, " of ", month.name[as.integer(format(d, "%m"))], ", ", format(d, "%Y"))
}
format_included_data_number <- function(data) {
@@ -1635,14 +1666,14 @@ add_MO_lookup_to_AMR_env <- function() {
if (is.null(AMR_env$MO_lookup)) {
MO_lookup <- AMR::microorganisms
MO_lookup$kingdom_index <- NA_real_
MO_lookup[which(MO_lookup$kingdom == "Bacteria" | as.character(MO_lookup$mo) == "UNKNOWN"), "kingdom_index"] <- 1
MO_lookup[which(MO_lookup$kingdom == "Fungi"), "kingdom_index"] <- 1.25
MO_lookup[which(MO_lookup$kingdom == "Protozoa"), "kingdom_index"] <- 1.5
MO_lookup[which(MO_lookup$kingdom == "Chromista"), "kingdom_index"] <- 1.75
MO_lookup[which(MO_lookup$kingdom == "Archaea"), "kingdom_index"] <- 2
MO_lookup$domain_index <- NA_real_
MO_lookup[which(MO_lookup$domain == "Bacteria" | as.character(MO_lookup$mo) == "UNKNOWN"), "domain_index"] <- 1
MO_lookup[which(MO_lookup$domain == "Fungi"), "domain_index"] <- 1.25
MO_lookup[which(MO_lookup$domain == "Protozoa"), "domain_index"] <- 1.5
MO_lookup[which(MO_lookup$domain == "Chromista"), "domain_index"] <- 1.75
MO_lookup[which(MO_lookup$domain == "Archaea"), "domain_index"] <- 2
# all the rest
MO_lookup[which(is.na(MO_lookup$kingdom_index)), "kingdom_index"] <- 3
MO_lookup[which(is.na(MO_lookup$domain_index)), "domain_index"] <- 3
# the fullname lowercase, important for the internal algorithms in as.mo()
MO_lookup$fullname_lower <- tolower(trimws2(paste(
@@ -1681,28 +1712,6 @@ readRDS_AMR <- function(file, refhook = NULL) {
readRDS(con, refhook = refhook)
}
get_n_cores <- function(max_cores = Inf) {
if (pkg_is_available("parallelly", min_version = "0.8.0", also_load = FALSE)) {
available_cores <- import_fn("availableCores", "parallelly")
n_cores <- min(available_cores(), na.rm = TRUE)
} else {
# `parallel` is part of base R since 2.14.0, but detectCores() is not very precise on exotic systems like Docker and quota-set Linux environments
n_cores <- parallel::detectCores()[1]
if (is.na(n_cores)) {
n_cores <- 1
}
}
max_cores <- floor(max_cores)
if (max_cores == 0) {
n_cores <- 1
} else if (max_cores < 0) {
n_cores <- max(1, n_cores - abs(max_cores))
} else if (max_cores > 0) {
n_cores <- min(n_cores, max_cores)
}
n_cores
}
# Support `where()` if tidyselect not installed ----
if (!is.null(import_fn("where", "tidyselect", error_on_fail = FALSE))) {
# tidyselect::where() exists, retrieve from their namespace to make `where()`s work across the package in default arguments
@@ -1837,4 +1846,10 @@ if (getRversion() < "4.0.0") {
}
}
if (getRversion() < "4.4.0") {
`%||%` <- function(x, y) {
if (is.null(x)) y else x
}
}
# nolint end

2
R/ab.R
View File

@@ -54,7 +54,7 @@
#' @section Source:
#' World Health Organization (WHO) Collaborating Centre for Drug Statistics Methodology: \url{https://atcddd.fhi.no/atc_ddd_index/}
#'
#' European Commission Public Health PHARMACEUTICALS - COMMUNITY REGISTER: \url{https://health.ec.europa.eu/documents/community-register/html/reg_hum_atc.htm}
#' European Commission Public Health PHARMACEUTICALS - COMMUNITY REGISTER: \url{https://health.ec.europa.eu/documents/community-register/html/index_en.htm}
#' @aliases ab
#' @return A [character] [vector] with additional class [`ab`]
#' @seealso

0
R/amr_course.R Normal file → Executable file
View File

View File

@@ -202,7 +202,7 @@
#' # data.table --------------------------------------------------------------
#'
#' # data.table is supported as well, just use it in the same way as with
#' # base R, but add `with = FALSE` if using a single AB selector.
#' # base R, but add `with = FALSE` if using a single AMR selector.
#'
#' if (require("data.table")) {
#' dt <- as.data.table(example_isolates)
@@ -215,7 +215,7 @@
#' dt[, carbapenems(), with = FALSE]
#' }
#'
#' # for multiple selections or AB selectors, `with = FALSE` is not needed:
#' # for multiple selections or AMR selectors, `with = FALSE` is not needed:
#' if (require("data.table")) {
#' dt[, c("mo", aminoglycosides())]
#' }

File diff suppressed because it is too large Load Diff

View File

@@ -129,16 +129,21 @@ bug_drug_combinations <- function(x,
# turn and merge everything
pivot <- lapply(x_mo_filter, function(x) {
m <- as.matrix(table(as.sir(x), useNA = "always"))
na_idx <- which(is.na(rownames(m)))
get_row <- function(lbl) {
idx <- which(rownames(m) == lbl)
if (length(idx) == 1L) unname(m[idx, ]) else rep(0L, ncol(m))
}
data.frame(
S = m["S", ],
SDD = m["SDD", ],
I = m["I", ],
R = m["R", ],
NI = m["NI", ],
WT = m["WT", ],
NWT = m["NWT", ],
NS = m["NS", ],
na = m[which(is.na(rownames(m))), ],
S = get_row("S"),
SDD = get_row("SDD"),
I = get_row("I"),
R = get_row("R"),
NI = get_row("NI"),
WT = get_row("WT"),
NWT = get_row("NWT"),
NS = get_row("NS"),
na = if (length(na_idx) == 1L) unname(m[na_idx, ]) else rep(0L, ncol(m)),
stringsAsFactors = FALSE
)
})

View File

@@ -126,6 +126,11 @@ count_resistant <- function(...,
only_all_tested = FALSE,
guideline = getOption("AMR_guideline", "EUCAST")) {
# other arguments for meet_criteria are handled by sir_calc()
if (guideline %like% "EUCAST") {
guideline <- "EUCAST"
} else if (guideline %like% "CLSI") {
guideline <- "CLSI"
}
meet_criteria(guideline, allow_class = "character", is_in = c("EUCAST", "CLSI"), has_length = 1)
if (is.null(getOption("AMR_guideline")) && missing(guideline) && message_not_thrown_before("count_resistant", "eucast_default", entire_session = TRUE)) {
message_("{.help [{.fun count_resistant}](AMR::count_resistant)} assumes the EUCAST guideline and thus considers the 'I' category susceptible. Set the {.arg guideline} argument or the {.code AMR_guideline} option to either \"CLSI\" or \"EUCAST\", see {.topic [AMR-options](AMR::AMR-options)}.")
@@ -150,6 +155,11 @@ count_susceptible <- function(...,
only_all_tested = FALSE,
guideline = getOption("AMR_guideline", "EUCAST")) {
# other arguments for meet_criteria are handled by sir_calc()
if (guideline %like% "EUCAST") {
guideline <- "EUCAST"
} else if (guideline %like% "CLSI") {
guideline <- "CLSI"
}
meet_criteria(guideline, allow_class = "character", is_in = c("EUCAST", "CLSI"), has_length = 1)
if (is.null(getOption("AMR_guideline")) && missing(guideline) && message_not_thrown_before("count_susceptible", "eucast_default", entire_session = TRUE)) {
message_("{.help [{.fun count_susceptible}](AMR::count_susceptible)} assumes the EUCAST guideline and thus considers the 'I' category susceptible. Set the {.arg guideline} argument or the {.code AMR_guideline} option to either \"CLSI\" or \"EUCAST\", see {.topic [AMR-options](AMR::AMR-options)}.")

View File

@@ -27,27 +27,27 @@
# how to conduct AMR data analysis: https://amr-for-r.org #
# ==================================================================== #
#' Define Custom EUCAST Rules
#' Define Custom Interpretive Rules
#'
#' Define custom EUCAST rules for your organisation or specific analysis and use the output of this function in [eucast_rules()].
#' Define custom interpretive rules for your organisation or specific analysis and use the output of this function in [interpretive_rules()].
#' @param ... Rules in [formula][base::tilde] notation, see below for instructions, and in *Examples*.
#' @details
#' Some organisations have their own adoption of EUCAST rules. This function can be used to define custom EUCAST rules to be used in the [eucast_rules()] function.
#' Some organisations have their own adoption of interpretive rules. This function can be used to define custom rules to be used in the [interpretive_rules()] function.
#'
#' ### Basics
#'
#' If you are familiar with the [`case_when()`][dplyr::case_when()] function of the `dplyr` package, you will recognise the input method to set your own rules. Rules must be set using what \R considers to be the 'formula notation'. The rule itself is written *before* the tilde (`~`) and the consequence of the rule is written *after* the tilde:
#'
#' ```r
#' x <- custom_eucast_rules(TZP == "S" ~ aminopenicillins == "S",
#' TZP == "R" ~ aminopenicillins == "R")
#' x <- custom_interpretive_rules(TZP == "S" ~ aminopenicillins == "S",
#' TZP == "R" ~ aminopenicillins == "R")
#' ```
#'
#' These are two custom EUCAST rules: if TZP (piperacillin/tazobactam) is "S", all aminopenicillins (ampicillin and amoxicillin) must be made "S", and if TZP is "R", aminopenicillins must be made "R". These rules can also be printed to the console, so it is immediately clear how they work:
#' These are two custom interpretive rules: if TZP (piperacillin/tazobactam) is "S", all aminopenicillins (ampicillin and amoxicillin) must be made "S", and if TZP is "R", aminopenicillins must be made "R". These rules can also be printed to the console, so it is immediately clear how they work:
#'
#' ```r
#' x
#' #> A set of custom EUCAST rules:
#' #> A set of custom interpretive rules:
#' #>
#' #> 1. If TZP is "S" then set to S :
#' #> amoxicillin (AMX), ampicillin (AMP)
@@ -68,11 +68,11 @@
#' #> 1 Escherichia coli R S S
#' #> 2 Klebsiella pneumoniae R S S
#'
#' eucast_rules(df,
#' rules = "custom",
#' custom_rules = x,
#' info = FALSE,
#' overwrite = TRUE)
#' interpretive_rules(df,
#' rules = "custom",
#' custom_rules = x,
#' info = FALSE,
#' overwrite = TRUE)
#' #> mo TZP ampi cipro
#' #> 1 Escherichia coli R R S
#' #> 2 Klebsiella pneumoniae R R S
@@ -83,16 +83,16 @@
#' There is one exception in columns used for the rules: all column names of the [microorganisms] data set can also be used, but do not have to exist in the data set. These column names are: `r vector_and(colnames(microorganisms), sort = FALSE, documentation = TRUE)`. Thus, this next example will work as well, despite the fact that the `df` data set does not contain a column `genus`:
#'
#' ```r
#' y <- custom_eucast_rules(
#' y <- custom_interpretive_rules(
#' TZP == "S" & genus == "Klebsiella" ~ aminopenicillins == "S",
#' TZP == "R" & genus == "Klebsiella" ~ aminopenicillins == "R"
#' )
#'
#' eucast_rules(df,
#' rules = "custom",
#' custom_rules = y,
#' info = FALSE,
#' overwrite = TRUE)
#' interpretive_rules(df,
#' rules = "custom",
#' custom_rules = y,
#' info = FALSE,
#' overwrite = TRUE)
#' #> mo TZP ampi cipro
#' #> 1 Escherichia coli R S S
#' #> 2 Klebsiella pneumoniae R R S
@@ -109,9 +109,9 @@
#' Rules can also be applied to multiple antimicrobials and antimicrobial groups simultaneously. Use the `c()` function to combine multiple antimicrobials. For instance, the following example sets all aminopenicillins and ureidopenicillins to "R" if column TZP (piperacillin/tazobactam) is "R":
#'
#' ```r
#' x <- custom_eucast_rules(TZP == "R" ~ c(aminopenicillins, ureidopenicillins) == "R")
#' x <- custom_interpretive_rules(TZP == "R" ~ c(aminopenicillins, ureidopenicillins) == "R")
#' x
#' #> A set of custom EUCAST rules:
#' #> A set of custom interpretive rules:
#' #>
#' #> 1. If TZP is "R" then set to "R":
#' #> amoxicillin (AMX), ampicillin (AMP), azlocillin (AZL), mezlocillin (MEZ), piperacillin (PIP), piperacillin/tazobactam (TZP)
@@ -123,7 +123,7 @@
#' @returns A [list] containing the custom rules
#' @export
#' @examples
#' x <- custom_eucast_rules(
#' x <- custom_interpretive_rules(
#' AMC == "R" & genus == "Klebsiella" ~ aminopenicillins == "R",
#' AMC == "I" & genus == "Klebsiella" ~ aminopenicillins == "I"
#' )
@@ -141,24 +141,24 @@
#' # combine rule sets
#' x2 <- c(
#' x,
#' custom_eucast_rules(TZP == "R" ~ carbapenems == "R")
#' custom_interpretive_rules(TZP == "R" ~ carbapenems == "R")
#' )
#' x2
custom_eucast_rules <- function(...) {
custom_interpretive_rules <- function(...) {
dots <- tryCatch(list(...),
error = function(e) "error"
)
stop_if(
identical(dots, "error"),
"rules must be a valid formula inputs (e.g., using '~'), see {.help [{.fun custom_eucast_rules}](AMR::custom_eucast_rules)}"
"rules must be a valid formula inputs (e.g., using '~'), see {.help [{.fun custom_interpretive_rules}](AMR::custom_interpretive_rules)}"
)
n_dots <- length(dots)
stop_if(n_dots == 0, "no custom rules were set. Please read the documentation using {.help [{.fun custom_eucast_rules}](AMR::custom_eucast_rules)}.")
stop_if(n_dots == 0, "no custom rules were set. Please read the documentation using {.help [{.fun custom_interpretive_rules}](AMR::custom_interpretive_rules)}.")
out <- vector("list", n_dots)
for (i in seq_len(n_dots)) {
stop_ifnot(
inherits(dots[[i]], "formula"),
"rule ", i, " must be a valid formula input (e.g., using '~'), see {.help [{.fun custom_eucast_rules}](AMR::custom_eucast_rules)}"
"rule ", i, " must be a valid formula input (e.g., using '~'), see {.help [{.fun custom_interpretive_rules}](AMR::custom_interpretive_rules)}"
)
# Query
@@ -180,7 +180,7 @@ custom_eucast_rules <- function(...) {
result <- dots[[i]][[3]]
stop_ifnot(
deparse(result) %like% "==",
"the result of rule ", i, " (the part after the `~`) must contain `==`, such as in `... ~ ampicillin == \"R\"`, see {.help [{.fun custom_eucast_rules}](AMR::custom_eucast_rules)}"
"the result of rule ", i, " (the part after the `~`) must contain `==`, such as in `... ~ ampicillin == \"R\"`, see {.help [{.fun custom_interpretive_rules}](AMR::custom_interpretive_rules)}"
)
result_group <- as.character(result)[[2]]
result_group <- as.character(str2lang(result_group))
@@ -230,13 +230,13 @@ custom_eucast_rules <- function(...) {
}
names(out) <- paste0("rule", seq_len(n_dots))
set_clean_class(out, new_class = c("custom_eucast_rules", "list"))
set_clean_class(out, new_class = c("custom_interpretive_rules", "list"))
}
#' @method c custom_eucast_rules
#' @method c custom_interpretive_rules
#' @noRd
#' @export
c.custom_eucast_rules <- function(x, ...) {
c.custom_interpretive_rules <- function(x, ...) {
if (length(list(...)) == 0) {
return(x)
}
@@ -245,21 +245,21 @@ c.custom_eucast_rules <- function(x, ...) {
out <- c(out, unclass(e))
}
names(out) <- paste0("rule", seq_len(length(out)))
set_clean_class(out, new_class = c("custom_eucast_rules", "list"))
set_clean_class(out, new_class = c("custom_interpretive_rules", "list"))
}
#' @method as.list custom_eucast_rules
#' @method as.list custom_interpretive_rules
#' @noRd
#' @export
as.list.custom_eucast_rules <- function(x, ...) {
as.list.custom_interpretive_rules <- function(x, ...) {
c(x, ...)
}
#' @method print custom_eucast_rules
#' @method print custom_interpretive_rules
#' @export
#' @noRd
print.custom_eucast_rules <- function(x, ...) {
cat("A set of custom EUCAST rules:\n")
print.custom_interpretive_rules <- function(x, ...) {
cat("A set of custom interpretive rules:\n")
for (i in seq_len(length(x))) {
rule <- x[[i]]
rule$query <- format_custom_query_rule(rule$query)
@@ -291,3 +291,19 @@ print.custom_eucast_rules <- function(x, ...) {
cat("\n ", rule_if, "\n", rule_then, "\n", sep = "")
}
}
# Backward-compat S3 dispatch for objects created with the old custom_eucast_rules() function
#' @method c custom_eucast_rules
#' @noRd
#' @export
c.custom_eucast_rules <- function(x, ...) c.custom_interpretive_rules(x, ...)
#' @method as.list custom_eucast_rules
#' @noRd
#' @export
as.list.custom_eucast_rules <- function(x, ...) as.list.custom_interpretive_rules(x, ...)
#' @method print custom_eucast_rules
#' @export
#' @noRd
print.custom_eucast_rules <- function(x, ...) print.custom_interpretive_rules(x, ...)

View File

@@ -195,11 +195,13 @@ add_custom_microorganisms <- function(x) {
if (!"fullname" %in% colnames(x)) {
x$fullname <- trimws2(paste(x$genus, x$species, x$subspecies))
}
if (!"domain" %in% colnames(x)) x$domain <- ""
if (!"kingdom" %in% colnames(x)) x$kingdom <- ""
if (!"phylum" %in% colnames(x)) x$phylum <- ""
if (!"class" %in% colnames(x)) x$class <- ""
if (!"order" %in% colnames(x)) x$order <- ""
if (!"family" %in% colnames(x)) x$family <- ""
x$domain[is.na(x$domain)] <- ""
x$kingdom[is.na(x$kingdom)] <- ""
x$phylum[is.na(x$phylum)] <- ""
x$class[is.na(x$class)] <- ""
@@ -217,6 +219,7 @@ add_custom_microorganisms <- function(x) {
# fill in taxonomy based on genus
genus_to_check <- gsub("^(.*)[^a-zA-Z].*", "\\1", x$genus, perl = TRUE)
x$domain[which(x$domain == "" & genus_to_check != "")] <- AMR_env$MO_lookup$domain[match(genus_to_check[which(x$domain == "" & genus_to_check != "")], AMR_env$MO_lookup$genus)]
x$kingdom[which(x$kingdom == "" & genus_to_check != "")] <- AMR_env$MO_lookup$kingdom[match(genus_to_check[which(x$kingdom == "" & genus_to_check != "")], AMR_env$MO_lookup$genus)]
x$phylum[which(x$phylum == "" & genus_to_check != "")] <- AMR_env$MO_lookup$phylum[match(genus_to_check[which(x$phylum == "" & genus_to_check != "")], AMR_env$MO_lookup$genus)]
x$class[which(x$class == "" & genus_to_check != "")] <- AMR_env$MO_lookup$class[match(genus_to_check[which(x$class == "" & genus_to_check != "")], AMR_env$MO_lookup$genus)]
@@ -229,9 +232,9 @@ add_custom_microorganisms <- function(x) {
x$prevalence[is.na(x$prevalence)] <- 1.25
x$status <- "accepted"
x$ref <- paste("Self-added,", format(Sys.Date(), "%Y"))
x$kingdom_index <- AMR_env$MO_lookup$kingdom_index[match(genus_to_check, AMR_env$MO_lookup$genus)]
# complete missing kingdom index, so mo_matching_score() will not return NA
x$kingdom_index[is.na(x$kingdom_index)] <- 1
x$domain_index <- AMR_env$MO_lookup$domain_index[match(genus_to_check, AMR_env$MO_lookup$genus)]
# complete missing domain index, so mo_matching_score() will not return NA
x$domain_index[is.na(x$domain_index)] <- 1
x$fullname_lower <- tolower(x$fullname)
x$full_first <- substr(x$fullname_lower, 1, 1)
x$species_first <- tolower(substr(x$species, 1, 1))

View File

@@ -77,13 +77,13 @@
#'
#' Synonyms (i.e. trade names) were derived from the PubChem Compound ID (column `cid`) and are consequently only available where a CID is available.
#' @inheritSection AMR Download Our Reference Data
#' @source
#' @references
#'
#' * `r TAXONOMY_VERSION$ATC_DDD$citation` Accessed from <`r TAXONOMY_VERSION$ATC_DDD$url`> on `r documentation_date(TAXONOMY_VERSION$ATC_DDD$accessed_date)`.
#'
#' * `r TAXONOMY_VERSION$LOINC$citation` Accessed from <`r TAXONOMY_VERSION$LOINC$url`> on `r documentation_date(TAXONOMY_VERSION$LOINC$accessed_date)`.
#'
#' * European Commission Public Health PHARMACEUTICALS - COMMUNITY REGISTER: <https://ec.europa.eu/health/documents/community-register/html/reg_hum_atc.htm>
#' * European Commission Public Health PHARMACEUTICALS - COMMUNITY REGISTER: <https://ec.europa.eu/health/documents/community-register/html/index_en.htm>
#' @inheritSection WHOCC WHOCC
#' @seealso [microorganisms], [intrinsic_resistant]
#' @examples
@@ -100,17 +100,18 @@
#' Data Set with `r format(nrow(microorganisms), big.mark = " ")` Taxonomic Records of Microorganisms
#'
#' @description
#' A data set containing the full microbial taxonomy (**last updated: `r documentation_date(max(TAXONOMY_VERSION$GBIF$accessed_date, TAXONOMY_VERSION$LPSN$accessed_date, TAXONOMY_VERSION$MycoBank$accessed_date))`**) of `r nr2char(length(unique(microorganisms$kingdom[!microorganisms$kingdom %like% "unknown"])))` kingdoms. This data set is the backbone of this `AMR` package. MO codes can be looked up using [as.mo()] and microorganism properties can be looked up using any of the [`mo_*`][mo_property()] functions.
#' A data set containing the full microbial taxonomy (**last updated: `r documentation_date(max(TAXONOMY_VERSION$GBIF$accessed_date, TAXONOMY_VERSION$LPSN$accessed_date, TAXONOMY_VERSION$MycoBank$accessed_date))`**) of `r nr2char(length(unique(microorganisms$domain[!microorganisms$domain %like% "unknown"])))` domains. This data set is the backbone of this `AMR` package. MO codes can be looked up using [as.mo()] and microorganism properties can be looked up using any of the [`mo_*`][mo_property()] functions.
#'
#' This data set is carefully crafted, yet made 100% reproducible from public and authoritative taxonomic sources (using [this script](https://github.com/msberends/AMR/blob/main/data-raw/_reproduction_scripts/reproduction_of_microorganisms.R)), namely: *`r TAXONOMY_VERSION$LPSN$name`* for bacteria, *`r TAXONOMY_VERSION$MycoBank$name`* for fungi, and *`r TAXONOMY_VERSION$GBIF$name`* for all others taxons.
#' @format A [tibble][tibble::tibble] with `r format(nrow(microorganisms), big.mark = " ")` observations and `r ncol(microorganisms)` variables:
#' - `mo`\cr ID of microorganism as used by this package. ***This is a unique identifier.***
#' - `fullname`\cr Full name, like `"Escherichia coli"`. For the taxonomic ranks genus, species and subspecies, this is the 'pasted' text of genus, species, and subspecies. For all taxonomic ranks higher than genus, this is the name of the taxon. ***This is a unique identifier.***
#' - `status` \cr Status of the taxon, either `r vector_or(microorganisms$status, documentation = TRUE)`
#' - `kingdom`, `phylum`, `class`, `order`, `family`, `genus`, `species`, `subspecies`\cr Taxonomic rank of the microorganism. Note that for fungi, *phylum* is equal to their taxonomic *division*. Also, for fungi, *subkingdom* and *subdivision* were left out since they do not occur in the bacterial taxonomy.
#' - `domain`, `kingdom`, `phylum`, `class`, `order`, `family`, `genus`, `species`, `subspecies`\cr Taxonomic rank of the microorganism. Note that for fungi, *phylum* is used for their taxonomic *division*. Also, for fungi, *subkingdom* and *subdivision* were left out since they do not occur in the bacterial taxonomy. For all species outside the domains of Bacteria and Archaea, the `domain` and `kingdom` are identical.
#' - `rank`\cr Text of the taxonomic rank of the microorganism, such as `"species"` or `"genus"`
#' - `ref`\cr Author(s) and year of related scientific publication. This contains only the *first surname* and year of the *latest* authors, e.g. "Wallis *et al.* 2006 *emend.* Smith and Jones 2018" becomes "Smith *et al.*, 2018". This field is directly retrieved from the source specified in the column `source`. Moreover, accents were removed to comply with CRAN that only allows ASCII characters.
#' - `oxygen_tolerance` \cr Oxygen tolerance, either `r vector_or(microorganisms$oxygen_tolerance, documentation = TRUE)`. These data were retrieved from BacDive (see *Source*). Items that contain "likely" are missing from BacDive and were extrapolated from other species within the same genus to guess the oxygen tolerance. Currently `r round(length(microorganisms$oxygen_tolerance[which(!is.na(microorganisms$oxygen_tolerance))]) / nrow(microorganisms[which(microorganisms$kingdom == "Bacteria"), ]) * 100, 1)`% of all `r format_included_data_number(nrow(microorganisms[which(microorganisms$kingdom == "Bacteria"), ]))` bacteria in the data set contain an oxygen tolerance.
#' - `ref`\cr Abbreviated authority citation for the nomenclatural act that established the current name combination, following ICNP conventions. For species described in their current genus (*sp. nov.*), this is the original description author(s) and year. For species transferred to a different genus (*comb. nov.*), this is the reclassification author(s) and year. Emendations are excluded. For synonyms, this is the authority under which the synonym was originally published. This field is directly retrieved from the source specified in the column `source`. Diacritics were removed to comply with CRAN, that only allows ASCII characters.
#' - `oxygen_tolerance` \cr Oxygen tolerance, either `r vector_or(microorganisms$oxygen_tolerance, documentation = TRUE)`. These data were retrieved from BacDive (see *Source*). Items that contain "likely" are missing from BacDive and were extrapolated from other species within the same genus to guess the oxygen tolerance. Currently `r round(length(microorganisms$oxygen_tolerance[which(!is.na(microorganisms$oxygen_tolerance))]) / nrow(microorganisms[which(microorganisms$domain == "Bacteria"), ]) * 100, 1)`% of all `r format_included_data_number(nrow(microorganisms[which(microorganisms$domain == "Bacteria"), ]))` bacteria in the data set contain an oxygen tolerance.
#' - `morphology` \cr Morphology (cell shape), either `r vector_or(microorganisms$morphology, documentation = TRUE)`. These data were retrieved from BacDive (see *Source*). Genera that are clinically established as coccobacilli (the HACEK group and beyond, such as *Haemophilus* and *Acinetobacter*) are classified as such regardless of BacDive majority vote. Items that contain "likely" are missing from BacDive and were extrapolated from other species within the same genus. Currently `r round(length(microorganisms$morphology[which(!is.na(microorganisms$morphology))]) / nrow(microorganisms[which(microorganisms$domain == "Bacteria"), ]) * 100, 1)`% of all `r format_included_data_number(nrow(microorganisms[which(microorganisms$domain == "Bacteria"), ]))` bacteria in the data set contain a morphology.
#' - `source`\cr Either `r vector_or(microorganisms$source, documentation = TRUE)` (see *Source*)
#' - `lpsn`\cr Identifier ('Record number') of `r TAXONOMY_VERSION$LPSN$name`. This will be the first/highest LPSN identifier to keep one identifier per row. For example, *Acetobacter ascendens* has LPSN Record number 7864 and 11011. Only the first is available in the `microorganisms` data set. ***This is a unique identifier***, though available for only `r format_included_data_number(sum(!is.na(microorganisms$lpsn)))` records.
#' - `lpsn_parent`\cr LPSN identifier of the parent taxon
@@ -130,27 +131,26 @@
#'
#' @section Included Taxa:
#' Included taxonomic data from [LPSN](`r TAXONOMY_VERSION$LPSN$url`), [MycoBank](`r TAXONOMY_VERSION$MycoBank$url`), and [GBIF](`r TAXONOMY_VERSION$GBIF$url`) are:
#' - All `r format_included_data_number(microorganisms[which(microorganisms$kingdom %in% c("Archeae", "Bacteria")), , drop = FALSE])` (sub)species from the kingdoms of Archaea and Bacteria
#' - `r format_included_data_number(microorganisms[which(microorganisms$kingdom == "Fungi"), , drop = FALSE])` species from the kingdom of Fungi. The kingdom of Fungi is a very large taxon with almost 300,000 different (sub)species, of which most are not microbial (but rather macroscopic, like mushrooms). Because of this, not all fungi fit the scope of this package. Only relevant fungi are covered (such as all species of *Aspergillus*, *Candida*, *Cryptococcus*, *Histoplasma*, *Pneumocystis*, *Saccharomyces* and *Trichophyton*).
#' - All `r format_included_data_number(microorganisms[which(microorganisms$domain %in% c("Archeae", "Bacteria")), , drop = FALSE])` (sub)species from the domains of Archaea and Bacteria
#' - `r format_included_data_number(microorganisms[which(microorganisms$domain == "Fungi"), , drop = FALSE])` species from the kingdom of Fungi. The kingdom of Fungi is a very large taxon with almost 300,000 different (sub)species, of which most are not microbial (but rather macroscopic, like mushrooms). Because of this, not all fungi fit the scope of this package. Only relevant fungi are covered (such as all species of *Aspergillus*, *Candida*, *Cryptococcus*, *Histoplasma*, *Pneumocystis*, *Saccharomyces* and *Trichophyton*).
#' - `r format_included_data_number(microorganisms[which(microorganisms$kingdom == "Protozoa"), , drop = FALSE])` (sub)species from the kingdom of Protozoa
#' - `r format_included_data_number(microorganisms[which(microorganisms$kingdom == "Animalia"), , drop = FALSE])` (sub)species from `r format_included_data_number(microorganisms[which(microorganisms$kingdom == "Animalia"), "genus", drop = TRUE])` other relevant genera from the kingdom of Animalia (such as *Strongyloides* and *Taenia*)
#' - All `r format_included_data_number(microorganisms[which(microorganisms$status != "accepted"), , drop = FALSE])` previously accepted names of all included (sub)species (these were taxonomically renamed)
#' - The complete taxonomic tree of all included (sub)species: from kingdom to subspecies
#' - The complete taxonomic tree of all included (sub)species: from domain to subspecies
#' - The identifier of the parent taxons
#' - The year and first author of the related scientific publication
#'
#' ### Manual additions
#' For convenience, some entries were added manually:
#'
#' - All `r format_included_data_number(length(which(microorganisms$rank == "species group")))` groups and complexes of the [microorganisms.groups] data set, for cross-reference (examples include beta-haemolytic *Streptococcus* groups A to K, coagulase-negative *Staphylococcus* (CoNS), *Mycobacterium tuberculosis* complex, etc.)
#' - `r format_included_data_number(microorganisms[which(microorganisms$source == "manually added" & microorganisms$genus == "Salmonella"), , drop = FALSE])` entries of *Salmonella*, such as the city-like serovars and groups A to H
#' - `r format_included_data_number(length(which(microorganisms$rank == "species group")))` species groups (such as the beta-haemolytic *Streptococcus* groups A to K, coagulase-negative *Staphylococcus* (CoNS), *Mycobacterium tuberculosis* complex, etc.), of which the group compositions are stored in the [microorganisms.groups] data set
#' - 1 entry of *Blastocystis* (*B. hominis*), although it officially does not exist (Noel *et al.* 2005, PMID 15634993)
#' - 1 entry of *Moraxella* (*M. catarrhalis*), which was formally named *Branhamella catarrhalis* (Catlin, 1970) though this change was never accepted within the field of clinical microbiology
#' - 8 other 'undefined' entries (unknown, unknown Gram-negatives, unknown Gram-positives, unknown yeast, unknown fungus, and unknown anaerobic Gram-pos/Gram-neg bacteria)
#' - `r sum(microorganisms$fullname %like% "unknown")` other 'undefined' entries (unknown, unknown Gram-negatives, unknown Gram-positives, unknown yeast, unknown fungus, and unknown anaerobic Gram-pos/Gram-neg bacteria)
#'
#' The syntax used to transform the original data to a cleansed \R format, can be [found here](https://github.com/msberends/AMR/blob/main/data-raw/_reproduction_scripts/reproduction_of_microorganisms.R).
#' @inheritSection AMR Download Our Reference Data
#' @source
#' @references
#' Taxonomic entries were imported in this order of importance:
#' 1. `r TAXONOMY_VERSION$LPSN$name`:\cr\cr
#' `r TAXONOMY_VERSION$LPSN$citation` Accessed from <`r TAXONOMY_VERSION$LPSN$url`> on `r documentation_date(TAXONOMY_VERSION$LPSN$accessed_date)`.
@@ -339,6 +339,7 @@
#' This data set is internally used by:
#' * [not_intrinsic_resistant()] (an [antimicrobial selector][antimicrobial_selectors])
#' * [mo_is_intrinsic_resistant()]
#' * [wisca()] to model \eqn{\beta(1, 9999)} for resistant bug-drug combinations, per \doi{10.1093/jac/dkv397}
#' @inheritSection AMR Download Our Reference Data
#' @examples
#' intrinsic_resistant

View File

@@ -83,34 +83,31 @@ as.disk <- function(x, na.rm = FALSE) {
na_before <- length(x[is.na(x)])
# heavily based on cleaner::clean_double():
clean_double2 <- function(x, remove = "[^0-9.,-]", fixed = FALSE) {
# extract a plausible numeric disk zone value from character input
extract_disk_value <- function(x) {
x <- as.character(x)
# normalise decimal separators
x <- gsub(",", ".", x, fixed = TRUE)
# remove ending dot/comma
x <- gsub("[,.]$", "", x)
# only keep last dot/comma
reverse <- function(x) vapply(FUN.VALUE = character(1), lapply(strsplit(x, NULL), rev), paste, collapse = "")
x <- sub("{{dot}}", ".",
gsub(".", "",
reverse(sub(".", "}}tod{{",
reverse(x),
fixed = TRUE
)),
fixed = TRUE
),
fixed = TRUE
)
x_clean <- gsub(remove, "", x, ignore.case = TRUE, fixed = fixed)
# remove everything that is not a number or dot
as.double(gsub("[^0-9.]+", "", x_clean))
# strip known context: leading/trailing whitespace, SIR interpretations,
# comparison operators, semicolons, and surrounding whitespace
x <- trimws(x)
# remove trailing SIR interpretation (e.g., "42; S", "28 R")
x <- gsub("[;[:space:]]+[SIRsir]$", "", x)
# remove leading comparison operators (e.g., ">=20", "<=6")
x <- gsub("^[<>=]+\\s*", "", x)
x <- trimws(x)
# now the remainder must be a plausible standalone number
out <- rep(NA_real_, length(x))
is_numeric <- grepl("^[0-9]+\\.?[0-9]*$", x)
out[is_numeric] <- as.double(x[is_numeric])
out
}
# round up and make it an integer
x <- as.integer(ceiling(clean_double2(x)))
# round up and coerce to integer
x <- as.integer(ceiling(extract_disk_value(x)))
# valid disk diffusion zones: 0-50 mm
x[x < 0 | x > 50] <- NA_integer_
# disks can never be less than 0 mm or more than 50 mm
x[x < 0 | x > 99] <- NA_integer_
x[x > 50] <- 50L
na_after <- length(x[is.na(x)])
if (na_before != na_after) {

2
R/first_isolate.R Normal file → Executable file
View File

@@ -134,7 +134,7 @@
#' @seealso [key_antimicrobials()]
#' @export
#' @return A [logical] vector
#' @source Methodology of these functions is strictly based on:
#' @references Methodology of these functions is strictly based on:
#'
#' - **M39 Analysis and Presentation of Cumulative Antimicrobial Susceptibility Test Data, 5th Edition**, 2022, *Clinical and Laboratory Standards Institute (CLSI)*. <https://clsi.org/standards/products/microbiology/documents/m39/>.
#'

0
R/get_episode.R Normal file → Executable file
View File

View File

@@ -62,17 +62,17 @@ format_eucast_version_nr <- function(version, markdown = TRUE) {
#' @param x A data set with antimicrobials columns, such as `amox`, `AMX` and `AMC`.
#' @param info A [logical] to indicate whether progress should be printed to the console - the default is only print while in interactive sessions.
#' @param guideline A guideline name, either "EUCAST" (default) or "CLSI". This can be set with the package option [`AMR_guideline`][AMR-options].
#' @param rules A [character] vector that specifies which rules should be applied. Must be one or more of `"breakpoints"`, `"expected_phenotypes"`, `"expert"`, `"other"`, `"custom"`, `"all"`, and defaults to `c("breakpoints", "expected_phenotypes")`. The default value can be set to another value using the package option [`AMR_interpretive_rules`][AMR-options]: `options(AMR_interpretive_rules = "all")`. If using `"custom"`, be sure to fill in argument `custom_rules` too. Custom rules can be created with [custom_eucast_rules()].
#' @param rules A [character] vector that specifies which rules should be applied. Must be one or more of `"breakpoints"`, `"expected_phenotypes"`, `"expert"`, `"other"`, `"custom"`, `"all"`, and defaults to `c("breakpoints", "expected_phenotypes")`. The default value can be set to another value using the package option [`AMR_interpretive_rules`][AMR-options]: `options(AMR_interpretive_rules = "all")`. If using `"custom"`, be sure to fill in argument `custom_rules` too. Custom rules can be created with [custom_interpretive_rules()].
#' @param verbose A [logical] to turn Verbose mode on and off (default is off). In Verbose mode, the function does not apply rules to the data, but instead returns a data set in logbook form with extensive info about which rows and columns would be effected and in which way. Using Verbose mode takes a lot more time.
#' @param version_breakpoints The version number to use for the EUCAST Clinical Breakpoints guideline. Can be `r vector_or(names(EUCAST_VERSION_BREAKPOINTS), documentation = TRUE, reverse = TRUE)`.
#' @param version_expected_phenotypes The version number to use for the EUCAST Expected Phenotypes. Can be `r vector_or(names(EUCAST_VERSION_EXPECTED_PHENOTYPES), documentation = TRUE, reverse = TRUE)`.
#' @param version_expertrules The version number to use for the EUCAST Expert Rules and Intrinsic Resistance guideline. Can be `r vector_or(names(EUCAST_VERSION_EXPERT_RULES), documentation = TRUE, reverse = TRUE)`.
#' @param ampc_cephalosporin_resistance (only applies when `rules` contains `"expert"` or `"all"`) a [character] value that should be applied to cefotaxime, ceftriaxone and ceftazidime for AmpC de-repressed cephalosporin-resistant mutants - the default is `NA`. Currently only works when `version_expertrules` is `3.2` and higher; these versions of '*EUCAST Expert Rules on Enterobacterales*' state that results of cefotaxime, ceftriaxone and ceftazidime should be reported with a note, or results should be suppressed (emptied) for these three drugs. A value of `NA` (the default) for this argument will remove results for these three drugs, while e.g. a value of `"R"` will make the results for these drugs resistant. Use `NULL` or `FALSE` to not alter results for these three drugs of AmpC de-repressed cephalosporin-resistant mutants. Using `TRUE` is equal to using `"R"`. \cr For *EUCAST Expert Rules* v3.2, this rule applies to: `r vector_and(gsub("[^a-zA-Z ]+", "", unlist(strsplit(EUCAST_RULES_DF[which(EUCAST_RULES_DF$reference.version %in% c(3.2, 3.3) & EUCAST_RULES_DF$reference.rule %like% "ampc"), "this_value"][1], "|", fixed = TRUE))), quotes = "*")`.
#' @param ampc_cephalosporin_resistance (only applies when `rules` contains `"expert"` or `"all"`) a [character] value that should be applied to cefotaxime, ceftriaxone and ceftazidime for AmpC de-repressed cephalosporin-resistant mutants - the default is `NA`. Currently only works when `version_expertrules` is `3.2` and higher; these versions of '*EUCAST Expert Rules on Enterobacterales*' state that results of cefotaxime, ceftriaxone and ceftazidime should be reported with a note, or results should be suppressed (emptied) for these three drugs. A value of `NA` (the default) for this argument will remove results for these three drugs, while e.g. a value of `"R"` will make the results for these drugs resistant. Use `NULL` or `FALSE` to not alter results for these three drugs of AmpC de-repressed cephalosporin-resistant mutants. Using `TRUE` is equal to using `"R"`. \cr For *EUCAST Expert Rules* v3.2, this rule applies to: `r vector_and(gsub("[^a-zA-Z ]+", "", unlist(strsplit(INTERPRETIVE_RULES_DF[which(INTERPRETIVE_RULES_DF$reference.version %in% c(3.2, 3.3) & INTERPRETIVE_RULES_DF$reference.rule %like% "ampc"), "this_value"][1], "|", fixed = TRUE))), quotes = "*")`.
#' @param ... Column names of antimicrobials. To automatically detect antimicrobial column names, do not provide any named arguments; [guess_ab_col()] will then be used for detection. To manually specify a column, provide its name (case-insensitive) as an argument, e.g. `AMX = "amoxicillin"`. To skip a specific antimicrobial, set it to `NULL`, e.g. `TIC = NULL` to exclude ticarcillin. If a manually defined column does not exist in the data, it will be skipped with a warning.
#' @param ab Any (vector of) text that can be coerced to a valid antimicrobial drug code with [as.ab()].
#' @param administration Route of administration, either `r vector_or(dosage$administration, documentation = TRUE)`.
#' @param only_sir_columns A [logical] to indicate whether only antimicrobial columns must be included that were transformed to class [sir][as.sir()] on beforehand. Defaults to `FALSE` if no columns of `x` have a class [sir][as.sir()].
#' @param custom_rules Custom rules to apply, created with [custom_eucast_rules()].
#' @param custom_rules Custom rules to apply, created with [custom_interpretive_rules()].
#' @param overwrite A [logical] indicating whether to overwrite existing SIR values (default: `FALSE`). When `FALSE`, only non-SIR values are modified (i.e., any value that is not already S, I or R). To ensure compliance with EUCAST guidelines, **this should remain** `FALSE`, as EUCAST notes often state that an organism "should be tested for susceptibility to individual agents or be reported resistant".
#' @param add_if_missing A [logical] indicating whether rules should also be applied to missing (`NA`) values (default: `TRUE`). When `FALSE`, rules are only applied to cells that already contain an SIR value; cells with `NA` are left untouched. This is particularly useful when using `overwrite = TRUE` with custom rules and you want to update reported results without imputing values for untested drugs.
#' @inheritParams first_isolate
@@ -80,17 +80,17 @@ format_eucast_version_nr <- function(version, markdown = TRUE) {
#' **Note:** This function does not translate MIC or disk values to SIR values. Use [as.sir()] for that. \cr
#' **Note:** When ampicillin (AMP, J01CA01) is not available but amoxicillin (AMX, J01CA04) is, the latter will be used for all rules where there is a dependency on ampicillin. These drugs are interchangeable when it comes to expression of antimicrobial resistance. \cr
#'
#' The file containing all EUCAST rules is located here: <https://github.com/msberends/AMR/blob/main/data-raw/eucast_rules.tsv>. **Note:** Old taxonomic names are replaced with the current taxonomy where applicable. For example, *Ochrobactrum anthropi* was renamed to *Brucella anthropi* in 2020; the original EUCAST rules v3.1 and v3.2 did not yet contain this new taxonomic name. The `AMR` package contains the full microbial taxonomy updated until `r documentation_date(max(TAXONOMY_VERSION$GBIF$accessed_date, TAXONOMY_VERSION$LPSN$accessed_date))`, see [microorganisms].
#' The file containing all interpretive rules is located here: <https://github.com/msberends/AMR/blob/main/data-raw/interpretive_rules.tsv>. **Note:** Old taxonomic names are replaced with the current taxonomy where applicable. For example, *Ochrobactrum anthropi* was renamed to *Brucella anthropi* in 2020; the original EUCAST rules v3.1 and v3.2 did not yet contain this new taxonomic name. The `AMR` package contains the full microbial taxonomy updated until `r documentation_date(max(TAXONOMY_VERSION$GBIF$accessed_date, TAXONOMY_VERSION$LPSN$accessed_date))`, see [microorganisms].
#'
#' ### Custom Rules
#'
#' Custom rules can be created using [custom_eucast_rules()], e.g.:
#' Custom rules can be created using [custom_interpretive_rules()], e.g.:
#'
#' ```r
#' x <- custom_eucast_rules(AMC == "R" & genus == "Klebsiella" ~ aminopenicillins == "R",
#' AMC == "I" & genus == "Klebsiella" ~ aminopenicillins == "I")
#' x <- custom_interpretive_rules(AMC == "R" & genus == "Klebsiella" ~ aminopenicillins == "R",
#' AMC == "I" & genus == "Klebsiella" ~ aminopenicillins == "I")
#'
#' eucast_rules(example_isolates, rules = "custom", custom_rules = x)
#' interpretive_rules(example_isolates, rules = "custom", custom_rules = x)
#' ```
#'
#' ### 'Other' Rules
@@ -102,12 +102,12 @@ format_eucast_version_nr <- function(version, markdown = TRUE) {
#'
#' Important examples include amoxicillin and amoxicillin/clavulanic acid, and trimethoprim and trimethoprim/sulfamethoxazole. Needless to say, for these rules to work, both drugs must be available in the data set.
#'
#' Since these rules are not officially approved by EUCAST, they are not applied at default. To use these rules, include `"other"` to the `rules` argument, or use `eucast_rules(..., rules = "all")`. You can also set the package option [`AMR_interpretive_rules`][AMR-options], i.e. run `options(AMR_interpretive_rules = "all")`.
#' Since these rules are not officially approved by EUCAST, they are not applied at default. To use these rules, include `"other"` to the `rules` argument, or use `interpretive_rules(..., rules = "all")`. You can also set the package option [`AMR_interpretive_rules`][AMR-options], i.e. run `options(AMR_interpretive_rules = "all")`.
#' @aliases EUCAST
#' @rdname interpretive_rules
#' @export
#' @return The input of `x`, possibly with edited values of antimicrobials. Or, if `verbose = TRUE`, a [data.frame] with all original and new values of the affected bug-drug combinations.
#' @source
#' @references
#' - EUCAST Expert Rules. Version 2.0, 2012.\cr
#' Leclercq et al. **EUCAST expert rules in antimicrobial susceptibility testing.** *Clin Microbiol Infect.* 2013;19(2):141-60; \doi{https://doi.org/10.1111/j.1469-0691.2011.03703.x}
#' - EUCAST Expert Rules, Intrinsic Resistance and Exceptional Phenotypes Tables. Version 3.1, 2016. [(link)](https://www.eucast.org/fileadmin/src/media/PDFs/EUCAST_files/Expert_Rules/Expert_rules_intrinsic_exceptional_V3.1.pdf)
@@ -175,6 +175,11 @@ interpretive_rules <- function(x,
...) {
meet_criteria(x, allow_class = "data.frame")
meet_criteria(col_mo, allow_class = "character", has_length = 1, is_in = colnames(x), allow_NULL = TRUE)
if (guideline %like% "EUCAST") {
guideline <- "EUCAST"
} else if (guideline %like% "CLSI") {
guideline <- "CLSI"
}
meet_criteria(guideline, allow_class = "character", has_length = 1, is_in = c("EUCAST", "CLSI"))
meet_criteria(info, allow_class = "logical", has_length = 1)
meet_criteria(rules, allow_class = "character", has_length = c(1, 2, 3, 4, 5, 6), is_in = c("breakpoints", "expected_phenotypes", "expert", "other", "all", "custom"))
@@ -184,7 +189,7 @@ interpretive_rules <- function(x,
meet_criteria(version_expertrules, allow_class = c("numeric", "integer"), has_length = 1, is_in = as.double(names(EUCAST_VERSION_EXPERT_RULES)))
meet_criteria(ampc_cephalosporin_resistance, allow_class = c("logical", "character", "sir"), has_length = 1, allow_NA = TRUE, allow_NULL = TRUE)
meet_criteria(only_sir_columns, allow_class = "logical", has_length = 1)
meet_criteria(custom_rules, allow_class = "custom_eucast_rules", allow_NULL = TRUE)
meet_criteria(custom_rules, allow_class = c("custom_interpretive_rules", "custom_eucast_rules"), allow_NULL = TRUE)
meet_criteria(overwrite, allow_class = "logical", has_length = 1)
meet_criteria(add_if_missing, allow_class = "logical", has_length = 1)
@@ -193,11 +198,6 @@ interpretive_rules <- function(x,
"Either set {.arg overwrite} or {.arg add_if_missing} to {.code TRUE}, or both."
)
stop_if(
guideline == "CLSI",
"CLSI guideline is not yet supported."
)
stop_if(
!is.na(ampc_cephalosporin_resistance) && !any(c("expert", "all") %in% rules),
"For the {.arg ampc_cephalosporin_resistance} argument to work, the {.arg rules} argument must contain {.code \"expert\"} or {.code \"all\"}."
@@ -206,7 +206,7 @@ interpretive_rules <- function(x,
add_MO_lookup_to_AMR_env()
if ("custom" %in% rules && is.null(custom_rules)) {
warning_("in {.help [{.fun eucast_rules}](AMR::eucast_rules)}: no custom rules were set with the {.arg custom_rules} argument",
warning_("in {.help [{.fun interpretive_rules}](AMR::interpretive_rules)}: no custom rules were set with the {.arg custom_rules} argument",
immediate = TRUE
)
rules <- rules[rules != "custom"]
@@ -229,13 +229,13 @@ interpretive_rules <- function(x,
if (interactive() && isTRUE(verbose) && isTRUE(info)) {
txt <- paste0(
"WARNING: In Verbose mode, the eucast_rules() function does not apply rules to the data, but instead returns a data set in logbook form with comprehensive info about which rows and columns would be effected and in which way.",
"WARNING: In Verbose mode, the interpretive_rules() function does not apply rules to the data, but instead returns a data set in logbook form with comprehensive info about which rows and columns would be effected and in which way.",
"\n\nThis may overwrite your existing data if you use e.g.:",
"\ndata <- eucast_rules(data, verbose = TRUE)\n\nDo you want to continue?"
"\ndata <- interpretive_rules(data, verbose = TRUE)\n\nDo you want to continue?"
)
showQuestion <- import_fn("showQuestion", "rstudioapi", error_on_fail = FALSE)
if (!is.null(showQuestion)) {
q_continue <- showQuestion("Using verbose = TRUE with eucast_rules()", txt)
q_continue <- showQuestion("Using verbose = TRUE with interpretive_rules()", txt)
} else {
q_continue <- utils::menu(choices = c("OK", "Cancel"), graphics = FALSE, title = txt)
}
@@ -330,7 +330,7 @@ interpretive_rules <- function(x,
verbose = verbose,
info = info,
only_sir_columns = only_sir_columns,
fn = "eucast_rules",
fn = "interpretive_rules",
...
)
@@ -484,14 +484,13 @@ interpretive_rules <- function(x,
if (any(c("all", "other") %in% rules)) {
if (isTRUE(info)) {
cat(paste0("\n", font_grey(strrep("-", 0.95 * getOption("width", 100))), "\n"))
cat(word_wrap(
paste0(
"Rules by the ",
font_bold(paste0("AMR package v", utils::packageDescription("AMR")$Version)),
" (", format(as.Date(utils::packageDescription("AMR")$Date), format = "%Y"),
"), see {.help [{.fun eucast_rules}](AMR::eucast_rules)}\n"
)
))
message_(
"Rules by the ",
font_bold(paste0("AMR package v", utils::packageDescription("AMR")$Version)),
" (", format(as.Date(utils::packageDescription("AMR")$Date), format = "%Y"),
"), see {.help [{.fun interpretive_rules}](AMR::interpretive_rules)}",
as_note = FALSE
)
cat("\n\n")
}
ab_enzyme <- subset(AMR::antimicrobials, name %like% "/")[, c("ab", "name"), drop = FALSE]
@@ -522,10 +521,11 @@ interpretive_rules <- function(x,
tolower(ab_enzyme$enzyme_name[i]), " ({.field ", font_bold(col_enzyme), "}) = R"
)
if (isTRUE(info)) {
cat(word_wrap(rule_current,
width = getOption("width") - 30,
message_(rule_current,
as_note = FALSE,
appendLF = FALSE,
extra_indent = 6
))
)
}
run_changes <- edit_sir(
x = x,
@@ -611,59 +611,63 @@ interpretive_rules <- function(x,
if (!any(c("all", "custom") %in% rules) && !is.null(custom_rules)) {
if (isTRUE(info)) {
message_("Skipping custom EUCAST rules, since the {.arg rules} argument does not contain {.code \"custom\"}.")
message_("Skipping custom interpretive rules, since the {.arg rules} argument does not contain {.code \"custom\"}.")
}
custom_rules <- NULL
}
# >>> Apply Official EUCAST rules <<< ---------------------------------------------------
# >>> Apply Official interpretive rules <<< ---------------------------------------------------
eucast_notification_shown <- FALSE
if (!is.null(list(...)$eucast_rules_df)) {
# this allows: eucast_rules(x, eucast_rules_df = AMR:::EUCAST_RULES_DF |> filter(is.na(have_these_values)))
eucast_rules_df_total <- list(...)$eucast_rules_df
if (!is.null(list(...)$interpretive_rules_df)) {
# this allows: interpretive_rules(x, interpretive_rules_df = AMR:::INTERPRETIVE_RULES_DF |> filter(is.na(have_these_values)))
interpretive_rules_df_total <- list(...)$interpretive_rules_df
} else if (!is.null(list(...)$eucast_rules_df)) {
# deprecated parameter name kept for backward compatibility
interpretive_rules_df_total <- list(...)$eucast_rules_df
warning("Used interpretive_rules(x, eucast_rules_df = ...) - Do use newer argument interpretive_rules_df now.")
} else {
# otherwise internal data file, created in data-raw/_pre_commit_checks.R
eucast_rules_df_total <- EUCAST_RULES_DF
# internal data file, created in data-raw/_pre_commit_checks.R
interpretive_rules_df_total <- INTERPRETIVE_RULES_DF
}
## filter on user-set guideline versions ----
eucast_rules_df <- data.frame()
## filter on guideline provider and user-set guideline versions ----
interpretive_rules_df <- data.frame()
if (any(c("all", "breakpoints") %in% rules)) {
eucast_rules_df <- eucast_rules_df %pm>%
rbind_AMR(eucast_rules_df_total %pm>%
subset(reference.rule_group %like% "breakpoint" & reference.version == version_breakpoints))
interpretive_rules_df <- interpretive_rules_df %pm>%
rbind_AMR(interpretive_rules_df_total %pm>%
subset(rule.provider == guideline & reference.rule_group %like% "breakpoint" & reference.version == version_breakpoints))
}
if (any(c("all", "expected_phenotypes") %in% rules)) {
eucast_rules_df <- eucast_rules_df %pm>%
rbind_AMR(eucast_rules_df_total %pm>%
subset(reference.rule_group %like% "expected" & reference.version == version_expected_phenotypes))
interpretive_rules_df <- interpretive_rules_df %pm>%
rbind_AMR(interpretive_rules_df_total %pm>%
subset(rule.provider == guideline & reference.rule_group %like% "expected" & reference.version == version_expected_phenotypes))
}
if (any(c("all", "expert") %in% rules)) {
eucast_rules_df <- eucast_rules_df %pm>%
rbind_AMR(eucast_rules_df_total %pm>%
subset(reference.rule_group %like% "expert" & reference.version == version_expertrules))
interpretive_rules_df <- interpretive_rules_df %pm>%
rbind_AMR(interpretive_rules_df_total %pm>%
subset(rule.provider == guideline & reference.rule_group %like% "expert" & reference.version == version_expertrules))
}
## filter out AmpC de-repressed cephalosporin-resistant mutants ----
# no need to filter on version number here - the rules contain these version number, so are inherently filtered
# cefotaxime, ceftriaxone, ceftazidime
if (is.null(ampc_cephalosporin_resistance) || isFALSE(ampc_cephalosporin_resistance)) {
eucast_rules_df <- subset(
eucast_rules_df,
interpretive_rules_df <- subset(
interpretive_rules_df,
reference.rule %unlike% "ampc"
)
} else {
if (isTRUE(ampc_cephalosporin_resistance)) {
ampc_cephalosporin_resistance <- "R"
}
if (!is.null(eucast_rules_df$reference.rule)) {
eucast_rules_df[which(eucast_rules_df$reference.rule %like% "ampc"), "to_value"] <- as.character(ampc_cephalosporin_resistance)
if (!is.null(interpretive_rules_df$reference.rule)) {
interpretive_rules_df[which(interpretive_rules_df$reference.rule %like% "ampc"), "to_value"] <- as.character(ampc_cephalosporin_resistance)
}
}
# sometimes, the screenings are missing but the names are actually available
# we only hints on remaining rows in `eucast_rules_df`
# we only hints on remaining rows in `interpretive_rules_df`
screening_abx <- as.character(AMR::antimicrobials$ab[which(AMR::antimicrobials$ab %like% "-S$")])
screening_abx <- screening_abx[screening_abx %in% unique(unlist(strsplit(EUCAST_RULES_DF$and_these_antibiotics[!is.na(EUCAST_RULES_DF$and_these_antibiotics)], ", *")))]
screening_abx <- screening_abx[screening_abx %in% unique(unlist(strsplit(interpretive_rules_df_total$and_these_antibiotics[!is.na(interpretive_rules_df_total$and_these_antibiotics)], ", *")))]
if (isTRUE(info)) {
cat("\n")
}
@@ -682,12 +686,12 @@ interpretive_rules <- function(x,
}
## Go over all rules and apply them ----
for (i in seq_len(nrow(eucast_rules_df))) {
rule_previous <- eucast_rules_df[max(1, i - 1), "reference.rule", drop = TRUE]
rule_current <- eucast_rules_df[i, "reference.rule", drop = TRUE]
rule_next <- eucast_rules_df[min(nrow(eucast_rules_df), i + 1), "reference.rule", drop = TRUE]
rule_group_previous <- eucast_rules_df[max(1, i - 1), "reference.rule_group", drop = TRUE]
rule_group_current <- eucast_rules_df[i, "reference.rule_group", drop = TRUE]
for (i in seq_len(nrow(interpretive_rules_df))) {
rule_previous <- interpretive_rules_df[max(1, i - 1), "reference.rule", drop = TRUE]
rule_current <- interpretive_rules_df[i, "reference.rule", drop = TRUE]
rule_next <- interpretive_rules_df[min(nrow(interpretive_rules_df), i + 1), "reference.rule", drop = TRUE]
rule_group_previous <- interpretive_rules_df[max(1, i - 1), "reference.rule_group", drop = TRUE]
rule_group_current <- interpretive_rules_df[i, "reference.rule_group", drop = TRUE]
# don't apply rules if user doesn't want to apply them
if (rule_group_current %like% "breakpoint" && !any(c("all", "breakpoints") %in% rules)) {
next
@@ -702,16 +706,16 @@ interpretive_rules <- function(x,
if (isFALSE(info) || isFALSE(verbose)) {
rule_text <- ""
} else {
if (is.na(eucast_rules_df[i, "and_these_antibiotics", drop = TRUE])) {
rule_text <- paste0("always report as '", eucast_rules_df[i, "to_value", drop = TRUE], "': ", get_antibiotic_names(eucast_rules_df[i, "then_change_these_antibiotics", drop = TRUE]))
if (is.na(interpretive_rules_df[i, "and_these_antibiotics", drop = TRUE])) {
rule_text <- paste0("always report as '", interpretive_rules_df[i, "to_value", drop = TRUE], "': ", get_antibiotic_names(interpretive_rules_df[i, "then_change_these_antibiotics", drop = TRUE]))
} else {
rule_text <- paste0(
"report as '", eucast_rules_df[i, "to_value", drop = TRUE], "' when ",
"report as '", interpretive_rules_df[i, "to_value", drop = TRUE], "' when ",
format_antibiotic_names(
ab_names = get_antibiotic_names(eucast_rules_df[i, "and_these_antibiotics", drop = TRUE]),
ab_results = eucast_rules_df[i, "have_these_values", drop = TRUE]
ab_names = get_antibiotic_names(interpretive_rules_df[i, "and_these_antibiotics", drop = TRUE]),
ab_results = interpretive_rules_df[i, "have_these_values", drop = TRUE]
), ": ",
get_antibiotic_names(eucast_rules_df[i, "then_change_these_antibiotics", drop = TRUE])
get_antibiotic_names(interpretive_rules_df[i, "then_change_these_antibiotics", drop = TRUE])
)
}
}
@@ -720,7 +724,7 @@ interpretive_rules <- function(x,
rule_previous <- ""
rule_group_previous <- ""
}
if (i == nrow(eucast_rules_df)) {
if (i == nrow(interpretive_rules_df)) {
rule_next <- ""
}
@@ -789,13 +793,13 @@ interpretive_rules <- function(x,
}
## Get rule from file ------------------------------------------------------
if_mo_property <- trimws(eucast_rules_df[i, "if_mo_property", drop = TRUE])
like_is_one_of <- trimws(eucast_rules_df[i, "like.is.one_of", drop = TRUE])
mo_value <- trimws(eucast_rules_df[i, "this_value", drop = TRUE])
source_antibiotics <- eucast_rules_df[i, "and_these_antibiotics", drop = TRUE]
source_value <- trimws(unlist(strsplit(eucast_rules_df[i, "have_these_values", drop = TRUE], ",", fixed = TRUE)))
target_antibiotics <- eucast_rules_df[i, "then_change_these_antibiotics", drop = TRUE]
target_value <- eucast_rules_df[i, "to_value", drop = TRUE]
if_mo_property <- trimws(interpretive_rules_df[i, "if_mo_property", drop = TRUE])
like_is_one_of <- trimws(interpretive_rules_df[i, "like.is.one_of", drop = TRUE])
mo_value <- trimws(interpretive_rules_df[i, "this_value", drop = TRUE])
source_antibiotics <- interpretive_rules_df[i, "and_these_antibiotics", drop = TRUE]
source_value <- trimws(unlist(strsplit(interpretive_rules_df[i, "have_these_values", drop = TRUE], ",", fixed = TRUE)))
target_antibiotics <- interpretive_rules_df[i, "then_change_these_antibiotics", drop = TRUE]
target_value <- interpretive_rules_df[i, "to_value", drop = TRUE]
# if amo_value contains a group name, expand that name with all species in it
if (any(trimws(strsplit(mo_value, ",")[[1]]) %in% AMR::microorganisms.groups$mo_group_name, na.rm = TRUE)) {
@@ -894,7 +898,7 @@ interpretive_rules <- function(x,
if (!is.null(custom_rules)) {
if (isTRUE(info)) {
cat("\n")
cat(font_bold("Custom EUCAST rules, set by user"), "\n")
cat(font_bold("Custom interpretive rules, set by user"), "\n")
}
for (i in seq_len(length(custom_rules))) {
rule <- custom_rules[[i]]
@@ -929,8 +933,8 @@ interpretive_rules <- function(x,
to = target_value,
rule = c(
rule_text,
"Custom EUCAST rules",
paste0("Custom EUCAST rule ", i),
"Custom interpretive rules",
paste0("Custom interpretive rule ", i),
paste0(
"Object '", deparse(substitute(custom_rules)),
"' consisting of ", length(custom_rules), " custom rules"
@@ -1075,7 +1079,7 @@ interpretive_rules <- function(x,
warn_lacking_sir_class <- warn_lacking_sir_class[order(colnames(x.bak))]
warn_lacking_sir_class <- warn_lacking_sir_class[!is.na(warn_lacking_sir_class)]
warning_(
"in {.help [{.fun eucast_rules}](AMR::eucast_rules)}: not all columns with antimicrobial results are of class {.cls sir}. Transform them on beforehand, e.g.:\n\n",
"in {.help [{.fun interpretive_rules}](AMR::interpretive_rules)}: not all columns with antimicrobial results are of class {.cls sir}. Transform them on beforehand, e.g.:\n\n",
"\u00a0\u00a0", AMR_env$bullet_icon, " ", highlight_code(paste0(x_deparsed, " |> as.sir(", ifelse(length(warn_lacking_sir_class) == 1,
warn_lacking_sir_class,
paste0(warn_lacking_sir_class[1], ":", warn_lacking_sir_class[length(warn_lacking_sir_class)])
@@ -1177,7 +1181,7 @@ edit_sir <- function(x,
new_edits[rows, cols] == "NS")
non_SIR <- !isSIR
if (isFALSE(overwrite) && any(isSIR) && message_not_thrown_before("edit_sir.warning_overwrite")) {
warning_("in {.help [{.fun eucast_rules}](AMR::eucast_rules)}: some columns had SIR values which were not overwritten, since {.code overwrite = FALSE}.")
warning_("in {.help [{.fun interpretive_rules}](AMR::interpretive_rules)}: some columns had SIR values which were not overwritten, since {.code overwrite = FALSE}.")
}
# determine which cells to modify based on overwrite and add_if_missing
if (isTRUE(overwrite)) {
@@ -1211,7 +1215,7 @@ edit_sir <- function(x,
})
suppressWarnings(do_assign())
warning_(
"in {.help [{.fun eucast_rules}](AMR::eucast_rules)}: value \"", to, "\" added to the factor levels of column",
"in {.help [{.fun interpretive_rules}](AMR::interpretive_rules)}: value \"", to, "\" added to the factor levels of column",
ifelse(length(cols) == 1, "", "s"),
" ", vector_and(cols, quotes = "`", sort = FALSE),
" because this value was not an existing factor level."
@@ -1219,7 +1223,7 @@ edit_sir <- function(x,
txt_warning()
warned <<- FALSE
} else {
warning_("in {.help [{.fun eucast_rules}](AMR::eucast_rules)}: ", w$message)
warning_("in {.help [{.fun interpretive_rules}](AMR::interpretive_rules)}: ", w$message)
txt_warning()
}
},

View File

@@ -161,11 +161,11 @@ key_antimicrobials <- function(x = NULL,
if (is.null(col_mo)) {
warning_("in {.fun key_antimicrobials}: no column found for {.arg col_mo}, ignoring antibiotics set in {.arg gram_negative} and {.arg gram_positive}, and antimycotics set in {.arg antifungal}")
gramstain <- NA_character_
kingdom <- NA_character_
domain <- NA_character_
} else {
x.mo <- as.mo(x[, col_mo, drop = TRUE])
gramstain <- mo_gramstain(x.mo, language = NULL)
kingdom <- mo_kingdom(x.mo, language = NULL)
domain <- mo_domain(x.mo, language = NULL)
}
AMR_string <- function(x, values, name, filter, cols = cols) {
@@ -219,11 +219,11 @@ key_antimicrobials <- function(x = NULL,
cols = cols
)
key_ab[which(kingdom == "Fungi")] <- AMR_string(
key_ab[which(domain == "Fungi")] <- AMR_string(
x = x,
values = antifungal,
name = "antifungal",
filter = kingdom == "Fungi",
filter = domain == "Fungi",
cols = cols
)

View File

@@ -1511,8 +1511,8 @@ mdro <- function(x = NULL,
fluoroquinolones <- c(CIP, NOR, LVX) # note 5: ciprofloxacin or norfloxacin or levofloxacin
carbapenems <- carbapenems[!is.na(carbapenems)]
carbapenems_without_imipenem <- carbapenems[carbapenems != IPM]
amino <- AMX %or% AMP
third <- CAZ %or% CTX
amino <- AMX %or_if_na% AMP
third <- CAZ %or_if_na% CTX
ESBLs <- c(amino, third)
ESBLs <- ESBLs[!is.na(ESBLs)]
if (length(ESBLs) != 2) {
@@ -1524,7 +1524,7 @@ mdro <- function(x = NULL,
trans_tbl(
2, # positive, unconfirmed
rows = which(x$order == "Enterobacterales" & col_values(x, ESBLs[1]) == "R" & col_values(x, ESBLs[2]) == "R" & is.na(esbl)),
cols = c(AMX %or% AMP, cephalosporins_3rd),
cols = c(AMX %or_if_na% AMP, cephalosporins_3rd),
any_all = "all",
reason = "Enterobacterales: potential ESBL"
)
@@ -1634,7 +1634,7 @@ mdro <- function(x = NULL,
trans_tbl(
3,
rows = which(x$genus == "Enterococcus" & x$species == "faecium"),
cols = c(PEN %or% AMX %or% AMP, VAN),
cols = c(PEN %or_if_na% AMX %or_if_na% AMP, VAN),
any_all = "all",
reason = "E. faecium: vancomycin + penicillin group"
)
@@ -1677,8 +1677,8 @@ mdro <- function(x = NULL,
aminoglycosides <- aminoglycosides[!is.na(aminoglycosides)]
fluoroquinolones <- fluoroquinolones[!is.na(fluoroquinolones)]
carbapenems <- carbapenems[!is.na(carbapenems)]
amino <- AMX %or% AMP
third <- CAZ %or% CTX
amino <- AMX %or_if_na% AMP
third <- CAZ %or_if_na% CTX
ESBLs <- c(amino, third)
ESBLs <- ESBLs[!is.na(ESBLs)]
if (length(ESBLs) != 2) {

0
R/mic.R Normal file → Executable file
View File

95
R/mo.R
View File

@@ -29,7 +29,7 @@
#' Transform Arbitrary Input to Valid Microbial Taxonomy
#'
#' Use this function to get a valid microorganism code ([`mo`]) based on arbitrary user input. Determination is done using intelligent rules and the complete taxonomic tree of the kingdoms `r vector_and(unique(microorganisms$kingdom[which(!grepl("(unknown|Fungi)", microorganisms$kingdom))]), quotes = FALSE)`, and most microbial species from the kingdom Fungi (see *Source*). The input can be almost anything: a full name (like `"Staphylococcus aureus"`), an abbreviated name (such as `"S. aureus"`), an abbreviation known in the field (such as `"MRSA"`), or just a genus. See *Examples*.
#' Use this function to get a valid microorganism code ([`mo`]) based on arbitrary user input. Determination is done using intelligent rules and the complete taxonomic tree of the domains `r vector_and(unique(microorganisms$domain[which(!grepl("(unknown|Fungi)", microorganisms$domain))]), quotes = FALSE)`, and most microbial species from the domain Fungi (see *Source*). The input can be almost anything: a full name (like `"Staphylococcus aureus"`), an abbreviated name (such as `"S. aureus"`), an abbreviation known in the field (such as `"MRSA"`), or just a genus. See *Examples*.
#' @param x A [character] vector or a [data.frame] with one or two columns.
#' @param Becker A [logical] to indicate whether staphylococci should be categorised into coagulase-negative staphylococci ("CoNS") and coagulase-positive staphylococci ("CoPS") instead of their own species, according to Karsten Becker *et al.* (see *Source*). Please see *Details* for a full list of staphylococcal species that will be converted.
#'
@@ -37,14 +37,14 @@
#' @param Lancefield A [logical] to indicate whether a beta-haemolytic *Streptococcus* should be categorised into Lancefield groups instead of their own species, according to Rebecca C. Lancefield (see *Source*). These streptococci will be categorised in their first group, e.g. *Streptococcus dysgalactiae* will be group C, although officially it was also categorised into groups G and L. . Please see *Details* for a full list of streptococcal species that will be converted.
#'
#' This excludes enterococci at default (who are in group D), use `Lancefield = "all"` to also categorise all enterococci as group D.
#' @param minimum_matching_score A numeric value to set as the lower limit for the [MO matching score][mo_matching_score()]. When left blank, this will be determined automatically based on the character length of `x`, its [taxonomic kingdom][microorganisms] and [human pathogenicity][mo_matching_score()].
#' @param minimum_matching_score A numeric value to set as the lower limit for the [MO matching score][mo_matching_score()]. When left blank, this will be determined automatically based on the character length of `x`, its [taxonomic domain][microorganisms] and [human pathogenicity][mo_matching_score()].
#' @param keep_synonyms A [logical] to indicate if outdated, previously valid taxonomic names must be preserved and not be corrected to currently accepted names. Do note that the term "synonym" is in this case jargon from the field of microbial taxonomy - it is not in place to denote that e.g. "Streptococcus Group A" is a synonym of *S. pyogenes*. Though this is practically the case, taxonomically it is not as "Streptococcus Group A" is not even a valid taxonomic name.
#'
#' The default is `FALSE`, which will return a note if outdated taxonomic names were processed. The default can be set with the package option [`AMR_keep_synonyms`][AMR-options], i.e. `options(AMR_keep_synonyms = TRUE)` or `options(AMR_keep_synonyms = FALSE)`.
#' @param reference_df A [data.frame] to be used for extra reference when translating `x` to a valid [`mo`]. See [set_mo_source()] and [get_mo_source()] to automate the usage of your own codes (e.g. used in your analysis or organisation).
#' @param ignore_pattern A Perl-compatible [regular expression][base::regex] (case-insensitive) of which all matches in `x` must return `NA`. This can be convenient to exclude known non-relevant input and can also be set with the package option [`AMR_ignore_pattern`][AMR-options], e.g. `options(AMR_ignore_pattern = "(not reported|contaminated flora)")`.
#' @param cleaning_regex A Perl-compatible [regular expression][base::regex] (case-insensitive) to clean the input of `x`. Every matched part in `x` will be removed. At default, this is the outcome of [mo_cleaning_regex()], which removes texts between brackets and texts such as "species" and "serovar". The default can be set with the package option [`AMR_cleaning_regex`][AMR-options].
#' @param only_fungi A [logical] to indicate if only fungi must be found, making sure that e.g. misspellings always return records from the kingdom of Fungi. This can be set globally for [all microorganism functions][mo_property()] with the package option [`AMR_only_fungi`][AMR-options], i.e. `options(AMR_only_fungi = TRUE)`.
#' @param only_fungi A [logical] to indicate if only fungi must be found, making sure that e.g. misspellings always return records from the domain of Fungi. This can be set globally for [all microorganism functions][mo_property()] with the package option [`AMR_only_fungi`][AMR-options], i.e. `options(AMR_only_fungi = TRUE)`.
#' @param language Language to translate text like "no growth", which defaults to the system language (see [get_AMR_locale()]).
#' @param info A [logical] to indicate that info must be printed, e.g. a progress bar when more than 25 items are to be coerced, or a list with outdated taxonomic names. The default is `TRUE` only in interactive mode.
#' @param ... Other arguments passed on to functions.
@@ -64,7 +64,7 @@
#' | | | \---> subspecies, a 3-5 letter acronym
#' | | \----> species, a 3-6 letter acronym
#' | \----> genus, a 4-8 letter acronym
#' \----> kingdom: A (Archaea), AN (Animalia), B (Bacteria),
#' \----> domain: A (Archaea), AN (Animalia), B (Bacteria),
#' C (Chromista), F (Fungi), PL (Plantae),
#' P (Protozoa)
#' ```
@@ -77,7 +77,7 @@
#'
#' ### Coping with Uncertain Results
#'
#' Results of non-exact taxonomic input are based on their [matching score][mo_matching_score()]. The lowest allowed score can be set with the `minimum_matching_score` argument. At default this will be determined based on the character length of the input, the [taxonomic kingdom][microorganisms], and the [human pathogenicity][mo_matching_score()] of the taxonomic outcome. If values are matched with uncertainty, a message will be shown to suggest the user to inspect the results with [mo_uncertainties()], which returns a [data.frame] with all specifications.
#' Results of non-exact taxonomic input are based on their [matching score][mo_matching_score()]. The lowest allowed score can be set with the `minimum_matching_score` argument. At default this will be determined based on the character length of the input, the [taxonomic domain][microorganisms], and the [human pathogenicity][mo_matching_score()] of the taxonomic outcome. If values are matched with uncertainty, a message will be shown to suggest the user to inspect the results with [mo_uncertainties()], which returns a [data.frame] with all specifications.
#'
#' To increase the quality of matching, the `cleaning_regex` argument is used to clean the input. This must be a [regular expression][base::regex] that matches parts of the input that should be removed before the input is matched against the [available microbial taxonomy][microorganisms]. It will be matched Perl-compatible and case-insensitive. The default value of `cleaning_regex` is the outcome of the helper function [mo_cleaning_regex()].
#'
@@ -241,7 +241,7 @@ as.mo <- function(x,
out[is.na(out) & toupper(x) %in% AMR_env$MO_lookup$mo] <- toupper(x[is.na(out) & toupper(x) %in% AMR_env$MO_lookup$mo])
# From full name ----
out[is.na(out) & x_lower %in% AMR_env$MO_lookup$fullname_lower] <- AMR_env$MO_lookup$mo[match(x_lower[is.na(out) & x_lower %in% AMR_env$MO_lookup$fullname_lower], AMR_env$MO_lookup$fullname_lower)]
# one exception: "Fungi" matches the kingdom, but instead it should return the 'unknown' code for fungi
# one exception: "Fungi" matches the domain, but instead it should return the 'unknown' code for fungi
out[out == "F_[KNG]_FUNGI"] <- "F_FUNGUS"
# From known codes ----
ind <- is.na(out) & toupper(x) %in% AMR::microorganisms.codes$code
@@ -300,7 +300,7 @@ as.mo <- function(x,
MO_lookup_current <- AMR_env$MO_lookup
if (isTRUE(only_fungi)) {
MO_lookup_current <- MO_lookup_current[MO_lookup_current$kingdom == "Fungi", , drop = FALSE]
MO_lookup_current <- MO_lookup_current[MO_lookup_current$domain == "Fungi", , drop = FALSE]
}
# run it
@@ -322,6 +322,15 @@ as.mo <- function(x,
return(as.character(MO_lookup_current$mo[match(x_out, MO_lookup_current$fullname_lower)]))
}
# Issue #287: "X complex" is not a distinct taxon - strip " complex" and try "X"
if (grepl(" complex$", x_out, ignore.case = FALSE)) {
x_out <- sub(" complex$", "", x_out)
x_search_cleaned <- sub(" [Cc]omplex$", "", x_search_cleaned)
if (x_out %in% MO_lookup_current$fullname_lower) {
return(as.character(MO_lookup_current$mo[match(x_out, MO_lookup_current$fullname_lower)]))
}
}
# input must not be too short
if (nchar(x_out) < 3) {
return("UNKNOWN")
@@ -343,6 +352,36 @@ as.mo <- function(x,
(MO_lookup_current$species_first == substr(x_parts[2], 1, 1) |
MO_lookup_current$subspecies_first == substr(x_parts[2], 1, 1) |
MO_lookup_current$subspecies_first == substr(x_parts[3], 1, 1)))
# Issue #288 (extended): if the species (and subspecies) word(s) in the input
# exactly match candidates that all belong to one and the same genus, bypass the
# 0.55 cutoff. A species together with its subspecies/autonyms (e.g. Plasmodium
# ovale + curtisi + wallikeri) is the same taxon, so for a genus+species input we
# collapse to the species-rank record (subspecies == ""). This prevents prevalent
# bacteria from outranking a rarer organism whose species epithet is an
# unambiguous exact match, e.g. "S. apiospermum" -> Scedosporium, "P. ovale" ->
# Plasmodium ovale. If two different genera share the epithet, the genus check
# stays FALSE and the normal matching score arbitrates.
sp_exact <- tolower(MO_lookup_current$species[filtr]) == x_parts[2]
if (length(x_parts) == 3) {
sp_exact <- sp_exact & tolower(MO_lookup_current$subspecies[filtr]) == x_parts[3]
}
exact_idx <- filtr[sp_exact]
if (length(exact_idx) >= 1 &&
length(unique(MO_lookup_current$genus_lower[exact_idx])) == 1) {
if (length(x_parts) == 2) {
# genus + species only: collapse to the species-rank record (subspecies == "")
is_species_rank <- MO_lookup_current$subspecies[exact_idx] == ""
if (any(is_species_rank)) {
filtr <- exact_idx[is_species_rank][1]
} else {
filtr <- exact_idx[1]
}
} else {
# explicit subspecies given, unambiguous within the genus
filtr <- exact_idx[1]
}
minimum_matching_score <- 0
}
} else {
filtr <- which(MO_lookup_current$full_first == substr(x_parts[1], 1, 1) |
MO_lookup_current$species_first == substr(x_parts[2], 1, 1) |
@@ -385,8 +424,8 @@ as.mo <- function(x,
minimum_matching_score_current <- min(0.6, min(10, nchar(x_search_cleaned)) * 0.08)
# correct back for prevalence
minimum_matching_score_current <- minimum_matching_score_current / MO_lookup_current$prevalence[match(mo_to_search, MO_lookup_current$fullname)]
# correct back for kingdom
minimum_matching_score_current <- minimum_matching_score_current / MO_lookup_current$kingdom_index[match(mo_to_search, MO_lookup_current$fullname)]
# correct back for domain
minimum_matching_score_current <- minimum_matching_score_current / MO_lookup_current$domain_index[match(mo_to_search, MO_lookup_current$fullname)]
minimum_matching_score_current <- pmax(minimum_matching_score_current, m)
if (length(x_parts) > 1 && all(m <= 0.55, na.rm = TRUE)) {
# if the highest score is 0.5, we have nothing serious - 0.5 is the lowest for pathogenic group 1
@@ -647,7 +686,7 @@ NA_mo_ <- set_clean_class(NA_character_,
pillar_shaft.mo <- function(x, ...) {
add_MO_lookup_to_AMR_env()
out <- trimws(format(x))
# grey out the kingdom (part until first "_")
# grey out the domain (part until first "_")
out[!is.na(x)] <- gsub("^([A-Z]+_)(.*)", paste0(pillar::style_subtle("\\1"), "\\2"), out[!is.na(x)], perl = TRUE)
# and grey out every _
out[!is.na(x)] <- gsub("_", pillar::style_subtle("_"), out[!is.na(x)])
@@ -673,9 +712,7 @@ pillar_shaft.mo <- function(x, ...) {
(!is.null(df) && !all(unlist(df[, which(mo_cols), drop = FALSE]) %in% all_mos))) {
# markup old mo codes
out[!x %in% all_mos] <- font_italic(
pillar::style_na(x[!x %in% all_mos],
collapse = NULL
),
pillar::style_na(x[!x %in% all_mos]),
collapse = NULL
)
# throw a warning with the affected column name(s)
@@ -685,7 +722,7 @@ pillar_shaft.mo <- function(x, ...) {
col <- "The data"
}
warning_(
col, " contains old MO codes (from a previous AMR package version). ",
col, " contains old MO codes (from another AMR package version). ",
"Please update your MO codes with {.help [{.fun as.mo}](AMR::as.mo)}.",
call = FALSE
)
@@ -1002,17 +1039,19 @@ print.mo_uncertainties <- function(x, n = 10, ...) {
message_(out2, as_note = FALSE)
}
other_matches <- paste0(
"Also matched: ",
vector_and(
paste0(
candidates_formatted,
font_blue(paste0(" (", scores_formatted, ")"), collapse = NULL)
),
quotes = FALSE, sort = FALSE
if (x[i, ]$candidates != "") {
other_matches <- paste0(
"Also matched: ",
vector_and(
paste0(
candidates_formatted,
font_blue(paste0(" (", scores_formatted, ")"), collapse = NULL)
),
quotes = FALSE, sort = FALSE
)
)
)
message_(other_matches, as_note = FALSE)
message_(other_matches, as_note = FALSE)
}
}
if (isTRUE(any_maxed_out)) {
@@ -1228,13 +1267,13 @@ replace_old_mo_codes <- function(x, property) {
solved_unique <- unlist(lapply(
strsplit(affected_unique, ""),
function(m) {
kingdom <- paste0("^", m[1])
domain <- paste0("^", m[1])
name <- m[3:length(m)]
name[name == "_"] <- " "
name <- tolower(paste0(name, ".*", collapse = ""))
name <- gsub(" .*", " ", name, fixed = TRUE)
name <- paste0("^", name)
results <- AMR_env$MO_lookup$mo[AMR_env$MO_lookup$kingdom %like_case% kingdom &
results <- AMR_env$MO_lookup$mo[AMR_env$MO_lookup$domain %like_case% domain &
AMR_env$MO_lookup$fullname_lower %like_case% name]
if (length(results) > 1) {
all_direct_matches <<- FALSE
@@ -1258,14 +1297,14 @@ replace_old_mo_codes <- function(x, property) {
warning_(
"in {.help [{.fun mo_", property, "}](AMR::mo_", property, ")}: the input contained ", n_matched,
" old MO code", ifelse(n_matched == 1, "", "s"),
" (", n_unique, "from a previous AMR package version). ",
" (", n_unique, "from another AMR package version). ",
"Please update your MO codes with {.help [{.fun as.mo}](AMR::as.mo)} to increase speed."
)
} else {
warning_(
"in {.help [{.fun as.mo}](AMR::as.mo)}: the input contained ", n_matched,
" old MO code", ifelse(n_matched == 1, "", "s"),
" (", n_unique, "from a previous AMR package version). ",
" (", n_unique, "from another AMR package version). ",
n_solved, " old MO code", ifelse(n_solved == 1, "", "s"),
ifelse(n_solved == 1, " was", " were"),
ifelse(all_direct_matches, " updated ", font_bold(" guessed ")),

View File

@@ -47,7 +47,7 @@
#' * \eqn{l_n} is the length of \eqn{n};
#' * \eqn{lev} is the [Levenshtein distance function](https://en.wikipedia.org/wiki/Levenshtein_distance) (counting any insertion as 1, and any deletion or substitution as 2) that is needed to change \eqn{x} into \eqn{n};
#' * \eqn{p_n} is the human pathogenic prevalence group of \eqn{n}, as described below;
#' * \eqn{k_n} is the taxonomic kingdom of \eqn{n}, set as Bacteria = 1, Fungi = 1.25, Protozoa = 1.5, Chromista = 1.75, Archaea = 2, others = 3.
#' * \eqn{k_n} is the taxonomic domain ('kingdom' until taxonomic reclassification of 2024) of \eqn{n}, set as Bacteria = 1, Fungi = 1.25, Protozoa = 1.5, Chromista = 1.75, Archaea = 2, others = 3.
#'
#' The grouping into human pathogenic prevalence \eqn{p} is based on recent work from Bartlett *et al.* (2022, \doi{10.1099/mic.0.001269}) who extensively studied medical-scientific literature to categorise all bacterial species into these groups:
#'
@@ -122,8 +122,8 @@ mo_matching_score <- function(x, n) {
# human pathogenic prevalence (1 to 3), see ?as.mo
p_n <- AMR_env$MO_lookup[match(n, AMR_env$MO_lookup$fullname), "prevalence", drop = TRUE]
# kingdom index (Bacteria = 1, Fungi = 2, Protozoa = 3, Archaea = 4, others = 5)
k_n <- AMR_env$MO_lookup[match(n, AMR_env$MO_lookup$fullname), "kingdom_index", drop = TRUE]
# domain index (Bacteria = 1, Fungi = 2, Protozoa = 3, Archaea = 4, others = 5)
k_n <- AMR_env$MO_lookup[match(n, AMR_env$MO_lookup$fullname), "domain_index", drop = TRUE]
# matching score:
(l_n - 0.5 * l_n.lev) / (l_n * p_n * k_n)

View File

@@ -42,21 +42,23 @@
#' - `mo_ref("Enterobacter aerogenes")` will return `"Tindall et al., 2017"` (with a note about the renaming)
#' - `mo_ref("Enterobacter aerogenes", keep_synonyms = TRUE)` will return `"Hormaeche et al., 1960"` (with a once-per-session warning that the name is outdated)
#'
#' 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* (kingdom of Bacteria) as well as *Entamoeba coli* (kingdom 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 be considered *Escherichia coli*. As a result, `mo_fullname(mo_shortname("Entamoeba coli"))` returns `"Escherichia coli"`.
#' [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.
#'
#' Since the top-level of the taxonomy is sometimes referred to as 'kingdom' and sometimes as 'domain', the functions [mo_kingdom()] and [mo_domain()] return the exact same results.
#' 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* (kingdom of Bacteria) as well as *Entamoeba coli* (kingdom 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.
#'
#' Determination of human pathogenicity ([mo_pathogenicity()]) is strongly based on Bartlett *et al.* (2022, \doi{10.1099/mic.0.001269}). This function returns a [factor] with the levels *Pathogenic*, *Potentially pathogenic*, *Non-pathogenic*, and *Unknown*.
#'
#' Determination of the Gram stain ([mo_gramstain()]) will be based on the taxonomic kingdom and phylum. Originally, Cavalier-Smith defined the so-called subkingdoms Negibacteria and Posibacteria (2002, [PMID 11837318](https://pubmed.ncbi.nlm.nih.gov/11837318/)), and only considered these phyla as Posibacteria: Actinobacteria, Chloroflexi, Firmicutes, and Tenericutes. These phyla were later renamed to Actinomycetota, Chloroflexota, Bacillota, and Mycoplasmatota (2021, [PMID 34694987](https://pubmed.ncbi.nlm.nih.gov/34694987/)). Bacteria in these phyla are considered Gram-positive in this `AMR` package, except for members of the class Negativicutes (within phylum Bacillota) which are Gram-negative. All other bacteria are considered Gram-negative. Species outside the kingdom of Bacteria will return a value `NA`. Functions [mo_is_gram_negative()] and [mo_is_gram_positive()] always return `TRUE` or `FALSE` (or `NA` when the input is `NA` or the MO code is `UNKNOWN`), thus always return `FALSE` for species outside the taxonomic kingdom of Bacteria.
#' Determination of the Gram stain ([mo_gramstain()] is based on the taxonomic kingdom and phylum. Originally, Cavalier-Smith defined the so-called subkingdoms Negibacteria and Posibacteria (2002, [PMID 11837318](https://pubmed.ncbi.nlm.nih.gov/11837318/)), and only considered these phyla as Posibacteria: Actinobacteria, Chloroflexi, Firmicutes, and Tenericutes. These phyla were later renamed to Actinomycetota, Chloroflexota, Bacillota, and Mycoplasmatota (2021, [PMID 34694987](https://pubmed.ncbi.nlm.nih.gov/34694987/)). Bacteria in these phyla are considered Gram-positive in this `AMR` package, except for members of the class Negativicutes (within phylum Bacillota) which are Gram-negative. All other bacteria are considered Gram-negative. Species outside the kingdom of Bacteria will return a value `NA`. Functions [mo_is_gram_negative()] and [mo_is_gram_positive()] always return `TRUE` or `FALSE` (or `NA` when the input is `NA` or the MO code is `UNKNOWN`), thus always return `FALSE` for species outside the taxonomic kingdom of Bacteria.
#'
#' Determination of yeasts ([mo_is_yeast()]) will be based on the taxonomic kingdom and class. *Budding yeasts* are yeasts that reproduce asexually through a process called budding, where a new cell develops from a small protrusion on the parent cell. Taxonomically, these are members of the phylum Ascomycota, class Saccharomycetes (also called Hemiascomycetes) or Pichiomycetes. *True yeasts* quite specifically refers to yeasts in the underlying order Saccharomycetales (such as *Saccharomyces cerevisiae*). Thus, for all microorganisms that are member of the taxonomic class Saccharomycetes or Pichiomycetes, the function will return `TRUE`. It returns `FALSE` otherwise (or `NA` when the input is `NA` or the MO code is `UNKNOWN`).
#' Determination of yeasts ([mo_is_yeast()]) is based on the taxonomic kingdom and class. *Budding yeasts* are yeasts that reproduce asexually through a process called budding, where a new cell develops from a small protrusion on the parent cell. Taxonomically, these are members of the phylum Ascomycota, class Saccharomycetes (also called Hemiascomycetes) or Pichiomycetes. *True yeasts* quite specifically refers to yeasts in the underlying order Saccharomycetales (such as *Saccharomyces cerevisiae*). Thus, for all microorganisms that are member of the taxonomic class Saccharomycetes or Pichiomycetes, the function will return `TRUE`. It returns `FALSE` otherwise (or `NA` when the input is `NA` or the MO code is `UNKNOWN`).
#'
#' Determination of intrinsic resistance ([mo_is_intrinsic_resistant()]) will be based on the [intrinsic_resistant] data set, which is based on `r format_eucast_version_nr(names(EUCAST_VERSION_EXPECTED_PHENOTYPES[1]))`. The [mo_is_intrinsic_resistant()] function can be vectorised over both argument `x` (input for microorganisms) and `ab` (input for antimicrobials).
#' Determination of intrinsic resistance ([mo_is_intrinsic_resistant()]) is based on the [intrinsic_resistant] data set, which is based on `r format_eucast_version_nr(names(EUCAST_VERSION_EXPECTED_PHENOTYPES[1]))`. The [mo_is_intrinsic_resistant()] function can be vectorised over both argument `x` (input for microorganisms) and `ab` (input for antimicrobials).
#'
#' Determination of bacterial oxygen tolerance ([mo_oxygen_tolerance()]) will be based on BacDive, see *Source*. The function [mo_is_anaerobic()] only returns `TRUE` if the oxygen tolerance is `"anaerobe"`, indicting an obligate anaerobic species or genus. It always returns `FALSE` for species outside the taxonomic kingdom of Bacteria.
#' Determination of both bacterial oxygen tolerance ([mo_oxygen_tolerance()]) and morphology ([mo_morphology()]) are based on BacDive, see *Source*. The function [mo_is_anaerobic()] only returns `TRUE` if the oxygen tolerance is `"anaerobe"`, indicating an obligate anaerobic species or genus. It always returns `FALSE` for species outside the taxonomic kingdom of Bacteria.
#'
#' The function [mo_url()] will return the direct URL to the online database entry, which also shows the scientific reference of the concerned species. [This MycoBank URL](`r TAXONOMY_VERSION$MycoBank$url`) will be used for fungi wherever available , [this LPSN URL](`r TAXONOMY_VERSION$MycoBank$url`) for bacteria wherever available, and [this GBIF link](`r TAXONOMY_VERSION$GBIF$url`) otherwise.
#' The function [mo_url()] will return the direct URL to the online database entry, which also shows the scientific reference of the concerned species. [This MycoBank URL](`r TAXONOMY_VERSION$MycoBank$url`) is used for fungi wherever available , [this LPSN URL](`r TAXONOMY_VERSION$MycoBank$url`) for bacteria wherever available, and [this GBIF link](`r TAXONOMY_VERSION$GBIF$url`) otherwise.
#'
#' SNOMED codes ([mo_snomed()]) was last updated on `r documentation_date(TAXONOMY_VERSION$SNOMED$accessed_date)`. See *Source* and the [microorganisms] data set for more info.
#'
@@ -81,6 +83,7 @@
#' @examples
#' # taxonomic tree -----------------------------------------------------------
#'
#' mo_domain("Klebsiella pneumoniae")
#' mo_kingdom("Klebsiella pneumoniae")
#' mo_phylum("Klebsiella pneumoniae")
#' mo_class("Klebsiella pneumoniae")
@@ -90,6 +93,8 @@
#' mo_species("Klebsiella pneumoniae")
#' mo_subspecies("Klebsiella pneumoniae")
#'
#' # all in one go
#' mo_taxonomy("Klebsiella pneumoniae")
#'
#' # full names and short names -----------------------------------------------
#'
@@ -100,14 +105,17 @@
#'
#' # other properties ---------------------------------------------------------
#'
#' mo_pathogenicity("Klebsiella pneumoniae")
#' mo_morphology("Klebsiella pneumoniae")
#' mo_gramstain("Klebsiella pneumoniae")
#' mo_gramstain("Klebsiella pneumoniae", add_morphology = TRUE)
#' mo_pathogenicity("Klebsiella pneumoniae")
#' mo_snomed("Klebsiella pneumoniae")
#' mo_type("Klebsiella pneumoniae")
#' mo_rank("Klebsiella pneumoniae")
#' mo_url("Klebsiella pneumoniae")
#' mo_is_yeast(c("Candida", "Trichophyton", "Klebsiella"))
#'
#' mo_group_members("Streptococcus group A")
#' mo_group_members(c(
#' "Streptococcus group A",
#' "Streptococcus group C",
@@ -151,6 +159,7 @@
#'
#' mo_fullname("Staph epidermidis")
#' mo_fullname("Staph epidermidis", Becker = TRUE)
#'
#' mo_shortname("Staph epidermidis")
#' mo_shortname("Staph epidermidis", Becker = TRUE)
#'
@@ -159,6 +168,7 @@
#'
#' mo_fullname("Strep agalactiae")
#' mo_fullname("Strep agalactiae", Lancefield = TRUE)
#'
#' mo_shortname("Strep agalactiae")
#' mo_shortname("Strep agalactiae", Lancefield = TRUE)
#'
@@ -171,10 +181,10 @@
#' mo_gramstain("Klebsiella pneumoniae", language = "el") # Greek
#' mo_gramstain("Klebsiella pneumoniae", language = "uk") # Ukrainian
#'
#' # mo_type is equal to mo_kingdom, but mo_kingdom will remain untranslated
#' mo_kingdom("Klebsiella pneumoniae")
#' # mo_type is equal to mo_domain, but mo_domain will remain untranslated
#' mo_domain("Klebsiella pneumoniae")
#' mo_type("Klebsiella pneumoniae")
#' mo_kingdom("Klebsiella pneumoniae", language = "zh") # Chinese, no effect
#' mo_domain("Klebsiella pneumoniae", language = "zh") # Chinese, no effect
#' mo_type("Klebsiella pneumoniae", language = "zh") # Chinese, translated
#'
#' mo_fullname("S. pyogenes", Lancefield = TRUE, language = "de")
@@ -249,8 +259,8 @@ mo_shortname <- function(x, language = get_AMR_locale(), keep_synonyms = getOpti
}
# get first char of genus and complete species in English
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)))
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 where no species is known
shortnames[shortnames %like% ".[.] spp[.]"] <- genera[shortnames %like% ".[.] spp[.]"]
@@ -262,7 +272,7 @@ mo_shortname <- function(x, language = get_AMR_locale(), keep_synonyms = getOpti
# unknown species etc.
shortnames[shortnames %like% "unknown"] <- paste0("(", trimws2(gsub("[^a-zA-Z -]", "", shortnames[shortnames %like% "unknown"], perl = TRUE)), ")")
shortnames[mo_rank(x.mo) %in% c("kingdom", "phylum", "class", "order", "family")] <- mo_name(x.mo[mo_rank(x.mo) %in% c("kingdom", "phylum", "class", "order", "family")], language = NULL, keep_synonyms = keep_synonyms)
shortnames[mo_rank(x.mo, keep_synonyms = TRUE, ...) %in% c("domain", "kingdom", "phylum", "class", "order", "family")] <- mo_name(x.mo[mo_rank(x.mo, keep_synonyms = TRUE, ...) %in% c("domain", "kingdom", "phylum", "class", "order", "family")], language = NULL, keep_synonyms = keep_synonyms, ...)
shortnames[is.na(x.mo)] <- NA_character_
load_mo_uncertainties(metadata)
@@ -379,7 +389,18 @@ mo_kingdom <- function(x, language = get_AMR_locale(), keep_synonyms = getOption
language <- validate_language(language)
meet_criteria(keep_synonyms, allow_class = "logical", has_length = 1)
translate_into_language(mo_validate(x = x, property = "kingdom", language = language, keep_synonyms = keep_synonyms, ...), language = language, only_unknown = TRUE)
x.mo <- as.mo(x, language = language, keep_synonyms = keep_synonyms, ...)
for (new_kingdom in c("Archaea", "Bacteria")) {
if (any(mo_domain(x.mo) == new_kingdom, na.rm = TRUE) && message_not_thrown_before("mo_kingdom", new_kingdom, entire_session = TRUE)) {
message_(
"Since {.pkg AMR v3.1.0}, {.help [{.fun mo_kingdom}](AMR::mo_kingdom)} returns the taxonomic kingdom as defined by G\u00f6ker and Oren (2024), who formally introduced a new kingdom rank into prokaryotic nomenclature ({.href [DOI: 10.1099/ijsem.0.006242](https://doi.org/10.1099/ijsem.0.006242)}). ",
"{.strong The former kingdom of ", new_kingdom, "} was divided into four new kingdoms under the {.strong new domain of ", new_kingdom, "}. ",
"For the old behaviour, use {.help [{.fun mo_domain}](AMR::mo_domain)}. ",
"This note will be shown once per session."
)
}
}
translate_into_language(mo_validate(x = x.mo, property = "kingdom", language = language, keep_synonyms = keep_synonyms, ...), language = language, only_unknown = TRUE)
}
#' @rdname mo_property
@@ -389,7 +410,11 @@ mo_domain <- function(x, language = get_AMR_locale(), keep_synonyms = getOption(
# this tries to find the data and an 'mo' column
x <- find_mo_col(fn = "mo_domain")
}
mo_kingdom(x = x, language = language, keep_synonyms = keep_synonyms, ...)
meet_criteria(x, allow_NA = TRUE)
language <- validate_language(language)
meet_criteria(keep_synonyms, allow_class = "logical", has_length = 1)
translate_into_language(mo_validate(x = x, property = "domain", language = language, keep_synonyms = keep_synonyms, ...), language = language, only_unknown = TRUE)
}
#' @rdname mo_property
@@ -404,7 +429,8 @@ mo_type <- function(x, language = get_AMR_locale(), keep_synonyms = getOption("A
meet_criteria(keep_synonyms, allow_class = "logical", has_length = 1)
x.mo <- as.mo(x, language = language, keep_synonyms = keep_synonyms, ...)
out <- mo_kingdom(x.mo, language = NULL, keep_synonyms = keep_synonyms)
out <- mo_domain(x.mo, language = NULL, keep_synonyms = keep_synonyms)
out <- gsub(" \\{.*\\}", "", out) # strip curly brackets
out[which(mo_is_yeast(x.mo, keep_synonyms = keep_synonyms))] <- "Yeasts"
translate_into_language(out, language = language, only_unknown = FALSE)
}
@@ -440,7 +466,7 @@ mo_pathogenicity <- function(x, language = get_AMR_locale(), keep_synonyms = get
metadata <- get_mo_uncertainties()
prev <- AMR_env$MO_lookup$prevalence[match(x.mo, AMR_env$MO_lookup$mo)]
kngd <- AMR_env$MO_lookup$kingdom[match(x.mo, AMR_env$MO_lookup$mo)]
kngd <- AMR_env$MO_lookup$domain[match(x.mo, AMR_env$MO_lookup$mo)]
rank <- AMR_env$MO_lookup$rank[match(x.mo, AMR_env$MO_lookup$mo)]
out <- factor(
@@ -460,8 +486,9 @@ mo_pathogenicity <- function(x, language = get_AMR_locale(), keep_synonyms = get
}
#' @rdname mo_property
#' @param add_morphology a [logical] to indicate whether the morphology (from [mo_morphology()]) should be added to the Gram stain result, e.g. `"Gram-negative rods"` instead of `"Gram-negative"`. The default is `FALSE`.
#' @export
mo_gramstain <- function(x, language = get_AMR_locale(), keep_synonyms = getOption("AMR_keep_synonyms", FALSE), ...) {
mo_gramstain <- function(x, language = get_AMR_locale(), keep_synonyms = getOption("AMR_keep_synonyms", FALSE), add_morphology = FALSE, ...) {
if (missing(x)) {
# this tries to find the data and an 'mo' column
x <- find_mo_col(fn = "mo_gramstain")
@@ -469,13 +496,14 @@ mo_gramstain <- function(x, language = get_AMR_locale(), keep_synonyms = getOpti
meet_criteria(x, allow_NA = TRUE)
language <- validate_language(language)
meet_criteria(keep_synonyms, allow_class = "logical", has_length = 1)
meet_criteria(add_morphology, allow_class = "logical", has_length = 1)
x.mo <- as.mo(x, language = language, keep_synonyms = keep_synonyms, ...)
metadata <- get_mo_uncertainties()
x <- rep(NA_character_, length(x))
# make all bacteria Gram negative
x[mo_kingdom(x.mo, language = NULL, keep_synonyms = keep_synonyms) == "Bacteria"] <- "Gram-negative"
x[mo_domain(x.mo, language = NULL, keep_synonyms = keep_synonyms) == "Bacteria"] <- "Gram-negative"
# overwrite these 4 phyla with Gram-positives
# Source: https://itis.gov/servlet/SingleRpt/SingleRpt?search_topic=TSN&search_value=956097 (Cavalier-Smith, 2002)
x[(mo_phylum(x.mo, language = NULL, keep_synonyms = keep_synonyms) %in% c(
@@ -494,6 +522,12 @@ mo_gramstain <- function(x, language = get_AMR_locale(), keep_synonyms = getOpti
# and of course our own ID for Gram-positives
| x.mo %in% c("B_GRAMP", "B_ANAER-POS")] <- "Gram-positive"
if (isTRUE(add_morphology)) {
morphs <- mo_morphology(x.mo, language = NULL)
morphs[is.na(x)] <- ""
x[!is.na(x)] <- paste(x[!is.na(x)], tolower(morphs[!is.na(x)]))
}
load_mo_uncertainties(metadata)
translate_into_language(x, language = language, only_unknown = FALSE)
}
@@ -552,12 +586,12 @@ mo_is_yeast <- function(x, language = get_AMR_locale(), keep_synonyms = getOptio
x.mo <- as.mo(x, language = language, keep_synonyms = keep_synonyms, ...)
metadata <- get_mo_uncertainties()
x.kingdom <- mo_kingdom(x.mo, language = NULL, keep_synonyms = keep_synonyms)
x.domain <- mo_domain(x.mo, language = NULL, keep_synonyms = keep_synonyms)
x.class <- mo_class(x.mo, language = NULL, keep_synonyms = keep_synonyms)
load_mo_uncertainties(metadata)
out <- x.mo == "F_YEAST" | (x.kingdom == "Fungi" & x.class %in% c("Saccharomycetes", "Pichiomycetes"))
out <- x.mo == "F_YEAST" | (x.domain == "Fungi" & x.class %in% c("Saccharomycetes", "Pichiomycetes"))
out[x.mo %in% c(NA_character_, "UNKNOWN")] <- NA
out
}
@@ -634,6 +668,21 @@ mo_is_anaerobic <- function(x, language = get_AMR_locale(), keep_synonyms = getO
out
}
#' @rdname mo_property
#' @export
mo_morphology <- function(x, language = get_AMR_locale(), keep_synonyms = getOption("AMR_keep_synonyms", FALSE), ...) {
if (missing(x)) {
# this tries to find the data and an 'mo' column
x <- find_mo_col(fn = "mo_morphology")
}
meet_criteria(x, allow_NA = TRUE)
language <- validate_language(language)
meet_criteria(keep_synonyms, allow_class = "logical", has_length = 1)
out <- mo_validate(x = x, property = "morphology", language = language, keep_synonyms = keep_synonyms, ...)
gsub("^(\\w)", "\\U\\1", out, perl = TRUE)
}
#' @rdname mo_property
#' @export
mo_snomed <- function(x, language = get_AMR_locale(), keep_synonyms = getOption("AMR_keep_synonyms", FALSE), ...) {
@@ -764,18 +813,19 @@ mo_taxonomy <- function(x, language = get_AMR_locale(), keep_synonyms = getOptio
language <- validate_language(language)
meet_criteria(keep_synonyms, allow_class = "logical", has_length = 1)
x <- as.mo(x, language = language, keep_synonyms = keep_synonyms, ...)
x.mo <- as.mo(x, language = language, keep_synonyms = keep_synonyms, ...)
metadata <- get_mo_uncertainties()
out <- list(
kingdom = mo_kingdom(x, language = language, keep_synonyms = keep_synonyms),
phylum = mo_phylum(x, language = language, keep_synonyms = keep_synonyms),
class = mo_class(x, language = language, keep_synonyms = keep_synonyms),
order = mo_order(x, language = language, keep_synonyms = keep_synonyms),
family = mo_family(x, language = language, keep_synonyms = keep_synonyms),
genus = mo_genus(x, language = language, keep_synonyms = keep_synonyms),
species = mo_species(x, language = language, keep_synonyms = keep_synonyms),
subspecies = mo_subspecies(x, language = language, keep_synonyms = keep_synonyms)
domain = mo_domain(x.mo, language = language, keep_synonyms = keep_synonyms),
kingdom = suppressMessages(mo_kingdom(x.mo, language = language, keep_synonyms = keep_synonyms)),
phylum = mo_phylum(x.mo, language = language, keep_synonyms = keep_synonyms),
class = mo_class(x.mo, language = language, keep_synonyms = keep_synonyms),
order = mo_order(x.mo, language = language, keep_synonyms = keep_synonyms),
family = mo_family(x.mo, language = language, keep_synonyms = keep_synonyms),
genus = mo_genus(x.mo, language = language, keep_synonyms = keep_synonyms),
species = mo_species(x.mo, language = language, keep_synonyms = keep_synonyms),
subspecies = mo_subspecies(x.mo, language = language, keep_synonyms = keep_synonyms)
)
load_mo_uncertainties(metadata)
@@ -885,6 +935,7 @@ mo_info <- function(x, language = get_AMR_locale(), keep_synonyms = getOption("A
status = mo_status(y, language = language, keep_synonyms = keep_synonyms),
synonyms = mo_synonyms(y, keep_synonyms = keep_synonyms),
gramstain = mo_gramstain(y, language = language, keep_synonyms = keep_synonyms),
morphology = mo_morphology(y, language = language, keep_synonyms = keep_synonyms),
oxygen_tolerance = mo_oxygen_tolerance(y, language = language, keep_synonyms = keep_synonyms),
url = unname(mo_url(y, open = FALSE, keep_synonyms = keep_synonyms)),
ref = mo_ref(y, keep_synonyms = keep_synonyms),
@@ -978,11 +1029,11 @@ mo_validate <- function(x, property, language, keep_synonyms = keep_synonyms, ..
dots <- list(...)
Becker <- dots$Becker
if (is.null(Becker) || property %in% c("kingdom", "phylum", "class", "order", "family", "genus")) {
if (is.null(Becker) || property %in% c("domain", "kingdom", "phylum", "class", "order", "family", "genus")) {
Becker <- FALSE
}
Lancefield <- dots$Lancefield
if (is.null(Lancefield) || property %in% c("kingdom", "phylum", "class", "order", "family", "genus")) {
if (is.null(Lancefield) || property %in% c("domain", "kingdom", "phylum", "class", "order", "family", "genus")) {
Lancefield <- FALSE
}
has_Becker_or_Lancefield <- Becker %in% c(TRUE, "all") || Lancefield %in% c(TRUE, "all")

View File

@@ -482,7 +482,7 @@ scale_x_sir <- function(colours_SIR = c(
R = "#ED553B"
),
language = get_AMR_locale(),
eucast_I = getOption("AMR_guideline", "EUCAST") == "EUCAST",
eucast_I = getOption("AMR_guideline", "EUCAST") %like% "EUCAST",
...) {
meet_criteria(colours_SIR, allow_class = "character", has_length = c(1, 3, 4))
language <- validate_language(language)
@@ -499,7 +499,7 @@ scale_colour_sir <- function(colours_SIR = c(
R = "#ED553B"
),
language = get_AMR_locale(),
eucast_I = getOption("AMR_guideline", "EUCAST") == "EUCAST",
eucast_I = getOption("AMR_guideline", "EUCAST") %like% "EUCAST",
...) {
meet_criteria(colours_SIR, allow_class = "character", has_length = c(1, 3, 4))
language <- validate_language(language)
@@ -528,7 +528,7 @@ scale_fill_sir <- function(colours_SIR = c(
R = "#ED553B"
),
language = get_AMR_locale(),
eucast_I = getOption("AMR_guideline", "EUCAST") == "EUCAST",
eucast_I = getOption("AMR_guideline", "EUCAST") %like% "EUCAST",
...) {
meet_criteria(colours_SIR, allow_class = "character", has_length = c(1, 3, 4))
language <- validate_language(language)

12
R/proportion.R Normal file → Executable file
View File

@@ -100,7 +100,7 @@
#' ```
#'
#' Using `only_all_tested` has no impact when only using one antibiotic as input.
#' @source **M39 Analysis and Presentation of Cumulative Antimicrobial Susceptibility Test Data, 5th Edition**, 2022, *Clinical and Laboratory Standards Institute (CLSI)*. <https://clsi.org/standards/products/microbiology/documents/m39/>.
#' @references **M39 Analysis and Presentation of Cumulative Antimicrobial Susceptibility Test Data, 5th Edition**, 2022, *Clinical and Laboratory Standards Institute (CLSI)*. <https://clsi.org/standards/products/microbiology/documents/m39/>.
#' @seealso [AMR::count()] to count resistant and susceptible isolates.
#' @return A [double] or, when `as_percent = TRUE`, a [character].
#' @rdname proportion
@@ -236,6 +236,11 @@ resistance <- function(...,
only_all_tested = FALSE,
guideline = getOption("AMR_guideline", "EUCAST")) {
# other arguments for meet_criteria are handled by sir_calc()
if (guideline %like% "EUCAST") {
guideline <- "EUCAST"
} else if (guideline %like% "CLSI") {
guideline <- "CLSI"
}
meet_criteria(guideline, allow_class = "character", is_in = c("EUCAST", "CLSI"), has_length = 1)
if (is.null(getOption("AMR_guideline")) && missing(guideline) && message_not_thrown_before("resistance", "eucast_default", entire_session = TRUE)) {
message_("{.help [{.fun resistance}](AMR::resistance)} assumes the EUCAST guideline and thus considers the 'I' category susceptible. Set the {.arg guideline} argument or the {.code AMR_guideline} option to either \"CLSI\" or \"EUCAST\", see {.topic [AMR-options](AMR::AMR-options)}.")
@@ -264,6 +269,11 @@ susceptibility <- function(...,
only_all_tested = FALSE,
guideline = getOption("AMR_guideline", "EUCAST")) {
# other arguments for meet_criteria are handled by sir_calc()
if (guideline %like% "EUCAST") {
guideline <- "EUCAST"
} else if (guideline %like% "CLSI") {
guideline <- "CLSI"
}
meet_criteria(guideline, allow_class = "character", is_in = c("EUCAST", "CLSI"), has_length = 1)
if (is.null(getOption("AMR_guideline")) && missing(guideline) && message_not_thrown_before("susceptibility", "eucast_default", entire_session = TRUE)) {
message_("{.help [{.fun susceptibility}](AMR::susceptibility)} assumes the EUCAST guideline and thus considers the 'I' category susceptible. Set the {.arg guideline} argument or the {.code AMR_guideline} option to either \"CLSI\" or \"EUCAST\", see {.topic [AMR-options](AMR::AMR-options)}.")

181
R/sir.R
View File

@@ -73,6 +73,7 @@ VALID_SIR_LEVELS <- c("S", "SDD", "I", "R", "NI", "WT", "NWT", "NS")
#' @param threshold Maximum fraction of invalid antimicrobial interpretations of `x`, see *Examples*.
#' @param conserve_capped_values Deprecated, use `capped_mic_handling` instead.
#' @param ... For using on a [data.frame]: selection of columns to apply `as.sir()` to. Supports [tidyselect language][tidyselect::starts_with()] such as `where(is.mic)`, `starts_with(...)`, or `column1:column4`, and can thus also be [antimicrobial selectors][amr_selector()], e.g. `as.sir(df, penicillins())`.
#' @param enforce_method A [character] string to force interpretation as a specific method, useful when the S3 class of `x` is lost (e.g., when called from Python via rpy2). Must be one of `"auto"` (default), `"mic"`, or `"disk"`.
#'
#' Otherwise: arguments passed on to methods.
#' @details
@@ -95,7 +96,7 @@ VALID_SIR_LEVELS <- c("S", "SDD", "I", "R", "NI", "WT", "NWT", "NS")
#' # for veterinary breakpoints, also set `host`:
#' your_data %>% mutate_if(is.mic, as.sir, host = "column_with_animal_species", guideline = "CLSI")
#'
#' # fast processing with parallel computing:
#' # fast processing with parallel computing (requires future.apply):
#' as.sir(your_data, ..., parallel = TRUE)
#' ```
#' * Operators like "<=" will be considered according to the `capped_mic_handling` setting. At default, an MIC value of e.g. ">2" will return "NI" (non-interpretable) if the breakpoint is 4-8; the *true* MIC could be at either side of the breakpoint. This is to prevent that capped values from raw laboratory data would not be treated conservatively.
@@ -112,7 +113,7 @@ VALID_SIR_LEVELS <- c("S", "SDD", "I", "R", "NI", "WT", "NWT", "NS")
#' # for veterinary breakpoints, also set `host`:
#' your_data %>% mutate_if(is.disk, as.sir, host = "column_with_animal_species", guideline = "CLSI")
#'
#' # fast processing with parallel computing:
#' # fast processing with parallel computing (requires future.apply):
#' as.sir(your_data, ..., parallel = TRUE)
#' ```
#'
@@ -175,7 +176,7 @@ VALID_SIR_LEVELS <- c("S", "SDD", "I", "R", "NI", "WT", "NWT", "NS")
#' @aliases sir
#' @export
#' @seealso [as.mic()], [as.disk()], [as.mo()]
#' @source
#' @references
#' For interpretations of minimum inhibitory concentration (MIC) values and disk diffusion diameters:
#'
#' - **CLSI M39: Analysis and Presentation of Cumulative Antimicrobial Susceptibility Test Data**, `r min(as.integer(gsub("[^0-9]", "", subset(AMR::clinical_breakpoints, guideline %like% "CLSI")$guideline)))`-`r max(as.integer(gsub("[^0-9]", "", subset(AMR::clinical_breakpoints, guideline %like% "CLSI")$guideline)))`, *Clinical and Laboratory Standards Institute* (CLSI). <https://clsi.org/standards/products/microbiology/documents/m39/>.
@@ -220,9 +221,6 @@ VALID_SIR_LEVELS <- c("S", "SDD", "I", "R", "NI", "WT", "NWT", "NS")
#' sir_interpretation_history()
#'
#' \donttest{
#' # using parallel computing, which is available in base R:
#' as.sir(df_wide, parallel = TRUE, info = TRUE)
#'
#'
#' ## Using dplyr -------------------------------------------------
#' if (require("dplyr")) {
@@ -388,8 +386,15 @@ VALID_SIR_LEVELS <- c("S", "SDD", "I", "R", "NI", "WT", "NWT", "NS")
#' # mutate(across(where(is_sir_eligible), as.sir))
#' }
#' }
as.sir <- function(x, ...) {
UseMethod("as.sir")
as.sir <- function(x, ..., enforce_method = "auto") {
meet_criteria(enforce_method, allow_class = "character", has_length = 1, is_in = c("auto", "mic", "disk"))
if (enforce_method == "mic") {
as.sir.mic(x, ...)
} else if (enforce_method == "disk") {
as.sir.disk(x, ...)
} else {
UseMethod("as.sir")
}
}
as_sir_structure <- function(x,
@@ -528,7 +533,7 @@ as.sir.default <- function(x,
} else if (!all(is.na(x)) && !identical(levels(x), VALID_SIR_LEVELS) && !all(x %in% c(VALID_SIR_LEVELS, NA))) {
if (all(x %unlike% "(S|I|R)", na.rm = TRUE) && !all(x %in% c(1, 2, 3, 4, 5), na.rm = TRUE)) {
# check if they are actually MICs or disks
if (all_valid_mics(x)) {
if (all_valid_mics(x) && !(all_valid_disks(x) && identical(x, tryCatch(floor(x), error = function(e) NULL)))) {
warning_("in {.help [{.fun as.sir}](AMR::as.sir)}: input values were guessed to be MIC values - preferably transform them with {.help [{.fun as.mic}](AMR::as.mic)} before running {.help [{.fun as.sir}](AMR::as.sir)}.")
return(as.sir(as.mic(x), ...))
} else if (all_valid_disks(x)) {
@@ -716,8 +721,7 @@ as.sir.disk <- function(x,
}
#' @rdname as.sir
#' @param parallel A [logical] to indicate if parallel computing must be used, defaults to `FALSE`. The `parallel` package is part of base \R and no additional packages are required. On Unix/macOS with \R >= 4.0.0, [parallel::mclapply()] (fork-based) is used; on Windows and \R < 4.0.0, [parallel::parLapply()] with a PSOCK cluster is used (requires the AMR package to be installed, not just loaded via `devtools::load_all()`). Parallelism distributes columns across cores; it is most beneficial when there are many antibiotic columns and a large number of rows.
#' @param max_cores Maximum number of cores to use if `parallel = TRUE`. Use a negative value to subtract that number from the available number of cores, e.g. a value of `-2` on an 8-core machine means that at most 6 cores will be used. Defaults to `-1`. There will never be used more cores than variables to analyse. The available number of cores are detected using [parallelly::availableCores()] if that package is installed, and base \R's [parallel::detectCores()] otherwise.
#' @param parallel A [logical] to indicate if parallel computing must be used, defaults to `FALSE`. Requires the [`future.apply`][future.apply::future_lapply()] package. **A non-sequential [future::plan()] must already be active before setting `parallel = TRUE`** — for example, `future::plan(future::multisession)`. An error is thrown if `parallel = TRUE` is used without a plan set by the user. Parallelism distributes columns (and optionally row batches) across workers; it is most beneficial when there are many antibiotic columns and a large number of rows.
#' @export
as.sir.data.frame <- function(x,
...,
@@ -737,7 +741,6 @@ as.sir.data.frame <- function(x,
verbose = FALSE,
info = interactive(),
parallel = FALSE,
max_cores = -1,
conserve_capped_values = NULL) {
meet_criteria(x, allow_class = "data.frame") # will also check for dimensions > 0
meet_criteria(col_mo, allow_class = "character", is_in = colnames(x), allow_NULL = TRUE)
@@ -756,7 +759,6 @@ as.sir.data.frame <- function(x,
meet_criteria(verbose, allow_class = "logical", has_length = 1)
meet_criteria(info, allow_class = "logical", has_length = 1)
meet_criteria(parallel, allow_class = "logical", has_length = 1)
meet_criteria(max_cores, allow_class = c("numeric", "integer"), has_length = 1)
x.bak <- x
if (isTRUE(info) && message_not_thrown_before("as.sir", "sir_interpretation_history")) {
@@ -911,40 +913,34 @@ as.sir.data.frame <- function(x,
}
# set up parallel computing
n_cores <- get_n_cores(max_cores = max_cores)
n_cores <- min(n_cores, length(ab_cols)) # never more cores than variables required
if (isTRUE(parallel) && (.Platform$OS.type == "windows" || getRversion() < "4.0.0")) {
cl <- tryCatch(parallel::makeCluster(n_cores, type = "PSOCK"),
error = function(e) {
if (isTRUE(info)) {
message_("Could not create parallel cluster, using single-core computation. Error message: ", conditionMessage(e))
}
return(NULL)
}
)
if (!is.null(cl)) {
# Each PSOCK worker is a fresh R session — the AMR package must be loaded there
# so all exported functions (as.sir, as.mic, as.disk, ...) are available.
amr_loaded_on_workers <- tryCatch({
parallel::clusterEvalQ(cl, library(AMR, quietly = TRUE))
TRUE
}, error = function(e) FALSE)
if (!amr_loaded_on_workers) {
if (isTRUE(info)) {
message_("Could not load AMR on parallel workers (package may not be installed); falling back to single-core computation.")
}
parallel::stopCluster(cl)
cl <- NULL
}
}
if (is.null(cl)) {
n_cores <- 1
if (requireNamespace("future.apply", quietly = TRUE) && !inherits(future::plan(), "sequential")) {
if (isFALSE(parallel)) {
message_("Assuming {.code parallel = TRUE} since parallel computing has been set up using the {.pkg future} package before. Set {.help [{.fun plan}](future::plan)} to sequential to prevent this.")
}
parallel <- TRUE
}
if (isTRUE(parallel)) {
stop_ifnot(
requireNamespace("future.apply", quietly = TRUE),
"Setting {.code parallel = TRUE} requires the {.pkg future.apply} package.\n",
"Install it with {.code install.packages(\"future.apply\")}."
)
stop_if(inherits(future::plan(), "sequential"),
"Setting {.code parallel = TRUE} requires a non-sequential {.help [{.fun future::plan}](future::plan)} to be active.\n",
"For your system, you could first run: {.code library(future); ",
ifelse(.Platform$OS.type == "windows" || in_rstudio(),
"plan(multisession)",
"plan(multicore)"
),
"}",
call = FALSE
)
if (isTRUE(info)) {
message_(as_note = FALSE) # empty line
message_("Processing columns:", as_note = FALSE)
n_workers <- future::nbrOfWorkers()
n_cores <- min(n_workers, length(ab_cols))
} else {
n_workers <- 1L
n_cores <- 1L
}
# In parallel mode suppress per-column messages: workers print simultaneously and
@@ -952,31 +948,23 @@ as.sir.data.frame <- function(x,
is_parallel_run <- isTRUE(parallel) && n_cores > 1 && length(ab_cols) > 1
effective_info <- if (is_parallel_run) FALSE else info
# Row-batch mode: when n_cols < n_cores we would leave cores idle under plain
# column-parallel dispatch. Instead we split rows into pieces so every core
# gets work. pieces_per_col = ceil(n_cores / n_cols) gives ~n_cores jobs
# Row-batch mode: when n_cols < n_workers we would leave workers idle under plain
# column-parallel dispatch. Instead we split rows into pieces so every worker
# gets work. pieces_per_col = ceil(n_workers / n_cols) gives ~n_workers jobs
# total; each job processes one column on one row slice, which also reduces
# per-worker memory pressure (smaller breakpoints search space).
# Only used for the fork path (R >= 4.0, non-Windows); PSOCK clusters already
# incur high per-job serialisation overhead so we keep column-mode there.
use_fork <- is_parallel_run &&
!(.Platform$OS.type == "windows" || getRversion() < "4.0.0")
pieces_per_col <- if (use_fork && length(ab_cols) < n_cores) {
ceiling(n_cores / length(ab_cols))
if (is_parallel_run && length(ab_cols) < n_workers) {
pieces_per_col <- ceiling(n_workers / length(ab_cols))
} else {
1L
pieces_per_col <- 1L
}
run_as_sir_column <- function(i, rows = NULL) {
# Always resolve AMR_env from the package namespace. This is essential for
# PSOCK workers (where the closure-captured AMR_env is a stale serialised copy
# while as.sir() writes to the live AMR:::AMR_env) and also avoids capturing
# pre-existing log entries from earlier in the session when forking.
# Always resolve AMR_env from the package namespace so workers get the live
# environment rather than a stale serialised copy from the closure.
.amr_env <- get("AMR_env", envir = asNamespace("AMR"), inherits = FALSE)
# In parallel mode each worker (fork or PSOCK) has its own copy of the
# history; record the current length so we capture only the new rows added
# by the as.sir() call below, not any pre-existing entries inherited at fork
# time or carried over from earlier as.sir() calls.
# In parallel mode each worker has its own copy of the history; record the
# current length so we capture only the rows added by this as.sir() call.
if (is_parallel_run) pre_log_n <- NROW(.amr_env$sir_interpretation_history)
ab_col <- ab_cols[i]
@@ -1057,7 +1045,7 @@ as.sir.data.frame <- function(x,
ab <- ab_col
ab_coerced <- suppressWarnings(as.ab(ab, info = FALSE))
show_message <- FALSE
if (!all(x[row_idx, ab, drop = TRUE] %in% c("S", "SDD", "I", "R", "NI", NA), na.rm = TRUE)) {
if (!all(x[row_idx, ab, drop = TRUE] %in% c(VALID_SIR_LEVELS, NA), na.rm = TRUE)) {
show_message <- TRUE
if (isTRUE(effective_info)) {
message_("\u00a0\u00a0", .amr_env$bullet_icon, " Cleaning values in column ", paste0("{.field ", font_bold(ab), "}"), " (",
@@ -1090,31 +1078,17 @@ as.sir.data.frame <- function(x,
return(out)
}
if (isTRUE(parallel) && n_cores > 1 && length(ab_cols) > 1) {
if (is_parallel_run) {
if (isTRUE(info)) {
message_(as_note = FALSE)
if (pieces_per_col > 1L) {
message_("Running in parallel mode using ", n_cores, " out of ", get_n_cores(Inf), " cores, on columns ", vector_and(paste0("{.field ", font_bold(ab_cols, collapse = NULL), "}"), quotes = FALSE, sort = FALSE), " (", pieces_per_col, " row slices per column)...", as_note = FALSE, appendLF = FALSE)
message_("Running in parallel mode using ", n_cores, " workers, on columns ", vector_and(paste0("{.field ", font_bold(ab_cols, collapse = NULL), "}"), quotes = FALSE, sort = FALSE), " (", pieces_per_col, " row slices per column)...", as_note = FALSE, appendLF = FALSE)
} else {
message_("Running in parallel mode using ", n_cores, " out of ", get_n_cores(Inf), " cores, on columns ", vector_and(paste0("{.field ", font_bold(ab_cols, collapse = NULL), "}"), quotes = FALSE, sort = FALSE), "...", as_note = FALSE, appendLF = FALSE)
message_("Running in parallel mode using ", n_cores, " workers, on columns ", vector_and(paste0("{.field ", font_bold(ab_cols, collapse = NULL), "}"), quotes = FALSE, sort = FALSE), "...", as_note = FALSE, appendLF = FALSE)
}
}
if (.Platform$OS.type == "windows" || getRversion() < "4.0.0") {
# PSOCK cluster: column-mode only (row-batch serialisation overhead not worth it)
on.exit(parallel::stopCluster(cl), add = TRUE)
parallel::clusterExport(cl, varlist = c(
"x", "x.bak", "x_mo", "ab_cols", "types",
"capped_mic_handling", "as_wt_nwt", "add_intrinsic_resistance",
"reference_data", "substitute_missing_r_breakpoint", "include_screening", "include_PKPD",
"breakpoint_type", "guideline", "host", "uti", "verbose",
"col_mo", "conserve_capped_values",
"effective_info", "is_parallel_run",
"run_as_sir_column"
), envir = environment())
result_list <- parallel::parLapply(cl, seq_along(ab_cols), run_as_sir_column)
} else if (pieces_per_col > 1L) {
# Row-batch mode (R >= 4.0, non-Windows, n_cols < n_cores):
# build (col, row_slice) job pairs so all cores stay active
if (pieces_per_col > 1L) {
# Row-batch mode: build (col, row_slice) job pairs so all workers stay active
row_cuts <- unique(round(seq(0, nrow(x), length.out = pieces_per_col + 1L)))
row_ranges <- lapply(seq_len(length(row_cuts) - 1L), function(p) {
seq.int(row_cuts[p] + 1L, row_cuts[p + 1L])
@@ -1122,23 +1096,23 @@ as.sir.data.frame <- function(x,
jobs <- do.call(c, lapply(seq_along(ab_cols), function(ci) {
lapply(seq_along(row_ranges), function(p) list(col = ci, rows = row_ranges[[p]]))
}))
flat <- parallel::mclapply(jobs, function(job) {
flat <- future.apply::future_lapply(jobs, function(job) {
run_as_sir_column(job$col, job$rows)
}, mc.cores = n_cores)
}, future.seed = TRUE)
# Reassemble: for each column concatenate row pieces in order
result_list <- lapply(seq_along(ab_cols), function(ci) {
pieces <- flat[vapply(jobs, function(j) j$col == ci, logical(1L))]
list(
result = as.sir(do.call(c, lapply(pieces, function(p) as.character(p$result)))),
log = {
log = {
logs <- Filter(Negate(is.null), lapply(pieces, function(p) p$log))
if (length(logs) > 0L) do.call(rbind_AMR, logs) else NULL
}
)
})
} else {
# Column-parallel mode (R >= 4.0, non-Windows, n_cols >= n_cores)
result_list <- parallel::mclapply(seq_along(ab_cols), run_as_sir_column, mc.cores = n_cores)
# Column-parallel mode: one job per antibiotic column
result_list <- future.apply::future_lapply(seq_along(ab_cols), run_as_sir_column, future.seed = TRUE)
}
if (isTRUE(info)) {
message_(font_green_bg("\u00a0DONE\u00a0"), as_note = FALSE)
@@ -1148,9 +1122,16 @@ as.sir.data.frame <- function(x,
} else {
# sequential mode (non-parallel)
if (isTRUE(info) && n_cores > 1 && NROW(x) * NCOL(x) > 10000) {
# give a note that parallel mode might be better
suggest <- ifelse(.Platform$OS.type == "windows" || in_rstudio(),
"plan(multisession)",
"plan(multicore)"
)
message_(as_note = FALSE)
message_("Running in sequential mode. Consider setting {.arg parallel} to {.code TRUE} to speed up processing on multiple cores.\n")
if (requireNamespace("future.apply", quietly = TRUE)) {
message_("Running in sequential mode. To speed up processing, set a parallel {.help [{.fun future::plan}](future::plan)} such as {.code ", suggest, "}.")
} else {
message_("Running in sequential mode. To speed up processing, install the {.pkg future.apply} package and then set {.code parallel = TRUE}.\n")
}
}
# this will contain a progress bar already
result_list <- lapply(seq_along(ab_cols), run_as_sir_column)
@@ -1280,7 +1261,7 @@ as_sir_method <- function(method_short,
# backward compatibilty
dots <- list(...)
dots <- dots[which(!names(dots) %in% c("warn", "mo.bak", "is_data.frame"))]
dots <- dots[which(!names(dots) %in% c("warn", "mo.bak", "is_data.frame", "as_wt_nwt"))]
if (length(dots) != 0) {
warning_("These arguments in {.help [{.fun as.sir}](AMR::as.sir)} are no longer used: ", vector_and(names(dots), quotes = "`"), ".", call = FALSE)
}
@@ -1681,7 +1662,7 @@ as_sir_method <- function(method_short,
mo_current_other <- structure("UNKNOWN", class = c("mo", "character"))
# formatted for notes
mo_formatted <- mo_current_name
if (!mo_current_rank %in% c("kingdom", "phylum", "class", "order")) {
if (!mo_current_rank %in% c("domain", "kingdom", "phylum", "class", "order")) {
mo_formatted <- font_italic(mo_formatted, collapse = NULL)
}
ab_formatted <- paste0(
@@ -2121,7 +2102,7 @@ sir_interpretation_history <- function(clean = FALSE) {
#' @noRd
print.sir_log <- function(x, ...) {
if (NROW(x) == 0) {
message_("No results to print. First run {.help [{.fun as.sir}](AMR::as.sir)} on MIC values or disk diffusion zones (or on a {.cls data.frame} containing any of these) to print a {.val logbook} data set here.")
message_("No results to print. First run {.help [{.fun as.sir}](AMR::as.sir)} on MIC values or disk diffusion zones (or on a {.cls data.frame} containing any of these) to print a 'logbook' data set here.")
return(invisible(NULL))
}
class(x) <- class(x)[class(x) != "sir_log"]
@@ -2138,7 +2119,7 @@ pillar_shaft.sir <- function(x, ...) {
out[is.na(x)] <- pillar::style_subtle(" NA")
out[x == "S"] <- font_green_bg(" S ") # has font_black internally
out[x == "SDD"] <- font_green_lighter_bg(" SDD ") # has font_black internally
if (getOption("AMR_guideline", "EUCAST")[1] == "EUCAST") {
if (getOption("AMR_guideline", "EUCAST")[1] %like% "EUCAST") {
out[x == "I"] <- font_green_lighter_bg(" I ") # has font_black internally
} else {
out[x == "I"] <- font_orange_bg(" I ") # has font_black internally
@@ -2319,12 +2300,12 @@ c.sir <- function(...) {
lst <- list(...)
# TODO for #170
# guideline <- vapply(FUN.VALUE = character(1), lst, function(x) attributes(x)$guideline %or% NA_character_)
# mo <- vapply(FUN.VALUE = character(1), lst, function(x) attributes(x)$mo %or% NA_character_)
# ab <- vapply(FUN.VALUE = character(1), lst, function(x) attributes(x)$ab %or% NA_character_)
# method <- vapply(FUN.VALUE = character(1), lst, function(x) attributes(x)$method %or% NA_character_)
# ref_tbl <- vapply(FUN.VALUE = character(1), lst, function(x) attributes(x)$ref_tbl %or% NA_character_)
# ref_breakpoints <- vapply(FUN.VALUE = character(1), lst, function(x) attributes(x)$ref_breakpoints %or% NA_character_)
# guideline <- vapply(FUN.VALUE = character(1), lst, function(x) attributes(x)$guideline %||% NA_character_)
# mo <- vapply(FUN.VALUE = character(1), lst, function(x) attributes(x)$mo %||% NA_character_)
# ab <- vapply(FUN.VALUE = character(1), lst, function(x) attributes(x)$ab %||% NA_character_)
# method <- vapply(FUN.VALUE = character(1), lst, function(x) attributes(x)$method %||% NA_character_)
# ref_tbl <- vapply(FUN.VALUE = character(1), lst, function(x) attributes(x)$ref_tbl %||% NA_character_)
# ref_breakpoints <- vapply(FUN.VALUE = character(1), lst, function(x) attributes(x)$ref_breakpoints %||% NA_character_)
out <- as.sir(unlist(lapply(list(...), as.character)))
@@ -2363,7 +2344,7 @@ coerce_reference_data_columns <- function(x) {
ref <- AMR::clinical_breakpoints
for (col in names(ref)) {
col_ref <- ref[[col]]
col_x <- x[[col]]
col_x <- x[[col]]
if (identical(class(col_ref), class(col_x))) next
if (col == "mo") {
x[[col]] <- suppressMessages(as.mo(col_x))

Binary file not shown.

0
R/tidymodels.R Normal file → Executable file
View File

View File

@@ -29,73 +29,88 @@
#' Filter Top *n* Microorganisms
#'
#' This function filters a data set to include only the top *n* microorganisms based on a specified property, such as taxonomic family or genus. For example, it can filter a data set to the top 3 species, or to any species in the top 5 genera, or to the top 3 species in each of the top 5 genera.
#' Filters a data set to include only the top *n* microorganisms based on a specified property, such as taxonomic family or genus. For example, it can filter a data set to the top 3 species, to any species in the top 5 genera, or to the top 3 species in each of the top 5 genera.
#' @param x A data frame containing microbial data.
#' @param n An integer specifying the maximum number of unique values of the `property` to include in the output.
#' @param property A character string indicating the microorganism property to use for filtering. Must be one of the column names of the [microorganisms] data set: `r vector_or(colnames(microorganisms), sort = FALSE, documentation = TRUE)`. If `NULL`, the raw values from `col_mo` will be used without transformation. When using `"species"` (default) or `"subpecies"`, the genus will be added to make sure each (sub)species still belongs to the right genus.
#' @param n_for_each An optional integer specifying the maximum number of rows to retain for each value of the selected property. If `NULL`, all rows within the top *n* groups will be included.
#' @param n A positive whole number specifying the maximum number of unique values of `property` to include in the output.
#' @param property A character string indicating the microorganism property to use for filtering. Must be one of the column names of the [microorganisms] data set: `r vector_or(colnames(microorganisms), sort = FALSE, documentation = TRUE)`. If `NULL`, the raw values from `col_mo` will be used without transformation. When using `"species"` (default) or `"subspecies"`, the genus is prepended to ensure each name is unambiguous.
#' @param n_for_each An optional positive whole number specifying the maximum number of distinct microorganism groups at the level of `property_for_each` to retain within each of the top *n* groups. Only used when `property_for_each` is also set.
#' @param property_for_each The microorganism property to use for sub-grouping within each top *n* group. Must be one of the column names of the [microorganisms] data set and at a strictly lower taxonomic rank than `property` (allowed order: domain > kingdom > phylum > class > order > family > genus > species > subspecies). Defaults to `"species"`. Only relevant when `n_for_each` is set.
#' @param col_mo A character string indicating the column in `x` that contains microorganism names or codes. Defaults to the first column of class [`mo`]. Values will be coerced using [as.mo()].
#' @param ... Additional arguments passed on to [mo_property()] when `property` is not `NULL`.
#' @details This function is useful for preprocessing data before creating [antibiograms][antibiogram()] or other analyses that require focused subsets of microbial data. For example, it can filter a data set to only include isolates from the top 10 species.
#' @details This function is useful for preprocessing data before creating [antibiograms][antibiogram()] or other analyses that require focused subsets of microbial data.
#' @export
#' @seealso [mo_property()], [as.mo()], [antibiogram()]
#' @examples
#' # filter to the top 3 species:
#' top_n_microorganisms(example_isolates,
#' n = 3
#' )
#' top_n_microorganisms(example_isolates, n = 3)
#'
#' # filter to any species in the top 5 genera:
#' top_n_microorganisms(example_isolates,
#' n = 5, property = "genus"
#' )
#' top_n_microorganisms(example_isolates, n = 5, property = "genus")
#'
#' # filter to the top 3 species in each of the top 5 genera:
#' top_n_microorganisms(example_isolates,
#' n = 5, property = "genus", n_for_each = 3
#' )
top_n_microorganisms <- function(x, n, property = "species", n_for_each = NULL, col_mo = NULL, ...) {
#'
#' # filter to the top 2 genera in each of the top 3 families:
#' top_n_microorganisms(example_isolates,
#' n = 3, property = "family", n_for_each = 2, property_for_each = "genus"
#' )
top_n_microorganisms <- function(x, n, property = "species", n_for_each = NULL, property_for_each = "species", col_mo = NULL, ...) {
meet_criteria(x, allow_class = "data.frame") # also checks dimensions to be >0
meet_criteria(n, allow_class = c("numeric", "integer"), has_length = 1, is_finite = TRUE, is_positive = TRUE)
meet_criteria(property, allow_class = "character", has_length = 1, is_in = colnames(AMR::microorganisms))
meet_criteria(property, allow_class = "character", has_length = 1, is_in = colnames(AMR::microorganisms), allow_NULL = TRUE)
meet_criteria(n_for_each, allow_class = c("numeric", "integer"), has_length = 1, is_finite = TRUE, is_positive = TRUE, allow_NULL = TRUE)
meet_criteria(property_for_each, allow_class = "character", has_length = 1, is_in = colnames(AMR::microorganisms), allow_NULL = TRUE)
meet_criteria(col_mo, allow_class = "character", has_length = 1, allow_NULL = TRUE, is_in = colnames(x))
if (is.null(col_mo)) {
col_mo <- search_type_in_df(x = x, type = "mo", info = TRUE)
stop_if(is.null(col_mo), "{.arg col_mo} must be set")
}
x.bak <- x
.taxonomic_ranks <- c("domain", "kingdom", "phylum", "class", "order", "family", "genus", "species", "subspecies")
if (!is.null(n_for_each) && !is.null(property) && !is.null(property_for_each)) {
prop_rank <- match(property, .taxonomic_ranks)
each_rank <- match(property_for_each, .taxonomic_ranks)
if (!is.na(prop_rank) && !is.na(each_rank) && each_rank <= prop_rank) {
stop_(
"`property_for_each` (\"", property_for_each, "\") must be at a lower ",
"taxonomic rank than `property` (\"", property, "\")"
)
}
}
x.bak <- x
x[, col_mo] <- as.mo(x[, col_mo, drop = TRUE], keep_synonyms = TRUE)
if (is.null(property)) {
x$prop_val <- x[[col_mo]]
} else if (property == "species") {
x$prop_val <- paste(mo_genus(x[[col_mo]], ...), mo_species(x[[col_mo]], ...))
} else if (property == "subspecies") {
x$prop_val <- paste(mo_genus(x[[col_mo]], ...), mo_species(x[[col_mo]], ...), mo_subspecies(x[[col_mo]], ...))
} else {
x$prop_val <- mo_property(x[[col_mo]], property = property, ...)
get_prop_val <- function(prop) {
if (is.null(prop)) {
x[[col_mo]]
} else if (prop == "species") {
paste(mo_genus(x[[col_mo]], ...), mo_species(x[[col_mo]], ...))
} else if (prop == "subspecies") {
paste(mo_genus(x[[col_mo]], ...), mo_species(x[[col_mo]], ...), mo_subspecies(x[[col_mo]], ...))
} else {
mo_property(x[[col_mo]], property = prop, ...)
}
}
counts <- sort(table(x$prop_val), decreasing = TRUE)
n <- as.integer(n)
if (length(counts) < n) {
n <- length(counts)
}
count_values <- names(counts)[seq_len(n)]
filtered_rows <- which(x$prop_val %in% count_values)
x$prop_val <- get_prop_val(property)
counts <- sort(table(x$prop_val), decreasing = TRUE)
n <- min(as.integer(n), length(counts))
filtered_rows <- which(x$prop_val %in% names(counts)[seq_len(n)])
if (!is.null(n_for_each)) {
n_for_each <- as.integer(n_for_each)
x$prop_val_each <- get_prop_val(property_for_each)
filtered_x <- x[filtered_rows, , drop = FALSE]
filtered_x$.orig_row <- filtered_rows
filtered_rows <- do.call(
c,
lapply(split(filtered_x, filtered_x$prop_val), function(group) {
top_values <- names(sort(table(group[[col_mo]]), decreasing = TRUE)[seq_len(n_for_each)])
top_values <- top_values[!is.na(top_values)]
which(x[[col_mo]] %in% top_values)
top_each <- names(sort(table(group$prop_val_each), decreasing = TRUE)[seq_len(n_for_each)])
group$.orig_row[group$prop_val_each %in% top_each[!is.na(top_each)]]
})
)
}

View File

@@ -31,7 +31,7 @@
#'
#' All antimicrobial drugs and their official names, ATC codes, ATC groups and defined daily dose (DDD) are included in this package, using the WHO Collaborating Centre for Drug Statistics Methodology.
#' @section WHOCC:
#' This package contains **all ~550 antibiotic, antimycotic and antiviral drugs** and their Anatomical Therapeutic Chemical (ATC) codes, ATC groups and Defined Daily Dose (DDD) from the World Health Organization Collaborating Centre for Drug Statistics Methodology (WHOCC, <https://atcddd.fhi.no>) and the Pharmaceuticals Community Register of the European Commission (<https://ec.europa.eu/health/documents/community-register/html/reg_hum_atc.htm>).
#' This package contains **all ~550 antibiotic, antimycotic and antiviral drugs** and their Anatomical Therapeutic Chemical (ATC) codes, ATC groups and Defined Daily Dose (DDD) from the World Health Organization Collaborating Centre for Drug Statistics Methodology (WHOCC, <https://atcddd.fhi.no>) and the Pharmaceuticals Community Register of the European Commission (<https://ec.europa.eu/health/documents/community-register/html/index_en.htm>).
#'
#' These have become the gold standard for international drug utilisation monitoring and research.
#'

View File

@@ -70,6 +70,13 @@ as.data.frame.deprecated_amr_dataset <- function(x, ...) {
# - `antibiotics` in `antibiogram()`
# - `converse_capped_values` in `as.sir()`
#' @rdname AMR-deprecated
#' @export
custom_eucast_rules <- function(...) {
deprecation_warning("custom_eucast_rules", "custom_interpretive_rules", is_function = TRUE)
custom_interpretive_rules(...)
}
#' @rdname AMR-deprecated
#' @export
ab_class <- function(...) {

View File

@@ -86,10 +86,10 @@ AMR_env$chmatch <- import_fn("chmatch", "data.table", error_on_fail = FALSE)
AMR_env$chin <- import_fn("%chin%", "data.table", error_on_fail = FALSE)
# take cli symbols and error function if available
AMR_env$bullet_icon <- import_fn("symbol", "cli", error_on_fail = FALSE)$bullet %or% "*"
AMR_env$ellipsis_icon <- import_fn("symbol", "cli", error_on_fail = FALSE)$ellipsis %or% "..."
AMR_env$info_icon <- import_fn("symbol", "cli", error_on_fail = FALSE)$info %or% "i"
AMR_env$sup_1_icon <- import_fn("symbol", "cli", error_on_fail = FALSE)$sup_1 %or% "*"
AMR_env$bullet_icon <- import_fn("symbol", "cli", error_on_fail = FALSE)$bullet %||% "*"
AMR_env$ellipsis_icon <- import_fn("symbol", "cli", error_on_fail = FALSE)$ellipsis %||% "..."
AMR_env$info_icon <- import_fn("symbol", "cli", error_on_fail = FALSE)$info %||% "i"
AMR_env$sup_1_icon <- import_fn("symbol", "cli", error_on_fail = FALSE)$sup_1 %||% "*"
AMR_env$cli_abort <- import_fn("cli_abort", "cli", error_on_fail = FALSE)
AMR_env$cross_icon <- if (isTRUE(base::l10n_info()$`UTF-8`)) "\u00d7" else "x"

View File

@@ -11,6 +11,7 @@ knitr::opts_chunk$set(
# fig.path = "man/figures/README-",
out.width = "100%"
)
options(width = 100)
AMR:::reset_all_thrown_messages()
```
@@ -21,8 +22,8 @@ Please visit our comprehensive package website <https://amr-for-r.org> to read m
Overview:
* Provides an **all-in-one solution** for antimicrobial resistance (AMR) data analysis in a One Health approach
* Peer-reviewed, used in over 175 countries, available in `r length(AMR:::LANGUAGES_SUPPORTED)` languages
* Generates **antibiograms** - traditional, combined, syndromic, and even WISCA
* **Peer-reviewed**, used in over 175 countries, available in `r length(AMR:::LANGUAGES_SUPPORTED)` languages
* Generates **antibiograms** - WISCA for empiric coverage estimates, or traditional/syndromic for AMR surveillance
* Provides the **full microbiological taxonomy** of `r AMR:::format_included_data_number(AMR::microorganisms)` distinct species and extensive info of `r AMR:::format_included_data_number(NROW(AMR::antimicrobials) + NROW(AMR::antivirals))` antimicrobial drugs
* Applies **CLSI `r min(as.integer(gsub("[^0-9]", "", subset(AMR::clinical_breakpoints, grepl("CLSI", guideline))$guideline)))`-`r max(as.integer(gsub("[^0-9]", "", subset(AMR::clinical_breakpoints, grepl("CLSI", guideline))$guideline)))`** and **EUCAST `r min(as.integer(gsub("[^0-9]", "", subset(AMR::clinical_breakpoints, grepl("EUCAST", guideline))$guideline)))`-`r max(as.integer(gsub("[^0-9]", "", subset(AMR::clinical_breakpoints, grepl("EUCAST", guideline))$guideline)))`** clinical and veterinary breakpoints, and ECOFFs, for MIC and disk zone interpretation
* Corrects for duplicate isolates, **calculates** and **predicts** AMR per antimicrobial class

View File

@@ -10,10 +10,11 @@ Overview:
- Provides an **all-in-one solution** for antimicrobial resistance (AMR)
data analysis in a One Health approach
- Peer-reviewed, used in over 175 countries, available in 28 languages
- Generates **antibiograms** - traditional, combined, syndromic, and
even WISCA
- Provides the **full microbiological taxonomy** of ~79 000 distinct
- **Peer-reviewed**, used in over 175 countries, available in 28
languages
- Generates **antibiograms** - WISCA for empiric coverage estimates, or
traditional/syndromic for AMR surveillance
- Provides the **full microbiological taxonomy** of ~97 000 distinct
species and extensive info of ~620 antimicrobial drugs
- Applies **CLSI 2011-2026** and **EUCAST 2011-2026** clinical and
veterinary breakpoints, and ECOFFs, for MIC and disk zone

View File

@@ -56,7 +56,7 @@ footer:
left: [devtext]
right: [logo]
components:
devtext: '<code>AMR</code> (for R). Free and open-source, licenced under the <a target="_blank" href="https://github.com/msberends/AMR/blob/main/LICENSE">GNU General Public License version 2.0 (GPL-2)</a>.<br>Developed at the <a target="_blank" href="https://www.rug.nl">University of Groningen</a> and <a target="_blank" href="https://www.umcg.nl">University Medical Center Groningen</a> in The Netherlands.'
devtext: '<code>AMR</code> (for R). Free and open-source, licenced under the <a target="_blank" href="https://github.com/msberends/AMR/blob/main/LICENSE">GNU GPL 2.0</a>. Developed at the <a target="_blank" href="https://www.rug.nl">University of Groningen</a> and <a target="_blank" href="https://www.umcg.nl">University Medical Center Groningen</a> in The Netherlands, in collaboration with <a href="https://amr-for-r.org/authors.html">many colleagues from around the world</a>.'
logo: '<a target="_blank" href="https://www.rug.nl"><img src="https://amr-for-r.org/logo_rug.svg" style="max-width: 150px;"></a><a target="_blank" href="https://www.umcg.nl"><img src="https://amr-for-r.org/logo_umcg.svg" style="max-width: 150px;"></a>'
home:
@@ -156,17 +156,17 @@ reference:
- "`atc_online_property`"
- "`add_custom_antimicrobials`"
- title: "Preparing data: antimicrobial results"
- title: "Interpreting data: antimicrobial results"
desc: >
With `as.mic()` and `as.disk()` you can transform your raw input to valid MIC or disk diffusion values.
Use `as.sir()` for cleaning raw data to let it only contain "R", "I" and "S", or to interpret MIC or disk diffusion values as SIR based on the lastest EUCAST and CLSI guidelines.
Afterwards, you can extend antibiotic interpretations by applying [EUCAST rules](https://www.eucast.org/expert_rules_and_intrinsic_resistance/) with `eucast_rules()`.
Afterwards, you can extend antibiotic interpretations by applying interpretive rules, for example [from EUCAST](https://www.eucast.org/expert_rules_and_intrinsic_resistance/) with `interpretive_rules()`.
contents:
- "`as.sir`"
- "`as.mic`"
- "`as.disk`"
- "`eucast_rules`"
- "`custom_eucast_rules`"
- "`interpretive_rules`"
- "`custom_interpretive_rules`"
- title: "Analysing data"
desc: >
@@ -265,7 +265,7 @@ reference:
- title: "Other: statistical tests"
desc: >
Some statistical tests or methods are not part of base R and were added to this package for convenience.
Some statistical tests or methods usable for AMR analyses are not part of base R and were added to this package for convenience.
contents:
- "`g.test`"
- "`kurtosis`"

View File

@@ -33,18 +33,20 @@
rm -rf ../PythonPackage/AMR/*
mkdir -p ../PythonPackage/AMR/AMR
# Output Python file
# Output files
setup_file="../PythonPackage/AMR/setup.py"
functions_file="../PythonPackage/AMR/AMR/functions.py"
datasets_file="../PythonPackage/AMR/AMR/datasets.py"
init_file="../PythonPackage/AMR/AMR/__init__.py"
engine_file="../PythonPackage/AMR/AMR/_engine.py"
datasets_file="../PythonPackage/AMR/AMR/datasets.py"
functions_file="../PythonPackage/AMR/AMR/functions.py"
beta_file="../PythonPackage/AMR/AMR/beta.py"
description_file="../DESCRIPTION"
# Write header to the datasets Python file, including the convert_to_python function
cat <<EOL > "$datasets_file"
# ---- _engine.py: R environment setup and installation logic ---- #
cat <<'EOL' > "$engine_file"
import os
import sys
import pandas as pd
import importlib.metadata as metadata
# Get the path to the virtual environment
@@ -56,48 +58,127 @@ os.makedirs(r_lib_path, exist_ok=True)
os.environ['R_LIBS_SITE'] = r_lib_path
from rpy2 import robjects
from rpy2.robjects.conversion import localconverter
from rpy2.robjects import default_converter, numpy2ri, pandas2ri
from rpy2.robjects.vectors import StrVector
from rpy2.robjects.packages import importr, isinstalled
# Import base and utils
# Import base and utils once
base = importr('base')
utils = importr('utils')
base.options(warn=-1)
# Ensure library paths explicitly
# Silence R console output entirely
robjects.r('suppressMessages(suppressWarnings(sink(tempfile())))')
base._libPaths(r_lib_path)
# Check if the AMR package is installed in R
if not isinstalled('AMR', lib_loc=r_lib_path):
print(f"AMR: Installing latest AMR R package to {r_lib_path}...", flush=True)
utils.install_packages('AMR', repos='beta.amr-for-r.org', quiet=True)
_installed_source = None
# Retrieve Python AMR version
try:
python_amr_version = str(metadata.version('AMR'))
except metadata.PackageNotFoundError:
python_amr_version = str('')
# Retrieve R AMR version
r_amr_version = robjects.r(f'as.character(packageVersion("AMR", lib.loc = "{r_lib_path}"))')
r_amr_version = str(r_amr_version[0])
# Compare R and Python package versions
if r_amr_version != python_amr_version:
def _r_version():
"""Return the currently installed AMR R package version, or None."""
try:
print(f"AMR: Updating AMR package in {r_lib_path}...", flush=True)
utils.install_packages('AMR', repos='beta.amr-for-r.org', quiet=True)
except Exception as e:
print(f"AMR: Could not update: {e}", flush=True)
return str(robjects.r(
f'as.character(packageVersion("AMR", lib.loc = "{r_lib_path}"))')[0])
except Exception:
return None
print(f"AMR: Setting up R environment and AMR datasets...", flush=True)
def _py_version():
"""Return the Python AMR package version from metadata, or empty string."""
try:
return str(metadata.version('AMR'))
except metadata.PackageNotFoundError:
return ''
# Activate the automatic conversion between R and pandas DataFrames
with localconverter(default_converter + numpy2ri.converter + pandas2ri.converter):
# example_isolates
example_isolates = robjects.r('''
def _install_cran():
"""Install AMR from CRAN into the isolated library."""
print("AMR: Installing from CRAN...", flush=True)
utils.install_packages(
'AMR',
repos='https://cloud.r-project.org',
lib=r_lib_path,
quiet=True
)
def _install_github():
"""Install AMR development version from GitHub into the isolated library."""
print("AMR: Installing development version from GitHub...", flush=True)
utils.install_packages(
StrVector(['remotes', 'desc']),
repos='https://cloud.r-project.org',
lib=r_lib_path,
quiet=True
)
remotes = importr('remotes', lib_loc=r_lib_path)
remotes.install_github('msberends/AMR', lib=r_lib_path, quiet=True)
def ensure_amr(source="cran"):
"""Ensure AMR is installed from the requested source. Idempotent per source."""
global _installed_source
if _installed_source == source:
return
install_fn = _install_github if source == "github" else _install_cran
if not isinstalled('AMR', lib_loc=r_lib_path):
install_fn()
else:
# Check for version mismatch and update if needed
r_ver = _r_version()
py_ver = _py_version()
if r_ver != py_ver:
try:
install_fn()
except Exception as e:
print(f"AMR: Could not update ({e})", flush=True)
print(f"AMR: R package version {_r_version()} ready.", flush=True)
_installed_source = source
def restore_sink():
"""Restore R console output after setup is complete."""
try:
robjects.r('sink()')
except Exception:
pass
EOL
# ---- datasets.py: only dataset loading ---- #
cat <<'EOL' > "$datasets_file"
import pandas as pd
from rpy2 import robjects
from rpy2.robjects.conversion import localconverter
from rpy2.robjects import default_converter, numpy2ri, pandas2ri
from ._engine import ensure_amr, restore_sink
_cache = {}
_loaded_source = None
def _load_datasets(source="cran"):
"""Load all AMR datasets into the module cache."""
global _loaded_source
if _cache and _loaded_source == source:
return
if _cache and _loaded_source != source:
_cache.clear()
ensure_amr(source)
with localconverter(default_converter + numpy2ri.converter + pandas2ri.converter):
_cache['example_isolates'] = _load_example_isolates()
_cache['microorganisms'] = robjects.r(
'AMR::microorganisms[, !sapply(AMR::microorganisms, is.list)]')
_cache['antimicrobials'] = robjects.r(
'AMR::antimicrobials[, !sapply(AMR::antimicrobials, is.list)]')
_cache['clinical_breakpoints'] = robjects.r(
'AMR::clinical_breakpoints[, !sapply(AMR::clinical_breakpoints, is.list)]')
restore_sink()
_loaded_source = source
def _load_example_isolates():
df = robjects.r('''
df <- AMR::example_isolates
df[] <- lapply(df, function(x) {
if (inherits(x, c("Date", "POSIXt", "factor"))) {
@@ -109,26 +190,72 @@ with localconverter(default_converter + numpy2ri.converter + pandas2ri.converter
df <- df[, !sapply(df, is.list)]
df
''')
example_isolates['date'] = pd.to_datetime(example_isolates['date'])
df['date'] = pd.to_datetime(df['date'])
return df
# microorganisms
microorganisms = robjects.r('AMR::microorganisms[, !sapply(AMR::microorganisms, is.list)]')
antimicrobials = robjects.r('AMR::antimicrobials[, !sapply(AMR::antimicrobials, is.list)]')
clinical_breakpoints = robjects.r('AMR::clinical_breakpoints[, !sapply(AMR::clinical_breakpoints, is.list)]')
base.options(warn = 0)
print(f"AMR: Done.", flush=True)
def get(name, source="cran"):
"""Retrieve a dataset by name, installing AMR if needed."""
_load_datasets(source)
return _cache[name]
EOL
echo "from .datasets import example_isolates" >> $init_file
echo "from .datasets import microorganisms" >> $init_file
echo "from .datasets import antimicrobials" >> $init_file
echo "from .datasets import clinical_breakpoints" >> $init_file
# ---- __init__.py: lazy module, CRAN by default ---- #
cat <<'EOL' > "$init_file"
import sys
# Write header to the functions Python file, including the convert_to_python function
cat <<EOL > "$functions_file"
_DATASETS = frozenset({
'example_isolates', 'microorganisms',
'antimicrobials', 'clinical_breakpoints'
})
class _AMRModule(type(sys.modules[__name__])):
"""Lazy-loading module: nothing runs until an attribute is accessed."""
def __getattr__(self, name):
if name in _DATASETS:
from .datasets import get
return get(name, source="cran")
try:
from . import functions
return getattr(functions, name)
except AttributeError:
raise AttributeError(
f"module 'AMR' has no attribute '{name}'")
sys.modules[__name__].__class__ = _AMRModule
EOL
# ---- beta.py: GitHub development version ---- #
cat <<'EOL' > "$beta_file"
import sys
_DATASETS = frozenset({
'example_isolates', 'microorganisms',
'antimicrobials', 'clinical_breakpoints'
})
class _BetaModule(type(sys.modules[__name__])):
"""Lazy-loading module: installs AMR from GitHub on first access."""
def __getattr__(self, name):
if name in _DATASETS:
from .datasets import get
return get(name, source="github")
try:
from . import functions
return getattr(functions, name)
except AttributeError:
raise AttributeError(
f"module 'AMR.beta' has no attribute '{name}'")
sys.modules[__name__].__class__ = _BetaModule
EOL
# ---- functions.py: R-to-Python wrapper functions ---- #
cat <<'EOL' > "$functions_file"
import functools
import rpy2.robjects as robjects
from rpy2.robjects.packages import importr
@@ -138,7 +265,10 @@ from rpy2.robjects import default_converter, numpy2ri, pandas2ri
import pandas as pd
import numpy as np
# Import the AMR R package
from ._engine import ensure_amr
# Ensure AMR is available before importing it in R
ensure_amr("cran")
amr_r = importr('AMR')
def convert_to_r(value):
@@ -204,12 +334,11 @@ def r_to_python(r_func):
return wrapper
EOL
# Directory where the .Rd files are stored (update path as needed)
# ---- Generate wrapper functions from .Rd files ---- #
rd_dir="../man"
# Iterate through each .Rd file in the man directory
for rd_file in "$rd_dir"/*.Rd; do
# Extract function names and their arguments from the .Rd files
awk '
BEGIN {
usage_started = 0
@@ -292,18 +421,19 @@ for rd_file in "$rd_dir"/*.Rd; do
' "$rd_file"
done
# Output completion message
echo "Python wrapper functions generated in $functions_file."
echo "Python wrapper functions listed in $init_file."
# ---- README ---- #
cp ../vignettes/AMR_for_Python.Rmd ../PythonPackage/AMR/README.md
sed -i '1,/^# Introduction$/d' ../PythonPackage/AMR/README.md
echo "README copied"
echo "README copied."
# ---- setup.py ---- #
# Extract the relevant fields from DESCRIPTION
version=$(grep "^Version:" "$description_file" | awk '{print $2}')
# Write the setup.py file
cat <<EOL > "$setup_file"
from setuptools import setup, find_packages
@@ -334,10 +464,10 @@ setup(
)
EOL
# Output completion message
echo "setup.py has been generated in $setup_file."
echo "setup.py generated."
# ---- Build ---- #
cd ../PythonPackage/AMR
pip3 install build
python3 -m build
# python3 setup.py sdist bdist_wheel

View File

@@ -42,9 +42,9 @@ pre_commit_lst <- list()
usethis::ui_info(paste0("Updating internal package data"))
# See 'data-raw/eucast_rules.tsv' for the EUCAST reference file
pre_commit_lst$EUCAST_RULES_DF <- utils::read.delim(
file = "data-raw/eucast_rules.tsv",
# See 'data-raw/interpretive_rules.tsv' for the interpretive rules reference file
pre_commit_lst$INTERPRETIVE_RULES_DF <- utils::read.delim(
file = "data-raw/interpretive_rules.tsv",
skip = 9,
sep = "\t",
stringsAsFactors = FALSE,
@@ -109,11 +109,11 @@ create_species_cons_cops <- function(type = c("CoNS", "CoPS")) {
which(MO_staph$species %in% c(
"coagulase-negative", "argensis", "arlettae",
"auricularis", "borealis", "caeli", "capitis", "caprae",
"carnosus", "casei", "caseolyticus", "chromogenes", "cohnii", "condimenti",
"carnosus", "casei", "caseorum", "caseolyticus", "chromogenes", "cohnii", "condimenti",
"croceilyticus",
"debuckii", "devriesei", "edaphicus", "epidermidis",
"equorum", "felis", "fleurettii", "gallinarum",
"haemolyticus", "hominis", "jettensis", "kloosii",
"equorum", "felis", "fleurettii", "gallinarum", "halotolerans",
"haemolyticus", "hominis", "hsinchuensis", "jettensis", "kloosii",
"lentus", "lugdunensis", "massiliensis", "microti",
"muscae", "nepalensis", "pasteuri", "petrasii",
"pettenkoferi", "piscifermentans", "pragensis", "pseudoxylosus",
@@ -142,7 +142,8 @@ create_species_cons_cops <- function(type = c("CoNS", "CoPS")) {
"pseudintermedius", "pseudointermedius",
"schweitzeri", "simiae",
"roterodami",
"singaporensis"
"singaporensis",
"ursi"
) |
# old, now renamed to S. coagulans (but still as synonym in our data of course):
(MO_staph$species == "schleiferi" & MO_staph$subspecies == "coagulans")),
@@ -280,6 +281,7 @@ pre_commit_lst$MO_RELEVANT_GENERA <- c(
"Malbranchea",
"Metagonimus",
"Meyerozyma",
"Microascus",
"Microsporidium",
"Microsporum",
"Millerozyma",
@@ -306,6 +308,7 @@ pre_commit_lst$MO_RELEVANT_GENERA <- c(
"Piedraia",
"Pithomyces",
"Pityrosporum",
"Plasmodium",
"Pneumocystis",
"Pseudallescheria",
"Pseudoscopulariopsis",
@@ -323,6 +326,7 @@ pre_commit_lst$MO_RELEVANT_GENERA <- c(
"Sarcoptes",
"Scedosporium",
"Schistosoma",
"Schizophyllum",
"Schizosaccharomyces",
"Scolecobasidium",
"Scopulariopsis",
@@ -364,7 +368,7 @@ pre_commit_lst$MO_RELEVANT_GENERA <- c(
)
# antibiotic groups
# (these will also be used for eucast_rules() and understanding data-raw/eucast_rules.tsv)
# (these will also be used for interpretive_rules() and understanding data-raw/interpretive_rules.tsv)
pre_commit_lst$AB_AMINOGLYCOSIDES <- antimicrobials %>%
filter(group %like% "aminoglycoside|paromomycin|spectinomycin") %>%
pull(ab)

View File

@@ -293,7 +293,7 @@ breakpoints_new <- breakpoints |>
host = ifelse(BREAKPOINT_TYPE == "ECOFF", "ECOFF", tolower(HOST)),
method = TEST_METHOD,
site = SITE_OF_INFECTION,
mo,
mo = as.mo(mo),
rank_index = case_when(
is.na(mo_rank(mo, keep_synonyms = TRUE)) ~ 6, # for UNKNOWN, B_GRAMN, B_ANAER, B_ANAER-NEG, etc.
mo_rank(mo, keep_synonyms = TRUE) %like% "(infra|sub)" ~ 1,
@@ -453,6 +453,9 @@ breakpoints_new$breakpoint_R[breakpoints_new$guideline %like% "EUCAST" & breakpo
breakpoints_new <- breakpoints_new |>
filter(!(guideline %like% "EUCAST (2024|2025|2026)" & ref_tbl == "PK/PD"))
# WHONET still contains generic anaerobic rules for EUCAST >= 2021, but this was ended from v12 (2022) on
breakpoints_new <- breakpoints_new |>
filter(!(guideline %like% "EUCAST (2022|2023|2024|2025|2026)" & ref_tbl %like% "anaerob"))
# WHONET adds one log2 level to the R breakpoint for their software, e.g. in AMC in Enterobacterales:
# EUCAST 2023 guideline: S <= 8 and R > 8

View File

@@ -35,13 +35,13 @@ for (i in seq_len(nrow(antimicrobials))) {
colnames(int_resis)[ncol(int_resis)] <- antimicrobials$ab[i]
}
int_resis <- eucast_rules(int_resis,
eucast_rules_df = subset(
AMR:::EUCAST_RULES_DF,
is.na(have_these_values) & reference.rule_group == "Expected phenotypes" & reference.version == 1.2
),
overwrite = TRUE,
info = FALSE
int_resis <- interpretive_rules(int_resis,
interpretive_rules_df = subset(
AMR:::INTERPRETIVE_RULES_DF,
is.na(have_these_values) & reference.rule_group == "Expected phenotypes" & reference.version == 1.2
),
overwrite = TRUE,
info = FALSE
)
int_resis2 <- int_resis[, sapply(int_resis, function(x) any(!is.sir(x) | x == "R")), drop = FALSE] %>%

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
45068afc4cd9770dea329782c1aed045
7bcb6eaf7e2da23ac552acbfd12b3e62

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

View File

@@ -298,7 +298,6 @@
"B_HACEK" "B_HMPHL_PSCM" "Haemophilus, Aggregatibacter, Cardiobacterium, Eikenella, Kingella (HACEK)" "Haemophilus piscium"
"B_HACEK" "B_HMPHL_PTTM" "Haemophilus, Aggregatibacter, Cardiobacterium, Eikenella, Kingella (HACEK)" "Haemophilus pittmaniae"
"B_HACEK" "B_ACTNB_PLRP" "Haemophilus, Aggregatibacter, Cardiobacterium, Eikenella, Kingella (HACEK)" "Haemophilus pleuropneumoniae"
"B_HACEK" "B_HMPHL_QNTN" "Haemophilus, Aggregatibacter, Cardiobacterium, Eikenella, Kingella (HACEK)" "Haemophilus quentini"
"B_HACEK" "B_AGGRG_SGNS" "Haemophilus, Aggregatibacter, Cardiobacterium, Eikenella, Kingella (HACEK)" "Haemophilus segnis"
"B_HACEK" "B_HMPHL_SMNL" "Haemophilus, Aggregatibacter, Cardiobacterium, Eikenella, Kingella (HACEK)" "Haemophilus seminalis"
"B_HACEK" "B_HMPHL_SPTR" "Haemophilus, Aggregatibacter, Cardiobacterium, Eikenella, Kingella (HACEK)" "Haemophilus sputorum"
@@ -319,7 +318,6 @@
"B_KLBSL_PNMN-C" "B_KLBSL_QSPN" "Klebsiella pneumoniae complex" "Klebsiella quasipneumoniae"
"B_KLBSL_PNMN-C" "B_KLBSL_QSPN_QSPN" "Klebsiella pneumoniae complex" "Klebsiella quasipneumoniae quasipneumoniae"
"B_KLBSL_PNMN-C" "B_KLBSL_QSPN_SMLP" "Klebsiella pneumoniae complex" "Klebsiella quasipneumoniae similipneumoniae"
"B_KLBSL_PNMN-C" "B_KLBSL_QSVR" "Klebsiella pneumoniae complex" "Klebsiella quasivariicola"
"B_KLBSL_PNMN-C" "B_KLBSL_VRCL" "Klebsiella pneumoniae complex" "Klebsiella variicola"
"B_KLBSL_PNMN-C" "B_KLBSL_VRCL_TRPC" "Klebsiella pneumoniae complex" "Klebsiella variicola tropica"
"B_KLBSL_PNMN-C" "B_KLBSL_VRCL_LNSS" "Klebsiella pneumoniae complex" "Klebsiella variicola tropicalensis"
@@ -332,22 +330,20 @@
"F_MYRZY_GLLR-C" "F_MYRZY_GLLR" "Meyerozyma guilliermondii complex" "Meyerozyma guilliermondii japonica"
"F_MYRZY_GLLR-C" "F_MYRZY_GLLR" "Meyerozyma guilliermondii complex" "Meyerozyma guilliermondii muhira"
"F_MYRZY_GLLR-C" "F_MYRZY_GLLR" "Meyerozyma guilliermondii complex" "Meyerozyma guilliermondii pseudoguilliermondii"
"B_MYCBC_AVIM-C" "B_MYCBC_AVIM" "Mycobacterium avium-intracellulare complex" "Mycobacterium avium"
"B_MYCBC_AVIM-C" "B_MYCBC_AVIM_AVIM" "Mycobacterium avium-intracellulare complex" "Mycobacterium avium avium"
"B_MYCBC_AVIM-C" "B_MYCBC_AVIM_PRTB" "Mycobacterium avium-intracellulare complex" "Mycobacterium avium paratuberculosis"
"B_MYCBC_AVIM-C" "B_MYCBC_AVIM_SLVT" "Mycobacterium avium-intracellulare complex" "Mycobacterium avium silvaticum"
"B_MYCBC_AVIM-C" "B_MYCBC_LLRE" "Mycobacterium avium-intracellulare complex" "Mycobacterium intracellulare"
"B_MYCBC_AVIM-C" "B_MYCBC_LLRE_CHMR" "Mycobacterium avium-intracellulare complex" "Mycobacterium intracellulare chimaera"
"B_MYCBC_AVIM-C" "B_MYCBC_LLRE_INTR" "Mycobacterium avium-intracellulare complex" "Mycobacterium intracellulare intracellulare"
"B_MYCBC_AVIM-C" "B_MYCBC_LLRE_CHMR" "Mycobacterium avium-intracellulare complex" "Mycobacterium intracellulare yongonense"
"B_MYCBC_AVIM-C" "B_MYCBC_AVIM" "Mycobacterium avium complex" "Mycobacterium avium"
"B_MYCBC_AVIM-C" "B_MYCBC_AVIM_AVIM" "Mycobacterium avium complex" "Mycobacterium avium avium"
"B_MYCBC_AVIM-C" "B_MYCBC_AVIM_PRTB" "Mycobacterium avium complex" "Mycobacterium avium paratuberculosis"
"B_MYCBC_AVIM-C" "B_MYCBC_AVIM_SLVT" "Mycobacterium avium complex" "Mycobacterium avium silvaticum"
"B_MYCBC_AVIM-C" "B_MYCBC_LLRE" "Mycobacterium avium complex" "Mycobacterium intracellulare"
"B_MYCBC_AVIM-C" "B_MYCBC_LLRE_CHMR" "Mycobacterium avium complex" "Mycobacterium intracellulare chimaera"
"B_MYCBC_AVIM-C" "B_MYCBC_LLRE_INTR" "Mycobacterium avium complex" "Mycobacterium intracellulare intracellulare"
"B_MYCBC_AVIM-C" "B_MYCBC_LLRE_CHMR" "Mycobacterium avium complex" "Mycobacterium intracellulare yongonense"
"B_MYCBC_TBRC-C" "B_MYCBC_TBRC" "Mycobacterium tuberculosis complex" "Mycobacterium africanum"
"B_MYCBC_TBRC-C" "B_MYCBC_TBRC" "Mycobacterium tuberculosis complex" "Mycobacterium bovis"
"B_MYCBC_TBRC-C" "B_MYCBC_TBRC" "Mycobacterium tuberculosis complex" "Mycobacterium bovis bovis"
"B_MYCBC_TBRC-C" "B_MYCBC_TBRC" "Mycobacterium tuberculosis complex" "Mycobacterium bovis caprae"
"B_MYCBC_TBRC-C" "B_MYCBC_TBRC" "Mycobacterium tuberculosis complex" "Mycobacterium caprae"
"B_MYCBC_TBRC-C" "B_MYCBC_TBRC" "Mycobacterium tuberculosis complex" "Mycobacterium microti"
"B_MYCBC_TBRC-C" "B_MYCBC_MUNG" "Mycobacterium tuberculosis complex" "Mycobacterium mungi"
"B_MYCBC_TBRC-C" "B_MYCBC_ORYG" "Mycobacterium tuberculosis complex" "Mycobacterium orygis"
"B_MYCBC_TBRC-C" "B_MYCBC_TBRC" "Mycobacterium tuberculosis complex" "Mycobacterium pinnipedii"
"B_MYCBC_TBRC-C" "B_MYCBC_TBRC" "Mycobacterium tuberculosis complex" "Mycobacterium tuberculosis"
"B_MYCBC_TBRC-C" "B_MYCBC_TBRC" "Mycobacterium tuberculosis complex" "Mycobacterium tuberculosis caprae"

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1 +1 @@
990cbdfa55a1c2340aecfa67e8ac84d6
ec377180475ec3cd61ffaed401643e9a

View File

@@ -1 +1 @@
6ef98bb1bcd27052fde453bb12c0b285
5fba98b9dd8845adc9f83d52b28f8254

View File

@@ -1 +1 @@
dfdbbebfe1a542270d63b94c12889860
43f0086ac00f84bbda973c6c5e332c49

View File

@@ -1 +1 @@
6dc4dded108052760bfb626df03435e2
7613ad032fa1078b6c2ad46d7ae8236f

Binary file not shown.

BIN
data-raw/taxonomy0b.rds Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More