Skip to content
Merged
10 changes: 10 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# nestedtune 0.0.0.9000

* Three arguments from tidymodels are new. `nested_workflow_map()` takes
`verbose`, which writes a message as each workflow starts and finishes.
It sits before `...`, where `workflowsets::workflow_map()` has it. So an
unnamed third argument is now read as `verbose`. A value that is not
`TRUE` or `FALSE` there gives an error of class `nestedtune_bad_verbose`,
where it was `nestedtune_bad_dots`.
`collect_inner_metrics()` takes `type = "wide"`, which gives one column
per metric, as `tune::collect_metrics()` does. `autoplot()` on a
workflow-set run takes `id`, which draws the plot of one workflow.

* `nested_tune_bayes()` and `nested_tune_sim_anneal()` now list `eval_time`
before `objective` and `initial`, in the order `tune::tune_bayes()` and
`finetune::tune_sim_anneal()` use. These arguments come after `...`, so
Expand Down
35 changes: 33 additions & 2 deletions R/checks.R
Original file line number Diff line number Diff line change
Expand Up @@ -2951,6 +2951,37 @@ check_map_fn <- function(fn, call = rlang::caller_env()) {
)
}

# The map's `verbose` (M149): one `TRUE` or `FALSE`, as workflowsets'
# `check_bool()` reads it, refused before any workflow runs. It sits in
# `workflow_map()`'s place, before `...`, so a third argument passed by
# position lands here. When the caller did not name `verbose`, the hint
# says the rest are matched by name, which the dots check said before
# `verbose` took the place.
check_map_verbose <- function(
verbose,
named = TRUE,
call = rlang::caller_env()
) {
if (rlang::is_bool(verbose)) {
return(invisible(verbose))
}
hint <- if (!named) {
c(
i = "Every argument of {.arg fn} must be named, such as \\
{.code resamples = }."
)
}
cli::cli_abort(
c(
"{.arg verbose} must be {.code TRUE} or {.code FALSE}.",
x = "Got {.obj_type_friendly {verbose}}.",
hint
),
class = "nestedtune_bad_verbose",
call = call
)
}

