| Title: | Population-Aware Evaluation of Antimicrobial Resistance Predictions |
|---|---|
| Description: | Evaluates antimicrobial resistance phenotype predictions made by external models and rule-based tools against phenotypic reference results, accounting for the population structure of bacterial isolates. Provides a documented data contract and input validation; classification and probability metrics and antimicrobial susceptibility testing agreement metrics (categorical agreement, very major, major and minor errors, essential agreement) per species and antimicrobial; checks for overlap between training and test isolates; performance by genomic distance to the training data; cluster bootstrap intervals and paired model comparisons; calibration; prevalence-adjusted predictive values; and reports aligned with the TRIPOD+AI checklist (Collins et al. (2024) <doi:10.1136/bmj-2023-078378>). Organism and antimicrobial identifiers use the classes of the 'AMR' package. Models are not trained inside the package. |
| Authors: | Md Omar Faruqe [aut, cre, cph] (ORCID: <https://orcid.org/0000-0002-1226-7226>) |
| Maintainer: | Md Omar Faruqe <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 1.0.0 |
| Built: | 2026-11-12 04:52:21 UTC |
| Source: | https://github.com/omarfaruqe/AMRvalid |
Computes the agreement metrics used to compare a susceptibility test (or a genome-based prediction) with a reference method.
amr_ast_agreement( x, threshold = NULL, vme_denominator = c("class", "all"), ci = c("wilson", "clopper-pearson"), conf = 0.95, weight_by = NULL, split = "test" )amr_ast_agreement( x, threshold = NULL, vme_denominator = c("class", "all"), ci = c("wilson", "clopper-pearson"), conf = 0.95, weight_by = NULL, split = "test" )
x |
An |
threshold |
Probability at or above which a score is called R. Required for classification metrics when predictions are scores; there is deliberately no default of 0.5. Choose it on training or validation data, never on the test set. |
vme_denominator |
Denominator for very major and major error rates:
|
ci |
Interval method for proportions, passed to |
conf |
Confidence level. |
weight_by |
Optional column for cluster-weighted rates; see
|
split |
Which partition to evaluate; see |
Category metrics compare predicted and reference categories (S, SDD, I, R); NI and missing values are excluded:
Categorical agreement (CA): predicted category equals reference.
Very major error (VME): predicted S, reference R.
Major error (ME): predicted R, reference S.
Minor error (mE): one of the two is I or SDD and the other is S or R. The denominator is all evaluable isolates.
For scores, the category is R when the probability is at or above
threshold, otherwise S.
MIC metrics need predicted_mic and truth_mic:
Essential agreement (EA): predicted MIC within one doubling
dilution of the reference MIC. Off-scale reference values are handled
conservatively: a reference of <=a agrees with predictions <= 2a,
and >a agrees with predictions >= a. Predicted off-scale values are
compared at their bound.
Bias: mean of log2(predicted / reference) over on-scale pairs; positive values mean predictions are too high.
Acceptance thresholds (for example from ISO 20776-2 or regulatory guidance) are not applied here; compare the interval bounds against the criteria that apply to your setting.
A data frame with columns metric, estimate, lower,
upper, numerator and denominator.
preds <- data.frame( isolate_id = sprintf("iso%02d", 1:6), species = "E. coli", drug = "ciprofloxacin", truth = c("R", "R", "I", "S", "S", "S"), prediction = c("R", "S", "R", "S", "R", "S") ) amr_ast_agreement(amr_import_predictions(preds))preds <- data.frame( isolate_id = sprintf("iso%02d", 1:6), species = "E. coli", drug = "ciprofloxacin", truth = c("R", "R", "I", "S", "S", "S"), prediction = c("R", "S", "R", "S", "R", "S") ) amr_ast_agreement(amr_import_predictions(preds))
Resamples whole groups (isolates, lineages or genomic clusters) with replacement and recomputes metrics in every replicate. Resampling rows as if isolates were independent understates uncertainty when related isolates share outcomes; resampling clusters respects that dependence.
amr_bootstrap( x, resample_by = "isolate_id", B = 2000, threshold = NULL, intermediate = c("exclude", "susceptible", "resistant"), conf = 0.95, seed = NULL, split = "test", min_groups = 20, group = NULL )amr_bootstrap( x, resample_by = "isolate_id", B = 2000, threshold = NULL, intermediate = c("exclude", "susceptible", "resistant"), conf = 0.95, seed = NULL, split = "test", min_groups = 20, group = NULL )
x |
An |
resample_by |
Column defining the resampling unit, e.g. |
B |
Number of bootstrap replicates. |
threshold |
Probability at or above which a score is called R. Required for classification metrics when predictions are scores; there is deliberately no default of 0.5. Choose it on training or validation data, never on the test set. |
intermediate |
How to treat I and SDD in binary metrics, for both
reference and prediction: |
conf |
Confidence level. |
seed |
Optional random seed for reproducibility. The global random state is restored afterwards. |
split |
Which partition to evaluate when a |
min_groups |
Strata with fewer independent groups than this are flagged: cluster bootstrap intervals are unreliable with few clusters. |
group |
Deprecated; use |
Metrics are computed on S/R categories after applying the intermediate
policy, so the very major error rate equals 1 - sensitivity and the
major error rate equals 1 - specificity. Minor errors are not
bootstrapped. Intervals are percentile intervals; n_valid counts the
replicates in which a metric was defined (for example, sensitivity is
undefined in a replicate that drew no resistant isolates). Treat
intervals with many invalid replicates with caution.
When every replicate gives the same value (typically a sensitivity of 1
or a very major error rate of 0), the percentile interval has zero width
and cannot express uncertainty. Such rows are marked zero_width; use
the Wilson or Clopper-Pearson intervals from amr_evaluate() for
them instead.
An object of class amr_bootstrap: a data frame with the
stratum columns, metric, estimate, lower, upper, n_groups,
B, n_valid, few_groups and zero_width.
set.seed(2) preds <- data.frame( isolate_id = sprintf("iso%02d", 1:30), species = "K. pneumoniae", drug = "meropenem", truth = rep(c("R", "S", "S"), 10), probability = runif(30), lineage = rep(sprintf("ST%d", 1:10), each = 3) ) amr_bootstrap(amr_import_predictions(preds), resample_by = "lineage", B = 200, threshold = 0.5, seed = 1)set.seed(2) preds <- data.frame( isolate_id = sprintf("iso%02d", 1:30), species = "K. pneumoniae", drug = "meropenem", truth = rep(c("R", "S", "S"), 10), probability = runif(30), lineage = rep(sprintf("ST%d", 1:10), each = 3) ) amr_bootstrap(amr_import_predictions(preds), resample_by = "lineage", B = 200, threshold = 0.5, seed = 1)
Assesses whether predicted probabilities of resistance match observed frequencies. Discrimination (AUC) and calibration are different properties: a model can rank isolates well while its probabilities are systematically too high or too low, which matters whenever the probability itself is used, for example to set a threshold or to report a risk.
amr_calibration( x, bins = 10, binning = c("equal_width", "equal_count"), by = NULL, intermediate = c("exclude", "susceptible", "resistant"), conf = 0.95, split = "test", min_events = 10 )amr_calibration( x, bins = 10, binning = c("equal_width", "equal_count"), by = NULL, intermediate = c("exclude", "susceptible", "resistant"), conf = 0.95, split = "test", min_events = 10 )
x |
An |
bins |
Number of bins for the binned reliability curve and the expected calibration error. |
binning |
|
by |
Optional column name to evaluate calibration separately for
each value, in addition to the stratum (e.g. |
intermediate |
How to treat I and SDD in binary metrics, for both
reference and prediction: |
conf |
Confidence level. |
split |
Which partition to evaluate when a |
min_events |
Strata with fewer resistant or fewer susceptible isolates than this are flagged as too sparse for stable estimates. |
Reported per stratum (model, dataset, species, antimicrobial):
mean predicted and observed proportion resistant, and their ratio O/E (observed / expected; 1 is ideal);
calibration intercept (calibration-in-the-large): the intercept of a logistic model of the outcome with the logit of the predicted probability as an offset; 0 is ideal, positive means predictions are too low on average;
calibration slope: the coefficient of a logistic regression of the outcome on the logit of the predicted probability; 1 is ideal, below 1 means predictions are too extreme (over-confident);
expected calibration error (ECE): the count-weighted mean absolute difference between mean predicted and observed proportion across bins, and the maximum calibration error (MCE). Both depend on the binning and are secondary to the slope and intercept.
Probabilities are clipped to [1e-6, 1 - 1e-6] before taking logits. Intervals for the intercept and slope are Wald intervals assuming independent isolates.
The curve element holds a loess-smoothed reliability curve (drawn when
there are at least 20 isolates and 10 distinct predicted values) and the
bins element the binned points with Wilson intervals; plot() draws
both.
An object of class amr_calibration: a list with summary (one
row per stratum), bins and curve.
Van Calster B, McLernon DJ, van Smeden M, Wynants L, Steyerberg EW (2019). Calibration: the Achilles heel of predictive analytics. BMC Medicine 17:230.
sim <- amr_simulate(n_lineages = 20, isolates_per_lineage = 10, external_lineages = 20, return_distances = FALSE, seed = 1) x <- amr_import_predictions(sim$predictions) cal <- amr_calibration(x[x$dataset_id == "external", ]) cal plot(cal)sim <- amr_simulate(n_lineages = 20, isolates_per_lineage = 10, external_lineages = 20, return_distances = FALSE, seed = 1) x <- amr_import_predictions(sim$predictions) cal <- amr_calibration(x[x$dataset_id == "external", ]) cal plot(cal)
Returns the field specification that every prediction table must follow after import. One row is one prediction for one isolate, one antimicrobial, one model and one evaluation context (dataset, split and fold).
amr_contract()amr_contract()
A data frame with columns field, requirement, type and
description.
amr_contract()amr_contract()
The schema lets workflows in other languages (for example Python) export prediction tables that AMRvalid can import without changes.
amr_contract_schema()amr_contract_schema()
Path to amrvalid-contract.json inside the installed package.
amr_contract_schema()amr_contract_schema()
Counts predictions and reference phenotypes per model, dataset, species and antimicrobial.
amr_data_summary(x)amr_data_summary(x)
x |
An |
A data frame with one row per stratum.
preds <- data.frame( isolate_id = c("a", "b", "c", "d"), species = "K. pneumoniae", drug = c("meropenem", "meropenem", "ciprofloxacin", "ciprofloxacin"), truth = c("R", "S", "I", "R"), prediction = c("R", "S", "R", "R") ) amr_data_summary(amr_import_predictions(preds))preds <- data.frame( isolate_id = c("a", "b", "c", "d"), species = "K. pneumoniae", drug = c("meropenem", "meropenem", "ciprofloxacin", "ciprofloxacin"), truth = c("R", "S", "I", "R"), prediction = c("R", "S", "R", "R") ) amr_data_summary(amr_import_predictions(preds))
Assigns each test isolate the distance to its nearest training isolate, groups test isolates into distance bands and evaluates each band per species-antimicrobial stratum. A trend model then summarises how fast performance decays as isolates become less similar to anything the model was trained on.
amr_distance_eval( x, distances, train_manifest = NULL, bands = 3, threshold = NULL, intermediate = c("exclude", "susceptible", "resistant"), ci = c("wilson", "clopper-pearson"), conf = 0.95, vme_denominator = c("class", "all"), target_prevalence = NULL, weight_by = NULL )amr_distance_eval( x, distances, train_manifest = NULL, bands = 3, threshold = NULL, intermediate = c("exclude", "susceptible", "resistant"), ci = c("wilson", "clopper-pearson"), conf = 0.95, vme_denominator = c("class", "all"), target_prevalence = NULL, weight_by = NULL )
x |
An |
distances |
Pairwise distances: a matrix or long table, see
|
train_manifest |
Optional data frame listing training isolates, with
an |
bands |
Either a single number of quantile-based bands (default
|
threshold |
Probability at or above which a score is called R. Required for classification metrics when predictions are scores; there is deliberately no default of 0.5. Choose it on training or validation data, never on the test set. |
intermediate |
How to treat I and SDD in binary metrics, for both
reference and prediction: |
ci |
Interval method for proportions, passed to |
conf |
Confidence level. |
vme_denominator, target_prevalence, weight_by
|
Passed to
|
The trend summary depends on what the predictions allow:
with categories (binary, S/I/R, or scores with a threshold): a
logistic regression of correct classification on distance, reported
as the odds ratio per standard deviation of distance;
with scores and no threshold: a linear regression of the squared error (the per-isolate Brier score) on distance, reported as the change per standard deviation.
With weight_by, band metrics are cluster-weighted (see amr_metrics())
and the trend model weights each cluster equally, so its standard error
reflects the number of clusters rather than isolates. Calibration rows
are not weighted.
For score predictions, each band also reports the calibration intercept
and slope (see amr_calibration()), showing whether a model becomes
over-confident on isolates unlike its training data.
An odds ratio below 1 (or a positive Brier slope) means performance worsens with distance. Intervals are Wald intervals that treat isolates as independent; use them as a description, not a test.
An object of class amr_distance_eval: a list with nearest
(per-isolate distances and bands), by_band (metrics per band, in the
format of amr_evaluate()) and trend (one row per stratum).
set.seed(1) ids <- sprintf("iso%02d", 1:40) preds <- data.frame( isolate_id = ids, species = "E. coli", drug = "ciprofloxacin", truth = rep(c("R", "S"), 20), probability = runif(40), split = rep(c("train", "test"), each = 20) ) dist <- as.matrix(stats::dist(seq_along(ids))) dimnames(dist) <- list(ids, ids) amr_distance_eval(amr_import_predictions(preds), dist, threshold = 0.5)set.seed(1) ids <- sprintf("iso%02d", 1:40) preds <- data.frame( isolate_id = ids, species = "E. coli", drug = "ciprofloxacin", truth = rep(c("R", "S"), 20), probability = runif(40), split = rep(c("train", "test"), each = 20) ) dist <- as.matrix(stats::dist(seq_along(ids))) dimnames(dist) <- list(ids, ids) amr_distance_eval(amr_import_predictions(preds), dist, threshold = 0.5)
Splits an amr_data object by model, dataset, species and antimicrobial
(and optionally further columns) and computes amr_metrics() and, where
possible, amr_ast_agreement() in each stratum. There is no pooled
headline score.
amr_evaluate( x, threshold = NULL, intermediate = c("exclude", "susceptible", "resistant"), vme_denominator = c("class", "all"), ci = c("wilson", "clopper-pearson"), conf = 0.95, split = "test", validate = TRUE, target_prevalence = NULL, weight_by = NULL, by = NULL )amr_evaluate( x, threshold = NULL, intermediate = c("exclude", "susceptible", "resistant"), vme_denominator = c("class", "all"), ci = c("wilson", "clopper-pearson"), conf = 0.95, split = "test", validate = TRUE, target_prevalence = NULL, weight_by = NULL, by = NULL )
x |
An |
threshold |
Probability at or above which a score is called R. Required for classification metrics when predictions are scores; there is deliberately no default of 0.5. Choose it on training or validation data, never on the test set. |
intermediate |
How to treat I and SDD in binary metrics, for both
reference and prediction: |
vme_denominator |
Denominator for very major and major error rates:
|
ci |
Interval method for proportions, passed to |
conf |
Confidence level. |
split |
Which partition to evaluate when a |
validate |
If |
target_prevalence, weight_by
|
Passed to |
by |
Optional column name(s) to evaluate subgroups within each
stratum, e.g. |
An object of class amr_eval: a long data frame with the
stratum columns (and any by columns), species_name, drug_name, family
("ml" or "ast") and the metric columns.
preds <- data.frame( isolate_id = sprintf("iso%02d", 1:8), species = "K. pneumoniae", drug = rep(c("meropenem", "ciprofloxacin"), each = 4), truth = c("R", "S", "S", "R", "R", "R", "S", "S"), probability = c(0.9, 0.2, 0.4, 0.7, 0.8, 0.3, 0.1, 0.6) ) x <- amr_import_predictions(preds) amr_evaluate(x, threshold = 0.5) # subgroups, e.g. by country of isolation x$country <- c("CA", "CA", "US", "US", "CA", "US", "CA", "US") amr_evaluate(x, threshold = 0.5, by = "country")preds <- data.frame( isolate_id = sprintf("iso%02d", 1:8), species = "K. pneumoniae", drug = rep(c("meropenem", "ciprofloxacin"), each = 4), truth = c("R", "S", "S", "R", "R", "R", "S", "S"), probability = c(0.9, 0.2, 0.4, 0.7, 0.8, 0.3, 0.1, 0.6) ) x <- amr_import_predictions(preds) amr_evaluate(x, threshold = 0.5) # subgroups, e.g. by country of isolation x$country <- c("CA", "CA", "US", "US", "CA", "US", "CA", "US") amr_evaluate(x, threshold = 0.5, by = "country")
Real, public data for trying AMRvalid: laboratory susceptibility results for meropenem and ciprofloxacin in Klebsiella pneumoniae isolates from NCBI Pathogen Detection, with predictions from two transparent rule-based predictors per antimicrobial applied to the AMRFinderPlus genotypes.
amr_example_data()amr_example_data()
Source: NCBI Pathogen Detection, Klebsiella analysis PDG000000012.2568 (metadata and SNP cluster files, downloaded 2 November 2026). Isolates were kept if NCBI labelled them K. pneumoniae and they had a meropenem or ciprofloxacin phenotype. Breakpoint standards are those of the submitting laboratories and are not recorded consistently, so the data are suitable for demonstrating methods, not for drawing clinical conclusions.
Rules applied to the AMRFinderPlus calls (calls flagged MISTRANSLATION ignored):
carbapenemase genes (meropenem): any KPC, NDM, VIM, IMP or OXA-48-like gene;
carbapenemase or porin + ESBL/AmpC (meropenem): the above, or an OmpK35/OmpK36 alteration together with a CTX-M, DHA, CMY, ACT or FOX gene;
QRDR mutations (ciprofloxacin): gyrA S83/D87 or parC S80/E84 substitutions;
QRDR mutations or PMQR genes (ciprofloxacin): the above, or a qnr, aac(6')-Ib-cr or qepA gene.
These are illustrative textbook rules, not recommendations. The script
that builds the data, data-raw/ncbi_kpneumoniae.R, is in the package
source repository.
A data frame ready for amr_import_predictions(), with one row
per isolate, antimicrobial and predictor:
NCBI BioSample accession.
Always "Klebsiella pneumoniae".
"meropenem" or "ciprofloxacin".
Reference category (S, I or R) as submitted to BioSample.
Predicted category (S or R) from the rule.
The rule (see Details).
Source and NCBI analysis version.
NCBI SNP cluster (PDS accession); empty when the isolate is not in a cluster.
BioProject accession (the submitting study).
Year, and full date when available.
Country of isolation.
AMRFinderPlus version used by NCBI.
NCBI Pathogen Detection, https://www.ncbi.nlm.nih.gov/pathogens/. Data produced by the U.S. National Center for Biotechnology Information are in the public domain.
d <- amr_example_data() head(d) x <- amr_import_predictions(d) xd <- amr_example_data() head(d) x <- amr_import_predictions(d) x
Renames columns, coerces identifiers to AMR package classes, converts
predictions to a common representation and detects the prediction type.
The function never drops rows; problems are reported by
amr_validate().
amr_import_predictions( data, mapping = NULL, prediction_type = NULL, source = "custom", strict = TRUE )amr_import_predictions( data, mapping = NULL, prediction_type = NULL, source = "custom", strict = TRUE )
data |
A data frame with one row per prediction. |
mapping |
Optional named character vector mapping contract field
names to column names in |
prediction_type |
One of |
source |
Short label for where the predictions came from (recorded
in the |
strict |
If |
A column called truth is treated as truth_sir. Predicted classes may
be given as S/I/R text, 0/1 or FALSE/TRUE (where 1 or TRUE
means resistant). Missing model_id and dataset_id columns are filled
with "model_1" and "dataset_1".
The coercion attribute of the result lists every distinct input name,
the code it was matched to and whether the match was exact.
Detection order for prediction_type: predicted_mic gives "mic";
otherwise probability gives "score"; otherwise a prediction column
containing I or SDD gives "sir"; otherwise "binary".
A data frame of class amr_data.
preds <- data.frame( isolate_id = c("a", "b", "c"), species = "K. pneumoniae", drug = "meropenem", truth = c("R", "S", "S"), probability = c(0.9, 0.2, 0.6) ) amr_import_predictions(preds)preds <- data.frame( isolate_id = c("a", "b", "c"), species = "K. pneumoniae", drug = "meropenem", truth = c("R", "S", "S"), probability = c(0.9, 0.2, 0.6) ) amr_import_predictions(preds)
Computes machine-learning performance metrics for the rows of an
amr_data object, treating resistance (R) as the positive class. Use
amr_evaluate() to compute them per species-antimicrobial stratum;
pooling strata is rarely meaningful.
amr_metrics( x, threshold = NULL, intermediate = c("exclude", "susceptible", "resistant"), ci = c("wilson", "clopper-pearson"), conf = 0.95, target_prevalence = NULL, weight_by = NULL, split = "test" )amr_metrics( x, threshold = NULL, intermediate = c("exclude", "susceptible", "resistant"), ci = c("wilson", "clopper-pearson"), conf = 0.95, target_prevalence = NULL, weight_by = NULL, split = "test" )
x |
An |
threshold |
Probability at or above which a score is called R. Required for classification metrics when predictions are scores; there is deliberately no default of 0.5. Choose it on training or validation data, never on the test set. |
intermediate |
How to treat I and SDD in binary metrics, for both
reference and prediction: |
ci |
Interval method for proportions, passed to |
conf |
Confidence level. |
target_prevalence |
Optional proportion resistant in the setting
where the predictor will be used. Adds |
weight_by |
Optional column (e.g. |
split |
Which partition to evaluate when a |
ROC-AUC is computed with the Mann-Whitney statistic (ties count one half) and its interval with the DeLong variance. Row-level intervals assume isolates are independent, which is optimistic when isolates are clonally related.
Prevalence-adjusted values use PPV = sens x p / (sens x p + (1 - spec) x (1 - p)) and NPV = spec x (1 - p) / (spec x (1 - p) + (1 - sens) x p). Both increase with sensitivity and specificity, so their bounds combine the lower (and upper) bounds of the two; these bounds are conservative.
With weight_by, numerators and denominators are sums of weights, and
proportion intervals use the Kish effective sample size
(sum of weights squared / sum of squared weights). The AUC becomes a
weighted Mann-Whitney statistic without a closed-form interval.
A data frame with one row per metric and columns metric,
estimate, lower, upper, numerator and denominator. Metrics
without a closed-form interval (balanced accuracy, F1, MCC, Brier
score, log loss) have NA bounds; use amr_bootstrap() for
group-aware intervals.
DeLong ER, DeLong DM, Clarke-Pearson DL (1988). Comparing the areas under two or more correlated receiver operating characteristic curves: a nonparametric approach. Biometrics 44:837-845.
preds <- data.frame( isolate_id = sprintf("iso%02d", 1:8), species = "K. pneumoniae", drug = "meropenem", truth = c("R", "R", "R", "S", "S", "S", "S", "R"), probability = c(0.91, 0.75, 0.30, 0.10, 0.20, 0.65, 0.05, 0.88), lineage = c("ST258", "ST258", "ST258", "ST11", "ST15", "ST15", "ST307", "ST147") ) x <- amr_import_predictions(preds) amr_metrics(x, threshold = 0.5) # PPV and NPV where 10% of isolates are resistant amr_metrics(x, threshold = 0.5, target_prevalence = 0.1) # each lineage counts once amr_metrics(x, threshold = 0.5, weight_by = "lineage")preds <- data.frame( isolate_id = sprintf("iso%02d", 1:8), species = "K. pneumoniae", drug = "meropenem", truth = c("R", "R", "R", "S", "S", "S", "S", "R"), probability = c(0.91, 0.75, 0.30, 0.10, 0.20, 0.65, 0.05, 0.88), lineage = c("ST258", "ST258", "ST258", "ST11", "ST15", "ST15", "ST307", "ST147") ) x <- amr_import_predictions(preds) amr_metrics(x, threshold = 0.5) # PPV and NPV where 10% of isolates are resistant amr_metrics(x, threshold = 0.5, target_prevalence = 0.1) # each lineage counts once amr_metrics(x, threshold = 0.5, weight_by = "lineage")
For every test isolate, finds the genomically closest training isolate using distances computed upstream (for example Mash, SKA, core-genome SNP or cgMLST allele distances). AMRvalid does not compute distances.
amr_nearest_train_distance(test_ids, train_ids, distances)amr_nearest_train_distance(test_ids, train_ids, distances)
test_ids |
Character vector of test isolate identifiers. |
train_ids |
Character vector of training isolate identifiers. |
distances |
Either a square numeric matrix with isolate identifiers
as row and column names, or a data frame with columns |
A data frame with isolate_id, nearest_train_id and
distance. Test isolates with no distance to any training isolate get
NA. A test isolate that is itself in the training set gets distance 0.
m <- matrix(c(0, 2, 9, 2, 0, 7, 9, 7, 0), 3, dimnames = list(c("a", "b", "c"), c("a", "b", "c"))) amr_nearest_train_distance(c("b", "c"), "a", m)m <- matrix(c(0, 2, 9, 2, 0, 7, 9, 7, 0), 3, dimnames = list(c("a", "b", "c"), c("a", "b", "c"))) amr_nearest_train_distance(c("b", "c"), "a", m)
Restricts both models to the isolates they share in each stratum and bootstraps the difference in each metric, resampling the same groups for both models so the pairing is kept in every replicate.
amr_paired_compare( x, models = NULL, resample_by = "isolate_id", B = 2000, threshold = NULL, intermediate = c("exclude", "susceptible", "resistant"), conf = 0.95, seed = NULL, split = "test", min_groups = 20, group = NULL )amr_paired_compare( x, models = NULL, resample_by = "isolate_id", B = 2000, threshold = NULL, intermediate = c("exclude", "susceptible", "resistant"), conf = 0.95, seed = NULL, split = "test", min_groups = 20, group = NULL )
x |
An |
models |
Character vector of two |
resample_by |
Column defining the resampling unit, e.g. |
B |
Number of bootstrap replicates. |
threshold |
Probability threshold for scores: a single number, or a named numeric vector with one threshold per model. |
intermediate |
How to treat I and SDD in binary metrics, for both
reference and prediction: |
conf |
Confidence level. |
seed |
Optional random seed for reproducibility. The global random state is restored afterwards. |
split |
Which partition to evaluate when a |
min_groups |
Strata with fewer independent groups than this are flagged: cluster bootstrap intervals are unreliable with few clusters. |
group |
Deprecated; use |
Besides the bootstrap intervals, an exact McNemar test compares the discordant pairs of correct and incorrect classifications. It assumes independent isolates, so prefer the group-aware intervals when isolates are clustered.
An object of class amr_paired_compare: a data frame with
dataset_id, species, drug, metric, the estimate for each model
(model_a, model_b), difference, lower, upper, n_matched,
n_groups, n_valid and mcnemar_p (on the accuracy row).
set.seed(3) ids <- sprintf("iso%02d", 1:40) truth <- rep(c("R", "S"), 20) preds <- rbind( data.frame(isolate_id = ids, truth = truth, model_id = "old", probability = ifelse(truth == "R", 0.6, 0.4) + rnorm(40, 0, 0.2)), data.frame(isolate_id = ids, truth = truth, model_id = "new", probability = ifelse(truth == "R", 0.8, 0.2) + rnorm(40, 0, 0.1)) ) preds$probability <- pmin(pmax(preds$probability, 0), 1) preds$species <- "E. coli" preds$drug <- "ciprofloxacin" amr_paired_compare(amr_import_predictions(preds), models = c("old", "new"), threshold = 0.5, B = 200, seed = 1)set.seed(3) ids <- sprintf("iso%02d", 1:40) truth <- rep(c("R", "S"), 20) preds <- rbind( data.frame(isolate_id = ids, truth = truth, model_id = "old", probability = ifelse(truth == "R", 0.6, 0.4) + rnorm(40, 0, 0.2)), data.frame(isolate_id = ids, truth = truth, model_id = "new", probability = ifelse(truth == "R", 0.8, 0.2) + rnorm(40, 0, 0.1)) ) preds$probability <- pmin(pmax(preds$probability, 0), 1) preds$species <- "E. coli" preds$drug <- "ciprofloxacin" amr_paired_compare(amr_import_predictions(preds), models = c("old", "new"), threshold = 0.5, B = 200, seed = 1)
Converts sensitivity and specificity into the positive and negative
predictive values expected where the proportion of resistant isolates is
prevalence.
amr_ppv_npv(sensitivity, specificity, prevalence)amr_ppv_npv(sensitivity, specificity, prevalence)
sensitivity, specificity
|
Numeric vectors of values in [0, 1]. |
prevalence |
Proportion resistant in the target setting. |
A data frame with prevalence, ppv and npv.
# a test with 95% sensitivity and 90% specificity amr_ppv_npv(0.95, 0.90, prevalence = c(0.01, 0.1, 0.5))# a test with 95% sensitivity and 90% specificity amr_ppv_npv(0.95, 0.90, prevalence = c(0.01, 0.1, 0.5))
Wilson score and Clopper-Pearson (exact) intervals. Both behave well when the denominator is small or the proportion is near 0 or 1, as is common for very major error rates where few isolates are resistant.
amr_prop_ci(x, n, method = c("wilson", "clopper-pearson"), conf = 0.95)amr_prop_ci(x, n, method = c("wilson", "clopper-pearson"), conf = 0.95)
x |
Number of events (numerator). |
n |
Number of trials (denominator). |
method |
|
conf |
Confidence level. |
A data frame with estimate, lower and upper. Rows with
n == 0 give NA.
Wilson EB (1927). Probable inference, the law of succession, and statistical inference. Journal of the American Statistical Association 22:209-212.
Clopper CJ, Pearson ES (1934). The use of confidence or fiducial limits illustrated in the case of the binomial. Biometrika 26:404-413.
# 1 very major error among 12 resistant isolates amr_prop_ci(1, 12) amr_prop_ci(1, 12, method = "clopper-pearson")# 1 very major error among 12 resistant isolates amr_prop_ci(1, 12) amr_prop_ci(1, 12, method = "clopper-pearson")
Collects what was evaluated and how, and labels every item by where it
came from: checked (derived by AMRvalid from the data or results),
supplied (declared by you in model_info or dataset_info) or
not assessable (needed for reporting but neither available nor
declared). Each item is mapped to the TRIPOD+AI checklist item it
informs.
amr_provenance(x, ..., model_info = list(), dataset_info = list())amr_provenance(x, ..., model_info = list(), dataset_info = list())
x |
An |
... |
Result objects to include: any of |
model_info |
Named list describing the model(s), e.g.
|
dataset_info |
Named list describing the evaluation data, e.g.
|
An object of class amr_provenance: a data frame with columns
section, item, value, status and tripod_item.
Collins GS, Moons KGM, Dhiman P, et al. (2024). TRIPOD+AI statement: updated guidance for reporting clinical prediction models that use regression or machine learning methods. BMJ 385:e078378.
sim <- amr_simulate(n_lineages = 20, isolates_per_lineage = 10, external_lineages = 0, return_distances = FALSE, seed = 1) x <- amr_import_predictions(sim$predictions) e <- amr_evaluate(x, threshold = 0.5) amr_provenance(x, e, model_info = list(threshold_selection = "fixed at 0.5"))sim <- amr_simulate(n_lineages = 20, isolates_per_lineage = 10, external_lineages = 0, return_distances = FALSE, seed = 1) x <- amr_import_predictions(sim$predictions) e <- amr_evaluate(x, threshold = 0.5) amr_provenance(x, e, model_info = list(threshold_selection = "fixed at 0.5"))
Writes a self-contained Markdown (or HTML) report: data summary, validation checks, split integrity, performance per stratum with intervals, calibration, distance-to-training results, uncertainty, automatically detected limitations, and a TRIPOD+AI checklist table showing which items the analysis covers.
amr_report( x, ..., model_info = list(), dataset_info = list(), file = NULL, format = c("markdown", "html"), title = "AMRvalid evaluation report" )amr_report( x, ..., model_info = list(), dataset_info = list(), file = NULL, format = c("markdown", "html"), title = "AMRvalid evaluation report" )
x |
An |
... |
Result objects to include: any of |
model_info |
Named list describing the model(s), e.g.
|
dataset_info |
Named list describing the evaluation data, e.g.
|
file |
Output path. If |
format |
|
title |
Report title. |
The path to the report (invisibly), or the Markdown text when
file is NULL.
sim <- amr_simulate(n_lineages = 20, isolates_per_lineage = 10, external_lineages = 0, return_distances = FALSE, seed = 1) x <- amr_import_predictions(sim$predictions) e <- amr_evaluate(x, threshold = 0.5) report <- amr_report(x, e, file = file.path(tempdir(), "amrvalid-report.md")) cat(readLines(report, n = 20), sep = "\n")sim <- amr_simulate(n_lineages = 20, isolates_per_lineage = 10, external_lineages = 0, return_distances = FALSE, seed = 1) x <- amr_import_predictions(sim$predictions) e <- amr_evaluate(x, threshold = 0.5) report <- amr_report(x, e, file = file.path(tempdir(), "amrvalid-report.md")) cat(readLines(report, n = 20), sep = "\n")
Generates isolates in lineages, a resistance phenotype driven by a causal marker plus lineage-inherited background, genomic distances, and predictions from two fixed predictors evaluated under a random and a lineage-held-out split. It exists to teach and test evaluation methods: because the generating process is known, it shows how much each validation design overstates performance on new lineages.
amr_simulate( n_lineages = 40, isolates_per_lineage = 25, external_lineages = 40, marker_effect = 3, lineage_sd = 1.5, intercept = -1.5, within_sd = 5, test_fraction = 0.2, k = 5, return_distances = TRUE, seed = NULL )amr_simulate( n_lineages = 40, isolates_per_lineage = 25, external_lineages = 40, marker_effect = 3, lineage_sd = 1.5, intercept = -1.5, within_sd = 5, test_fraction = 0.2, k = 5, return_distances = TRUE, seed = NULL )
n_lineages |
Number of lineages in the development dataset. |
isolates_per_lineage |
Isolates per lineage. |
external_lineages |
Number of fresh lineages in an external dataset
( |
marker_effect |
Log-odds effect of the causal marker. |
lineage_sd |
Standard deviation of the lineage effect on the log-odds of resistance (resistance inherited clonally but not explained by the marker). |
intercept |
Baseline log-odds of resistance. |
within_sd |
Spread of isolates around their lineage centre, in distance units (lineage centres are spread over 0 to 1000). |
test_fraction |
Fraction of isolates (random split) or lineages (lineage-held-out split) placed in the test set. |
k |
Number of neighbours for the nearest-neighbour predictor. |
return_distances |
If |
seed |
Optional random seed; the global random state is restored. |
Each lineage has a centre in a two-dimensional space; isolate distances are Euclidean distances between isolates, standing in for genomic distances. Marker frequency varies by lineage (Beta(0.5, 0.5)), so the marker is partly confounded with lineage, as resistance genes often are.
The two predictors are fixed rules, not trained machine-learning models:
knn: the proportion of resistant isolates among the k nearest
training isolates. It memorises lineages.
marker: the proportion resistant among training isolates with the
same marker status (with a +1/+2 smoothing). It learns the mechanism.
Each predictor is applied under two designs, giving model identifiers
knn/random, knn/lineage, marker/random and marker/lineage.
A list with predictions (a data frame ready for
amr_import_predictions(); test isolates only), train_manifest
(training isolates per model), isolates (all isolates with lineage,
marker, phenotype and coordinates), distances (a matrix or NULL)
and parameters.
sim <- amr_simulate(n_lineages = 20, isolates_per_lineage = 10, external_lineages = 10, seed = 1) x <- amr_import_predictions(sim$predictions) e <- amr_evaluate(x, threshold = 0.5, validate = FALSE) e[e$metric == "accuracy", c("model_id", "dataset_id", "estimate")]sim <- amr_simulate(n_lineages = 20, isolates_per_lineage = 10, external_lineages = 10, seed = 1) x <- amr_import_predictions(sim$predictions) e <- amr_evaluate(x, threshold = 0.5, validate = FALSE) e[e$metric == "accuracy", c("model_id", "dataset_id", "estimate")]
Runs structural and scientific checks before evaluation. Blocking problems are reported as errors; problems that need attention but do not prevent analysis are warnings; notes that affect interpretation are info.
amr_validate(x, min_n = 10, min_resistant = 5)amr_validate(x, min_n = 10, min_resistant = 5)
x |
An |
min_n |
Strata with fewer evaluable isolates than this trigger a warning. |
min_resistant |
Strata with fewer reference-resistant isolates than this trigger a warning, because very major error rates are then unstable. |
An object of class amr_validation: a list with ok (TRUE if
there are no errors) and issues (a data frame with columns level,
check, message and n_rows).
preds <- data.frame( isolate_id = c("a", "a", "b"), species = "E. coli", drug = "ciprofloxacin", truth = c("R", "R", "S"), probability = c(0.9, 0.8, 1.4) ) amr_validate(amr_import_predictions(preds))preds <- data.frame( isolate_id = c("a", "a", "b"), species = "E. coli", drug = "ciprofloxacin", truth = c("R", "R", "S"), probability = c(0.9, 0.8, 1.4) ) amr_validate(amr_import_predictions(preds))
Compares the isolates a model was trained on with the isolates it is evaluated on, and reports which validation designs the split is consistent with. AMRvalid cannot see upstream modelling steps, so a clean result shows only that no visible overlap exists in the supplied metadata.
amr_validate_splits( x, train_manifest = NULL, group = NULL, claimed_design = NULL )amr_validate_splits( x, train_manifest = NULL, group = NULL, claimed_design = NULL )
x |
An |
train_manifest |
Optional data frame listing training isolates, with
an |
group |
Grouping columns to check for overlap. Defaults to whichever
of |
claimed_design |
Optional design the evaluation claims to follow:
|
An object of class amr_split_check: a list with ok,
summary (one row per model and check) and consistent_designs
(one row per model).
preds <- data.frame( isolate_id = c("a", "b", "c", "d", "e", "f"), species = "K. pneumoniae", drug = "meropenem", truth = c("R", "S", "R", "S", "R", "S"), probability = c(0.9, 0.1, 0.8, 0.3, 0.7, 0.2), split = c("train", "train", "train", "test", "test", "test"), lineage = c("ST258", "ST11", "ST258", "ST15", "ST258", "ST307") ) amr_validate_splits(amr_import_predictions(preds), claimed_design = "group")preds <- data.frame( isolate_id = c("a", "b", "c", "d", "e", "f"), species = "K. pneumoniae", drug = "meropenem", truth = c("R", "S", "R", "S", "R", "S"), probability = c(0.9, 0.1, 0.8, 0.3, 0.7, 0.2), split = c("train", "train", "train", "test", "test", "test"), lineage = c("ST258", "ST11", "ST258", "ST15", "ST258", "ST307") ) amr_validate_splits(amr_import_predictions(preds), claimed_design = "group")
Dot-and-interval plots of bootstrap estimates, one panel per metric.
## S3 method for class 'amr_bootstrap' plot(x, metrics = NULL, ...)## S3 method for class 'amr_bootstrap' plot(x, metrics = NULL, ...)
x |
An |
metrics |
Metrics to draw; by default sensitivity and specificity when available, otherwise ROC-AUC. |
... |
Unused. |
x, invisibly.
x <- amr_import_predictions(amr_example_data()) b <- amr_bootstrap(x, resample_by = "genomic_cluster", B = 100, seed = 1) plot(b)x <- amr_import_predictions(amr_example_data()) b <- amr_bootstrap(x, resample_by = "genomic_cluster", B = 100, seed = 1) plot(b)
Draws one panel per stratum (model, dataset, species and antimicrobial,
plus the by column if used) with the binned observed proportions, their
Wilson intervals and the loess-smoothed calibration curve.
## S3 method for class 'amr_calibration' plot(x, max_panels = 12, ...)## S3 method for class 'amr_calibration' plot(x, max_panels = 12, ...)
x |
An |
max_panels |
Maximum number of panels to draw. |
... |
Unused. |
x, invisibly.
sim <- amr_simulate(n_lineages = 20, isolates_per_lineage = 10, external_lineages = 20, return_distances = FALSE, seed = 1) x <- amr_import_predictions(sim$predictions) plot(amr_calibration(x[x$dataset_id == "external" & x$model_id == "marker/lineage", ]))sim <- amr_simulate(n_lineages = 20, isolates_per_lineage = 10, external_lineages = 20, return_distances = FALSE, seed = 1) x <- amr_import_predictions(sim$predictions) plot(amr_calibration(x[x$dataset_id == "external" & x$model_id == "marker/lineage", ]))
Draws one metric across the distance bands, with intervals: one line per stratum, coloured by model, with line type distinguishing datasets.
## S3 method for class 'amr_distance_eval' plot(x, metric = NULL, ...)## S3 method for class 'amr_distance_eval' plot(x, metric = NULL, ...)
x |
An |
metric |
Metric to draw. Defaults to ROC-AUC for scores without a threshold and accuracy otherwise. |
... |
Unused. |
x, invisibly.
sim <- amr_simulate(n_lineages = 20, isolates_per_lineage = 10, external_lineages = 20, seed = 1) x <- amr_import_predictions(sim$predictions) d <- amr_distance_eval(x[x$model_id == "knn/random", ], sim$distances, train_manifest = sim$train_manifest) plot(d)sim <- amr_simulate(n_lineages = 20, isolates_per_lineage = 10, external_lineages = 20, seed = 1) x <- amr_import_predictions(sim$predictions) d <- amr_distance_eval(x[x$model_id == "knn/random", ], sim$distances, train_manifest = sim$train_manifest) plot(d)
Dot-and-interval plots of selected metrics, one panel per metric and one
row per stratum. Rows are labelled with the antimicrobial (and model,
dataset and by values where there are several); colour repeats the
model.
## S3 method for class 'amr_eval' plot(x, metrics = NULL, ...)## S3 method for class 'amr_eval' plot(x, metrics = NULL, ...)
x |
An |
metrics |
Metrics to draw. By default sensitivity and specificity when available, otherwise ROC-AUC (threshold-free scores) or essential agreement (MIC predictions). |
... |
Unused. |
x, invisibly.
x <- amr_import_predictions(amr_example_data()) plot(amr_evaluate(x), metrics = c("very_major_error", "major_error"))x <- amr_import_predictions(amr_example_data()) plot(amr_evaluate(x), metrics = c("very_major_error", "major_error"))
Differences between the two models (second minus first) with bootstrap intervals and a reference line at zero.
## S3 method for class 'amr_paired_compare' plot(x, metrics = NULL, ...)## S3 method for class 'amr_paired_compare' plot(x, metrics = NULL, ...)
x |
An |
metrics |
Metrics to draw; by default every metric in the results. |
... |
Unused. |
x, invisibly.
x <- amr_import_predictions(amr_example_data()) mero <- x[x$drug == "MEM", ] p <- amr_paired_compare(mero, models = c("carbapenemase genes", "carbapenemase or porin + ESBL/AmpC"), B = 100, seed = 1) plot(p)x <- amr_import_predictions(amr_example_data()) mero <- x[x$drug == "MEM", ] p <- amr_paired_compare(mero, models = c("carbapenemase genes", "carbapenemase or porin + ESBL/AmpC"), B = 100, seed = 1) plot(p)