-
Notifications
You must be signed in to change notification settings - Fork 28
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Three monitoring functions (for use in dashboards) #92
Changes from all commits
80521bc
2839d1d
99d4761
06c9b78
04b6cad
737ea6b
570603f
c7982ca
ec5c326
ebf9059
ba26008
b82d7bc
90a9303
2d6b1d1
ddb694c
2b0463c
d67a743
422fa3f
977070f
9aed822
08f8c4a
22bbccb
b1daf0e
2e025dc
f1b6bde
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
# Workflow derived from https://github.com/r-lib/actions/tree/v2/examples | ||
# Need help debugging build failures? Start at https://github.com/r-lib/actions#where-to-find-help | ||
# | ||
# NOTE: This workflow only directly installs "hard" dependencies, i.e. Depends, | ||
# Imports, and LinkingTo dependencies. Notably, Suggests dependencies are never | ||
# installed, with the exception of testthat, knitr, and rmarkdown. The cache is | ||
# never used to avoid accidentally restoring a cache containing a suggested | ||
# dependency. | ||
on: | ||
push: | ||
branches: [main] | ||
pull_request: | ||
branches: [main] | ||
|
||
name: R-CMD-check-hard | ||
|
||
jobs: | ||
R-CMD-check: | ||
runs-on: ${{ matrix.config.os }} | ||
|
||
name: ${{ matrix.config.os }} (${{ matrix.config.r }}) | ||
|
||
strategy: | ||
fail-fast: false | ||
matrix: | ||
config: | ||
- {os: ubuntu-18.04, r: 'release'} | ||
|
||
env: | ||
GITHUB_PAT: ${{ secrets.GITHUB_TOKEN }} | ||
R_KEEP_PKG_SOURCE: yes | ||
|
||
steps: | ||
- uses: actions/checkout@v2 | ||
|
||
- uses: r-lib/actions/setup-pandoc@v2 | ||
|
||
- uses: r-lib/actions/setup-r@v2 | ||
with: | ||
r-version: ${{ matrix.config.r }} | ||
http-user-agent: ${{ matrix.config.http-user-agent }} | ||
use-public-rspm: true | ||
|
||
- uses: r-lib/actions/setup-r-dependencies@v2 | ||
with: | ||
dependencies: '"hard"' | ||
cache: false | ||
extra-packages: | | ||
any::rcmdcheck | ||
any::testthat | ||
any::knitr | ||
any::rmarkdown | ||
needs: check | ||
|
||
- uses: r-lib/actions/check-r-package@v2 | ||
with: | ||
upload-snapshots: true |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
@@ -0,0 +1,226 @@ | ||||||||||
#' Aggregate, store, and plot model metrics over time for monitoring | ||||||||||
#' | ||||||||||
#' These three functions can be used for model monitoring (such as in a | ||||||||||
#' monitoring dashboard): | ||||||||||
#' - `vetiver_compute_metrics()` computes metrics (such as accuracy for a | ||||||||||
#' classification model or RMSE for a regression model) at a chosen time | ||||||||||
#' aggregation `.period` | ||||||||||
#' - `vetiver_pin_metrics()` updates an existing pin storing model metrics | ||||||||||
#' over time | ||||||||||
#' - `vetiver_plot_metrics()` creates a plot of metrics over time | ||||||||||
#' | ||||||||||
#' @inheritParams yardstick::metrics | ||||||||||
#' @inheritParams pins::pin_read | ||||||||||
#' @inheritParams slider::slide_period | ||||||||||
#' @param date_var The column in `data` containing dates or date-times for | ||||||||||
#' monitoring, to be aggregated with `.period` | ||||||||||
#' @param metric_set A [yardstick::metric_set()] function for computing metrics. | ||||||||||
#' Defaults to [yardstick::metrics()]. | ||||||||||
#' @param df_metrics A tidy dataframe of metrics over time, such as created by | ||||||||||
#' `vetiver_compute_metrics()`. | ||||||||||
#' @param metrics_pin_name Pin name for where the *metrics* are stored (as | ||||||||||
#' opposed to where the model object is stored with [vetiver_pin_write()]). | ||||||||||
#' @param overwrite If `TRUE` (the default), overwrite any metrics for dates | ||||||||||
#' that exist both in the existing pin and new metrics with the _new_ values. | ||||||||||
#' If `FALSE`, error when the new metrics contain overlapping dates with the | ||||||||||
#' existing pin. | ||||||||||
#' @param .index The variable in `df_metrics` containing the aggregated dates | ||||||||||
#' or date-times (from `time_var` in `data`). Defaults to `.index`. | ||||||||||
#' @param .estimate The variable in `df_metrics` containing the metric estimate. | ||||||||||
#' Defaults to `.estimate`. | ||||||||||
#' @param .metric The variable in `df_metrics` containing the metric type. | ||||||||||
#' Defaults to `.metric`. | ||||||||||
#' @param .n The variable in `df_metrics` containing the number of observations | ||||||||||
#' used for estimating the metric. | ||||||||||
#' | ||||||||||
#' @return Both `vetiver_compute_metrics()` and `vetiver_pin_metrics()` return | ||||||||||
#' a dataframe of metrics. The `vetiver_plot_metrics()` function returns a | ||||||||||
#' `ggplot2` object. | ||||||||||
#' | ||||||||||
#' @details Sometimes when you monitor a model at a given time aggregation, you | ||||||||||
#' may end up with dates in your new metrics (like `new_metrics` in the example) | ||||||||||
#' that are the same as dates in your existing aggregated metrics (like | ||||||||||
#' `original_metrics` in the example). This can happen if you need to re-run a | ||||||||||
#' monitoring report because something failed. With `overwrite = TRUE` (the | ||||||||||
#' default), `vetiver_pin_metrics()` will replace such metrics with the new | ||||||||||
#' values. With `overwrite = FALSE`, `vetiver_pin_metrics()` will error when | ||||||||||
#' there are overlapping dates. | ||||||||||
#' | ||||||||||
#' For arguments used more than once in your monitoring dashboard, | ||||||||||
#' such as `date_var`, consider using | ||||||||||
#' [R Markdown parameters](https://bookdown.org/yihui/rmarkdown/parameterized-reports.html) | ||||||||||
#' to reduce repetition and/or errors. | ||||||||||
#' | ||||||||||
#' @examplesIf rlang::is_installed(c("dplyr", "parsnip", "modeldata", "ggplot2")) | ||||||||||
#' library(dplyr) | ||||||||||
#' library(parsnip) | ||||||||||
#' data(Chicago, package = "modeldata") | ||||||||||
#' Chicago <- Chicago %>% select(ridership, date, all_of(stations)) | ||||||||||
#' training_data <- Chicago %>% filter(date < "2009-01-01") | ||||||||||
#' testing_data <- Chicago %>% filter(date >= "2009-01-01", date < "2011-01-01") | ||||||||||
#' monitoring <- Chicago %>% filter(date >= "2011-01-01", date < "2012-12-31") | ||||||||||
#' lm_fit <- linear_reg() %>% fit(ridership ~ ., data = training_data) | ||||||||||
#' | ||||||||||
#' library(pins) | ||||||||||
#' b <- board_temp() | ||||||||||
#' | ||||||||||
#' ## before starting monitoring, initiate the metrics and pin | ||||||||||
#' ## (for example, with the testing data): | ||||||||||
#' original_metrics <- | ||||||||||
#' augment(lm_fit, new_data = testing_data) %>% | ||||||||||
#' vetiver_compute_metrics(date, "week", ridership, .pred, every = 4L) | ||||||||||
#' pin_write(b, original_metrics, "lm_fit_metrics") | ||||||||||
#' | ||||||||||
#' ## to continue monitoring with new data, compute metrics and update pin: | ||||||||||
#' new_metrics <- | ||||||||||
#' augment(lm_fit, new_data = monitoring) %>% | ||||||||||
#' vetiver_compute_metrics(date, "week", ridership, .pred, every = 4L) | ||||||||||
#' vetiver_pin_metrics(b, new_metrics, "lm_fit_metrics") | ||||||||||
#' | ||||||||||
#' library(ggplot2) | ||||||||||
#' vetiver_plot_metrics(new_metrics) + | ||||||||||
#' scale_size(range = c(2, 4)) | ||||||||||
#' | ||||||||||
#' @export | ||||||||||
vetiver_compute_metrics <- function(data, | ||||||||||
juliasilge marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
date_var, | ||||||||||
period, | ||||||||||
truth, estimate, ..., | ||||||||||
metric_set = yardstick::metrics, | ||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Might be an issue if yardstick is Suggests and you have this here There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I checked with the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I checked again (with CRAN back up this time) and it still looks OK to have |
||||||||||
every = 1L, | ||||||||||
origin = NULL, | ||||||||||
before = 0L, | ||||||||||
after = 0L, | ||||||||||
complete = FALSE) { | ||||||||||
|
||||||||||
rlang::check_installed("slider") | ||||||||||
truth_quo <- enquo(truth) | ||||||||||
estimate_quo <- enquo(estimate) | ||||||||||
|
||||||||||
# Figure out which column in `data` corresponds to `date_var` | ||||||||||
date_var <- enquo(date_var) | ||||||||||
date_var <- eval_select_one(date_var, data, "date_var") | ||||||||||
|
||||||||||
index <- data[[date_var]] | ||||||||||
|
||||||||||
slider::slide_period_dfr( | ||||||||||
.x = data, | ||||||||||
.i = index, | ||||||||||
.period = period, | ||||||||||
.f = compute_metrics, | ||||||||||
date_var = date_var, | ||||||||||
metric_set = metric_set, | ||||||||||
truth_quo = truth_quo, | ||||||||||
estimate_quo = estimate_quo, | ||||||||||
..., | ||||||||||
.every = every, | ||||||||||
.origin = origin, | ||||||||||
.before = before, | ||||||||||
.after = after, | ||||||||||
.complete = complete | ||||||||||
) | ||||||||||
|
||||||||||
} | ||||||||||
|
||||||||||
#' @rdname vetiver_compute_metrics | ||||||||||
#' @export | ||||||||||
vetiver_pin_metrics <- function(board, | ||||||||||
df_metrics, | ||||||||||
metrics_pin_name, | ||||||||||
.index = .index, | ||||||||||
overwrite = TRUE) { | ||||||||||
.index <- enquo(.index) | ||||||||||
.index <- eval_select_one(.index, df_metrics, "date_var") | ||||||||||
new_dates <- unique(df_metrics[[.index]]) | ||||||||||
|
||||||||||
old_metrics <- pins::pin_read(board, metrics_pin_name) | ||||||||||
overlapping_dates <- old_metrics[[.index]] %in% new_dates | ||||||||||
if (overwrite) { | ||||||||||
old_metrics <- vec_slice(old_metrics, !overlapping_dates) | ||||||||||
} else { | ||||||||||
if (any(overlapping_dates)) | ||||||||||
abort(c( | ||||||||||
glue("The new metrics overlap with dates \\ | ||||||||||
already stored in {glue::single_quote(metrics_pin_name)}"), | ||||||||||
i = "Check the aggregated dates or use `overwrite = TRUE`" | ||||||||||
)) | ||||||||||
} | ||||||||||
new_metrics <- vctrs::vec_rbind(old_metrics, df_metrics) | ||||||||||
new_metrics <- vec_slice( | ||||||||||
new_metrics, | ||||||||||
vctrs::vec_order(new_metrics[[.index]]) | ||||||||||
) | ||||||||||
|
||||||||||
pins::pin_write(board, new_metrics, basename(metrics_pin_name)) | ||||||||||
juliasilge marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||
new_metrics | ||||||||||
|
||||||||||
} | ||||||||||
|
||||||||||
compute_metrics <- function(data, | ||||||||||
date_var, | ||||||||||
metric_set, | ||||||||||
truth_quo, | ||||||||||
estimate_quo, | ||||||||||
...) { | ||||||||||
index <- data[[date_var]] | ||||||||||
index <- min(index) | ||||||||||
|
||||||||||
n <- nrow(data) | ||||||||||
|
||||||||||
metrics <- metric_set( | ||||||||||
data = data, | ||||||||||
truth = !!truth_quo, | ||||||||||
estimate = !!estimate_quo, | ||||||||||
... | ||||||||||
) | ||||||||||
|
||||||||||
tibble::tibble( | ||||||||||
.index = index, | ||||||||||
.n = n, | ||||||||||
metrics | ||||||||||
) | ||||||||||
} | ||||||||||
|
||||||||||
eval_select_one <- function(col, data, arg, ..., call = caller_env()) { | ||||||||||
rlang::check_installed("tidyselect") | ||||||||||
check_dots_empty() | ||||||||||
|
||||||||||
# `col` is a quosure that has its own environment attached | ||||||||||
env <- empty_env() | ||||||||||
|
||||||||||
loc <- tidyselect::eval_select( | ||||||||||
expr = col, | ||||||||||
data = data, | ||||||||||
env = env, | ||||||||||
error_call = call | ||||||||||
) | ||||||||||
|
||||||||||
if (length(loc) != 1L) { | ||||||||||
message <- glue::glue("`{arg}` must specify exactly one column from `data`.") | ||||||||||
abort(message, call = call) | ||||||||||
} | ||||||||||
|
||||||||||
loc | ||||||||||
} | ||||||||||
|
||||||||||
#' @rdname vetiver_compute_metrics | ||||||||||
#' @export | ||||||||||
vetiver_plot_metrics <- function(df_metrics, | ||||||||||
.index = .index, | ||||||||||
.estimate = .estimate, | ||||||||||
.metric = .metric, | ||||||||||
Comment on lines
+210
to
+211
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just because I think we only ever add (I realize it is a little weird here because the column name is |
||||||||||
.n = .n) { | ||||||||||
rlang::check_installed("ggplot2") | ||||||||||
.metric <- enquo(.metric) | ||||||||||
|
||||||||||
ggplot2::ggplot(data = df_metrics, | ||||||||||
ggplot2::aes({{ .index }}, {{.estimate}})) + | ||||||||||
ggplot2::geom_line(ggplot2::aes(color = !!.metric), alpha = 0.7) + | ||||||||||
ggplot2::geom_point(ggplot2::aes(color = !!.metric, | ||||||||||
size = {{.n}}), | ||||||||||
alpha = 0.9) + | ||||||||||
ggplot2::facet_wrap(ggplot2::vars(!!.metric), | ||||||||||
scales = "free_y", ncol = 1) + | ||||||||||
ggplot2::guides(color = "none") + | ||||||||||
ggplot2::labs(x = NULL, y = NULL, size = NULL) | ||||||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This PR adds A LOT of packages here. I put all the packages for monitoring in
Suggests
because I am up against 20 packages in Imports already and I can sort of make the argument that people may want to deploy a model but not monitor it. I am very open to ideas for reducing this somehow.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm only using dplyr in examples and tests FWIW 🤷♀️