meuseInterpolasi titik adalah proses menduga nilai suatu variabel pada
lokasi yang tidak diamati berdasarkan nilai pada lokasi-lokasi yang
diamati. Pada praktikum ini digunakan data meuse (kandungan
logam berat pada tanah di sekitar Sungai Meuse, Belanda) dengan variabel
yang diinterpolasi adalah kadar seng (zinc,
ppm).
Metode interpolasi yang dibandingkan:
nmax tetangga terdekat.Evaluasi dilakukan dengan k-fold Cross Validation dan ukuran akurasi RMSE (Root Mean Square Error).
library(sp)
library(sf)
library(gstat)
library(ggplot2)
library(knitr)
meusedata(meuse)
data(meuse.grid)
head(meuse)
str(meuse[, c("x", "y", "zinc")])
## 'data.frame': 155 obs. of 3 variables:
## $ x : num 181072 181025 181165 181298 181307 ...
## $ y : num 333611 333558 333537 333484 333330 ...
## $ zinc: num 1022 1141 640 257 269 ...
summary(meuse$zinc)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 113.0 198.0 326.0 469.7 674.5 1839.0
meuse_sf <- st_as_sf(meuse, coords = c("x", "y"), crs = 28992,
remove = FALSE, agr = "constant")
grid_sf <- st_as_sf(meuse.grid, coords = c("x", "y"), crs = 28992,
remove = FALSE, agr = "constant")
nrow(meuse_sf)
## [1] 155
nrow(grid_sf)
## [1] 3103
zincggplot() +
geom_tile(data = meuse.grid, aes(x, y), fill = "grey90") +
geom_point(data = meuse, aes(x, y, color = zinc, size = zinc)) +
scale_color_viridis_c(option = "C") +
coord_equal() +
labs(title = "Sebaran titik pengamatan `zinc` (meuse)",
x = "X (m)", y = "Y (m)", color = "zinc (ppm)", size = "zinc (ppm)") +
theme_minimal()
ggplot(meuse, aes(zinc)) +
geom_histogram(bins = 25, fill = "steelblue", color = "white") +
labs(title = "Distribusi zinc", x = "zinc (ppm)", y = "Frekuensi") +
theme_minimal()
Setiap fungsi menerima data train dan data test
(objek sf), lalu mengembalikan vektor prediksi
zinc di lokasi test. Dengan format yang seragam
ini, semua metode dapat dipakai baik untuk cross
validation maupun untuk pemetaan grid.
Setiap lokasi diberi nilai dari titik pengamatan yang jaraknya paling dekat (sama dengan Voronoi/Thiessen polygon).
pred_closest <- function(train, test) {
idx <- st_nearest_feature(test, train) # indeks titik train terdekat
train$zinc[idx]
}
Rata-rata dari nmax tetangga terdekat (tanpa pembobotan
jarak, idp = 0).
pred_nn <- function(train, test, nmax = 5) {
g <- gstat(formula = zinc ~ 1, data = train,
nmax = nmax, set = list(idp = 0))
predict(g, newdata = test, debug.level = 0)$var1.pred
}
Bobot berbanding terbalik dengan jarak pangkat idp.
pred_idw <- function(train, test, idp = 2, nmax = Inf) {
idw(zinc ~ 1, locations = train, newdata = test,
idp = idp, nmax = nmax, debug.level = 0)$var1.pred
}
Langkah: (1) hitung variogram empiris, (2) fit model variogram, (3) lakukan kriging.
v_emp <- variogram(zinc ~ 1, meuse_sf)
init_model <- vgm(psill = max(v_emp$gamma) * 0.8,
model = "Sph",
range = max(v_emp$dist) / 3,
nugget = min(v_emp$gamma))
v_fit <- fit.variogram(v_emp, init_model)
v_fit
plot(v_emp, v_fit, main = "Variogram empiris dan model Spherical (zinc)")
Model variogram (nugget, sill, range) selanjutnya dipakai untuk Ordinary Kriging.
Pada saat cross validation, variogram difit ulang pada data train di setiap fold agar tidak terjadi kebocoran informasi dari data test.
fit_vgm <- function(data) {
v <- variogram(zinc ~ 1, data)
init <- vgm(psill = max(v$gamma) * 0.8,
model = "Sph",
range = max(v$dist) / 3,
nugget = min(v$gamma))
m <- tryCatch(suppressWarnings(fit.variogram(v, init)),
error = function(e) init)
if (isTRUE(attr(m, "singular"))) m <- init # jaga-jaga jika fit gagal
m
}
pred_ok <- function(train, test) {
m <- fit_vgm(train)
krige(zinc ~ 1, locations = train, newdata = test,
model = m, debug.level = 0)$var1.pred
}
Digunakan 5-fold cross validation: data dibagi acak menjadi 5 bagian; setiap bagian bergantian menjadi data test (diprediksi) sedangkan 4 bagian lainnya menjadi data train.
set.seed(123)
k <- 5
folds <- sample(rep(1:k, length.out = nrow(meuse_sf)))
table(folds)
## folds
## 1 2 3 4 5
## 31 31 31 31 31
nmax pada NN dan idp pada
IDWrmse <- function(obs, pred) sqrt(mean((obs - pred)^2))
cv_generic <- function(fun, ...) {
pred <- numeric(nrow(meuse_sf))
for (i in 1:k) {
tr <- meuse_sf[folds != i, ]
te <- meuse_sf[folds == i, ]
pred[folds == i] <- fun(tr, te, ...)
}
rmse(meuse_sf$zinc, pred)
}
tune_nn <- data.frame(nmax = 2:10)
tune_nn$RMSE <- sapply(tune_nn$nmax, function(n) cv_generic(pred_nn, nmax = n))
tune_idw <- data.frame(idp = seq(0.5, 4, by = 0.5))
tune_idw$RMSE <- sapply(tune_idw$idp, function(p) cv_generic(pred_idw, idp = p))
kable(tune_nn, digits = 2, caption = "RMSE NN untuk berbagai nmax")
| nmax | RMSE |
|---|---|
| 2 | 278.01 |
| 3 | 261.20 |
| 4 | 266.29 |
| 5 | 267.83 |
| 6 | 266.07 |
| 7 | 269.53 |
| 8 | 268.30 |
| 9 | 274.09 |
| 10 | 277.89 |
kable(tune_idw, digits = 2, caption = "RMSE IDW untuk berbagai idp")
| idp | RMSE |
|---|---|
| 0.5 | 354.79 |
| 1.0 | 330.75 |
| 1.5 | 301.39 |
| 2.0 | 278.16 |
| 2.5 | 267.13 |
| 3.0 | 266.29 |
| 3.5 | 270.63 |
| 4.0 | 276.76 |
nmax_opt <- tune_nn$nmax[which.min(tune_nn$RMSE)]
idp_opt <- tune_idw$idp[which.min(tune_idw$RMSE)]
c(nmax_opt = nmax_opt, idp_opt = idp_opt)
## nmax_opt idp_opt
## 3 3
Parameter terbaik (nmax_opt=3 dan
idp_opt=3.0) dipakai pada perbandingan akhir.
Catatan: parameter dipilih dari data yang sama dengan evaluasi sehingga RMSE NN dan IDW sedikit bersifat optimistik. Untuk evaluasi yang lebih ketat dapat digunakan nested cross validation.
cv_pred <- data.frame(obs = meuse_sf$zinc,
fold = folds,
closest = NA_real_, nn = NA_real_,
idw = NA_real_, kriging = NA_real_)
for (i in 1:k) {
tr <- meuse_sf[folds != i, ]
te <- meuse_sf[folds == i, ]
sel <- folds == i
cv_pred$closest[sel] <- pred_closest(tr, te)
cv_pred$nn[sel] <- pred_nn(tr, te, nmax = nmax_opt)
cv_pred$idw[sel] <- pred_idw(tr, te, idp = idp_opt)
cv_pred$kriging[sel] <- pred_ok(tr, te)
}
head(cv_pred)
Ensemble menggabungkan prediksi beberapa metode. Di sini digunakan 3 metode: NN, IDW, dan Kriging, dengan dua skema:
base_methods <- c("nn", "idw", "kriging")
# RMSE tiap metode dasar
rmse_base <- sapply(base_methods, function(m) rmse(cv_pred$obs, cv_pred[[m]]))
# Bobot = 1/RMSE, dinormalkan agar jumlahnya 1
w <- (1 / rmse_base) / sum(1 / rmse_base)
w
## nn idw kriging
## 0.3225264 0.3163590 0.3611145
cv_pred$ens_mean <- rowMeans(cv_pred[, base_methods])
cv_pred$ens_wt <- as.vector(as.matrix(cv_pred[, base_methods]) %*% w)
Catatan: bobot dihitung dari RMSE cross validation yang sama, sehingga RMSE
ens_wtsedikit optimistik dibandingens_mean.
metode <- c(closest = "Closest Observation",
nn = "Nearest Neighbour",
idw = "IDW",
kriging = "Ordinary Kriging",
ens_mean = "Ensemble (rata-rata NN+IDW+Kriging)",
ens_wt = "Ensemble (berbobot 1/RMSE)")
hasil <- data.frame(
Metode = unname(metode),
RMSE = sapply(names(metode), function(m) rmse(cv_pred$obs, cv_pred[[m]])),
MAE = sapply(names(metode), function(m) mean(abs(cv_pred$obs - cv_pred[[m]])))
)
hasil <- hasil[order(hasil$RMSE), ]
hasil$Peringkat <- seq_len(nrow(hasil))
rownames(hasil) <- NULL
kable(hasil, digits = 2, caption = "Perbandingan RMSE (5-fold CV) seluruh metode interpolasi")
| Metode | RMSE | MAE | Peringkat |
|---|---|---|---|
| Ordinary Kriging | 233.29 | 161.81 | 1 |
| Ensemble (berbobot 1/RMSE) | 245.53 | 170.81 | 2 |
| Ensemble (rata-rata NN+IDW+Kriging) | 246.30 | 171.32 | 3 |
| Nearest Neighbour | 261.20 | 177.29 | 4 |
| IDW | 266.29 | 186.02 | 5 |
| Closest Observation | 351.95 | 220.69 | 6 |
ggplot(hasil, aes(x = reorder(Metode, -RMSE), y = RMSE, fill = RMSE)) +
geom_col(show.legend = FALSE) +
geom_text(aes(label = round(RMSE, 1)), hjust = -0.1, size = 3.5) +
coord_flip() +
scale_fill_viridis_c(option = "D", direction = -1) +
expand_limits(y = max(hasil$RMSE) * 1.1) +
labs(title = "RMSE hasil 5-fold Cross Validation",
x = NULL, y = "RMSE (ppm)") +
theme_minimal()
library(tidyr)
long <- pivot_longer(cv_pred, cols = names(metode),
names_to = "metode", values_to = "pred")
long$metode <- factor(long$metode, levels = names(metode), labels = metode)
ggplot(long, aes(obs, pred)) +
geom_point(alpha = 0.6, color = "steelblue") +
geom_abline(slope = 1, intercept = 0, color = "red") +
facet_wrap(~ metode) +
labs(title = "Nilai observasi vs prediksi (Cross Validation)",
x = "Observasi zinc (ppm)", y = "Prediksi zinc (ppm)") +
theme_minimal()
Setelah dievaluasi, setiap metode diterapkan pada seluruh
data untuk memprediksi zinc di seluruh
meuse.grid.
pred_grid <- data.frame(
closest = pred_closest(meuse_sf, grid_sf),
nn = pred_nn(meuse_sf, grid_sf, nmax = nmax_opt),
idw = pred_idw(meuse_sf, grid_sf, idp = idp_opt),
kriging = krige(zinc ~ 1, meuse_sf, grid_sf, model = v_fit,
debug.level = 0)$var1.pred
)
pred_grid$ens_mean <- rowMeans(pred_grid[, base_methods])
pred_grid$ens_wt <- as.vector(as.matrix(pred_grid[, base_methods]) %*% w)
grid_long <- cbind(meuse.grid[, c("x", "y")], pred_grid)
grid_long <- pivot_longer(grid_long, cols = names(metode),
names_to = "metode", values_to = "zinc")
grid_long$metode <- factor(grid_long$metode, levels = names(metode), labels = metode)
ggplot(grid_long, aes(x, y, fill = zinc)) +
geom_raster() +
geom_point(data = meuse, aes(x, y), inherit.aes = FALSE,
size = 0.4, color = "black", alpha = 0.5) +
facet_wrap(~ metode, ncol = 3) +
coord_equal() +
scale_fill_viridis_c(option = "C") +
labs(title = "Peta interpolasi zinc (meuse.grid)",
x = "X (m)", y = "Y (m)", fill = "zinc (ppm)") +
theme_minimal() +
theme(axis.text = element_blank(), axis.ticks = element_blank())
Kelebihan Kriging dibanding metode lain adalah tersedianya ukuran ketidakpastian (varians prediksi).
ok_full <- krige(zinc ~ 1, meuse_sf, grid_sf, model = v_fit, debug.level = 0)
ggplot(data.frame(meuse.grid[, c("x", "y")], var = ok_full$var1.var),
aes(x, y, fill = var)) +
geom_raster() +
geom_point(data = meuse, aes(x, y), inherit.aes = FALSE, size = 0.5) +
coord_equal() +
scale_fill_viridis_c(option = "B") +
labs(title = "Varians prediksi Ordinary Kriging",
x = "X (m)", y = "Y (m)", fill = "Varians") +
theme_minimal()
Berdasarkan 5-fold cross validation, metode dengan RMSE terkecil
adalah Ordinary Kriging (RMSE =
233.29 ppm), sedangkan metode dengan RMSE terbesar adalah
Closest Observation (RMSE =
351.95 ppm).