Interpolasi Spasial: Data Jura (Logam Berat Tanah)

Data yang digunakan

Dataset jura berisi hasil pengukuran konsentrasi logam berat pada tanah lapisan atas di wilayah Jura, Swiss, dalam satuan mg/kg. Dataset ini mencakup beberapa logam (Cd, Co, Cr, Cu, Ni, Pb, dan Zn), dan variabel yang diinterpolasi dalam analisis ini adalah konsentrasi seng (Zn). Dataset terdiri dari jura.pred (259 titik), jura.val (100 titik), dan jura.grid (grid lokasi prediksi). Karena evaluasi dilakukan dengan K-fold cross validation, jura.pred dan jura.val digabung menjadi 359 titik. Koordinat Xloc dan Yloc berada dalam satuan kilometer pada sistem koordinat lokal. Konsentrasi Zn ditransformasi dengan logaritma natural, log(Zn), sehingga semua nilai prediksi, peta, dan RMSE pada analisis ini berada dalam skala log.

library(gstat)
## Warning: package 'gstat' was built under R version 4.5.3
library(sf)
## Warning: package 'sf' was built under R version 4.5.3
## Linking to GEOS 3.14.1, GDAL 3.12.1, PROJ 9.7.1; sf_use_s2() is TRUE
library(terra)
## Warning: package 'terra' was built under R version 4.5.3
## terra 1.9.34
library(tmap)
## Warning: package 'tmap' was built under R version 4.5.3
library(viridis)
## Warning: package 'viridis' was built under R version 4.5.3
## Loading required package: viridisLite
## Warning: package 'viridisLite' was built under R version 4.5.2
library(dismo)
## Warning: package 'dismo' was built under R version 4.5.3
## Loading required package: raster
## Warning: package 'raster' was built under R version 4.5.3
## Loading required package: sp
## Warning: package 'sp' was built under R version 4.5.3
library(dplyr)
## Warning: package 'dplyr' was built under R version 4.5.2
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:raster':
## 
##     intersect, select, union
## The following objects are masked from 'package:terra':
## 
##     intersect, union
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
# Fungsi untuk membantu tampilan peta
tmap_v4 <- utils::packageVersion("tmap") >= "4.0.0"

lay_poly   <- function(a) if (tmap_v4) tm_polygons(fill_alpha = a) else tm_polygons(alpha = a)
lay_dots   <- function(v) if (tmap_v4) tm_dots(fill = v, fill.scale = tm_scale_continuous(values = "viridis")) else tm_dots(v, palette = "viridis")
lay_fill   <- function(v, a) if (tmap_v4) tm_fill(fill = v, fill.scale = tm_scale_continuous(values = "viridis"), fill_alpha = a) else tm_fill(col = v, palette = "viridis", alpha = a)
lay_raster <- function(a) if (tmap_v4) tm_raster(col.scale = tm_scale_continuous(values = "viridis"), col_alpha = a) else tm_raster(alpha = a, palette = "viridis")
lay_judul  <- function(t) if (tmap_v4) tm_title(t) else tm_layout(title = t)
data(jura) # memuat jura.pred, jura.val, jura.grid

# Pilih variabel yang diinterpolasi
VAR       <- "Zn"  
LOG_TRANS <- TRUE   # transformasi log

dat <- rbind(jura.pred, jura.val)
d <- st_as_sf(dat, coords = c("Xloc", "Yloc"), crs = NA, remove = FALSE)

# Menambahkan kolom variabel yang diinterpolasi ke dalam dataset
d$vble <- if (LOG_TRANS) log(dat[[VAR]]) else dat[[VAR]]

nrow(d)
## [1] 359
summary(d$vble)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   3.219   4.000   4.298   4.254   4.500   5.560
hist(d$vble, main = VAR, xlab = ifelse(LOG_TRANS, paste0("log(", VAR, ")"), VAR))

# Batas wilayah
r0 <- rast(xmin = min(jura.grid$Xloc) - 0.025, xmax = max(jura.grid$Xloc) + 0.025,
           ymin = min(jura.grid$Yloc) - 0.025, ymax = max(jura.grid$Yloc) + 0.025,
           resolution = 0.05)
r0[cellFromXY(r0, as.matrix(jura.grid[, c("Xloc", "Yloc")]))] <- 1
map <- as.polygons(r0, dissolve = TRUE) |> fillHoles() |>
  st_as_sf() |> st_set_crs(NA)

