Praktikum 10 Interpolasi Titik

Tugas

Buat interpolasi menggunakan data meuse, dengan menggunakan seluruh metode interpolasi, yaitu Closest Observation, NN, IDW, Ensembel (Bebas menggunakan metode interpolasi apa saja yang ingin di ensemble, minimal 3), dan Kriging. setelah itu bandingkan RMSE dengan menggunakan Cross Validation

Persiapan Data

Data yang dipakai adalah meuse (155 titik pengamatan) dan meuse.grid (3103 lokasi prediksi). Peubah yang diinterpolasi adalah log(zinc), sama seperti contoh Kriging di modul.

library(sp)
library(sf)
library(terra)
library(gstat)
library(dplyr)
library(ggplot2)
library(viridis)

data(meuse)
data(meuse.grid)

# titik pengamatan, vble = log(zinc)
d <- st_as_sf(meuse, coords = c("x", "y"), crs = 28992) %>%
  transmute(vble = log(zinc))

# lokasi yang akan diprediksi (grid 40 m x 40 m)
coop <- st_as_sf(meuse.grid, coords = c("x", "y"), crs = 28992, remove = FALSE)

# batas wilayah untuk diagram Voronoi
map <- st_sf(geometry = st_buffer(
  st_convex_hull(st_union(c(st_geometry(d), st_geometry(coop)))), 100))

c(jumlah_titik = nrow(d), jumlah_lokasi_prediksi = nrow(coop))
##           jumlah_titik jumlah_lokasi_prediksi 
##                    155                   3103
summary(d$vble)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   4.727   5.288   5.787   5.886   6.514   7.517
ggplot() +
  geom_tile(data = meuse.grid, aes(x, y), fill = "grey90", width = 40, height = 40) +
  geom_sf(data = d, aes(color = vble), size = 2) +
  scale_color_viridis(name = "log(zinc)") +
  theme_bw() + labs(title = "Sebaran titik pengamatan log(zinc)")

Titik dengan nilai tinggi dan rendah terlihat mengelompok, artinya ada pola spasial.

Fungsi Prediksi Tiap Metode

Setiap metode dibuat dalam bentuk fungsi dengan input data latih (train) dan lokasi prediksi (test), supaya bisa dipakai lagi saat cross validation.

RMSE <- function(observed, predicted) sqrt(mean((observed - predicted)^2))

# 1. Closest Observation: ambil nilai titik terdekat (poligon Voronoi)
closest_obs <- function(train, test) {
  v <- st_as_sf(terra::voronoi(terra::vect(train), bnd = terra::vect(map)))
  st_crs(v) <- st_crs(train)
  p <- st_join(test, v["vble"], join = st_within, suffix = c("", ".v"))
  p[[if ("vble.v" %in% names(p)) "vble.v" else "vble"]]
}

# 2. IDW: semua titik ikut, pangkat jarak (idp) = 1
idw_p <- function(train, test) {
  g <- gstat(formula = vble ~ 1, locations = train, nmax = nrow(train),
             set = list(idp = 1, debug = 0))
  predict(g, test)$var1.pred
}

# 3. Nearest Neighbors: rata-rata 5 titik terdekat, bobot sama (idp = 0)
nn_p <- function(train, test) {
  g <- gstat(formula = vble ~ 1, locations = train, nmax = 5,
             set = list(idp = 0, debug = 0))
  predict(g, test)$var1.pred
}

# 4. Ordinary Kriging: variogram spherical, nilai awal sama seperti di modul
fit_vgm <- function(train) {
  v <- variogram(vble ~ 1, train)
  fit.variogram(v, vgm(psill = 0.5, model = "Sph", range = 900, nugget = 0.1))
}
kr_p <- function(train, test) {
  g <- gstat(formula = vble ~ 1, data = train, model = fit_vgm(train),
             set = list(debug = 0))
  predict(g, test)$var1.pred
}

Closest Observation

Nilai prediksi diambil dari titik pengamatan yang paling dekat, dengan bantuan poligon Voronoi.

v <- st_as_sf(terra::voronoi(terra::vect(d), bnd = terra::vect(map)))
st_crs(v) <- st_crs(d)

ggplot() +
  geom_sf(data = v, aes(fill = vble), alpha = 0.7, color = "white", linewidth = 0.1) +
  geom_sf(data = d, size = 0.6) +
  scale_fill_viridis(name = "log(zinc)") +
  theme_bw() + labs(title = "Diagram Voronoi")

Inverse Distance Weighting (IDW)

Bobot berbanding terbalik dengan jarak (idp = 1) dan semua titik pengamatan dipakai. Petanya ada di bagian Peta Hasil Interpolasi.

Nearest Neighbors (NN)

Prediksi adalah rata-rata 5 titik terdekat dengan bobot sama (nmax = 5, idp = 0). Petanya juga ada di bagian Peta Hasil Interpolasi.

