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
Example: Poisson regression for count data
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()
spec <- bru_reg(mode = "regression", family = "poisson")
fit <- spec |>
set_engine("inlabru") |>
fit(counts ~ Intercept(1) + area + effort, data = dat)
fit
#> parsnip model object
#>
#> inlabru version: 2.15.0
#> INLA version: 26.08.07
#> Latent components:
#> Intercept: main = linear(1)
#> area: main = linear(area)
#> effort: main = linear(effort)
#> Observation models:
#> Model tag: <No tag>
#> Family: 'poisson'
#> Data class: 'data.frame'
#> Response class: 'integer'
#> Predictor: counts ~ Intercept + area + effort
#> Additive/Linear/Rowwise: TRUE/TRUE/TRUE
#> Used components: effect[Intercept, area, effort], latent[]
#> Time used:
#> Pre = 0.188, Running = 0.336, Post = 0.0734, Total = 0.598
#> Fixed effects:
#> mean sd 0.025quant 0.5quant 0.975quant mode kld
#> Intercept -0.271 0.127 -0.519 -0.271 -0.023 -0.271 0
#> area 0.762 0.035 0.694 0.762 0.829 0.762 0
#> effort 0.751 0.086 0.583 0.751 0.920 0.751 0
#>
#> Deviance Information Criterion (DIC) ...............: 994.69
#> Deviance Information Criterion (DIC, saturated) ....: 292.49
#> Effective number of parameters .....................: 3.00
#>
#> Watanabe-Akaike information criterion (WAIC) ...: 994.85
#> Effective number of parameters .................: 3.02
#>
#> Marginal log-Likelihood: -513.88
#> is computed
#> Posterior summaries for the linear predictor and the fitted values are computed
#> (Posterior marginals needs also 'control.compute=list(return.marginals.predictor=TRUE)')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).
tidy(fit$fit)
#> # A tibble: 3 × 5
#> term estimate std.error conf.low conf.high
#> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 Intercept -0.271 0.127 -0.519 -0.0227
#> 2 area 0.762 0.0346 0.694 0.829
#> 3 effort 0.751 0.0860 0.583 0.920
glance(fit$fit)
#> # A tibble: 1 × 5
#> dic waic marginal_loglik nobs elapsed
#> <dbl> <dbl> <dbl> <int> <dbl>
#> 1 995. 995. -514. 300 0.695Predict
new_dat <- data.frame(area = c(-1, 0, 1), effort = c(1, 1, 1))
predict(fit, new_data = new_dat)
#> # A tibble: 3 × 1
#> .pred
#> <dbl>
#> 1 0.757
#> 2 1.62
#> 3 3.46The parsnip predict() returns the posterior mean on the response scale. For predictive uncertainty, use augment() directly:
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.
# 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)
#> # A tibble: 3 × 5
#> term estimate std.error statistic p.value
#> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 (Intercept) -0.268 0.127 -2.12 3.42e- 2
#> 2 area 0.761 0.0346 22.0 1.93e-107
#> 3 effort 0.751 0.0860 8.73 2.55e- 18
broom::tidy(fit_bru$fit)
#> # A tibble: 3 × 5
#> term estimate std.error conf.low conf.high
#> <chr> <dbl> <dbl> <dbl> <dbl>
#> 1 Intercept -0.271 0.127 -0.519 -0.0227
#> 2 area 0.762 0.0346 0.694 0.829
#> 3 effort 0.751 0.0860 0.583 0.920recipes 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:
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
#> # A tibble: 300 × 5
#> .truth .pred .pred_low .pred_high .pred_sd
#> <int> <dbl> <dbl> <dbl> <dbl>
#> 1 2 1.91 1.71 2.14 0.107
#> 2 11 9.95 8.82 11.1 0.603
#> 3 3 5.62 5.17 6.06 0.236
#> 4 6 2.07 1.82 2.36 0.133
#> 5 2 2.19 1.94 2.48 0.133
#> 6 6 2.86 2.52 3.26 0.193
#> 7 2 2.02 1.80 2.27 0.117
#> 8 1 0.957 0.796 1.14 0.0872
#> 9 0 0.314 0.247 0.394 0.0380
#> 10 5 2.65 2.24 3.11 0.212
#> # ℹ 290 more rows
# Compute RMSE across folds
yardstick::rmse(cv_preds, truth = .truth, estimate = .pred)
#> # A tibble: 1 × 3
#> .metric .estimator .estimate
#> <chr> <chr> <dbl>
#> 1 rmse standard 1.63For 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:
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)
#> Creating new version '20261016T203236Z-0bd13'
#> Writing to pin 'poisson_glm'
#>
#> Create a Model Card for your published model
#> • Model Cards provide a framework for transparent, responsible reporting
#> • Use the vetiver `.Rmd` template as a place to startSee vignette("vetiver") for the full serving and monitoring workflow.