# cek apakah semua titik pengamatan berada di dalam batas (TRUE)
sum(lengths(st_intersects(d, map)) > 0) == nrow(d)
## [1] TRUE
# tampilin peta
tmap_mode("plot")
## ℹ tmap modes "plot" - "view"
## ℹ toggle with `tmap::ttm()`
tm_shape(map) + lay_poly(0.3) + tm_shape(d) + lay_dots("vble")

Untuk membuat prediksi, kita buat grid raster yang mencakup seluruh area dan menggunakan titik pusat grid sebagai lokasi prediksi.

# Membuat grid raster 100x100 sel untuk menutupi wilayah Jura
grid <- rast(map, nrows = 100, ncols = 100)

# Mengambil koordinat titik pusat dari setiap sel dalam grid
xy <- xyFromCell(grid, 1:ncell(grid))

# Membuat objek sf untuk lokasi prediksi
coop <- st_as_sf(as.data.frame(xy), coords = c("x", "y"), crs = st_crs(map))

# Menyaring lokasi prediksi yang berada dalam batas wilayah
coop <- st_filter(coop, map)

qtm(coop)

Metode Deterministik

Closest Observation

resp <- coop
resp$pred <- d$vble[st_nearest_feature(coop, d)]
pred <- rasterize(vect(resp), grid, field = "pred", fun = "mean")
c1 <- tm_shape(pred) + lay_raster(0.6) + lay_judul("Closest Observation") 
c1

Peta Closest Observation menampilkan permukaan berbentuk petak-petak poligon (Voronoi) dengan perubahan nilai yang tiba-tiba di batas antarpetak. Rentang prediksinya paling lebar di antara metode lain (log Zn sekitar 3,2 sampai 5,6) karena nilai titik pengamatan disalin apa adanya tanpa perataan, sehingga nilai ekstrem tetap muncul. Karena satu titik dianggap mewakili seluruh petaknya, metode ini paling sensitif terhadap variasi lokal.

Invers Distance Weight

# Menerapkan IDW menggunakan semua lokasi titik
res <- gstat(formula = vble ~ 1, locations = d, nmax = nrow(d),
             set = list(idp = 1))

# Mendapatkan prediksi untuk lokasi yang belum teramati
resp <- predict(res, coop, debug.level = 0)

resp$x <- st_coordinates(resp)[, 1]
resp$y <- st_coordinates(resp)[, 2]
resp$pred <- resp$var1.pred

pred <- rasterize(vect(resp), grid, field = "pred", fun = "mean")
c2 <- tm_shape(pred) + lay_raster(0.6) + lay_judul("IDW")
c2

Peta IDW menunjukkan kadar log(Zn) didominasi nilai sedang (sekitar 4,2 sampai 4,4), dengan dua area bernilai rendah di bagian tengah-atas dan kiri-bawah serta beberapa bintik bernilai tinggi yang tersebar. Pola bintik melingkar di sekitar titik pengamatan merupakan ciri khas IDW, dan rentang prediksinya lebih sempit daripada data asli karena nilai ekstrem terhalus oleh perataan jarak.

Nearest Neighbors

res <- gstat(formula = vble ~ 1, locations = d, nmax = 5,
             set = list(idp = 0))

resp <- predict(res, coop, debug.level = 0)

resp$x <- st_coordinates(resp)[, 1]
resp$y <- st_coordinates(resp)[, 2]
resp$pred <- resp$var1.pred

pred <- rasterize(vect(resp), grid, field = "pred", fun = "mean")
c3 <- tm_shape(pred) + lay_raster(0.6) + lay_judul("Nearest Neighbors")
c3

Metode Nearest Neighbors menghasilkan pola interpolasi berupa zona-zona dengan nilai konsentrasi Zn yang relatif serupa. Wilayah dengan konsentrasi relatif tinggi ditunjukkan oleh warna kuning, sedangkan konsentrasi relatif rendah ditunjukkan oleh warna ungu. Prediksi diperoleh dari rata-rata lima titik pengamatan terdekat dengan bobot yang sama, sehingga pola spasial yang terbentuk dipengaruhi oleh nilai dan lokasi titik-titik pengamatan di sekitarnya.

Pendekatan Ensembel

Ensembel di bagian ini menggabungkan tiga metode (Closest Observation, IDW, Nearest Neighbors) dengan bobot sama.

# Closest observation (Voronoi)
p1 <- d$vble[st_nearest_feature(coop, d)]

# IDW
gs <- gstat(formula = vble ~ 1, locations = d, nmax = nrow(d),
            set = list(idp = 1))
