library(INLA)
library(inlabru)
library(sf)
library(rnaturalearth)
library(ggplot2)
library(viridis)
library(terra)
library(arrow)
library(dplyr)
library(stringr)
library(readr)
3 Spatial modelling with INLA and SPDE
3.1 INLA
Having covered the need-to-know material for INLA and SPDE, next I’m going to build a spatial model using both INLA
and inlabru
. Comparing the two packages is useful because, though they can be expected to give the same results in most cases, the implementation is a bit different. Knowing when using the more complicated INLA
can be justified is a useful exercise, I think. The template for this code using INLA
follows the air pollution example from the geostatistical chapter in Paula Moraga’s Spatial Statistics book, https://www.paulamoraga.com/book-spatial/sec-geostatisticaldataSPDE.html. The difference is that I will use air pollution data from the Netherlands, compare INLA
and inlabru
, and consider a few extra technical details. For the inlabru
code I follow the examples the authors provide at their site for the package: https://inlabru-org.github.io/inlabru/articles/.
The model fit here is a simple geostatistical one. I’ll be using air pollution data in the Netherlands. The model can be written as \[ Y_i \sim N(u_i, \sigma^2), \] with mean \(\mu_i\) parameterized as \[ u_i = \beta_0 + \beta_1\cdot\text{temperature}_i + \beta_2\cdot\text{precipitation}_i + S(x_i). \] So it’s a typical Gaussian distributed variable with underlying latent structure \(S(x_i)\) that is spatially indexed. It represents measurements taken at discrete locations but used to describe or estimate a spatially continuous process, such as air pollution.
Let’s get straight to data downloading and processing. I downloaded air pollution data in the Belgium and Netherlands for the year of 2023 from https://eeadmz1-downloads-webapp.azurewebsites.net/. For now I just focus on the Netherlands data.
In the dataset, 8 is the id for Nitrogen dioxide, 5 is for Particulate matter < 10 µm, and 6001 for Particulate matter < 2.5 µm. I focused on Particulate matter < 2.5 µm, referred to as PM2.5.
#data_path <- file.path("D:", "data", "air_quality_data", "belgium_eea", "E1a")
<- file.path("D:", "data", "air_quality_data", "netherlands_eea", "E1a")
data_path <- open_dataset(data_path)
d %>% group_by(Samplingpoint) |> filter(Pollutant==6001, Value>-1) |> collect() -> df
d df
# A tibble: 442,873 × 12
# Groups: Samplingpoint [53]
Samplingpoint Pollutant Start End Value Unit
<chr> <int> <dttm> <dttm> <dbl> <chr>
1 NL/SPO-NL00007… 6001 2023-01-01 01:00:00 2023-01-01 02:00:00 131. ug.m…
2 NL/SPO-NL00007… 6001 2023-01-01 02:00:00 2023-01-01 03:00:00 86.2 ug.m…
3 NL/SPO-NL00007… 6001 2023-01-01 03:00:00 2023-01-01 04:00:00 30.7 ug.m…
4 NL/SPO-NL00007… 6001 2023-01-01 04:00:00 2023-01-01 05:00:00 11 ug.m…
5 NL/SPO-NL00007… 6001 2023-01-01 05:00:00 2023-01-01 06:00:00 9.1 ug.m…
6 NL/SPO-NL00007… 6001 2023-01-01 06:00:00 2023-01-01 07:00:00 5.3 ug.m…
7 NL/SPO-NL00007… 6001 2023-01-01 07:00:00 2023-01-01 08:00:00 2.8 ug.m…
8 NL/SPO-NL00007… 6001 2023-01-01 08:00:00 2023-01-01 09:00:00 2.3 ug.m…
9 NL/SPO-NL00007… 6001 2023-01-01 10:00:00 2023-01-01 11:00:00 -0.6 ug.m…
10 NL/SPO-NL00007… 6001 2023-01-01 11:00:00 2023-01-01 12:00:00 1.4 ug.m…
# ℹ 442,863 more rows
# ℹ 6 more variables: AggType <chr>, Validity <int>, Verification <int>,
# ResultTime <dttm>, DataCapture <dbl>, FkObservationLog <chr>
Next add the the locations of the measuring stations to the dataset. The air pollution observations include an station identifier, but the coordinates of each of these stations is not included. It comes in a separate dataset.
<- df %>%
df #mutate(Samplingpoint = str_remove(Samplingpoint, "BE/"))
mutate(Samplingpoint = str_remove(Samplingpoint, "NL/"))
#station_location_path <- file.path("D:", "data", "air_quality_data", "eea_stations_2023", "belgium_stations_2023.csv")
<- file.path("D:", "data", "air_quality_data", "eea_stations_2023", "nl_stations_2023.csv")
station_location_path <- read_csv(station_location_path) station_locations
Rows: 4685 Columns: 27
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (19): Country, Air Quality Network, Air Quality Network Name, Air Qualit...
dbl (8): Year, Air Pollution Level, Data Coverage, Verification, Longitude,...
ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
<- rename(station_locations, Samplingpoint = 'Sampling Point Id')
station_locations %>%
station_locations distinct(Samplingpoint, Longitude, Latitude) -> unique_locations
<- df %>%
merged_df left_join(unique_locations %>% select(Samplingpoint, Longitude, Latitude), by = "Samplingpoint")
<- st_as_sf(station_locations, coords = c("Longitude", "Latitude"))
station_locations_sf <- st_as_sf(merged_df, coords = c("Longitude", "Latitude"))
air_sf st_crs(air_sf) <- "EPSG:4326"
Then examine the time series
<- unique(air_sf$Samplingpoint)[1:12]
distinct_stations <- air_sf[air_sf$Samplingpoint%in%distinct_stations,]
plotting_stations %>%
plotting_stations ggplot(aes(x=Start, y=Value)) +
geom_line() +
facet_wrap(~ Samplingpoint) +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) +
labs(title = "Time series of monitoring stations", x = "Time, hourly", y = "Conc (μg/m³)")
The above figure shows 12 of the stations observations over 2023. The data is currently at the hourly resolution.
Next, load a map of the Netherlands. The border is from https://service.pdok.nl/kadaster/bestuurlijkegrenzen/atom/bestuurlijke_grenzen.xml. A number of processing steps are done here, namely to generate some grid points within the map border. These points will be used as locations to make spatial predictions later on.
<- st_read(file.path("D:", "data", "maps", "netherlands_bestuurlijkegrenzen_2021", "bestuurlijkegrenzen.gpkg"))
map <- st_union(map)
map <- st_as_sf(map)
map <- st_transform(map, crs = st_crs(air_sf))
map # raster grid covering map
<- terra::rast(map, nrows = 100, ncols = 100)
grid # coordinates of all cells
<- terra::xyFromCell(grid, 1:ncell(grid))
xy # transform points to a sf object
<- st_as_sf(as.data.frame(xy), coords = c("x", "y"),
dp crs = st_crs(map))
# indices points within the map
<- which(st_intersects(dp, map,
indicespointswithin sparse = FALSE))
# points within the map
<- st_filter(dp, map) dp
Let’s check the map and grid of prediction points.
# plot
ggplot() + geom_sf(data = map) +
geom_sf(data = dp)
We can see the grid of spatial points is at quite a high resolution.
The next step is to download temperature and precipitation data using the geodata
package.
library(geodata)
# With geodata library
<- file.path("D:", "data", "air_quality_data", "aux_variables")
save_path <- worldclim_global(var = "tavg", res = 10,
covtemp path = save_path)
<- worldclim_global(var = "prec", res = 10,
covprec path = save_path)
# Extract at observed locations
$covtemp <- extract(mean(covtemp), st_coordinates(air_sf))[, 1]
air_sf$covprec <- extract(mean(covprec), st_coordinates(air_sf))[, 1]
air_sf# Extract at prediction locations
$covtemp <- extract(mean(covtemp), st_coordinates(dp))[, 1]
dp$covprec <- extract(mean(covprec), st_coordinates(dp))[, 1] dp
We can then take a look at the air pollution and the covariates at the measuring stations locations.
ggplot() + geom_sf(data = map) +
geom_sf(data = air_sf, aes(col = Value)) +
ggtitle("Air pollution at measuring stations") +
scale_color_viridis()
ggplot() + geom_sf(data = map) +
geom_sf(data = air_sf, aes(col = covtemp)) +
ggtitle("Temperature at measuring stations") +
scale_color_viridis()
ggplot() + geom_sf(data = map) +
geom_sf(data = air_sf, aes(col = covprec)) +
ggtitle("Precipitation at measuring stations") +
scale_color_viridis()
The above code also extracted the covariate data at each prediction point. We can see what that looks like as well:
ggplot() + geom_sf(data = map) +
geom_sf(data = dp, aes(col = covtemp)) +
ggtitle("Temperature at prediction points") +
scale_color_viridis()
ggplot() + geom_sf(data = map) +
geom_sf(data = dp, aes(col = covprec)) +
ggtitle("Precipitation at prediction points") +
scale_color_viridis()
Next, mean impute out the NANs in the data. A better imputation method would certainly be better, wouldn’t it?
<- dp %>% mutate(covprec=ifelse(is.na(covprec), mean(covprec, na.rm=TRUE), covprec),
dp covtemp=ifelse(is.na(covtemp), mean(covtemp, na.rm=TRUE), covtemp))
ggplot() + geom_sf(data = map) +
geom_sf(data = dp, aes(col = covtemp)) +
scale_color_viridis()
ggplot() + geom_sf(data = map) +
geom_sf(data = dp, aes(col = covprec)) +
scale_color_viridis()
I saw in the INLA forum, https://groups.google.com/g/r-inla-discussion-group/c/z_v2oIh2egs, that it can be helpful to work with units of kilometers. In my experience, this has also held true, but it also takes some finagling and mindfulness depending on the data you are working with.
<-"+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0
projMercator+x_0=0 +y_0=0 +k=1 +units=km +nadgrids=@null +wktext +no_defs"
<- st_transform(air_sf, crs = projMercator)
air_sf_project <- st_transform(dp, crs = projMercator)
dp # Observed coordinates
<- st_coordinates(air_sf_project)
coo
# Predicted coordinates
<- st_coordinates(dp)
coop #summary(dist(coo)) # summary of distances between locations
That gives us enough data and more importantly, motivation, to make a model. Now I can get into the INLA
implementation. First: the mesh. I mentioned the mesh in the theory discussion. The mesh enables us to compute the discretized approximation to the GF using a GRMF.
INLA
provides the function to create the mesh. Here I’m using inla.mesh.2d
. Let’s take a look at a few meshes created with different parameter options.
<- inla.mesh.2d(loc = coo, max.edge=c(200, 500))
mesh0 <- inla.mesh.2d(loc = coo, max.edge=c(200, 500), cutoff=1)
mesh1 <- inla.mesh.2d(loc = coo, max.edge=c(200, 500), cutoff=0.3)
mesh2 <- inla.mesh.2d(loc = coo, max.edge=c(100, 150), cutoff=1)
mesh3 <- inla.mesh.2d(loc = coo, max.edge=c(100, 150), cutoff=0.3)
mesh4 <- inla.mesh.2d(loc = coo, max.edge=c(100), cutoff=1)
mesh5 <- inla.mesh.2d(loc = coo, max.edge=c(100), cutoff=0.3)
mesh6 plot(mesh0)
plot(mesh1)
plot(mesh2)
plot(mesh3)
plot(mesh4)
plot(mesh5)
plot(mesh6)
# Using mesh0
<-inla.mesh.2d(loc = coo, max.edge=c(200, 500), crs=st_crs(air_sf_project)) mesh
There are a number of options to play with: max.edge
controls the largest triangle edge length, and providing it with a vector c(inner, outer)
sets the max edge for inside the boundary and outside the boundary. The purpose of this is to avoid boundary effects in the estimation of the model, where boundary values have high variance. Lindgren and Rue (2015) suggests to extend the domain by some amount.
cutoff
is the minimum allowed distance between points. Otherwise, points are replaced by a single vertex. For areas of high clusters of points, this could be useful to reduce redundancy. If no boundary
is set the mesh is created on the convex hull of the observations.
https://punama.github.io/BDI_INLA/ is also a good source for this. Thanks!
Next we construct the A matrix that “A that projects the GRF from the observations to the vertices of the triangulated mesh” (Moraga (2023)). The A matrix has a row for each observation, and columns equal to the number of vertices in the mesh. The number of vertices in our mesh can be checked with mesh$n
. Below, two different meshes are generated. One for the observation locations, and one for the prediction locations. We need to set these both at once, because to make predictions in INLA we have to have that all pre-specified, unlike with the typical modelling style in R. That was why I created the grid of prediction points initially.
The inla.spde2.matern
function is used to build the SPDE model with matern covariance, using the mesh. The smoothness parameter \(\nu\) for the SPDE is implicitly set to 1, where in the spatial case \(d=2\) and \(\alpha=\nu + d/2 = 2\), seen in the code below. In the model itself the Matern covariance function parameters will be estimated; more on that later.
plot(mesh)
points(coo, col = "red")
axis(1)
axis(2)
<- inla.spde2.matern(mesh = mesh, alpha = 2, constr = TRUE)
spde <- inla.spde.make.index("s", spde$n.spde)
indexs lengths(indexs)
s s.group s.repl
280 280 280
# Make the projection matrices
<- inla.spde.make.A(mesh = mesh, loc = coo)
A <- inla.spde.make.A(mesh = mesh, loc = coop)
Ap dim(A)
[1] 52 280
dim(Ap)
[1] 4944 280
The index allows us to extract fitted values from the model, which has a length equal to the number of vertices in the mesh. s.group
and s.repl
(replicate) are related to how the dependence structure is specified in the model.
Then we have to make the INLA stack. This is done because we need to combine the data for estimation with the prediction data, as well as the projection matrices. It contains the response data, the list of covariate data–here the temperature and precipitation, the projection matrices, and the indices. Though this is a bit technical, it actually adds a lot of flexibility to INLA
because we can manipulate the stack based on the data structure that we want. For example, when modelling multiple likelihoods in a single model, these will be combined all within the stack.
# stack for estimation stk.e
<- inla.stack(tag = "est",
stk.e data = list(y = air_sf_project$Value), A = list(1, A),
effects = list(data.frame(b0 = rep(1, nrow(A)),
covtemp = air_sf_project$covtemp, covprec = air_sf_project$covprec),
s = indexs))
#stk.e
# stack for prediction stk.p
<- inla.stack(tag = "pred",
stk.p data = list(y = NA), A = list(1, Ap),
effects = list(data.frame(b0 = rep(1, nrow(Ap)),
covtemp = dp$covtemp, covprec = dp$covprec),
s = indexs))
#stk.p
# stk.full has stk.e and stk.p
<- inla.stack(stk.e, stk.p) stk.full
Finally, we can specify the model in INLA. All the hard work has been done above and at least the model specification in INLA is easier. This model has its mean specified by \[
\mu_i = \beta_0 + \beta_1 \cdot \text{temp}_i + \beta_2 \cdot \text{prec}_i + S(x_i),
\] so there is some contribution from fixed effects as well as a latent process modelled as a zero-mean Gaussian Random Field with Matern covariance function. This puts us well within INLA territory. The model equation can be seen quite clearly in the formula
variable below. We include 0 to remove the intercept that is added by default because we specifically refer to \(\beta_0\) in the stack and then need to do so in the model equation.
<- y ~ 0 + b0 + covtemp + covprec + f(s, model = spde)
formula <- inla(formula, family = "gaussian",
res data = inla.stack.data(stk.full),
control.inla=list(lincomb.derived.correlation.matrix=TRUE),
control.predictor = list(compute = TRUE,
A = inla.stack.A(stk.full)),
control.compute = list(return.marginals.predictor = TRUE))
Notice that I am passing a few options to the INLA
call. Importantly, control.compute
. We have a nice description of the control options at https://becarioprecario.bitbucket.io/inla-gitbook/ch-INLA.html#sec:controlops. It controls what quantities are actually computed and returned during the INLA estimation. For example, there are a few different information criteria that it can returned. control.predictor
will compute the posterior marginals of the parameters. Also useful is lincomb.derived.correlation.matrix=TRUE
or control.fixed = list(correlation.matrix=TRUE)
to retrieve the covariance matrix for the fixed effects. All the options can be seen with the command inla.set.control.compute.default()
Once the model is fit, we can inspect the fixed parameters, the estimated latent field, as well as the hyperparameters for the latent field.
$summary.fixed res
mean sd 0.025quant 0.5quant 0.975quant
b0 -15.29769449 21.3472625 -57.7595559 -15.17245556 26.4583704
covtemp 2.17003514 2.0949918 -1.8002534 2.09513247 6.5392273
covprec -0.01550201 0.2034683 -0.4214077 -0.01404086 0.3818577
mode kld
b0 -15.17140288 4.819848e-08
covtemp 2.09359434 3.231952e-07
covprec -0.01411427 5.717319e-08
# Latent field is here but it will print out a lot of values
# res$summary.random$s
$summary.hyperpar res
mean sd 0.025quant
Precision for the Gaussian observations 0.3076177 0.07974476 0.1774270
Theta1 for s 1.9282907 0.53127489 0.9078608
Theta2 for s -4.1344657 0.69219969 -5.5329805
0.5quant 0.975quant mode
Precision for the Gaussian observations 0.2986888 0.4890943 0.2825065
Theta1 for s 1.9197803 2.9993397 1.8830630
Theta2 for s -4.1223427 -2.8081708 -4.0698031
This shows us the relevant statistics for our intercept and parameters for the covariates.
Our predictions at the points in our spatial field have already been made, so we need to extract the estimated values:
<- inla.stack.index(stack = stk.full, tag = "pred")$data
index <- res$summary.fitted.values[index, "mean"]
pred_mean <- res$summary.fitted.values[index, "0.025quant"]
pred_ll <- res$summary.fitted.values[index, "0.975quant"]
pred_ul $mean <- NA
grid$ll <- NA
grid$ul <- NA
grid$mean[indicespointswithin] <- pred_mean
grid$ll[indicespointswithin] <- pred_ll
grid$ul[indicespointswithin] <- pred_ul
grid
summary(grid) # negative values for the lower limit
mean ll ul
Min. :1.338 Min. :-4.722 Min. : 3.733
1st Qu.:3.626 1st Qu.: 0.359 1st Qu.: 6.530
Median :4.665 Median : 1.462 Median : 7.813
Mean :4.595 Mean : 1.368 Mean : 7.874
3rd Qu.:5.553 3rd Qu.: 2.405 3rd Qu.: 9.087
Max. :8.316 Max. : 6.753 Max. :12.860
NA's :5056 NA's :5056 NA's :5056
Then create a plot of the predictions:
library(rasterVis)
Loading required package: lattice
levelplot(grid, layout = c(1, 3),
names.attr = c("Mean", "2.5 percentile", "97.5 percentile"))
This shows the mean and the 95% credible interval surrounding it.
When compute=TRUE
in control.predictor
, we can also obtain the quantities below. https://www.paulamoraga.com/book-geospatial/sec-inla.html describes this.
$summary.fitted.values
res$summary.linear.predictor
res
marginals.linear.predictor marginals.fitted.values
but it’s a lot of output so I won’t show it.
From https://becarioprecario.bitbucket.io/inla-gitbook/ch-INLA.html#model-fitting-strategies, we can also see the differences using the different fitting strategies, set by the control.inla
argument. We can change both the Gaussian approximation strategy for the posterior full conditional distributions, as well as the integration strategy used to integrate out the \(\theta_{-i}\) parameters to get the marginal distribution for \(\theta_i\). The "grid"
option is the most costly, compared to the central composite design ("ccd"
). In the empirical bayes option ("eb"
), the posterior mode is used as the integration point.
<- c("gaussian", "simplified.laplace", "laplace")
approx_strategy <- c("ccd", "grid", "eb")
int_strategy <- c("iid", "matern")
models <- matrix(nrow=length(approx_strategy)*length(int_strategy)*length(models), ncol=3)
fits <- vector(mode="list", length=length(approx_strategy)*length(int_strategy)*length(models))
fits_marginals_b <- vector(mode="list", length=length(approx_strategy)*length(int_strategy)*length(models))
fits_marginals_temp <- vector(mode="list", length=length(approx_strategy)*length(int_strategy)*length(models))
fits_marginals_prec <- 0
index_f <- c()
model_names for (m in models){
for(a in approx_strategy){
for (i in int_strategy){
<- index_f + 1
index_f if (m=="matern"){
<- y ~ 0 + b0 + covtemp + covprec + f(s, model = spde)
formula_approx
}else{
<- y ~ 0 + b0 + covtemp + covprec + f(s, model = "iid")
formula_approx
}print(paste(a, ", ", i, ", ", m))
<- c(model_names, paste(a, ", ", i, ", ", m))
model_names <- inla(formula, family = "gaussian",
fit_approx data = inla.stack.data(stk.full),
control.inla = list(strategy = a, int.strategy = i),
control.compute = list(cpo = TRUE, dic = TRUE, waic = TRUE),
control.predictor = list(compute = TRUE, A = inla.stack.A(stk.full)))
<- fit_approx$summary.fixed$mean
fits[index_f,] <- fit_approx$marginals.fixed$b0
fits_marginals_b[[index_f]] <- fit_approx$marginals.fixed$covtemp
fits_marginals_temp[[index_f]] <- fit_approx$marginals.fixed$covprec
fits_marginals_prec[[index_f]]
}
} }
[1] "gaussian , ccd , iid"
[1] "gaussian , grid , iid"
[1] "gaussian , eb , iid"
[1] "simplified.laplace , ccd , iid"
[1] "simplified.laplace , grid , iid"
[1] "simplified.laplace , eb , iid"
[1] "laplace , ccd , iid"
[1] "laplace , grid , iid"
[1] "laplace , eb , iid"
[1] "gaussian , ccd , matern"
[1] "gaussian , grid , matern"
[1] "gaussian , eb , matern"
[1] "simplified.laplace , ccd , matern"
[1] "simplified.laplace , grid , matern"
[1] "simplified.laplace , eb , matern"
[1] "laplace , ccd , matern"
[1] "laplace , grid , matern"
[1] "laplace , eb , matern"
This prints out all the combinations of approximation, integration strategy and correlation structure.
Now we can compare the means of the parameters per different model set-up.
<- as.data.frame(fits)
fits_df names(fits_df) <- c("b0", "covtemp", "covprec")
$models <- model_names
fits_df fits_df
b0 covtemp covprec models
1 -13.85789 2.049883 -0.01973532 gaussian , ccd , iid
2 -15.29769 2.170035 -0.01550201 gaussian , grid , iid
3 -13.11329 1.992509 -0.02262044 gaussian , eb , iid
4 -13.85789 2.049883 -0.01973532 simplified.laplace , ccd , iid
5 -15.29769 2.170035 -0.01550201 simplified.laplace , grid , iid
6 -13.11329 1.992509 -0.02262044 simplified.laplace , eb , iid
7 -13.85789 2.049883 -0.01973532 laplace , ccd , iid
8 -15.29769 2.170035 -0.01550201 laplace , grid , iid
9 -13.11329 1.992509 -0.02262044 laplace , eb , iid
10 -13.85789 2.049883 -0.01973532 gaussian , ccd , matern
11 -15.29769 2.170035 -0.01550201 gaussian , grid , matern
12 -13.11329 1.992509 -0.02262044 gaussian , eb , matern
13 -13.85789 2.049883 -0.01973532 simplified.laplace , ccd , matern
14 -15.29769 2.170035 -0.01550201 simplified.laplace , grid , matern
15 -13.11329 1.992509 -0.02262044 simplified.laplace , eb , matern
16 -13.85789 2.049883 -0.01973532 laplace , ccd , matern
17 -15.29769 2.170035 -0.01550201 laplace , grid , matern
18 -13.11329 1.992509 -0.02262044 laplace , eb , matern
It’s also possible to compare the posteriors themselves.
# Combine dataframes
<- lapply(fits_marginals_b, function(m) {
df_list_1 data.frame(x = m[,1], y = m[,2])
})<- lapply(fits_marginals_temp, function(m) {
df_list_2 data.frame(x = m[,1], y = m[,2])
})<- lapply(fits_marginals_prec, function(m) {
df_list_3 data.frame(x = m[,1], y = m[,2])
})<- do.call(rbind, df_list_1)
new_df_1 <- do.call(rbind, df_list_2)
new_df_2 <- do.call(rbind, df_list_3)
new_df_3 # Add an ID column
<- sapply(fits_marginals_b, nrow)
row_counts <- factor(rep(1:length(fits_marginals_b), each = row_counts)) ids
Warning in rep(1:length(fits_marginals_b), each = row_counts): first element
used of 'each' argument
$id <- ids
new_df_1$id <- ids
new_df_2$id <- ids
new_df_3ggplot(new_df_1, aes(x = x, y = y, colour = id)) +
geom_line() + ggtitle("Posterior of B0")
ggplot(new_df_2, aes(x = x, y = y, colour = id)) +
geom_line() + ggtitle("Posterior of Temperature parameter")
ggplot(new_df_3, aes(x = x, y = y, colour = id)) +
geom_line() + ggtitle("Posterior of Precipitation parameter")
So with this data set there is not really much different between estimation methods, regardless of the options used. I suppose that is a good sign?
Another useful feature is the set of functions that can be used to manipulate the marginal distributions: https://becarioprecario.bitbucket.io/inla-gitbook/ch-INLA.html#sec:marginals. for example,
inla.mmarginal(res$marginals.fixed$b0)
[1] -14.90955
plot(res$marginals.fixed$b0, type = "l", xlab = "b0", ylab = "Density")
# This should give about the same answer:
$summary.fixed$mode[1] res
[1] -15.1714
3.2 Inlabru
Next, let’s write that model in inlabru
and check if the estimates are the same. We can use the same mesh and spde function as we used before. And also the same formula! But what is especially nice about inlabru
is that we don’t have to set up the stack with the projection matrices. We just model the response variable as a function of the covariates in the dataset we set up earlier and the locations of the observations. In inlabru
we can use the sf
dataset object directly. Because of this, it’s amazingly simple to fit the model, compared to INLA
.
<- Value ~ Intercept(1) + covtemp + covprec + f(geometry, model = spde)
formula # Fit the model for inlabru
<- bru(formula, data = air_sf_project, family = "gaussian")
fit # Summarize the results
summary(fit)
inlabru version: 2.12.0
INLA version: 24.05.01-1
Components:
Intercept: main = linear(1), group = exchangeable(1L), replicate = iid(1L), NULL
covtemp: main = linear(covtemp), group = exchangeable(1L), replicate = iid(1L), NULL
covprec: main = linear(covprec), group = exchangeable(1L), replicate = iid(1L), NULL
f: main = spde(geometry), group = exchangeable(1L), replicate = iid(1L), NULL
Likelihoods:
Family: 'gaussian'
Tag: ''
Data class: 'sf', 'grouped_df', 'tbl_df', 'tbl', 'data.frame'
Response class: 'numeric'
Predictor: Value ~ .
Used components: effects[Intercept, covtemp, covprec, f], latent[]
Time used:
Pre = 1.77, Running = 0.732, Post = 0.347, Total = 2.85
Fixed effects:
mean sd 0.025quant 0.5quant 0.975quant mode kld
Intercept -15.298 21.347 -57.760 -15.172 26.458 -15.171 0
covtemp 2.170 2.095 -1.800 2.095 6.539 2.094 0
covprec -0.016 0.203 -0.421 -0.014 0.382 -0.014 0
Random effects:
Name Model
f SPDE2 model
Model hyperparameters:
mean sd 0.025quant 0.5quant
Precision for the Gaussian observations 0.308 0.080 0.177 0.299
Theta1 for f 1.928 0.531 0.908 1.920
Theta2 for f -4.134 0.692 -5.533 -4.122
0.975quant mode
Precision for the Gaussian observations 0.489 0.283
Theta1 for f 2.999 1.883
Theta2 for f -2.808 -4.070
Deviance Information Criterion (DIC) ...............: 231.65
Deviance Information Criterion (DIC, saturated) ....: 71.69
Effective number of parameters .....................: 16.69
Watanabe-Akaike information criterion (WAIC) ...: 232.14
Effective number of parameters .................: 14.00
Marginal log-Likelihood: -145.00
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)')
Fortunately, that gives nearly the same estimates as from INLA
There’s also a lot of additional useful output, including the spatial variance \(\tau\) and spatial range \(\kappa\) for the spatial field, respectively referred to as theta1
and theta2
by default (which are a log transformation of the parameters). We can also see the information criteria. We could have gotten this same output from INLA
with summary(fit)
.
Then let’s plot the predictions. In inlabru
, we don’t have to make predictions at the same time as the model fitting, which is convenient.
<- st_transform(map, crs = projMercator)
map_prj <- predict(fit, newdata=dp, formula = ~ Intercept + covtemp + covprec + f)
predictions1 <- predict(fit, newdata=dp, formula = ~ f)
predictions2 ggplot() +
geom_sf(data=predictions1, aes(color=mean)) +
scale_colour_gradient(low = "blue", high = "yellow")
# Check the contribution of just the spatial field
ggplot() +
geom_sf(data=predictions2, aes(color=mean)) +
scale_colour_gradient(low = "blue", high = "yellow")
Looks very nice. Of course, there are a lot more diagnostics we could consider, including looking at the posterior distributions of all parameters and hyperparameters and checking the information criteria. We could also consider the effect of setting priors on the SPDE parameters. This will end up making a difference for more complex models. I could also simulate data and see how well INLA recovers the parameters. There is also the matter of fitting a temporal model in both packages.
But for now, that concludes this section, showing how to fit spatial model in INLA
and inlabru
. The next section shows how to do this for a point process.