In this Vignette, we demonstrate how to fit Bayesian spatial models using the multi-resolution model using Markov Chain Monte Carlo (MCMC).

Defining the MRA model

This package provides a Bayesian implementation of the multi-resolution Gaussian process model presented by Nychka et al. (2015). This package is similar to the R package LatticeKrig::LatticeKrig (Nychka et al. (2016)) and provides an implementation that is suitable for use in MCMC samplers.

The model that the function mcmc_mra() fits is defined below. Let \(\mathbf{s}\) be a spatial location in a domain of interest \(\mathcal{D}\) and let \(y(\mathbf{s})\) be the observation of the spatial process at location \(\mathbf{s}\). Then, we can write the observation as

\[\begin{align*} y(\mathbf{s}) & = \mathbf{x}(\mathbf{s})' \boldsymbol{\beta} + \eta(\mathbf{s}) + \varepsilon(\mathbf{s}), \end{align*}\]

where \(\mathbf{x}(s)\) is a vector of covariates at site \(\mathbf{s}\), \(\boldsymbol{\beta}\) are regression coefficients, \(\boldsymbol{\eta}(\mathbf{s})\) is the realization of a correlated Gaussian process at location \(\mathbf{s}\) and \(\varepsilon(\mathbf{s})\) is the realization of an uncorrelated Gaussian error process. The model can be written in matrix form where

\[\begin{align*} \mathbf{y} & = \mathbf{X} \boldsymbol{\beta} + \boldsymbol{\eta} + \boldsymbol{\varepsilon}. \end{align*}\]

In matrix form, the spatially correlated random process has the distribution \(\boldsymbol{\eta} \sim \operatorname{N}(\mathbf{0}, \mathbf{C})\) where the \(\mathbf{C}_{ij}\) (the \(i\)th row and \(j\)th column of \(\mathbf{C}\)) is defined as the output of the covariance function \(cov(\mathbf{s}_i, \mathbf{s}_j)\) at sites \(\mathbf{s}_i\) and \(\mathbf{s}_j\). The residual process \(\boldsymbol{\varepsilon} \sim \operatorname{N}(\mathbf{0}, \sigma^2 \mathbf{I})\) models measurement error, commonly referred to as the nugget in the spatial statistics literature.

Rather than specifying a parametric form for the covariance function \(cov(\mathbf{s}_i, \mathbf{s}_j)\), we approximate the covariance function effect with a multi-resolution approximation. This approximation gives \(\boldsymbol{\eta} \approx \sum_{m=1}^M \mathbf{W}_m \boldsymbol{\alpha}_m\) where \(\mathbf{W}_m\) is the Wendland basis expansion at the \(m\)th resolution and \(\boldsymbol{\alpha}_m\) are the \(m\)th resolution random effect.

Simulating data from the model

First, we simulate some data from the process of interest. We simulate a spatially explicit set of fired effects X that represent spatial variables like elevation, latitude, etc. Next, the spatial process is initialized

set.seed(11)

N <- 40^2

## setup the spatial process
locs <- as.matrix(
  expand.grid(
    seq(0, 1, length.out = sqrt(N)),
    seq(0, 1, length.out = sqrt(N))
  )
)
D <- fields::rdist(locs)

## fixed effects include intercept, elevation, and latitude
X <- cbind(1, as.vector(mvnfast::rmvn(1, rep(0, N), 3 * exp(-D / 20))), locs[, 2])
p <- ncol(X)

beta <- rnorm(ncol(X))

## MRA spatio-temporal random effect
M <- 3
n_coarse <- 10

MRA    <- mra_wendland_2d(locs, M = M, n_coarse = n_coarse, use_spam = TRUE)

# MRA    <- mra_wendland_2d(locs, M = M, n_max_fine_grid = 2^8, use_spam = TRUE)
W_list   <- MRA$W
n_dims   <- rep(NA, length(W_list))
dims_idx <- NULL
for (i in 1:M) {
  n_dims[i] <- ncol(W_list[[i]])
  dims_idx  <- c(dims_idx, rep(i, n_dims[i]))
}
W <- do.call(cbind, W_list)

Q_alpha      <- make_Q_alpha_2d(sqrt(n_dims), rep(0.999, length(n_dims)), prec_model = "CAR")

tau2         <- 10 * 2^(2 * (1:M - 1))
Q_alpha_tau2 <- make_Q_alpha_tau2(Q_alpha, tau2)

## initialize the random effect
## set up a linear constraint so that each resolution sums to one
A_constraint <- sapply(1:M, function(i){
        tmp = rep(0, sum(n_dims))
        tmp[dims_idx == i] <- 1
        return(tmp)
    })

a_constraint <- rep(0, M)
alpha   <- as.vector(rmvnorm.prec.const(n = 1, mu = rep(0, sum(n_dims)), Q = Q_alpha_tau2, A = t(A_constraint), a = a_constraint))

sigma2 <- runif(1, 0.25, 0.5)

y <- X %*% beta + W %*% alpha + rnorm(N, 0, sqrt(sigma2))

Next, the sum-to-one constraint is verified by checking that within each dimension the sum of the \(\boldsymbol{\alpha}_m\) parameters is 0.

sum(alpha)
## [1] 1.98782e-14
sum(alpha[dims_idx == 1])
## [1] 1.450229e-15
sum(alpha[dims_idx == 2])
## [1] 4.22995e-14
sum(alpha[dims_idx == 3])
## [1] -2.387153e-14

Plotting the simulated data

To explore the structure of the precision matrix of the spatial random effects, we plot the pattern of nonzero elements in the precision matrices \(\mathbf{Q}_{m}\) for each resolution \(m\). The sparse, banded structure in the precision matrices at each resolution are what enable efficient computation.

## plot Q_alpha_tau2 structure
## note: the display function gives a warning about the default cex setting
layout(matrix(1:4, 2, 2, byrow = TRUE))
for (m in 1:M) {
  display(Q_alpha_tau2[which(dims_idx == m), which(dims_idx == m)])
  title(main = paste("Nonzero elements of \n precision matrix; resolution", m))
}

plot of chunk plot-precision-matrices

The MRA approach requires a set of grid point at each resolution. The set of grid points at each resolution extends beyond the spatial domain that is highlighted in the black box.

data.frame(
    x = unlist(sapply(1:M, function(i) MRA$locs_grid[[i]][, 1])),
    y = unlist(sapply(1:M, function(i) MRA$locs_grid[[i]][, 2])),
    resolution = factor(unlist(sapply(1:M, function(i) rep(i, each = nrow(MRA$locs_grid[[i]])))))
) %>%
    ggplot(aes(x = x, y = y, color = resolution)) +
    geom_point(alpha = 0.75, size = 0.75) +
    ggtitle("MRA grid points") +
    theme_bw() +
  scale_colour_viridis_d(end = 0.8) +
  geom_rect(
    data = NULL,
    aes(xmin = min(locs[, 1]),
        xmax = max(locs[, 1]),
        ymin = min(locs[, 2]),
        ymax = max(locs[, 2])
    ),
    fill  = NA,
    color = "black",
    alpha = 0.5
  ) +
  theme_custom()

plot of chunk MRA-grid

At each internal grid cell, we expect there to be 68 neighbors where the Wendland basis is nonzero. The plot below shows the number of neighbors at each grid point for each resolution.

nnn <- sapply(1:M, function(i) {
  D <- rdist(MRA$locs_grid[[i]])
  sapply(1:nrow(MRA$locs_grid[[i]]), function(j) {
    sum((D[j, ]) < MRA$radius[i])}
  )
})

data.frame(
  x = unlist(sapply(1:M, function(i) MRA$locs_grid[[i]][, 1])),
  y = unlist(sapply(1:M, function(i) MRA$locs_grid[[i]][, 2])),
  resolution = factor(unlist(sapply(1:M, function(i) rep(paste("resolution", i), each = nrow(MRA$locs_grid[[i]]))))),
  neighbors <- unlist(nnn)
)  %>%
  ggplot(aes(x = x, y = y, fill = neighbors)) +
  geom_raster() +
  facet_wrap(~ resolution) +
  ggtitle("number of neighbors") +
  scale_fill_viridis_c() +
    geom_rect(
    data = NULL,
    aes(xmin = min(locs[, 1]),
        xmax = max(locs[, 1]),
        ymin = min(locs[, 2]),
        ymax = max(locs[, 2])
    ),
    fill  = NA,
    color = "black",
    alpha = 0.5
  ) +
  theme_bw() +
  theme_custom()

plot of chunk plot-neighbors

The next plot shows the simulated spatial random effects \(\boldsymbol{\alpha}_m\) at each of the grid points. Notice that the variation is largest for the coarse resolution and the variability decreases for each finer resolution.

sim_alphas <- data.frame(
    x = unlist(sapply(1:M, function(i) MRA$locs_grid[[i]][, 1])),
    y = unlist(sapply(1:M, function(i) MRA$locs_grid[[i]][, 2])),
    resolution = factor(unlist(sapply(1:M, function(i) rep(paste("resolution", i), each = nrow(MRA$locs_grid[[i]]))))),
    alpha = unlist(sapply(1:M, function(i) alpha[dims_idx == i]))
) %>%
    ggplot(aes(x = x, y = y, fill = alpha)) +
    geom_raster() +
  ggtitle("MRA spatial random effects") +
    scale_fill_viridis_c() +
    geom_rect(
    data = NULL,
    aes(xmin = min(locs[, 1]),
        xmax = max(locs[, 1]),
        ymin = min(locs[, 2]),
        ymax = max(locs[, 2])
    ),
    fill  = NA,
    color = "black",
    alpha = 0.5
  ) +
  facet_wrap( ~ resolution, ncol = 2) +
  theme_bw() +
  theme_custom()

sim_alphas

plot of chunk plot-random-effects

Next, we show a simulated realization of the model. The left figure below shows the fixed effects in the model, the middle figure shows the simulated spatial process, and the right plot shows the observed surface.

zlims <- c(
  min(c(X %*% beta), c(y), c(W %*% alpha)),
  max(c(X %*% beta), c(y), c(W %*% alpha))
)

g_fixed <- data.frame(
  temp = c(X %*% beta),
  x    = locs[, 1],
  y    = locs[, 2]
) %>%
  ggplot(aes(x = x, y = y, fill = temp)) +
  geom_raster() +
  xlab("x") +
  ylab("y") +
  ggtitle("Fixed effects") +
  colorspace::scale_fill_continuous_diverging("Blue-Red 3", limits = zlims) +
  theme_bw() +
  theme_custom(0.70)
## Error in ggplot2::continuous_scale(aesthetics, "continuous_diverging", : object 'zlims' not found
dat_map <- data.frame(
  temp = c(y),
  re   = c(W %*% alpha),
  x    = locs[, 1],
  y    = locs[, 2]
)

g_full <- dat_map %>%
  ggplot(aes(x = x, y = y, fill = temp)) +
  geom_raster() +
  xlab("x") +
  ylab("y") +
  ggtitle("Observed surface") +
  colorspace::scale_fill_continuous_diverging("Blue-Red 3", limits = zlims) +
  theme_bw() +
  theme_custom(0.70)
## Error in ggplot2::continuous_scale(aesthetics, "continuous_diverging", : object 'zlims' not found
g_re <- dat_map %>%
  ggplot(aes(x = x, y = y, fill = re)) +
  geom_raster() +
  xlab("x") +
  ylab("y") +
  ggtitle("Spatial process") +
  colorspace::scale_fill_continuous_diverging("Blue-Red 3", limits = zlims) +
  theme_bw() +
  theme(legend.position = "none") +
  theme_custom(0.70)
## Error in ggplot2::continuous_scale(aesthetics, "continuous_diverging", : object 'zlims' not found
g_fixed + g_re + g_full + plot_layout(guides = "collect")
## Error in eval(expr, envir, enclos): object 'g_fixed' not found

To further examine how the spatial process is constructed, the plot below shows the contribution to the full spatial process (bottom right plot) from each of the different resolutions. The plot shows how the coarsest resolution (top left) produces a coarse process over large spatial resolutions and the finest resolution (lower left) produces a finer-scale process. The three (M) resolutions are added together to produce the full process.

## MRA spatial process
W_alpha_res = unlist(sapply(1:M, function(m) W[, dims_idx == m] %*% alpha[dims_idx == m], simplify = "matrix"))
dimnames(W_alpha_res) <- list(
  site = 1:N,
  res  = paste("resolution", 1:M)
)
dat_locs <- data.frame(
  site = 1:N,
  x    = locs[, 1],
  y    = locs[, 2]
)

sim_MRA <- rbind(
  merge(dat_locs, as.data.frame.table(W_alpha_res, responseName = "W_alpha")),
  data.frame(
    site    = 1:N,
    x       = locs[, 1],
    y       = locs[, 2],
    W_alpha = W %*% alpha,
    res     = "full process"
  )
) %>%
  ggplot(aes(x = x, y = y, fill = W_alpha)) +
  geom_raster() +
  facet_wrap( ~ res, ncol = 2) +
  xlab("x") +
  ylab("y") +
  ggtitle("Multi-resolution process") +
  colorspace::scale_fill_continuous_diverging("Blue-Red 3") +
  theme_bw() +
  theme_custom()
sim_MRA

plot of chunk plot-multiresolution-process

Fitting the MRA model

To explore how well the fitted model can predict out of sample data, we take a sample \(s\) of size \(n_{sites}\) from the \(N\) simulated points and fit the model using mcmc_mra().

set.seed(111)
n_sites   <- 500
s         <- sample(N, n_sites)
y_s       <- y[s, ]
y_oos     <- y[ - s, ]
X_s       <- X[s, ]
X_oos     <- X[ - s, ]
locs_s    <- locs[s, ]
locs_oos  <- locs[ - s, ]

The plot below shows the sampled data to which the model is fit, showing the spatial patterns of the sampled data.

data.frame(
  x     = locs_s[, 1],
  y     = locs_s[, 2],
  site  = factor(1:n_sites),
  y_s     = y_s
) %>%
  ggplot(aes(x = x, y = y, fill = y_s)) +
  geom_raster() +
  colorspace::scale_fill_continuous_diverging("Blue-Red 3", limits = zlims) +
  ggtitle("Sampled y") +
  theme_bw() +
  theme_custom() +
  theme(legend.position = "none")
## Error in ggplot2::continuous_scale(aesthetics, "continuous_diverging", : object 'zlims' not found

To fit the model, we must specify the parameters form model fitting. The parameter params$n_adapt determines the number of warm-up MCMC iterations and these iterations are discarded. The parameter params$n_mcmc determines the number of post warm-up MCMC iterations (i.e., the total number of MCMC iterations is params$n_adapt + params$n_mcmc). The parameter params$n_thin determines the thinning rate for keeping posterior samples which is primarily used when the number of MCMC samples to keep stresses the computer memory usage. The total number of samples kept is params$n_mcmc / params$n_thin. The parameter params$n_message specifies the frequency of messages to display showing the progress of the MCMC algorithm. The object priors defines the priors for fitting the model. If priors is not specified, default priors are used.

params <- list(
  n_mcmc    = 500,
  n_adapt   = 500,
  n_thin    = 1,
  n_message = 50
)

priors <- list(
  alpha_tau2   = 1,
  beta_tau2    = 1,
  alpha_sigma2 = 1,
  beta_sigma2  = 1,
  mu_beta      = rep(0, ncol(X_s)),
  Sigma_beta   = 5 * diag(ncol(X_s)))

Now we can fit the model to the simulated data. Using a project-oriented workflow, we save the MCMC output that results from mcmc_mra() in a folder named results in the current project.

Fitting the model using MCMC

set.seed(999)

if (file.exists(here::here("results", "fit-mra-sim.RData"))) {
  load(here::here("results", "fit-mra-sim.RData"))
} else {
  start   <- Sys.time()
  out     <- mcmc_mra(
    y             = y_s,
    X             = X_s,
    locs          = locs_s,
    params        = params,
    priors        = priors,
    M             = M,
    n_coarse_grid = n_coarse,
    n_cores       = 1L,
    verbose       = FALSE
  )
  end     <- Sys.time()
  runtime <- end - start
  save(out, runtime, file = here::here("results", "fit-mra-sim.RData"))
}

The fitting of mcmc_mra() took 6.02 mins.

Examining the MRA MCMC output

After fitting the MCMC using mcmc_mra(), we plot the trace plots of the parameters to check for evidence that the model has not converged and to identify that the parameters have mixed. The first trace plots we examine are for the variance parameters and the regression coefficients. Overall, these trace plots look decent although the effective samples size for the \(\tau^2\)s and \(\beta\)s might be low and more samples might be needed for making inference with the model.

dat_tau2 <- data.frame(
  tau2      = c(out$tau2),
  iteration = rep(1:nrow(out$tau2), times = M),
  resolution = factor(rep(1:M, each = nrow(out$tau2)))
)

p_tau2 <- ggplot(dat_tau2, aes(x = iteration, y = tau2, group = resolution, color = resolution)) +
  geom_line() +
  scale_color_viridis_d(end = 0.8) +
  ggtitle("Trace plots for tau2") +
  theme_bw() +
  theme_custom(0.7)

dat_lambda <- data.frame(
  lambda      = c(out$lambda),
  iteration = rep(1:nrow(out$lambda), times = M),
  resolution = factor(rep(1:M, each = nrow(out$lambda)))
)

p_lambda <- ggplot(dat_lambda, aes(x = iteration, y = lambda, group = resolution, color = resolution)) +
  geom_line() +
  scale_color_viridis_d(end = 0.8) +
  ggtitle("Trace plots for lambda") +
  theme_bw() +
  theme_custom(0.7)


p_sigma2 <- data.frame(sigma2 = c(out$sigma2), iteration = 1:length(out$sigma2)) %>%
  ggplot(aes(x = iteration, y = sigma2)) +
  geom_line() +
  scale_color_viridis_d(end = 0.8) +
  ggtitle("Trace plots for sigma2") +
  theme_bw() +
  theme_custom(0.7)

dat_beta <- data.frame(
  beta      = c(out$beta),
  iteration = rep(1:nrow(out$beta), times = ncol(X)),
  covariate = factor(rep(1:ncol(X), each = nrow(out$beta)))
)
p_beta  <- ggplot(dat_beta, aes(x = iteration, y = beta, group = covariate, color = covariate)) +
  geom_line() +
  scale_color_viridis_d(end = 0.8) +
  ggtitle("Trace plots for beta") +
  theme_bw() +
  theme_custom(0.7)

(p_tau2 + p_lambda) /  (p_sigma2 + p_beta)

plot of chunk trace-plots

Next, the trace plots for the spatial parameter \(\boldsymbol{\alpha}_m\) are shown. Because there are so many parameters, a sample is taken for plotting. These trace plots suggest that perhaps \(\boldsymbol{\alpha}\) hasn’t entirely converged. However, as there is a non-identifiability between \(\tau^2_m\) and \(\boldsymbol{\alpha}_m\), this is not surprising and might not be remedied with more samples.

## posterior plots for alpha

dat_alpha <- data.frame(
  alpha = c(out$alpha),
  iteration = rep(1:nrow(out$alpha), each = ncol(out$alpha)),
  knot      = factor(unlist(sapply(1:M, function(m) 1:n_dims[m]))),
  resolution = factor(paste("resolution", dims_idx))
)

plot_idx <- sapply(1:M, function(m) sample(n_dims[m], 10))

dat_alpha %>%
  subset(
    (resolution == "resolution 1" & knot %in% plot_idx[, 1]) |
      (resolution == "resolution 2" & knot %in% plot_idx[, 2]) |
      (resolution == "resolution 3" & knot %in% plot_idx[, 3])
  ) %>%
  ggplot(aes(x = iteration, y = alpha, group = knot, color = knot)) +
  geom_line(alpha = 0.5) +
  facet_wrap( ~ resolution, ncol = 1) +
  scale_color_viridis_d(end = 0.8) +
  ggtitle("Trace plots for alpha") +
  theme_bw() +
  theme_custom(0.7) +
  theme(legend.position = "none")

plot of chunk trace-plots-alpha

Because the model was fit to simulated data, the posterior predictions can be compared to the simulated parameters to evaluate the model performance. The following plots show the 95% central credible intervals for different parameters plotted against the simulated parameters. For the regression parameters \(\boldsymbol{\beta}\), the model is doing a reasonable job of recovering these parameters. The parameter estimates for \(\tau^2_m\) and \(\boldsymbol{\alpha}_m\) show poor performance in estimating the true parameters. This is due to a non-identifiability in the model; however, this is not a concern as the spatial process \(\sum_{m=1}^M \mathbf{W}_m \boldsymbol{\alpha}_m\) is well estimated which will be shown later.

## predicted vs. estimated beta
dat_plot <- data.frame(
  truth = beta,
  mean  = apply(out$beta, 2, mean),
  lower = apply(out$beta, 2, quantile, prob = 0.025),
  upper = apply(out$beta, 2, quantile, prob = 0.975),
  parameter = factor(1:length(beta))
)

p_beta <- dat_plot %>%
  ggplot(aes(x = truth, y = mean, color = parameter)) +
  scale_color_viridis_d(begin = 0, end = 0.8) +
  geom_point(alpha = 0.5) +
  geom_linerange(aes(ymin = lower, ymax = upper)) +
  ylab("estimate") +
  geom_abline(intercept = 0, slope = 1, col = "red") +
  ggtitle("Estimated vs. predicted beta") +
  theme(legend.position = "none")

## predicted vs. estimated sigma2
p_sigma2 <- data.frame(sigma2 = out$sigma2) %>%
  ggplot(aes(y = sigma2)) +
  geom_boxplot() +
  geom_point(data = data.frame(sigma2 = sigma2),
             aes(x = 0, y = sigma2), color = "red", size = 2) +
  ylab("estimate") +
  xlab("sigma2") +
  ggtitle("Estimated vs. predicted sigma2") +
  theme(legend.position = "none",
        axis.text.x  = element_blank(),
        axis.ticks.x = element_blank())

## predicted vs. estimated tau2
dat_plot <- data.frame(
  truth = tau2,
  mean  = apply(out$tau2, 2, mean),
  lower = apply(out$tau2, 2, quantile, prob = 0.025),
  upper = apply(out$tau2, 2, quantile, prob = 0.975),
  parameter = factor(1:length(tau2))
)


p_tau2 <- dat_plot %>%
  ggplot(aes(x = truth, y = mean, color = parameter)) +
  scale_color_viridis_d(begin = 0, end = 0.8) +
  geom_point(alpha = 0.5) +
  geom_linerange(aes(ymin = lower, ymax = upper)) +
  ylab("estimate") +
  geom_abline(intercept = 0, slope = 1, col = "red") +
  ggtitle("Estimated vs. predicted tau2") +
  theme(legend.position = "none")

# plot alpha estimates
dat_plot <- data.frame(
  truth = alpha,
  mean = apply(out$alpha, 2, mean),
  lower = apply(out$alpha, 2, quantile, prob = 0.025),
  upper = apply(out$alpha, 2, quantile, prob = 0.975),
  parameter = factor(1:length(alpha)),
  res       = factor(dims_idx)
)

p_alpha <- dat_plot %>%
  ggplot(aes(x = truth, y = mean, color = res)) +
  scale_color_viridis_d(begin = 0, end = 0.8) +
  geom_point(alpha = 0.5) +
  geom_linerange(aes(ymin = lower, ymax = upper)) +
  geom_abline(intercept = 0, slope = 1, col = "red") +
  ggtitle("Estimated vs. predicted alpha /nnon-identifialbe but not really a concern") +
  ylab("estimate")

(p_beta + p_sigma2) / (p_tau2 + p_alpha)

plot of chunk estimated-vs-predicted

Next, the simulated and estimated \(\boldsymbol{\alpha}_m\) are plotted on the spatial grid. The plots show that the \(\boldsymbol{\alpha}_m\)s are not being identified.

fitted_alphas <- data.frame(
    x = unlist(sapply(1:M, function(i) MRA$locs_grid[[i]][, 1])),
    y = unlist(sapply(1:M, function(i) MRA$locs_grid[[i]][, 2])),
    res = factor(unlist(sapply(1:M, function(i) rep(i, each = nrow(MRA$locs_grid[[i]]))))),
    alpha = unlist(sapply(1:M, function(i) apply(out$alpha, 2, mean)[dims_idx == i]))
) %>%
    ggplot(aes(x = x, y = y, fill = alpha)) +
    geom_raster() +
    scale_fill_viridis_c() +
  ggtitle("Posterior mean spatial random effects") +
  facet_wrap( ~ res, ncol = 2) +
  geom_rect(
    data = NULL,
    aes(xmin = min(locs[, 1]),
        xmax = max(locs[, 1]),
        ymin = min(locs[, 2]),
        ymax = max(locs[, 2])
    ),
    fill  = NA,
    color = "black",
    alpha = 0.5
  ) +
  theme_bw() +
  theme_custom(0.5)
sim_alphas + theme_custom(0.5) + fitted_alphas

plot of chunk estimated-alphas

Despite the parameters \(\tau^2_m\) and \(\boldsymbol{\alpha}_m\) not being identifiable, the following plot shows that the full spatial process \(\sum_{m=1}^M \mathbf{W}_m \boldsymbol{\alpha}_m\). The two figures (simulated on the left, fitted on the right) demonstrate that each of the \(m\) resolutions of the process \(\mathbf{W}_m \boldsymbol{\alpha}_m\) are also non-identifiable.

## Estimated MRA spatial proces
W_alpha_res = unlist(sapply(1:M, function(m) W[, dims_idx == m] %*% apply(out$alpha, 2, mean)[dims_idx == m], simplify = "matrix"))
dimnames(W_alpha_res) <- list(
  site = 1:N,
  res  = paste("resolution", 1:M)
)
dat_locs <- data.frame(
  site = 1:N,
  lat  = locs[, 2],
  lon  = locs[, 1]
)

estimated_MRA <- rbind(
  merge(dat_locs, as.data.frame.table(W_alpha_res, responseName = "W_alpha")),
  data.frame(
    site = 1:N,
    lat  = locs[, 2],
    lon  = locs[, 1],
    W_alpha = W %*% alpha,
    res = "full process"
  )
) %>%
  ggplot(aes(x = lon, y = lat, fill = W_alpha)) +
  geom_raster() +
  facet_wrap( ~ res, ncol = 2) +
  xlab("Longitude") +
  ylab("Latitude") +
  ggtitle("Estimated Multi-resolution process") +
  colorspace::scale_fill_continuous_diverging("Blue-Red 3") +
  theme_bw() +
  theme_custom(0.5)

sim_MRA + theme_custom(0.5) + estimated_MRA

plot of chunk simulated-vs-estimated-process

The next plots show the estimated vs. simulated fixed effects, spatial process, and mean response. There is some evidence of non-identifiability between the fixed effects plot and the spatial effects plot (left two plots) as there is a bias in each of the predictions. This is understood to be a consequence of spatial confounding (Hodges and Reich (2010), Hughes and Haran (2013), Hanks et al. (2015)).

## plot estimated mean response
Xbeta_post <- t(X_s %*% t(out$beta))
Walpha_post <- t(do.call(cbind, out$MRA$W) %*% t(out$alpha))
mu_post <- Xbeta_post + Walpha_post

dat_plot <- data.frame(
  mean_Xbeta   = apply(Xbeta_post, 2, mean),
  lower_Xbeta  = apply(Xbeta_post, 2, quantile, prob = 0.025),
  upper_Xbeta  = apply(Xbeta_post, 2, quantile, prob = 0.975),
  truth_Xbeta  = X_s %*% beta,

  mean_Walpha  = apply(Walpha_post, 2, mean),
  lower_Walpha = apply(Walpha_post, 2, quantile, prob = 0.025),
  upper_Walpha = apply(Walpha_post, 2, quantile, prob = 0.975),
  truth_Walpha = (W %*% alpha)[s],

  mean_mu      = apply(mu_post, 2, mean),
  lower_mu     = apply(mu_post, 2, quantile, prob = 0.025),
  upper_mu     = apply(mu_post, 2, quantile, prob = 0.975),
  truth_mu     = X_s %*% beta + (W %*% alpha)[s]
)

plot_Xbeta <- dat_plot %>%
  ggplot(aes(x = truth_Xbeta, y = mean_Xbeta)) +
  scale_color_viridis_d(begin = 0, end = 0.8) +
  geom_point(alpha = 0.5) +
  geom_errorbar(aes(ymin = lower_Xbeta, ymax = upper_Xbeta)) +
  geom_abline(intercept = 0, slope = 1, col = "red") +
  ggtitle("Estimated vs. simulated fixed effects")

plot_Walpha <- dat_plot %>%
  ggplot(aes(x = truth_Walpha, y = mean_Walpha)) +
  scale_color_viridis_d(begin = 0, end = 0.8) +
  geom_point(alpha = 0.5) +
  geom_errorbar(aes(ymin = lower_Walpha, ymax = upper_Walpha)) +
  geom_abline(intercept = 0, slope = 1, col = "red") +
  ggtitle("Estimated vs. simulated spatial process")

plot_mu <- dat_plot %>%
  ggplot(aes(x = truth_mu, y = mean_mu)) +
  scale_color_viridis_d(begin = 0, end = 0.8) +
  geom_point(alpha = 0.5) +
  geom_errorbar(aes(ymin = lower_mu, ymax = upper_mu)) +
  geom_abline(intercept = 0, slope = 1, col = "red") +
  ggtitle("Estimated vs. simulated mean response")

plot_Xbeta + plot_Walpha + plot_mu

plot of chunk plot-sim-vs-fitted-effects

References

Hanks, Ephraim M, Erin M Schliep, Mevin B Hooten, and Jennifer A Hoeting. 2015. “Restricted Spatial Regression in Practice: Geostatistical Models, Confounding, and Robustness Under Model Misspecification.” Environmetrics 26 (4): 243–54.

Hodges, James S, and Brian J Reich. 2010. “Adding Spatially-Correlated Errors Can Mess up the Fixed Effect You Love.” The American Statistician 64 (4): 325–34.

Hughes, John, and Murali Haran. 2013. “Dimension Reduction and Alleviation of Confounding for Spatial Generalized Linear Mixed Models.” Journal of the Royal Statistical Society: Series B (Statistical Methodology) 75 (1): 139–59.

Nychka, Douglas, Soutir Bandyopadhyay, Dorit Hammerling, Finn Lindgren, and Stephan Sain. 2015. “A Multiresolution Gaussian Process Model for the Analysis of Large Spatial Datasets.” Journal of Computational and Graphical Statistics 24 (2): 579–99.

Nychka, Douglas, Dorit Hammerling, Stephan Sain, and Nathan Lenssen. 2016. “LatticeKrig: Multiresolution Kriging Based on Markov Random Fields.” Boulder, CO, USA: University Corporation for Atmospheric Research. https://github.com/NCAR/LatticeKrig.