8 Soil depth mapping
8.1 Introduction
8.1.1 Purpose
This document is meant to present the methodology we used for soil depth mapping. Our data are based on the sampling campaigns 2022 - 2023 and additional remote sensing observation for bare rock formations. We based our approach on Liu et al. (2022) and added small modifications.
8.1.2 Covariates
All the data listed below are available freely online excepted for the digitized maps (Geology).
Name/ID | Original resolution (m) | Type/Unit | Source |
---|---|---|---|
Aspect/TE.1 | 25 | Radians | ESA and Airbus (2022) |
DEM/TE.5 | 25 | Meters | ESA and Airbus (2022) |
General curvature/TE.7 | 25 | - | ESA and Airbus (2022) |
MrRTF/TE.8 | 25 | - | ESA and Airbus (2022) |
MrVBF/TE.9 | 25 | - | ESA and Airbus (2022) |
Plan curvature/TE.12 | 25 | - | ESA and Airbus (2022) |
Profile curvature/TE.14 | 25 | - | ESA and Airbus (2022) |
Slope/TE.16 | 25 | Radians | ESA and Airbus (2022) |
TPI/TE.21 | 25 | - | ESA and Airbus (2022) |
Landsat 5 B2/LA5.1 | 30 | 0.52 - 0.60 µm | EROS (2013) |
Landsat 5 B3/LA5.2 | 30 | 0.63 - 0.69 µm | EROS (2013) |
Landsat 5 B4/LA5.3 | 30 | 0.76 - 0.90 µm | EROS (2013) |
Landsat 5 B7/LA5.4 | 30 | 2.08 - 2.35 µm | EROS (2013) |
Landsat 5 NDVI/LA5.5 | 30 | - | Mathias Bellat / EROS (2013) |
Landsat 5 NDWI/LA5.6 | 30 | - | EROS (2013) |
LST Apr-May/LST.1 | 1000 | Kelvin | Hulley and Hook (2018) |
LST Feb-Mar/LST.2 | 1000 | Kelvin | Hulley and Hook (2018) |
LST Jun-Jul/LST.3 | 1000 | Kelvin | Hulley and Hook (2018) |
LST Oct-Nov/LST.4 | 1000 | Kelvin | Hulley and Hook (2018) |
Geology/OT.2 | 1 : 250 000 | Lithology | Sissakian, Hagopian, and Hasan (1995), al-mousawi_geological_2007 |
Landuse/OT.4 | 10 | Landuse class | Zanaga et al. (2021) |
PET sum/OT.5 | 750 | mm | Zomer and Trabucco (2022) |
Prec. sum/OT.6 | 1000 | mm | Fick and Hijmans (2017) |
SRAD sum/OT.7 | 1000 | MJ m\(^{-2}\) | Fick and Hijmans (2017) |
Wind sum/OT.9 | 1000 | m s\(^{-1}\) | Fick and Hijmans (2017) |
Temp avg/OT.10 | 1000 | °C | Fick and Hijmans (2017) |
8.1.2.1 Terrain
All terrain covariates were computed the same way as in the previous chapter.
8.1.2.2 Remote sensing images and indexes
The Landsat 5 images were collected via a Google Earth Engine script on a period covering 1990 - 2010, the median of the composite image from Collection 2 Tier 1 TOA was used. The land surface temperature (LST) were computed also on Google Earth Engine with a time covering from 2012 to 2016 from the Suomi National Polar-Orbiting satellite. We took the average of each period: February - Mars, April - May, June - July and October November from the VIIRS instrument. The javascript codes for scraping these images are available in the supplementary file inside the “8 - Soil depth/code” folder. We computed the following indexes: Normalized Difference Vegetation Index (NDVI) (McFeeters 1996) and Normalized Difference Water Index (NDWI) (Rouse et al. 1974).
\[ NDVI = (NIR - Red) / (NIR + Red) \]
\[ NDWI = (Green - NIR) / (Green + NIR) \]
8.1.3 Soil depths
The soil depths of the 122 soil profiles were measured during the field campaign and can be found found online at https://doi.org/10.1594/PANGAEA.973714. Additional 25 site with no soil (or zero value to soil depth), were added after looking at Bing and Google earth satellite image. We only selected bare rock formation for these sites.
8.1.4 Preparation of the data
All raster were sampled to 25 x 25 m tiles to match the DEM. We used bilinear method excepted for the discrete maps (geology) where we used ngb resampling.
library(raster)
library(terra)
# 1.1 Import data ==============================================================
DEM <- raster("./data/DEM_GLO_25.tif")
rlist<-list.files("./data/remote", full.names=T)
rlist2<-list.files("./data/remote/", full.names=F)
outpath<-"./data/remote/resample/"
outfiles <- paste0(outpath, rlist2)
# 1.1 Resample loop ============================================================
for (i in (1:length(rlist))){
x<-raster(rlist[i])
x <- projectRaster(x, crs=CRS("+init=epsg:32638"))
x<-resample(x,DEM,method = "bilinear") #Careful to implement "ngb" option for discrete values
x<-crop(x,DEM)
writeRaster(x,filename=outfiles[i], format="GTiff",overwrite=T)
}
# 1.2 Export as a stack_raster =================================================
rlist <- list.files("./data/remote/resample/", full.names=T)
x <- stack(rlist, DEM)
x <- rast(x)
terra::writeRaster(x, "./export/Stack_layers_depth.tif")
8.2 Soil depth mapping prediction
8.2.1 Preparation of the environment
# 0.1 Prepare environment ======================================================
# Folder check
getwd()
# Set folder direction
setwd(dir="./depth")
# Clean up workspace
rm(list = ls(all.names = TRUE))
# 0.2 Install packages =========================================================
install.packages("pacman") #Install and load the "pacman" package (allow easier download of packages)
library(pacman)
pacman::p_load(ggplot2, terra, mapview, sf, raster, dplyr, corrplot, viridis, rsample, caret, parsnip, tmap, DescTools, patchwork, quantregForest, wesanderson, cblindplot, grid, gridExtra, ggspatial, cowplot, ncdf4)
## R version 4.4.0 (2024-04-24 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 22621)
##
## Matrix products: default
##
##
## locale:
## [1] LC_COLLATE=French_France.utf8 LC_CTYPE=French_France.utf8
## [3] LC_MONETARY=French_France.utf8 LC_NUMERIC=C
## [5] LC_TIME=French_France.utf8
##
## time zone: Asia/Tbilisi
## tzcode source: internal
##
## attached base packages:
## [1] grid stats graphics grDevices utils datasets methods
## [8] base
##
## other attached packages:
## [1] ncdf4_1.23 cowplot_1.1.3 ggspatial_1.1.9
## [4] gridExtra_2.3 cblindplot_0.0.4 wesanderson_0.3.7
## [7] quantregForest_1.3-7.1 RColorBrewer_1.1-3 randomForest_4.7-1.2
## [10] patchwork_1.3.0 DescTools_0.99.58 tmap_3.3-4
## [13] parsnip_1.2.1 caret_6.0-94 lattice_0.22-6
## [16] rsample_1.2.1 viridis_0.6.5 viridisLite_0.4.2
## [19] corrplot_0.95 dplyr_1.1.4 raster_3.6-26
## [22] sp_2.1-4 sf_1.0-16 mapview_2.11.2
## [25] terra_1.7-78 ggplot2_3.5.1 pacman_0.5.1
## [28] DT_0.33 readr_2.1.5
##
## loaded via a namespace (and not attached):
## [1] rstudioapi_0.16.0 jsonlite_1.8.8 magrittr_2.0.3
## [4] farver_2.1.2 rmarkdown_2.27 vctrs_0.6.5
## [7] base64enc_0.1-3 forcats_1.0.0 htmltools_0.5.8.1
## [10] leafsync_0.1.0 haven_2.5.4 cellranger_1.1.0
## [13] pROC_1.18.5 sass_0.4.9 parallelly_1.38.0
## [16] KernSmooth_2.23-22 bslib_0.8.0 htmlwidgets_1.6.4
## [19] plyr_1.8.9 rootSolve_1.8.2.4 lubridate_1.9.3
## [22] stars_0.6-6 cachem_1.1.0 lifecycle_1.0.4
## [25] iterators_1.0.14 pkgconfig_2.0.3 Matrix_1.7-0
## [28] R6_2.5.1 fastmap_1.2.0 future_1.34.0
## [31] digest_0.6.36 Exact_3.3 colorspace_2.1-1
## [34] furrr_0.3.1 leafem_0.2.3 crosstalk_1.2.1
## [37] lwgeom_0.2-14 fansi_1.0.6 timechange_0.3.0
## [40] httr_1.4.7 abind_1.4-8 compiler_4.4.0
## [43] proxy_0.4-27 withr_3.0.2 DBI_1.2.3
## [46] MASS_7.3-60.2 lava_1.8.0 tmaptools_3.1-1
## [49] leaflet_2.2.2 classInt_0.4-10 gld_2.6.6
## [52] ModelMetrics_1.2.2.2 tools_4.4.0 units_0.8-5
## [55] future.apply_1.11.2 nnet_7.3-19 glue_1.7.0
## [58] satellite_1.0.5 nlme_3.1-164 reshape2_1.4.4
## [61] generics_0.1.3 recipes_1.1.0 gtable_0.3.6
## [64] tzdb_0.4.0 class_7.3-22 tidyr_1.3.1
## [67] data.table_1.15.4 lmom_3.2 hms_1.1.3
## [70] utf8_1.2.4 foreach_1.5.2 pillar_1.9.0
## [73] stringr_1.5.1 splines_4.4.0 survival_3.5-8
## [76] tidyselect_1.2.1 knitr_1.48 bookdown_0.40
## [79] stats4_4.4.0 xfun_0.46 expm_1.0-0
## [82] hardhat_1.4.0 timeDate_4032.109 stringi_1.8.4
## [85] yaml_2.3.10 boot_1.3-30 evaluate_0.24.0
## [88] codetools_0.2-20 tibble_3.2.1 cli_3.6.3
## [91] rpart_4.1.23 munsell_0.5.1 jquerylib_0.1.4
## [94] dichromat_2.0-0.1 Rcpp_1.0.13 readxl_1.4.3
## [97] globals_0.16.3 png_0.1-8 XML_3.99-0.17
## [100] parallel_4.4.0 gower_1.0.1 listenv_0.9.1
## [103] mvtnorm_1.3-2 ipred_0.9-15 scales_1.3.0
## [106] prodlim_2024.06.25 e1071_1.7-14 purrr_1.0.2
## [109] rlang_1.1.4
8.2.2 Prepare the data
# 01 Import data sets ##########################################################
# 01.1 Import soils depths =====================================================
depth <- read.csv("./data/Soil_depth.csv", sep=";")
depth$Depth <- as.numeric(depth$Depth)
# 01.2 Plot the depth information ==============================================
# Short overview of the values
head(depth)
## X Y Site.Name Depth
## 1 287940.2 4095202 B07_2022_001 100
## 2 286917.0 4104485 B07_2022_002 65
## 3 280594.1 4090376 B07_2022_003 40
## 4 275039.6 4097134 B07_2022_004 80
## 5 289512.9 4086706 B07_2022_006 80
## 6 287922.6 4092158 B07_2022_007 0
# Histogram of the values
ggplot(depth, aes(x = Depth)) +
geom_histogram(breaks = seq(0, 100, by = 10), alpha = 0.5, fill = 'steelblue', color = 'black') +
labs(title ="Number of smaple for each soil depth", x = "Depth (cm)", y = "Number of samples") +
xlim(0, 100) +
theme_minimal() +
theme(plot.title = element_text(hjust = 0.5))
# 01.3 Transform and plot the data =============================================
depth$sqrt <- sqrt(depth$Depth)
# Histogram of the sqrt values
ggplot(depth, aes(x = sqrt)) +
geom_histogram(breaks = seq(0, 10, by = 0.5), alpha = 0.5, fill = 'steelblue', color = 'black') +
labs(title ="Number of sample for each soil depth in cm", x = "Sqrt-transformed soil depth in cm", y = "Number of samples") +
xlim(0, 11) +
theme_minimal() +
theme(plot.title = element_text(hjust = 0.5))
# 01.4 Plot the depth spatial information ======================================
# Create a spatial dataframe
sp_df <- st_as_sf(depth, coords = c("X", "Y"), crs = 32638)
# Plot the values of depth regarding the elevation
mapview(sp_df, zcol = "Depth", legend =TRUE, map.types ="Esri.WorldShadedRelief")
# 01.5 Import covariates raster ================================================
Landsat <- raster::stack(list.files("./data/Landsat/", full.names = TRUE))
names(Landsat)
Terrain <- raster::stack(list.files("./data/Terrain/", full.names = TRUE))
names(Terrain)
Others <- raster::stack(list.files("./data/Others/", full.names = TRUE))
names(Others)
LST <- raster::stack(list.files("./data/LST/", full.names = TRUE))
names(LST)
DSM_cov_name <- read.table("./data/Covariates_names_DSM.txt")
df_names <- data.frame()
for (i in 1:length(names(Terrain))) {
col_name <- names(Terrain)[i]
if (col_name %in% DSM_cov_name$V2) {
transformed_name <- DSM_cov_name$V1[DSM_cov_name$V2 == col_name]
} else {
transformed_name <- paste0("TE.26")
}
df_names[i,1] <- transformed_name
df_names[i,2] <- names(Terrain)[i]
}
t <- nrow(df_names)
for (i in 1:length(names(Landsat))) {
c <- paste0("LA5.",i)
df_names[i+t,1] <- c
df_names[i+t,2] <- names(Landsat)[i]
}
t <- nrow(df_names)
for (i in 1:length(names(LST))) {
c <- paste0("LST.",i)
df_names[i+t,1] <- c
df_names[i+t,2] <- names(LST)[i]
}
t <- nrow(df_names)
for (i in 1:length(names(Others))) {
col_name <- names(Others)[i]
if (col_name %in% DSM_cov_name$V2) {
transformed_name <- DSM_cov_name$V1[DSM_cov_name$V2 == col_name]
} else {
transformed_name <- "OT.10" #Only one new variable
}
df_names[i+t,1] <- transformed_name
df_names[i+t,2] <- names(Others)[i]
}
write.table(df_names,"./data/Covariates_names_soil_depth.txt")
all_cov_name <- rbind(DSM_cov_name, df_names)
all_cov_name <- distinct(all_cov_name)
x <- raster::stack(Terrain, Landsat, LST, Others)
names(x) <- df_names[,1]
x <- rast(x)
terra::writeRaster(x, "./data/Stack_layers_soil_depth.tif", overwrite = TRUE)
covariates <- stack("./data/Stack_layers_soil_depth.tif")
# 01.6 Plot the covariates maps ================================================
reduce <- aggregate(covariates, fact=10, fun=mean)
plot(reduce)
\(\\[0.5cm]\)
# 01.7 Extract the values ======================================================
# Extract the values of each band for the sampling location
df_cov = raster::extract(covariates, sp_df, method='simple')
# 01.8 Save the data ===========================================================
# Create a csv out of it
write.csv(data.frame(df_cov), "./data/df_cov_soil_depth.csv")
# 02 Check the data ############################################################
# 02.1 Import the data and merge ===============================================
Covdfgis <- read.csv("./data/df_cov_soil_depth.csv")
ID <- 1:nrow(Covdfgis)
SoilCov <- cbind(ID, depth, Covdfgis[,-c(1)])
#Check the data
names(SoilCov)
str(SoilCov)
# Prepare data for ML modelling
SoilCovML <- SoilCov[,-c(1:7)]
## [1] "ID" "X" "Y" "Site.Name" "Depth" "sqrt"
## [7] "TE.1" "TE.5" "TE.7" "TE.8" "TE.9" "TE.12"
## [13] "TE.14" "TE.16" "TE.21" "LA5.1" "LA5.2" "LA5.3"
## [19] "LA5.4" "LA5.5" "LA5.6" "LST.1" "LST.2" "LST.3"
## [25] "LST.4" "OT.2" "OT.4" "OT.5" "OT.6" "OT.7"
## [31] "OT.10" "OT.9"
## 'data.frame': 147 obs. of 32 variables:
## $ ID : int 1 2 3 4 5 6 7 8 9 10 ...
## $ X : num 287940 286917 280594 275040 289513 ...
## $ Y : num 4095202 4104485 4090376 4097134 4086706 ...
## $ Site.Name: chr "B07_2022_001" "B07_2022_002" "B07_2022_003" "B07_2022_004" ...
## $ Depth : num 100 65 40 80 80 0 25 100 75 75 ...
## $ sqrt : num 10 8.06 6.32 8.94 8.94 ...
## $ TE.1 : num 5.13 4.27 3.53 4.41 2.6 ...
## $ TE.5 : num 483 613 352 400 380 ...
## $ TE.7 : num -0.003644 0.000265 0 -0.003209 -0.006941 ...
## $ TE.8 : num 0.0733 0.4736 0.7583 0.0832 0.0127 ...
## $ TE.9 : num 1.637 0.445 2.258 0.118 0.394 ...
## $ TE.12 : num -2.09e-04 1.64e-05 2.24e-04 1.59e-03 3.37e-04 ...
## $ TE.14 : num -0.000943 0.000229 -0.000917 0.001199 -0.000281 ...
## $ TE.16 : num 0.0928 0.06845 0.00189 0.17723 0.15261 ...
## $ TE.21 : num -2.078 1.333 -0.852 1.378 -3.201 ...
## $ LA5.1 : num 0.16 0.18 0.2 0.167 0.196 ...
## $ LA5.2 : num 0.192 0.225 0.243 0.204 0.23 ...
## $ LA5.3 : num 0.272 0.288 0.314 0.252 0.28 ...
## $ LA5.4 : num 0.193 0.248 0.282 0.226 0.264 ...
## $ LA5.5 : num 0.16 0.123 0.135 0.103 0.117 ...
## $ LA5.6 : num -0.244 -0.233 -0.23 -0.206 -0.197 ...
## $ LST.1 : num 311 310 312 312 314 ...
## $ LST.2 : num 296 296 296 296 296 ...
## $ LST.3 : num 327 327 328 329 329 ...
## $ LST.4 : num 306 305 306 306 307 ...
## $ OT.2 : int 13 13 5 8 6 8 13 7 8 7 ...
## $ OT.4 : int 5 11 11 11 11 11 11 5 5 11 ...
## $ OT.5 : num 2113 2069 2122 2127 2144 ...
## $ OT.6 : num 734 738 664 647 739 ...
## $ OT.7 : num 211009 210498 211285 210657 211260 ...
## $ OT.10 : num 19.5 19 19.9 19.9 20 ...
## $ OT.9 : num 25.3 24.6 25.5 25.6 25.6 ...
# 02.3 Plot and export the correlation matrix ==================================
# Correlation of the data
corrplot(cor(SoilCovML), method = "color", col = viridis(200),
type = "upper",
addCoef.col = "black", # Add coefficient of correlation
tl.col = "black", tl.srt = 45, # Text label color and rotation
number.cex = 0.7, # Size of the text labels
cl.cex = 0.7) # Size of the color legend text
Regarding the correlation plot, the Landsat 5 bands and land surface temperature layers have correlation index over 0.75, as some transformed layers based on DEM (curvature, slope ,aspect …) and the solar radiation with wind.
8.2.3 Tune the model
# 03 Tune the model ############################################################
load(file = "./export/Save/Pre_process_soil_depth.RData")
# 03.1 Create the train and test set ===========================================
#R Remove the site name and x, y columns
df_soilCov <- SoilCov[,c(1,5:length(SoilCov))]
# Set a random seed
set.seed(1070)
# preprocess the layers
preproc <- preProcess(df_soilCov[,4:length(df_soilCov)], method=c("range"))
df_soilCovTrans <- predict(preproc, df_soilCov[,4:length(df_soilCov)])
df_soilCovTrans <- cbind(df_soilCovTrans, df_soilCov[,1:3])
# Create a 80% split
df_soil <- initial_split(df_soilCovTrans, prop = 0.8, strata = ID)
trainData <- training(df_soil)
testData <- testing(df_soil)
# Implement the index
row.names(testData) <- testData$ID
row.names(trainData) <- trainData$ID
# Create the splits
X_train <- trainData[,-c(27:29)]
y_train <- trainData[,c(28)]
y_train_sqrt <- trainData[,c(29)]
X_test <- testData[,-c(27:29)]
y_test <- testData[,c(28)]
y_test_sqrt <- testData[,c(29)]
# 03.2 Create the model train controls with cross-validation ==================
TrainControl <- trainControl(method="repeatedcv", 10, 3, allowParallel = TRUE, savePredictions=TRUE, verboseIter = TRUE)
# 03.3 Set the grid for mtry and nodesize ======================================
tuneGrid <- expand.grid(mtry = c(1, 3, 5, 7, 10, 12, 15))
nodesize_values <- c(3, 4, 5, 6, 7, 8, 9, 10, 11, 12)
all_results <- list()
set.seed(1070)
# 03.4 Run the model tunning ===================================================
# Loop over different nodesize values and different mtry
for (nodesize in nodesize_values) {
start_time <- Sys.time()
qrf_model <- train(x = X_train, y = y_train_sqrt,
method = "qrf",
trControl = TrainControl,
tuneGrid = tuneGrid,
ntree = 500, # Number of trees
metric = "RMSE", # Specify the metric you want to optimize
nodesize = nodesize # Minimum node size
)
end_time <- Sys.time()
print(end_time - start_time)
# Store the results with the corresponding nodesize
results <- qrf_model$results
results$nodesize <- nodesize # Add the nodesize information
all_results[[as.character(nodesize)]] <- results
}
final_results <- do.call(rbind, all_results)
# View the combined results for all mtry and nodesize combinations
print(final_results)
## mtry RMSE Rsquared MAE RMSESD RsquaredSD MAESD nodesize
## 3.1 1 2.729334 0.4962853 1.842530 0.8103775 0.2462337 0.5842215 3
## 3.2 3 2.833004 0.4773500 1.904092 0.7890278 0.2513403 0.5469537 3
## 3.3 5 2.841813 0.4714421 1.922363 0.7898268 0.2548695 0.5590358 3
## 3.4 7 2.882372 0.4651227 1.941985 0.7952213 0.2559438 0.5670334 3
## 3.5 10 2.910301 0.4604997 1.974741 0.7570323 0.2461620 0.5293020 3
## 3.6 12 2.916578 0.4596933 1.975224 0.7161846 0.2408349 0.5149407 3
## 3.7 15 2.951068 0.4526866 2.001091 0.7184835 0.2388678 0.5245988 3
## 4.1 1 2.773342 0.4929364 1.867376 0.7325954 0.2481142 0.5790523 4
## 4.2 3 2.838034 0.4895359 1.915626 0.7145291 0.2440721 0.5656305 4
## 4.3 5 2.830851 0.4901692 1.905814 0.7078239 0.2359281 0.5483191 4
## 4.4 7 2.878877 0.4750751 1.973288 0.7049437 0.2440490 0.5876352 4
## 4.5 10 2.908365 0.4670291 1.969554 0.6328296 0.2241551 0.5133742 4
## 4.6 12 2.947753 0.4580659 1.994925 0.6831769 0.2171447 0.5480810 4
## 4.7 15 2.982162 0.4583561 2.026137 0.6708416 0.2158921 0.5384385 4
## 5.1 1 2.663055 0.5221769 1.800572 0.7448825 0.2207681 0.5635888 5
## 5.2 3 2.779503 0.5010455 1.867847 0.7509376 0.2170405 0.5612229 5
## 5.3 5 2.789159 0.4988129 1.876542 0.7544052 0.2195226 0.5458377 5
## 5.4 7 2.845951 0.4780145 1.923041 0.7654205 0.2351576 0.5985707 5
## 5.5 10 2.916293 0.4551181 1.983060 0.7682618 0.2436306 0.5844046 5
## 5.6 12 2.901288 0.4604422 1.969209 0.7500825 0.2318361 0.5945813 5
## 5.7 15 2.928156 0.4543225 1.980911 0.7637384 0.2398471 0.5924727 5
## 6.1 1 2.737298 0.5416168 1.848400 0.7511112 0.2307820 0.5255341 6
## 6.2 3 2.769118 0.5411176 1.875529 0.7338547 0.2318195 0.5177122 6
## 6.3 5 2.832541 0.5293194 1.907256 0.6903118 0.2239275 0.4742872 6
## 6.4 7 2.857926 0.5173423 1.931808 0.7112448 0.2242474 0.4928804 6
## 6.5 10 2.926328 0.4900923 1.970553 0.7080537 0.2288874 0.4873877 6
## 6.6 12 2.942479 0.4853409 1.977365 0.6608536 0.2189466 0.4556416 6
## 6.7 15 2.957402 0.4782926 1.985686 0.7045822 0.2218300 0.4805035 6
## 7.1 1 2.730479 0.5095423 1.827464 0.7556501 0.2258124 0.5525021 7
## 7.2 3 2.794204 0.4925000 1.903247 0.8293877 0.2384477 0.5883647 7
## 7.3 5 2.851583 0.4780661 1.933760 0.7945918 0.2344559 0.5876801 7
## 7.4 7 2.893746 0.4675265 1.944191 0.7999453 0.2305590 0.5874803 7
## 7.5 10 2.871070 0.4677090 1.948722 0.7555909 0.2257198 0.5477448 7
## 7.6 12 2.991799 0.4378629 2.030453 0.7139264 0.2149300 0.5409700 7
## 7.7 15 3.007470 0.4320991 2.046692 0.8110356 0.2321078 0.5965111 7
## 8.1 1 2.685270 0.5249273 1.793010 0.6919063 0.2178054 0.4843446 8
## 8.2 3 2.763586 0.5095831 1.848866 0.7294080 0.2303865 0.5100895 8
## 8.3 5 2.779153 0.5088139 1.871082 0.7549698 0.2398836 0.5340549 8
## 8.4 7 2.773158 0.5061415 1.878156 0.7643847 0.2390085 0.5535613 8
## 8.5 10 2.842823 0.4844367 1.922981 0.7414883 0.2325255 0.5451719 8
## 8.6 12 2.779985 0.4986677 1.890600 0.7697851 0.2382519 0.5572981 8
## 8.7 15 2.832611 0.4851692 1.920453 0.7173450 0.2241389 0.5217087 8
## 9.1 1 2.708775 0.5081740 1.822192 0.6939379 0.2420910 0.5060151 9
## 9.2 3 2.843956 0.4809980 1.902569 0.6654775 0.2326433 0.4704214 9
## 9.3 5 2.865376 0.4708315 1.891287 0.6255177 0.2188883 0.4407110 9
## 9.4 7 2.882954 0.4646611 1.915415 0.6112141 0.2152987 0.4561261 9
## 9.5 10 2.895203 0.4690362 1.920161 0.6001704 0.2199620 0.4374223 9
## 9.6 12 2.876605 0.4713040 1.922756 0.5518848 0.1996927 0.4064532 9
## 9.7 15 2.910187 0.4565587 1.927773 0.5937036 0.2140814 0.4306462 9
## 10.1 1 2.669146 0.5193111 1.814073 0.7783866 0.2272123 0.5854923 10
## 10.2 3 2.693680 0.5166215 1.828976 0.7915503 0.2280681 0.5746169 10
## 10.3 5 2.735058 0.5080277 1.850283 0.7329562 0.2181400 0.5332899 10
## 10.4 7 2.799899 0.4926177 1.898245 0.7327455 0.2183946 0.5429160 10
## 10.5 10 2.822178 0.4870636 1.934005 0.7389095 0.2152389 0.5410894 10
## 10.6 12 2.840115 0.4845868 1.934662 0.7306276 0.2082656 0.5552059 10
## 10.7 15 2.828377 0.4889028 1.921956 0.7200878 0.2129413 0.5440597 10
## 11.1 1 2.735141 0.5307591 1.850768 0.7655969 0.2501171 0.5644426 11
## 11.2 3 2.738556 0.5260517 1.863128 0.7605979 0.2525759 0.5524572 11
## 11.3 5 2.752541 0.5154618 1.874229 0.7562428 0.2553953 0.5714878 11
## 11.4 7 2.820722 0.4978160 1.914186 0.7809397 0.2586396 0.5791621 11
## 11.5 10 2.869406 0.4885411 1.950299 0.7591539 0.2539700 0.5947951 11
## 11.6 12 2.876452 0.4770955 1.967754 0.7989715 0.2685377 0.6284647 11
## 11.7 15 2.906921 0.4680887 1.989789 0.7629615 0.2646727 0.6125713 11
## 12.1 1 2.702005 0.5072654 1.818173 0.8076490 0.2471731 0.6133061 12
## 12.2 3 2.754790 0.4953014 1.880478 0.8198944 0.2518294 0.6431279 12
## 12.3 5 2.790744 0.4896488 1.921956 0.8345103 0.2542266 0.6653885 12
## 12.4 7 2.857767 0.4728454 1.958128 0.8065503 0.2489765 0.6457750 12
## 12.5 10 2.855369 0.4734595 1.966860 0.8357491 0.2489488 0.6733135 12
## 12.6 12 2.906046 0.4599975 1.987815 0.7731286 0.2355587 0.6506323 12
## 12.7 15 2.904322 0.4593194 1.995720 0.8032117 0.2424297 0.6707301 12
The two parameters that we are modifying are the minimum node size/ nodesize (min_n) and the number of randomly selected covariates (mtry). We set 500 trees as default.
# 03.5 Plot the different optimization =========================================
gg1 <- ggplot(final_results, aes(x = mtry, y = Rsquared, color = as.factor(nodesize))) +
geom_line() +
geom_point() +
labs(x = "mtry", y = "R²", color = "nodesize") +
theme_minimal()
gg2 <- ggplot(final_results, aes(x = mtry, y = RMSE, color = as.factor(nodesize))) +
geom_line() +
geom_point() +
labs(x = "mtry", y = expression("RMSE (" *cm^0.5* ")"), color = "nodesize") +
theme_minimal()
# Combine R2 and RMSE in one plot
combined_plot <- wrap_plots(gg1, gg2, ncol = 2)
plot(combined_plot)
8.2.4 Final model
Here we selected optimised nodesize of 6 and mtry of 1 with ntree always set at 500.
# 04.1 Produce the final model #################################################
nodesize <- 6
mtry <- expand.grid(mtry = 1)
set.seed(1070)
qrf_final <- train(x = X_train, y = y_train_sqrt,
method = "qrf",
trControl = TrainControl,
tuneGrid = mtry,
ntree = 500, # Number of trees
metric = "RMSE", # Specify the metric you want to optimize
nodesize = nodesize )
results_qrf <- qrf_final$results
# View R² and RMSE values for final model
print(results_qrf[, c("mtry", "RMSE", "Rsquared")])
## mtry RMSE Rsquared
## 1 1 2.619363 0.5257963
# Select the final model
final_model <- qrf_final$finalModel
# 04.2 Show covariates importance ==============================================
importance_df <- as.data.frame(varImp(final_model))
# Convert row names (variables) to a column for ggplot
importance_df$scale <- (importance_df$Overall/sum(importance_df$Overall)*100)
importance_df$Variable <- rownames(importance_df)
# Plot using ggplot2
gg1 <- ggplot(importance_df, aes(x = reorder(Variable, scale), y = scale)) +
geom_bar(stat = "identity", fill = "lightblue") +
coord_flip() +
xlab("Covariates") +
ylab("Importance scaled") +
ggtitle("Variable Importance from QRF Model (%)")
gg1
8.2.5 Model evaluation
We used five metrics to evaluate the model:
- RMSE= Root mean square error
- R\(^2\)= Coefficient of determination, also called rsquared.
- MAE = Mean absolute error
- CCC = Concordance correlation coefficient
- PICP = Prediction interval coverage probability set at 90% interval
\[ \\[0.5cm] RMSE = \sqrt{\frac{\sum_{i=1}^{n}(y_i - \hat{y}_i)^2}{N}} \]
\[ \\[0.5cm] R^2 = 1 - \frac{\sum_{i=1}^{n}(y_i - \hat{y}_i)^2}{\sum_{i=1}^{n}(y_i - \bar{y}_i)^2} \]
\[ \\[0.5cm] MAE = \frac{1}{n} \sum_{i=1}^{n}|Y_i - \hat{Y}_i| \]
\[ \\[0.5cm] CCC = \frac{2S_{XY}}{S_{X}^2+S_{Y}^2+(\bar{X}-\bar{Y})^2} \]
\[ \\[0.5cm] PICP = \frac{1}{v} ~ count ~ j \\ j = PL^{L}_{j} \leq t_j \leq PL^{U}_{j} \]
# Save and export the plot
ggsave("./export/Variables importance.png", gg1, width = 10, height = 8)
ggsave("./export/Variables importance.pdf", gg1, width = 10, height = 8)
# 04.3 Predict the test set ====================================================
predictions <- predict(final_model, X_test)
# 04.4 Get the metrics ========================================================
metrics <- postResample(predictions[,2], y_test_sqrt)
ccc <- CCC(y_test_sqrt, predictions[,2])
PICP <- (y_test_sqrt >= predictions[,1]) & (y_test_sqrt <= predictions[,3])
PICP <- mean(PICP)*100
Final_stats <- as.data.frame(t(c(metrics, ccc$rho.c$est, PICP)))
colnames(Final_stats) <- c("RMSE", "R²", "MAE", "CCC", "PICP")
# Print the metrics
print(Final_stats)
## RMSE (cm-0.5) R² MAE CCC PICP
## 1 2.422883 0.5680413 1.538656 0.7375648 96.875
# Write it
write.table(Final_stats, file="./export/Final_stats.txt", row.names = FALSE)
# 04.5 Save the model and train and test sets ==================================
save(trainData, testData, final_model, qrf_model, all_results, qrf_final, file = "./export/Save/Model.RData")
rm(list = ls())
The final model explains around 56% of the variability (R\(^2\)) of the soil depth with a precision (RMSE) of soil depth around 2.42 cm\(^{-0.5}\).
8.2.6 Mapping the soil depth of the area
Due to the highly detailed data we had to split the prediction into different blocks (16). This process allows the computer to run separately each prediction.
# 05 Predict the map ###########################################################
# 05.1 Import the previous data ================================================
load(file = "./export/save/Model_soil_depth.RData")
stack_raster <- stack("./data/Stack_layers_soil_depth.tif")
cov <- read.csv("./data/df_cov_soil_depth.csv")
cov <- cov[,-1]
cov[] <- lapply(cov , as.numeric)
# 05.2 Normalise the values from the rasters ===================================
process_layer <- function(layer) {
# Convert in numeric
layer <- as.numeric(layer)
# Replace the NAs by median
median_value <- median(layer, na.rm = TRUE)
layer[is.na(layer)] <- median_value
return(layer)
}
scaling_params <- lapply(1:ncol(cov), function(i) {
list(min = min(cov[[i]], na.rm = TRUE), max = max(cov[[i]], na.rm = TRUE))
})
scale_layer <- function(layer, min_val, max_val) {
(layer - min_val) / (max_val - min_val)
}
stack_scaled <- stack()
# Apply transformation
for (i in 1:nlayers(raster_stack)) {
min_val <- scaling_params[[i]]$min
max_val <- scaling_params[[i]]$max
# Normalised the layer
layer_processed <- calc(raster_stack[[i]], process_layer)
layer_scaled <- calc(layer_processed, function(x) scale_layer(x, min_val, max_val))
stack_scaled <- addLayer(stack_scaled, layer_scaled)
cat(round((i/nlayers(raster_stack))*100, 1),"% \n")
}
stack_scaled <- rast(stack_scaled)
sum(is.na(values(stack_scaled)))
names(stack_scaled) <- names(raster_stack)
terra::writeRaster(stack_scaled, "./export/Soil_depth/Stack_raster_normalised_soil_depth.tif", overwrite = TRUE)
# 05.3 Prediction map for soil depth ===========================================
rm(list = ls(all.names = TRUE))
load("./export/save/Model_soil_depth.RData")
raster_stack_normalised <- stack("./export/Soil_depth/Stack_raster_normalised_soil_depth.tif")
# Divide in block for allowing the computer to run it
block_info <- blockSize(raster_stack_normalised )
# Create an empty raster for storing predicted values
predicted_raster <- raster_stack_normalised [[1]] # Use the first layer as a template
predicted_raster <- writeStart(predicted_raster, "./export/Prediction_depth_sqrt.tif", overwrite = TRUE)
# Loop through each block of the raster stack
for (i in 1:block_info$n) {
# Read block of raster data
start_time <- proc.time()
block <- getValuesBlock(raster_stack_normalised , row = block_info$row[i], nrows = block_info$nrows[i])
block <- as.data.frame(block)
# Process the block
predicted_block <- predict(final_model, block, what = c(0.05, 0.5, 0.95))
# Write the predicted block to the output raster
predicted_raster <- writeValues(predicted_raster, predicted_block[,2], block_info$row[i])
end_time <- proc.time()
print(end_time - start_time)
print(i)
}
# Close the creation of the raster
predicted_raster <- writeStop(predicted_raster)
# Rescale the raster and export it
predicted_raster_sqrt <- (predicted_raster)^2
writeRaster(predicted_raster_sqrt,"./export/Prediction_depth.tif", format = "GTiff",overwrite=T)
8.2.7 Prediction of uncertainty map
The uncertainty of the model is based on the following equation from Yan et al. (2018).
\[ \\[0.5cm] Uncertainty = \frac{Qp ~ 0.95- Qp ~ 0.05}{Qp ~ 0.5} \]
# 05.4 Uncertainty map of soil depth ===========================================
# Create an empty raster for storing predicted values
uncertainty_raster <- raster_stack_normalised [[1]] # Use the first layer as a template
uncertainty_raster <- writeStart(uncertainty_raster, "./export/Uncertainty_depth_sqrt.tif", overwrite = TRUE)
# Loop through each block of the raster stack
for (i in 1:block_info$n) {
# Read block of raster data
start_time <- proc.time()
block <- getValuesBlock(raster_stack_normalised, row = block_info$row[i], nrows = block_info$nrows[i])
block <- as.data.frame(block)
# Process the block
predicted_block <- predict(final_model, block, what = c(0.05, 0.5, 0.95))
# Write the predicted block to the output raster
values <- (predicted_block[,3] - predicted_block[,1]) /predicted_block[,2]
uncertainty_raster <- writeValues(uncertainty_raster, values, block_info$row[i])
end_time <- proc.time()
print(end_time - start_time)
print(i)
}
# Close the creation of the raster
uncertainty_raster <- writeStop(uncertainty_raster)
uncertainty_raster_sqrt <- (uncertainty_raster)^2
writeRaster(uncertainty_raster_sqrt ,"./export/Uncertainty_depth.tif", format = "GTiff",overwrite=T)
8.3 Visualisations and exports
8.3.1 Basic visualisations
# 06 Visualisation of the prediction ###########################################
# 06.1 Plot the depth maps =====================================================
rm(list = ls())
load(file = "./export/save/Pre_process_soil_depth.RData")
predicted_raster <- raster("./export/Prediction_depth.tif")
uncertainty_raster <- raster("./export/Uncertainty_depth.tif")
survey <- st_read("./data/Survey_Area.gpkg", layer = "Survey_Area")
sp_df <- st_as_sf(SoilCov, coords = c("X", "Y"), crs = 32638)
predicted_raster_resize <- aggregate(predicted_raster, fact=5, fun=mean)
uncertainty_resize <- aggregate(uncertainty_raster, fact=5, fun=mean)
# Plot continuous values of the map
mapview(predicted_raster_resize, at = seq(0,100,20), legend = TRUE, layer.name = "Soil depth prediction (cm)",col.regions = wes_palettes$Zissou1Continuous) +
mapview(sp_df, zcol = "Depth", at = seq(0,100,20), legend = TRUE, layer.name = "Soil samples depth (cm)", col.regions = wes_palettes$Zissou1Continuous)
\(\\[0.5cm]\)
mapview(uncertainty_resize, legend = TRUE, layer.name = "Soil depth prediction uncertainty", col.regions = brewer.pal(5, "YlGnBu"))
\(\\[0.5cm]\)
8.3.2 Colorblind visualisations
# 06.3 Visualise for colorblind ================================================
wes_palette_hcl <- function(palette_name, n = 7) {
wes_colors <- wes_palette(palette_name, n = n, type = "continuous")
hex_colors <- as.character(wes_colors)
return(hex_colors)
}
palette_wes <- rev(wes_palette_hcl("Zissou1", n = 7))
palette_blue <- colorRampPalette(c("lightblue", "blue", "darkblue"))(7)
# Replace the cvd palette with palette_wes for depht and palette blue for uncertainty
gg1 <- cblind.plot(uncertainty_resize, cvd = palette_blue)
gg2 <- cblind.plot(uncertainty_resize, cvd = "deuteranopia")
gg3 <- cblind.plot(uncertainty_resize, cvd = "tritanopia")
gg4 <- cblind.plot(uncertainty_resize, cvd = "protanopia")
grid <- grid.arrange(
arrangeGrob(gg1, bottom = textGrob("Original", gp = gpar(fontsize = 14))),
arrangeGrob(gg2, bottom = textGrob("Deuteranopia", gp = gpar(fontsize = 14))),
arrangeGrob(gg3, bottom = textGrob("Tritanopia", gp = gpar(fontsize = 14))),
arrangeGrob(gg4, bottom = textGrob("Protanopia", gp = gpar(fontsize = 14))),
nrow = 2, ncol = 2,
top = textGrob("Soil depth uncertainty for different visions", gp = gpar(fontsize = 16, fontface = "bold")))
ggsave("./export/Visualisation_soil_depth_uncertainty.png", grid, width = 20, height = 10)
ggsave("./export/Visualisation_soil_depth_uncertainty.pdf", grid, width = 20, height = 10)
dev.off()