p2 <- predict(gs, coop, debug.level = 0)$var1.pred

# Nearest neighbors
nn <- gstat(formula = vble ~ 1, locations = d, nmax = 5,
            set = list(idp = 0))
p3 <- predict(nn, coop, debug.level = 0)$var1.pred

# Ensemble (equal weights)
weights <- c(1/3, 1/3, 1/3)
p4 <- p1 * weights[1] + p2 * weights[2] + p3 * weights[3]

resp <- data.frame(x = st_coordinates(coop)[, 1],
                   y = st_coordinates(coop)[, 2], pred = p4)
resp <- st_as_sf(resp, coords = c("x", "y"), crs = st_crs(map))
pred <- rasterize(vect(resp), grid, field = "pred", fun = "mean")
c4 <- tm_shape(pred) + lay_raster(0.6) + lay_judul("Ensembel (CLosest Observation + IDW + NN)")  
c4
## [plot mode] fit legend/component: Some legend items or map compoments do not
## fit well, and are therefore rescaled.
## ℹ Set the tmap option `component.autoscale = FALSE` to disable rescaling.

Peta Ensemble menunjukkan persebaran prediksi konsentrasi Zn yang merupakan hasil penggabungan tiga metode interpolasi dengan bobot yang sama. Wilayah dengan konsentrasi Zn relatif tinggi ditunjukkan oleh warna kuning, terutama di bagian selatan dan beberapa area tengah, sedangkan konsentrasi relatif rendah ditunjukkan oleh warna ungu, terutama di bagian tengah-utara dan barat daya. Sementara itu, sebagian besar wilayah didominasi warna hijau hingga biru kehijauan yang menunjukkan konsentrasi Zn pada tingkat menengah. Pola yang dihasilkan memperlihatkan variasi spasial yang merupakan kombinasi karakteristik ketiga metode, sehingga prediksi tidak hanya bergantung pada titik pengamatan terdekat, tetapi juga mempertimbangkan pembobotan berdasarkan jarak.

# Gabungkan semua plot dalam satu frame
tmap_arrange(c1, c2, c3, c4, ncol = 2)
## [plot mode] fit legend/component: Some legend items or map compoments do not
## fit well, and are therefore rescaled.
## ℹ Set the tmap option `component.autoscale = FALSE` to disable rescaling.
## [plot mode] fit legend/component: Some legend items or map compoments do not
## fit well, and are therefore rescaled.
## ℹ Set the tmap option `component.autoscale = FALSE` to disable rescaling.
## [plot mode] fit legend/component: Some legend items or map compoments do not
## fit well, and are therefore rescaled.
## ℹ Set the tmap option `component.autoscale = FALSE` to disable rescaling.

Evaluasi Model

set.seed(123)

# Fungsi untuk menghitung RMSE
RMSE <- function(observed, predicted) {
  sqrt(mean((observed - predicted)^2))
}

# Pembagian data untuk K-fold
kf <- kfold(nrow(d), k = 5)

# Vectors to store the RMSE values obtained with each method
rmse1 <- rep(NA, 5) # Closest observation
rmse2 <- rep(NA, 5) # IDW
rmse3 <- rep(NA, 5) # Nearest neighbors
rmse4 <- rep(NA, 5) # Ensemble

for (k in 1:5) {
  # Split data into test and train
  test  <- d[kf == k, ]
  train <- d[kf != k, ]

  # Closest observation (nilai titik terdekat = prediksi Voronoi)
  p1 <- train$vble[st_nearest_feature(test, train)]
  rmse1[k] <- RMSE(test$vble, p1)

  # IDW
  gs <- gstat(formula = vble ~ 1, locations = train,
              nmax = nrow(train), set = list(idp = 1))
  p2 <- predict(gs, test, debug.level = 0)$var1.pred
  rmse2[k] <- RMSE(test$vble, p2)

  # Nearest neighbors
  nn <- gstat(formula = vble ~ 1, locations = train,
              nmax = 5, set = list(idp = 0))
  p3 <- predict(nn, test, debug.level = 0)$var1.pred
  rmse3[k] <- RMSE(test$vble, p3)

  # Ensemble (weights are inverse RMSE so lower RMSE higher weight)
  w <- 1 / c(rmse1[k], rmse2[k], rmse3[k])
  weights <- w / sum(w)
  p4 <- p1 * weights[1] + p2 * weights[2] + p3 * weights[3]
  rmse4[k] <- RMSE(test$vble, p4)
}

