In this tutorial we illustrate a new implementation of the model proposed in Bakka et al. (2019). It consider the problem of modeling over a spatial domain accounting for physical barriers. The original implementation is illustrated in this tutorial. The re-implementation consider a new (faster) computational method available in the INLA package. See details at the end of this vignette to update old working code to use this new implementation.
Suppose that there is a phenomena that varies over a spatial domain. Models usually consider correlation between observations made. Considering a pair of observations, the correlation is usually specified as a function of the distance between the location sites from where these observations were collected. If there is a barrier in the spatial domain, this correlation for a pair of observations taken from sites in different sides of the barrier shall consider the barrier. In resume, it should no longer be just a function of the distance. Basically, the correlation between points in two different sides of the barrier should decay faster (than the usual) to zero and “does not cross the domain”.
In order to illustrate the problem, let us define a rectangle as our spatial domain:
<- c(10, 6)
rxy <- mean(rxy)
r <- rbind(
bb c(0, 1)*rxy[1],
c(0, 1)*rxy[2])
<- cbind(
domain.xy c(0, 1, 1, 0, 0) * rxy[1],
c(0, 0, 1, 1, 0) * rxy[2])
We will now set the barrier as the two polygons defined as follows
<- cbind(
barrier1 cos(seq(0, 2*pi, length=50)) * 0.7 + 0.50 * rxy[1],
sin(seq(0, 2*pi, length=50)) * 1.0 + 0.75 * rxy[2])
<- cbind(
barrier2 c(0.50, 1.50, 1.50, 0.50, 0.50) * rxy[1],
c(0.33, 0.33, 0.40, 0.40, 0.33) * rxy[2])
Define the objects using the package:
library(sf)
#> Linking to GEOS 3.10.2, GDAL 3.4.1, PROJ 8.2.1; sf_use_s2() is TRUE
<- st_sfc(st_polygon(list(domain.xy)))
domain <- st_sfc(st_multipolygon(
barriers list(st_polygon(list(barrier1)),
st_polygon(list(barrier2)))))
We can visualize these polygons in order to make the problem clear:
library(ggplot2)
<- ggplot() +
gg0 xlab("") + ylab("") +
theme_minimal() + coord_fixed()
+ xlim(bb[1, ]) +
gg0 geom_sf(data = domain, fill = "blue") +
geom_sf(data = barriers, fill = "gray")
#> Coordinate system already present. Adding new coordinate system, which will
#> replace the existing one.
The problem is to model data in both sides of the barrier considering that the correlation between then should account for the barrier. The proposed method in Bakka et al. (2019) models the entire domain, including the barrier. To deal with the barrier, the correlation range is set to be reasonable shorter over the barrier domain than the one at the domain of interest. This idea is simple and does not add any parameter in the usual stationary model. Furthermore, it can be implemented with any inferential method.
In Bakka et al. (2019), the proposed method consider the Stochastic Partial Differential Equations - SPDE approach proposed in Lindgren, Rue, and Lindström (2011). The implementation illustrated in this tutorial deals with a discretization of the domain. The discretization should be considered with some care. The fraction fixed for the analysis interplay with the width of the barrier. One should choose a small enough fraction, see details in Bakka et al. (2019).
We first load the other packages that will be used in this tutorial.
library(INLA)
#> Loading required package: Matrix
#> Loading required package: foreach
#> Loading required package: parallel
#> Loading required package: sp
#> The legacy packages maptools, rgdal, and rgeos, underpinning this package
#> will retire shortly. Please refer to R-spatial evolution reports on
#> https://r-spatial.org/r/2023/05/15/evolution4.html for details.
#> This package is now running under evolution status 0
#> This is INLA_23.05.30-1 built 2023-05-30 11:52:19 UTC.
#> - See www.r-inla.org/contact-us for how to get help.
#> - To enable PARDISO sparse library; see inla.pardiso()
library(INLAspacetime)
library(inlabru)
library(patchwork)
Let us consider that there is an underlying random field over the specified domain. This random field will be discretized using the mesh so that a precision matrix is defined for the distribution at the mesh nodes. We will define the barrier model as proposed in Bakka et al. (2019) which make use of a triangulation.
The solution consider a discretization of the spatial domain and the simplest one is using triangles. Therefore we start with the following mesh made up of small triangles in the domain and bigger around it.
<- inla.mesh.2d(
mesh loc.domain = domain.xy,
max.edge = c(0.03, 0.1) * r,
offset = c(0.1, 0.3) * r,
cutoff = 0.01 * r)
$n
mesh#> [1] 4364
The solution needs the triangles inside the barrier domain to be identified. For each triangle center we check if it is inside or not of the barrier domain and and create a vector to identify to which domain is each triangle center inside
<- unlist(fm_contains(
triBarrier x = barriers,
y = mesh,
type = "centroid"))
We can visualize the mesh and color the triangle centers using the ggplot methods from the inlabru package
<- cbind(
triCenters.xy $loc[mesh$graph$tv[,1], 1:2] +
mesh$loc[mesh$graph$tv[,2], 1:2] +
mesh$loc[mesh$graph$tv[,3], 1:2])/3
mesh+
gg0 gg(mesh) +
geom_point(aes(
x = triCenters.xy[triBarrier, 1],
y = triCenters.xy[triBarrier, 2]))
Notice that the mesh goes beyond the domain area including a boundary extension. Then we also had defined the barrier so that it is also defined in this boundary. Therefore, when building the model over this discretization, the barrier will be well defined and the behavior of the process will be as intended.
The parameters in this model are the range and the marginal variance, We also define the range parameter in the barrier so that the method works. We consider the range for the barrier as a fraction of the range over the domain. We just use half of the average rectangle edges as the range in the domain and 10% of it in the barrier.
<- 1
sigma <- r * c(0.5, 0.05))
(ranges #> [1] 4.0 0.4
We now have to compute the Finite Element matrices needed for the model discretization, as detailed in Bakka et al. (2019).
<- mesh2fem.barrier(mesh, triBarrier) bfem
We now build the precision matrix with
<- inla.barrier.q(bfem, ranges = ranges, sigma = sigma) Q
We can use the discretized model to compute correlation between pairs of points in order to check the properties of the model. Let us define a function that computes the correlation from a given set of location to each mesh node location.
<- function(locs, mesh, Q) {
localCorrel <- nrow(locs)
nl <- sapply(1:nl, function(i)
ii which.min(rowSums(sweep(
$loc[, 1:ncol(locs)], 2, locs[i, ], "-")^2)))
mesh<- matrix(0, nrow(Q), nl)
b for(i in 1:nl)
<- 1
b[ii[i], i] <- inla.qsolve(Q, b)
cc <- sqrt(diag(inla.qinv(Q)))
s for(i in 1:nl)
<- cc[, i] / (s * s[ii[i]])
cc[, i] return(drop(cc))
}
Consider a set of locations and compute the correlations with
<- cbind(c(0.4, 0.6, 0.7, 0.5) * rxy[1],
locs c(0.7, 0.6, 0.3, 0.5) * rxy[2])
<- localCorrel(locs, mesh, Q)
mcorrels dim(mcorrels)
#> [1] 4364 4
We have the correlation from each mesh node to each of the specified locations. We would like to visualize it as an image so that we have these correlation from the location to “everywhere”. Let us build a projector to project any vector from the mesh nodes to a fine grid and projected with
<- inla.mesh.projector(
pgrid
mesh,xlim = bb[1, ],
ylim = bb[2, ],
dims = round(500 * rxy/r))
<- as.matrix(inla.mesh.project(
gcorrels field = mcorrels
pgrid, ))
We can now create a data.frame
with the expanded
coordinates of the pixels
<- data.frame(
grid.df x = rep(pgrid$x, times = length(pgrid$y)),
y = rep(pgrid$y, each = length(pgrid$x)))
The correlations can be projected and prepared for plotting with
<- do.call(
ggcorrels
rbind, lapply(1:4, function(l)
data.frame(grid.df,
loc = paste(sprintf("%1.1f", locs[l, 1]),
sprintf("%1.1f", locs[l, 2]), sep = ", "),
correlation = gcorrels[, l])))
In order to help the visualization of correlations, we will dropped the pixels with small correlations, less than \(0.1\). We can now visualize the images with
+
gg0 geom_raster(
data = ggcorrels[ggcorrels$correlation>0.1, ],
mapping = aes(x = x, y = y, fill = correlation)) +
facet_wrap(~ loc) +
+ gg.add ## look to the appendix for the code for this add.b0
Notice that the model is also defined in the barrier domain. Therefore we can do computations at the barrier. We visualized the correlation over the barriers as well, and added the barrier borders, so that we can see the action of the short spatial range over the barriers. This visual inspection helps to verify if the short range over the barrier domain was set small enough.
We consider that the random field is observed with noise. We first sample from the random field distribution.
There are several algorithms to sample from a random field specified over a continuous domain without barriers. We will use the already defined precision matrix to draw a sample at the mesh nodes with
<- inla.qsample(1, Q, seed = 1)[,1]
u #> Warning in inla.qsample(1, Q, seed = 1): Since 'seed!=0', parallel model is
#> disabled and serial model is selected
These values sampled at the mesh nodes can be projected to a set of small pixels for visualization purpose with
<- inla.mesh.project(pgrid, field = u) ugrid.sim
We can now visualize the projected simulated values at the small pixels, inclusive those values in the barriers.
$u <- as.vector(ugrid.sim)
grid.df+
gg0 geom_raster(
data = grid.df,
aes(x = x, y = y, fill = u)) +
+ gg.add ## look to the appendix for the code for this add.b0
The visualization of the simulated process over the barrier allows a visual inspection of the model property. The process in different sides of the barriers seen to be independent, as illustrated by the correlation plots.
We define a initial set of locations to consider that we observe the field at these locations. We start with a set of locations chosen completely at random in the rectangle domain, including a barrier part
<- 500
n0 set.seed(2)
<- cbind(
xy0 runif(n0, bb[1, 1], bb[1, 2]),
runif(n0, bb[2, 1], bb[2, 2]))
Then we check which of these is inside the barrier
<- (splancs::inout(xy0, barrier1)) |
xy.b ::inout(xy0, barrier2))
(splancs<- which(!xy.b) ii
The locations to form the data consider only the locations in the desired domain and outside the barrier
<- data.frame(
dataset x = xy0[ii, 1], y = xy0[ii, 2])
<- nrow(dataset))
(n #> [1] 464
To simulate the outcome, we project the field to these locations, add an intercept and some random noise
<- 1
sigma.e set.seed(3)
$outcome <-
datasetdrop(inla.mesh.project(
mesh,loc = cbind(dataset$x, dataset$y),
field = u)) +
10 + rnorm(n, 0.0, sigma.e)
Here we illustrate the use of the function in
INLAspacetime to implement the barrier model as in
Bakka et al. (2019). This implementation
consider the cgeneric
computational method that is useful
to implement new models with INLA. This method allows
the computations in INLA to take full advantage of
parallel computations, achieving shorter computation time than with the
original implementation.
We define the model object with
<- barrierModel.define(
bmodel mesh = mesh,
barrier.triangles = triBarrier,
prior.range = c(1, 0.01), ## P(range < 1) = 0.01
prior.sigma = c(1, 0.01), ## P(sigma > 1) = 0.01
range.fraction = 0.1)
We use the model formula in the inlabru way, where
the field
will be the name for the spatial effect:
<- outcome ~ Intercept(1) +
model field(cbind(x, y), model = bmodel)
The inlabru main function can be supplied with only the model formula, the data and the family, as follows
<- bru(
result family = "gaussian") model, dataset,
We can see the summary for the posterior marginal distribution for the intercept with
$summary.fix
result#> mean sd 0.025quant 0.5quant 0.975quant mode
#> Intercept 9.999526 0.6151982 8.774542 9.990566 11.27748 9.976343
#> kld
#> Intercept 6.737575e-06
For the hyper-parameters the computations were performed in an internal scale. That is log of range and log(\(\sigma\)), and \(\log(1/\sigma_e^2)\). However, we can transform each of the hyper-parameters from the internal marginal scale to its scale with
<-
pmarginals list(
data.frame(
param = "sigma.e",
inla.tmarginal(
function(x) exp(-x/2),
$internal.marginals.hyperpar[[1]])),
resultdata.frame(
param = "range",
inla.tmarginal(
function(x) exp(x),
$internal.marginals.hyperpar[[2]])),
resultdata.frame(
param = "sigma",
inla.tmarginal(
function(x) exp(x),
$internal.marginals.hyperpar[[3]]))
result )
From these transformed posterior marginal distributions we can extract a summary to compare with the values used to sample the data:
rbind(true = c(sigma.e = sigma.e, range = ranges[1], sigma = 1),
sapply(pmarginals, function(m)
unlist(inla.zmarginal(m[, -1], TRUE))[1:2]))
#> sigma.e range sigma
#> true 1.00000000 4.000000 1.0000000
#> mean 1.03459040 5.048045 1.1868571
#> sd 0.03772133 1.256295 0.1858563
We can also visualize these these posterior marginal distributions with
ggplot(do.call(rbind, pmarginals)) +
geom_line(aes(x=x, y=y)) +
facet_wrap(~param, scales = "free") +
theme_minimal()
We can visualize summaries from the fitted field. The posterior mean and the standard deviation can be projected into small pixels and added to the raster data with
$u.mean <- as.vector(
grid.dfinla.mesh.project(
pgrid,$summary.random$field$mean))
result$u.sd <- as.vector(
grid.dfinla.mesh.project(
pgrid,$summary.random$field$sd)) result
To help the visualization, we will filter the grid pixels considering the barriers.
<-
gInBarrier ::inout(cbind(grid.df$x, grid.df$y), barrier1)) |
(splancs::inout(cbind(grid.df$x, grid.df$y), barrier2)) (splancs
We can now visualize the posterior mean with
+
gg0 geom_raster(
data = grid.df[!gInBarrier, ],
aes(x = x, y = y, fill = u.mean)) +
## look to the appendix for the code for this gg.add
The estimated field, considering the posterior mean shown, exhibits a different pattern in each side of the barrier, particularly far from its tip. Therefore we successfully accounted for the barrier in the model. See this tutorial for details and how to visualize the implied correlation between two points.
Similarly, we can plot the posterior standard deviation of the field, and the data locations used to estimate the model. This has lower values around more densely sampled locations and higher values otherwise. The highest are near the boundaries, including the boundary at the barriers, and far from observed locations.
+
gg0 geom_raster(
data = grid.df[!gInBarrier, ],
aes(x = x, y = y, fill = u.sd)) +
geom_point(data = dataset, aes(x = x, y = y)) +
## look to the appendix for the code for this gg.add
To adapt code based in this tutorial, one should consider that there are two new functions implemented, as detailed bellow:
Old | New |
---|---|
inla.barrier.fem() | mesh2fem.barrier() |
inla.barrier.pcmatern() | barrierModel.define() |
The arguments in the new functions have the same names as in the old ones. The new function used to define the model has additional arguments. See the help of this function for details.
The additional ggplot2 code used
<- list(
gg.add scale_fill_distiller(
type = "div",
palette = "RdBu",
na.value = "transparent")
)<- list(
add.b0 geom_polygon(
mapping = aes(x, y),
data = data.frame(
x = pmin(barrier1[,1], bb[1, 2]),
y = pmin(barrier1[,2], bb[2, 2])
colour = "black",
), fill = gray(0.9, 0.5)), ##"transparent"),
geom_polygon(
mapping = aes(x, y),
data = data.frame(
x = pmin(barrier2[,1], bb[1, 2]),
y = pmin(barrier2[,2], bb[2, 2])
colour = "black",
), fill = gray(0.9, 0.5))##"transparent")
)