(#fig:Colorblind plot hide)Soil depth prediction map adapted for colorblindness
\(\\[0.5cm]\)

(#fig:Colorblind plot hide second)Soil depth prediction uncertainty map adapted for colorblind
8.3.3 Export final maps
# 06.4 Export final maps =======================================================
# Replace missing values by zero as it occurs only in mountainous area
uncertainty_raster[is.na(uncertainty_raster)] <- 0
predicted_raster[is.na(predicted_raster)] <- 0
# For GeoTiff format
soil_depth_map <- stack(predicted_raster, uncertainty_raster)
soil_depth_crop <- crop(soil_depth_map, survey)
soil_depth_mask <- mask(soil_depth_crop, survey, inverse=FALSE, updatevalue=NA, updateNA=TRUE)
crs(soil_depth_mask) <- "EPSG:32638"
x <- rast(soil_depth_mask)
x_repro <- project(x, "EPSG:4326")
names(x_repro) <- c("Prediction", "Uncertainty")
terra::writeRaster(x_repro,"./export/Soil_depth_prediction_map.tif", overwrite=TRUE)
# For netCDF format
CDF_df <- lapply(1:nlayers(x_repro), function(i) {
rast(x_repro[[i]])
})
names(CDF_df) <- c("Prediction", "Uncertainty")
soil_to_netcdf <- function(soil_list, output_file, overwrite = FALSE) {
# Check if file exists and handle overwrite
if (file.exists(output_file) && !overwrite) {
stop("File already exists and overwrite = FALSE")
}
# If file exists and overwrite is TRUE, remove the existing file
if (file.exists(output_file) && overwrite) {
file.remove(output_file)
}
# Get dimensions and CRS from first raster
r <- soil_list[[1]]
nx <- ncol(r)
ny <- nrow(r)
crs_string <- crs(r)
# Create longitude and latitude vectors
ext <- ext(r)
lon <- seq(from = ext[1], to = ext[2], length.out = nx)
lat <- seq(from = ext[3], to = ext[4], length.out = ny) # Changed back to ascending order
# Define dimensions
londim <- ncdim_def("longitude", "degrees_east", lon)
latdim <- ncdim_def("latitude", "degrees_north", lat)
# Define units for each soil property
units_list <- list(
Prediction = "cm",
Uncertainty = "cm"
)
# Create list of variables with appropriate units
var_list <- list()
for (var_name in names(soil_list)) {
var_list[[var_name]] <- ncvar_def(
name = var_name,
units = units_list[[var_name]],
dim = list(londim, latdim),
missval = NA
)
}
# Create netCDF file
ncout <- nc_create(output_file, var_list)
# Write data
for (var_name in names(soil_list)) {
# Convert SpatRaster to matrix and handle orientation
values_matrix <- t(as.matrix(soil_list[[var_name]], wide=TRUE))
values_matrix <- values_matrix[,ncol(values_matrix):1]
ncvar_put(ncout, var_list[[var_name]], vals = values_matrix)
}
# Add global attributes
ncatt_put(ncout, 0, "title", "Soil depth until R horizon ")
ncatt_put(ncout,0,"institution","Tuebingen University, CRC1070 ResourceCultures")
ncatt_put(ncout, 0, "description", "Soil depth until R horizon for the top 100 cm in the Northern Kurdsitan region of Irak")
ncatt_put(ncout,0,"author", "Mathias Bellat PhD. candidate at Tuebingen University (mathias.bellat@uni-tuebingen.de)")
ncatt_put(ncout, 0, "creation_date", format(Sys.time(), "%Y-%m-%d %H:%M:%S"))
# Add CRS information
if (!is.na(crs_string)) {
ncatt_put(ncout, 0, "crs", crs_string)
ncatt_put(ncout, 0, "spatial_ref", crs_string)
# Add standard CF grid mapping attributes
ncatt_put(ncout, 0, "Conventions", "CF-1.6")
ncatt_put(ncout, "longitude", "standard_name", "longitude")
ncatt_put(ncout, "longitude", "axis", "X")
ncatt_put(ncout, "latitude", "standard_name", "latitude")
ncatt_put(ncout, "latitude", "axis", "Y")
}
# Add variable descriptions
var_descriptions <- list(
Prediction = "Soil depth until R horizon up to 100 cm",
Uncertainty = "Uncertainty of the Quantile Random Forest of soil depth prediction based on 0.05 - 0.95 interval"
)
# Add variable-specific attributes
for (var_name in names(soil_list)) {
ncatt_put(ncout, var_list[[var_name]], "long_name", var_descriptions[[var_name]])
}
# Close the file
nc_close(ncout)
}
soil_to_netcdf(CDF_df, "./export/Soil_depth_prediction_map.nc", overwrite = TRUE)
# 06.5 Final visualisations ====================================================
#Change parameters for uncertainy and predictions
raster_resize <- aggregate(soil_depth_map, fact=5, fun=mean)
rasterdf <- raster::as.data.frame(raster_resize, xy = TRUE)
rasterdf <- rasterdf[complete.cases(rasterdf),]
bounds <- st_bbox(survey)
xlim_new <- c(bounds["xmin"] - 3000, bounds["xmax"] + 3000)
ylim_new <- c(bounds["ymin"] - 3000, bounds["ymax"] + 3000)
palette_wes <- rev(wes_palette("Zissou1", type = "continuous"))
palette_blue <- colorRampPalette(c("lightblue", "blue", "darkblue"))(7)
gg1 <- ggplot() +
geom_raster(data = rasterdf,
aes(x = x, y = y,fill = Uncertainty )) +
ggtitle("Soil depth prediction uncertainty map") +
scale_fill_gradientn(colors = palette_blue,
name = "Uncertainty") +
annotation_scale(
location = "br",
width_hint = 0.3,
height = unit(0.3, "cm"),
line_col = "black",
text_col = "black",
bar_cols = c("white", "red")
) +
annotate("text", label = paste("Projection: WGS84 UTM 38N"),
x = Inf, y = -Inf, hjust = 1.5, vjust = -3, size = 3, color = "black") +
annotation_north_arrow(location = "tr", which_north = "true", height = unit(1, "cm"), width = unit(0.75, "cm")) +
theme_bw() +
theme(
panel.grid = element_blank(),
axis.text.y = element_text(angle = 90, vjust = 0.5, hjust = 0.5),
axis.title = element_blank(),
legend.position = "right")+
coord_equal(ratio = 1)
ggsave("./export/Visualisation_soil_depth_uncertainty_prediction_maps.pdf", gg1, width = 20, height = 10)
ggsave("./export/Visualisation_soil_depth_uncertainty_prediction_maps.png", gg1, width = 20, height = 10)

(#fig:final plot hide)Final soil depth prediction map

(#fig:final plot second hide)Final soil depth prediction uncertainty map adapted
# 06.6 Simplified version for publication =======================================
gg1 <- ggplot() +
geom_raster(data = rasterdf, aes(x = x, y = y, fill = Prediction)) +
ggtitle("Soil depth prediction map") +
scale_fill_gradientn(colors = palette_wes, name = "Soil depth in cm") +
theme_void() +
theme(
legend.position = "right",
plot.title = element_text(size = 8),
legend.title = element_text(size = 10),
legend.text = element_text(size = 8),
legend.key.size = unit(0.7, "cm")) +
coord_equal(ratio = 1)
gg2 <- ggplot() +
geom_raster(data = rasterdf, aes(x = x, y = y, fill = Uncertainty)) +
ggtitle("Soil depth uncertainty prediction map") +
scale_fill_gradientn(colors = palette_blue, name = "Uncertainty") +
theme_void() +
theme(
legend.position = "right",
plot.title = element_text(size = 8),
legend.title = element_text(size = 10),
legend.text = element_text(size = 8),
legend.key.size = unit(0.7, "cm")) +
coord_equal(ratio = 1)
combined_plot <- plot_grid(gg1, gg2, ncol = 2)
ggsave("./export/Publication_soil_depth_map.pdf", combined_plot, width = 20, height = 10)
ggsave("./export/Publication_soil_depth_map.png", combined_plot, width = 20, height = 10)
dev.off()

(#fig:publication plot hide)Simplified plot of the prediction maps