Introduce model_compare() for model comparison with support of new predictive measures - #380
florence-bockting wants to merge 78 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## pred_measure #380 +/- ##
================================================
+ Coverage 91.29% 91.65% +0.36%
================================================
Files 35 38 +3
Lines 4168 5044 +876
================================================
+ Hits 3805 4623 +818
- Misses 363 421 +58 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
This is how benchmark results would change (along with a 95% confidence interval in relative change) if d03261f is merged into pred_measure:
|
…nals Re-derives the file split on top of the current branch rather than merging the earlier WIP, which git resolved into eight duplicate definitions. model_compare.R (2082 lines) is split by concern into model_compare.R (the generic, default method, ordering and diagnostics), model_compare-pred_measure.R (the multi-measure path, rank resolution, standard errors) and model_compare-print.R (print.compare.loo and its table helpers). Every moved body is byte-identical to its previous version. loo_compare.R keeps only the forwarding generic and its methods. The copies of elpd_diffs, se_elpd_diff, find_model_names, middle_idx, order_stat_heuristic, diag_elpd, diag_diff and print.compare.loo it also held were dead: alphabetical collation meant model_compare.R won. diag_diff and print.compare.loo had diverged, so the dead copies were also wrong. loo_compare_checks, loo_compare_matrix, loo_compare_order and loo_order_stat_check are removed for the same reason. Warning helpers renamed to the package convention: .warn_insample_compare, .warn_kfold_K_mismatch and .warn_omitted_compare_measures become throw_*_warning(); .inform_compare_sign_conversion loses its dot prefix. Adds loo_compare.psis_loo_ss_list so subsampling objects keep dispatching under the old name, and fixes the undefined `loo3` in the loo_compare examples. NAMESPACE and man/ still need regenerating with roxygen2 8.0.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The refactor copied an older print.compare.loo into model_compare.R, which alphabetical collation made the live one, so the simplify argument added later at a user's request stopped existing. Because print.compare.loo takes `...`, `print(comp, simplify = FALSE)` was silently swallowed rather than erroring, and the snapshot tests that would have caught it skip unless NOT_CRAN is set. Merges the two: keeps the pred_measure dispatch, the compare_ref_model message and .print_compare_diag_message() from the model_compare.R version, and restores the flexible column selection and simplify argument from the loo_compare.R one. test_compare.R with NOT_CRAN=true: 355 pass, 0 fail (was 352 pass, 3 fail on 8897e1f and on the split commit). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…_compare # Conflicts: # NEWS.md # R/loo.R # R/loo_compare.psis_loo_ss_list.R # R/pred_measure-builtin.R # R/pred_measure-compute.R # R/pred_measure.R # man/pred_measure.Rd # tests/testthat/_snaps/loo_subsampling_cases.md # tests/testthat/test_pred_measure.R # vignettes/articles-online-only/overview-measures.Rmd # vignettes/articles-online-only/pred-measure-workflow.Rmd # vignettes/loo2-large-data.Rmd
loo_compare() to support new pred_measure APImodel_compare() for model comparison with support of new predictive measures
…nt folds; update vignette example
…es other than elpd, mlpd, and ic
Overview of changesFeaturesComparison
Standard error of differences
Checks
Print method (
User-facing functions
Internal functions
|
…_compare # Conflicts: # R/pred_measure-compute.R # R/pred_measure-helpers.R # R/pred_measure.R # man/insample_pred_measure.Rd # man/pred_measure_params.Rd
jgabry
left a comment
There was a problem hiding this comment.
Thanks @florence-bockting, this is great! Here's a first round of review comments, but I probably missed some things. I should probably do another round, but we can start with these first and then I'll review again
There was a problem hiding this comment.
With the addition of a file this big the package tarball goes from about 4.1 MB to 7.3 MB, which is over CRAN's 5 MB limit. We might be able to get an exception, but I'm not sure. I guess the issue is that it stores full ypred and mupred draws for three models. Could we use fewer draws, e.g. 100 maybe? Or could we use a script to generate this instead of saving it? Would that be too slow?
There was a problem hiding this comment.
I reduced the size in two ways:
test_data_roaches_compare.Rdsnow comes from a brms fit withthin = 4, so it holds 100 draws instead of 400.- All
test_data_*.Rdsfiles are now saved withcompress = "xz".
The tarball is now about 4.4 MB, built without the vignettes.
Commit: ee7b
| if (!length(cols)) { | ||
| stop("No measure is shared by all models.", call. = FALSE) | ||
| } | ||
| internal <- cols[1L] |
There was a problem hiding this comment.
The doc says if elpd is present then rows are ordered by elpd, but the code doesn't prefer elpd when present. I think it just takes the first shared measure of whichever model you pass first, regardless of whether elpd is present, right?
Also, if the models list measures in different orders, then the ranking measure depends on the order you pass the models in, so e.g. if you pass in the models as list(m1, m2) that can lead to a different measure used as the ranking measure compared to list(m2, m1). Is that intentional?
Should we prefer elpd explicitly and if there's no elpd then use an order that doesn't depend on which model comes first?
There was a problem hiding this comment.
Thank you.
- The ranking measure is now
elpdif all models share it. Otherwise it is the first shared measure in alphabetical order. The other measures follow in alphabetical order. - The order no longer depends on the order of the models.
- The print method and the
compare_measuresattribute use the same order.
Tests:
- updated existing tests: without elpd now expects alphabetical order of measures; with elpd expects elpd to be first.
- added new test that checks
list(m1, m2)andlist(m2, m1)give the same measure order.
I updated the docs for the new behavior: NEWS.md, model_compare documentation, the glossary, and the model-comparison vignette.
Commit: fd42
| intersect, | ||
| lapply(loos, function(x) colnames(x$pointwise)) | ||
| ) | ||
| cols[!grepl("^p_", cols)] |
There was a problem hiding this comment.
I think this means you can't have a custom measure with a name that starts with p_ because this will drop it. I think just dropping columns by exact name would fix it.
There was a problem hiding this comment.
Good point.
I changed it to: cols <- cols[!cols %in% c("p_loo", "p_waic", "p_kfold")] and added a test that ensures a custom measure starting with p_ is not dropped.
Commit: f7a7
| #' print(comp, measures = "all") # all measure diff tables | ||
| #' | ||
| #' # the same works for k-fold CV | ||
| #' kf1 <- brms::kfold(fit1, folds = folds, save_fits = TRUE) |
There was a problem hiding this comment.
This errors because folds isn't defined
There was a problem hiding this comment.
indeed. I added a definition of folds before calling brms::kfold
Commit: a509
| #' measures = c("rmse", "r2") | ||
| #' ) | ||
| #' comp <- model_compare(pm1, pm2) | ||
| #' print(comp) # ranked by elpd (default) |
There was a problem hiding this comment.
the comment says ranked by elpd (default) but elpd isn't one of the measures, there's only rmse and r2 here
There was a problem hiding this comment.
thanks, I removed the comment. (The ordering information is now already at several places in the documentation so no need for a code comment)
Commit: 102a
| toc: true | ||
| toc_depth: 3 | ||
| params: | ||
| EVAL: TRUE #!r identical(Sys.getenv("NOT_CRAN"), "true") |
There was a problem hiding this comment.
Do we definitely want this as online only? If so, we can remove the commented out part here. Or should we consider it to be included in the CRAN package too? I don't have a very strong opinion, just curious what your thoughts were about this.
| print(model_compare(loos), simplify = FALSE) | ||
| ``` | ||
|
|
||
| ## Preview: Glimpse into the model comparison results |
There was a problem hiding this comment.
I think we should maybe just title this section "Basic usage", even though it's not a super exciting title
| attr(fun, "measure_name") <- name | ||
| attr(fun, "measure_loss") <- loss | ||
| attr(fun, "measure_se_diff") <- se_diff_fun | ||
| .measure_entry_custom(fun) |
There was a problem hiding this comment.
This calls .measure_entry_custom() only for the validation checks and throws away the result, but that means lines 80-84 of pred_measure-helpers.R can produce a confusing error message for bad name arguments. custom_measure(f, name = "") or name = NULL, or name = c("a", "b"), etc., get this error message:
Error: A custom function passed to 'measure' must have attribute 'measure_name', e.g. attr(my_fun, "measure_name") <- "my_metric".
But the user never passed anything to measure and also the error message suggests they should assign the attribute themselves, but custom_message exists to avoid them having to do that.
There was a problem hiding this comment.
Just noticed that the other errors thrown by .measure_entry_custom() (e.g. for loss and se_diff) also suggest assigning the attribute directly. I guess that's still supported behavior, but not what we prefer if custom_measure exists.
I think there are a couple ways to potentially handle all of these:
- Use
custom_measurein the wording of the messages, e.g "Custom measure 'huber' must declare loss as TRUE or FALSE; seecustom_measure()"
or
- Let the calling function pick the wording. There's already an
originargument for.check_se_diff_value()for this, so.measure_entry_customcould do something similar andcustom_measurecould pass it the argument names.
I would prefer option 1 probably, and updating the examples to use custom_measure instead of assigning attributes even if technically that's still allowed.
| #' | ||
| #' Custom measures are assumed to be on a utility scale (higher is better) in | ||
| #' [model_compare()]. Declare a custom loss with | ||
| #' `attr(my_fun, "measure_loss") <- TRUE` so that [model_compare()] converts and |
There was a problem hiding this comment.
Should we avoid suggesting to assign attributes and just encourage using the custom_measure function? Same for se diff below
| #' attr(my_abs_err, "measure_name") <- "my_abs_err" | ||
| #' # the estimate is the mean of the pointwise values, so declare "mean" | ||
| #' attr(my_abs_err, "measure_se_diff") <- "mean" |
There was a problem hiding this comment.
same point as above about whether we should be showing custom_measure instead?
…remove unnecessary line break
Fixes #220
Summary
This PR adds
model_compare(). The function compares models on all predictive measures of the*_pred_measure()API (#363).model_compare()replacesloo_compare().loo_compare()is deprecated. It warns once per session. It still compares"loo","waic", and"kfold"objects on ELPD.loo_comparemethods in other packages (e.g.loo_compare.brmsfit) still dispatch.model_compare()accepts results fromloo_pred_measure(),kfold_pred_measure(),test_pred_measure(), andinsample_pred_measure().mse) to the utility scale. A higher difference is then always better.print()marks each flipped measure.custom_measure()defines a custom measure. It sets the name, the orientation, and the standard error of the difference.The output for
"loo","waic", and"kfold"objects does not change.The comment below lists all new features and functions.
The review decisions are in notes/design-discussions/model_compare.md.
Examples
vignettes/articles-online-only/model-comparison.Rmd