Skip to contents

precrec needs no adapter. tune::collect_predictions() returns a fold column, a truth column and one .pred_<class> column per class, which is the shape mmdata() already takes through nfold_df.

Set the positive class

This is the one thing to get right. yardstick treats the first factor level as the event; precrec treats the last as the positive class. Pair yardstick’s default score column with precrec’s default and the AUC comes out inverted, with no warning.

lv <- c("bad", "good")
truth <- factor(c("bad", "bad", "good", "good", "good"), levels = lv)
scores <- c(0.1, 0.2, 0.8, 0.9, 0.7)

c(
  default = auc(evalmod(mmdata(scores, truth)))$aucs[1],
  first = auc(evalmod(mmdata(scores, truth, posclass = "bad")))$aucs[1]
)
#> default   first 
#>       1       0

Feed .pred_<last level> and leave posclass alone, or feed .pred_<first level> and name it in posclass. Pointed at the same class the two agree exactly: on a 4-fold logistic_reg() fit of two_class_dat, yardstick’s roc_auc() and auc() both return 0.885564, and the pairing above returns 0.114436.

One model over folds

collect_predictions() gives id for the fold and .row for the instance.

preds <- wf |>
  fit_resamples(folds, control = control_resamples(save_pred = TRUE)) |>
  collect_predictions()

The chunks that need tidymodels are not evaluated, so the site builds without it. The rest of the page runs against a frame carrying the columns collect_predictions() returns.

set.seed(1)
y <- sample(rep(c("good", "bad"), each = 40))
preds <- data.frame(
  id = rep(paste0("Fold", 1:4), each = 20), .row = 1:80,
  .pred_good = ifelse(y == "good", runif(80, 0.35, 1), runif(80, 0, 0.65)),
  truth = factor(y, levels = c("bad", "good"))
)
curves <- evalmod(
  nfold_df = preds, score_cols = ".pred_good",
  lab_col = "truth", fold_col = "id",
  modnames = "rf", dsids = 1:4
)

knitr::kable(auc_ci(curves))
modnames curvetypes mean error lower_bound upper_bound n
rf ROC 0.9664583 0.0255685 0.9408898 0.9920268 4
rf PRC 0.9640375 0.0295294 0.9345081 0.9935669 4

Several models

collect_predictions() stacks models by wflow_id or .config, so widen first and name one score column per model. On a workflow_set it averages over the folds unless you ask it not to, and then there is no id column to widen on - so pass summarize = FALSE.

wide <- workflow_map(wset, "fit_resamples",
  resamples = folds,
  control = control_resamples(save_pred = TRUE)
) |>
  collect_predictions(summarize = FALSE) |>
  tidyr::pivot_wider(
    id_cols = c(id, .row, truth),
    names_from = wflow_id, values_from = .pred_good
  )

evalmod(
  nfold_df = as.data.frame(wide), score_cols = c("rec_rf", "rec_glm"),
  lab_col = "truth", fold_col = "id",
  modnames = c("rec_rf", "rec_glm"), dsids = 1:4
)

More than two classes

Hand the .pred_ columns over as a matrix. Classes are matched by column name, so drop the prefix. Select them by level rather than by pattern - .pred_class also starts with .pred_, and it is a factor, so a loose match turns the whole matrix into characters.

sc <- as.matrix(preds[paste0(".pred_", levels(preds$truth))])
colnames(sc) <- levels(preds$truth)

evalmod(mmdata(sc, preds$truth))

precrec adds a macro-average row of its own, which is the number yardstick’s roc_auc(estimator = "macro") reports.

One-vs-rest cannot be combined with nfold_df, so pool the folds for a multiclass run, or loop over them yourself.

The report

.pred_class is the hard prediction parsnip derives at a 0.5 threshold, and classification_report() takes that threshold directly.

classification_report(mmdata(preds$.pred_good, preds$truth), at = 0.5)
#> 
#>               precision    recall  f1-score   support
#> 
#>      negative      0.86      0.93      0.89        40
#>      positive      0.92      0.85      0.88        40
#> 
#>      accuracy                          0.89        80
#>     macro avg      0.89      0.89      0.89        80
#>  weighted avg      0.89      0.89      0.89        80