--- title: "Classification metrics and explicit operating points" author: "Raban Heller" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Classification metrics and explicit operating points} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} resource_files: - figures/logo.svg - figures/logo-dark.svg --- ```{r setup, include=FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") library(inferguidR) ``` This tutorial evaluates one operating point from synthetic counts and then from scored observations. It runs offline and does not train a model. Define the positive class, score direction, comparator and evaluation sample before interpreting performance. ## Start with the four counts ```{r counts} spec <- classification_spec( counts = c(tp = 40, fn = 10, fp = 20, tn = 130), cutoff = 0.5 ) result <- evaluate_cutoff(spec) metrics <- classification_metrics(result) metrics[metrics$metric_id %in% c("tpr", "tnr", "ppv", "npv"), c("metric_id", "value", "status", "denominator_value")] ``` Sensitivity (`tpr`) divides true positives by all events; specificity (`tnr`) divides true negatives by all non-events. Positive predictive value (`ppv`) divides true positives by all positive calls. These denominators differ. A missing denominator produces an explicit undefined result, not a zero. Aggregate counts identify one operating point; they cannot reconstruct a ROC curve or a cutoff sweep. ## State how scored observations are evaluated The following small data frame is entirely synthetic. The scores are declared as probabilities of the positive class; the comparator includes observations whose score is exactly 0.5. These are demonstration observations, not a held-out validation set. ```{r scored-data} d <- data.frame( id = seq_len(8), event = c(1, 1, 1, 0, 0, 0, 0, 0), risk = c(0.9, 0.8, 0.4, 0.7, 0.5, 0.2, 0.2, 0.1) ) scored <- classification_spec( data = d, truth = "event", positive_label = 1, negative_label = 0, scores = "risk", id = "id", cutoff = 0.5, direction = "higher_is_positive", comparator = ">=", score_scale = "probability", evaluation_split = "same_data", sampling = "unknown" ) evaluated <- evaluate_cutoff(scored) tab <- classification_metrics(evaluated) tab[tab$metric_id %in% c("tp", "fn", "fp", "tn", "tpr", "ppv"), c("metric_id", "value", "status")] ``` A score's numeric range alone does not establish probability calibration. The `score_scale` and `sampling` settings document assumptions; they do not validate them. Predictive values depend on prevalence, and the event proportion in an outcome-selected sample need not equal population prevalence. A prevalence scenario is a separate modelling assumption, not an observed population rate. ## Inspect probability diagnostics at explicit thresholds Calibration compares predicted probabilities with observed event frequencies. Net benefit uses a declared decision probability and its relative harm weight `p / (1 - p)`. These displays require positive-class probabilities with `higher_is_positive`; neither is a prevalence-scenario calculation. ```{r probability-diagnostics} probability_result <- evaluate_cutoff( scored, calibration_bins = 3, threshold_probability = c(0.2, 0.5, 0.8) ) classification_plot_data(probability_result, "calibration")$data$points classification_plot_data(probability_result, "net_benefit")$data$points ``` The net-benefit display contains only the requested thresholds and does not interpolate a decision curve. Calibration groups are a display choice, with no sampling confidence intervals. In the workbench, changing these settings makes the previous evaluation stale until it is recalculated. ## Missingness and uncertainty The default missing-data policy is `error`. Choosing `missing = "exclude"` changes the evaluated sample and must be reported. Repeated observations from the same unit need an explicit cluster definition; treating them as independent can give misleading uncertainty estimates. Consult `?classification_uncertainty` for supported interval and resampling settings. ```{r result-details} evaluated$status evaluated$limitations evaluated$uncertainty ``` A cutoff selected and evaluated on the same data can overstate performance. Prespecify a cutoff or separate tuning and evaluation samples. Small synthetic examples cannot establish transportability, clinical utility or a recommended operating threshold. ## Keep the analysis record ```{r export} path <- tempfile(fileext = ".rds") saveRDS(spec, path) restored <- readRDS(path) stopifnot(identical(ig_hash(spec), ig_hash(restored))) ``` Use RDS for the classification specification: `ig_from_json()` currently imports power and report objects, not classification specifications. Retain the input definition, data provenance, result and software revision alongside the exported record. The [getting-started tutorial](getting-started.html) explains the package's typed outcomes and reproducibility workflow. Portable reports use a separate privacy boundary. They omit individual rows, full score-derived ROC/PR curves and density tables. To include only the frozen calibration-bin means/counts and the requested net-benefit threshold aggregates, opt in explicitly: ```{r aggregate-report} envelope <- analysis_envelope( classification_evaluated_spec(probability_result), probability_result ) report <- create_report_snapshot(envelope, include_probability_summaries = TRUE) stopifnot( !report$privacy$raw_rows_included, identical(ig_hash(report), ig_hash(ig_from_json(ig_to_json(report)))) ) ``` Small groups can still disclose sensitive information. Review aggregate content and user-supplied titles or model metadata before sharing. The report records the inclusion decision in schema 0.4; reports without these summaries retain their existing format unless another feature, such as a Boschloo lattice, requires the newer version. ```{r session-information} sessionInfo() ```