mvgam | R Documentation |
This function estimates the posterior distribution for Generalised Additive
Models (GAMs) that can include smooth spline functions, specified in the GAM
formula, as well as latent temporal processes, specified by trend_model
.
Further modelling options include State-Space representations to allow covariates
and dynamic processes to occur on the latent 'State' level while also capturing
observation-level effects. Prior specifications are flexible and explicitly
encourage users to apply prior distributions that actually reflect their beliefs.
In addition, model fits can easily be assessed and
compared with posterior predictive checks, forecast comparisons and
leave-one-out / leave-future-out cross-validation.
mvgam(
formula,
trend_formula,
knots,
trend_knots,
trend_model = "None",
noncentred = FALSE,
family = poisson(),
share_obs_params = FALSE,
data,
newdata,
use_lv = FALSE,
n_lv,
trend_map,
priors,
run_model = TRUE,
prior_simulation = FALSE,
residuals = TRUE,
return_model_data = FALSE,
backend = getOption("brms.backend", "cmdstanr"),
algorithm = getOption("brms.algorithm", "sampling"),
control = list(max_treedepth = 10, adapt_delta = 0.8),
chains = 4,
burnin = 500,
samples = 500,
thin = 1,
parallel = TRUE,
threads = 1,
save_all_pars = FALSE,
silent = 1,
autoformat = TRUE,
refit = FALSE,
lfo = FALSE,
...
)
formula |
A |
trend_formula |
An optional |
knots |
An optional |
trend_knots |
As for |
trend_model |
For all trend types apart from |
noncentred |
|
family |
Default is |
share_obs_params |
|
data |
A
Note however that there are special cases where these identifiers are not
needed. For
example, models with hierarchical temporal correlation processes (e.g.
|
newdata |
Optional |
use_lv |
|
n_lv |
|
trend_map |
Optional |
priors |
An optional |
run_model |
|
prior_simulation |
|
residuals |
Logical indicating whether to compute series-level randomized quantile residuals and include
them as part of the returned object. Defaults to |
return_model_data |
|
backend |
Character string naming the package to use as the backend for fitting
the Stan model. Options are "cmdstanr" (the default) or "rstan". Can be set globally
for the current R session via the |
algorithm |
Character string naming the estimation approach to use.
Options are |
control |
A named |
chains |
|
burnin |
|
samples |
|
thin |
Thinning interval for monitors. Ignored
if |
parallel |
|
threads |
|
save_all_pars |
|
silent |
Verbosity level between |
autoformat |
|
refit |
Logical indicating whether this is a refit, called using |
lfo |
Logical indicating whether this is part of a call to lfo_cv.mvgam. Returns a
lighter version of the model with no residuals and fewer monitored parameters to speed up
post-processing. But other downstream functions will not work properly, so users should always
leave this set as |
... |
Further arguments passed to Stan.
For |
Dynamic GAMs are useful when we wish to predict future values from time series that show temporal dependence
but we do not want to rely on extrapolating from a smooth term (which can sometimes lead to unpredictable and unrealistic behaviours).
In addition, smooths can often try to wiggle excessively to capture any autocorrelation that is present in a time series,
which exacerbates the problem of forecasting ahead. As GAMs are very naturally viewed through a Bayesian lens, and we often
must model time series that show complex distributional features and missing data, parameters for mvgam
models are estimated
in a Bayesian framework using Markov Chain Monte Carlo by default. A general overview is provided
in the primary vignettes: vignette("mvgam_overview")
and vignette("data_in_mvgam")
.
For a full list of available vignettes see vignette(package = "mvgam")
Formula syntax: Details of the formula syntax used by mvgam can be found in
mvgam_formulae
. Note that it is possible to supply an empty formula where
there are no predictors or intercepts in the observation model (i.e. y ~ 0
or y ~ -1
).
In this case, an intercept-only observation model will be set up but the intercept coefficient
will be fixed at zero. This can be handy if you wish to fit pure State-Space models where
the variation in the dynamic trend controls the average expectation, and/or where intercepts
are non-identifiable (as in piecewise trends, see examples below)
Families and link functions: Details of families supported by mvgam
can be found in mvgam_families
.
Trend models: Details of latent error process models supported by mvgam
can be found in mvgam_trends
.
Priors: Default priors for intercepts and any variance parameters are chosen
to be vaguely informative, but these should always be checked by the user.
Prior distributions for most important model parameters can be altered
(see get_mvgam_priors()
for details).
Note that latent trends are estimated on the link scale so choose priors
accordingly. However more control over the model specification can be accomplished
by setting run_model = FALSE
and then editing the model code (
found in the model_file
slot in the returned object) before running the
model using either rstan or cmdstanr. This is encouraged for
complex modelling tasks. Note, no priors are formally checked to ensure
they are in the right syntax so it is up to the user to ensure these are correct
Random effects: For any smooth terms using the random effect basis (smooth.construct.re.smooth.spec
),
a non-centred parameterisation is automatically employed to avoid degeneracies that are common in hierarchical models.
Note however that centred versions may perform better for series that are particularly informative, so as with any
foray into Bayesian modelling, it is worth building an understanding of the model's assumptions and limitations by following a
principled workflow. Also note that models are parameterised using drop.unused.levels = FALSE
in jagam
to ensure predictions can be made for all levels of the supplied factor variable
Observation level parameters: When more than one series is included in data
and an
observation family that contains more than one parameter is used, additional observation family parameters
(i.e. phi
for nb()
or sigma
for gaussian()
) are
by default estimated independently for each series. But if you wish for the series to share
the same observation parameters, set share_obs_params = TRUE
Residuals: For each series, randomized quantile (i.e. Dunn-Smyth) residuals are calculated for inspecting model diagnostics
If the fitted model is appropriate then Dunn-Smyth residuals will be standard normal in distribution and no
autocorrelation will be evident. When a particular observation is missing, the residual is calculated by comparing independent
draws from the model's posterior distribution
Using Stan: mvgam
is primarily designed to use Hamiltonian Monte Carlo for parameter estimation
via the software Stan
(using either the cmdstanr
or rstan
interface).
There are great advantages when using Stan
over Gibbs / Metropolis Hastings samplers, which includes the option
to estimate nonlinear effects via Hilbert space approximate Gaussian Processes,
the availability of a variety of inference algorithms (i.e. variational inference, laplacian inference etc...) and
capabilities to enforce stationarity for complex Vector Autoregressions.
Because of the many advantages of Stan
over JAGS
,
further development of the package will only be applied to Stan
. This includes the planned addition
of more response distributions, plans to handle zero-inflation, and plans to incorporate a greater
variety of trend models. Users are strongly encouraged to opt for Stan
over JAGS
in any proceeding workflows
How to start?: The mvgam
cheatsheet is a
good starting place if you are just learning to use the package. It gives an overview of the package's key functions and objects,
as well as providing a reasonable workflow that new users can follow. In general it is recommended to
1. Check that your time series data are in a suitable tidy format for mvgam
modeling (see the data formatting vignette for guidance)
2. Inspect features of the data using plot_mvgam_series
. Now is also a good time to familiarise yourself
with the package's example workflows that are detailed in the vignettes. In particular,
the getting started vignette,
the shared latent states vignette,
the time-varying effects vignette and
the State-Space models vignette all provide
useful information about how to structure, fit and interrogate Dynamic Generalized Additive Models in mvgam. Some
more specialized how-to articles include
"Fitting N-mixture models in mgam
,
"Joint Species Distribution Models in mgam
,
"Incorporating time-varying seasonality in forecast models"
and "Temporal autocorrelation in GAMs and the mvgam
package"
3. Carefully think about how to structure linear predictor effects (i.e. smooth terms using s()
,
te()
or ti()
, GPs using gp()
, dynamic time-varying effects using dynamic()
, and parametric terms), latent temporal trend components (see mvgam_trends
) and the appropriate
observation family (see mvgam_families
). Use get_mvgam_priors()
to see default prior distributions
for stochastic parameters
4. Change default priors using appropriate prior knowledge (see prior()
). When using State-Space models
with a trend_formula
, pay particular attention to priors for any variance parameters such as process errors and observation
errors. Default priors on these parameters are chosen to be vaguely informative and to avoid
zero (using Inverse Gamma priors), but more informative priors will often help with
model efficiency and convergence
5. Fit the model using either Hamiltonian Monte Carlo or an approximation algorithm (i.e.
change the backend
argument) and use summary.mvgam()
, conditional_effects.mvgam()
,
mcmc_plot.mvgam()
, pp_check.mvgam()
, pairs.mvgam()
and
plot.mvgam()
to inspect / interrogate the model
6. Update the model as needed and use loo_compare.mvgam()
for in-sample model comparisons,
or alternatively use forecast.mvgam()
, lfo_cv.mvgam()
and
score.mvgam_forecast()
to compare models based on out-of-sample forecasts
(see the forecast evaluation vignette
for guidance)
7. When satisfied with the model structure, use predict.mvgam()
,
plot_predictions()
and/or plot_slopes()
for
more targeted inferences (see "How to interpret and report nonlinear effects from Generalized Additive Models" for some guidance on interpreting GAMs)
8. Use how_to_cite()
to obtain a scaffold methods section (with full references) to begin describing this
model in scientific publications
A list
object of class mvgam
containing model output, the text representation of the model file,
the mgcv model output (for easily generating simulations at
unsampled covariate values), Dunn-Smyth residuals for each series and key information needed
for other functions in the package. See mvgam-class
for details.
Use methods(class = "mvgam")
for an overview on available methods.
Nicholas J Clark
Nicholas J Clark & Konstans Wells (2023). Dynamic generalised additive models (DGAMs) for forecasting discrete ecological time series.
Methods in Ecology and Evolution. 14:3, 771-784.
Nicholas J Clark, SK Morgan Ernest, Henry Senyondo, Juniper Simonis, Ethan P White,
Glenda M Yenni, KANK Karunarathna (2025). Beyond single-species models: leveraging
multispecies forecasts to navigate the dynamics of ecological predictability. PeerJ.
13:e18929 https://doi.org/10.7717/peerj.18929
jagam()
, gam()
, gam.models
,
get_mvgam_priors()
, jsdgam()
# Simulate three time series that have shared seasonal dynamics,
# independent AR(1) trends, and Poisson observations
set.seed(0)
dat <- sim_mvgam(
T = 80,
n_series = 3,
mu = 2,
trend_model = AR(p = 1),
prop_missing = 0.1,
prop_trend = 0.6
)
# Plot key summary statistics for a single series
plot_mvgam_series(data = dat$data_train, series = 1)
# Plot all series together
plot_mvgam_series(data = dat$data_train, series = "all")
# Formulate a model using Stan where series share a cyclic smooth for
# seasonality and each series has an independent AR1 temporal process.
# Note that 'noncentred = TRUE' will likely give performance gains.
# Set run_model = FALSE to inspect the returned objects
mod1 <- mvgam(
formula = y ~ s(season, bs = "cc", k = 6),
data = dat$data_train,
trend_model = AR(),
family = poisson(),
noncentred = TRUE,
run_model = FALSE
)
# View the model code in Stan language
stancode(mod1)
# View the data objects needed to fit the model in Stan
sdata1 <- standata(mod1)
str(sdata1)
# Now fit the model
mod1 <- mvgam(
formula = y ~ s(season, bs = "cc", k = 6),
data = dat$data_train,
trend_model = AR(),
family = poisson(),
noncentred = TRUE,
chains = 2,
silent = 2
)
# Extract the model summary
summary(mod1)
# Plot the estimated historical trend and forecast for one series
plot(mod1, type = "trend", series = 1)
plot(mod1, type = "forecast", series = 1)
# Residual diagnostics
plot(mod1, type = "residuals", series = 1)
resids <- residuals(mod1)
str(resids)
# Fitted values and residuals can also be added to training data
augment(mod1)
# Compute the forecast using covariate information in data_test
fc <- forecast(mod1, newdata = dat$data_test)
str(fc)
fc_summary <- summary(fc)
head(fc_summary, 12)
plot(fc)
# Plot the estimated seasonal smooth function
plot(mod1, type = "smooths")
# Plot estimated first derivatives of the smooth
plot(mod1, type = "smooths", derivatives = TRUE)
# Plot partial residuals of the smooth
plot(mod1, type = "smooths", residuals = TRUE)
# Plot posterior realisations for the smooth
plot(mod1, type = "smooths", realisations = TRUE)
# Plot conditional response predictions using marginaleffects
conditional_effects(mod1)
plot_predictions(mod1, condition = "season", points = 0.5)
# Generate posterior predictive checks using bayesplot
pp_check(mod1)
# Extract observation model beta coefficient draws as a data.frame
beta_draws_df <- as.data.frame(mod1, variable = "betas")
head(beta_draws_df)
str(beta_draws_df)
# Investigate model fit
mc.cores.def <- getOption("mc.cores")
options(mc.cores = 1)
loo(mod1)
options(mc.cores = mc.cores.def)
# Fit a model to the portal time series that uses a latent
# Vector Autoregression of order 1
mod <- mvgam(
formula = captures ~ -1,
trend_formula = ~ trend,
trend_model = VAR(cor = TRUE),
family = poisson(),
data = portal_data,
chains = 2,
silent = 2
)
# Plot the autoregressive coefficient distributions;
# use 'dir = "v"' to arrange the order of facets
# correctly
mcmc_plot(
mod,
variable = 'A',
regex = TRUE,
type = 'hist',
facet_args = list(dir = 'v')
)
# Plot the process error variance-covariance matrix in the same way;
mcmc_plot(
mod,
variable = 'Sigma',
regex = TRUE,
type = 'hist',
facet_args = list(dir = 'v')
)
# Calulate Generalized IRFs for each series
irfs <- irf(
mod,
h = 12,
cumulative = FALSE
)
# Plot some of them
plot(irfs, series = 1)
plot(irfs, series = 2)
# Calulate forecast error variance decompositions for each series
fevds <- fevd(mod, h = 12)
# Plot median contributions to forecast error variance
plot(fevds)
# Now fit a model that uses two RW dynamic factors to model
# the temporal dynamics of the four rodent species
mod <- mvgam(
captures ~ series,
trend_model = RW(),
use_lv = TRUE,
n_lv = 2,
data = portal_data,
chains = 2,
silent = 2
)
# Plot the factors
plot(mod, type = 'factors')
# Plot the hindcasts
hcs <- hindcast(mod)
plot(hcs,
series = 1)
plot(hcs,
series = 2)
plot(hcs,
series = 3)
plot(hcs,
series = 4)
# Use residual_cor() to calculate temporal correlations among the series
# based on the factor loadings
lvcors <- residual_cor(mod)
names(lvcors)
lvcors$cor
# For those correlations whose credible intervals did not include
# zero, plot them as a correlation matrix (all other correlations
# are shown as zero on this plot)
plot(lvcors, cluster = TRUE)
# Example of supplying a trend_map so that some series can share
# latent trend processes
sim <- sim_mvgam(n_series = 3)
mod_data <- sim$data_train
# Here, we specify only two latent trends; series 1 and 2 share a trend,
# while series 3 has it's own unique latent trend
trend_map <- data.frame(
series = unique(mod_data$series),
trend = c(1, 1, 2)
)
# Fit the model using AR1 trends
mod <- mvgam(
formula = y ~ s(season, bs = "cc", k = 6),
trend_map = trend_map,
trend_model = AR(),
data = mod_data,
return_model_data = TRUE,
chains = 2,
silent = 2
)
# The mapping matrix is now supplied as data to the model in the 'Z' element
mod$model_data$Z
# The first two series share an identical latent trend; the third is different
plot(residual_cor(mod))
plot(mod, type = "trend", series = 1)
plot(mod, type = "trend", series = 2)
plot(mod, type = "trend", series = 3)
# Example of how to use dynamic coefficients
# Simulate a time-varying coefficient for the effect of temperature
set.seed(123)
N <- 200
beta_temp <- vector(length = N)
beta_temp[1] <- 0.4
for (i in 2:N) {
beta_temp[i] <- rnorm(1, mean = beta_temp[i - 1] - 0.0025, sd = 0.05)
}
plot(beta_temp)
# Simulate a covariate called 'temp'
temp <- rnorm(N, sd = 1)
# Simulate some noisy Gaussian observations
out <- rnorm(N,
mean = 4 + beta_temp * temp,
sd = 0.5
)
# Gather necessary data into a data.frame; split into training / testing
data <- data.frame(out, temp, time = seq_along(temp))
data_train <- data[1:180, ]
data_test <- data[181:200, ]
# Fit the model using the dynamic() function
mod <- mvgam(
formula =
out ~ dynamic(
temp,
scale = FALSE,
k = 40
),
family = gaussian(),
data = data_train,
newdata = data_test,
chains = 2,
silent = 2
)
# Inspect the model summary, forecast and time-varying coefficient distribution
summary(mod)
plot(mod, type = "smooths")
fc <- forecast(mod, newdata = data_test)
plot(fc)
# Propagating the smooth term shows how the coefficient is expected to evolve
plot_mvgam_smooth(mod, smooth = 1, newdata = data)
abline(v = 180, lty = "dashed", lwd = 2)
points(beta_temp, pch = 16)
# Example showing how to incorporate an offset; simulate some count data
# with different means per series
set.seed(100)
dat <- sim_mvgam(
prop_trend = 0, mu = c(0, 2, 2),
seasonality = "hierarchical"
)
# Add offset terms to the training and testing data
dat$data_train$offset <- 0.5 * as.numeric(dat$data_train$series)
dat$data_test$offset <- 0.5 * as.numeric(dat$data_test$series)
# Fit a model that includes the offset in the linear predictor as well as
# hierarchical seasonal smooths
mod <- mvgam(
formula = y ~ offset(offset) +
s(series, bs = "re") +
s(season, bs = "cc") +
s(season, by = series, m = 1, k = 5),
data = dat$data_train,
chains = 2,
silent = 2
)
# Inspect the model file to see the modification to the linear predictor
# (eta)
stancode(mod)
# Forecasts for the first two series will differ in magnitude
fc <- forecast(mod, newdata = dat$data_test)
plot(fc, series = 1, ylim = c(0, 75))
plot(fc, series = 2, ylim = c(0, 75))
# Changing the offset for the testing data should lead to changes in
# the forecast
dat$data_test$offset <- dat$data_test$offset - 2
fc <- forecast(mod, newdata = dat$data_test)
plot(fc)
# Relative Risks can be computed by fixing the offset to the same value
# for each series
dat$data_test$offset <- rep(1, NROW(dat$data_test))
preds_rr <- predict(mod,
type = "link", newdata = dat$data_test,
summary = FALSE
)
series1_inds <- which(dat$data_test$series == "series_1")
series2_inds <- which(dat$data_test$series == "series_2")
# Relative Risks are now more comparable among series
layout(matrix(1:2, ncol = 2))
plot(preds_rr[1, series1_inds],
type = "l", col = "grey75",
ylim = range(preds_rr),
ylab = "Series1 Relative Risk", xlab = "Time"
)
for (i in 2:50) {
lines(preds_rr[i, series1_inds], col = "grey75")
}
plot(preds_rr[1, series2_inds],
type = "l", col = "darkred",
ylim = range(preds_rr),
ylab = "Series2 Relative Risk", xlab = "Time"
)
for (i in 2:50) {
lines(preds_rr[i, series2_inds], col = "darkred")
}
layout(1)
# Example showcasing how cbind() is needed for Binomial observations
# Simulate two time series of Binomial trials
trials <- sample(c(20:25), 50, replace = TRUE)
x <- rnorm(50)
detprob1 <- plogis(-0.5 + 0.9 * x)
detprob2 <- plogis(-0.1 - 0.7 * x)
dat <- rbind(
data.frame(
y = rbinom(n = 50, size = trials, prob = detprob1),
time = 1:50,
series = "series1",
x = x,
ntrials = trials
),
data.frame(
y = rbinom(n = 50, size = trials, prob = detprob2),
time = 1:50,
series = "series2",
x = x,
ntrials = trials
)
)
dat <- dplyr::mutate(dat, series = as.factor(series))
dat <- dplyr::arrange(dat, time, series)
plot_mvgam_series(data = dat, series = "all")
# Fit a model using the binomial() family; must specify observations
# and number of trials in the cbind() wrapper
mod <- mvgam(
formula =
cbind(y, ntrials) ~ series + s(x, by = series),
family = binomial(),
data = dat,
chains = 2,
silent = 2
)
summary(mod)
pp_check(mod,
type = "bars_grouped",
group = "series", ndraws = 50
)
pp_check(mod,
type = "ecdf_overlay_grouped",
group = "series", ndraws = 50
)
conditional_effects(mod, type = "link")
# To view predictions on the probability scale,
# use ntrials = 1 in datagrid()
plot_predictions(
mod,
by = c('x', 'series'),
newdata = datagrid(x = runif(100, -2, 2),
series = unique,
ntrials = 1),
type = 'expected'
)
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.