Skip to content

Commit d66e4ed

Browse files
authored
handling no calibration (#88)
* changes for method = "none" * add data helper for calibration * bug fix: multinomial data were assigned type "binary" * expanded testing * add tests from main back in * update test data * re-doc * changes based on reviewer comments
1 parent ac798e9 commit d66e4ed

14 files changed

Lines changed: 580 additions & 122 deletions

R/adjust-numeric-calibration.R

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,14 @@
77
#' range of outputs.
88
#'
99
#' @param x A [tailor()].
10-
#' @param method Character. One of `"linear"`, `"isotonic"`, or
11-
#' `"isotonic_boot"`, corresponding to the function from the \pkg{probably}
10+
#' @param method Character. One of `"linear"`, `"isotonic"`,`"isotonic_boot"`,
11+
#' or `"none"`, corresponding to the function from the \pkg{probably}
1212
#' package `probably::cal_estimate_linear()`,
1313
#' `probably::cal_estimate_isotonic()`, or
1414
#' `probably::cal_estimate_isotonic_boot()`, respectively. The default is to
1515
#' use `"linear"` which, despite its name, fits a generalized additive model.
16+
#' Note that when [fit.tailor()] is called, the value may be changed to `"none"`
17+
#' if there is insufficient data.
1618
#' @param ... Optional arguments to pass to the corresponding function in the
1719
#' \pkg{probably} package. These arguments must be named.
1820
#'
@@ -37,6 +39,9 @@
3739
#' This adjustment requires estimation and, as such, different subsets of data
3840
#' should be used to train it and evaluate its predictions.
3941
#'
42+
#' Note that, when calling [fit.tailor()], if the calibration data have zero or
43+
#' one row, the `method` is changed to `"none"`.
44+
#'
4045
#' @return An updated [tailor()] containing the new operation.
4146
#'
4247
#' @examplesIf rlang::is_installed("probably")
@@ -66,11 +71,11 @@ adjust_numeric_calibration <- function(x, method = NULL, ...) {
6671
validate_probably_available()
6772

6873
check_tailor(x, calibration_type = "numeric")
69-
# wait to `check_method()` until `fit()` time
74+
# We will check the method again during `fit()` using `check_cal_method()`
7075
if (!is.null(method) & !is_tune(method)) {
7176
arg_match0(
7277
method,
73-
c("linear", "isotonic", "isotonic_boot")
78+
c("linear", "isotonic", "isotonic_boot", "none")
7479
)
7580
}
7681

@@ -128,7 +133,11 @@ print.numeric_calibration <- function(x, ...) {
128133
fit.numeric_calibration <- function(object, data, tailor = NULL, ...) {
129134
validate_probably_available()
130135

131-
method <- check_method(object$arguments$method, tailor$type)
136+
method <- check_cal_method(
137+
object$arguments$method,
138+
type = tailor$type,
139+
cal_data = data
140+
)
132141

133142
cl <- rlang::call2(
134143
paste0("cal_estimate_", method),

R/adjust-probability-calibration.R

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
#'
1010
#' @inheritParams adjust_numeric_calibration
1111
#' @param method Character. One of `"logistic"`, `"multinomial"`,
12-
#' `"beta"`, `"isotonic"`, or `"isotonic_boot"`, corresponding to the
12+
#' `"beta"`, `"isotonic"`, `"isotonic_boot"`, or `"none"`, corresponding to the
1313
#' function from the \pkg{probably} package `probably::cal_estimate_logistic()`,
1414
#' `probably::cal_estimate_multinomial()`, etc., respectively. The default is to
1515
#' use `"logistic"` which, despite its name, fits a generalized additive model.
16+
#' Note that when [fit.tailor()] is called, the value may be changed to `"none"`
17+
#' if there is insufficient data.
1618
#'
1719
#' @details
1820
#' The "logistic" and "multinomial" methods fit models that predict the observed
@@ -86,11 +88,11 @@ adjust_probability_calibration <- function(x, method = NULL, ...) {
8688
validate_probably_available()
8789

8890
check_tailor(x, calibration_type = "probability")
89-
# wait to `check_method()` until `fit()` time
91+
# We will check the method again during `fit()` using `check_cal_method()`
9092
if (!is.null(method) & !is_tune(method)) {
9193
arg_match(
9294
method,
93-
c("logistic", "multinomial", "beta", "isotonic", "isotonic_boot")
95+
c("logistic", "multinomial", "beta", "isotonic", "isotonic_boot", "none")
9496
)
9597
}
9698

@@ -148,7 +150,11 @@ print.probability_calibration <- function(x, ...) {
148150
fit.probability_calibration <- function(object, data, tailor = NULL, ...) {
149151
validate_probably_available()
150152

151-
method <- check_method(object$arguments$method, tailor$type)
153+
method <- check_cal_method(
154+
object$arguments$method,
155+
type = tailor$type,
156+
cal_data = data
157+
)
152158

153159
cl <- rlang::call2(
154160
paste0("cal_estimate_", method),

R/tailor.R

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,8 @@ print.tailor <- function(x, ...) {
167167
#' learn from data; in that case, separate subsets of data ought to be used
168168
#' for training the tailor and evaluating its performance on predictions.
169169
#'
170+
#' Note that if `.data` has zero or one row, the `method` is changed to `"none"`.
171+
#'
170172
#' @param object A [tailor()].
171173
#' @param .data,new_data A data frame containing predictions from a model.
172174
#' @param outcome <[`tidy-select`][dplyr::dplyr_tidy_select]>
@@ -222,6 +224,9 @@ fit.tailor <- function(
222224
columns$probabilities <- names(
223225
tidyselect::eval_select(enquo(probabilities), .data)
224226
)
227+
# For type = "binary", update based on number of probability estimates
228+
object$type <- update_type(object$type, columns$probabilities)
229+
225230
if (
226231
"probability" %in%
227232
purrr::map_chr(object$adjustments, purrr::pluck, "inputs")

R/utils.R

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -333,19 +333,20 @@ check_calibration_type <- function(
333333
}
334334
}
335335

336-
types_regression <- c("linear", "isotonic", "isotonic_boot")
337-
types_binary <- c("logistic", "beta", "isotonic", "isotonic_boot")
338-
types_multiclass <- c("multinomial", "beta", "isotonic", "isotonic_boot")
336+
types_regression <- c("linear", "isotonic", "isotonic_boot", "none")
337+
types_binary <- c("logistic", "beta", "isotonic", "isotonic_boot", "none")
338+
types_multiclass <- c("multinomial", "beta", "isotonic", "isotonic_boot", "none")
339339
# a check function to be called when a tailor is being `fit()`ted.
340340
# by the time a tailor is fitted, we have:
341341
# * `method`, the `method` argument passed to an `adjust_*` function
342342
# * this argument has already been checked to agree with the kind of
343343
# `adjust_*()` function via `arg_match0()`.
344344
# * `tailor_type`, the `type` argument either specified in `tailor()`
345345
# or inferred in `fit.tailor()`.
346-
check_method <- function(
346+
check_cal_method <- function(
347347
method,
348348
type,
349+
cal_data,
349350
arg = caller_arg(method),
350351
call = caller_env()
351352
) {
@@ -357,6 +358,15 @@ check_method <- function(
357358
)
358359
}
359360

361+
if (nrow(cal_data) < 2) {
362+
cli::cli_warn(
363+
"The calibration data has {nrow(cal_data)} row{?s}. There is not enough
364+
data for calibration so {.arg method} is changed from {.val {method}}
365+
to {.val none}."
366+
)
367+
method <- "none"
368+
}
369+
360370
# if no `method` was supplied, infer a reasonable one based on the `type`
361371
if (is.null(method)) {
362372
switch(

R/validation-rules.R

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ infer_type <- function(orderings) {
114114
return("regression")
115115
}
116116

117+
# Note: default for classification is "binary"; this can be updated once
118+
# the data are seen. If there are 3+ classes, the type is changed to
119+
# "multiclass"
117120
if (
118121
all(orderings$output_prob | orderings$output_class | orderings$output_all)
119122
) {
@@ -122,3 +125,12 @@ infer_type <- function(orderings) {
122125

123126
"unknown"
124127
}
128+
129+
update_type <- function(type, probabilities) {
130+
if (type == "binary") {
131+
if (length(probabilities) > 2) {
132+
type <- "multiclass"
133+
}
134+
}
135+
type
136+
}

man/adjust_numeric_calibration.Rd

Lines changed: 8 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/adjust_probability_calibration.Rd

Lines changed: 7 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/fit.tailor.Rd

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/tailor-package.Rd

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/_snaps/adjust-numeric-calibration.md

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,25 +48,53 @@
4848
adjust_numeric_calibration(tailor(), "boop")
4949
Condition
5050
Error in `adjust_numeric_calibration()`:
51-
! `method` must be one of "linear", "isotonic", or "isotonic_boot", not "boop".
51+
! `method` must be one of "linear", "isotonic", "isotonic_boot", or "none", not "boop".
5252

5353
---
5454

5555
Code
5656
adjust_numeric_calibration(tailor(), "binary")
5757
Condition
5858
Error in `adjust_numeric_calibration()`:
59-
! `method` must be one of "linear", "isotonic", or "isotonic_boot", not "binary".
59+
! `method` must be one of "linear", "isotonic", "isotonic_boot", or "none", not "binary".
6060
i Did you mean "linear"?
6161

6262
# tuning the calibration method
6363

6464
Code
65-
fit(tlr, d_calibration, outcome = y, estimate = y_pred)
65+
fit(tlr, d_reg_calibration, outcome = y, estimate = y_pred)
6666
Condition
6767
Error in `fit()`:
6868
! The calibration method cannot be a value of `tune()` at `fit()` time.
6969

70+
# too few data
71+
72+
Code
73+
fit(tlr, d_reg_calibration[0, ], outcome = y, estimate = y_pred)
74+
Condition
75+
Warning:
76+
The calibration data has 0 rows. There is not enough data for calibration so `method` is changed from "linear" to "none".
77+
Message
78+
79+
-- tailor ----------------------------------------------------------------------
80+
A regression postprocessor with 1 adjustment:
81+
82+
* Re-calibrate numeric predictions using linear method. [trained]
83+
84+
---
85+
86+
Code
87+
fit(tlr, d_reg_calibration[1, ], outcome = y, estimate = y_pred)
88+
Condition
89+
Warning:
90+
The calibration data has 1 row. There is not enough data for calibration so `method` is changed from "linear" to "none".
91+
Message
92+
93+
-- tailor ----------------------------------------------------------------------
94+
A regression postprocessor with 1 adjustment:
95+
96+
* Re-calibrate numeric predictions using linear method. [trained]
97+
7098
# passing arguments to adjust_numeric_calibration
7199

72100
Code

0 commit comments

Comments
 (0)