Putri Dwi Suci Sri Mulyaningsih Sitanggang (M0401241033)
# install.packages(c("sf", "sp", "gstat", "ggplot2", "knitr"))
library(sf)
library(sp)
library(gstat)
library(ggplot2)
Variabel yang diinterpolasi: kandungan tembaga (copper)
pada data meuse, ditransformasi log.
data(meuse); data(meuse.grid); data(meuse.riv)
meuse$log_cu <- log(meuse$copper)
meuse_sf <- st_as_sf(meuse, coords = c("x", "y"), crs = 28992, remove = FALSE)
grid_sf <- st_as_sf(meuse.grid, coords = c("x", "y"), crs = 28992, remove = FALSE)
sungai <- setNames(as.data.frame(meuse.riv), c("x", "y"))
n_obs <- nrow(meuse_sf)
summary(meuse$copper)
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 14.00 23.00 31.00 40.32 49.50 128.00
batas_x <- range(meuse.grid$x) + c(-150, 150)
batas_y <- range(meuse.grid$y) + c(-150, 150)
ggplot() +
geom_polygon(data = sungai, aes(x, y), fill = "#bcd8f2", colour = "#4a90c2", alpha = 0.7) +
geom_point(data = meuse, aes(x, y, colour = copper, size = copper), alpha = 0.85) +
scale_colour_viridis_c(option = "C", name = "Cu (ppm)") +
scale_size_continuous(range = c(1, 5), guide = "none") +
coord_equal(xlim = batas_x, ylim = batas_y) +
labs(title = "Lokasi 155 titik sampel dan kandungan tembaga", x = "x (m)", y = "y (m)") +
theme_minimal(base_size = 12)
Kandungan Cu menjulur ke kanan (kemencengan 1.39, turun menjadi 0.65 setelah di-log), sehingga seluruh analisis memakai log(Cu). Titik bernilai tinggi (kuning-oranye) berderet di sepanjang tepi sungai, sedangkan titik bernilai rendah (ungu) berada di bagian dalam dataran. Titik yang berdekatan bernilai mirip, jadi ada autokorelasi spasial dan interpolasi layak dilakukan.
Lokasi prediksi adalah 3.103 sel grid meuse.grid.
ggplot() +
geom_polygon(data = sungai, aes(x, y), fill = "#bcd8f2", colour = "#4a90c2", alpha = 0.7) +
geom_tile(data = meuse.grid, aes(x, y), fill = "grey80") +
geom_point(data = meuse, aes(x, y), size = 0.8) +
coord_equal(xlim = batas_x, ylim = batas_y) +
labs(title = "Grid prediksi dan titik sampel", x = "x (m)", y = "y (m)") +
theme_minimal(base_size = 12)
RMSE <- function(obs, pred) sqrt(mean((obs - pred)^2, na.rm = TRUE))
batas_warna <- range(meuse$copper)
# Peta prediksi pada grid (input skala log, ditampilkan dalam ppm)
peta_grid <- function(pred_log, judul, opsi = "C", legenda = "Cu (ppm)", ke_ppm = TRUE, batas = batas_warna) {
df <- data.frame(x = grid_sf$x, y = grid_sf$y, nilai = if (ke_ppm) exp(pred_log) else pred_log)
p <- ggplot(df, aes(x, y, fill = nilai)) +
geom_raster() +
geom_polygon(data = sungai, aes(x, y), inherit.aes = FALSE, fill = NA, colour = "#4a90c2") +
geom_point(data = meuse, aes(x, y), inherit.aes = FALSE, size = 0.5, colour = "grey15") +
coord_equal(xlim = batas_x, ylim = batas_y) +
labs(title = judul, x = "x (m)", y = "y (m)") +
theme_minimal(base_size = 12)
if (is.null(batas)) p + scale_fill_viridis_c(option = opsi, name = legenda)
else p + scale_fill_viridis_c(option = opsi, name = legenda, limits = batas, oob = scales::squish)
}
vor <- st_collection_extract(st_voronoi(st_union(st_geometry(meuse_sf))), "POLYGON")
vor_sf <- st_sf(geometry = vor, crs = st_crs(meuse_sf))
idx <- st_intersects(vor_sf, meuse_sf)
vor_sf$cu <- meuse_sf$copper[sapply(idx, function(i) i[1])]
batas_studi <- st_convex_hull(st_union(st_geometry(grid_sf)))
st_agr(vor_sf) <- "constant"
vor_potong <- suppressWarnings(st_intersection(vor_sf, batas_studi))
ggplot() +
geom_sf(data = vor_potong, aes(fill = cu), colour = "white", linewidth = 0.2) +
geom_sf(data = meuse_sf, size = 0.6, colour = "grey15") +
scale_fill_viridis_c(option = "C", name = "Cu (ppm)") +
coord_sf(datum = st_crs(28992)) +
labs(title = "Diagram Voronoi", x = "x (m)", y = "y (m)") +
theme_minimal(base_size = 12)
# Nilai titik sampel terdekat (ekuivalen dengan poligon Voronoi)
p_closest <- function(train, test) train$log_cu[st_nearest_feature(test, train)]
pred_closest <- p_closest(meuse_sf, grid_sf)
peta_grid(pred_closest, "Closest Observation (Voronoi)")
Peta berblok dengan batas yang tegas dan hanya berisi 60 nilai berbeda, persis nilai asli titik sampel tanpa penghalusan. Satu titik bernilai ekstrem menguasai seluruh poligonnya, sehingga permukaannya diskontinu.
Nilai \(k\) dipilih dengan Leave-One-Out CV.
p_nn <- function(train, test, k) {
idw(log_cu ~ 1, locations = train, newdata = test, idp = 0, nmax = k, debug.level = 0)$var1.pred
}
loo_rmse <- function(f) {
pred <- sapply(seq_len(n_obs), function(i) f(meuse_sf[-i, ], meuse_sf[i, ]))
RMSE(meuse_sf$log_cu, pred)
}
kandidat_k <- c(1, 2, 3, 4, 5, 7, 10, 15, 20)
rmse_k <- sapply(kandidat_k, function(k) loo_rmse(function(tr, te) p_nn(tr, te, k)))
k_opt <- kandidat_k[which.min(rmse_k)]
knitr::kable(data.frame(k = kandidat_k, RMSE_LOOCV = rmse_k), digits = 4)
| k | RMSE_LOOCV |
|---|---|
| 1 | 0.4441 |
| 2 | 0.3949 |
| 3 | 0.3387 |
| 4 | 0.3611 |
| 5 | 0.3714 |
| 7 | 0.3750 |
| 10 | 0.3838 |
| 15 | 0.4074 |
| 20 | 0.4238 |
pred_nn <- p_nn(meuse_sf, grid_sf, k_opt)
peta_grid(pred_nn, paste0("Nearest Neighbors (k = ", k_opt, ")"))
\(k\) = 3 memberi RMSE LOOCV terkecil (0.3387), lebih baik dibanding \(k\) = 1 (0.4441) dan memburuk lagi saat \(k\) membesar karena tetangga yang jauh ikut dirata-ratakan. Peta lebih halus daripada Closest Observation; rentang prediksi menyempit menjadi 15.6 sampai 110.5 ppm (data asli 14 sampai 128 ppm) karena nilai ekstrem diredam oleh rata-rata.
Nilai power \(p\) dipilih dengan Leave-One-Out CV.
p_idw <- function(train, test, idp) {
idw(log_cu ~ 1, locations = train, newdata = test, idp = idp, debug.level = 0)$var1.pred
}
kandidat_p <- c(0.5, 1, 1.5, 2, 3, 4, 5)
rmse_p <- sapply(kandidat_p, function(p) loo_rmse(function(tr, te) p_idw(tr, te, p)))
p_opt <- kandidat_p[which.min(rmse_p)]
knitr::kable(data.frame(p = kandidat_p, RMSE_LOOCV = rmse_p), digits = 4)
| p | RMSE_LOOCV |
|---|---|
| 0.5 | 0.4912 |
| 1.0 | 0.4597 |
| 1.5 | 0.4218 |
| 2.0 | 0.3905 |
| 3.0 | 0.3665 |
| 4.0 | 0.3720 |
| 5.0 | 0.3830 |
pred_idw <- p_idw(meuse_sf, grid_sf, p_opt)
peta_grid(pred_idw, paste0("IDW (p = ", p_opt, ")"))
\(p\) = 3 memberi RMSE LOOCV terkecil (0.3665); pada \(p\) kecil kesalahan jauh lebih besar (0.4912 pada \(p\) = 0.5) karena titik jauh terlalu berpengaruh. Peta mulus dan kontinu, tetapi muncul pola lingkaran kecil di sekitar titik sampel bernilai tinggi, dan IDW tidak memberi ukuran ketidakpastian prediksi.
v_cloud <- variogram(log_cu ~ 1, meuse_sf, cloud = TRUE)
ggplot(v_cloud, aes(dist, gamma)) +
geom_point(alpha = 0.12, size = 0.8, colour = "#7570b3") +
labs(title = "Variogram cloud log(Cu)", x = "Jarak (m)", y = "Semivariansi") +
theme_minimal(base_size = 12)
v_emp <- variogram(log_cu ~ 1, meuse_sf)
ggplot(v_emp, aes(dist, gamma)) +
geom_point(aes(size = np), colour = "#1b9e77") +
scale_size_continuous(name = "Jumlah pasangan", range = c(2, 6)) +
expand_limits(y = 0) +
labs(title = "Variogram empiris log(Cu)", x = "Jarak (m)", y = "Semivariansi") +
theme_minimal(base_size = 12)
Pada variogram cloud, pasangan titik berjarak dekat memiliki semivariansi yang lebih rendah dan sebarannya melebar pada jarak jauh. Variogram empiris naik dari 0.095 pada jarak terdekat lalu mendatar di kisaran 0.26 sampai 0.31. Pola ini menunjukkan adanya ketergantungan spasial: makin jauh dua titik, makin berbeda nilainya.
Tiga model dicocokkan (Spherical, Exponential, Gaussian), dipilih yang SSErr-nya terkecil.
nilai_awal <- function(m, v) {
vgm(psill = max(v$gamma) * 0.8, model = m, range = max(v$dist) / 3, nugget = min(v$gamma))
}
kandidat_model <- c("Sph", "Exp", "Gau")
fit_model <- setNames(lapply(kandidat_model, function(m) fit.variogram(v_emp, nilai_awal(m, v_emp))),
kandidat_model)
tabel_fit <- do.call(rbind, lapply(kandidat_model, function(m) {
f <- fit_model[[m]]
data.frame(Model = m, Nugget = f$psill[1], Partial_sill = f$psill[2],
Sill_total = sum(f$psill), Range_m = f$range[2], SSErr = attr(f, "SSErr"))
}))
knitr::kable(tabel_fit, digits = 4)
| Model | Nugget | Partial_sill | Sill_total | Range_m | SSErr |
|---|---|---|---|---|---|
| Sph | 0.0610 | 0.2275 | 0.2885 | 775.0711 | 0 |
| Exp | 0.0373 | 0.2748 | 0.3122 | 366.5063 | 0 |
| Gau | 0.0891 | 0.1933 | 0.2825 | 348.9513 | 0 |
model_terpilih <- tabel_fit$Model[which.min(tabel_fit$SSErr)]
fit_terpilih <- fit_model[[model_terpilih]]
par_t <- tabel_fit[tabel_fit$Model == model_terpilih, ]
garis_model <- do.call(rbind, lapply(kandidat_model, function(m) {
g <- variogramLine(fit_model[[m]], maxdist = max(v_emp$dist)); g$Model <- m; g
}))
ggplot() +
geom_point(data = v_emp, aes(dist, gamma), size = 3, colour = "grey25") +
geom_line(data = garis_model, aes(dist, gamma, colour = Model), linewidth = 1) +
scale_colour_brewer(palette = "Dark2") +
expand_limits(y = 0) +
labs(title = "Variogram empiris dan model terfit", x = "Jarak (m)", y = "Semivariansi") +
theme_minimal(base_size = 12)
Model terpilih adalah Sph (SSErr terkecil = 2.27^{-6}) dengan nugget 0.061, partial sill 0.227 (sill total 0.288), dan range 775 m. Nugget hanya 21% dari sill total, artinya 79% variasi terstruktur secara spasial, dan titik yang terpisah lebih dari 775 m praktis tidak lagi berkorelasi.
# Variogram dicocokkan ulang pada data latih yang diberikan (dipakai juga saat CV)
p_krig <- function(train, test, model = model_terpilih) {
v <- variogram(log_cu ~ 1, train)
fit <- suppressWarnings(try(fit.variogram(v, nilai_awal(model, v)), silent = TRUE))
if (inherits(fit, "try-error") || isTRUE(attr(fit, "singular"))) {
fit <- vgm(psill = var(train$log_cu) * 0.7, model = model,
range = max(v$dist) / 3, nugget = var(train$log_cu) * 0.3)
}
krige(log_cu ~ 1, locations = train, newdata = test, model = fit, debug.level = 0)$var1.pred
}
krig_grid <- krige(log_cu ~ 1, locations = meuse_sf, newdata = grid_sf,
model = fit_terpilih, debug.level = 0)
pred_krig <- krig_grid$var1.pred
peta_grid(pred_krig, "Ordinary Kriging")
peta_grid(krig_grid$var1.var, "Variansi Ordinary Kriging (skala log)",
opsi = "A", legenda = "Variansi", ke_ppm = FALSE, batas = NULL)
Permukaan kriging paling halus dan natural: tidak ada blok seperti Closest Observation maupun lingkaran kecil seperti IDW. Cu tinggi terkonsentrasi di dekat sungai dan rendah di bagian dalam dataran. Variansi prediksi berkisar 0.086 sampai 0.263; rendah di sekitar titik sampel dan tinggi di tepi area yang jarang sampel (korelasi dengan jarak ke titik sampel terdekat = 0.92), sehingga prediksi di area padat sampel lebih dapat dipercaya.
Dua versi ensemble:
semua_metode <- function(train, test) {
cbind(Closest = p_closest(train, test),
NN = p_nn(train, test, k_opt),
IDW = p_idw(train, test, p_opt),
Kriging = p_krig(train, test))
}
buat_fold <- function(n, K) sample(rep(1:K, length.out = n))
hitung_bobot <- function(train, K_dalam = 5) {
fold_dalam <- buat_fold(nrow(train), K_dalam)
P <- matrix(NA, nrow(train), 4)
for (j in 1:K_dalam) {
P[fold_dalam == j, ] <- semua_metode(train[fold_dalam != j, ], train[fold_dalam == j, ])
}
w <- 1 / apply(P, 2, function(p) mean((train$log_cu - p)^2))
w / sum(w)
}
set.seed(2025)
bobot_penuh <- hitung_bobot(meuse_sf)
names(bobot_penuh) <- c("Closest", "NN", "IDW", "Kriging")
knitr::kable(data.frame(Metode = names(bobot_penuh), Bobot = bobot_penuh),
digits = 3, row.names = FALSE)
| Metode | Bobot |
|---|---|
| Closest | 0.188 |
| NN | 0.251 |
| IDW | 0.253 |
| Kriging | 0.308 |
P_grid <- cbind(Closest = pred_closest, NN = pred_nn, IDW = pred_idw, Kriging = pred_krig)
pred_ens_eq <- rowMeans(P_grid[, c("NN", "IDW", "Kriging")])
pred_ens_w <- as.numeric(P_grid %*% bobot_penuh)
peta_grid(pred_ens_eq, "Ensemble (Equal): NN + IDW + Kriging")
peta_grid(pred_ens_w, "Ensemble (Weighted): Closest + NN + IDW + Kriging")
Kedua peta ensemble halus dan stabil, mewarisi karakter IDW, NN, dan Kriging. Bobot terbesar jatuh pada Kriging (0.308) dan terkecil pada Closest (0.188), artinya metode yang lebih akurat di validasi internal diberi kontribusi lebih besar. Selisih bobotnya tidak begitu ekstrem, sehingga kedua versi ensemble masih menghasilkan peta yang mirip.
nama_metode <- c("Closest Observation", paste0("Nearest Neighbors (k=", k_opt, ")"),
paste0("IDW (p=", p_opt, ")"), "Ordinary Kriging",
"Ensemble (Equal)", "Ensemble (Weighted)")
daftar_pred <- list(pred_closest, pred_nn, pred_idw, pred_krig, pred_ens_eq, pred_ens_w)
semua_peta <- do.call(rbind, lapply(seq_along(nama_metode), function(i) {
data.frame(x = grid_sf$x, y = grid_sf$y, cu = exp(daftar_pred[[i]]),
Metode = factor(nama_metode[i], levels = nama_metode))
}))
ggplot(semua_peta, aes(x, y, fill = cu)) +
geom_raster() +
facet_wrap(~Metode, ncol = 2) +
scale_fill_viridis_c(option = "C", name = "Cu (ppm)", limits = batas_warna, oob = scales::squish) +
coord_equal() +
labs(title = "Perbandingan peta prediksi Cu", x = NULL, y = NULL) +
theme_minimal(base_size = 11) +
theme(axis.text = element_blank(), panel.grid = element_blank())
Semua metode menangkap pola besar yang sama (Cu tinggi di tepi sungai, rendah di bagian dalam), tetapi berbeda tekstur: Closest Observation berblok, NN berbercak halus, IDW mulus dengan lingkaran kecil di sekitar titik, sedangkan Kriging dan kedua Ensemble paling mulus.
K-fold Cross Validation dengan \(K\) = 5. Semua metode dievaluasi pada fold yang sama yaitu variogram dan bobot ensemble dihitung ulang hanya dari data latih di setiap fold.
K <- 5
set.seed(2025)
fold_id <- buat_fold(n_obs, K)
nama_cv <- c("Closest", "NN", "IDW", "Kriging", "Ens_Equal", "Ens_Weighted")
rmse_log <- matrix(NA, K, 6, dimnames = list(paste("Fold", 1:K), nama_cv))
rmse_ppm <- rmse_log
bobot_fold <- matrix(NA, K, 4, dimnames = list(paste("Fold", 1:K), c("Closest", "NN", "IDW", "Kriging")))
for (i in 1:K) {
latih <- meuse_sf[fold_id != i, ]
uji <- meuse_sf[fold_id == i, ]
P <- semua_metode(latih, uji)
w <- hitung_bobot(latih)
bobot_fold[i, ] <- w
P_cv <- cbind(P, Ens_Equal = rowMeans(P[, c("NN", "IDW", "Kriging")]),
Ens_Weighted = as.numeric(P %*% w))
for (m in nama_cv) {
rmse_log[i, m] <- RMSE(uji$log_cu, P_cv[, m])
rmse_ppm[i, m] <- RMSE(uji$copper, exp(P_cv[, m]))
}
}
knitr::kable(data.frame(Fold = 1:K, rmse_log), digits = 4, row.names = FALSE,
caption = "RMSE tiap fold (skala log)")
| Fold | Closest | NN | IDW | Kriging | Ens_Equal | Ens_Weighted |
|---|---|---|---|---|---|---|
| 1 | 0.4154 | 0.3481 | 0.3492 | 0.3056 | 0.3204 | 0.3226 |
| 2 | 0.3428 | 0.3909 | 0.3588 | 0.3440 | 0.3585 | 0.3423 |
| 3 | 0.4493 | 0.3215 | 0.3685 | 0.3213 | 0.3256 | 0.3413 |
| 4 | 0.4458 | 0.3245 | 0.3270 | 0.3230 | 0.3158 | 0.3231 |
| 5 | 0.5125 | 0.4838 | 0.4669 | 0.4041 | 0.4439 | 0.4407 |
label <- c(Closest = "Closest Observation", NN = paste0("Nearest Neighbors (k=", k_opt, ")"),
IDW = paste0("IDW (p=", p_opt, ")"), Kriging = "Ordinary Kriging",
Ens_Equal = "Ensemble (Equal)", Ens_Weighted = "Ensemble (Weighted)")
rata_log <- colMeans(rmse_log)
sd_log <- apply(rmse_log, 2, sd)
rata_ppm <- colMeans(rmse_ppm)
menang <- table(factor(nama_cv[apply(rmse_log, 1, which.min)], levels = nama_cv))
ringkasan <- data.frame(Metode = label[nama_cv], RMSE_log = as.numeric(rata_log),
SD_antar_fold = as.numeric(sd_log), RMSE_ppm = as.numeric(rata_ppm),
Fold_terbaik = as.integer(menang))
ringkasan <- ringkasan[order(ringkasan$RMSE_log), ]
ringkasan <- cbind(Peringkat = seq_len(nrow(ringkasan)), ringkasan)
rownames(ringkasan) <- NULL
knitr::kable(ringkasan, digits = 4,
col.names = c("Peringkat", "Metode", "RMSE rata-rata (log)", "SD antar fold",
"RMSE rata-rata (ppm)", "Jumlah fold terbaik"),
caption = "Perbandingan RMSE rata-rata 5-fold Cross Validation")
| Peringkat | Metode | RMSE rata-rata (log) | SD antar fold | RMSE rata-rata (ppm) | Jumlah fold terbaik |
|---|---|---|---|---|---|
| 1 | Ordinary Kriging | 0.3396 | 0.0385 | 16.6472 | 3 |
| 2 | Ensemble (Equal) | 0.3528 | 0.0536 | 17.4384 | 1 |
| 3 | Ensemble (Weighted) | 0.3540 | 0.0494 | 17.3603 | 1 |
| 4 | Nearest Neighbors (k=3) | 0.3737 | 0.0675 | 18.2379 | 0 |
| 5 | IDW (p=3) | 0.3741 | 0.0541 | 18.4320 | 0 |
| 6 | Closest Observation | 0.4332 | 0.0616 | 19.9574 | 0 |
cv_panjang <- data.frame(
Fold = rep(1:K, times = 6),
Metode = factor(rep(label[nama_cv], each = K), levels = label[names(sort(rata_log))]),
RMSE = as.vector(rmse_log)
)
ggplot(cv_panjang, aes(Metode, RMSE, fill = Metode)) +
geom_boxplot(alpha = 0.6, outlier.shape = NA, show.legend = FALSE) +
geom_jitter(width = 0.12, size = 2, alpha = 0.8, show.legend = FALSE) +
stat_summary(fun = mean, geom = "point", shape = 23, size = 4, fill = "white") +
scale_fill_brewer(palette = "Set2") +
labs(title = "Sebaran RMSE 5-fold Cross Validation",
subtitle = "Belah ketupat putih = rata-rata", x = NULL, y = "RMSE (skala log)") +
theme_minimal(base_size = 12) +
theme(axis.text.x = element_text(angle = 25, hjust = 1))
Interpretasi.