# RMSE obtained for each of the 5 splits
data.frame(closest.obs = rmse1, IDW = rmse2,
           nearest.neigh = rmse3, ensemble = rmse4)
##   closest.obs       IDW nearest.neigh  ensemble
## 1   0.4343861 0.3303143     0.3646094 0.3433597
## 2   0.3469489 0.3561124     0.3206350 0.3104163
## 3   0.4219734 0.3468850     0.3368395 0.3383548
## 4   0.3367056 0.3125789     0.2895793 0.2775792
## 5   0.3130178 0.2761392     0.2829812 0.2511632
# Average RMSE over the 5 splits
data.frame(closest.obs = mean(rmse1), IDW = mean(rmse2),
           nearest.neigh = mean(rmse3), ensemble = mean(rmse4))
##   closest.obs      IDW nearest.neigh  ensemble
## 1   0.3706064 0.324406     0.3189289 0.3041746

Metode Stokastik

Kriging

library(sp)
library(gstat)
library(sf)
library(mapview)
## Warning: package 'mapview' was built under R version 4.5.3
# Peta interaktif memakai koordinat lintang-bujur bawaan jura
d_ll <- st_as_sf(dat, coords = c("long", "lat"), crs = 4326)
d_ll$vble <- d$vble
mapview(d_ll, zcol = "vble", map.types = "CartoDB.Voyager")
# Variogram dengan cloud plot
vc <- variogram(vble ~ 1, d, cloud = TRUE)
plot(vc)

# Variogram dengan plot standar
v <- variogram(vble ~ 1, data = d)
plot(v)

vgm()
##    short                                      long
## 1    Nug                              Nug (nugget)
## 2    Exp                         Exp (exponential)
## 3    Sph                           Sph (spherical)
## 4    Gau                            Gau (gaussian)
## 5    Exc        Exclass (Exponential class/stable)
## 6    Mat                              Mat (Matern)
## 7    Ste Mat (Matern, M. Stein's parameterization)
## 8    Cir                            Cir (circular)
## 9    Lin                              Lin (linear)
## 10   Bes                              Bes (bessel)
## 11   Pen                      Pen (pentaspherical)
## 12   Per                            Per (periodic)
## 13   Wav                                Wav (wave)
## 14   Hol                                Hol (hole)
## 15   Log                         Log (logarithmic)
## 16   Pow                               Pow (power)
## 17   Spl                              Spl (spline)
## 18   Leg                            Leg (Legendre)
## 19   Err                   Err (Measurement error)
## 20   Int                           Int (Intercept)
# Menampilkan daftar model variogram
show.vgms(par.strip.text = list(cex = 0.75))

# Model variogram awal
MODEL_VGM <- "Sph"   # model yang di-fit; coba juga "Exp" lalu bandingkan RMSE
plateau  <- mean(tail(v$gamma, 6))
vinitial <- vgm(psill = max(plateau - min(v$gamma), 0.1 * plateau), model = MODEL_VGM,
                range = max(v$dist) / 3, nugget = min(v$gamma))
vinitial
##   model      psill     range
## 1   Nug 0.05061674 0.0000000
## 2   Sph 0.11025864 0.7308067
plot(v, vinitial, cex = 1.5)

# Menyesuaikan (fit) variogram dengan model MODEL_VGM
fv <- fit.variogram(object = v, model = vinitial)
fv
##   model      psill     range
## 1   Nug 0.03101508 0.0000000
## 2   Sph 0.12051941 0.6076852
plot(v, fv, cex = 1.5)

library(automap)
## Warning: package 'automap' was built under R version 4.5.3
# Autofit variogram
afvg <- autofitVariogram(vble ~ 1, as(d, "Spatial"))

# Plot hasil
plot(afvg)

library(ggplot2)
## Warning: package 'ggplot2' was built under R version 4.5.3
library(viridis)

# Membuat objek gstat dengan model variogram hasil fitting (fv)
k <- gstat(formula = vble ~ 1, data = d, model = fv)

# Melakukan prediksi pada grid
kpred <- predict(k, coop)
## [using ordinary kriging]
# Visualisasi prediksi
ggplot() + geom_sf(data = kpred, aes(color = var1.pred)) +
  geom_sf(data = d) +
  scale_color_viridis(name = ifelse(LOG_TRANS, paste0("log(", VAR, ")"), VAR)) +
  theme_bw()

