--- title: "inlabru as a Bayesian GLM with parsnip" format: html vignette: > %\VignetteIndexEntry{inlabru as a Bayesian GLM with parsnip} %\VignetteEngine{quarto::html} %\VignetteEncoding{UTF-8} --- ```{r setup, include = FALSE} knitr::opts_chunk$set( collapse = TRUE, comment = "#>", # Evaluated wherever INLA and the packages used here are installed. eval = all(vapply( c("INLA", "parsnip", "broom", "rsample", "yardstick", "dplyr", "vetiver", "pins"), requireNamespace, logical(1), quietly = TRUE )) ) ``` ## Scope `bru_reg()` is scoped to **inlabru used as a fast Bayesian GLM**: models with fixed effects and IID random intercepts only. If your model includes any of the following, stop here and use inlabru's native interface instead: - Spatial random effects: BYM2, SPDE, LGCP - Structured temporal effects: AR1, RW1, RW2 - Custom nonlinear predictors with mapper functions For those models, cross-validation RMSE is not a meaningful quantity and the parsnip abstraction does not hold. See `vignette("scoring")` for the statistically appropriate evaluation approach. For simple Bayesian GLMs — Poisson regression for counts, Gaussian regression for continuous outcomes, binomial for proportions — the parsnip layer gives you a consistent interface for comparison with frequentist or ML baselines and clean integration with the rest of the tidymodels ecosystem. ## Setup ```{r, message = FALSE} library(inlabru) library(inlaops) library(parsnip) library(broom) library(rsample) library(yardstick) library(dplyr) ``` ## Example: Poisson regression for count data ```{r} set.seed(123) n <- 300 dat <- data.frame( area = rnorm(n), effort = runif(n, 0.5, 2.0) ) log_mu <- 0.5 + 0.8 * dat$area + log(dat$effort) dat$counts <- rpois(n, exp(log_mu)) ``` `bru_reg()` does not auto-encode factor or character predictors — fitting `counts ~ Intercept(1) + habitat` on a raw `factor` or `character` column errors with a pointer to three workarounds: pre-encode via `recipes::step_dummy()` + `recipes::bake()` before calling `fit()` (recipes/workflows cannot be wrapped around `bru_reg` because they rebuild formulas without inlabru's component syntax), convert to integer indicator columns yourself, or use inlabru's native `habitat(habitat, model = "factor_full")` component syntax. ### Fit with bru_reg() ```{r} spec <- bru_reg(mode = "regression", family = "poisson") fit <- spec |> set_engine("inlabru") |> fit(counts ~ Intercept(1) + area + effort, data = dat) fit ``` The engine automatically sets `control.compute = list(config = TRUE, dic = TRUE, waic = TRUE)`. The fit is serving-ready for the vetiver bridge without any extra options. For large data sets where WAIC is too expensive, opt out via `set_engine("inlabru", dic = FALSE, waic = FALSE)`; `config = TRUE` is always set because the vetiver `/predict` endpoint needs it. ### Inspect with broom The `tidy()`, `glance()`, and `augment()` methods for `bru` fits come from inlabru (>= 2.15.0). ```{r} tidy(fit$fit) glance(fit$fit) ``` ### Predict ```{r} new_dat <- data.frame(area = c(-1, 0, 1), effort = c(1, 1, 1)) predict(fit, new_data = new_dat) ``` The parsnip `predict()` returns the posterior mean on the response scale. For predictive uncertainty, use `augment()` directly: ```{r} augment(fit$fit, data = new_dat, pred_formula = ~ exp(Intercept + area + effort)) ``` ## Comparing against a frequentist baseline A side-by-side comparison with a frequentist GLM is a one-liner per engine. Note that `bru_reg` requires inlabru component syntax (`Intercept(1)` is explicit), while `glm()` uses the standard implicit-intercept form — the formulas differ on purpose. ```{r} # Frequentist baseline fit_glm <- glm(counts ~ area + effort, data = dat, family = poisson()) # Bayesian with inlabru, via parsnip bru_spec <- bru_reg(family = "poisson") |> set_engine("inlabru") fit_bru <- bru_spec |> fit(counts ~ Intercept(1) + area + effort, data = dat) # Coefficient comparison (response-scale interval for the Bayesian fit) broom::tidy(fit_glm) broom::tidy(fit_bru$fit) ``` `recipes` and `workflows` are not compatible with `bru_reg` because they rebuild formulas in the standard implicit-intercept shape, which `bru()` rejects (it needs the explicit `Intercept(1)` component). For preprocessing, manipulate `data` before the call to `fit()` rather than wrapping in a recipe. ## Cross-validation with bru_cv_predict() For fixed-effects models, `bru_cv_predict()` wraps `vfold_cv()` and returns a tidy holdout prediction tibble: ```{r} cv_preds <- bru_cv_predict( formula = counts ~ Intercept(1) + area + effort, family = "poisson", data = dat, v = 5L, pred_formula = ~ exp(Intercept + area + effort), n_samples = 500L ) cv_preds # Compute RMSE across folds yardstick::rmse(cv_preds, truth = .truth, estimate = .pred) ``` For proper scoring rules that use the full posterior distribution rather than just the mean, see `vignette("scoring")`. ## Connecting to vetiver A model fit via `bru_reg()` is automatically serving-ready: ```{r} library(vetiver) library(pins) v <- vetiver_model(fit$fit, "poisson_glm") v$metadata$user <- list( pred_formula = "~ exp(Intercept + area + effort)", n_samples = 1000L ) board <- board_temp() vetiver_pin_write(board, v) ``` See `vignette("vetiver")` for the full serving and monitoring workflow.