In this topic we show an example of the Barrier model, with simulation, inference and plotting. The example terrain/map/study-area has a barrier going from west to east, and a gap in the middle of the barrier.
This page is (as usual) a self-contained example of a good solution.
At the end follows sections on how to avoid errors when constructing an example like this, and some extra information on how to do things that are slightly different from this example.
We load the libraries and functions we need. You need to have a “testing version” of INLA.
You may need to install these libraries (Installation and general troubleshooting). We also set random seeds to be used later.
library(INLA)
library(fields)
library(rgdal)
library(viridisLite)
set.seed(2016)
set.inla.seed = 2016
We will create the representation of our spatial domain, i.e. the locations where the random effect is defined.
We set up a square polygon p
to define the barrier. In
this example, I know I want to have a \([0,
10]\times [0, 10]\) study area, and I construct the barrier to
fit inside this. In general, to use these barrier models, you need the
barrier region (e.g. land) to be represented by a polygon. In most
cases, you will use shapefiles or maps or other sources for determining
your barrier, as in BTopic127 and BTopic128.
Talking about polygons in general, however, is out of the scope of
this tutorial. More information about polygons can be found in the
R-packages sp
and maps
.
smalldist = 0.2
# - the width of the opening in the barrier
width = 0.5
# - The width/thickness of the barrier
local.square.polygon = function(xlim, ylim){
# - output is a square
xlim = range(xlim); ylim = range(ylim)
corner1 = c(xlim[1], ylim[2])
corner2 = c(xlim[2], ylim[1])
poly = Polygon(rbind(corner1, c(corner1[1], corner2[2]), corner2, c(corner2[1], corner1[2]), corner1), hole = FALSE)
return(SpatialPolygons(list(Polygons(list(poly), ID = runif(1)))))
}
poly1 = local.square.polygon(xlim=c(-1, 5-smalldist/2),
ylim=5+width*c(-.5, .5))
poly2 = local.square.polygon(xlim=c(5+smalldist/2, 11),
ylim=5+width*c(-.5, .5))
poly.original = SpatialPolygons(c(poly1@polygons, poly2@polygons))
plot(poly.original, main="Barrier area polygon")
First we set the input variables. Feel free to experiment with
different values for smalldist
and width
, as
changes can result in markedly different behavior.
We set up the mesh
which will be the numerical/discrete
representation of the spatial study region. We will not describe the
process of mesh construction in this section. Good mesh construction is
a somewhat involved process see BTopic104.
Some of you may know how to construct good meshes already,
e.g. from
the
SPDE-tutorial.
max.edge.length = 0.4
# - The coarseness of the finite element approximation
# - Corresponds to grid-square width in discretisations
# - - Except that finite element approximations are better
# - Should be compared to size of study area
# - Should be less than a fourth of the estimated (posterior)
# spatial range
# - Up to 8x computational time when you halve this value
loc1 = matrix(c(0,0, 10,0, 0,10, 10,10), 4, 2, byrow = T)
# - This defines the extent of the interior part of the mesh
# - In an application, if you want the mesh to depend on your
# data locations, you may use those locations instead
seg = inla.sp2segment(poly.original)
# - Transforms a SpatialPolygon to an "inla polygon"
mesh = inla.mesh.2d(loc=loc1, interior = seg,
max.e = max.edge.length, offset=1)
# - The INLA mesh constructor, used for any INLA-SPDE model
The interior
argument of inla.mesh.2d
makes
sure that the triangles in the mesh respect the boundary of our
barrier.
Next, we will pick out which triangles in the mesh belongs to the barrier area.
tl = length(mesh$graph$tv[,1])
# - the number of triangles in the mesh
posTri = matrix(0, tl, 2)
for (t in 1:tl){
temp = mesh$loc[mesh$graph$tv[t, ], ]
posTri[t,] = colMeans(temp)[c(1,2)]
}
posTri = SpatialPoints(posTri)
# - the positions of the triangle centres
barrier = over(poly.original, SpatialPoints(posTri), returnList=T)
# - checking which mesh triangles are inside the barrier area
barrier = unlist(barrier)
poly.barrier = inla.barrier.polygon(mesh, barrier.triangles = barrier)
# - the Barrier model's polygon
# - in most cases this should be the same as poly.original
Then we define the Omega
object which is the object that
the code for the new Barrier model needs. We also define the
poly.barrier
which is the spatial polygon needed for
plotting the boundaries of the barrier area.
At this point, it is crucial to check that our mesh, and the two
areas (normal area and barrier area) are correct. We make sure to always
use poly.barrier
to plot the barrier from now on, since
this is the same as what is used by the algorithm (which may not be true
for our original polygon p
).
In this figure we see the entire mesh. The grey background is for the barrier area. The light blur background is for the normal area. The blue and red squares are the boundaries of the barrier area. The black circles are our initial locations (here, we use them only to determine the extent of the mesh).
plot(mesh, main="Mesh and Omega")
plot(poly.barrier, add=T, col='lightblue')
plot(mesh, add=T)
points(loc1)
The triangles here are light blue (normal area) and grey (barrier
area). The four circles are the mesh locations. The blue and red squares
are a part of the mesh
object.
Next we define a function defining how to plot any spatial fields
(for this example). We have also defined the part of space that is to be
plotted, in any future plots, namely xlim=ylim=c(2,8)
.
(Since we only care about this region, the mesh extension is large
enough. We discuss mesh extensions in a different topic)
local.plot.field = function(field, ...){
xlim = c(2, 8); ylim = xlim;
proj = inla.mesh.projector(mesh, xlim = xlim,
ylim = ylim, dims=c(300, 300))
# - Can project from the mesh onto a 300x300 grid
# for plots
field.proj = inla.mesh.project(proj, field)
# - Do the projection
image.plot(list(x = proj$x, y=proj$y, z = field.proj),
xlim = xlim, ylim = ylim, col = plasma(17), ...)
# - Use image.plot to get nice colors and legend
}
print(mesh$n)
## [1] 2604
# - This is the appropriate length of the field variable
The input variable field
must have the same length as
there are mesh nodes in the mesh.
Next, we compute the Finite Element matrices needed to solve the SPDE for the non-stationary Barrier model. \[\begin{align} u(s) - \nabla \cdot \frac{r^2}{8} \nabla u(s) &= r \sqrt{\frac{\pi}{2}} \sigma_u \mathcal{W}(s), \text{ for } s \in \Omega_n \\ u(s) - \nabla \cdot \frac{r_b^2}{8} \nabla u(s) &= r_b \sqrt{\frac{\pi}{2}} \sigma_u \mathcal{W}(s), \text{ for } s \in \Omega_b, \end{align}\] To learn more about these equations, see the paper on Arxiv (Bakka et al. 2016), and the appendix therein. Essentially, we are doing all the computations we can in advance, so that solving the system of equations happens quickly at each step of simulation and inference. We will not comment on the internal workings of this function now, but it will be described in detail in another topic.
barrier.model = inla.barrier.pcmatern(mesh, barrier.triangles = barrier, prior.range = c(1.44, 0.5), prior.sigma = c(0.7, 0.5), range.fraction = 0.1)
# - Set up the inla model, including the matrices for solving the SPDE
In this subsection, we simulate data. When simulating data, we are in a sense pretending to be nature, producing a set of observations for the human observers. We will do inference on this data further down on this page.
First we set the ranges, with a range 0.3 on the first region, i.e. the barrier, and a range of 3 in the second region, i.e. the normal region. The range is the distance at which correlation is essentially zero. Then we compute the precision matrix \(Q\) (inverse covariance matrix) for the spatial random field \[u \sim \mathcal N(0, Q^{-1})\]
range = 3
# - the spatial range parameter
Q = inla.rgeneric.q(barrier.model, "Q", theta = c(0, log(range)))
# - the precision matrix for fixed ranges
# - Q is a function of the hyperparameters theta = c( log(sigma), log(range1), log(range2),...)
Now, we sample a spatial field using the precision matrix \(Q\). This is done through a sparse Cholesky factorization (Rue and Held 2005). Then we plot that field, adding the barrier area in grey on top. The value of the field on the barrier is of no interest, as there can never be observations there.
u = inla.qsample(n=1, Q=Q, seed = set.inla.seed)
u = u[ ,1]
# - access the first sample
local.plot.field(u, main="The true (simulated) spatial field")
plot(poly.barrier, add=T, col='grey')
# - Overlay the barrier
Now we simulate the spatial locations and the data. We never have observation locations in the barrier region (e.g. no measurements of fish on land), so the easiest way to sample locations is to sample everywhere, and then just delete those samples that end up in the barrier region. The projector matrix is the function taking a field-defined-on-the-mesh into a field-defined-on-the-data-locations, see BTopic101.
num.try = 500
# - try to sample this number of data locations
loc.try = matrix(runif(num.try*2, min=2, max=8),
num.try, 2)
# - locations sampled inside the barrier will be removed
# in a few lines
temp = SpatialPoints(loc.try)
loc.ok = is.na(over(temp, poly.barrier))
# - only allow locations that are not inside the Barrier area
loc.data = loc.try[loc.ok, ]
A.data = inla.spde.make.A(mesh, loc.data)
# - the projector matrix required for any spatial model
# - this matrix can transform the field-defined-on-the-mesh
# to the field-defined-on-the-data-locations
c(dim(A.data), mesh$n, nrow(loc.data))
## [1] 469 2604 2604 469
# - shows that the dimensions are correct
u.data = A.data %*% u
# - project the field from the finite element
# representation to the data locations
Next we create the dataframe object.
df = data.frame(loc.data)
# - df is the dataframe used for modeling
names(df) = c('locx', 'locy')
sigma.u = 1
# - size of the random effect
# - feel free to change this value
sigma.epsilon = 0.2
# - size of the iid noise in the Gaussian likelihood
# - feel free to change this value
df$y = drop(sigma.u*u.data + sigma.epsilon*rnorm(nrow(df)))
# - sample observations with gaussian noise
We have now completed the simulation of the spatial dataset.
summary(df)
## locx locy y
## Min. :2.0 Min. :2.0 Min. :-3.4
## 1st Qu.:3.4 1st Qu.:3.7 1st Qu.:-0.9
## Median :5.0 Median :5.5 Median :-0.5
## Mean :5.0 Mean :5.1 Mean :-0.5
## 3rd Qu.:6.5 3rd Qu.:6.7 3rd Qu.: 0.0
## Max. :8.0 Max. :8.0 Max. : 1.6
This is the entire dataframe, and is the only object we ‘know’, besides the spatial mesh and barrier, when we are performing inference.
The model in this subsection not the new model, but the standard spatial model in INLA. We add this for comparing the stationary and Barrier model. This means that the model in this subsection is not the model used for simulation (that model will come in the next subsection).
The stack will be used both for the stationary model and for the Barrier model. This is just a way of organizing the different variables. We could have written this example without using the stack, but if you want to use this model for anything more advanced, you will need this stack functionality.
stk <- inla.stack(data=list(y=df$y), A=list(A.data, 1),
effects=list(s=1:mesh$n,
intercept=rep(1, nrow(df))),
remove.unused = FALSE, tag='est')
# - this is the common stack used in INLA SPDE models
# - see the SPDE-tutorial
# - - http://www.r-inla.org/examples/tutorials/spde-tutorial
Next, we set up a standard INLA call for a stationary spatial effect.
We use the PC prior for the Gaussian \(\sigma_\epsilon\). We start the numerical
optimizer with a good value in control.mode
, to reduce the
computational time (you may remove this line, but it will take longer).
We know where to start the optimizer because we have run this model one
without that line. In your case, you may run a subset of your data to
find good starting values, especially for space-time models.
model.stat = inla.spde2.pcmatern(mesh, prior.range = c(1, 0.5), prior.sigma = c(1, 0.5))
# - Set up the model component for the spatial SPDE model:
# Stationary Matern model
# - I assume you are somewhat familiar with this model
formula <- y ~ 0 + intercept + f(s, model=model.stat)
# - Remove the default intercept
# - - Having it in the stack instead improves the numerical
# accuracy of the INLA algorithm
# - Fixed effects + random effects
res.stationary <- inla(formula, data=inla.stack.data(stk),
control.predictor=list(A = inla.stack.A(stk)),
family = 'gaussian',
control.family = list(hyper = list(prec = list(
prior = "pc.prec", fixed = FALSE,
param = c(0.2,0.5)))),
control.mode=list(restart=T, theta=c(4,-1.7,0.25)))
We look at the standard summary.
summary(res.stationary)
##
## Call:
## c("inla.core(formula = formula, family = family, contrasts =
## contrasts, ", " data = data, quantiles = quantiles, E = E,
## offset = offset, ", " scale = scale, weights = weights,
## Ntrials = Ntrials, strata = strata, ", " lp.scale = lp.scale,
## link.covariates = link.covariates, verbose = verbose, ", "
## lincomb = lincomb, selection = selection, control.compute =
## control.compute, ", " control.predictor = control.predictor,
## control.family = control.family, ", " control.inla =
## control.inla, control.fixed = control.fixed, ", " control.mode
## = control.mode, control.expert = control.expert, ", "
## control.hazard = control.hazard, control.lincomb =
## control.lincomb, ", " control.update = control.update,
## control.lp.scale = control.lp.scale, ", " control.pardiso =
## control.pardiso, only.hyperparam = only.hyperparam, ", "
## inla.call = inla.call, inla.arg = inla.arg, num.threads =
## num.threads, ", " blas.num.threads = blas.num.threads, keep =
## keep, working.directory = working.directory, ", " silent =
## silent, inla.mode = inla.mode, safe = FALSE, debug = debug, ",
## " .parent.frame = .parent.frame)")
## Time used:
## Pre = 3.59, Running = 7.89, Post = 0.0623, Total = 11.5
## Fixed effects:
## mean sd 0.025quant 0.5quant 0.97quant mode kld
## intercept -0.3 0.35 -0.97 -0.32 0.4 -0.34 0
##
## Random effects:
## Name Model
## s SPDE2 model
##
## Model hyperparameters:
## mean sd 0.025quant
## Precision for the Gaussian observations 28.051 2.871 22.77
## Range for s 2.527 0.567 1.69
## Stdev for s 0.989 0.174 0.72
## 0.5quant 0.97quant mode
## Precision for the Gaussian observations 27.924 33.80 27.700
## Range for s 2.434 3.82 2.225
## Stdev for s 0.964 1.38 0.903
##
## Marginal log-Likelihood: -152.92
## is computed
## Posterior summaries for the linear predictor and the fitted values are computed
## (Posterior marginals needs also 'control.compute=list(return.marginals.predictor=TRUE)')
We plot the result of the stationary model in the next figure.
local.plot.field(res.stationary$summary.random$s$mean,
main="Spatial estimate with the stationary model")
# - plot the posterior spatial marginal means
# - we call this the spatial estimate, or the smoothed data
plot(poly.barrier, add=T, col='grey')
# - Posterior spatial estimate using the stationary model
This is the same model as we used for simulating the data. Except that we have now “forgotten” the range \(r_b\) in the barrier area. This range is never known, so it is unreasonable to use it for inference. However, using any small value for the range in the barrier region is OK, it does not need to be the “true value”. For more information about this, see appendix “Choosing barrier range” in (Bakka et al. 2016). The Barrier model is a special case of the Different
First we set up extra variables needed for my implementation of the
new model. My implementation is based on “rgeneric” in INLA, see
\inlinecode{inla.doc("rgeneric")}
. This part of the code
defines what range parameters belong to each area.
formula2 <- y ~ 0 + intercept + f(s, model=barrier.model)
# - The spatial model component is different from before
# - The rest of the model setup is the same as in the stationary case!
# - - e.g. the inla(...) call below is the same,
# only this formula is different
Finally, we are ready to run inference on the simulated dataset.
Similarly to the stationary case, you may remove the
control.mode
input. That will cause the model to run
slightly slower. If you have trouble with inference in a different
dataset, setting these values (the initial values for the algorithm)
often solves the problem. Please note that these are the values for the
internal parametrization, so \(\log(precision), \log(sigma),
\log(range)\), see
result$internal.summary.hyperpar
.
res.barrier = inla(formula2, data=inla.stack.data(stk),
control.predictor=list(A = inla.stack.A(stk)),
family = 'gaussian',
control.family = list(hyper = list(prec = list(
prior = "pc.prec", fixed = FALSE,
param = c(0.2,0.5)))),
control.mode=list(restart=T, theta=c(3.2, 0.4, 1.6)))
summary(res.barrier)
##
## Call:
## c("inla.core(formula = formula, family = family, contrasts =
## contrasts, ", " data = data, quantiles = quantiles, E = E,
## offset = offset, ", " scale = scale, weights = weights,
## Ntrials = Ntrials, strata = strata, ", " lp.scale = lp.scale,
## link.covariates = link.covariates, verbose = verbose, ", "
## lincomb = lincomb, selection = selection, control.compute =
## control.compute, ", " control.predictor = control.predictor,
## control.family = control.family, ", " control.inla =
## control.inla, control.fixed = control.fixed, ", " control.mode
## = control.mode, control.expert = control.expert, ", "
## control.hazard = control.hazard, control.lincomb =
## control.lincomb, ", " control.update = control.update,
## control.lp.scale = control.lp.scale, ", " control.pardiso =
## control.pardiso, only.hyperparam = only.hyperparam, ", "
## inla.call = inla.call, inla.arg = inla.arg, num.threads =
## num.threads, ", " blas.num.threads = blas.num.threads, keep =
## keep, working.directory = working.directory, ", " silent =
## silent, inla.mode = inla.mode, safe = FALSE, debug = debug, ",
## " .parent.frame = .parent.frame)")
## Time used:
## Pre = 3, Running = 12.6, Post = 0.0595, Total = 15.6
## Fixed effects:
## mean sd 0.025quant 0.5quant 0.97quant mode kld
## intercept -0.33 0.31 -0.95 -0.34 0.29 -0.35 0
##
## Random effects:
## Name Model
## s RGeneric2
##
## Model hyperparameters:
## mean sd 0.025quant
## Precision for the Gaussian observations 27.779 2.84 22.579
## Theta1 for s -0.118 0.16 -0.406
## Theta2 for s 0.893 0.20 0.535
## 0.5quant 0.97quant mode
## Precision for the Gaussian observations 27.649 33.476 27.408
## Theta1 for s -0.127 0.207 -0.166
## Theta2 for s 0.880 1.299 0.831
##
## Marginal log-Likelihood: -146.67
## is computed
## Posterior summaries for the linear predictor and the fitted values are computed
## (Posterior marginals needs also 'control.compute=list(return.marginals.predictor=TRUE)')
We plot the result of the Barrier model in the next figure.
local.plot.field(res.barrier$summary.random$s$mean,
main="Spatial posterior for Barrier model")
# - plot the posterior spatial marginal means
# - we call this the spatial (smoothing) estimate
plot(poly.barrier, add=T, col='grey')
# - Posterior spatial estimate using the Barrier model
Compare this plot to the plot “The true (simulated) field” and to the stationary solution “Posterior spatial estimate using the stationary model”. Depending on the simulated field (i.e. the random seeds), these figures may look very much the same, or be very different. Our experience is that, even when the spatial means look quite different, there may be a big difference in model fit or prediction evaluations. Feel free to also plot the quantiles and sd of the two posterior fields. For a discussion of the results we refer you to the main paper .
For how to plot the posterior of the hyper-parameters in the stationary model, see BTopic108.
res.barrier$summary.hyperpar
## mean sd 0.025quant
## Precision for the Gaussian observations 27.78 2.84 22.58
## Theta1 for s -0.12 0.16 -0.41
## Theta2 for s 0.89 0.20 0.53
## 0.5quant 0.97quant mode
## Precision for the Gaussian observations 27.65 33.48 27.41
## Theta1 for s -0.13 0.21 -0.17
## Theta2 for s 0.88 1.30 0.83
This shows the ordering of the hyper-parameters, and is how we know what index to use later in the plots.
To plot the hyper-parameters in the Barrier model, we must know the transformations. \[\begin{align} \sigma &= e^ {\theta_1} \quad \text{is the marginal standard deviation} \\ r &= e^ {\theta_2} \quad \text{is the spatial range} \end{align}\]
For the parameter \(\sigma\), the code is as follows (the dashed line is the prior, with an arbitrary scaling).
tmp = inla.tmarginal(function(x) exp(x), res.barrier$marginals.hyperpar[[2]])
plot(tmp, type = "l", xlab = "sigma", ylab = "Density")
xvals = seq(0, 10, length.out=1000)
lambda = 0.99; lines(xvals, 3*exp(-lambda*xvals), lty='dashed')
abline(v=1, col="blue")
For the parameter \(r\), the code is as follows (the dashed line is the prior, with an arbitrary scaling).
tmp = inla.tmarginal(function(x) exp(x), res.barrier$marginals.hyperpar[[3]])
plot(tmp, type = "l", xlab = "r", ylab = "Density")
xvals = seq(0, 10, length.out=1000)
lambda = 1.00; lines(xvals, 3*exp(-lambda*xvals), lty='dashed')
abline(v=range, col="blue")
6 Comments
6.1 Mistakes to avoid
I will not discuss potential mistakes from the definition of the mesh or the barrier, as that is included in other topics. Make sure that you do not mix up what area is the barrier, and what is the normal.
6.2 How to use this as a model component in other models
Here, I outline how to do things that are slightly different.
To add more covariates in the model, add them in the
inla.stack
. See the SPDE-book for more information on this .You can easily use other likelihoods, by changing the argument of the -call. See for a description of these. Please make sure you choose good priors for any hyper-parameters in the likelihood (the choice we made in the code here is aarbitrary and not good).
You can also add different random effects to the formula (see info about latent models on ). When you use additional random effects, you must include them into the stack, similarly to how the intercept is here included in the stack. For examples of this, use the SPDE-book.
References