# Visualisasi prediksi varians
ggplot() + geom_sf(data = kpred, aes(color = var1.var)) +
  geom_sf(data = d) +
  scale_color_viridis(name = "variance") + theme_bw()

# Peta raster kriging, dibandingkan dengan metode lain
resp <- kpred
resp$pred <- resp$var1.pred
pred <- rasterize(vect(resp), grid, field = "pred", fun = "mean")
c5 <- tm_shape(pred) + lay_raster(0.6) + lay_judul("Kriging (OK)")

tmap_arrange(c1, c2, c3, c4, c5, ncol = 3)
## [plot mode] fit legend/component: Some legend items or map compoments do not
## fit well, and are therefore rescaled.
## ℹ Set the tmap option `component.autoscale = FALSE` to disable rescaling.
## [plot mode] fit legend/component: Some legend items or map compoments do not
## fit well, and are therefore rescaled.
## ℹ Set the tmap option `component.autoscale = FALSE` to disable rescaling.
## [plot mode] fit legend/component: Some legend items or map compoments do not
## fit well, and are therefore rescaled.
## ℹ Set the tmap option `component.autoscale = FALSE` to disable rescaling.

Evaluasi Kriging dan Perbandingan Semua Metode

rmse5 <- rep(NA, 5) # Ordinary kriging

for (k in 1:5) {
  test  <- d[kf == k, ]
  train <- d[kf != k, ]

  vt  <- variogram(vble ~ 1, train)
  plt <- mean(tail(vt$gamma, 6))
  init <- function(m) vgm(psill = max(plt - min(vt$gamma), 0.1 * plt), model = m,
                        range = max(vt$dist) / 3, nugget = min(vt$gamma))
  fvt <- tryCatch(fit.variogram(vt, init(MODEL_VGM)), error = function(e) NULL)
  if (is.null(fvt) || isTRUE(attr(fvt, "singular"))) fvt <- fit.variogram(vt, init("Exp"))

  kr <- gstat(formula = vble ~ 1, data = train, model = fvt)
  p5 <- predict(kr, test, debug.level = 0)$var1.pred
  rmse5[k] <- RMSE(test$vble, p5)
}

# RMSE per fold, semua metode
data.frame(closest.obs = rmse1, IDW = rmse2, nearest.neigh = rmse3,
           ensemble = rmse4, kriging = rmse5)
##   closest.obs       IDW nearest.neigh  ensemble   kriging
## 1   0.4343861 0.3303143     0.3646094 0.3433597 0.3640644
## 2   0.3469489 0.3561124     0.3206350 0.3104163 0.2969862
## 3   0.4219734 0.3468850     0.3368395 0.3383548 0.3192235
## 4   0.3367056 0.3125789     0.2895793 0.2775792 0.2889410
## 5   0.3130178 0.2761392     0.2829812 0.2511632 0.2447274
# Rata-rata RMSE, semua metode
data.frame(closest.obs = mean(rmse1), IDW = mean(rmse2),
           nearest.neigh = mean(rmse3), ensemble = mean(rmse4),
           kriging = mean(rmse5))
##   closest.obs      IDW nearest.neigh  ensemble   kriging
## 1   0.3706064 0.324406     0.3189289 0.3041746 0.3027885

Rata-rata RMSE terkecil dimiliki kriging (0.303), diikuti ensembel (0.304), Nearest Neighbors (0.319), IDW (0.324), dan Closest Observation (0.371). Kriging dan ensembel nyaris setara, sedangkan Closest Observation paling besar galatnya.

Kesimpulan

Model variogram log(Zn) memiliki nugget sebesar 0.031, sill sebesar 0.152, dan range sebesar 0.61 km. Rasio nugget terhadap sill yang mencapai sekitar 20% mengindikasikan adanya ketergantungan spasial yang relatif kuat. Berdasarkan hasil evaluasi menggunakan 5-fold cross validation, metode Ordinary Kriging menunjukkan performa interpolasi terbaik dengan rata-rata RMSE sebesar 0.303, hampir setara dengan ensembel(0.304), lebih rendah dibandingkan Closest Observation (0.371), IDW (0.324), dan Nearest Neighbors (0.319). Hasil ini menunjukkan bahwa pemodelan ketergantungan spasial melalui variogram dapat menghasilkan prediksi konsentrasi Zn yang lebih akurat. Selain itu, Ordinary Kriging juga menyediakan informasi ketidakpastian melalui peta varians kriging sehingga dapat digunakan untuk menilai tingkat keandalan hasil interpolasi pada setiap lokasi.