--- title: "Calibrating supplied predictions" output: rmarkdown::html_vignette: toc: true vignette: > %\VignetteIndexEntry{Calibrating supplied predictions} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r setup, include=FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>", error = FALSE, fig.width = 6, fig.height = 4) ``` `vennabers` calibrates predictions from an existing model. The manual functions use only base R and packages included with R: you train the model, supply its predictions and keep it for future use. For package-managed training and preprocessing, continue with `vignette("workflows", package = "vennabers")`. `vignette("method", package = "vennabers")` explains the methods and their guarantees. | Task | Inductive | Cross | |:-----|:----------|:------| | Binary or multiclass classification | `venn_abers()` | `cross_venn_abers()` | | Regression | `venn_abers_regression()` | `cross_venn_abers_regression()` | Inductive calibration uses one fixed model and a separate calibration set. Cross calibration fits a calibrator for each held-out fold and combines their results. Each fold's model must exclude that fold from **all** learned steps, including preprocessing and tuning. That same retained model must supply its new-observation predictions. An all-data refit cannot replace these models in the manual Cross interface. Final assessment data stay outside both training and calibration. If assessment results influence the choice of method or parameters, those rows become selection data and another untouched assessment set is needed. The examples below illustrate the interfaces; their small assessment sets are not benchmarks. ## Binary Inductive calibration Generate a reproducible binary example with a training, calibration and assessment set. Here `TRUE` is the event whose probability we calibrate. ```{r binary-data} library(vennabers) set.seed(12) data <- data.frame(x = rnorm(240)) data$truth <- runif(nrow(data)) < plogis(data$x) training <- data[1:120, ] calibration <- data[121:200, ] assessment <- data[201:240, ] model <- glm(truth ~ x, data = training, family = binomial()) calibration_p <- predict(model, calibration, type = "response") new_p <- predict(model, assessment["x"], type = "response") calibrator <- venn_abers(calibration$truth, calibration_p) result <- predict(calibrator, new_p) head(result) ``` The result has `lower`, `upper` and `probability` columns in input row order. The endpoints are Venn-Abers bounds, **not confidence intervals**. The default log-loss regularization produces `upper / (1 - lower + upper)`. Brier regularization uses `upper + (lower^2 - upper^2) / 2` and can be selected at prediction time without refitting: ```{r binary-options} head(predict(calibrator, new_p, loss = "brier")) head(predict(calibrator, new_p, type = "class")) ``` Binary classes are logical: an event probability of at least 0.5 gives `TRUE`. Use `precision = 2` when fitting to round **calibration** probabilities to two decimal places. New probabilities remain unrounded. Changing precision needs a new calibrator; changing loss does not. Score the regularized probabilities against the untouched assessment outcomes: ```{r binary-scores} rbind( original = c(brier = brier_score(assessment$truth, new_p), log_loss = log_loss(assessment$truth, new_p)), calibrated = c(brier = brier_score(assessment$truth, result$probability), log_loss = log_loss(assessment$truth, result$probability)) ) ``` Lower is better for both scores. Calibration need not improve either score on a particular assessment set. A reliability diagram compares mean probability and observed event rate within bins; small bins can be noisy. ```{r binary-plot, fig.cap="Calibration on the separate assessment set (five equal-width bins)."} calibration_plot(assessment$truth, result$probability, bins = 5) ``` Fit diagnoses of the calibrator itself and CORP reliability diagrams with uncertainty bands are covered in `vignette("diagnostics", package = "vennabers")`. ## Binary Cross calibration: retain the fold models Use the first 200 observations as development data, still keeping assessment separate. Each model below trains on the complement of its assigned fold. ```{r binary-cross-fit} development <- data[1:200, ] set.seed(31) fold <- sample(rep(c("A", "B", "C", "D"), length.out = nrow(development))) models <- setNames(vector("list", 4), c("A", "B", "C", "D")) oof_p <- numeric(nrow(development)) for (id in names(models)) { held <- fold == id models[[id]] <- glm(truth ~ x, data = development[!held, ], family = binomial()) oof_p[held] <- predict(models[[id]], development[held, ], type = "response") } cross <- cross_venn_abers(development$truth, oof_p, fold) # Columns identify models; rows are the same new observations in every column. fold_p <- vapply(models, predict, numeric(nrow(assessment)), newdata = assessment["x"], type = "response") head(predict(cross, fold_p)) cross_details <- predict(cross, fold_p, type = "details") head(cross_details$folds) ``` The input is a **named matrix**, one column per retained fold model. Columns match fold IDs by name; rows match by position. The default output contains only `probability`. There is no aggregate classification interval. With log loss, Cross combines geometric means of the fold endpoints, rather than averaging fold point probabilities. With `loss = "brier"`, it averages the Brier-regularized fold points. Both give each fold equal weight, even when fold sizes differ. `type = "details"` exposes the individual fold bounds and points. The Cross aggregate does not inherit the Inductive perfect-calibration guarantee; see `?predict.cross_venn_abers` for the exact merger. ## Multiclass: one-vs-one and one-vs-rest Multiclass truth must be a factor. Probabilities must be a numeric matrix with columns named exactly after its levels, finite entries in `[0, 1]`, and rows summing to one within `1e-8`. Factor levels fix output order. Even a two-level factor selects this matrix interface; binary vector calibration instead uses logical or numeric 0/1 truth. The following small, illustrative arrays stand in for predictions from a fixed model on separate calibration and assessment observations. ```{r multiclass-input} classes <- c("cat", "dog", "bird") truth <- factor(rep(classes, 3), levels = classes) p <- rbind(c(.7, .2, .1), c(.2, .6, .2), c(.1, .2, .7), c(.5, .3, .2), c(.3, .5, .2), c(.2, .3, .5), c(.6, .3, .1), c(.1, .7, .2), c(.2, .2, .6)) colnames(p) <- classes new_p <- rbind(c(.6, .3, .1), c(.1, .4, .5), c(.2, .6, .2)) colnames(new_p) <- classes new_truth <- factor(c("cat", "bird", "dog"), levels = classes) ``` **One-vs-one (OvO)** calibrates each class pair using only that pair's rows, then couples the binary probabilities into one class vector. This does not train separate pairwise models. **One-vs-rest (OvR)** calibrates each class against all other classes using every row, then normalizes the class points. ```{r multiclass-fit} ovo <- venn_abers(truth, p, method = "ovo") ovr <- venn_abers(truth, p, method = "ovr") predict(ovo, new_p) predict(ovr, new_p) predict(ovo, new_p, type = "class") head(predict(ovo, new_p, type = "details")$pairs) head(predict(ovr, new_p, type = "details")$classes) c(brier = brier_score(new_truth, predict(ovo, new_p)), log_loss = log_loss(new_truth, predict(ovo, new_p))) ``` Score the **final class matrix**, not pair diagnostics. Multiclass Brier scores sum squared errors across classes before averaging observations; their scale differs from the binary event-only score. Exact class ties select the first factor level. Diagnostic bounds describe binary subproblems, not multiclass confidence intervals. `venn_abers_one_hot()` turns class decisions into a 0/1 matrix with one column per class; the installed example `one-hot-classes.R` covers binary decisions, ties and missing values. For manual Cross multiclass calibration, supply an out-of-fold class matrix and a shared fold vector. New predictions are a **list of class matrices named by fold ID**, with identical new-row order. This illustrative example assumes model A excluded the first six calibration rows and model B the last three: ```{r multiclass-cross} multi_fold <- rep(c("A", "B"), c(6, 3)) fold_new <- list(A = new_p, B = (new_p + 1 / 3) / 2) for (method in c("ovo", "ovr")) { fitted <- cross_venn_abers(truth, p, multi_fold, method = method) print(predict(fitted, fold_new)) } ``` Every class must occur globally; every OvO pair must have at least one row in each calibration fold. Merge folds within each pair or class first, then couple or normalize. Averaging already finished multiclass predictions is a different procedure. ## Regression: moderated Inductive and Cross calibration Regression accepts finite real-valued truth and predictions, including negative values. Train a numeric model on separate observations: ```{r regression-inductive} set.seed(52) regression <- data.frame(x = rnorm(240)) regression$truth <- 2 * regression$x + rnorm(240, sd = .7) reg_model <- lm(truth ~ x, data = regression[1:120, ]) reg_cal <- regression[121:200, ] reg_test <- regression[201:240, ] ivar <- venn_abers_regression(reg_cal$truth, predict(reg_model, reg_cal), m = 1) reg_result <- predict(ivar, predict(reg_model, reg_test["x"])) head(reg_result) sqrt(mean((reg_test$truth - reg_result$prediction)^2)) ``` The result contains `prediction`, `lower` and `upper` in outcome units. The point uses a weighted merger of the bounds and is generally not their midpoint. For calibration size `k`, moderation must satisfy `1 <= m <= floor((k - 1) / 2)`; the default `m = 1` needs at least three rows. Alternatively, supply only `epsilon`, which sets `m = floor(epsilon * (k + 1) / 2)` and must satisfy the same constraint. `epsilon` is a moderation parameter, **not a prediction-interval error rate**. Cross regression uses the same model-to-fold matching as binary Cross: ```{r regression-cross} reg_dev <- regression[1:200, ] reg_models <- setNames(vector("list", 4), names(models)) oof_numeric <- numeric(nrow(reg_dev)) for (id in names(reg_models)) { held <- fold == id reg_models[[id]] <- lm(truth ~ x, data = reg_dev[!held, ]) oof_numeric[held] <- predict(reg_models[[id]], reg_dev[held, ]) } cvar <- cross_venn_abers_regression(reg_dev$truth, oof_numeric, fold, m = 1) new_numeric <- vapply(reg_models, predict, numeric(nrow(reg_test)), newdata = reg_test["x"]) reg_details <- predict(cvar, new_numeric, type = "details") head(reg_details$prediction) head(reg_details$folds$A) ``` Every fold must support the requested moderation. CVAR averages complete fold points and each bound separately, with equal weights. It does not recompute a point from averaged bounds. Neither IVAR nor CVAR bounds are confidence or prediction intervals for future outcomes; Cross bounds are descriptive fold summaries and do not add a coverage guarantee. ## Keeping the complete predictor Save the calibrator **and its matching model or models**, for example `saveRDS(list(models = models, calibrator = cross), "predictor.rds")`. Load `vennabers` and the model's required packages before predicting in a new session. The manual calibrator does not retain the underlying model. The next vignette, `vignette("workflows", package = "vennabers")`, shows how to retain the complete preprocessing, model and calibration procedure together. For the complete standalone scripts with a contents list, open `vignette("examples-manual", package = "vennabers")`. `?vennabers_examples` indexes all four example collections.