Tugas Regresi Spasial
## Linking to GEOS 3.14.1, GDAL 3.12.1, PROJ 9.7.1; sf_use_s2() is TRUE
## terra 1.9.34
##
## Attaching package: 'dplyr'
## 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
## Loading required package: viridisLite
##
## Attaching package: 'knitr'
## The following object is masked from 'package:terra':
##
## spin
Data
Variabel vble menyimpan nilai yang diinterpolasi, yaitu
log(zinc). Transformasi log dipakai karena sebaran zinc menjulur ke
kanan.
data(meuse)
data(meuse.grid)
d <- st_as_sf(meuse, coords = c("x", "y"), crs = 28992)
d$vble <- log(d$zinc)
grid_sf <- st_as_sf(meuse.grid, coords = c("x", "y"), crs = 28992)
c(n_pengamatan = nrow(d), n_grid_prediksi = nrow(grid_sf))## n_pengamatan n_grid_prediksi
## 155 3103
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 113.0 198.0 326.0 469.7 674.5 1839.0
par(mfrow = c(1, 2))
hist(d$zinc, breaks = 20, col = "grey80", border = "white",
main = "zinc", xlab = "zinc (ppm)")
hist(d$vble, breaks = 20, col = "steelblue", border = "white",
main = "log(zinc)", xlab = "log(zinc)")ggplot() +
geom_sf(data = grid_sf, color = "grey90", size = 0.3) +
geom_sf(data = d, aes(color = vble), size = 2) +
scale_color_viridis(name = "log(zinc)") +
labs(title = "Sebaran titik pengamatan log(zinc)") +
theme_bw()Batas wilayah dan lokasi prediksi
Diagram Voronoi memerlukan batas wilayah. Di sini dipakai convex
hull dari seluruh titik pengamatan dan titik grid. Lokasi prediksi
adalah titik-titik meuse.grid.
batas <- st_sf(geometry = st_convex_hull(
st_union(c(st_geometry(d), st_geometry(grid_sf)))
))
ggplot() +
geom_sf(data = batas, fill = NA, color = "red") +
geom_sf(data = grid_sf, color = "grey70", size = 0.3) +
geom_sf(data = d, size = 0.8) +
labs(title = "Batas wilayah, grid prediksi, dan titik pengamatan") +
theme_bw()Fungsi Interpolasi
Setiap metode dibungkus menjadi fungsi dengan argumen
train (data yang diketahui) dan newdata
(lokasi yang diprediksi). Fungsi yang sama dipakai untuk membuat peta
prediksi pada grid dan untuk cross-validation, sehingga
hasilnya konsisten.
RMSE
Closest Observation
# 1. Closest observation (poligon Voronoi) ---------------------------------
pred_closest <- function(train, newdata) {
v <- st_as_sf(voronoi(x = vect(train), bnd = vect(batas)))
v <- st_sf(pred = v$vble, geometry = st_geometry(v))
lokasi <- st_sf(.id = seq_len(nrow(newdata)), geometry = st_geometry(newdata))
j <- st_join(lokasi, v, join = st_intersects)
j <- j[!duplicated(j$.id), ] # titik tepat di sisi poligon dihitung sekali
j <- j[order(j$.id), ]
p <- j$pred
# cadangan: bila ada titik di luar semua poligon, pakai titik terdekat
if (anyNA(p)) {
na <- which(is.na(p))
p[na] <- train$vble[st_nearest_feature(lokasi[na, ], train)]
}
p
}K-Nearest Neighbors
Inverse Distance Weighting
Ordinary Kriging
# 5. Ordinary kriging: semivariogram di-fit ulang pada data `train` ---------
fit_variogram <- function(train) {
v <- variogram(vble ~ 1, data = train)
fit.variogram(v, model = vgm(psill = 0.5, model = "Sph", range = 900, nugget = 0.1))
}
pred_ok <- function(train, newdata) {
g <- gstat(formula = vble ~ 1, data = train, model = fit_variogram(train))
predict(g, newdata, debug.level = 0)
}Metode Deterministik
Closest Observation
Ruang dibagi menjadi poligon Voronoi. Setiap titik di dalam suatu poligon lebih dekat ke titik pengamatan pemilik poligon itu daripada ke titik lain, sehingga prediksinya sama dengan nilai titik tersebut.
vor <- st_as_sf(voronoi(x = vect(d), bnd = vect(batas)))
ggplot() +
geom_sf(data = vor, aes(fill = vble), color = "white", linewidth = 0.1, alpha = 0.8) +
geom_sf(data = d, size = 0.6) +
scale_fill_viridis(name = "log(zinc)") +
labs(title = "Diagram Voronoi (Closest Observation)") +
theme_bw()Nearest Neighbors
Prediksi adalah rata-rata nilai \(k = 5\) titik terdekat, masing-masing dengan bobot sama.
Pendekatan Ensembel
Ensembel menggabungkan prediksi tiga metode deterministik di atas (Closest Observation, Nearest Neighbors, IDW):
\[\hat{z}_{ens}(s_0) = w_1 \hat{z}_{closest}(s_0) + w_2 \hat{z}_{NN}(s_0) + w_3 \hat{z}_{IDW}(s_0), \qquad \sum_j w_j = 1\]
Dua skema bobot dibandingkan:
- Bobot sama: \(w_j = 1/3\).
- Bobot invers RMSE: metode dengan RMSE lebih kecil mendapat bobot lebih besar, \(w_j \propto 1/RMSE_j\).
# bobot invers RMSE dari inner k-fold CV pada data `train`
hitung_bobot <- function(train, k = 5) {
kf_in <- dismo::kfold(nrow(train), k = k)
P <- matrix(NA_real_, nrow(train), 3,
dimnames = list(NULL, c("Closest", "NN", "IDW")))
for (i in 1:k) {
tr <- train[kf_in != i, ]
te <- train[kf_in == i, ]
P[kf_in == i, ] <- cbind(pred_closest(tr, te), pred_nn(tr, te), pred_idw(tr, te))
}
w <- 1 / apply(P, 2, function(p) RMSE(train$vble, p))
w / sum(w)
}
set.seed(123)
w_full <- hitung_bobot(d)
round(w_full, 4)## Closest NN IDW
## 0.3326 0.3640 0.3034
Metode Stokastik: Ordinary Kriging
Ordinary Kriging memanfaatkan autokorelasi spasial melalui semivariogram, dengan asumsi rata-rata konstan tetapi tidak diketahui.
Semivariogram empiris
Fitting model semivariogram
Model awal spherical dengan psill = 0.5,
range = 900, dan nugget = 0.1 (sama seperti
benchmark), kemudian dioptimasi dengan fit.variogram().
v_init <- vgm(psill = 0.5, model = "Sph", range = 900, nugget = 0.1)
fv <- fit.variogram(v_emp, model = v_init)
fv## model psill range
## 1 Nug 0.05066017 0.0000
## 2 Sph 0.59060556 897.0044
Parameter hasil fitting: nugget = 0.0507, partial sill = 0.5906 (sill total = 0.6413), dan range = 897 meter. Artinya, titik yang berjarak lebih dari sekitar 897 m dianggap tidak lagi berkorelasi secara spasial.
Peta Prediksi Seluruh Metode
xy <- st_coordinates(grid_sf)
peta <- bind_rows(
data.frame(metode = "Closest Observation", pred = p_closest),
data.frame(metode = "Nearest Neighbors", pred = p_nn),
data.frame(metode = "IDW", pred = p_idw),
data.frame(metode = "Ensembel (bobot invers RMSE)", pred = as.numeric(p_ens_w)),
data.frame(metode = "Ordinary Kriging", pred = p_ok)
) %>%
mutate(x = rep(xy[, 1], 5), y = rep(xy[, 2], 5),
metode = factor(metode, levels = unique(metode)))
ggplot(peta, aes(x, y, fill = pred)) +
geom_raster() +
coord_equal() +
facet_wrap(~ metode, ncol = 2, labeller = label_wrap_gen(28)) +
scale_fill_viridis(name = "log(zinc)") +
labs(title = "Prediksi log(zinc) pada grid meuse", x = NULL, y = NULL) +
theme_bw() +
theme(axis.text = element_blank(), axis.ticks = element_blank())Varians prediksi Kriging (hanya Kriging yang menghasilkan ukuran ketidakpastian prediksi):
ggplot() +
geom_sf(data = kpred, aes(color = var1.var), size = 1) +
geom_sf(data = d, size = 0.6) +
scale_color_viridis(name = "varians") +
labs(title = "Varians prediksi Ordinary Kriging") +
theme_bw()Varians kecil di sekitar titik pengamatan dan membesar di area yang jauh dari titik pengamatan.
Evaluasi Model: Cross-Validation
Data dibagi menjadi \(K = 5\) fold
dengan dismo::kfold(). Pada setiap iterasi, satu fold
menjadi data uji dan empat fold lainnya menjadi data latih. Pada setiap
iterasi, seluruh langkah dihitung ulang hanya dari data
latih: prediksi tiap metode, bobot ensembel (lewat inner CV),
dan semivariogram Kriging.
set.seed(123)
K <- 5
kf <- dismo::kfold(nrow(d), k = K)
metode <- c(closest = "Closest Observation",
nn = "Nearest Neighbors (k = 5)",
idw = "IDW (p = 1)",
ensemble_w = "Ensembel (bobot invers RMSE)",
ensemble_eq = "Ensembel (bobot sama)",
kriging = "Ordinary Kriging")
oof <- data.frame(fold = kf, obs = d$vble,
matrix(NA_real_, nrow(d), length(metode),
dimnames = list(NULL, names(metode))))
bobot_fold <- matrix(NA_real_, K, 3,
dimnames = list(paste("Fold", 1:K), c("Closest", "NN", "IDW")))
for (k in 1:K) {
test <- d[kf == k, ]
train <- d[kf != k, ]
i <- which(kf == k)
p1 <- pred_closest(train, test)
p2 <- pred_nn(train, test)
p3 <- pred_idw(train, test)
w <- hitung_bobot(train) # bobot hanya dari data latih
bobot_fold[k, ] <- w
oof$closest[i] <- p1
oof$nn[i] <- p2
oof$idw[i] <- p3
oof$ensemble_w[i] <- w[1] * p1 + w[2] * p2 + w[3] * p3
oof$ensemble_eq[i] <- (p1 + p2 + p3) / 3
oof$kriging[i] <- pred_ok(train, test)$var1.pred
}Bobot ensembel (invers RMSE) yang dipakai pada tiap fold:
| Closest | NN | IDW | |
|---|---|---|---|
| Fold 1 | 0.3367 | 0.3687 | 0.2947 |
| Fold 2 | 0.3122 | 0.3883 | 0.2994 |
| Fold 3 | 0.3436 | 0.3661 | 0.2904 |
| Fold 4 | 0.3182 | 0.3827 | 0.2990 |
| Fold 5 | 0.3437 | 0.3704 | 0.2859 |
RMSE pada setiap fold
rmse_fold <- sapply(names(metode), function(m)
tapply(seq_len(nrow(oof)), oof$fold, function(i) RMSE(oof$obs[i], oof[[m]][i])))
colnames(rmse_fold) <- metode
rownames(rmse_fold) <- paste("Fold", 1:K)
kable(rmse_fold, digits = 4, caption = "RMSE tiap fold (skala log(zinc))")| Closest Observation | Nearest Neighbors (k = 5) | IDW (p = 1) | Ensembel (bobot invers RMSE) | Ensembel (bobot sama) | Ordinary Kriging | |
|---|---|---|---|---|---|---|
| Fold 1 | 0.6327 | 0.4958 | 0.5496 | 0.4702 | 0.4707 | 0.4281 |
| Fold 2 | 0.5987 | 0.4943 | 0.5828 | 0.4718 | 0.4733 | 0.4183 |
| Fold 3 | 0.6328 | 0.6516 | 0.7843 | 0.6330 | 0.6379 | 0.5055 |
| Fold 4 | 0.4874 | 0.5316 | 0.6333 | 0.4907 | 0.4915 | 0.4198 |
| Fold 5 | 0.5593 | 0.4791 | 0.6293 | 0.4768 | 0.4820 | 0.3840 |
Rata-rata RMSE
hasil <- data.frame(Metode = metode,
RMSE_rata2 = colMeans(rmse_fold),
SD_antar_fold = apply(rmse_fold, 2, sd),
row.names = NULL) %>%
arrange(RMSE_rata2) %>%
mutate(Peringkat = row_number(),
Selisih_vs_terbaik = RMSE_rata2 - min(RMSE_rata2)) %>%
select(Peringkat, Metode, RMSE_rata2, SD_antar_fold, Selisih_vs_terbaik)
kable(hasil, digits = 4,
caption = "Rata-rata RMSE 5-fold CV (skala log(zinc)); makin kecil makin baik")| Peringkat | Metode | RMSE_rata2 | SD_antar_fold | Selisih_vs_terbaik |
|---|---|---|---|---|
| 1 | Ordinary Kriging | 0.4312 | 0.0449 | 0.0000 |
| 2 | Ensembel (bobot invers RMSE) | 0.5085 | 0.0701 | 0.0773 |
| 3 | Ensembel (bobot sama) | 0.5111 | 0.0714 | 0.0799 |
| 4 | Nearest Neighbors (k = 5) | 0.5305 | 0.0704 | 0.0993 |
| 5 | Closest Observation | 0.5822 | 0.0610 | 0.1510 |
| 6 | IDW (p = 1) | 0.6358 | 0.0899 | 0.2047 |
urutan <- rev(hasil$Metode) # terbaik di atas setelah coord_flip
rmse_long <- data.frame(Metode = factor(rep(metode, each = K), levels = urutan),
Fold = rep(1:K, times = length(metode)),
RMSE = as.vector(rmse_fold))
hasil_plot <- transform(hasil, Metode = factor(Metode, levels = urutan))
ggplot(hasil_plot, aes(x = Metode, y = RMSE_rata2)) +
geom_col(fill = "steelblue", alpha = 0.8) +
geom_point(data = rmse_long, aes(y = RMSE), shape = 21, fill = "white", size = 2) +
geom_text(aes(y = 0, label = sprintf("%.3f", RMSE_rata2)), hjust = -0.2, color = "white", fontface = "bold", size = 3.8) +
coord_flip() +
labs(title = "Perbandingan RMSE (5-fold CV)",
subtitle = "Batang = rata-rata 5 fold; titik = RMSE tiap fold",
x = NULL, y = "RMSE log(zinc)") +
theme_bw()RMSE pada skala asli (zinc, ppm)
RMSE di atas berada pada skala log. Untuk interpretasi pada satuan
asli, prediksi dikembalikan dengan exp()
(back-transform sederhana, tanpa koreksi bias) lalu
dibandingkan dengan nilai zinc sebenarnya.
rmse_asli <- sapply(names(metode), function(m)
tapply(seq_len(nrow(oof)), oof$fold,
function(i) RMSE(exp(oof$obs[i]), exp(oof[[m]][i]))))
colnames(rmse_asli) <- metode
kable(data.frame(Metode = metode, RMSE_zinc_ppm = colMeans(rmse_asli), row.names = NULL) %>%
arrange(RMSE_zinc_ppm),
digits = 2, caption = "Rata-rata RMSE 5-fold CV pada skala zinc (ppm)")| Metode | RMSE_zinc_ppm |
|---|---|
| Ordinary Kriging | 238.30 |
| Ensembel (bobot invers RMSE) | 292.23 |
| Ensembel (bobot sama) | 293.97 |
| Nearest Neighbors (k = 5) | 302.36 |
| Closest Observation | 311.25 |
| IDW (p = 1) | 350.54 |
Observasi vs prediksi
oof_long <- do.call(rbind, lapply(names(metode), function(m)
data.frame(Metode = metode[[m]], obs = oof$obs, pred = oof[[m]])))
oof_long$Metode <- factor(oof_long$Metode, levels = metode)
ggplot(oof_long, aes(obs, pred)) +
geom_abline(slope = 1, intercept = 0, color = "red") +
geom_point(alpha = 0.5, size = 1) +
facet_wrap(~ Metode, ncol = 3) +
labs(title = "Nilai observasi vs prediksi CV", x = "Observasi log(zinc)", y = "Prediksi log(zinc)") +
theme_bw()Kesimpulan
- Berdasarkan rata-rata RMSE 5-fold CV, metode terbaik adalah Ordinary Kriging (RMSE = 0.4312), sedangkan yang terburuk adalah IDW (p = 1) (RMSE = 0.6358).
- Ordinary Kriging memiliki RMSE 0.4312, dibandingkan 0.5305 untuk metode deterministik tunggal terbaik.
- Kriging juga menghasilkan varians prediksi sebagai ukuran ketidakpastian, yang tidak tersedia pada metode deterministik maupun ensembel.
- Ensembel (bobot invers RMSE) memiliki RMSE 0.5085, yaitu lebih kecil daripada anggota terbaiknya (0.5305). Ensembel tidak otomatis mengalahkan anggota terbaiknya: ia hanya meredam kesalahan anggota yang lemah (Closest Observation dan IDW) dan terbukti lebih baik daripada rata-rata ketiga anggotanya (0.5828). Bobot ensembel dihitung hanya dari data latih (tanpa kebocoran informasi dari data uji), sehingga RMSE-nya dapat dibandingkan secara jujur dengan metode lain.