Muhammad Daffa Fadhilah Athif
NIM: M0401241085
Tugas Regresi Spasial
Interpolasi spasial adalah proses menduga nilai suatu peubah pada
lokasi yang tidak tersampel berdasarkan nilai pada lokasi-lokasi yang
telah diamati. Pada tugas ini digunakan data
meuse (paket sp), yaitu data
kandungan logam berat tanah di dataran banjir Sungai Meuse, Belanda (155
titik pengamatan). Peubah yang diinterpolasi adalah
log(zinc), karena sebaran zinc menjulur ke
kanan dan transformasi log membuat sebaran lebih simetris serta lebih
sesuai untuk kriging.
Metode yang digunakan dibagi menjadi dua kelompok:
Kinerja seluruh metode dibandingkan menggunakan RMSE hasil k-fold Cross Validation.
library(sp) # sumber data meuse & meuse.grid
library(sf) # data spasial
library(gstat) # idw, variogram, kriging
library(ggplot2) # visualisasi
library(dplyr)
library(tidyr)
library(knitr)
data(meuse)
data(meuse.grid)
# Data titik pengamatan
meuse_sf <- st_as_sf(meuse, coords = c("x", "y"), crs = 28992, remove = FALSE)
meuse_sf$log_zinc <- log(meuse_sf$zinc)
# Grid lokasi prediksi
grid_sf <- st_as_sf(meuse.grid, coords = c("x", "y"), crs = 28992, remove = FALSE)
coords <- st_coordinates(grid_sf)
cat("Jumlah titik pengamatan:", nrow(meuse_sf), "\n")
## Jumlah titik pengamatan: 155
cat("Jumlah titik grid prediksi:", nrow(grid_sf), "\n")
## Jumlah titik grid prediksi: 3103
head(st_drop_geometry(meuse_sf)[, c("x", "y", "zinc", "log_zinc")])
summary(meuse_sf$log_zinc)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 4.727 5.288 5.787 5.886 6.514 7.517
hist_df <- st_drop_geometry(meuse_sf) |>
select(zinc, log_zinc) |>
pivot_longer(everything(), names_to = "Peubah", values_to = "nilai")
ggplot(hist_df, aes(nilai)) +
geom_histogram(bins = 20, fill = "steelblue", color = "white") +
facet_wrap(~ Peubah, scales = "free") +
labs(title = "Sebaran zinc dan log(zinc)", x = "Nilai", y = "Frekuensi") +
theme_minimal()
ggplot() +
geom_tile(data = meuse.grid, aes(x, y), fill = "grey92") +
geom_point(data = st_drop_geometry(meuse_sf),
aes(x, y, color = log_zinc, size = log_zinc)) +
scale_color_viridis_c(option = "plasma") +
coord_equal() +
labs(title = "Sebaran Titik Pengamatan log(zinc)", x = "X", y = "Y") +
theme_minimal()
Fungsi bantu dan pembagian fold untuk cross validation (fold yang sama dipakai oleh semua metode agar perbandingan adil):
rmse <- function(obs, pred) sqrt(mean((obs - pred)^2, na.rm = TRUE))
# fungsi untuk menggambar peta prediksi pada grid
peta_pred <- function(pred, judul, legend = "log(zinc)") {
ggplot(data.frame(X = coords[, 1], Y = coords[, 2], z = pred),
aes(X, Y, fill = z)) +
geom_raster() +
scale_fill_viridis_c(option = "plasma", name = legend) +
coord_equal() +
labs(title = judul, x = "X", y = "Y") +
theme_minimal()
}
set.seed(2024)
K <- 10
folds <- sample(rep(1:K, length.out = nrow(meuse_sf)))
table(folds)
## folds
## 1 2 3 4 5 6 7 8 9 10
## 16 16 16 16 16 15 15 15 15 15
Metode deterministik menduga nilai pada lokasi baru langsung dari nilai dan jarak titik-titik pengamatan di sekitarnya, tanpa memodelkan struktur autokorelasi spasial dan tanpa memberikan ukuran ketidakpastian prediksi.
Setiap titik grid diberi nilai dari satu pengamatan
terdekat (nmax = 1). Hasilnya setara dengan poligon
Thiessen/Voronoi.
pred_closest <- idw(log_zinc ~ 1, meuse_sf, grid_sf,
nmax = 1, idp = 0, debug.level = 0)
peta_pred(pred_closest$var1.pred, "Closest Observation")
Peta berbentuk blok-blok (poligon Thiessen) karena setiap sel hanya mengikuti satu titik terdekat, sehingga permukaan tidak halus dan sangat sensitif terhadap pengamatan tunggal.
IDW menduga nilai sebagai rata-rata terboboti dari seluruh
pengamatan, dengan bobot berbanding terbalik terhadap jarak berpangkat
idp:
\[\hat{z}(s_0) = \frac{\sum_{i=1}^{n} z(s_i)\, d_{0i}^{-p}}{\sum_{i=1}^{n} d_{0i}^{-p}}\]
Semakin besar p, semakin besar pengaruh titik yang
dekat. Nilai p terbaik dicari dengan 10-fold cross
validation.
idp_grid <- seq(0.5, 5, by = 0.5)
rmse_idp <- sapply(idp_grid, function(p) {
pred <- rep(NA_real_, nrow(meuse_sf))
for (i in 1:K) {
tr <- meuse_sf[folds != i, ]
te <- meuse_sf[folds == i, ]
pred[folds == i] <- idw(log_zinc ~ 1, tr, te, idp = p, debug.level = 0)$var1.pred
}
rmse(meuse_sf$log_zinc, pred)
})
tab_idp <- data.frame(idp = idp_grid, RMSE = round(rmse_idp, 4))
idp_opt <- idp_grid[which.min(rmse_idp)]
kable(tab_idp, caption = "RMSE IDW untuk berbagai nilai idp (10-fold CV)")
| idp | RMSE |
|---|---|
| 0.5 | 0.7033 |
| 1.0 | 0.6495 |
| 1.5 | 0.5822 |
| 2.0 | 0.5233 |
| 2.5 | 0.4853 |
| 3.0 | 0.4667 |
| 3.5 | 0.4610 |
| 4.0 | 0.4619 |
| 4.5 | 0.4656 |
| 5.0 | 0.4703 |
cat("idp optimum:", idp_opt, "\n")
## idp optimum: 3.5
ggplot(tab_idp, aes(idp, RMSE)) +
geom_line() + geom_point() +
geom_point(data = tab_idp[tab_idp$idp == idp_opt, ], color = "red", size = 3) +
labs(title = "Pemilihan idp pada IDW", x = "Pangkat jarak (idp)", y = "RMSE CV") +
theme_minimal()
pred_idw <- idw(log_zinc ~ 1, meuse_sf, grid_sf, idp = idp_opt, debug.level = 0)
peta_pred(pred_idw$var1.pred, paste0("IDW (idp = ", idp_opt, ")"))
Nilai prediksi adalah rata-rata sederhana dari k pengamatan
terdekat (idp = 0, semua tetangga berbobot sama). Jumlah
tetangga k terbaik dicari dengan 10-fold cross validation.
k_grid <- 2:15
rmse_k <- sapply(k_grid, function(k) {
pred <- rep(NA_real_, nrow(meuse_sf))
for (i in 1:K) {
tr <- meuse_sf[folds != i, ]
te <- meuse_sf[folds == i, ]
pred[folds == i] <- idw(log_zinc ~ 1, tr, te, nmax = k, idp = 0, debug.level = 0)$var1.pred
}
rmse(meuse_sf$log_zinc, pred)
})
tab_k <- data.frame(k = k_grid, RMSE = round(rmse_k, 4))
k_opt <- k_grid[which.min(rmse_k)]
kable(tab_k, caption = "RMSE NN untuk berbagai nilai k (10-fold CV)")
| k | RMSE |
|---|---|
| 2 | 0.4825 |
| 3 | 0.4443 |
| 4 | 0.4818 |
| 5 | 0.4906 |
| 6 | 0.4860 |
| 7 | 0.4917 |
| 8 | 0.4972 |
| 9 | 0.5140 |
| 10 | 0.5182 |
| 11 | 0.5364 |
| 12 | 0.5422 |
| 13 | 0.5481 |
| 14 | 0.5572 |
| 15 | 0.5672 |
cat("k optimum:", k_opt, "\n")
## k optimum: 3
ggplot(tab_k, aes(k, RMSE)) +
geom_line() + geom_point() +
geom_point(data = tab_k[tab_k$k == k_opt, ], color = "red", size = 3) +
labs(title = "Pemilihan jumlah tetangga (k) pada NN", x = "k", y = "RMSE CV") +
theme_minimal()
pred_nn <- idw(log_zinc ~ 1, meuse_sf, grid_sf,
nmax = k_opt, idp = 0, debug.level = 0)
peta_pred(pred_nn$var1.pred, paste0("Nearest Neighbors (k = ", k_opt, ")"))
Pendekatan ensembel menggabungkan prediksi dari beberapa metode sekaligus dengan harapan kesalahan masing-masing metode saling mengimbangi. Ensembel pada tugas ini dibentuk dari tiga metode deterministik: Closest Observation, IDW, dan NN. Dua skema penggabungan digunakan:
pred_ens <- (pred_closest$var1.pred + pred_idw$var1.pred + pred_nn$var1.pred) / 3
peta_pred(pred_ens, "Ensembel Rata-rata (Closest + IDW + NN)")
Berikut perbandingan peta keempat metode deterministik:
peta_det <- data.frame(
X = coords[, 1], Y = coords[, 2],
`Closest Observation` = pred_closest$var1.pred,
IDW = pred_idw$var1.pred,
`Nearest Neighbors` = pred_nn$var1.pred,
`Ensembel (rata-rata)` = pred_ens,
check.names = FALSE
) |>
pivot_longer(-c(X, Y), names_to = "Metode", values_to = "log_zinc")
peta_det$Metode <- factor(peta_det$Metode,
levels = c("Closest Observation", "IDW", "Nearest Neighbors", "Ensembel (rata-rata)"))
ggplot(peta_det, aes(X, Y, fill = log_zinc)) +
geom_raster() +
scale_fill_viridis_c(option = "plasma", name = "log(zinc)") +
coord_equal() +
facet_wrap(~ Metode, ncol = 2) +
labs(title = "Peta Interpolasi Metode Deterministik") +
theme_minimal() +
theme(axis.text = element_text(size = 6))
Evaluasi dilakukan dengan 10-fold cross validation: pada setiap iterasi model dibangun dari 9 fold (training) dan diuji pada 1 fold (testing). Hasil prediksi out-of-fold dikumpulkan lalu dihitung RMSE-nya.
cv_det <- function(data, folds, k_nn, idp) {
res <- data.frame(obs = data$log_zinc, fold = folds,
closest = NA_real_, idw = NA_real_, nn = NA_real_)
for (i in sort(unique(folds))) {
tr <- data[folds != i, ]
te <- data[folds == i, ]
idx <- folds == i
res$closest[idx] <- idw(log_zinc ~ 1, tr, te, nmax = 1, idp = 0, debug.level = 0)$var1.pred
res$idw[idx] <- idw(log_zinc ~ 1, tr, te, idp = idp, debug.level = 0)$var1.pred
res$nn[idx] <- idw(log_zinc ~ 1, tr, te, nmax = k_nn, idp = 0, debug.level = 0)$var1.pred
}
res
}
cv_res <- cv_det(meuse_sf, folds, k_nn = k_opt, idp = idp_opt)
# Ensembel rata-rata (Closest + IDW + NN)
cv_res$ens_mean <- (cv_res$closest + cv_res$idw + cv_res$nn) / 3
head(cv_res)
Bobot ensembel terboboti ditentukan dari kebalikan RMSE ketiga metode penyusun (semakin kecil RMSE, semakin besar bobotnya):
rmse_base <- c(Closest = rmse(cv_res$obs, cv_res$closest),
IDW = rmse(cv_res$obs, cv_res$idw),
NN = rmse(cv_res$obs, cv_res$nn))
w <- (1 / rmse_base) / sum(1 / rmse_base)
kable(data.frame(Metode = names(w), Bobot = round(w, 4)),
caption = "Bobot Ensembel Terboboti (1/RMSE)")
| Metode | Bobot | |
|---|---|---|
| Closest | Closest | 0.2891 |
| IDW | IDW | 0.3489 |
| NN | NN | 0.3620 |
cv_res$ens_wt <- w[["Closest"]] * cv_res$closest +
w[["IDW"]] * cv_res$idw +
w[["NN"]] * cv_res$nn
Catatan: bobot dihitung dari prediksi out-of-fold yang sama dengan yang dievaluasi, sehingga RMSE ensembel terboboti sedikit optimistik. Ensembel rata-rata bebas dari masalah ini.
ringkas <- function(obs, pred) {
c(RMSE = rmse(obs, pred), MAE = mean(abs(obs - pred)), R2_CV = cor(obs, pred)^2)
}
hasil_det <- rbind(
`Closest Observation` = ringkas(cv_res$obs, cv_res$closest),
`Inverse Distance Weight` = ringkas(cv_res$obs, cv_res$idw),
`Nearest Neighbors` = ringkas(cv_res$obs, cv_res$nn),
`Ensembel (rata-rata)` = ringkas(cv_res$obs, cv_res$ens_mean),
`Ensembel (terboboti)` = ringkas(cv_res$obs, cv_res$ens_wt)
) |> round(4)
kable(hasil_det, caption = "Evaluasi Metode Deterministik (10-fold CV, skala log(zinc))")
| RMSE | MAE | R2_CV | |
|---|---|---|---|
| Closest Observation | 0.5563 | 0.3890 | 0.4831 |
| Inverse Distance Weight | 0.4610 | 0.3549 | 0.5919 |
| Nearest Neighbors | 0.4443 | 0.3398 | 0.6207 |
| Ensembel (rata-rata) | 0.4546 | 0.3415 | 0.6010 |
| Ensembel (terboboti) | 0.4509 | 0.3404 | 0.6074 |
cv_long_det <- cv_res |>
select(obs, `Closest Observation` = closest, IDW = idw, `Nearest Neighbors` = nn,
`Ensembel (rata-rata)` = ens_mean, `Ensembel (terboboti)` = ens_wt) |>
pivot_longer(-obs, names_to = "Metode", values_to = "pred")
ggplot(cv_long_det, aes(obs, pred)) +
geom_point(alpha = 0.6, color = "steelblue") +
geom_abline(slope = 1, intercept = 0, color = "red", linetype = "dashed") +
facet_wrap(~ Metode, ncol = 3) +
labs(title = "Observasi vs Prediksi - Metode Deterministik",
x = "Observasi log(zinc)", y = "Prediksi log(zinc)") +
theme_minimal()
Metode stokastik memodelkan struktur autokorelasi spasial melalui semivariogram sehingga bobot pengamatan ditentukan oleh jarak sekaligus pola ketergantungan spasial data, dan prediksi dilengkapi dengan ukuran ketidakpastian (varians kriging). Metode yang digunakan adalah Ordinary Kriging.
Model semivariogram (Spherical, Exponential, Gaussian) dicocokkan, kemudian dipilih model dengan sum of squares error (SSErr) terkecil. Fungsi yang sama dipakai ulang di dalam cross validation sehingga semivariogram diestimasi ulang pada setiap training set (tidak ada kebocoran data dari testing set).
fit_vgm <- function(d, models = c("Sph", "Exp", "Gau")) {
v <- variogram(log_zinc ~ 1, d)
best <- NULL
best_sse <- Inf
for (m in models) {
f <- tryCatch(
suppressWarnings(
fit.variogram(v, vgm(psill = max(v$gamma) * 0.9, model = m,
range = max(v$dist) / 3, nugget = min(v$gamma)))
),
error = function(e) NULL
)
if (!is.null(f) && !isTRUE(attr(f, "singular"))) {
sse <- attr(f, "SSErr")
if (sse < best_sse) { best <- f; best_sse <- sse }
}
}
# cadangan jika semua model gagal
if (is.null(best)) best <- vgm(psill = 0.6, model = "Sph", range = 900, nugget = 0.05)
best
}
v_emp <- variogram(log_zinc ~ 1, meuse_sf)
v_fit <- fit_vgm(meuse_sf)
v_fit
plot(v_emp, v_fit, pch = 19, main = "Semivariogram log(zinc)")
pred_ok <- krige(log_zinc ~ 1, meuse_sf, grid_sf, model = v_fit, debug.level = 0)
peta_pred(pred_ok$var1.pred, "Ordinary Kriging")
ggplot(data.frame(X = coords[, 1], Y = coords[, 2], var = pred_ok$var1.var),
aes(X, Y, fill = var)) +
geom_raster() +
scale_fill_viridis_c(option = "magma", name = "Varians") +
coord_equal() +
labs(title = "Varians Prediksi Ordinary Kriging") +
theme_minimal()
Kriging dievaluasi dengan fold yang sama seperti metode deterministik.
cv_res$ok <- NA_real_
for (i in 1:K) {
tr <- meuse_sf[folds != i, ]
te <- meuse_sf[folds == i, ]
vf <- fit_vgm(tr)
cv_res$ok[folds == i] <- krige(log_zinc ~ 1, tr, te, model = vf, debug.level = 0)$var1.pred
}
Tabel perbandingan RMSE seluruh metode (diurutkan dari RMSE terkecil):
hasil_all <- rbind(
`Closest Observation` = ringkas(cv_res$obs, cv_res$closest),
`Inverse Distance Weight` = ringkas(cv_res$obs, cv_res$idw),
`Nearest Neighbors` = ringkas(cv_res$obs, cv_res$nn),
`Ensembel (rata-rata)` = ringkas(cv_res$obs, cv_res$ens_mean),
`Ensembel (terboboti)` = ringkas(cv_res$obs, cv_res$ens_wt),
`Kriging (Ordinary)` = ringkas(cv_res$obs, cv_res$ok)
) |>
as.data.frame() |>
tibble::rownames_to_column("Metode") |>
arrange(RMSE)
hasil_all[, -1] <- round(hasil_all[, -1], 4)
kable(hasil_all, caption = "Perbandingan Kinerja Seluruh Metode Interpolasi (10-fold CV, skala log(zinc))")
| Metode | RMSE | MAE | R2_CV |
|---|---|---|---|
| Kriging (Ordinary) | 0.4046 | 0.3095 | 0.6851 |
| Nearest Neighbors | 0.4443 | 0.3398 | 0.6207 |
| Ensembel (terboboti) | 0.4509 | 0.3404 | 0.6074 |
| Ensembel (rata-rata) | 0.4546 | 0.3415 | 0.6010 |
| Inverse Distance Weight | 0.4610 | 0.3549 | 0.5919 |
| Closest Observation | 0.5563 | 0.3890 | 0.4831 |
ggplot(hasil_all, aes(x = reorder(Metode, -RMSE), y = RMSE, fill = Metode)) +
geom_col(show.legend = FALSE) +
geom_text(aes(label = round(RMSE, 3)), hjust = -0.1) +
coord_flip() +
expand_limits(y = max(hasil_all$RMSE) * 1.12) +
labs(title = "RMSE Cross Validation per Metode", x = NULL, y = "RMSE") +
theme_minimal()
ggplot(data.frame(obs = cv_res$obs, pred = cv_res$ok), aes(obs, pred)) +
geom_point(alpha = 0.6, color = "steelblue") +
geom_abline(slope = 1, intercept = 0, color = "red", linetype = "dashed") +
labs(title = "Observasi vs Prediksi - Ordinary Kriging",
x = "Observasi log(zinc)", y = "Prediksi log(zinc)") +
theme_minimal()
meuse
(peubah log(zinc)), metode dengan RMSE terkecil adalah Kriging
(Ordinary) (RMSE = 0.4046), sedangkan metode dengan RMSE
terbesar adalah Closest Observation (RMSE = 0.5563).