Skip to contents

Introduction

This vignette details spOccupancy functionality introduced in v0.5.0 to fit single-species spatially-varying coefficient (SVC) models. When fitting models across large spatial domains, it is increasingly likely that the effect of spatial and/or temporal covariates on species occurrence varies across space. Ignoring nonstationarity can result in reduced predictive performance and biased inference on the effect of covariates at different spatial regions (Finley 2011; Rollinson et al. 2021). SVC occupancy models are a flexible extension of spatial occupancy models that allows for not only the intercept to vary across space, but also allows the effects of the covariates themselves to vary spatially, resulting in each spatial location having a unique effect of the covariate (Pease, Pacifici, and Kays 2022). These models can generate key insights into how environmental factors differentially influence species across its range, how temporal trends vary across different parts of a species range, and the relative importance of different covariate effects at different parts of the species range.

Here we introduce four functions for fitting SVC models in spOccupancy. The function svcPGOcc() fits a single-season SVC occupancy model, and is an extension to the basic spatial occupancy model fit by spPGOcc(). The function svcTPGOcc() fits a multi-season SVC occupancy model, which serves as an extension to the spatio-temporal occupancy model fit by stPGOcc() where replicated detection-nondetection data are collected over multiple primary time periods (i.e., breeding seasons, years). Additionally, we include two functions for fitting SVC generalized linear models (GLMs) where we ignore imperfect detection: svcPGBinom() fits a single-season SVC GLM and svcTPGBinom() fits a multi-species GLM. In these functions, we also allow for modeling binomial data instead of the usual binary detection-nondetection data, which may be applicable for certain species or scenarios where imperfect detection may not be as large of an issue (e.g., modeling tree species distributions using a nested plot/subplot design).

As per usual, we use Pólya-Gamma data augmentation to yield a computationally efficient Gibbs sampler for GLM-type models with a logit link function (Polson, Scott, and Windle 2013), and use Nearest Neighbor Gaussian Processes (Datta et al. 2016) in all SVC models to greatly reduce the computational burden encountered when fitting models with spatial random effects formulated as a Gaussian process.

In this vignette, we will walk through each of the four single-species SVC models in spOccupancy, detailing how to fit the models, do model comparison using the Widely Available Information Criterion (WAIC) and k-fold cross-validation, as well as make predictions across an area of interest to generate maps of the spatially-varying coefficients. We will work with simulated data sets and will walk through how to simulate data sets using the spOccupancy functions simOcc(), simTOcc(), simBinom(), and simTBinom(). We will not go into explicit detail for some of the model-fitting function arguments (e.g., priors, initial values) as the syntax is nearly identical to other spOccupancy functions, so we encourage you to first work through the spOccupancy introductory vignette if you are not at least vaguely familiar with spOccupancy syntax.

Below, we first load the spOccupancy package, as well as the ggplot2 package to create some basic plots of our results. We also set a seed so you can reproduce our results.

Spatially-varying coefficients occupancy model

Basic model description

Let \(\boldsymbol{s}_j\) denote the spatial coordinates of site \(j\), where \(j = 1, \dots, J\). We define \(z(\boldsymbol{s}_j)\) as the true presence (1) or absence (0) at site \(j\) with spatial coordinates \(\boldsymbol{s}_j\). We model \(z(\boldsymbol{s}_j)\) as

\[\begin{equation}\label{z} z(\boldsymbol{s}_j) \sim \text{Bernoulli}(\psi(\boldsymbol{s}_j)), \end{equation}\]

where \(\psi(\boldsymbol{s}_j)\) is the occurrence probability at site \(j\). We model \(\psi(\boldsymbol{s}_j)\) according to

\[\begin{equation}\label{psi} \text{logit}(\psi(\boldsymbol{s}_j)) = \textbf{x}(\boldsymbol{s}_j)\boldsymbol{\beta} + \tilde{\textbf{x}}(\boldsymbol{s}_j)\textbf{w}(\boldsymbol{s}_j), \end{equation}\]

where \(\boldsymbol{\beta}\) is a vector of \(q\) regression coefficients (including an intercept) that describe the non-spatial effects of covariates \(\textbf{x}(\boldsymbol{s}_j)\), and \(\textbf{w}(\boldsymbol{s}_j)\) is a vector of \(\tilde{q}\) spatially-varying effects of covariates \(\tilde{\textbf{x}}(\boldsymbol{s}_j)\). Note that \(\tilde{\textbf{x}}(\boldsymbol{s}_j)\) may be identical to \(\textbf{x}(\boldsymbol{s}_j)\) if all covariates effects are assumed to vary spatially, or a subset of \(\textbf{x}(\boldsymbol{s}_j)\) if some effects are assumed to be constant across space. The model reduces to a traditional single-species occupancy model when all covariate effects are assumed constant across space and a spatial occupancy model (Johnson et al. 2013; Doser et al. 2022) when only the intercept is assumed to vary across space.

The spatially-varying effects \(\textbf{w}(\boldsymbol{s}_j)\) serve as local adjustments of the covariate effects at site \(j\) from the overall non-spatial effects \(\boldsymbol{\beta}\), resulting in the covariate having a unique effect on species occurrence at each site \(j\). Following Gelfand et al. (2003), we model each \(r = 1, \dots, \tilde{q}\) spatially-varying effect \(\text{w}_r(\boldsymbol{s}_j)\) using a zero-mean spatial Gaussian process. More specifically, we have

