## ----setup, include = FALSE--------------------------------------------------- knitr::opts_chunk$set( collapse = TRUE, comment = "#>", eval = FALSE ) ## ----------------------------------------------------------------------------- library(inlabru) library(inlaops) library(vetiver) library(pins) library(purrr) library(dplyr) library(scoringRules) ## ----------------------------------------------------------------------------- # Versioned, so every vetiver_pin_write() keeps the earlier versions. board <- board_temp(versioned = TRUE) ## ----------------------------------------------------------------------------- set.seed(42) n_stations <- 120 year1 <- data.frame( station = seq_len(n_stations), habitat = sample(c("forest", "open"), n_stations, replace = TRUE), effort = runif(n_stations, 0.5, 2.0) ) year1$habitat_open <- as.integer(year1$habitat == "open") log_mu <- 0.8 + 0.6 * year1$habitat_open + log(year1$effort) year1$counts <- rpois(n_stations, exp(log_mu)) ## ----------------------------------------------------------------------------- set.seed(1) val_idx <- sample(n_stations, 20L) val_data <- year1[val_idx, ] train_y1 <- year1[-val_idx, ] ## ----------------------------------------------------------------------------- fit_v1 <- bru( counts ~ Intercept(1) + habitat_open + effort, family = "poisson", data = train_y1, options = bru_options( control.compute = list(config = TRUE, dic = TRUE, waic = TRUE) ) ) scores_v1 <- bru_score( fit_v1, newdata = val_data, # Poisson Y-samples: bru_score does not inject family-aware observation # noise — the user writes the rpois call. length(habitat_open) is the # observation count for the per-sample r*() (latent fields are scalars). pred_formula = ~ rpois( length(habitat_open), exp(Intercept + habitat_open + effort) ), y_col = "counts", n_samples = 1000L ) v1 <- vetiver_model( fit_v1, "wildlife_counts", metadata = list( pred_formula = "~ exp(Intercept + habitat_open + effort)", n_samples = 500L, model_year = 1L, training_rows = nrow(train_y1), dic = fit_v1$dic$dic, waic = fit_v1$waic$waic, mean_crps = mean(scores_v1$crps), mean_logs = mean(scores_v1$logs) ) ) vetiver_pin_write(board, v1) pin_versions(board, "wildlife_counts") ## ----------------------------------------------------------------------------- set.seed(99) year2 <- year1 log_mu2 <- 0.9 + 0.5 * year2$habitat_open + log(year2$effort) year2$counts <- rpois(n_stations, exp(log_mu2)) train_y2 <- year2[-val_idx, ] val_y2 <- year2[ val_idx, ] ## ----------------------------------------------------------------------------- fit_v2 <- bru( counts ~ Intercept(1) + habitat_open + effort, family = "poisson", data = train_y2, options = bru_options( control.compute = list(config = TRUE, dic = TRUE, waic = TRUE) ) ) scores_v2 <- bru_score( fit_v2, newdata = val_y2, pred_formula = ~ rpois( length(habitat_open), exp(Intercept + habitat_open + effort) ), y_col = "counts", n_samples = 1000L ) v2 <- vetiver_model( fit_v2, "wildlife_counts", metadata = list( pred_formula = "~ exp(Intercept + habitat_open + effort)", n_samples = 500L, model_year = 2L, training_rows = nrow(train_y2), dic = fit_v2$dic$dic, waic = fit_v2$waic$waic, mean_crps = mean(scores_v2$crps), mean_logs = mean(scores_v2$logs) ) ) vetiver_pin_write(board, v2) pin_versions(board, "wildlife_counts") ## ----------------------------------------------------------------------------- # Newest first. pin_versions() does not promise an order, so sort. versions <- pin_versions(board, "wildlife_counts") |> arrange(desc(created)) quality <- map_dfr(versions$version, function(ver) { meta <- pin_meta(board, "wildlife_counts", version = ver) tibble( version = ver, created = meta$created, training_rows = meta$user$training_rows, waic = meta$user$waic, dic = meta$user$dic, mean_crps = meta$user$mean_crps, mean_logs = meta$user$mean_logs ) }) quality ## ----------------------------------------------------------------------------- y_formula <- ~ rpois( length(habitat_open), exp(Intercept + habitat_open + effort) ) scores_v1_now <- bru_score( fit_v1, newdata = val_y2, pred_formula = y_formula, y_col = "counts", n_samples = 1000L ) # Match scores to versions through the model_year stored in each pin, not # through row order: timestamps can tie, and the board may hold more versions. crps_now <- c(`1` = mean(scores_v1_now$crps), `2` = mean(scores_v2$crps)) head_to_head <- tibble( version = versions$version, model_year = map_int(version, function(ver) { pin_meta(board, "wildlife_counts", version = ver)$user$model_year }), mean_crps = unname(crps_now[as.character(model_year)]) ) head_to_head ## ----------------------------------------------------------------------------- prod_board <- board_temp(versioned = TRUE) # Load the winning version from the staging board. vetiver_pin_read() # returns a fully-formed vetiver_model (description + prototype + metadata # intact), so we can re-pin it directly to production. Wrapping it again # in vetiver_model() would error — there is no method to create a # description from a vetiver_model object. best_version <- head_to_head$version[which.min(head_to_head$mean_crps)] v_winning <- vetiver_pin_read(board, "wildlife_counts", version = best_version) vetiver_pin_write(prod_board, v_winning) ## ----------------------------------------------------------------------------- # Re-read versions in case the board state has changed since section 3, # newest first. versions_now <- pin_versions(board, "wildlife_counts") |> arrange(desc(created)) stopifnot( "Need at least 2 pinned versions to demonstrate rollback" = nrow(versions_now) >= 2L ) # Roll back to the newest version other than the one in production. previous_version <- setdiff(versions_now$version, best_version)[1] v_rollback <- vetiver_pin_read(board, "wildlife_counts", version = previous_version) vetiver_pin_write(prod_board, v_rollback) message("Rolled back to version: ", previous_version) ## ----------------------------------------------------------------------------- library(ggplot2) quality |> mutate(created = as.POSIXct(created)) |> ggplot(aes(x = created)) + geom_line(aes(y = mean_crps, colour = "CRPS")) + geom_point(aes(y = mean_crps, colour = "CRPS"), size = 2) + geom_line(aes(y = waic / 1000, colour = "WAIC / 1000"), linetype = "dashed") + labs( x = "Version date", y = "Score (lower is better)", colour = NULL, title = "Model quality across versions" ) + theme_minimal() ## ----------------------------------------------------------------------------- library(plumber) # vetiver_pin_read() returns the full vetiver model, including the # prototype and the pred_formula in its metadata. v_prod <- vetiver_pin_read(prod_board, "wildlife_counts") pr() |> vetiver_api(v_prod) |> pr_run(port = 8088) ## ----------------------------------------------------------------------------- library(httr2) new_stations <- data.frame( habitat_open = c(0L, 1L), effort = c(1.0, 1.5) ) resp <- request("http://127.0.0.1:8088/predict") |> req_body_raw( jsonlite::toJSON(new_stations, dataframe = "rows"), type = "application/json" ) |> req_perform() tibble::as_tibble(resp_body_json(resp, simplifyVector = TRUE)) ## ----------------------------------------------------------------------------- Sys.setenv( CONNECT_SERVER = "https://connect.example.com/", CONNECT_API_KEY = "..." # set in your deploy environment, never commit ) prod_board <- board_connect() vetiver_pin_write(prod_board, v_winning) ## ----------------------------------------------------------------------------- prod_board <- board_s3( bucket = "my-org-models", prefix = "wildlife_counts/" ) vetiver_pin_write(prod_board, v_winning) ## ----------------------------------------------------------------------------- # At the top of plumber.R library(inlaops) # provides the methods that serve bru fits prod_board <- if (Sys.getenv("CONNECT_API_KEY") != "") { pins::board_connect() } else if (Sys.getenv("AWS_ACCESS_KEY_ID") != "") { pins::board_s3(bucket = Sys.getenv("MODEL_BUCKET"), prefix = Sys.getenv("MODEL_PREFIX")) } else { pins::board_folder(Sys.getenv("MODEL_DIR", "./model-store")) } v_prod <- vetiver::vetiver_pin_read(prod_board, "wildlife_counts")