Kriging

Semivariogram

Semivariogram empiris dihitung dulu dari data, lalu dicocokkan dengan model spherical. Nilai awalnya sama dengan modul (psill = 0.5, range = 900, nugget = 0.1).

v_emp <- variogram(vble ~ 1, d)
fv <- fit_vgm(d)
fv
##   model      psill    range
## 1   Nug 0.05066017   0.0000
## 2   Sph 0.59060556 897.0044
plot(v_emp, fv, cex = 1.5)

Hasilnya nugget 0.051, partial sill 0.591, dan range 897 meter.

Prediksi dan Varians

k <- gstat(formula = vble ~ 1, data = d, model = fv, set = list(debug = 0))
kpred <- predict(k, coop)

ggplot() + geom_sf(data = kpred, aes(color = var1.pred)) + geom_sf(data = d, size = 0.5) +
  scale_color_viridis(name = "log(zinc)") + theme_bw() +
  labs(title = "Prediksi Ordinary Kriging")

ggplot() + geom_sf(data = kpred, aes(color = var1.var)) + geom_sf(data = d, size = 0.5) +
  scale_color_viridis(name = "varians") + theme_bw() +
  labs(title = "Varians Kriging")

Varians paling kecil di sekitar titik pengamatan dan makin besar di area yang jauh dari titik.

Ensembel

Ada dua ensembel:

  • Ensembel A: rata-rata Closest Observation, IDW, dan NN dengan bobot sama (1/3 masing-masing).
  • Ensembel B: Closest Observation, IDW, NN, dan Kriging dengan bobot invers RMSE (RMSE makin kecil, bobot makin besar).

Bobot Ensembel B dihitung dari cross validation di dalam data latih, bukan dari data uji seperti di modul, supaya data uji tidak ikut menentukan bobot.

metode <- function(train, test) {
  data.frame(closest = closest_obs(train, test), idw = idw_p(train, test),
             nn = nn_p(train, test), krig = kr_p(train, test))
}

# bobot invers RMSE dari 5-fold CV di dalam data latih
bobot_cv <- function(train, k = 5) {
  kf <- dismo::kfold(nrow(train), k)
  hasil <- do.call(rbind, lapply(1:k, function(i) {
    te <- train[kf == i, ]
    cbind(obs = te$vble, metode(train[kf != i, ], te))
  }))
  r <- sapply(hasil[, -1], function(p) RMSE(hasil$obs, p))
  w <- 1 / r
  w / sum(w)
}

Peta Hasil Interpolasi

Peta dibuat dari seluruh 155 titik pengamatan.

set.seed(123)
w_all <- bobot_cv(d)

P <- metode(d, coop)
P$ens_sama  <- (P$closest + P$idw + P$nn) / 3
P$ens_bobot <- as.numeric(as.matrix(P[, c("closest", "idw", "nn", "krig")]) %*% w_all)

label <- c(closest = "Closest Observation", idw = "IDW", nn = "NN (k = 5)",
           ens_sama = "Ensembel A (bobot sama)", krig = "Kriging",
           ens_bobot = "Ensembel B (bobot RMSE)")
urut <- c("closest", "idw", "nn", "ens_sama", "krig", "ens_bobot")

peta <- do.call(rbind, lapply(urut, function(m)
  data.frame(x = coop$x, y = coop$y, pred = P[[m]], metode = label[m])))
peta$metode <- factor(peta$metode, levels = label[urut])

ggplot(peta, aes(x, y, fill = pred)) + geom_tile(width = 40, height = 40) +
  facet_wrap(~metode, ncol = 3) + coord_equal() +
  scale_fill_viridis(name = "log(zinc)") + theme_bw() +
  theme(axis.text = element_blank(), axis.ticks = element_blank(),
        axis.title = element_blank(), strip.text = element_text(size = 8))

Bobot Ensembel B pada peta di atas:

knitr::kable(data.frame(Metode = c("Closest Observation", "IDW", "NN", "Kriging"),
                        Bobot = round(w_all, 3)), row.names = FALSE)
Metode Bobot
Closest Observation 0.229
IDW 0.209
NN 0.251
Kriging 0.310

Cross Validation

Data dibagi menjadi 5 lipatan dengan set.seed(123). Pada tiap lipatan, model dibangun dari 4 lipatan lalu dipakai memprediksi 1 lipatan sisanya, kemudian RMSE dihitung. Variogram Kriging dicocokkan ulang pada setiap data latih.

cv_run <- function(seed, k = 5) {
  set.seed(seed)
  kf <- dismo::kfold(nrow(d), k)
  do.call(rbind, lapply(1:k, function(i) {
    tr <- d[kf != i, ]; te <- d[kf == i, ]
    p <- metode(tr, te)
    w <- bobot_cv(tr)
    p$ens_sama  <- (p$closest + p$idw + p$nn) / 3
    p$ens_bobot <- as.numeric(as.matrix(p[, c("closest", "idw", "nn", "krig")]) %*% w)
    r <- sapply(p[urut], function(x) RMSE(te$vble, x))
    data.frame(fold = i, t(r))
  }))
}