# What an orchestrator accepts beyond the workflow: its formals other than
# `object` (so `resamples` and everything behind the dots), and the `control`
# every one of the six takes through `...` (D-042). Read off the function
Expand All @@ -2967,7 +2998,7 @@ orchestrator_args <- function(fn) {
# The map's `...` (M71): every name must be one the orchestrator `fn` names
# accepts, so a typo is refused rather than narrowed away for every
# workflow; `object` is the set's to bind, never the caller's; everything
# after `fn` is matched by name; and the design is the one argument every
# after `fn` and `verbose` is matched by name; and the design is the one argument every
# route needs, so its absence is refused here rather than as a missing
# argument two frames down. `dots` is the list `capture_dots()` returns.
check_map_dots <- function(dots, fn, call = rlang::caller_env()) {
Expand All @@ -2978,7 +3009,7 @@ check_map_dots <- function(dots, fn, call = rlang::caller_env()) {
c(
"Every argument in {.arg ...} must be named.",
x = "Got {n_unnamed} unnamed argument{?s}; everything after \\
{.arg fn} is matched by name."
{.arg fn} and {.arg verbose} is matched by name."
),
class = "nestedtune_bad_dots",
call = call
Expand Down
23 changes: 21 additions & 2 deletions R/nested-results-collect.R
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@
#' does not identify a candidate across folds.
#'
#' @inheritParams collect_metrics.nested_results
#' @param type For `collect_inner_metrics()`, `"long"` (the default), one row
#' per fold, candidate and metric, or `"wide"`, one column per metric that
#' holds its `mean`. The wide shape drops `.estimator`, `n` and `std_err`,
#' and a metric that a fold did not score is `NA` in its rows. It
#' gives an error for a metric named like another column, such as a tuning
#' parameter, and for a metric scored with two estimators.
#'
#' @return A tibble with the fold labels first (`id`, and `id2` on a
#' repeated design), then the stacked columns. A fold that lacks a column
Expand Down Expand Up @@ -97,16 +103,29 @@ collect_inner_metrics.default <- function(x, ...) {

#' @rdname collect_selections
#' @export
collect_inner_metrics.nested_results <- function(x, ...) {
collect_inner_metrics.nested_results <- function(
x,
...,
type = c("long", "wide")
) {
rlang::check_dots_empty()
type <- check_metrics_type(type)
check_any_completed(x, action = "collect")
warn_partial_summary(x, noun = "table")
stack_fold_column(
out <- stack_fold_column(
x,
".inner_metrics",
completed_only = TRUE,
call = rlang::current_env()
)
# The wide shape is collect_metrics()'s (M149): the inner table holds
# tune's summarized columns, so the pivot keeps the fold labels, the
# parameters, `.config` and any `.iter` or `.eval_time` as keys, which on
# one fold's rows is the key set tune's own pivot uses (GP1).
if (identical(type, "wide")) {
out <- pivot_metrics_wide(out, call = rlang::current_env())
}
out
}

#' @rdname collect_selections
Expand Down
17 changes: 16 additions & 1 deletion R/nested-results-plot.R
Original file line number Diff line number Diff line change
Expand Up @@ -576,9 +576,24 @@ autoplot.nested_results_set <- function(
type = c("parameters", "performance"),
metric = NULL,
eval_time = NULL,
...
...,
id = NULL
) {
rlang::check_dots_empty()
# One workflow's plot (M149), as workflowsets' `id` draws one result: the
# single-run method on that row, with every argument it takes. Its
# conditions name the workflow and the user's call, as the set's readers'
# do (`for_workflow()`).
if (!is.null(id)) {
call <- rlang::current_env()
i <- match_set_id(object, id, call = call)
result <- object$result[[i]]
return(for_workflow(
object$wflow_id[[i]],
call,
autoplot(result, type = type, metric = metric, eval_time = eval_time)
))
}
type <- check_plot_type(type)
check_plot_filter(type, metric, eval_time)
call <- rlang::current_env()
Expand Down
27 changes: 22 additions & 5 deletions R/nested-results-set.R
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,12 @@
#' @param ... Not used, and must be empty.
#' @param summarize As on [collect_metrics.nested_results()], and for
#' `collect_predictions()` as on [collect_predictions.nested_results()].
#' @param type For `collect_metrics()`, `"long"` (the default) or `"wide"`.
#' The wide shape is keyed on `wflow_id` too, and a metric that a workflow
#' did not score is `NA` in its rows.
#' @param type For `collect_metrics()` and `collect_inner_metrics()`,
#' `"long"` (the default) or `"wide"`. The wide shape is keyed on
#' `wflow_id` too. A column that a workflow lacks is `NA` in its rows,
#' such as a metric it did not score or a parameter it does not tune.
#' A workflow with no inner candidates, such as a fixed one, has no rows
#' in `collect_inner_metrics()`.
#'
#' @return A tibble with `wflow_id` first, then the columns that the method
#' for one workflow returns, bound in the set's order. A column that one
Expand Down Expand Up @@ -83,9 +86,19 @@ collect_selections.nested_results_set <- function(x, ...) {

#' @rdname collect_metrics.nested_results_set
#' @export
collect_inner_metrics.nested_results_set <- function(x, ...) {
collect_inner_metrics.nested_results_set <- function(
x,
...,
type = c("long", "wide")
) {
rlang::check_dots_empty()
stack_set(x, collect_inner_metrics, call = rlang::current_env())
type <- check_metrics_type(type)
# Stacked long and pivoted once, as collect_metrics() does on a set.
out <- stack_set(x, collect_inner_metrics, call = rlang::current_env())
if (identical(type, "wide")) {
out <- pivot_metrics_wide(out, call = rlang::current_env())
}
out
}

#' @rdname collect_metrics.nested_results_set
Expand Down Expand Up @@ -269,6 +282,10 @@ stack_set <- function(
#' `"performance"`.
#' @param metric,eval_time For `type = "performance"`, the metrics and the
#' evaluation times to draw, as on [autoplot.nested_results()].
#' @param id For `autoplot()`, `NULL` (the default) to draw every workflow,
#' or one `wflow_id` to draw that workflow alone. One `wflow_id` gives the
#' plot that [autoplot.nested_results()] draws for that workflow's result.
#' That plot states a failed fold in its subtitle and gives no warning.
#' @inheritParams collect_metrics.nested_results_set
#'
#' @return `summary()` returns a `summary.nested_results_set`, a list of
Expand Down
48 changes: 47 additions & 1 deletion R/nested-workflow-map.R
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@
#' `"nested_tune_race_anova"`, `"nested_tune_race_win_loss"` and
#' `"nested_tune_sim_anneal"`. With `"nested_fit_resamples"`, no workflow
#' can have a parameter to tune.
#' @param verbose Whether `nested_workflow_map()` writes a message as each
#' workflow starts and as it finishes. The default is `FALSE`. The finish
#' message gives the time the workflow took. A workflow that gives an
#' error has no finish message.
#' @param ... The arguments of `fn`, each one named. `resamples`, the nested
#' design, is required, and a `control` goes here as well. A name that `fn`
#' does not take is an error. A `metrics` set reaches every workflow, and
Expand Down Expand Up @@ -83,10 +87,16 @@
#' [collect_metrics.nested_results_set()], [nested_final_fit()],
#' [workflowsets::workflow_map()]
#' @export
nested_workflow_map <- function(object, fn = "nested_tune_grid", ...) {
nested_workflow_map <- function(
object,
fn = "nested_tune_grid",
verbose = FALSE,
...
) {
dots <- capture_dots(...)
check_workflow_set(object)
check_map_fn(fn)
check_map_verbose(verbose, named = "verbose" %in% rlang::names2(sys.call()))
check_map_dots(dots, fn)
call <- rlang::current_env()

Expand Down Expand Up @@ -141,11 +151,13 @@ nested_workflow_map <- function(object, fn = "nested_tune_grid", ...) {
for (i in seq_along(ids)) {
assign(".Random.seed", entry_seed, envir = globalenv())
args <- map_args(dots, object$option[[i]], routes[[i]], fn)
started <- log_map_start(verbose, i, ids, routes[[i]])
results[[i]] <- for_workflow(
ids[[i]],
call,
run_orchestrator(routes[[i]], workflows[[i]], args)
)
log_map_end(verbose, i, ids, routes[[i]], started)
}
new_nested_results_set(ids, workflows, results, fn)
}
Expand All @@ -171,6 +183,40 @@ map_args <- function(dots, option, route, fn) {
args[intersect(names(args), orchestrator_args(route))]
}

# The `verbose` log (M149), in workflowsets' `log_progress()` form: a line as
# each workflow starts and one, with the time it took, as it finishes. A
# workflow that errors gets no end line, because the error stops the map
# and `for_workflow()` already names the workflow in it. The clock is read
# whether or not the log is on, and reading it draws no random number.
log_map_start <- function(verbose, i, ids, route) {
if (verbose) {
msg <- map_log_text(i, ids, route)
cli::cli_inform(c(i = "{msg}"))
}
proc.time()[["elapsed"]]
}

log_map_end <- function(verbose, i, ids, route, started) {
if (!verbose) {
return(invisible(NULL))
}
took <- proc.time()[["elapsed"]] - started
time <- paste0(format(round(took, 1L), nsmall = 1L), "s")
msg <- paste0(map_log_text(i, ids, route), " (", time, ")")
cli::cli_inform(c(v = "{msg}"))
}

map_log_text <- function(i, ids, route) {
event <- if (identical(route, "nested_fit_resamples")) {
"resampling"
} else {
"tuning"
}
# Pasted, not interpolated, and handed to cli as a value, so a brace in
# an id is not read as cli markup (the M83 lesson).
paste0(i, " of ", length(ids), " ", event, ": ", ids[[i]])
}

# One orchestrator call, built as a call in an environment that binds the
# workflow and every argument to a name (the M05 lesson: a value inlined
# into a call is deparsed into every condition raised beneath it, and a
Expand Down
2 changes: 1 addition & 1 deletion cairn/ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ _Last hygiene check: 2026-10-18 M148 done. Archived M148, pruned M145's row, add
## Milestones
| ID | Title | Status | Depends on | Priority | File/Archive |
|---|---|---|---|---|---|
| M149 | Three arguments from tidymodels: verbose on the map, a wide inner table, one workflow's plot | planned | — | normal | milestones/M149-argument-gaps.md |
| M149 | Three arguments from tidymodels: verbose on the map, a wide inner table, one workflow's plot | review | — | normal | milestones/M149-argument-gaps.md |
| M148 | Each export and result column is set beside its tidymodels counterpart | done | — | normal | milestones/archive/M148-export-review.md |
| M147 | CI runs the tests CRAN runs, with the Suggests and without them | done | — | normal | milestones/archive/M147-cran-subset-ci.md |
| M146 | DESCRIPTION's Title and Description say what the package does, in CRAN's form | done | — | normal | milestones/archive/M146-description-text.md |
Expand Down
Loading
Loading