scoringutils icon indicating copy to clipboard operation
scoringutils copied to clipboard

Decide on whether to use purrr::partial vs. customise_metric()

Open nikosbosse opened this issue 1 year ago • 5 comments

As discussed in #791:

Should we replace customise_metric by purrr::partial?

pro:

  • fewer NAMESPACE exports
  • less code to maintain
  • users already familiar with partial don't have to deal with a new function

con:

  • we lose the current documentation and have to find other, arguable less convenient ways to document the workflow
  • we rely on an external function and have to adapt to it when it changes
  • we lose the chance to add further checks in the future

Rogue alternative: using customise_metric() as an alias that directly calls partial?

image

full function code:

#' Customises a metric function with additional arguments.
#'
#' @description
#' This function takes a metric function and additional arguments, and returns
#' a new function that includes the additional arguments when calling the
#' original metric function.
#'
#' This is the expected way to pass additional
#' arguments to a metric when evaluating a forecast using [score()]:
#' To evaluate a forecast using a metric with an additional argument, you need
#' to create a custom version of the scoring function with the argument
#' included. You then need to create an updated version of the list of scoring
#' functions that includes your customised metric and pass this to [score()].
#'
#' @param metric The metric function to be customised.
#' @param ... Additional arguments to be included when calling the metric
#'   function.
#'
#' @return A customised metric function.
#' @keywords metric
#'
#' @export
#' @examples
#' # Create a customised metric function
#' custom_metric <- customise_metric(mean, na.rm = TRUE)
#'
#' # Use the customised metric function
#' values <- c(1, 2, NA, 4, 5)
#' custom_metric(values)
#'
#' # Updating metrics list to calculate 70% coverage in `score()`
#' interval_coverage_70 <- customise_metric(
#'   interval_coverage, interval_range = 70
#' )
#' updated_metrics <- c(
#'   metrics_quantile(),
#'   "interval_coverage_70" = interval_coverage_70
#' )
#' score(
#'   as_forecast(example_quantile),
#'   metrics = updated_metrics
#' )
customise_metric <- function(metric, ...) {
  assert_function(metric)
  dots <- list(...)
  customised_metric <- function(...) {
    do.call(metric, c(list(...), dots))
  }
  return(customised_metric)
}

nikosbosse avatar May 19 '24 08:05 nikosbosse

Pinging @seabbs @Bisaloo @sbfnk @elray1 @nickreich @jamesmbaazam @jhellewell14 for people who might have opinions

nikosbosse avatar May 19 '24 08:05 nikosbosse

I don't feel that strongly either way on this but I am not sure its working picking up a dependency just for this?

We could avoid that by just documenting that one could use purrr::partial (and maybe it would need to be a suggests) so that seems like a better way to go vs wrapping it.

One key question here is are we likely to add further checks in the future or other custom functionality?

seabbs avatar May 19 '24 18:05 seabbs

One key question here is are we likely to add further checks in the future or other custom functionality? Unsure I guess. You had a vision for actually doing proper checking of functions which might play into this.

So current plan is to document and make clear to users that this is basically the same as purrr::partial?

nikosbosse avatar May 23 '24 19:05 nikosbosse

No that is one option. The other is to leave as is.

It depends on if we like having it as part of the main stream docs and if we imagine future functionality. Do we?

I think of these I am leaning towards keeping as is

seabbs avatar May 24 '24 10:05 seabbs

My idea (but I may be missing some subtleties) would be to remove customise_metric() and update the docs to always recommend purrr::partial() in its place.

Bisaloo avatar May 27 '24 10:05 Bisaloo

My idea (but I may be missing some subtleties) would be to remove customise_metric() and update the docs to always recommend purrr::partial() in its place.

I thought we had agreed on what @Bisaloo is suggesting here? I think this is the way to go.

seabbs avatar Jul 18 '24 16:07 seabbs

Ah - in light of this statement of yours

I think of these I am leaning towards keeping as is

That was not clear to me. If you both agree then I'm happy to make the change to purrr::partial

nikosbosse avatar Jul 18 '24 17:07 nikosbosse

Working on swapping in purrr::partial() for customise_metric(). I do see one (and half) meaningful changes:

The first one is that customise_metric() is storing the object and it gets as part of the function definition, whereas purrr::partial() does not:

  # works with customise_metric
  argument <- c("hi", "hello", "I'm here")
  custom_metric <- customise_metric(print, x = argument)
  expect_output(custom_metric(), "I'm here")

  argument <- NULL
  expect_output(custom_metric(), "I'm here")
  # fails with purrr::partial
  argument <- c("hi", "hello", "I'm here")
  custom_metric <- purrr::partial(print, x = argument)
  expect_output(custom_metric(), "I'm here")

  argument <- NULL
  expect_output(custom_metric(), "I'm here") 
> Error: `custom_metric\(\)` does not match "I'm here".
> Actual value: "NULL"

The second change is that customise_metric() fails immediately instead of at runtime

  # passes with customise_metric
  expect_error(
    custom_metric <- customise_metric(print, x = doesnotexist),
    "object 'doesnotexist' not found"
  )

  # fails to produce an error with purrr::partial
  expect_error(
    custom_metric <- purrr::partial(print, x = doesnotexist),
    "object 'doesnotexist' not found"
  )

What do you think about those?

nikosbosse avatar Jul 27 '24 09:07 nikosbosse

argument <- c("hi", "hello", "I'm here")
custom_metric <- purrr::partial(print, x = !!argument)
testthat::expect_output(custom_metric(), "I'm here")

argument <- NULL
testthat::expect_output(custom_metric(), "I'm here") 

Created on 2024-07-27 with reprex v2.1.1

?purrr::partial says:

Please unquote the arguments that should be evaluated once at function creation time with ⁠!!⁠.

Bisaloo avatar Jul 27 '24 11:07 Bisaloo

Reading is an underrated skill... That makes a lot of sense, thank you! I'll add that to the documentation.

nikosbosse avatar Jul 28 '24 08:07 nikosbosse

The second issue is still the case right? I think its a bit less friendly but not the end of the world

seabbs avatar Jul 30 '24 11:07 seabbs

both are solved by unquoting!

nikosbosse avatar Jul 30 '24 12:07 nikosbosse

ah awesome!

seabbs avatar Jul 30 '24 12:07 seabbs