hasil <- cv_run(123)
names(hasil)[-1] <- label[urut]
knitr::kable(hasil, digits = 4, row.names = FALSE, caption = "RMSE tiap lipatan (skala log)")
RMSE tiap lipatan (skala log)
fold Closest Observation IDW NN (k = 5) Ensembel A (bobot sama) Kriging Ensembel B (bobot RMSE)
1 0.6327 0.5496 0.4958 0.4707 0.4281 0.4476
2 0.5987 0.5828 0.4943 0.4733 0.4183 0.4441
3 0.6328 0.7843 0.6516 0.6379 0.5055 0.5891
4 0.4874 0.6333 0.5316 0.4915 0.4198 0.4566
5 0.5593 0.6293 0.4791 0.4820 0.3840 0.4364
rata <- colMeans(hasil[, -1])
tab <- data.frame(Metode = names(rata), RMSE_rata_rata = round(rata, 4),
                  Peringkat = rank(rata), row.names = NULL)
knitr::kable(tab[order(tab$Peringkat), ], row.names = FALSE,
             caption = "Rata-rata RMSE 5 lipatan")
Rata-rata RMSE 5 lipatan
Metode RMSE_rata_rata Peringkat
Kriging 0.4312 1
Ensembel B (bobot RMSE) 0.4748 2
Ensembel A (bobot sama) 0.5111 3
NN (k = 5) 0.5305 4
Closest Observation 0.5822 5
IDW 0.6358 6
tab$Metode <- factor(tab$Metode, levels = tab$Metode[order(tab$RMSE_rata_rata)])
ggplot(tab, aes(Metode, RMSE_rata_rata, fill = Metode)) +
  geom_col(show.legend = FALSE) +
  geom_text(aes(label = sprintf("%.3f", RMSE_rata_rata)), vjust = -0.4) +
  scale_fill_viridis_d() + theme_bw() +
  labs(y = "RMSE rata-rata", x = NULL) +
  theme(axis.text.x = element_text(angle = 20, hjust = 1))

Ulang dengan Seed Lain

Pembagian lipatan bersifat acak, jadi 5-fold CV diulang dengan seed 1 sampai 20 untuk melihat apakah urutan metodenya berubah.

ulang <- t(sapply(1:20, function(s) colMeans(cv_run(s)[, -1])))
colnames(ulang) <- label[urut]

ringkas <- data.frame(
  Metode = colnames(ulang),
  Rata_rata = apply(ulang, 2, mean),
  SD = apply(ulang, 2, sd),
  Terbaik = as.integer(table(factor(colnames(ulang)[apply(ulang, 1, which.min)],
                                    levels = colnames(ulang)))),
  row.names = NULL)
knitr::kable(ringkas[order(ringkas$Rata_rata), ], digits = 4, row.names = FALSE,
             caption = "RMSE rata-rata dari 20 ulangan 5-fold CV (Terbaik = jumlah ulangan dengan RMSE terkecil)")
RMSE rata-rata dari 20 ulangan 5-fold CV (Terbaik = jumlah ulangan dengan RMSE terkecil)
Metode Rata_rata SD Terbaik
Kriging 0.4041 0.0127 20
Ensembel B (bobot RMSE) 0.4531 0.0152 0
NN (k = 5) 0.4881 0.0140 0
Ensembel A (bobot sama) 0.4928 0.0168 0
Closest Observation 0.5699 0.0290 0
IDW 0.6385 0.0067 0

Kesimpulan

Kriging memiliki RMSE terkecil, yaitu 0.431 pada seed 123, dan terbaik pada 20 dari 20 ulangan seed. Urutan dari RMSE terkecil: Kriging (0.431), Ensembel B (bobot RMSE) (0.475), Ensembel A (bobot sama) (0.511), NN (k = 5) (0.530), Closest Observation (0.582), IDW (0.636).

IDW memiliki RMSE paling besar karena semua titik ikut dihitung, sehingga prediksinya mendekati rata-rata dan variasi lokal tidak tertangkap (terlihat juga pada petanya). Closest Observation dan NN lebih baik karena hanya memakai titik di sekitar lokasi.

Ensembel A lebih baik daripada Closest Observation dan IDW, tetapi dibanding NN hasilnya tidak konsisten. Pada seed 123 Ensembel A lebih kecil, sedangkan pada rata-rata 20 ulangan NN (0.488) lebih kecil daripada Ensembel A (0.493). Ensembel B tidak mengalahkan Kriging karena sekitar 69% bobotnya ada pada tiga metode lain yang RMSE-nya lebih besar.

Semua RMSE dihitung pada skala log.