\[\begin{equation}\label{w} \text{$\text{w}_r(\boldsymbol{s})$} \sim N(\boldsymbol{0}, \boldsymbol{C}_r(\boldsymbol{s}, \boldsymbol{s}', \boldsymbol{\theta}_r)), \end{equation}\]

where \(\boldsymbol{C}_r(\boldsymbol{s}, \boldsymbol{s}', \boldsymbol{\theta}_r)\) is a \(J \times J\) covariance matrix that is a function of the distances between any pair of site coordinates \(\boldsymbol{s}\) and \(\boldsymbol{s}'\) and a set of parameters (\(\boldsymbol{\theta}_r\)) that govern the spatial process according to a spatial correlation function. Our associated software implementation in supports four correlation functions: exponential, spherical, Gaussian, and Matern (Banerjee, Carlin, and Gelfand 2003). For the exponential, spherical, and Gaussian correlation functions, \(\boldsymbol{\theta}_r = \{\sigma^2_r, \phi_r\}\), where \(\sigma^2_r\) is a spatial variance parameter and \(\phi_r\) is a spatial decay parameter. Large values of \(\sigma^2_r\) indicate large variation in the magnitude of a covariate effect across space, while values of \(\sigma^2_r\) close to 0 suggests little spatial variability in the magnitude of the effect. \(\phi_r\) controls the range of the spatial dependence in the covariate effect and is inversely related to the spatial range, such that when \(\phi_r\) is small, the covariate effect has a larger range of spatial dependence and varies more smoothly across space compared to larger values of \(\phi_r\). The Matern correlation function has an additional smoothness parameter \(\nu_r\), which provides further flexibility in the smoothness and decay of the spatial process. To avoid the computational burden associated with fitting the full Gaussian process model, we use NNGPs as a computationally efficient and statistically robust alternative (Datta et al. 2016). See the supplemental material in the spOccupancy manuscript and Datta et al. (2016) for more details on NNGPs and their implementations in occupancy models.

To account for imperfect detection in an occupancy modeling framework (MacKenzie et al. 2002, @tyre2003improving), \(k = 1, \dots, K(\boldsymbol{s}_j)\) sampling replicates are obtained at each site \(j\). We model the observed detection (1) or nondetection (0) of a study species at site \(j\), denoted \(y_k(\boldsymbol{s}_j)\), conditional on the true latent occupancy process \(z(\boldsymbol{s}_j)\) following

\[\begin{equation}\label{yDet} y_{k}(\boldsymbol{s}_j) \mid z(\boldsymbol{s}_j) \sim \text{Bernoulli}(p_{k}(\boldsymbol{s}_j)z(\boldsymbol{s}_j)), \end{equation}\]

where \(p_k(\boldsymbol{s})\) is the probability of detecting the species at site \(j\) during replicate \(k\) given the species is truly present at the site. We model detection probability as a function of site and/or observation-level covariates according to

\[\begin{equation}\label{pDet} \text{logit}(p_{k}(\boldsymbol{s}_j)) = \boldsymbol{v}_{k}(\boldsymbol{s}_j)\boldsymbol{\alpha}, \end{equation}\]

where \(\boldsymbol{\alpha}\) is a vector of regression coefficients (including an intercept) that describe the effect of site and/or observation covariates \(\boldsymbol{v}_{k}(\boldsymbol{s}_j)\) on detection.

To complete the Bayesian specification of the model, we assign Gaussian priors to the non-spatial occurrence and detection regression coefficients, an inverse-Gamma or uniform prior to the spatial variance parameters, and a uniform prior for the spatial decay (and smoothness if using a Matern correlation function) parameters.

Simulating data with simOcc()

The function simOcc() simulates single-species detection-nondetection data. simOcc() has the following arguments.

simOcc(J.x, J.y, n.rep, beta, alpha, psi.RE = list(), 
       p.RE = list(), sp = FALSE, svc.cols = 1, cov.model, 
       sigma.sq, phi, nu, x.positive = FALSE, ...)

J.x and J.y indicate the number of spatial locations to simulate data along a horizontal and vertical axis, respectively, such that J.x * J.y is the total number of sites (i.e., J). n.rep is a numeric vector of length J that indicates the number of replicates at each of the J sites (denoted as K in the previous model description). beta and alpha are numeric vectors containing the intercept and any regression coefficient parameters for the occurrence and detection portions of the occupancy model, respectively. psi.RE and p.RE are lists that are used to specify random intercepts on occurrence and detection, respectively. These are only specified when we want to simulate data with unstructured random intercepts. Each list should be comprised of two tags: levels, a vector that specifies the number of levels for each random effect included in the model, and sigma.sq.psi or sigma.sq.p, which specify the variances of the random effects for each random effect included in the model. sp is a logical value indicating whether to simulate data with at least one spatial Gaussian process for either the intercept or some of the occupancy covariate effects. svc.cols is a numeric vector indicating which of the covariates (including the intercept) are generated with spatially-varying effects. By default, svc.cols = 1, which corresponds to a spatially-varying intercept when sp = TRUE (i.e., a spatial occupancy model). cov.model specifies the covariance function used to model the spatial dependence structure, with supported values of exponential, matern, spherical, and gaussian. Finally, sigma.sq is the spatial variance parameter, phi is the spatial range parameter, and nu is the spatial smoothness parameter (only applicable when cov.model = 'matern'). Note that sigma.sq, phi, and nu should have the same length as the number of spatially-varying effects specified in svc.cols. Lastly, the x.positive argument indicates whether or not the occupancy covariates should be simulated to be only positive from a Uniform(0, 1) distribution (TRUE) or both positive and negative and simulated from a Normal(0, 1) distribution (FALSE).

Below we simulate data across 1600 sites with anywhere between 1-4 replicates at a given site, a single covariate effect on occurrence, and a single covariate effect on detection. We assume both the occupancy intercept and the effect of the covariate vary across space, so we set svc.cols = c(1, 2). We use a spherical correlation function. We do not include any unstructured random effects on occurrence or detection.

J.x <- 40
J.y <- 40
# Total number of sites
(J <- J.x * J.y)
[1] 1600
# Number of replicates at each site
n.rep <- sample(4:4, J, replace = TRUE)
# Intercept and covariate effect on occurrence
# Note these are the non-spatial effects. 
beta <- c(-0.5, -0.2)
# Intercept and covariate effect on detection
alpha <- c(0.9, -0.3)
# No unstructured random intercept on occurrence
psi.RE <- list() 
# No unstructured random intercept on detection
p.RE <- list()
# Spatial range for intercept and covariate effect
phi <- c(3 / .8, 3 / .7)
# Spatial variance for intercept and covariate effect
sigma.sq <- c(1, 0.5)
# Simulate the occupancy covariate from a Normal(0, 1) distribution
x.positive <- FALSE 
# Spatially-varying coefficient columns
svc.cols <- c(1, 2)
# Simulate the data
dat <- simOcc(J.x = J.x, J.y = J.y, n.rep = n.rep, beta = beta, 
              alpha = alpha, psi.RE = psi.RE, p.RE = p.RE, 
              sp = TRUE, sigma.sq = sigma.sq, phi = phi, 
              cov.model = 'spherical', svc.cols = svc.cols, 
              x.positive = x.positive)

Next, let’s explore the simulated data a bit before we move on (plotting code adapted from Hooten and Hefley (2019)).

str(dat)
List of 13
 $ X         : num [1:1600, 1:2] 1 1 1 1 1 1 1 1 1 1 ...
 $ X.p       : num [1:1600, 1:4, 1:2] 1 1 1 1 1 1 1 1 1 1 ...
 $ coords    : num [1:1600, 1:2] 0 0.0256 0.0513 0.0769 0.1026 ...
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : NULL
  .. ..$ : chr [1:2] "Var1" "Var2"
 $ w         : num [1:1600, 1:2] -1.184 -1.456 -0.633 -0.124 -0.206 ...
 $ psi       : num [1:1600, 1] 0.144 0.186 0.246 0.36 0.344 ...
 $ z         : int [1:1600] 0 0 0 0 1 1 0 0 0 0 ...
 $ p         : num [1:1600, 1:4] 0.672 0.738 0.581 0.737 0.697 ...
 $ y         : int [1:1600, 1:4] 0 0 0 0 1 1 0 0 0 0 ...
 $ X.p.re    : logi NA
 $ X.re      : logi NA
 $ X.w       : num [1:1600, 1:2] 1 1 1 1 1 1 1 1 1 1 ...
 $ alpha.star: logi NA
 $ beta.star : logi NA

The simulated data object consists of the following objects: X (the occurrence design matrix), X.p (the detection design matrix), coords (the spatial coordinates of each site), w (the latent spatial process for any covariates (and intercept) whose effects vary across space), psi (occurrence probability), z (the latent occupancy status), y (the detection-nondetection data), X.w (the design matrix for the spatially-varying coefficients), X.p.re (the detection random effect levels for each site), X.re (the occurrence random effect levels for each site), alpha.star (the detection random effects for each level of the random effect), beta.star (the occurrence random effects for each level of the random effect). Note because we did not include any unstructured effects on detection or occurrence, the objects associated with the unstructured random effects all have a value of NA.

# Detection-nondetection data
y <- dat$y
# Occurrence design matrix for fixed effects
X <- dat$X
# Detection design matrix for fixed effets
X.p <- dat$X.p
# Occurrence values
psi <- dat$psi
# Spatial coordinates
coords <- dat$coords
# Spatially-varying intercept and covariate effects
w <- dat$w
# Simple plot of the occurrence probability across space.
# Dark points indicate high occurrence. 
plot(coords, type = "n", xlab = "", ylab = "", asp = TRUE, 
     main = "Simulated Occurrence", bty = 'n')
points(coords, pch=15, cex = 2.1, col = rgb(0,0,0,(psi-min(psi))/diff(range(psi))))

We see there is clear variation in occurrence probability across the simulated spatial region.

We can also visualize the spatially-varying intercept and spatially-varying covariate effect. We do that by adding the spatial component of the intercept and covariate effect (stored in the w matrix) to the non-spatial components of the intercept and covariate effect (stored in beta), and then visualizing using the same code as before

# Intercept
int.effect <- beta[1] + w[, 1]
cov.effect <- beta[2] + w[, 2]
# Dark points indicate more positive effects, white points indicate more negative effects.
plot(coords, type = "n", xlab = "", ylab = "", asp = TRUE, main = "Intercept", 
     bty = 'n')
points(coords, pch=15, cex = 2.1, 
       col = rgb(0,0,0,(int.effect-min(int.effect))/diff(range(int.effect))))

plot(coords, type = "n", xlab = "", ylab = "", asp = TRUE, main = "Covariate Effect", 
     bty = 'n')
points(coords, pch=15, cex = 2.1, 
       col = rgb(0,0,0,(cov.effect-min(cov.effect))/diff(range(cov.effect))))

Note the spatial intercept corresponds fairly closely with the map of occurrence probability, which makes sense.

The final step before we can fit the model is to package up the data in a list for use in spOccupancy model fitting functions. This requires creating a list that consists of the detection-nondetection data (y), occurrence covariates (occ.covs), detection covariates (det.covs), and coordinates (coords). See the introductory vignette for more details. For our example here (and throughout the vignette), we will fit the model to 75% of the data points (1200 locations) and subsequently predict at the remaining 400 values to show the predictive ability of the model.

# Subset data for prediction. 
# Split into fitting and prediction data set
pred.indx <- sample(1:J, round(J * .25), replace = FALSE)
y.fit <- y[-pred.indx, ]
y.pred <- y[pred.indx, ]
X.fit <- X[-pred.indx, ]
X.pred <- X[pred.indx, ]
X.p.fit <- X.p[-pred.indx, , ]
X.p.pred <- X.p[pred.indx, , ]
coords.fit <- coords[-pred.indx, ]
coords.pred <- coords[pred.indx, ]
psi.fit <- psi[-pred.indx]
psi.pred <- psi[pred.indx]
w.fit <- w[-pred.indx, ]
w.pred <- w[pred.indx, ]

# Package all data into a list
# Occurrence covariates
occ.covs <- X.fit[, 2, drop = FALSE]
colnames(occ.covs) <- c('occ.cov.1')
# Detection covariates
det.covs <- list(det.cov.1 = X.p.fit[, , 2])
# Package into a list for spOccupancy
data.list <- list(y = y.fit,
                  occ.covs = occ.covs,
                  det.covs = det.covs,
                  coords = coords.fit)
# Take a look at the data structure.
str(data.list)
List of 4
 $ y       : int [1:1200, 1:4] 0 0 0 1 1 0 0 0 0 1 ...
 $ occ.covs: num [1:1200, 1] 0.536 1.828 0.125 -0.364 1.433 ...
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : NULL
  .. ..$ : chr "occ.cov.1"
 $ det.covs:List of 1
  ..$ det.cov.1: num [1:1200, 1:4] 0.612 -0.446 1.916 0.226 -0.813 ...
 $ coords  : num [1:1200, 1:2] 0 0.0256 0.0513 0.1026 0.1282 ...
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : NULL
  .. ..$ : chr [1:2] "Var1" "Var2"

Fitting spatially-varying coefficients occupancy models with svcPGOcc()

The function svcPGOcc() fits single-season SVC occupancy models. svcPGOcc() has the following arguments:

svcPGOcc(occ.formula, det.formula, data, inits, priors, 
         tuning, svc.cols = 1, cov.model = "exponential", NNGP = TRUE, 
         n.neighbors = 15, search.type = "cb", n.batch,
         batch.length, accept.rate = 0.43, 
         n.omp.threads = 1, verbose = TRUE, n.report = 100, 
         n.burn = round(.10 * n.batch * batch.length), 
         n.thin = 1, n.chains = 1, k.fold, k.fold.threads = 1, 
         k.fold.seed = 100, k.fold.only = FALSE, ...)

The arguments to svcPGOcc() are identical as those for a spatial occupancy model fit using spPGOcc(), with the addition of the svc.cols argument to specify the SVCs in the model. occ.formula and det.formula contain the R model formulas for the occurrence and detection portions of the occupancy model. Here we fit the model with a single covariate on both occupancy and detection

occ.formula <- ~ occ.cov.1
det.formula <- ~ det.cov.1

The svc.cols argument is used to specify the covariates whose effects are estimated as SVCs. svc.cols can either be a numeric indexing vector with integer numbers corresponding to the order in which you specified covariates in the occ.formula argument, or can be a character vector with the names of the covariates specified in occ.formula. Note that by default, svc.cols = 1, which is equivalent to fitting a spatial occupancy model with spPGOcc(). This clearly shows how SVC occupancy models are a simple extension of a spatial occupancy model. A spatial occupancy model is simply an SVC occupancy model where the only spatially varying “covariate” is the intercept. If specifying svc.cols as a character vector, use '(Intercept)' to specify a spatially-varying intercept. Here we set svc.cols to include a spatially-varying intercept and spatially-varying effect of the occurrence covariate. We also specify the cov.model argument to indicate we will use a spherical correlation function (note spOccupancy uses the same correlation function for all SVCs). Usually, our default correlation function is the exponential, which is what we use throughout most of the other vignettes, but here we use a spherical correlation function to show how spOccupancy can handle these other functions.

svc.cols <- c(1, 2)
# OR
# svc.cols <- c('(Intercept)', 'occ.cov.1')
cov.model <- 'spherical'

We next specify the initial values, which are specified in a list analogous to a spatial occupancy model using spPGOcc, with the only difference being that if you supply initial values for the spatial random effects w, these must be specified as a two-dimensional matrix with rows corresponding to SVC and column corresponding to site.

dist.data <- dist(data.list$coords)
inits.list <- list(alpha = 0, beta = 0, sigma.sq = 0.5,
                   phi = 3 / mean(dist.data), 
                   z = apply(data.list$y, 1, max, na.rm = TRUE), 
                   w = matrix(0, length(svc.cols), nrow(data.list$y)))

We next specify the priors to use for all parameters in the model (alternatively, we could not specify priors and simply use the default values svcPGOcc() provides). We will use the default normal priors for the occurrence (beta) and detection (alpha) regression coefficients. For the spatial decay parameter (phi), we specify a uniform prior with bounds based on the maximum and minimum inter-site distances. Our default prior for phi is to set the lower bound to 3 / max and upper bound to 3 / min, where max and min are the maximum and minimum inter-site distances, respectively. This results in a prior that states the effective spatial range is anywhere between the maximum distance between sites and the smallest distance between sites. Lastly, we specify an inverse-Gamma prior for sigma.sq. Following Banerjee, Carlin, and Gelfand (2003), we generally will set the scale parameter of the inverse-Gamma to 2 and the shape parameter to our buest guess of the spatial variance. We could also specify a uniform prior for the spatial variance parameter. For binary data, very large values of sigma.sq can result in undesirable and unrealistic values of the spatial random effects on the logit scale, and so a uniform prior can be used to restrict sigma.sq to some maximum value (e.g., 5) that is reasonable on the logit scale (Wright et al. 2021).

priors.list <- list(alpha.normal = list(mean = 0, var = 2.72), 
                    beta.normal = list(mean = 0, var = 2.72),
                    sigma.sq.ig = list(a = 2, b = 0.5),
                    phi.unif = list(a = 3 / max(dist.data), 
                                    b = 3 / min(dist.data)))

The next three arguments (n.batch, batch.length, and accept.rate) are all related to the Adaptive MCMC sampler used when we fit the model. Updates for all parameters with a uniform prior (in this case the spatial decay parameter phi and the spatial variance parameter sigma.sq) require the use of a Metropolis-Hastings algorithm. We implement an adaptive Metropolis-Hastings algorithm as discussed in Roberts and Rosenthal (2009). This algorithm adjusts the tuning values for each parameter that requires a Metropolis-Hastings update within the sampler itself. This process results in a more efficient sampler than if we were to fix the tuning parameters prior to fitting the model. The parameter accept.rate is the target acceptance rate for each parameter, and the algorithm will adjust the tuning parameters to hover around this value. The default value is 0.43, which we suggest leaving as is unless you have a good reason to change it. The tuning parameters are updated after a single “batch”. We break up the total number of MCMC samples into a set of “batches”, where each batch has a specific number of samples. We must specify both the total number of batches (n.batch) as well as the number of MCMC samples each batch contains (batch.length). Thus, the total number of MCMC samples is n.batch * batch.length. Typically, we set batch.length = 25 and then play around with n.batch until convergence is reached. Here we set n.batch = 800 for a total of 20000 MCMC samples. We will additionally specify a burn-in period of length 10000 and a thinning rate of 10. We run the model for 3 chains, ultimately resulting in 3000 posterior samples. Importantly, we also need to specify an initial value for the tuning parameters for the spatial decay parameter, spatial variance parameter if using a uniform prior for sigma.sq, and the smoothness parameter (if cov.model = 'matern'). These values are supplied as input in the form of a list with tags phi, sigma.sq, and nu. The initial tuning value can be any value greater than 0, but we recommend starting the value out around 0.5. After some initial runs of the model, if you notice the final acceptance rate of a parameter is much larger or smaller than the target acceptance rate (accept.rate), you can then change the initial tuning value to get closer to the target rate. Here we set the initial tuning value for phi to 0.2 after some initial exploratory runs of the model.

batch.length <- 25
n.batch <- 800
n.burn <- 10000
n.thin <- 10
n.chains <- 1
tuning.list <- list(phi = 0.2)

We are now ready to run the model. We set the verbose argument equal to TRUE and the n.report argument to 100 to report progress on the MCMC chain after every 100th batch. Additionally, we fit the model with an NNGP (NNGP = TRUE) using 5 neighbors (n.neighbors = 5). See the supplemental material in Doser et al. (2022) for more information on choosing the number of neighbors in the NNGP approximation.

n.omp.threads <- 1
verbose <- TRUE
n.report <- 100 # Report progress at every 100th batch.
# Approx. run time: 4.2 min
out.svc <- svcPGOcc(occ.formula = occ.formula, 
                    det.formula = det.formula, 
                    data = data.list, 
                    inits = inits.list, 
                    n.batch = n.batch, 
                    batch.length = batch.length, 
                    priors = priors.list, 
                    svc.cols = svc.cols,
                    cov.model = cov.model, 
                    NNGP = TRUE, 
                    n.neighbors = 5,
                    tuning = tuning.list, 
                    n.report = n.report, 
                    n.burn = n.burn, 
                    n.thin = n.thin, 
                    n.chains = n.chains)
----------------------------------------
    Preparing to run the model
----------------------------------------
----------------------------------------
    Building the neighbor list
----------------------------------------
----------------------------------------
Building the neighbors of neighbors list
----------------------------------------
----------------------------------------
    Model description
----------------------------------------
NNGP Occupancy model with Polya-Gamma latent
variable fit with 1200 sites.

Samples per chain: 20000 (800 batches of length 25)
Burn-in: 10000 
Thinning Rate: 10 
Number of Chains: 1 
Total Posterior Samples: 1000 

Number of spatially-varying coefficients: 2 
Using the spherical spatial correlation model.

Using 5 nearest neighbors.

Source compiled with OpenMP support and model fit using 1 thread(s).

Adaptive Metropolis with target acceptance rate: 43.0
----------------------------------------
    Chain 1
----------------------------------------
Sampling ... 
Batch: 100 of 800, 12.50%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     52.0        0.18279
    2       phi     52.0        0.21025
-------------------------------------------------
Batch: 200 of 800, 25.00%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     44.0        0.17917
    2       phi     24.0        0.15268
-------------------------------------------------
Batch: 300 of 800, 37.50%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     40.0        0.15268
    2       phi     20.0        0.17562
-------------------------------------------------
Batch: 400 of 800, 50.00%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     40.0        0.14378
    2       phi     64.0        0.16539
-------------------------------------------------
Batch: 500 of 800, 62.50%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     52.0        0.14378
    2       phi     40.0        0.18648
-------------------------------------------------
Batch: 600 of 800, 75.00%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     32.0        0.13815
    2       phi     32.0        0.18648
-------------------------------------------------
Batch: 700 of 800, 87.50%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     44.0        0.15268
    2       phi     44.0        0.28381
-------------------------------------------------
Batch: 800 of 800, 100.00%

We can take a look at the model results using the summary() function and compare them to the true values we used to simulate the data.

summary(out.svc)

Call:
svcPGOcc(occ.formula = occ.formula, det.formula = det.formula, 
    data = data.list, inits = inits.list, priors = priors.list, 
    tuning = tuning.list, svc.cols = svc.cols, cov.model = cov.model, 
    NNGP = TRUE, n.neighbors = 5, n.batch = n.batch, batch.length = batch.length, 
    n.report = n.report, n.burn = n.burn, n.thin = n.thin, n.chains = n.chains)

Samples per Chain: 20000
Burn-in: 10000
Thinning Rate: 10
Number of Chains: 1
Total Posterior Samples: 1000
Run Time (min): 8.0548

Occurrence (logit scale): 
               Mean     SD    2.5%     50%   97.5% Rhat ESS
(Intercept) -0.4291 0.1995 -0.7904 -0.4434 -0.0070   NA  91
occ.cov.1   -0.1082 0.2259 -0.5427 -0.1135  0.4487   NA  16

Detection (logit scale): 
               Mean     SD    2.5%     50%   97.5% Rhat  ESS
(Intercept)  0.8751 0.0701  0.7384  0.8734  1.0106   NA 1000
det.cov.1   -0.2621 0.0688 -0.3968 -0.2606 -0.1325   NA 1000

Spatial Covariance: 
                       Mean     SD   2.5%    50%  97.5% Rhat ESS
sigma.sq-(Intercept) 1.5021 0.4981 0.8141 1.4217 2.7294   NA  41
sigma.sq-occ.cov.1   0.5076 0.1856 0.2048 0.4835 0.9883   NA  50
phi-(Intercept)      6.9408 1.4537 4.1067 6.8634 9.8749   NA  52
phi-occ.cov.1        5.2571 2.0523 2.2056 5.1009 9.8257   NA  18
# True values
beta
[1] -0.5 -0.2
alpha
[1]  0.9 -0.3
sigma.sq
[1] 1.0 0.5
phi
[1] 3.750000 4.285714

Because we only ran one chain of the model, we see the Rhat values are reported as NA. For a complete analysis, we would run the model for multiple chains, make sure the Rhat values are less than 1.1, and also ensure the effective sample sizes are adequately large. Here, the ESS values are somewhat low for the occurrence parameters and spatial covariance parameters, but we will continue interpreting the results for our exploratory purposes here. We see our model does a good job of recovering the true occurrence and detection regression coefficient values. The spatial variance parameters are also quite close to the estimated values. The spatial decay parameter value for the intercept is a bit larger than the simulated value. The spatial decay parameters are only weakly identifiable (i.e., there is very little information to estimate them), and thus estimating their true values can be a difficult task, in particular when fitting a model with multiple SVCs. Generally, we do not attempt to interpret the spatial decay parameters when fitting spatially-explicit occupancy models. Instead, we will often interpret the actual estimated spatial process values at each location, which are of particular interest in spatially-varying coefficient models.

Next, let’s take a look at the resulting objects contained in the out.svc list.

names(out.svc)
 [1] "rhat"           "beta.samples"   "alpha.samples"  "theta.samples" 
 [5] "coords"         "z.samples"      "X"              "X.re"          
 [9] "X.w"            "w.samples"      "psi.samples"    "like.samples"  
[13] "X.p"            "X.p.re"         "y"              "ESS"           
[17] "call"           "n.samples"      "n.neighbors"    "cov.model.indx"
[21] "svc.cols"       "type"           "n.post"         "n.thin"        
[25] "n.burn"         "n.chains"       "pRE"            "psiRE"         
[29] "run.time"      

The resulting model object contains a variety of things, most of which are just used in subsequent functions for posterior predictive checks, prediction, and summarization. The objects that end in “samples” are the posterior MCMC samples for the different objects. See ?svcPGOcc for more information.

To extract the estimates of the spatially-varying coefficients at each of the spatial locations in the data set used to fit the model, we need to combine the non-spatial component of the coefficient (contained in out.svc$beta.samples) and the spatial component of the coefficient (contained in out.svc$w.samples). Recall that in an SVC occupancy model, the total effect of a covariate at any given location is the sum of the non-spatial effect and the adjustment of the effect at that specific location. We provide the function getSVCSamples() to extract the SVCs at each location.

svc.samples <- getSVCSamples(out.svc) 
str(svc.samples)
List of 2
 $ (Intercept): 'mcmc' num [1:1000, 1:1200] -1.497 -0.158 -0.62 -0.127 -0.355 ...
  ..- attr(*, "mcpar")= num [1:3] 1 1000 1
 $ occ.cov.1  : 'mcmc' num [1:1000, 1:1200] 0.6315 -0.4277 -0.7302 -0.6828 0.0796 ...
  ..- attr(*, "mcpar")= num [1:3] 1 1000 1

The resulting object, here called svc.samples, is a list with each component corresponding to a matrix of the MCMC samples of each spatially-varying coefficient estimated in the model, with rows corresponding to MCMC sample and column corresponding to site.

Below we plot the true SVCs for the covariate at the 1200 locations used for fitting the model compared to the mean estimates from our model.

# Get true covariate values at the locations used to fit the mdoel
cov.effect.fit <- beta[2] + w.fit[, 2]
# Get mean values of the SVC for the covariate
svc.cov.mean <- apply(svc.samples$occ.cov.1, 2, mean)
# Dark points indicate more positive effects, white points 
# indicate more negative effects.
plot(coords.fit, type = "n", xlab = "", ylab = "", asp = TRUE, 
     main = "True values", bty = 'n')
points(coords.fit, pch=15, cex = 2.1, 
       col = rgb(0,0,0,(cov.effect.fit-min(cov.effect.fit))/diff(range(cov.effect.fit))))

plot(coords.fit, type = "n", xlab = "", ylab = "", asp = TRUE, 
     main = "Estimated values", bty = 'n')
points(coords.fit, pch=15, cex = 2.1, 
       col = rgb(0,0,0,(svc.cov.mean-min(svc.cov.mean))/diff(range(svc.cov.mean))))

Note that because we held out 400 random values across the study area, some of the white squares in the above image correspond to locations where we did not have any data (we will subsequently predict at these locations). We see our estimates align pretty closely with the true values used to simulate the data. Our mean estimates are smoother than the values used to generate the data. This is what we would expect, as the true values are a single instance of a simulated spatial process, whereas the mean values we have plotted average across individual instances to generate a more smoothed estimate. Overall, the model seems to accurately identify locations of low and high effects of the covariate.

Posterior Predictive Checks

The spOccupancy function ppcOcc() performs a posterior predictive check for all spOccupancy model objects as an assessment of Goodness of Fit (GoF). The key idea of GoF testing is that a good model should generate data that closely align with the observed data. If there are large differences in the observed data from the model-generated data, our model is likely not very useful (Hooten and Hobbs 2015). We can use the ppcOcc() and summary() functions to generate a Bayesian p-value as a quick assessment of model fit. A Bayesian p-value that hovers around 0.5 indicates adequate model fit, while values less than 0.1 or greater than 0.9 suggest our model does not fit the data well. See the introductory spOccupancy vignette and help page for ppcOcc() for more details. Below we perform a posterior predictive check with the Freeman-Tukey statistic, grouping the data by individual sites.

ppc.out <- ppcOcc(out.svc, fit.stat = 'freeman-tukey', group = 1)
summary(ppc.out)

Call:
ppcOcc(object = out.svc, fit.stat = "freeman-tukey", group = 1)

Samples per Chain: 20000
Burn-in: 10000
Thinning Rate: 10
Number of Chains: 1
Total Posterior Samples: 1000

Bayesian p-value:  0.496 
Fit statistic:  freeman-tukey 

We see our Bayesian p-value is very close to the optimal 0.5, suggesting adequate model fit (it would be a bit concerning if we didn’t see a good model fit here since we are using simulated data!).

Model Selection using WAIC

The spOccupancy function waicOcc() calculates the Widely Applicable Information Criterion (WAIC) for all spOccupancy fitted model objects. The WAIC is a fully Bayesian information criterion that is adequate for comparing a set of hierarchical models and selecting the best-performing model for final analysis (see the introductory spOccupancy vignette for more details). Smaller values of WAIC indicate a better performing model. Below, we fit a spatial occupancy model without the SVC of the covariate effect using the spOcc() function (we could equivalently do this by using svcPGOcc() and setting svc.cols = 1).

# Using default priors and initial values.
# Approx. run time: 4.2 min
out.sp <- spPGOcc(occ.formula = occ.formula, 
                  det.formula = det.formula, 
                  data = data.list, 
                  n.batch = n.batch, 
                  batch.length = batch.length, 
                  cov.model = cov.model, 
                  NNGP = TRUE, 
                  n.neighbors = 5,
                  tuning = tuning.list, 
                  n.report = n.report, 
                  n.burn = n.burn, 
                  n.thin = n.thin, 
                  n.chains = n.chains)
----------------------------------------
    Preparing to run the model
----------------------------------------
No prior specified for beta.normal.
Setting prior mean to 0 and prior variance to 2.72
No prior specified for alpha.normal.
Setting prior mean to 0 and prior variance to 2.72
No prior specified for phi.unif.
Setting uniform bounds based on the range of observed spatial coordinates.
No prior specified for sigma.sq.
Using an inverse-Gamma prior with the shape parameter set to 2 and scale parameter to 1.
z.inits is not specified in initial values.
Setting initial values based on observed data
beta is not specified in initial values.
Setting initial values to random values from the prior distribution
alpha is not specified in initial values.
Setting initial values to random values from the prior distribution
phi is not specified in initial values.
Setting initial value to random value from the prior distribution
sigma.sq is not specified in initial values.
Setting initial value to random value from the prior distribution
w is not specified in initial values.
Setting initial value to 0
----------------------------------------
    Building the neighbor list
----------------------------------------
----------------------------------------
Building the neighbors of neighbors list
----------------------------------------
----------------------------------------
    Model description
----------------------------------------
NNGP Occupancy model with Polya-Gamma latent
variable fit with 1200 sites.

Samples per chain: 20000 (800 batches of length 25)
Burn-in: 10000 
Thinning Rate: 10 
Number of Chains: 1 
Total Posterior Samples: 1000 

Using the spherical spatial correlation model.

Using 5 nearest neighbors.

Source compiled with OpenMP support and model fit using 1 thread(s).

Adaptive Metropolis with target acceptance rate: 43.0
----------------------------------------
    Chain 1
----------------------------------------
Sampling ... 
Batch: 100 of 800, 12.50%
    Parameter   Acceptance  Tuning
    phi     44.0        0.15576
-------------------------------------------------
Batch: 200 of 800, 25.00%
    Parameter   Acceptance  Tuning
    phi     48.0        0.16212
-------------------------------------------------
Batch: 300 of 800, 37.50%
    Parameter   Acceptance  Tuning
    phi     32.0        0.16212
-------------------------------------------------
Batch: 400 of 800, 50.00%
    Parameter   Acceptance  Tuning
    phi     40.0        0.14378
-------------------------------------------------
Batch: 500 of 800, 62.50%
    Parameter   Acceptance  Tuning
    phi     60.0        0.15891
-------------------------------------------------
Batch: 600 of 800, 75.00%
    Parameter   Acceptance  Tuning
    phi     36.0        0.16873
-------------------------------------------------
Batch: 700 of 800, 87.50%
    Parameter   Acceptance  Tuning
    phi     40.0        0.16873
-------------------------------------------------
Batch: 800 of 800, 100.00%
# WAIC for the SVC model
waicOcc(out.svc)
     elpd        pD      WAIC 
-1185.427   151.770  2674.394 
# WAIC for the spatial occupancy model
waicOcc(out.sp)
      elpd         pD       WAIC 
-1232.0892   118.5872  2701.3528 

We see the WAIC for the SVC occupancy model is lower than the WAIC for the spatial occupancy model, indicating the SVC model has improved model performance.

Prediction

Finally, we can use the predict() function with all spOccupancy model-fitting functions to generate a series of posterior predictive samples at new locations (as well as already sampled locations), given a set of covariates and their spatial locations. Note that we can predict both new occupancy values as well as new detection values.

Below we predict occupancy probability at the 400 locations we held out when fitting the model. The predict() function for svcPGOcc() objects requires the model object, the design matrix of covariates at the new locations (including the intercept if specified in the model), and the spatial coordinates of the new locations. Below we predict across the 400 “new” locations and plot them in comparison to the true values we used to simulate the data.

# Take a look at X.pred, the design matrix for the prediction locations
head(X.pred)
     [,1]       [,2]
[1,]    1  1.1569942
[2,]    1 -0.8225662
[3,]    1 -1.2718065
[4,]    1  2.6700613
[5,]    1 -0.4581872
[6,]    1  0.1231113
# Predict occupancy at the 400 new sites
out.pred <- predict(out.svc, X.pred, coords.pred)
----------------------------------------
    Prediction description
----------------------------------------
NNGP Occupancy model with Polya-Gamma latent
variable fit with 1200 observations.

Number of covariates 2 (including intercept if specified).

Number of spatially-varying covariates 2 (including intercept if specified).

Using the spherical spatial correlation model.

Using 5 nearest neighbors.

Number of MCMC samples 1000.

Predicting at 400 non-sampled locations.


Source compiled with OpenMP support and model fit using 1 threads.
-------------------------------------------------
        Predicting
-------------------------------------------------
Location: 100 of 400, 25.00%
Location: 200 of 400, 50.00%
Location: 300 of 400, 75.00%
Location: 400 of 400, 100.00%
Generating latent occupancy state
# Use the getSVCSamples() function to extract the SVC values
# at the prediction locations
svc.pred.samples <- getSVCSamples(out.svc, pred.object = out.pred) 
# True covariate effect values at new locations
cov.effect.pred <- beta[2] + w.pred[, 2]
# Get mean values of the SVC for the covariate
svc.cov.pred.mean <- apply(svc.pred.samples$occ.cov.1, 2, mean)
# Dark points indicate more positive effects, white points indicate more 
# negative effects.
plot(coords.pred, type = "n", xlab = "", ylab = "", asp = TRUE, 
     main = "True values", bty = 'n')
points(coords.pred, pch=15, cex = 2.1, 
       col = rgb(0,0,0,(cov.effect.pred-min(cov.effect.pred))/
                        diff(range(cov.effect.pred))))

plot(coords.pred, type = "n", xlab = "", ylab = "", asp = TRUE, 
     main = "Estimated values", bty = 'n')
points(coords.pred, pch=15, cex = 2.1, 
       col = rgb(0,0,0,(svc.cov.pred.mean-min(svc.cov.pred.mean))/
                        diff(range(svc.cov.pred.mean))))

We see a pretty close correspondence between the true values and the predicted values.

Spatially-varying coefficients generalized linear model

Accounting for imperfect detection explicitly in an occupancy modeling framework may not be feasible given the data. In particular, single-visit (or nonreplicated) detection-nondetection data are a very common data source due to the extra expenses of doing multiple surveys at a given location. In this case, we cannot separately estimate imperfect detection from occupancy probability in an occupancy modeling framework without making strict assumptions (Lele, Moreno, and Bayne 2012), and so instead we might fit a more standard generalized linear model (GLM).

Additionally, occupancy models are much less frequently used for modeling plant species distributions than animal distributions. While imperfect detection is still prevalent in plant distribution studies (Chen et al. 2013), we may wish to model plant distribution data using a standard generalized linear modeling framework, in particular if we do not have replicate surveys available in our data set. If our inference focuses solely on the effects of covariates on where a species occurs and not the overall estimates of “occupancy probability”, this may be a feasible approach. However, all inferences made from such models that ignore imperfect detection should carefully consider how detection probability may vary across space and/or time, and what consequences this has on the interpretation of the results. For example, if a spatially-varying covariate influences both the probability a species occurs at a location and the probability the species is detected, one would interpret the effect of the covariate as the effect on the confounded process of detection/occupancy probability.

Basic model description

As before, let \(y_k(\boldsymbol{s}_j)\) be the detection (1) or non-detection (0) of our species of interest at site \(j\) with spatial coordinates \(\boldsymbol{s}_j\). When not explicitly accounting for imperfect detection in an occupancy model, we will model the detection-nondetection data directly in a SVC GLM framework. Specifically, we have

\[\begin{equation}\label{yNoDet} y^*(\boldsymbol{s}_j) \sim \text{Binomial}(K(\boldsymbol{s}_j), \psi(\boldsymbol{s}_j)), \end{equation}\]

where \(y^*(\boldsymbol{s}_j) = \sum_{k = 1}^{K(\boldsymbol{s}_j)}y_{k}(\boldsymbol{s}_j)\), \(K(\boldsymbol{s}_j)\) is the number of replicate surveys at site \(j\), and \(\boldsymbol{\psi}(\boldsymbol{s}_j)\) is the occurrence probability at site \(j\). When only one replicate survey is available at each site, the Binomial likelihood in the previous equation reduces to a Bernoulli likelihood. We model \(\psi(\boldsymbol{s}_j)\) as before. Note that we can incorporate spatially-varying covariates in the model of \(\psi(\boldsymbol{s}_j)\) that may influence detection probability of the species, but our estimates of \(\psi(\boldsymbol{s}_j)\) are still interpreted as relative occurrence probabilities.

Simulating data with simBinom()

The function simBinom() simulates single-species detection-nondetection data for which detection is assumed perfect. simBinom() has the following arguments, which are very similar to those we saw previously with simOcc().

simBinom(J.x, J.y, weights, beta, psi.RE = list(), 
         sp = FALSE, svc.cols = 1, cov.model, sigma.sq, phi, nu, 
         x.positive = FALSE, ...)

J.x and J.y indicate the number of spatial locations to simulate data along a horizontal and vertical axis, respectively, such that J.x * J.y is the total number of sites (i.e., J). weights is a numeric vector of length J that indicates the number of Bernoulli trials (replicates) at each of the J sites (denoted as K in the previous model description). beta is a numeric vector containing the intercept and any regression coefficient parameters for the model, respectively. psi.RE is a list that is used to specify unstructured random intercepts, respectively. These are only specified when we want to simulate data with random intercepts. All other arguments are the same as those we saw simOcc().

We simulate data across 1600 sites where we assume there is only a single replicate survey at a given site and a single covariate effect on relative occurrence. We assume both the intercept and the effect of the covariate vary across space, so we set svc.cols = c(1, 2). We use an exponential correlation function. We do not include any unstructured random effects on occurrence or detection.

# Set seed again to get the same data set
set.seed(488)
J.x <- 40
J.y <- 40
# Total number of sites
(J <- J.x * J.y)
[1] 1600
# Number of replicates at each site
weights <- rep(1, J)
# Intercept and covariate effect
# Note these are the non-spatial effects. 
beta <- c(-0.5, -0.2)
# No unstructured random intercepts
psi.RE <- list() 
# Spatial range for intercept and covariate effect
phi <- c(3 / .8, 3 / .7)
# Spatial variance for intercept and covariate effect
sigma.sq <- c(1, 0.5)
# Simulate the covariate from a Normal(0, 1) distribution
x.positive <- FALSE 
# Spatially-varying coefficient columns
svc.cols <- c(1, 2)
# Simulate the data
dat <- simBinom(J.x = J.x, J.y = J.y, weights = weights, beta = beta, 
                psi.RE = psi.RE, sp = TRUE, sigma.sq = sigma.sq, phi = phi, 
                cov.model = 'exponential', svc.cols = svc.cols, 
                x.positive = x.positive)

Next, let’s explore the simulated data a bit before we move on (plotting code adapted from Hooten and Hefley (2019)).

str(dat)
List of 8
 $ X        : num [1:1600, 1:2] 1 1 1 1 1 1 1 1 1 1 ...
 $ coords   : num [1:1600, 1:2] 0 0.0256 0.0513 0.0769 0.1026 ...
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : NULL
  .. ..$ : chr [1:2] "Var1" "Var2"
 $ w        : num [1:1600, 1:2] 0.61481 0.00417 -0.26095 -0.54444 -0.9741 ...
 $ psi      : num [1:1600, 1] 0.539 0.362 0.402 0.338 0.329 ...
 $ y        : int [1:1600] 0 1 0 1 0 0 0 0 1 0 ...
 $ X.re     : logi NA
 $ X.w      : num [1:1600, 1:2] 1 1 1 1 1 1 1 1 1 1 ...
 $ beta.star: logi NA

The simulated data object consists of the following objects: X (the design matrix), coords (the spatial coordinates of each site), w (the latent spatial process for any covariates (and intercept) whose effects vary across space), psi (relative occurrence probability), y (the detection-nondetection data), X.w (the design matrix for the spatially-varying coefficients), X.re (the occurrence random effect levels for each site), beta.star (the random effects for each level of the unstructured random effect). Note because we did not include any unstructured effects, the objects associated with the unstructured random effects all have a value of NA.

# Detection-nondetection data
y <- dat$y
# Design matrix
X <- dat$X
# Occurrence values
psi <- dat$psi
# Spatial coordinates
coords <- dat$coords
# Spatially-varying intercept and covariate effects
w <- dat$w
# Simple plot of the relative occurrence probability across space.
# Dark points indicate high occurrence. 
plot(coords, type = "n", xlab = "", ylab = "", asp = TRUE, 
     main = "Simulated Occurrence", bty = 'n')
points(coords, pch=15, cex = 2.1, col = rgb(0,0,0,(psi-min(psi))/diff(range(psi))))

Lastly, we package the data up in a list for use in spOccupancy model fitting functions. For SVC GLMs, this requires creating a list that consists of the detection-nondetection data (y), covariates (covs), the binomial weights aka the number of replicates (weights), and coordinates (coords). Note that the covariates here are stored in a single matrix object covs rather than split apart into the occurrence (occ.covs) and detection (det.covs) covariates as we do for occupancy models. As we did for the SVC occupancy model, we will fit the model to 75% of the data points (1200 locations) and subsequently predict at the remaining 400 values to show the predictive ability of the model.

# Subset data for prediction. 
# Split into fitting and prediction data set
pred.indx <- sample(1:J, round(J * .25), replace = FALSE)
y.fit <- y[-pred.indx]
y.pred <- y[pred.indx]
X.fit <- X[-pred.indx, ]
X.pred <- X[pred.indx, ]
coords.fit <- coords[-pred.indx, ]
coords.pred <- coords[pred.indx, ]
psi.fit <- psi[-pred.indx]
psi.pred <- psi[pred.indx]
w.fit <- w[-pred.indx, ]
w.pred <- w[pred.indx, ]
weights.fit <- weights[-pred.indx]
weights.pred <- weights[pred.indx]

# Package all data into a list
# Covariates
covs <- X.fit[, 2, drop = FALSE]
colnames(covs) <- c('cov.1')
# Package into a list for spOccupancy
data.list <- list(y = y.fit,
                  covs = covs,
                  coords = coords.fit, 
                  weights = weights.fit)
# Take a look at the data structure.
str(data.list)
List of 4
 $ y      : int [1:1200] 0 1 0 0 0 0 1 1 0 1 ...
 $ covs   : num [1:1200, 1] -0.178 0.791 0.733 -1.758 1.847 ...
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : NULL
  .. ..$ : chr "cov.1"
 $ coords : num [1:1200, 1:2] 0 0.0256 0.0513 0.1026 0.1282 ...
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : NULL
  .. ..$ : chr [1:2] "Var1" "Var2"
 $ weights: num [1:1200] 1 1 1 1 1 1 1 1 1 1 ...

Fitting spatially-varying coefficients generalized linear models with svcPGBinom()

The function svcPGBinom() fits single-season SVC GLMs. svcPGBinom() has the following arguments, which are nearly identical to those we saw with svcPGOcc():

svcPGBinom(formula, data, inits, priors, tuning, svc.cols = 1,
           cov.model = "exponential", NNGP = TRUE,
           n.neighbors = 15, search.type = "cb", n.batch,
           batch.length, accept.rate = 0.43,
           n.omp.threads = 1, verbose = TRUE, n.report = 100,
           n.burn = round(.10 * n.batch * batch.length),
           n.thin = 1, n.chains = 1, k.fold, k.fold.threads = 1,
           k.fold.seed = 100, k.fold.only = FALSE, ...)

The only difference between the arguments of svcPGBinom() and svcPGOcc() is that now we only specify a single formula, formula, since we do not separately model detection probability from occurrence probability. Below we specify the formula, including the single covariate we simulated the data with.

formula <- ~ cov.1

As before, the svc.cols argument specifies the covariates whose effects are estimated as SVCs. Here we set svc.cols to include a spatially-varying intercept and spatially-varying effect of the covariate. We also specify the cov.model argument to indicate we will use an exponential correlation function.

svc.cols <- c(1, 2)
cov.model <- 'exponential'

We next specify the initial values, which is exactly analogous to svcPGOcc(), but we don’t have any initial values for the detection parameters or the latent occupancy values, since we don’t estimate those in an SVC GLM.

dist.data <- dist(data.list$coords)
inits.list <- list(beta = 0, sigma.sq = 0.5,
                   phi = 3 / mean(dist.data), 
                   w = matrix(0, length(svc.cols), length(data.list$y)))

We next specify the priors to use for all parameters in the model, which we set to be the same as those we used for svcPGOcc().

priors.list <- list(beta.normal = list(mean = 0, var = 2.72),
                    sigma.sq.ig = list(a = 2, b = 0.5),
                    phi.unif = list(a = 3 / max(dist.data), 
                                    b = 3 / min(dist.data)))

Finally, we specify the MCMC criteria exactly as we saw previously and then we are all set to run the model.

batch.length <- 25
n.batch <- 800
n.burn <- 10000
n.thin <- 10
n.chains <- 1
tuning.list <- list(phi = 0.2, sigma.sq = 0.2)
n.omp.threads <- 1
verbose <- TRUE
n.report <- 100 # Report progress at every 100th batch.
# Approx. run time: 3.7 min
out.svc <- svcPGBinom(formula = formula, 
                      data = data.list, 
                      inits = inits.list, 
                      n.batch = n.batch, 
                      batch.length = batch.length, 
                      priors = priors.list, 
                      svc.cols = svc.cols,
                      cov.model = cov.model, 
                      NNGP = TRUE, 
                      n.neighbors = 5,
                      tuning = tuning.list, 
                      n.report = n.report, 
                      n.burn = n.burn, 
                      n.thin = n.thin, 
                      n.chains = n.chains)
----------------------------------------
    Preparing to run the model
----------------------------------------
----------------------------------------
    Building the neighbor list
----------------------------------------
----------------------------------------
Building the neighbors of neighbors list
----------------------------------------
----------------------------------------
    Model description
----------------------------------------
Spatial NNGP Binomial model with Polya-Gamma latent
variable fit with 1200 sites.

Samples per chain: 20000 (800 batches of length 25)
Burn-in: 10000 
Thinning Rate: 10 
Number of Chains: 1 
Total Posterior Samples: 1000 

Number of spatially-varying coefficients: 2 
Using the exponential spatial correlation model.

Using 5 nearest neighbors.

Source compiled with OpenMP support and model fit using 1 thread(s).

Adaptive Metropolis with target acceptance rate: 43.0
----------------------------------------
    Chain 1
----------------------------------------
Sampling ... 
Batch: 100 of 800, 12.50%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     36.0        0.18648
    2       phi     24.0        0.22326
-------------------------------------------------
Batch: 200 of 800, 25.00%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     40.0        0.16539
    2       phi     44.0        0.16873
-------------------------------------------------
Batch: 300 of 800, 37.50%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     52.0        0.17562
    2       phi     44.0        0.17917
-------------------------------------------------
Batch: 400 of 800, 50.00%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     24.0        0.22777
    2       phi     28.0        0.17214
-------------------------------------------------
Batch: 500 of 800, 62.50%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     76.0        0.20201
    2       phi     32.0        0.17214
-------------------------------------------------
Batch: 600 of 800, 75.00%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     44.0        0.15891
    2       phi     68.0        0.20609
-------------------------------------------------
Batch: 700 of 800, 87.50%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     48.0        0.19025
    2       phi     36.0        0.19409
-------------------------------------------------
Batch: 800 of 800, 100.00%
# Compare to values used to generate the data
summary(out.svc)

Call:
svcPGBinom(formula = formula, data = data.list, inits = inits.list, 
    priors = priors.list, tuning = tuning.list, svc.cols = svc.cols, 
    cov.model = cov.model, NNGP = TRUE, n.neighbors = 5, n.batch = n.batch, 
    batch.length = batch.length, n.report = n.report, n.burn = n.burn, 
    n.thin = n.thin, n.chains = n.chains)

Samples per Chain: 20000
Burn-in: 10000
Thinning Rate: 10
Number of Chains: 1
Total Posterior Samples: 1000
Run Time (min): 7.0394

Occurrence (logit scale): 
               Mean     SD    2.5%     50%  97.5% Rhat ESS
(Intercept) -0.2728 0.1473 -0.5770 -0.2596 0.0057   NA  34
cov.1        0.0831 0.1470 -0.2261  0.0913 0.3611   NA  22

Spatial Covariance: 
                        Mean      SD   2.5%    50%   97.5% Rhat ESS
sigma.sq-(Intercept)  0.3826  0.1631 0.1793 0.3473  0.8226   NA  31
sigma.sq-cov.1        0.2842  0.1788 0.0846 0.2438  0.8155   NA  14
phi-(Intercept)       6.8393  2.6174 2.8451 6.4441 13.8589   NA  27
phi-cov.1            14.9416 15.3815 2.7489 9.4919 66.0294   NA  10
# True values
beta
[1] -0.5 -0.2
sigma.sq
[1] 1.0 0.5
phi
[1] 3.750000 4.285714

Let’s next extract the full SVC values using the getSVCSamples() function and then compare the estimated values to those used to generate the model. This time, we won’t make a map of the predicted values, but rather will just look at a scatter plot of fitted values vs. true values.

# Intercept ---------------------------------------------------------------
svc.samples <- getSVCSamples(out.svc)
int.quants <- apply(svc.samples[["(Intercept)"]], 2, 
                    quantile, probs = c(0.025, 0.5, 0.975))
svc.true.fit <- beta + w.fit
plot(svc.true.fit[, 1], int.quants[2, ], pch = 19, 
     ylim = c(min(int.quants[1, ]), max(int.quants[3, ])),
     xlab = "True", ylab = "Fit", main = 'Intercept')
abline(0, 1)
arrows(svc.true.fit[, 1], int.quants[2, ], svc.true.fit[, 1], 
       col = 'gray', int.quants[1, ], length = 0.02, angle = 90)
arrows(svc.true.fit[, 1], int.quants[1, ], svc.true.fit[, 1], 
       col = 'gray', int.quants[3, ], length = 0.02, angle = 90)
points(svc.true.fit[, 1], int.quants[2, ], pch = 19)

# Covariate ---------------------------
cov.quants <- apply(svc.samples[["cov.1"]], 2, quantile, 
                    probs = c(0.025, 0.5, 0.975))
svc.true.fit <- beta + w.fit
plot(svc.true.fit[, 2], cov.quants[2, ], pch = 19, 
     ylim = c(min(cov.quants[1, ]), max(cov.quants[3, ])),
     xlab = "True", ylab = "Fit", main = 'Covariate')
abline(0, 1)
arrows(svc.true.fit[, 2], cov.quants[2, ], svc.true.fit[, 2], 
       col = 'gray', cov.quants[1, ], length = 0.02, angle = 90)
arrows(svc.true.fit[, 2], cov.quants[1, ], svc.true.fit[, 2], 
       col = 'gray', cov.quants[3, ], length = 0.02, angle = 90)
points(svc.true.fit[, 2], cov.quants[2, ], pch = 19)

Here we see our estimated values capture the patterns in the simulated values. Notice that the estimated values do not fall exactly on the one to one line, and rather the estimated values are all seemingly closer to zero than the true values, in particular for the sites that have a large magnitude of the effect. This is a common phenomenon in spatial models that use binary data in a GLM framework as a result of the lack of information in binary data as well as the link function (e.g., logit) that transforms binary data to the real scale. Generally, as the number of spatial locations increase and the spatial range increases (i.e., the spatial decay parameter \(\phi\) gets smaller), we will tend to see the values be more closely approximated by the model. Regardless, we generally see good coverage of the 95% credible intervals for the true values (ideally we want 95% of the gray lines to overlap the black one to one line), and the estimated means capture the pattern in the effects across space.

Model selection using WAIC

We can do model selection using WAIC as we saw with svcPGOcc(). Below we fit a spatial GLM that assumes the covariate effect is stationary across the study area. We then compare the two models with WAIC.

out.glm <- svcPGBinom(formula = formula, 
                      data = data.list, 
                      n.batch = n.batch, 
                      batch.length = batch.length, 
                      svc.cols = 1,
                      cov.model = cov.model, 
                      NNGP = TRUE, 
                      n.neighbors = 5,
                      tuning = tuning.list, 
                      n.report = n.report, 
                      n.burn = n.burn, 
                      n.thin = n.thin, 
                      n.chains = n.chains)
----------------------------------------
    Preparing to run the model
----------------------------------------
No prior specified for beta.normal.
Setting prior mean to 0 and prior variance to 2.72
No prior specified for phi.unif.
Setting uniform bounds based on the range of observed spatial coordinates.
No prior specified for sigma.sq.
Using an inverse-Gamma prior with the shape parameter set to 2 and scale parameter to 1.
beta is not specified in initial values.
Setting initial values to random values from the prior distribution
phi is not specified in initial values.
Setting initial value to random values from the prior distribution
sigma.sq is not specified in initial values.
Setting initial values to random values from the prior distribution
w is not specified in initial values.
Setting initial value to 0
----------------------------------------
    Building the neighbor list
----------------------------------------
----------------------------------------
Building the neighbors of neighbors list
----------------------------------------
----------------------------------------
    Model description
----------------------------------------
Spatial NNGP Binomial model with Polya-Gamma latent
variable fit with 1200 sites.

Samples per chain: 20000 (800 batches of length 25)
Burn-in: 10000 
Thinning Rate: 10 
Number of Chains: 1 
Total Posterior Samples: 1000 

Number of spatially-varying coefficients: 1 
Using the exponential spatial correlation model.

Using 5 nearest neighbors.

Source compiled with OpenMP support and model fit using 1 thread(s).

Adaptive Metropolis with target acceptance rate: 43.0
----------------------------------------
    Chain 1
----------------------------------------
Sampling ... 
Batch: 100 of 800, 12.50%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     64.0        0.19801
-------------------------------------------------
Batch: 200 of 800, 25.00%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     24.0        0.17917
-------------------------------------------------
Batch: 300 of 800, 37.50%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     52.0        0.15268
-------------------------------------------------
Batch: 400 of 800, 50.00%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     28.0        0.18279
-------------------------------------------------
Batch: 500 of 800, 62.50%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     52.0        0.16539
-------------------------------------------------
Batch: 600 of 800, 75.00%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     44.0        0.17214
-------------------------------------------------
Batch: 700 of 800, 87.50%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     32.0        0.19025
-------------------------------------------------
Batch: 800 of 800, 100.00%
# SVC model
waicOcc(out.svc)
      elpd         pD       WAIC 
-715.54045   80.42124 1591.92337 
# Spatially-varying intercept model
waicOcc(out.glm)
      elpd         pD       WAIC 
-735.14310   67.59059 1605.46739 

As expected, the SVC GLM outperforms the spatial GLM that assumes a constant covariate effect.

Prediction

Finally, we can use the predict() function just as we saw with svcPGOcc() to predict relative occurrence and the effects of the covariates at new (and old) locations.

# Take a look at X.pred, the design matrix for the prediction locations
head(X.pred)
     [,1]       [,2]
[1,]    1 -1.3248935
[2,]    1  0.4733202
[3,]    1 -0.4613708
[4,]    1  0.6230023
[5,]    1  0.2933181
[6,]    1 -0.5235239
# Predict occupancy at the 400 new sites
out.pred <- predict(out.svc, X.pred, coords.pred, weights.pred)
----------------------------------------
    Prediction description
----------------------------------------
NNGP Occupancy model with Polya-Gamma latent
variable fit with 1200 observations.

Number of covariates 2 (including intercept if specified).

Number of spatially-varying covariates 2 (including intercept if specified).

Using the exponential spatial correlation model.

Using 5 nearest neighbors.

Number of MCMC samples 1000.

Predicting at 400 non-sampled locations.


Source compiled with OpenMP support and model fit using 1 threads.
-------------------------------------------------
        Predicting
-------------------------------------------------
Location: 100 of 400, 25.00%
Location: 200 of 400, 50.00%
Location: 300 of 400, 75.00%
Location: 400 of 400, 100.00%
Generating latent occupancy state
# Use the getSVCSamples() function to extract the SVC values
# at the prediction locations
svc.pred.samples <- getSVCSamples(out.svc, pred.object = out.pred) 
# True covariate effect values at new locations
cov.effect.pred <- beta[2] + w.pred[, 2]
# Get median and 95% CIs of the SVC for the covariate effect
cov.pred.quants <- apply(svc.pred.samples[["cov.1"]], 2, 
                         quantile, probs = c(0.025, 0.5, 0.975))
plot(cov.effect.pred, cov.pred.quants[2, ], pch = 19, 
     ylim = c(min(cov.pred.quants[1, ]), max(cov.pred.quants[3, ])),
     xlab = "True", ylab = "Fit", main = 'Covariate')
abline(0, 1)
arrows(cov.effect.pred, cov.pred.quants[2, ], cov.effect.pred, 
       col = 'gray', cov.pred.quants[1, ], 
       length = 0.02, angle = 90)
arrows(cov.effect.pred, cov.pred.quants[1, ], cov.effect.pred, 
       col = 'gray', cov.pred.quants[3, ], 
       length = 0.02, angle = 90)
points(cov.effect.pred, cov.pred.quants[2, ], pch = 19)

As with the fitted values, we see the predicted values seem to capture the pattern in the true estimates.

Spatially-varying coefficients multi-season occupancy model

Consider the case where detection-nondetection data are collected across multiple seasons (e.g., years, breeding periods) during which the true occupancy status can change. Such spatio-temporal data can be used to understand occurrence trends over time (e.g., Isaac et al. (2014)), as well as the environmental variables that drive spatio-temporal shifts in species distributions (e.g., Rushing et al. (2020)). As an extension to the SVC occupancy model, here we present a SVC multi-season occupancy model that estimates spatially-varying effects (as described previously) of covariates and accounts for temporal autocorrelation using temporal random effects. This model is an extension of the multi-season spatial (stPGOcc()) and non-spatial (tPGOcc()) occupancy models introduced in spOccupancy v0.4.0. See the vignette on multiseason models for additional background information on modeling spatio-temporal patterns in occupancy.

Basic model description

Let \(y_{k, t}(\boldsymbol{s}_j)\) denote the detection or nondetection of the species of interest at site \(j\) during replicate survey \(k\) during time period \(t\), with \(t = 1, \dots, T\). We model \(y_{k, t}(\boldsymbol{s}_j)\) conditional on the true occupancy status, \(z_t(\boldsymbol{s}_j)\) of the species at site \(j\) during time \(t\) according to

\[\begin{equation}\label{zTime} z_{t}(\boldsymbol{s}_j) \sim \text{Bernoulli}(\psi_t(\boldsymbol{s}_j)), \end{equation}\]

where \(\psi_t(\boldsymbol{s}_j)\) is the occurrence probability at site \(j\) during primary time period \(t\). We model \(\psi_{t}(\boldsymbol{s}_j)\) following

\[\begin{equation}\label{psiTime} \text{logit}(\psi_t(\boldsymbol{s}_j)) = \textbf{x}_t(\boldsymbol{s}_j)\boldsymbol{\beta} + \tilde{\textbf{x}}_t(\boldsymbol{s}_j)\textbf{w}(\boldsymbol{s}_j) + \eta_t, \end{equation}\]

where \(\eta_t\) is a random temporal effect and the other parameters are defined as before. Note that the covariates can now vary across space and/or time period. Because we assume the non-spatial (\(\boldsymbol{\beta})\) and spatially-varying (\(\textbf{w}(\boldsymbol{s}_j)\)) coefficients are constant over the \(T\) primary time periods, they represent the average covariate effects across the temporal extent of the data. We model \(\eta_t\) as either an unstructured random effect (i.e., \(\eta_t \sim \text{Normal}(0, \sigma^2_{T})\)) or using a first-order autoregressive (i.e., AR(1)) covariance structure in which we estimate a temporal variance parameter, \(\sigma^2_T\), and a temporal correlation parameter, \(\rho\).

The data \(y_{k, t}(\boldsymbol{s}_j)\) are modeled conditional on the true occupancy status \(z_t(\boldsymbol{s}_j)\) analogous to the SVC occupancy model, with detection probability now allowed to vary across site, replicates, and/or primary time periods.

Simulating data with simTOcc()

The function simTOcc() simulates single-species multi-season detection-nondetection data. simTOcc() has the following arguments, which again closely resemble all other data simulation functions in spOccupancy.

simTOcc(J.x, J.y, n.time, n.rep, beta, alpha, sp.only = 0, trend = TRUE, 
        psi.RE = list(), p.RE = list(), sp = FALSE, svc.cols = 1, cov.model, 
        sigma.sq, phi, nu, ar1 = FALSE, rho, sigma.sq.t, x.positive = FALSE,...)

J.x and J.y indicate the number of spatial locations to simulate data along a horizontal and vertical axis, respectively, such that J.x * J.y is the total number of sites (i.e., J). n.time indicates the number of time periods over which data are collected. n.rep is a numeric matrix with rows corresponding to sites and columns corresponding to time periods where each element corresponds to the number of replicate surveys performed within a given time period at a given site (denoted as K in the previous model description). Note that throughout we will refer to “time period” as the additional temporal replication in multi-season models (often called primary time period) and will use the term “replicate” to refer to the multiple visits obtained at a given site during a given time period (often called secondary time periods). beta and alpha are numeric vectors containing the intercept and any regression coefficient parameters for the occurrence and detection portions of the occupancy model, respectively. sp.only is a numeric vector specifying which occurrence covariates should only vary over space and not over time. trend is a logical value indicating whether or not a trend parameter is included in the model (the trend is assumed to be the second covariate listed in beta). psi.RE and p.RE are lists that are used to specify random intercepts on occurrence and detection, respectively, which are analogous to previous data simulation functions. sp is a logical value indicating whether to simulate data with at least one spatial Gaussian process for either the intercept or some of the occupancy covariate effects. svc.cols is a numeric vector indicating which of the covariates (including the intercept) are generated with spatially-varying effects. cov.model specifies the covariance function used to model the spatial dependence structure. Finally, sigma.sq is the spatial variance parameter, phi is the spatial range parameter, and nu is the spatial smoothness parameter (only applicable when cov.model = 'matern'). Note that sigma.sq, phi, and nu should have the same length as the number of spatially-varying effects specified in svc.cols. Lastly, the x.positive argument indicates whether or not the occupancy covariates should be simulated to be only positive from a Uniform(0, 1) distribution (TRUE) or both positive and negative and simulated from a Normal(0, 1) distribution (FALSE).

For this simulation, we will attempt to estimate a spatially-varying trend parameter. Such an analysis can provide useful information for monitoring programs by allowing for prioritization of locations where occupancy trends are declining. Additionally, it can often help generate hypotheses as to what factors are driving the heterogenous trends across space. Below we simulate data across 400 sites and ten time periods with anywhere between 1-4 replicates at a given time-period, a spatially-varying intercept, a spatially-varying trend on occupancy, and a single covariate effect on detection. Note that we vary the number of time periods each site is sampled. We assume both the occupancy intercept and the trend vary across space, so we set svc.cols = c(1, 2). We use an exponential correlation function. We do not include any unstructured random effects on occurrence or detection.

set.seed(10)
J.x <- 20
J.y <- 20
# Total number of sites
(J <- J.x * J.y)
[1] 400
# Total number of time periods at each site
n.time <- sample(10, J, replace = TRUE)
n.time.max <- max(n.time)
# Number of replicates at each site/time period combination
n.rep <- matrix(NA, J, max(n.time))
for (j in 1:J) {
  n.rep[j, 1:n.time[j]] <- sample(4, n.time[j], replace = TRUE)
}
sp.only <- 0
# Simulate a trend parameter 
trend <- TRUE
# Intercept and trend on occurrence
beta <- c(-0.5, -0.2)
# Intercept and covariate effect on detection
alpha <- c(0.9, -0.3)
# No unstructured random intercept on occurrence
psi.RE <- list() 
# No unstructured random intercept on detection
p.RE <- list()
# Spatial range for intercept and trend 
phi <- c(3 / .8, 3 / .7)
# Spatial variance for intercept and trend
sigma.sq <- c(1, 0.5)
# Simulate data with an AR1 temporal process
ar1 <- TRUE
# Temporal variance of AR1 random effect
sigma.sq.t <- 0.5
# Temporal correlation of AR1 random effect
rho <- 0.25
# Simulate the occupancy covariate from a Normal(0, 1) distribution
x.positive <- FALSE 
# Spatially-varying coefficient columns
svc.cols <- c(1, 2)
# Simulate the data
dat <- simTOcc(J.x = J.x, J.y = J.y, n.time = n.time, n.rep = n.rep, beta = beta, 
               alpha = alpha, sp.only = sp.only, trend = trend, 
               psi.RE = psi.RE, p.RE = p.RE, sp = TRUE, 
               sigma.sq = sigma.sq, phi = phi, 
               cov.model = 'exponential', svc.cols = svc.cols, 
               x.positive = x.positive, ar1 = ar1, sigma.sq.t = sigma.sq.t, 
               rho = rho)

First let’s take a look at the true occupancy values averaged across all spatial locations to see if there appears to be an overall trend over time

str(dat)
List of 13
 $ X         : num [1:400, 1:10, 1:2] 1 1 1 1 1 1 1 1 1 1 ...
 $ X.p       : num [1:400, 1:10, 1:4, 1:2] 1 1 1 1 1 1 1 1 1 1 ...
 $ coords    : num [1:400, 1:2] 0 0.0526 0.1053 0.1579 0.2105 ...
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : NULL
  .. ..$ : chr [1:2] "Var1" "Var2"
 $ psi       : num [1:400, 1:10] 0.535 0.395 0.229 0.465 0.91 ...
 $ z         : int [1:400, 1:10] 1 1 1 1 1 1 0 1 0 0 ...
 $ p         : num [1:400, 1:10, 1:4] 0.658 0.722 0.568 0.741 0.664 ...
 $ y         : int [1:400, 1:10, 1:4] 1 1 0 0 1 1 0 1 0 0 ...
 $ w         : num [1:400, 1:2] -0.188959 -0.000185 -1.212923 -0.730247 0.348285 ...
 $ X.p.re    : logi NA
 $ X.re      : logi NA
 $ alpha.star: logi NA
 $ beta.star : logi NA
 $ eta       : num [1:10, 1] 0.194 0.425 0.587 0.161 1.795 ...
# Plot average occupancy probability
plot(apply(dat$psi, 2, mean), pch = 19, xlab = 'Time Period',
     ylab = 'Average Occupancy Probability',
     cex = 1.5, frame = FALSE, ylim = c(0, 1))

The simulated data object consists of the same objects as we saw previously with simOcc(). However, because of the additional time period dimension, all of the objects (except for spatial coordinates and spatially-varying effects) now have an additional dimension of time period. For example, the occurrence design matrix X is now a three-dimensional array with dimensions corresponding to site, time period, and covariate. We see on average occupancy is decreasing over the simulated ten year period, which makes sense given the average value we used to simulate the data (-0.2).

Below we extract the data we need to fit the model and create a plot of the spatially-varying trend across the study region

# Detection-nondetection data
y <- dat$y
# Occurrence design matrix for fixed effects
X <- dat$X
# Detection design matrix for fixed effets
X.p <- dat$X.p
# Occurrence values
psi <- dat$psi
# Spatial coordinates
coords <- dat$coords
# Spatially-varying intercept and covariate effects
w <- dat$w

cov.effect <- beta[2] + w[, 2]
plot.dat <- data.frame(x = coords[, 1], 
                       y = coords[, 2], 
                       cov.effect = cov.effect)
ggplot(plot.dat, aes(x = x, y = y, fill = cov.effect)) + 
  geom_raster() + 
  scale_fill_gradient2(midpoint = 0, low = '#B2182B', mid = 'white', 
                       high = '#2166AC', na.value = NA) + 
  theme_bw()

We see heterogenous trends across the study area with some locations showing a large decrease in occurrence (red) and others showing a large increase (blue).

We finally package up the data into the data list format for multi-season occupancy models. As before, we will fit the model with 75% of the spatial locations and then predict at the remaining 25% of the locations. With multi-season occupancy models, the occurrence covariates can now vary across both space and time, and so we specify occ.covs as a list rather than as a matrix/data frame as we saw with svcPGOcc(). See the multi-season occupancy model vignette for additional details.

# Subset data for prediction. 
# Split into fitting and prediction data set
pred.indx <- sample(1:J, round(J * .25), replace = FALSE)
y.fit <- y[-pred.indx, , ]
y.pred <- y[pred.indx, , ]
X.fit <- X[-pred.indx, , ]
X.pred <- X[pred.indx, , ]
X.p.fit <- X.p[-pred.indx, , , ]
X.p.pred <- X.p[pred.indx, , , ]
coords.fit <- coords[-pred.indx, ]
coords.pred <- coords[pred.indx, ]
psi.fit <- psi[-pred.indx, ]
psi.pred <- psi[pred.indx, ]
w.fit <- w[-pred.indx, ]
w.pred <- w[pred.indx, ]

# Package all data into a list
# Occurrence covariates
occ.covs <- list(trend = X.fit[, , 2])
# Detection covariates
det.covs <- list(det.cov.1 = X.p.fit[, , , 2])
# Package into a list for spOccupancy
data.list <- list(y = y.fit,
                  occ.covs = occ.covs,
                  det.covs = det.covs,
                  coords = coords.fit)
# Take a look at the data structure.
str(data.list)
List of 4
 $ y       : int [1:300, 1:10, 1:4] 1 1 0 1 0 1 0 0 1 0 ...
 $ occ.covs:List of 1
  ..$ trend: num [1:300, 1:10] -1.57 -1.57 -1.57 -1.57 -1.57 ...
 $ det.covs:List of 1
  ..$ det.cov.1: num [1:300, 1:10, 1:4] 0.818 -0.177 -0.501 0.353 0.701 ...
 $ coords  : num [1:300, 1:2] 0 0.0526 0.1579 0.2632 0.3158 ...
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : NULL
  .. ..$ : chr [1:2] "Var1" "Var2"

Fitting spatially-varying coefficients multi-season occupancy models with svcPGOcc()

The function svcTPGOcc() fits spatially-varyig coefficients multi-season occupancy models. It’s arguments are exactly identical to svcPGOcc(), with the only addition being the ar1 argument.

svcTPGOcc(occ.formula, det.formula, data, inits, priors, 
          tuning, svc.cols = 1, cov.model = 'exponential', NNGP = TRUE, 
          n.neighbors = 15, search.type = 'cb', n.batch, 
          batch.length, accept.rate = 0.43, n.omp.threads = 1, 
          verbose = TRUE, ar1 = FALSE, n.report = 100, 
          n.burn = round(.10 * n.batch * batch.length), 
          n.thin = 1, n.chains = 1, k.fold, k.fold.threads = 1, 
          k.fold.seed = 100, k.fold.only = FALSE, ...)

Given the similarity in the syntax for svcTPGOcc() and svcPGOcc(), we won’t go into all that much detail on them here. For additional details on the input parameters related to the temporal parameters, see the multi-season occupancy model vignette.

The occ.formula and det.formula are specified in the same manner as we saw previously. The data argument contains the list of the formatted data, which we discussed in the preceding section for multi-season models.

data <- data.list
str(data)
List of 4
 $ y       : int [1:300, 1:10, 1:4] 1 1 0 1 0 1 0 0 1 0 ...
 $ occ.covs:List of 1
  ..$ trend: num [1:300, 1:10] -1.57 -1.57 -1.57 -1.57 -1.57 ...
 $ det.covs:List of 1
  ..$ det.cov.1: num [1:300, 1:10, 1:4] 0.818 -0.177 -0.501 0.353 0.701 ...
 $ coords  : num [1:300, 1:2] 0 0.0526 0.1579 0.2632 0.3158 ...
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : NULL
  .. ..$ : chr [1:2] "Var1" "Var2"
occ.formula <- ~ trend
det.formula <- ~ det.cov.1

Next we specify the initial values for all model parameters. We will fit the model with an AR(1) temporal covariance structure, which includes two additional parameters: sigma.sq.t (a temporal variance parameter) and rho (a temporal correlation parameter). We specifiy initial values for z to 1 if the species was ever observed at the given site/time period combination and 0 otherwise.

z.inits <- apply(data$y, c(1, 2), function(a) as.numeric(sum(a, na.rm = TRUE) > 0))
inits <- list(beta = 0, alpha = 0, z = z.inits,
              sigma.sq = 1, phi = 3 / 0.5,
              sigma.sq.t = 1.5, rho = 0.2)

We specify priors in the priors argument just as we saw with svcPGOcc(). We use an inverse-Gamma prior for the temporal variance sigma.sq.t and a uniform prior for the temporal correlation parameter rho.

priors <- list(beta.normal = list(mean = 0, var = 2.72),
               alpha.normal = list(mean = 0, var = 2.72),
               sigma.sq.t.ig = c(2, 0.5),
               rho.unif = c(-1, 1),
               sigma.sq.ig = list(a = 2, b = 1),
               phi.unif = list(a = 3 / 1, b = 3 / .1))

Next we specify parameters associated with the spatial random effects. In particular, we set cov.model = 'exponential' to use an exponential spatial correlation function, n.neighbors = 5 to use an NNGP with 5 nearest neighbors, and svc.cols = c(1, 2) to specify a spatially-varying intercept and trend. We also specify the ar1 argument to indicate we will use an AR(1) temporal covariance structure.

cov.model <- 'exponential'
svc.cols <- c(1, 2)
n.neighbors <- 5
ar1 <- TRUE

Finally, we set the number of MCMC batches, batch length, the amount of burn-in, and our thinning rate.

n.batch <- 600
batch.length <- 25
# Total number of samples
n.batch * batch.length
[1] 15000
n.burn <- 10000
n.thin <- 20

We now run the model with svcTPGOcc() and take a look at a summary of the results using summary().

# Approx. run time: ~ 1.1 min
out.svc.trend <- svcTPGOcc(occ.formula = occ.formula,
                           det.formula = det.formula,
                           data = data,
                           inits = inits,
                           priors = priors,
                           cov.model = cov.model,
                           svc.cols = svc.cols,
                           n.neighbors = n.neighbors,
                           n.batch = n.batch,
                           batch.length = batch.length,
                           verbose = TRUE,
                           ar1 = ar1,
                           n.report = 200,
                           n.burn = n.burn,
                           n.thin = n.thin,
                           n.chains = 1)
----------------------------------------
    Preparing the data
----------------------------------------
----------------------------------------
    Building the neighbor list
----------------------------------------
----------------------------------------
Building the neighbors of neighbors list
----------------------------------------
----------------------------------------
    Model description
----------------------------------------
Spatial NNGP Trend Occupancy Model with Polya-Gamma latent
variable fit with 300 sites and 10 years.

Samples per chain: 15000 (600 batches of length 25)
Burn-in: 10000 
Thinning Rate: 20 
Number of Chains: 1 
Total Posterior Samples: 250 

Number of spatially-varying coefficients: 2 
Using the exponential spatial correlation model.

Using 5 nearest neighbors.

Using an AR(1) temporal autocorrelation matrix.

Source compiled with OpenMP support and model fit using 1 thread(s).

Adaptive Metropolis with target acceptance rate: 43.0
----------------------------------------
    Chain 1
----------------------------------------
Sampling ... 
Batch: 200 of 600, 33.33%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     40.0        0.66365
    2       phi     48.0        0.57695
    1       rho     40.0        1.11628
-------------------------------------------------
Batch: 400 of 600, 66.67%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     40.0        0.63763
    2       phi     44.0        0.53259
    1       rho     48.0        1.13883
-------------------------------------------------
Batch: 600 of 600, 100.00%
summary(out.svc.trend)

Call:
svcTPGOcc(occ.formula = occ.formula, det.formula = det.formula, 
    data = data, inits = inits, priors = priors, svc.cols = svc.cols, 
    cov.model = cov.model, n.neighbors = n.neighbors, n.batch = n.batch, 
    batch.length = batch.length, verbose = TRUE, ar1 = ar1, n.report = 200, 
    n.burn = n.burn, n.thin = n.thin, n.chains = 1)

Samples per Chain: 15000
Burn-in: 10000
Thinning Rate: 20
Number of Chains: 1
Total Posterior Samples: 250
Run Time (min): 1.9582

Occurrence (logit scale): 
               Mean     SD    2.5%     50%  97.5% Rhat ESS
(Intercept) -0.3418 0.3825 -1.0919 -0.3266 0.4991   NA  53
trend       -0.7806 0.3669 -1.3587 -0.8054 0.0165   NA  24

Detection (logit scale): 
               Mean     SD    2.5%     50%   97.5% Rhat ESS
(Intercept)  0.9469 0.0564  0.8448  0.9492  1.0501   NA 250
det.cov.1   -0.3115 0.0515 -0.4147 -0.3087 -0.1962   NA 250

Spatio-temporal Covariance: 
                       Mean     SD    2.5%    50%   97.5% Rhat ESS
sigma.sq-(Intercept) 0.6124 0.2408  0.2654 0.5746  1.1159   NA  86
sigma.sq-trend       0.7903 0.3047  0.3939 0.7391  1.5609   NA  75
phi-(Intercept)      5.8699 2.2306  3.2610 5.2182 11.8320   NA  45
phi-trend            7.9204 3.1375  3.5599 7.5867 15.1393   NA  52
sigma.sq.t           0.6199 0.3944  0.2203 0.5300  1.3731   NA 250
rho                  0.0847 0.2361 -0.3542 0.0803  0.5332   NA 250
# Compare estimates to true values
beta
[1] -0.5 -0.2
alpha
[1]  0.9 -0.3
sigma.sq
[1] 1.0 0.5
phi
[1] 3.750000 4.285714
rho
[1] 0.25
sigma.sq.t
[1] 0.5

Next, we extract the full SVC values using the getSVCSamples() function and then compare the estimated values to those used to generate the model.

# Intercept ---------------------------------------------------------------
svc.samples <- getSVCSamples(out.svc.trend)
int.quants <- apply(svc.samples[["(Intercept)"]], 2, quantile, 
                    probs = c(0.025, 0.5, 0.975))
svc.true.fit <- beta + w.fit
plot(svc.true.fit[, 1], int.quants[2, ], pch = 19, 
     ylim = c(min(int.quants[1, ]), max(int.quants[3, ])),
     xlab = "True", ylab = "Fit", main = 'Intercept')
abline(0, 1)
arrows(svc.true.fit[, 1], int.quants[2, ], svc.true.fit[, 1], col = 'gray', 
       int.quants[1, ], length = 0.02, angle = 90)
arrows(svc.true.fit[, 1], int.quants[1, ], svc.true.fit[, 1], col = 'gray', 
       int.quants[3, ], length = 0.02, angle = 90)
points(svc.true.fit[, 1], int.quants[2, ], pch = 19)

# Trend -------------------------------------------------------------------
trend.quants <- apply(svc.samples[["trend"]], 2, quantile, 
                      probs = c(0.025, 0.5, 0.975))
plot(svc.true.fit[, 2], trend.quants[2, ], pch = 19, 
     ylim = c(min(trend.quants[1, ]), max(trend.quants[3, ])),
     xlab = "True", ylab = "Fit", main = 'Spatially-Varying Trends')
abline(0, 1)
arrows(svc.true.fit[, 2], trend.quants[2, ], svc.true.fit[, 2], col = 'gray', 
       trend.quants[1, ], length = 0.02, angle = 90)
arrows(svc.true.fit[, 2], trend.quants[1, ], svc.true.fit[, 2], col = 'gray', 
       trend.quants[3, ], length = 0.02, angle = 90)
points(svc.true.fit[, 2], trend.quants[2, ], pch = 19)

We see our model does a good job of recovering the true parameter values for both the spatially-varying intercept and spatially-varying trend.

Posterior Predictive Checks

Next we run a posterior predictive check using a Freeman-Tukey statistic and grouping by site.

ppc.out <- ppcOcc(out.svc.trend, fit.stat = 'freeman-tukey', group = 1)
Currently on time period 1 out of 10
Currently on time period 2 out of 10
Currently on time period 3 out of 10
Currently on time period 4 out of 10
Currently on time period 5 out of 10
Currently on time period 6 out of 10
Currently on time period 7 out of 10
Currently on time period 8 out of 10
Currently on time period 9 out of 10
Currently on time period 10 out of 10
summary(ppc.out)

Call:
ppcOcc(object = out.svc.trend, fit.stat = "freeman-tukey", group = 1)

Samples per Chain: 15000
Burn-in: 10000
Thinning Rate: 20
Number of Chains: 1
Total Posterior Samples: 250

----------------------------------------
    All time periods combined
----------------------------------------
Bayesian p-value:  0.5016 

----------------------------------------
    Individual time periods
----------------------------------------
Time Period 1 Bayesian p-value: 0.456
Time Period 2 Bayesian p-value: 0.456
Time Period 3 Bayesian p-value: 0.492
Time Period 4 Bayesian p-value: 0.404
Time Period 5 Bayesian p-value: 0.732
Time Period 6 Bayesian p-value: 0.64
Time Period 7 Bayesian p-value: 0.392
Time Period 8 Bayesian p-value: 0.46
Time Period 9 Bayesian p-value: 0.42
Time Period 10 Bayesian p-value: 0.564
Fit statistic:  freeman-tukey 

As expected, we see all Bayesian p-values are quite close to the optimal 0.5.

Model Comparison using WAIC

Here we use WAIC to compare to a model that assumes the temporal trend is constant over space using the stPGOcc() function (or equivalently, we could use svcTPGOcc() with svc.cols = 1).

priors.stPGOcc <- list(beta.normal = priors$beta.normal, 
                       alpha.normal = priors$alpha.normal, 
                       sigma.sq.t.ig = priors$sigma.sq.t.ig,
                       rho.unif = priors$rho.unif,
                       phi.unif = c(3 / 1, 3 / 0.1), 
                       sigma.sq.ig = c(2, 1))
out.stPGOcc <- stPGOcc(occ.formula = occ.formula,
                       det.formula = det.formula,
                       data = data,
                       inits = inits,
                       priors = priors.stPGOcc,
                       cov.model = cov.model,
                       n.neighbors = n.neighbors,
                       n.batch = n.batch,
                       batch.length = batch.length,
                       verbose = TRUE,
                       ar1 = ar1,
                       n.report = 200,
                       n.burn = n.burn,
                       n.thin = n.thin,
                       n.chains = 1)
----------------------------------------
    Preparing the data
----------------------------------------
----------------------------------------
    Building the neighbor list
----------------------------------------
----------------------------------------
Building the neighbors of neighbors list
----------------------------------------
----------------------------------------
    Model description
----------------------------------------
Spatial NNGP Multi-season Occupancy Model with Polya-Gamma latent
variable fit with 300 sites and 10 primary time periods.

Samples per chain: 15000 (600 batches of length 25)
Burn-in: 10000 
Thinning Rate: 20 
Number of Chains: 1 
Total Posterior Samples: 250 

Using the exponential spatial correlation model.

Using 5 nearest neighbors.

Using an AR(1) temporal autocorrelation matrix.

Source compiled with OpenMP support and model fit using 1 thread(s).

Adaptive Metropolis with target acceptance rate: 43.0
----------------------------------------
    Chain 1
----------------------------------------
Sampling ... 
Batch: 200 of 600, 33.33%
    Parameter   Acceptance  Tuning
    phi     44.0        0.66365
    rho     52.0        1.09417
-------------------------------------------------
Batch: 400 of 600, 66.67%
    Parameter   Acceptance  Tuning
    phi     36.0        0.69073
    rho     36.0        1.07251
-------------------------------------------------
Batch: 600 of 600, 100.00%
# SVC multi-season occupancy model
waicOcc(out.svc.trend)
      elpd         pD       WAIC 
-1987.9099   118.2237  4212.2672 
# Spatial multi-season occupancy model
waicOcc(out.stPGOcc)
       elpd          pD        WAIC 
-2068.97911    67.75701  4273.47224 

The lower WAIC value lends support for the model with the spatially-varying trend compared to a model where the trend is assumed constant over space.

Prediction

Finally, we can use the predict() function to predict relative occurrence and the effects of the covariates at new (and old) locations. The t.cols argument specifies the time periods over which you want to generate predictions. Here we will predict occupancy at the hold out locations for all ten time periods.

# Take a look at X.pred, the design matrix for the prediction locations
head(X.pred)
, , 1

     [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
[1,]    1    1    1    1    1    1    1    1    1     1
[2,]    1    1    1    1    1    1    1    1    1     1
[3,]    1    1    1    1    1    1    1    1    1     1
[4,]    1    1    1    1    1    1    1    1    1     1
[5,]    1    1    1    1    1    1    1    1    1     1
[6,]    1    1    1    1    1    1    1    1    1     1

, , 2

          [,1]      [,2]       [,3]       [,4]       [,5]      [,6]      [,7]
[1,] -1.566503 -1.218391 -0.8702795 -0.5221677 -0.1740559 0.1740559 0.5221677
[2,] -1.566503 -1.218391 -0.8702795 -0.5221677 -0.1740559 0.1740559 0.5221677
[3,] -1.566503 -1.218391 -0.8702795 -0.5221677 -0.1740559 0.1740559 0.5221677
[4,] -1.566503 -1.218391 -0.8702795 -0.5221677 -0.1740559 0.1740559 0.5221677
[5,] -1.566503 -1.218391 -0.8702795 -0.5221677 -0.1740559 0.1740559 0.5221677
[6,] -1.566503 -1.218391 -0.8702795 -0.5221677 -0.1740559 0.1740559 0.5221677
          [,8]     [,9]    [,10]
[1,] 0.8702795 1.218391 1.566503
[2,] 0.8702795 1.218391 1.566503
[3,] 0.8702795 1.218391 1.566503
[4,] 0.8702795 1.218391 1.566503
[5,] 0.8702795 1.218391 1.566503
[6,] 0.8702795 1.218391 1.566503
# Predict occupancy at the 400 new sites
out.pred <- predict(out.svc.trend, X.pred, coords.pred, t.cols = 1:n.time.max)
----------------------------------------
    Prediction description
----------------------------------------
Spatial NNGP Multi-season Occupancy model with Polya-Gamma latent
variable fit with 300 observations and 10 years.

Number of fixed covariates 2 (including intercept if specified).

Using the exponential spatial correlation model.

Using 5 nearest neighbors.

Number of MCMC samples 250.

Predicting at 100 non-sampled locations.


Source compiled with OpenMP support and model fit using 1 threads.
-------------------------------------------------
        Predicting
-------------------------------------------------
Location: 100 of 100, 100.00%
Generating latent occupancy state
# Use the getSVCSamples() function to extract the SVC values
# at the prediction locations
svc.pred.samples <- getSVCSamples(out.svc.trend, pred.object = out.pred) 
# True covariate effect values at new locations
trend.pred <- beta[2] + w.pred[, 2]
# Get meian and 95% CIs of the SVC for the trendariate effect
trend.pred.quants <- apply(svc.pred.samples[["trend"]], 2, quantile, 
                           probs = c(0.025, 0.5, 0.975))
plot(trend.pred, trend.pred.quants[2, ], pch = 19, 
     ylim = c(min(trend.pred.quants[1, ]), max(trend.pred.quants[3, ])),
     xlab = "True", ylab = "Fit", main = 'Spatially-varying trend')
abline(0, 1)
arrows(trend.pred, trend.pred.quants[2, ], trend.pred, col = 'gray', 
       trend.pred.quants[1, ], length = 0.02, angle = 90)
arrows(trend.pred, trend.pred.quants[1, ], trend.pred, col = 'gray', 
       trend.pred.quants[3, ], length = 0.02, angle = 90)
points(trend.pred, trend.pred.quants[2, ], pch = 19)

Spatially-varying coefficients multi-season generalized linear model

Basic model description

To model spatio-temporal patterns in occurrence when ignoring imperfect detection, we directly model the detection-nondetection data analogous to the SVC GLM, with all data and parameters now varying over time. The occurrence status of the species at site \(j\) during primary time period \(t\) (\(\psi_t(\boldsymbol{s}_j)\)) is analogous to the SVC multi-season occupancy model.

Simulating data with simTBinom()

The function simTBinom() simulates single-species multi-season detection-nondetection data assuming perfect detection. simTBinom() has the following arguments, which again closely resemble all other data simulation functions in spOccupancy.

simTBinom(J.x, J.y, n.time, weights, beta, sp.only = 0, 
          trend = TRUE, psi.RE = list(), sp = FALSE, 
          cov.model, sigma.sq, phi, nu, svc.cols = 1, 
          ar1 = FALSE, rho, sigma.sq.t, x.positive = FALSE, ...) 

All parameters are the same as those described previously for simTBinom() and simTOcc(). Below we simulate data from 400 sites over 10 years, with uneven sampling over the 10 years across the sites. As with the SVC multi-season occupancy model, our goal for this simulation is to assess a spatially-varying trend.

set.seed(500)
J.x <- 20
J.y <- 20
# Total number of sites
(J <- J.x * J.y)
[1] 400
# Total number of time periods at each site
n.time <- sample(10, J, replace = TRUE)
n.time.max <- max(n.time)
# Number of Binomial replicates at each site/time period combination.
weights <- matrix(NA, J, max(n.time))
for (j in 1:J) {
  weights[j, 1:n.time[j]] <- sample(4, n.time[j], replace = TRUE)
}
sp.only <- 0
# Simulate a trend parameter 
trend <- TRUE
# Intercept and trend on relative occurrence
beta <- c(-0.5, -0.2)
# No unstructured random intercept on relative occurrence
psi.RE <- list() 
# Spatial range for intercept and trend 
phi <- c(3 / .8, 3 / .7)
# Spatial variance for intercept and trend
sigma.sq <- c(1, 0.5)
# Simulate data with an AR1 temporal process
ar1 <- TRUE
# Temporal variance of AR1 random effect
sigma.sq.t <- 0.5
# Temporal correlation of AR1 random effect
rho <- 0.25
# Spatially-varying coefficient columns
svc.cols <- c(1, 2)
# Simulate the data
dat <- simTBinom(J.x = J.x, J.y = J.y, n.time = n.time, weights = weights, 
                 beta = beta, sp.only = sp.only, trend = trend, psi.RE = psi.RE, 
                 sp = TRUE, sigma.sq = sigma.sq, phi = phi, 
                 cov.model = 'exponential', svc.cols = svc.cols, 
                 ar1 = ar1, sigma.sq.t = sigma.sq.t, rho = rho)
str(dat)
List of 8
 $ X        : num [1:400, 1:10, 1:2] 1 1 1 1 1 1 1 1 1 1 ...
 $ coords   : num [1:400, 1:2] 0 0.0526 0.1053 0.1579 0.2105 ...
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : NULL
  .. ..$ : chr [1:2] "Var1" "Var2"
 $ psi      : num [1:400, 1:10] 0.25286 0.06884 0.0829 0.03187 0.00752 ...
 $ y        : int [1:400, 1:10] 2 0 1 0 0 0 0 0 0 0 ...
 $ w        : num [1:400, 1:2] -1.246 -1.52 -1.033 -0.798 -1.627 ...
 $ X.re     : logi NA
 $ beta.star: logi NA
 $ eta      : num [1:10, 1] -0.3429 0.0803 -0.5192 -0.1028 -0.0615 ...
# Plot average occupancy probability
plot(apply(dat$psi, 2, mean), pch = 19, xlab = 'Time Period',
     ylab = 'Average Occupancy Probability',
     cex = 1.5, frame = FALSE, ylim = c(0, 1))

We see a small decline in relative occurrence probability over the simulated ten years.

Below we extract the data we need to fit the model and create a plot of the spatially-varying trend across the study region

# Detection-nondetection data
y <- dat$y
# Design matrix for fixed effects
X <- dat$X
# Occurrence values
psi <- dat$psi
# Spatial coordinates
coords <- dat$coords
# Spatially-varying intercept and covariate effects
w <- dat$w

trend <- beta[2] + w[, 2]
plot.dat <- data.frame(x = coords[, 1], 
                       y = coords[, 2], 
                       trend = trend)
ggplot(plot.dat, aes(x = x, y = y, fill = trend)) + 
  geom_raster() + 
  scale_fill_gradient2(midpoint = 0, low = '#B2182B', mid = 'white', high = '#2166AC', 
                       na.value = NA) + 
  theme_bw() + 
  labs(x = 'Easting', y = 'Northing', fill = 'Trend')

Lastly, we package up the data in a list required for fitting the multi-season spatially-varying coefficient GLM. We create a list with the following objects: y (the detection-nondetection data specified as a two-dimensional matrix), covs (the covariates specified as a list), weights (a matrix of binomial weights indicating the number of Bernoulli trials), and coords (a matrix of spatial coordinates). As before, we subset the data for prediction to only fit the model with 75% of the sites.

# Subset data for prediction. 
# Split into fitting and prediction data set
pred.indx <- sample(1:J, round(J * .25), replace = FALSE)
y.fit <- y[-pred.indx, ]
y.pred <- y[pred.indx, ]
X.fit <- X[-pred.indx, , ]
X.pred <- X[pred.indx, , ]
coords.fit <- coords[-pred.indx, ]
coords.pred <- coords[pred.indx, ]
psi.fit <- psi[-pred.indx, ]
psi.pred <- psi[pred.indx, ]
w.fit <- w[-pred.indx, ]
w.pred <- w[pred.indx, ]
weights.fit <- weights[-pred.indx, ]
weights.pred <- weights[pred.indx, ]

# Package all data into a list
# Occurrence covariates
covs <- list(trend = X.fit[, , 2])
# Package into a list for spOccupancy
data.list <- list(y = y.fit,
                  covs = covs,
                  weights = weights.fit,
                  coords = coords.fit)
# Take a look at the data structure.
str(data.list)
List of 4
 $ y      : int [1:300, 1:10] 2 0 1 0 0 0 0 0 0 0 ...
 $ covs   :List of 1
  ..$ trend: num [1:300, 1:10] -1.57 -1.57 -1.57 -1.57 -1.57 ...
 $ weights: int [1:300, 1:10] 4 1 1 3 4 3 4 3 2 3 ...
 $ coords : num [1:300, 1:2] 0 0.0526 0.1053 0.1579 0.2105 ...
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : NULL
  .. ..$ : chr [1:2] "Var1" "Var2"

Fitting spatially-varying coefficients generalized linear models with svcTPGBinom()

The function svcTPGBinom() fits spatially-varyig coefficients multi-season generalized linear models.

svcTPGBinom(formula, data, inits, priors, 
            tuning, svc.cols = 1, cov.model = 'exponential', NNGP = TRUE, 
            n.neighbors = 15, search.type = 'cb', n.batch, 
            batch.length, accept.rate = 0.43, n.omp.threads = 1, 
            verbose = TRUE, ar1 = FALSE, n.report = 100, 
            n.burn = round(.10 * n.batch * batch.length), 
            n.thin = 1, n.chains = 1, k.fold, k.fold.threads = 1, 
            k.fold.seed = 100, k.fold.only = FALSE, ...)

There are no new arguments or details for fitting models with svcTPGBinom(), so we will go ahead and fit the model after specifying all the model arguments.

# Formatted data list
data <- data.list
str(data)
List of 4
 $ y      : int [1:300, 1:10] 2 0 1 0 0 0 0 0 0 0 ...
 $ covs   :List of 1
  ..$ trend: num [1:300, 1:10] -1.57 -1.57 -1.57 -1.57 -1.57 ...
 $ weights: int [1:300, 1:10] 4 1 1 3 4 3 4 3 2 3 ...
 $ coords : num [1:300, 1:2] 0 0.0526 0.1053 0.1579 0.2105 ...
  ..- attr(*, "dimnames")=List of 2
  .. ..$ : NULL
  .. ..$ : chr [1:2] "Var1" "Var2"
# Model formula
formula <- ~ trend
# Initial values
inits <- list(beta = 0, sigma.sq = 1, phi = 3 / 0.5,
              sigma.sq.t = 1.5, rho = 0.2)
# Priors
priors <- list(beta.normal = list(mean = 0, var = 2.72),
               sigma.sq.t.ig = c(2, 0.5),
               rho.unif = c(-1, 1),
               sigma.sq.ig = list(a = 2, b = 1),
               phi.unif = list(a = 3 / 1, b = 3 / .1))
# Exponential covariance function
cov.model <- 'exponential'
# Spatially-varying intercept and trend
svc.cols <- c(1, 2)
# Fit model with a NNGP with 5 neighbors
n.neighbors <- 5
# Include an AR(1) temporal covariance structure.
ar1 <- TRUE
# MCMC parameters
n.batch <- 600
batch.length <- 25
# Total number of samples
n.batch * batch.length
[1] 15000
n.burn <- 10000
n.thin <- 20
# Approx. run time: ~ 1.5 min
out.svc.trend <- svcTPGBinom(formula = formula,
                             data = data,
                             inits = inits,
                             priors = priors,
                             cov.model = cov.model,
                             svc.cols = svc.cols,
                             n.neighbors = n.neighbors,
                             n.batch = n.batch,
                             batch.length = batch.length,
                             verbose = TRUE,
                             ar1 = ar1,
                             n.report = 200,
                             n.burn = n.burn,
                             n.thin = n.thin,
                             n.chains = 1)
----------------------------------------
    Preparing the data
----------------------------------------
----------------------------------------
    Building the neighbor list
----------------------------------------
----------------------------------------
Building the neighbors of neighbors list
----------------------------------------
----------------------------------------
    Model description
----------------------------------------
Spatial NNGP Multi-season Binomial Model with Polya-Gamma latent
variable fit with 300 sites and 10 years.

Samples per chain: 15000 (600 batches of length 25)
Burn-in: 10000 
Thinning Rate: 20 
Number of Chains: 1 
Total Posterior Samples: 250 

Number of spatially-varying coefficients: 2 
Using the exponential spatial correlation model.

Using 5 nearest neighbors.

Using an AR(1) temporal autocorrelation matrix.

Source compiled with OpenMP support and model fit using 1 thread(s).

Adaptive Metropolis with target acceptance rate: 43.0
----------------------------------------
    Chain 1
----------------------------------------
Sampling ... 
Batch: 200 of 600, 33.33%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     28.0        0.58860
    2       phi     60.0        0.70469
    1       rho     32.0        1.28403
-------------------------------------------------
Batch: 400 of 600, 66.67%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     40.0        0.53259
    2       phi     40.0        0.81058
    1       rho     28.0        1.30996
-------------------------------------------------
Batch: 600 of 600, 100.00%
summary(out.svc.trend)

Call:
svcTPGBinom(formula = formula, data = data, inits = inits, priors = priors, 
    svc.cols = svc.cols, cov.model = cov.model, n.neighbors = n.neighbors, 
    n.batch = n.batch, batch.length = batch.length, verbose = TRUE, 
    ar1 = ar1, n.report = 200, n.burn = n.burn, n.thin = n.thin, 
    n.chains = 1)

Samples per Chain: 15000
Burn-in: 10000
Thinning Rate: 20
Number of Chains: 1
Total Posterior Samples: 250
Run Time (min): 1.8373

Occurrence (logit scale): 
               Mean     SD    2.5%    50%  97.5% Rhat ESS
(Intercept) -0.1490 0.2990 -0.7855 -0.131 0.4739   NA  32
trend       -0.2644 0.3101 -0.7975 -0.279 0.3572   NA  20

Spatio-temporal Covariance: 
                        Mean     SD    2.5%     50%   97.5% Rhat ESS
sigma.sq-(Intercept)  0.7342 0.1810  0.4590  0.7109  1.1274   NA 141
sigma.sq-trend        0.7398 0.2080  0.4181  0.7101  1.1573   NA 120
phi-(Intercept)       7.6085 2.4949  3.7415  7.2722 13.0823   NA 160
phi-trend             5.0191 1.4382  3.0930  4.7755  8.6837   NA  81
sigma.sq.t            0.2524 0.1365  0.0930  0.2154  0.6464   NA 250
rho                  -0.0135 0.2508 -0.5345 -0.0059  0.4355   NA 250
# Compare estimates to true values
beta
[1] -0.5 -0.2
alpha
[1]  0.9 -0.3
sigma.sq
[1] 1.0 0.5
phi
[1] 3.750000 4.285714
rho
[1] 0.25
sigma.sq.t
[1] 0.5

Let’s extract the spatially-varying intercept and spatially-varying trend estimates from our model using getSVCSamples() and compare them to the true values.

# Intercept ---------------------------------------------------------------
svc.samples <- getSVCSamples(out.svc.trend)
int.quants <- apply(svc.samples[["(Intercept)"]], 2, quantile, 
                    probs = c(0.025, 0.5, 0.975))
svc.true.fit <- beta + w.fit
plot(svc.true.fit[, 1], int.quants[2, ], pch = 19, 
     ylim = c(min(int.quants[1, ]), max(int.quants[3, ])),
     xlab = "True", ylab = "Fit", main = 'Intercept')
abline(0, 1)
arrows(svc.true.fit[, 1], int.quants[2, ], svc.true.fit[, 1], col = 'gray', 
       int.quants[1, ], length = 0.02, angle = 90)
arrows(svc.true.fit[, 1], int.quants[1, ], svc.true.fit[, 1], col = 'gray', 
       int.quants[3, ], length = 0.02, angle = 90)
points(svc.true.fit[, 1], int.quants[2, ], pch = 19)

# Trend -------------------------------------------------------------------
trend.quants <- apply(svc.samples[["trend"]], 2, quantile, 
                      probs = c(0.025, 0.5, 0.975))
plot(svc.true.fit[, 2], trend.quants[2, ], pch = 19, 
     ylim = c(min(trend.quants[1, ]), max(trend.quants[3, ])),
     xlab = "True", ylab = "Fit", main = 'Spatially-Varying Trends')
abline(0, 1)
arrows(svc.true.fit[, 2], trend.quants[2, ], svc.true.fit[, 2], col = 'gray', 
       trend.quants[1, ], length = 0.02, angle = 90)
arrows(svc.true.fit[, 2], trend.quants[1, ], svc.true.fit[, 2], col = 'gray', 
       trend.quants[3, ], length = 0.02, angle = 90)
points(svc.true.fit[, 2], trend.quants[2, ], pch = 19)

We see our model does a good job of recovering the true parameter values for both the spatially-varying intercept and spatially-varying trend.

Model Comparison using WAIC

Here we use WAIC to compare our full SVC model to a model that assumes the temporal trend is constant over space by setting svc.cols = 1.

out.trend.const <- svcTPGBinom(formula = formula,
                               data = data,
                               inits = inits,
                               priors = priors,
                               svc.cols = 1,
                               cov.model = cov.model,
                               n.neighbors = n.neighbors,
                               n.batch = n.batch,
                               batch.length = batch.length,
                               verbose = TRUE,
                               ar1 = ar1,
                               n.report = 200,
                               n.burn = n.burn,
                               n.thin = n.thin,
                               n.chains = 1)
----------------------------------------
    Preparing the data
----------------------------------------
----------------------------------------
    Building the neighbor list
----------------------------------------
----------------------------------------
Building the neighbors of neighbors list
----------------------------------------
----------------------------------------
    Model description
----------------------------------------
Spatial NNGP Multi-season Binomial Model with Polya-Gamma latent
variable fit with 300 sites and 10 years.

Samples per chain: 15000 (600 batches of length 25)
Burn-in: 10000 
Thinning Rate: 20 
Number of Chains: 1 
Total Posterior Samples: 250 

Number of spatially-varying coefficients: 1 
Using the exponential spatial correlation model.

Using 5 nearest neighbors.

Using an AR(1) temporal autocorrelation matrix.

Source compiled with OpenMP support and model fit using 1 thread(s).

Adaptive Metropolis with target acceptance rate: 43.0
----------------------------------------
    Chain 1
----------------------------------------
Sampling ... 
Batch: 200 of 600, 33.33%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     48.0        0.58860
    1       rho     36.0        1.18530
-------------------------------------------------
Batch: 400 of 600, 66.67%
    Coefficient Parameter   Acceptance  Tuning
    1       phi     44.0        0.58860
    1       rho     36.0        1.30996
-------------------------------------------------
Batch: 600 of 600, 100.00%
# SVC multi-season GLM
waicOcc(out.svc.trend)
     elpd        pD      WAIC 
-1517.713   198.032  3431.490 
# Spatial multi-season GLM
waicOcc(out.trend.const)
      elpd         pD       WAIC 
-1674.1340   162.3747  3673.0174 

Prediction

Finally, we use the predict() function to predict relative occurrence and the effects of the covariates at new (and old) locations over the simulated ten tme periods.

# Take a look at X.pred, the design matrix for the prediction locations
head(X.pred)
, , 1

     [,1] [,2] [,3] [,4] [,5] [,6] [,7] [,8] [,9] [,10]
[1,]    1    1    1    1    1    1    1    1    1     1
[2,]    1    1    1    1    1    1    1    1    1     1
[3,]    1    1    1    1    1    1    1    1    1     1
[4,]    1    1    1    1    1    1    1    1    1     1
[5,]    1    1    1    1    1    1    1    1    1     1
[6,]    1    1    1    1    1    1    1    1    1     1

, , 2

          [,1]      [,2]       [,3]       [,4]       [,5]      [,6]      [,7]
[1,] -1.566503 -1.218391 -0.8702795 -0.5221677 -0.1740559 0.1740559 0.5221677
[2,] -1.566503 -1.218391 -0.8702795 -0.5221677 -0.1740559 0.1740559 0.5221677
[3,] -1.566503 -1.218391 -0.8702795 -0.5221677 -0.1740559 0.1740559 0.5221677
[4,] -1.566503 -1.218391 -0.8702795 -0.5221677 -0.1740559 0.1740559 0.5221677
[5,] -1.566503 -1.218391 -0.8702795 -0.5221677 -0.1740559 0.1740559 0.5221677
[6,] -1.566503 -1.218391 -0.8702795 -0.5221677 -0.1740559 0.1740559 0.5221677
          [,8]     [,9]    [,10]
[1,] 0.8702795 1.218391 1.566503
[2,] 0.8702795 1.218391 1.566503
[3,] 0.8702795 1.218391 1.566503
[4,] 0.8702795 1.218391 1.566503
[5,] 0.8702795 1.218391 1.566503
[6,] 0.8702795 1.218391 1.566503
# Predict occupancy at the 400 new sites
out.pred <- predict(out.svc.trend, X.pred, coords.pred, t.cols = 1:n.time.max, 
                    weights = weights.pred)
----------------------------------------
    Prediction description
----------------------------------------
Spatial NNGP Multi-season Occupancy model with Polya-Gamma latent
variable fit with 300 observations and 10 years.

Number of fixed covariates 2 (including intercept if specified).

Using the exponential spatial correlation model.

Using 5 nearest neighbors.

Number of MCMC samples 250.

Predicting at 100 non-sampled locations.


Source compiled with OpenMP support and model fit using 1 threads.
-------------------------------------------------
        Predicting
-------------------------------------------------
Location: 100 of 100, 100.00%
Generating latent occupancy state
# Use the getSVCSamples() function to extract the SVC values
# at the prediction locations
svc.pred.samples <- getSVCSamples(out.svc.trend, pred.object = out.pred) 
# True covariate effect values at new locations
trend.pred <- beta[2] + w.pred[, 2]
# Get meian and 95% CIs of the SVC for the trendariate effect
trend.pred.quants <- apply(svc.pred.samples[["trend"]], 2, quantile, 
                           probs = c(0.025, 0.5, 0.975))
plot(trend.pred, trend.pred.quants[2, ], pch = 19, 
     ylim = c(min(trend.pred.quants[1, ]), max(trend.pred.quants[3, ])),
     xlab = "True", ylab = "Fit", main = 'Spatially-varying trend')
abline(0, 1)
arrows(trend.pred, trend.pred.quants[2, ], trend.pred, col = 'gray', 
       trend.pred.quants[1, ], length = 0.02, angle = 90)
arrows(trend.pred, trend.pred.quants[1, ], trend.pred, col = 'gray', 
       trend.pred.quants[3, ], length = 0.02, angle = 90)
points(trend.pred, trend.pred.quants[2, ], pch = 19)

References

Banerjee, Sudipto, Bradley P Carlin, and Alan E Gelfand. 2003. Hierarchical Modeling and Analysis for Spatial Data. Chapman; Hall/CRC.

Chen, Guoke, Marc Kéry, Matthias Plattner, Keping Ma, and Beth Gardner. 2013. “Imperfect Detection Is the Rule Rather Than the Exception in Plant Distribution Studies.” Journal of Ecology 101 (1): 183–91.

Datta, Abhirup, Sudipto Banerjee, Andrew O Finley, and Alan E Gelfand. 2016. “Hierarchical Nearest-Neighbor Gaussian Process Models for Large Geostatistical Datasets.” Journal of the American Statistical Association 111 (514): 800–812.

Doser, Jeffrey W, Andrew O Finley, Marc Kéry, and Elise F Zipkin. 2022. “spOccupancy: An R package for single-species, multi-species, and integrated spatial occupancy models.” Methods in Ecology and Evolution 13 (8): 1670–8.

Finley, Andrew O. 2011. “Comparing Spatially-Varying Coefficients Models for Analysis of Ecological Data with Non-Stationary and Anisotropic Residual Dependence.” Methods in Ecology and Evolution 2 (2): 143–54.

Gelfand, Alan E, Hyon-Jung Kim, CF Sirmans, and Sudipto Banerjee. 2003. “Spatial Modeling with Spatially Varying Coefficient Processes.” Journal of the American Statistical Association 98 (462): 387–96.

Hooten, Mevin B, and Trevor J Hefley. 2019. Bringing Bayesian Models to Life. CRC Press.

Hooten, Mevin B, and N Thompson Hobbs. 2015. “A Guide to Bayesian Model Selection for Ecologists.” Ecological Monographs 85 (1): 3–28.

Isaac, Nick JB, Arco J van Strien, Tom A August, Marnix P de Zeeuw, and David B Roy. 2014. “Statistics for Citizen Science: Extracting Signals of Change from Noisy Ecological Data.” Methods in Ecology and Evolution 5 (10): 1052–60.

Johnson, Devin S, Paul B Conn, Mevin B Hooten, Justina C Ray, and Bruce A Pond. 2013. “Spatial Occupancy Models for Large Data Sets.” Ecology 94 (4): 801–8.

Lele, Subhash R, Monica Moreno, and Erin Bayne. 2012. “Dealing with Detection Error in Site Occupancy Surveys: What Can We Do with a Single Survey?” Journal of Plant Ecology 5 (1): 22–31.

MacKenzie, Darryl I, James D Nichols, Gideon B Lachman, Sam Droege, J Andrew Royle, and Catherine A Langtimm. 2002. “Estimating Site Occupancy Rates When Detection Probabilities Are Less Than One.” Ecology 83 (8): 2248–55.

Pease, Brent S, Krishna Pacifici, and Roland Kays. 2022. “Exploring Spatial Nonstationarity for Four Mammal Species Reveals Regional Variation in Environmental Relationships.” Ecosphere 13 (8): e4166.

Polson, Nicholas G, James G Scott, and Jesse Windle. 2013. “Bayesian Inference for Logistic Models Using Pólya–Gamma Latent Variables.” Journal of the American Statistical Association 108 (504): 1339–49.

Roberts, Gareth O, and Jeffrey S Rosenthal. 2009. “Examples of Adaptive Mcmc.” Journal of Computational and Graphical Statistics 18 (2): 349–67.

Rollinson, Christine R, Andrew O Finley, M Ross Alexander, Sudipto Banerjee, Kelly-Ann Dixon Hamil, Lauren E Koenig, Dexter Henry Locke, et al. 2021. “Working Across Space and Time: Nonstationarity in Ecological Research and Application.” Frontiers in Ecology and the Environment 19 (1): 66–72.

Rushing, Clark S, J Andrew Royle, David J Ziolkowski Jr, and Keith L Pardieck. 2020. “Migratory Behavior and Winter Geography Drive Differential Range Shifts of Eastern Birds in Response to Recent Climate Change.” Proceedings of the National Academy of Sciences 117 (23): 12897–12903.

Tyre, Andrew J, Brigitte Tenhumberg, Scott A Field, Darren Niejalke, Kirsten Parris, and Hugh P Possingham. 2003. “Improving Precision and Reducing Bias in Biological Surveys: Estimating False-Negative Error Rates.” Ecological Applications 13 (6): 1790–1801.

Wright, Wilson J, Kathryn M Irvine, Thomas J Rodhouse, and Andrea R Litt. 2021. “Spatial Gaussian Processes Improve Multi-Species Occupancy Models When Range Boundaries Are Uncertain and Nonoverlapping.” Ecology and Evolution.