Interpolasi titik adalah proses menduga nilai suatu variabel pada lokasi yang belum diamati menggunakan pengamatan di lokasi lain. Contohnya, suhu di lokasi tanpa alat ukur diperkirakan menggunakan suhu beberapa stasiun di sekitarnya.
Capaian praktikum
# Semua kode muncul pada hasil Knit.
knitr::opts_chunk$set(
echo = TRUE, eval = TRUE, message = FALSE, warning = TRUE,
fig.width = 8, fig.height = 5.5, fig.align = "center",
dpi = 120, cache = FALSE, error = FALSE
)
options(width = 85, digits = 5)
# Instal hanya paket yang belum tersedia.
paket <- c("deldir", "knitr", "rmarkdown")
belum_ada <- paket[!vapply(paket, requireNamespace, logical(1), quietly = TRUE)]
if (length(belum_ada) > 0) {
install.packages(belum_ada, repos = "https://cloud.r-project.org")
}
stopifnot(all(vapply(paket, requireNamespace, logical(1), quietly = TRUE)))
Wilayah latihan berbentuk persegi 10 × 10 km. Empat stasiun berada di sudut wilayah dan 32 stasiun berada di bagian dalam.
set.seed(1352)
stasiun <- data.frame(
id = sprintf("S%02d", 1:36),
x = c(0, 10, 10, 0, runif(32, 0.5, 9.5)),
y = c(0, 0, 10, 10, runif(32, 0.5, 9.5))
)
# Tren spasial, variasi lokal, dan galat acak
stasiun$suhu <- with(stasiun,
25 + 0.45*x - 0.25*y +
2*exp(-((x - 7)^2 + (y - 3)^2)/8) + rnorm(36, 0, 0.6)
)
target <- data.frame(x = 5, y = 5)
stopifnot(
all(complete.cases(stasiun)),
all(is.finite(as.matrix(stasiun[c("x", "y", "suhu")]))),
!anyDuplicated(stasiun[c("x", "y")])
)
knitr::kable(head(stasiun, 8), digits = 2,
caption = "Delapan baris pertama data suhu simulasi")
| id | x | y | suhu |
|---|---|---|---|
| S01 | 0.00 | 0.00 | 23.76 |
| S02 | 10.00 | 0.00 | 29.63 |
| S03 | 10.00 | 10.00 | 27.73 |
| S04 | 0.00 | 10.00 | 21.68 |
| S05 | 5.78 | 7.23 | 27.87 |
| S06 | 9.16 | 6.92 | 27.83 |
| S07 | 1.82 | 5.57 | 24.20 |
| S08 | 2.60 | 4.48 | 24.73 |
summary(stasiun[c("x", "y", "suhu")])
## x y suhu
## Min. : 0.00 Min. : 0.00 Min. :21.7
## 1st Qu.: 2.32 1st Qu.: 2.91 1st Qu.:25.0
## Median : 4.88 Median : 5.21 Median :26.2
## Mean : 4.84 Mean : 5.33 Mean :26.2
## 3rd Qu.: 7.13 3rd Qu.: 7.68 3rd Qu.:27.6
## Max. :10.00 Max. :10.00 Max. :29.6
plot(stasiun$x, stasiun$y, pch = 21, bg = "#1495a7",
xlim = c(0, 10), ylim = c(0, 10), asp = 1,
xlab = "Koordinat x (km)", ylab = "Koordinat y (km)",
main = "Lokasi stasiun dan target")
text(stasiun$x, stasiun$y, labels = stasiun$id, pos = 3, cex = 0.6)
points(target$x, target$y, pch = 8, col = "#d44a30", cex = 1.8, lwd = 2)
legend("topleft", c("Stasiun", "Target (5,5)"),
pch = c(21, 8), pt.bg = "#1495a7",
col = c("black", "#d44a30"), bty = "n")
Grid adalah kumpulan lokasi yang akan diprediksi. Grid lebih rapat membuat gambar lebih rinci, tetapi tidak otomatis meningkatkan akurasi model.
gx <- seq(0, 10, length.out = 51)
gy <- seq(0, 10, length.out = 51)
grid <- expand.grid(x = gx, y = gy)
jarak_ke <- function(data, titik) {
sqrt((data$x - titik$x)^2 + (data$y - titik$y)^2)
}
# Semua peta memakai skala warna yang sama.
palet <- grDevices::hcl.colors(60, "YlOrRd", rev = TRUE)
batas_warna <- range(stasiun$suhu)
peta <- function(prediksi, judul) {
stopifnot(length(prediksi) == nrow(grid))
image(gx, gy, matrix(prediksi, nrow = length(gx)),
col = palet, zlim = batas_warna, asp = 1,
xlab = "Koordinat x (km)", ylab = "Koordinat y (km)", main = judul)
contour(gx, gy, matrix(prediksi, nrow = length(gx)),
add = TRUE, drawlabels = FALSE, col = "#55555555")
points(stasiun$x, stasiun$y, pch = 21, bg = "white", cex = 0.65)
points(target$x, target$y, pch = 8, col = "black", cex = 1.3)
box()
}
# Legenda warna bersama
image(seq(0, 1, length.out = 60), 1,
matrix(seq(batas_warna[1], batas_warna[2], length.out = 60), 60, 1),
col = palet, axes = FALSE, xlab = "Suhu (derajat Celsius)", ylab = "")
axis(1, at = seq(0, 1, length.out = 5),
labels = round(seq(batas_warna[1], batas_warna[2], length.out = 5), 1))
Kolom x dan y menyatakan lokasi dalam km, sedangkan suhu adalah variabel yang diprediksi dalam °C. Tanda bintang menunjukkan target yang tidak memiliki suhu pengamatan. Warna peta menunjukkan suhu dugaan, bukan ketidakpastian.
Peta pada bagian demonstrasi menggunakan semua stasiun. Penilaian akurasi dilakukan secara terpisah menggunakan pembagian data latih dan data uji.
IDW memberi bobot lebih besar kepada stasiun yang lebih dekat:
\[ \widehat{Z}(s_0)= \frac{\sum_{i=1}^{n}d(s_0,s_i)^{-p}Z(s_i)} {\sum_{i=1}^{n}d(s_0,s_i)^{-p}}. \]
Parameter \(p>0\) mengatur penurunan pengaruh terhadap jarak. Ketika \(p\) semakin besar, titik paling dekat semakin dominan. Nilai \(p=2\) dipakai untuk contoh, bukan nilai yang selalu terbaik.
Misalkan tersedia tiga stasiun A, B, dan C dengan suhu masing-masing 25 °C, 30 °C, dan 28 °C. Jarak setiap stasiun ke lokasi target berturut-turut adalah 2 km, 3 km, dan 5 km. Dengan parameter \(p=2\), suhu di lokasi target dihitung sebagai berikut.
contoh_idw <- data.frame(
titik = c("A", "B", "C"),
suhu = c(25, 30, 28), jarak = c(2, 3, 5)
)
contoh_idw$bobot_awal <- 1 / contoh_idw$jarak^2
contoh_idw$bobot <- contoh_idw$bobot_awal / sum(contoh_idw$bobot_awal)
contoh_idw$kontribusi <- contoh_idw$bobot * contoh_idw$suhu
knitr::kable(contoh_idw, digits = 4)
| titik | suhu | jarak | bobot_awal | bobot | kontribusi |
|---|---|---|---|---|---|
| A | 25 | 2 | 0.2500 | 0.6233 | 15.5817 |
| B | 30 | 3 | 0.1111 | 0.2770 | 8.3102 |
| C | 28 | 5 | 0.0400 | 0.0997 | 2.7922 |
idw_manual <- sum(contoh_idw$kontribusi)
idw_manual
## [1] 26.684
Jika target tepat sama dengan lokasi stasiun, fungsi mengembalikan nilai pengamatan untuk menghindari pembagian dengan nol.
pred_idw <- function(data, baru, p = 2) {
stopifnot(p > 0)
vapply(seq_len(nrow(baru)), function(j) {
d <- jarak_ke(data, baru[j, ])
if (any(d < 1e-10)) return(mean(data$suhu[d < 1e-10]))
# Setara dengan 1/d^p setelah normalisasi.
w <- (min(d) / d)^p
sum(w * data$suhu) / sum(w)
}, numeric(1))
}
idw_target <- pred_idw(stasiun, target, p = 2)
idw_grid <- pred_idw(stasiun, grid, p = 2)
idw_target
## [1] 26.801
peta(idw_grid, "Interpolasi IDW (p = 2)")
knitr::kable(data.frame(
p = c(1, 2, 3),
prediksi = vapply(c(1, 2, 3),
function(p) pred_idw(stasiun, target, p), numeric(1))
), digits = 3)
| p | prediksi |
|---|---|
| 1 | 26.516 |
| 2 | 26.801 |
| 3 | 26.979 |
Pada contoh tiga stasiun, metode IDW dengan parameter \(p=2\) menghasilkan dugaan suhu sebesar 26,684 °C. Stasiun A memberikan pengaruh terbesar karena jaraknya paling dekat dengan lokasi target. Dengan demikian, kontribusi setiap stasiun ditentukan oleh jaraknya, bukan hanya oleh besar suhunya.
Pada data simulasi, suhu di lokasi (5,5) diprediksi sebesar 26,8 °C menggunakan metode IDW dengan \(p=2\). Angka ini merupakan perkiraan suhu pada lokasi yang belum diamati, berdasarkan suhu stasiun-stasiun di sekitarnya.
Metode ini mengambil nilai dari satu stasiun terdekat, tanpa rata-rata beberapa stasiun:
\[ \widehat Z(s_0)=Z(s_{i^*}),\qquad i^*=\arg\min_i d(s_0,s_i). \]
# Ilustrasi slide 10–11
contoh_nn <- data.frame(
id = c("A", "B", "C"),
x = c(1, 4, 6), y = c(2, 4, 1), suhu = c(25, 30, 28)
)
contoh_nn$jarak <- jarak_ke(contoh_nn, data.frame(x = 3, y = 3))
knitr::kable(contoh_nn, digits = 3)
| id | x | y | suhu | jarak |
|---|---|---|---|---|
| A | 1 | 2 | 25 | 2.236 |
| B | 4 | 4 | 30 | 1.414 |
| C | 6 | 1 | 28 | 3.606 |
contoh_nn$suhu[which.min(contoh_nn$jarak)]
## [1] 30
pred_nn <- function(data, baru) {
vapply(seq_len(nrow(baru)), function(j) {
d <- jarak_ke(data, baru[j, ])
data$suhu[which.min(d)]
}, numeric(1))
}
# Jika jarak seri, which.min memilih baris pertama.
tetangga <- which.min(jarak_ke(stasiun, target))
knitr::kable(stasiun[tetangga, ], digits = 3)
| id | x | y | suhu | |
|---|---|---|---|---|
| 10 | S10 | 4.833 | 5.636 | 27.133 |
nn_target <- pred_nn(stasiun, target)
nn_grid <- pred_nn(stasiun, grid)
nn_target
## [1] 27.133
peta(nn_grid, "Interpolasi tetangga terdekat")
Pada contoh tiga stasiun, stasiun B merupakan lokasi terdekat dari target (3,3), dengan jarak sekitar 1,414 satuan. Oleh karena itu, suhu target diprediksi sebesar 30 °C, sama dengan suhu di stasiun B.
Pada data simulasi, stasiun terdekat dari target (5,5) adalah S10, sehingga suhu target diprediksi sebesar 27,13 °C. Metode Nearest Neighbor mengambil langsung nilai dari satu stasiun terdekat, tanpa menghitung rata-rata atau mempertimbangkan suhu stasiun lainnya.
Pada peta, setiap daerah berwarna seragam menunjukkan lokasi-lokasi yang memiliki stasiun terdekat yang sama. Perubahan warna dapat terjadi secara mendadak ketika berpindah ke daerah pengaruh stasiun lain.
Triangulasi Delaunay membentuk jaringan segitiga dari lokasi pengamatan. Setelah ditemukan segitiga yang memuat target, prediksi dihitung dari tiga simpul dengan bobot barycentric:
\[ \widehat Z(s_0)=w_A Z_A+w_B Z_B+w_C Z_C,\qquad w_A+w_B+w_C=1. \]
Bobot dapat diperoleh dari perbandingan luas subsegitiga dengan luas
segitiga utama. Untuk target di dalam segitiga, bobot tidak negatif.
Modul tidak melakukan ekstrapolasi. Target di luar convex
hull, yaitu batas cembung terluar lokasi stasiun, menghasilkan
NA.
abc <- data.frame(
id = c("A", "B", "C"),
x = c(1, 4, 2), y = c(1, 1, 4), suhu = c(20, 30, 25)
)
# Persamaan: sum(w)=1, sum(w*x)=x_target, sum(w*y)=y_target.
M <- rbind(1, abc$x, abc$y)
w_abc <- as.numeric(solve(M, c(1, 2, 2)))
tri_manual <- sum(w_abc * abc$suhu)
knitr::kable(data.frame(abc, bobot = w_abc), digits = 5)
| id | x | y | suhu | bobot |
|---|---|---|---|---|
| A | 1 | 1 | 20 | 0.44444 |
| B | 4 | 1 | 30 | 0.22222 |
| C | 2 | 4 | 25 | 0.33333 |
tri_manual
## [1] 23.889
Paket deldir membentuk segitiga. Fungsi berikut menyelesaikan persamaan bobot dan mencari segitiga yang memuat target.
pred_tri <- function(data, baru) {
jaringan <- deldir::deldir(data$x, data$y, round = FALSE)
segitiga <- deldir::triang.list(jaringan)
hasil <- rep(NA_real_, nrow(baru))
for (j in seq_len(nrow(baru))) {
for (s in segitiga) {
M <- rbind(1, s$x, s$y)
w <- as.numeric(solve(M, c(1, baru$x[j], baru$y[j])))
if (all(w >= -1e-8) && all(w <= 1 + 1e-8)) {
hasil[j] <- sum(w * data$suhu[s$ptNum])
break
}
}
}
hasil
}
jaringan <- deldir::deldir(stasiun$x, stasiun$y, round = FALSE)
plot(stasiun$x, stasiun$y, asp = 1, pch = 19,
xlab = "x (km)", ylab = "y (km)", main = "Jaringan segitiga Delaunay")
for (s in deldir::triang.list(jaringan)) {
polygon(s$x, s$y, border = "#168a9b")
}
points(stasiun$x, stasiun$y, pch = 19, cex = 0.6)
points(target$x, target$y, pch = 8, col = "#d44a30", cex = 1.5)
tri_target <- pred_tri(stasiun, target)
tri_grid <- pred_tri(stasiun, grid)
tri_target
## [1] 27.243
peta(tri_grid, "Interpolasi linier berbasis triangulasi")
# Di luar convex hull:
pred_tri(stasiun, data.frame(x = 12, y = 12))
## [1] NA
Berdasarkan interpolasi linier berbasis triangulasi Delaunay, suhu pada lokasi target (5,5) diprediksi sebesar 27,24 °C. Prediksi diperoleh dari suhu di tiga stasiun yang menjadi simpul segitiga tempat target berada, dengan bobot sesuai posisi target di dalam segitiga.
Pada peta, suhu berubah secara linier di dalam setiap segitiga. Nilai suhu tetap menyambung pada batas antarsegitiga, tetapi arah atau kemiringan perubahannya dapat berbeda. Hasil NA pada lokasi di luar cakupan titik pengamatan berarti metode tidak menghasilkan prediksi di lokasi tersebut, bukan berarti suhunya nol.
Poligon Thiessen membagi wilayah menjadi daerah pengaruh stasiun. Lokasi di dalam sebuah poligon lebih dekat ke stasiun pemiliknya daripada ke stasiun lain. Untuk prediksi titik, hasilnya sama dengan nearest neighbor.
Thiessen juga digunakan untuk rata-rata wilayah:
\[ \overline Z_A=\frac{\sum_i A_i Z_i}{\sum_i A_i}, \]
dengan \(A_i\) luas bagian poligon dalam wilayah kajian. Rata-rata wilayah berbeda dari prediksi satu lokasi.
contoh_thiessen <- data.frame(
stasiun = c("A", "B", "C"),
hujan_mm = c(100, 120, 80), luas_km2 = c(40, 35, 25)
)
contoh_thiessen$bobot <- contoh_thiessen$luas_km2 /
sum(contoh_thiessen$luas_km2)
knitr::kable(contoh_thiessen, digits = 3)
| stasiun | hujan_mm | luas_km2 | bobot |
|---|---|---|---|
| A | 100 | 40 | 0.40 |
| B | 120 | 35 | 0.35 |
| C | 80 | 25 | 0.25 |
weighted.mean(contoh_thiessen$hujan_mm, contoh_thiessen$luas_km2)
## [1] 102
Batas persegi ditentukan melalui rw. Pada wilayah nyata
yang tidak persegi, poligon harus dipotong sesuai batas wilayah sebelum
luas dihitung.
vor <- deldir::deldir(stasiun$x, stasiun$y,
rw = c(0, 10, 0, 10), round = FALSE)
ubin <- deldir::tile.list(vor)
plot(NA, xlim = c(0, 10), ylim = c(0, 10), asp = 1,
xlab = "x (km)", ylab = "y (km)", main = "Poligon Thiessen")
for (u in ubin) {
suhu_i <- stasiun$suhu[u$ptNum]
indeks <- findInterval(suhu_i,
seq(batas_warna[1], batas_warna[2], length.out = 61),
all.inside = TRUE)
polygon(u$x, u$y, col = palet[indeks], border = "white")
}
points(stasiun$x, stasiun$y, pch = 19, cex = 0.6)
points(target$x, target$y, pch = 8, cex = 1.4)
box()
luas <- data.frame(
indeks = vapply(ubin, function(u) u$ptNum, integer(1)),
luas_km2 = vapply(ubin, function(u) u$area, numeric(1))
)
luas$id <- stasiun$id[luas$indeks]
luas$suhu <- stasiun$suhu[luas$indeks]
luas$bobot <- luas$luas_km2 / sum(luas$luas_km2)
knitr::kable(head(luas, 8), digits = 3)
| indeks | luas_km2 | id | suhu | bobot | |
|---|---|---|---|---|---|
| pt.1 | 1 | 1.223 | S01 | 23.756 | 0.012 |
| pt.2 | 2 | 4.565 | S02 | 29.632 | 0.046 |
| pt.3 | 3 | 0.951 | S03 | 27.733 | 0.010 |
| pt.4 | 4 | 2.122 | S04 | 21.683 | 0.021 |
| pt.5 | 5 | 1.916 | S05 | 27.871 | 0.019 |
| pt.6 | 6 | 5.447 | S06 | 27.830 | 0.054 |
| pt.7 | 7 | 5.017 | S07 | 24.199 | 0.050 |
| pt.8 | 8 | 0.514 | S08 | 24.729 | 0.005 |
sum(luas$luas_km2) # Sekitar 100 km2
## [1] 100
suhu_wilayah <- weighted.mean(luas$suhu, luas$luas_km2)
suhu_wilayah
## [1] 26.402
thiessen_target <- pred_nn(stasiun, target)
thiessen_target
## [1] 27.133
stopifnot(identical(thiessen_target, nn_target))
Total luas poligon Thiessen adalah 100 km², sesuai dengan luas wilayah kajian. Rata-rata suhu wilayah berdasarkan pembobotan luas poligon diperoleh sebesar 26,402 °C, atau sekitar 26,40 °C. Stasiun dengan daerah pengaruh lebih luas memperoleh bobot lebih besar dalam perhitungan rata-rata tersebut.
Sementara itu, suhu pada target (5,5) diprediksi sebesar 27,133 °C, atau sekitar 27,13 °C, mengikuti suhu stasiun terdekat. Hasil ini sama dengan metode Nearest Neighbor karena keduanya menetapkan nilai target berdasarkan stasiun terdekat.
Kedua nilai tersebut memiliki makna berbeda: 26,40 °C menggambarkan rata-rata suhu seluruh wilayah, sedangkan 27,13 °C merupakan prediksi suhu pada satu lokasi target.
Local averaging menghitung rata-rata dari \(k\) stasiun terdekat:
\[ \widehat Z(s_0)=\frac{1}{k}\sum_{i\in N_k(s_0)} Z(s_i). \]
Jarak dipakai untuk memilih tetangga, tetapi bobot setiap tetangga terpilih sama, yaitu \(1/k\). Pada IDW, bobot berubah mengikuti jarak. Dengan \(k=1\), local averaging sama dengan NN.
# Contoh slide 23:
mean(c(80, 100, 70, 90)) # 85 mm
## [1] 85
pred_local <- function(data, baru, k = 4) {
stopifnot(k >= 1, k <= nrow(data), k == as.integer(k))
vapply(seq_len(nrow(baru)), function(j) {
urutan <- order(jarak_ke(data, baru[j, ]))
mean(data$suhu[urutan[seq_len(k)]])
}, numeric(1))
}
terpilih <- order(jarak_ke(stasiun, target))[1:4]
knitr::kable(data.frame(
stasiun[terpilih, ],
jarak_km = jarak_ke(stasiun, target)[terpilih], bobot = 1/4
), digits = 3)
| id | x | y | suhu | jarak_km | bobot | |
|---|---|---|---|---|---|---|
| 10 | S10 | 4.833 | 5.636 | 27.133 | 0.658 | 0.25 |
| 25 | S25 | 5.783 | 4.430 | 26.924 | 0.969 | 0.25 |
| 23 | S23 | 3.356 | 5.432 | 25.083 | 1.700 | 0.25 |
| 26 | S26 | 6.645 | 5.643 | 26.776 | 1.767 | 0.25 |
local_target <- pred_local(stasiun, target, k = 4)
local_grid <- pred_local(stasiun, grid, k = 4)
local_target
## [1] 26.479
peta(local_grid, "Local averaging (k = 4)")
knitr::kable(data.frame(
k = c(1, 4, 8),
prediksi = vapply(c(1, 4, 8),
function(k) pred_local(stasiun, target, k), numeric(1))
), digits = 3)
| k | prediksi |
|---|---|
| 1 | 27.133 |
| 4 | 26.479 |
| 8 | 26.762 |
Contoh menghasilkan 85 mm. Pada simulasi, empat stasiun terdekat menghasilkan prediksi 26.48 °C.
Nilai \(k\) lebih besar cenderung meratakan variasi lokal. Permukaan dapat berubah mendadak ketika kelompok tetangga terpilih berubah. Nilai \(k=4\) ditetapkan untuk latihan; pemilihan nilai terbaik memerlukan validasi pada data latih.
Peta yang menarik belum membuktikan akurasi. Prediksi perlu dibandingkan dengan pengamatan yang tidak digunakan untuk membentuk prediksi tersebut.
\[ RMSE=\sqrt{\frac{1}{n}\sum_{i=1}^{n}(y_i-\widehat y_i)^2}, \qquad MAE=\frac{1}{n}\sum_{i=1}^{n}|y_i-\widehat y_i|. \]
Keduanya bersatuan °C. Nilai lebih kecil menunjukkan galat lebih rendah pada skema yang sama. RMSE lebih peka terhadap galat besar.
Validasi acak di sini merupakan pengenalan untuk mengisi lokasi yang tersebar di wilayah serupa. Untuk menilai prediksi wilayah baru yang berjauhan, gunakan validasi berbasis blok spasial. Kedekatan lokasi latih dan uji dapat membuat evaluasi acak terlalu optimistis.
Thiessen tidak menjadi kolom terpisah karena prediksi titiknya sama dengan NN. Menggabungkan keduanya akan menghitung informasi yang sama dua kali.
set.seed(2026)
indeks_uji <- sample(seq_len(nrow(stasiun)), size = 7)
latih <- stasiun[-indeks_uji, ]
uji <- stasiun[indeks_uji, ]
data.frame(n_latih = nrow(latih), n_uji = nrow(uji))
## n_latih n_uji
## 1 29 7
pred_semua <- function(data, baru) {
data.frame(
IDW = pred_idw(data, baru, p = 2),
NN = pred_nn(data, baru),
Triangulasi = pred_tri(data, baru),
Local = pred_local(data, baru, k = 4)
)
}
nilai_metrik <- function(y, pred) {
ok <- is.finite(y) & is.finite(pred)
n_ok <- sum(ok)
if (n_ok == 0) return(c(n = 0, RMSE = NA_real_, MAE = NA_real_))
c(n = n_ok,
RMSE = sqrt(mean((y[ok] - pred[ok])^2)),
MAE = mean(abs(y[ok] - pred[ok])))
}
ringkas_metrik <- function(y, pred) {
out <- do.call(rbind, lapply(pred, function(z) nilai_metrik(y, z)))
data.frame(metode = rownames(out), out, row.names = NULL)
}
set.seed(27)
K <- 5
fold <- sample(rep(seq_len(K), length.out = nrow(latih)))
table(fold)
## fold
## 1 2 3 4 5
## 6 6 6 6 5
oof <- as.data.frame(matrix(NA_real_, nrow(latih), 4))
names(oof) <- c("IDW", "NN", "Triangulasi", "Local")
for (f in seq_len(K)) {
valid <- which(fold == f)
oof[valid, ] <- pred_semua(latih[-valid, ], latih[valid, ])
}
hasil_cv <- ringkas_metrik(latih$suhu, oof)
hasil_cv$cakupan_persen <- 100 * hasil_cv$n / nrow(latih)
knitr::kable(hasil_cv, digits = 3,
caption = "Galat OOF gabungan dan cakupan lokasi prediksi")
| metode | n | RMSE | MAE | cakupan_persen |
|---|---|---|---|---|
| IDW | 29 | 1.424 | 1.041 | 100.000 |
| NN | 29 | 1.053 | 0.826 | 100.000 |
| Triangulasi | 18 | 0.634 | 0.508 | 62.069 |
| Local | 29 | 1.153 | 0.928 | 100.000 |
# Perbandingan pada lokasi yang sama untuk seluruh metode
sama_cv <- complete.cases(oof)
hasil_cv_sama <- ringkas_metrik(latih$suhu[sama_cv],
oof[sama_cv, , drop = FALSE])
knitr::kable(hasil_cv_sama, digits = 3,
caption = "Perbandingan OOF pada irisan lokasi yang sama")
| metode | n | RMSE | MAE |
|---|---|---|---|
| IDW | 18 | 0.668 | 0.588 |
| NN | 18 | 0.885 | 0.669 |
| Triangulasi | 18 | 0.634 | 0.508 |
| Local | 18 | 0.840 | 0.688 |
# RMSE per fold dan rata-ratanya, sesuai pembahasan PDF
rmse_fold <- do.call(rbind, lapply(seq_len(K), function(f) {
idx <- fold == f
a <- ringkas_metrik(latih$suhu[idx], oof[idx, , drop = FALSE])
data.frame(fold = f, a)
}))
knitr::kable(rmse_fold, digits = 3)
| fold | metode | n | RMSE | MAE |
|---|---|---|---|---|
| 1 | IDW | 6 | 0.538 | 0.458 |
| 1 | NN | 6 | 0.898 | 0.681 |
| 1 | Triangulasi | 6 | 0.464 | 0.404 |
| 1 | Local | 6 | 0.457 | 0.374 |
| 2 | IDW | 6 | 0.814 | 0.727 |
| 2 | NN | 6 | 0.580 | 0.455 |
| 2 | Triangulasi | 5 | 0.602 | 0.385 |
| 2 | Local | 6 | 0.685 | 0.629 |
| 3 | IDW | 6 | 1.681 | 1.408 |
| 3 | NN | 6 | 1.046 | 0.846 |
| 3 | Triangulasi | 3 | 0.627 | 0.555 |
| 3 | Local | 6 | 1.343 | 1.235 |
| 4 | IDW | 6 | 2.262 | 1.762 |
| 4 | NN | 6 | 1.367 | 1.142 |
| 4 | Triangulasi | 2 | 0.537 | 0.479 |
| 4 | Local | 6 | 1.720 | 1.479 |
| 5 | IDW | 5 | 1.040 | 0.813 |
| 5 | NN | 5 | 1.228 | 1.040 |
| 5 | Triangulasi | 2 | 1.088 | 1.083 |
| 5 | Local | 5 | 1.085 | 0.921 |
aggregate(RMSE ~ metode, data = rmse_fold, FUN = mean)
## metode RMSE
## 1 IDW 1.26687
## 2 Local 1.05792
## 3 NN 1.02390
## 4 Triangulasi 0.66364
pred_uji <- pred_semua(latih, uji)
hasil_uji <- ringkas_metrik(uji$suhu, pred_uji)
hasil_uji$cakupan_persen <- 100 * hasil_uji$n / nrow(uji)
knitr::kable(hasil_uji, digits = 3,
caption = "Kinerja uji akhir dan cakupan prediksi")
| metode | n | RMSE | MAE | cakupan_persen |
|---|---|---|---|---|
| IDW | 7 | 0.920 | 0.765 | 100 |
| NN | 7 | 1.402 | 1.155 | 100 |
| Triangulasi | 7 | 1.091 | 0.858 | 100 |
| Local | 7 | 0.858 | 0.746 | 100 |
sama_uji <- complete.cases(pred_uji)
hasil_uji_sama <- ringkas_metrik(uji$suhu[sama_uji],
pred_uji[sama_uji, , drop = FALSE])
knitr::kable(hasil_uji_sama, digits = 3,
caption = "Kinerja uji pada lokasi yang sama untuk seluruh metode")
| metode | n | RMSE | MAE |
|---|---|---|---|
| IDW | 7 | 0.920 | 0.765 |
| NN | 7 | 1.402 | 1.155 |
| Triangulasi | 7 | 1.091 | 0.858 |
| Local | 7 | 0.858 | 0.746 |
Kolom n adalah jumlah lokasi yang berhasil diprediksi. Triangulasi dapat kehilangan cakupan jika titik validasi atau uji berada di luar convex hull data latih. Jangan menyimpulkan metode lebih baik hanya karena RMSE-nya dihitung pada lokasi yang lebih mudah diprediksi.
Tabel irisan lokasi membandingkan metode pada pengamatan yang sama. Jika irisan kecil, kesimpulannya terbatas. Pertimbangkan galat dan cakupan sekaligus.
RMSE OOF gabungan dihitung dari seluruh galat OOF yang tersedia. Rata-rata RMSE fold merata-ratakan RMSE setiap fold. Keduanya tidak harus sama karena ukuran fold dan operasi akar kuadrat berbeda. Jika jumlah fold sama dengan jumlah observasi, prosedurnya disebut leave-one-out cross-validation (LOOCV).
Parameter \(p=2\) dan \(k=4\) ditetapkan sebelum evaluasi. Jika melakukan tuning, pilih parameter menggunakan validasi di dalam data latih; jangan memakai galat uji akhir untuk mengubah parameter.
Ensemble menggabungkan prediksi beberapa metode. Modul menggunakan IDW, NN, dan local averaging. Thiessen diwakili NN karena identik untuk titik. Triangulasi dipertahankan sebagai pembanding tersendiri karena tidak selalu menghasilkan prediksi OOF di luar convex hull.
| Bentuk | Cara penggabungan | Sumber aturan penggabungan |
|---|---|---|
| Rata-rata sederhana | Bobot setiap metode \(1/3\) | Ditetapkan langsung |
| Rata-rata berbobot | Bobot proporsional terhadap \(1/RMSE_{OOF}\) | OOF data latih |
| Stacking (pengayaan) | Regresi linier sebagai meta-model | Prediktor OOF data latih |
Untuk rata-rata berbobot,
\[ \widehat Z_{\mathrm{ens}}(s_0)=\sum_{m=1}^{M} w_m\widehat Z_m(s_0), \quad w_m=\frac{1/RMSE_m}{\sum_{\ell=1}^{M}1/RMSE_\ell}. \]
Aturan inverse-RMSE merupakan pilihan sederhana, bukan jaminan bobot optimal. Stacking mempelajari intersep dan koefisien melalui regresi; koefisiennya tidak harus positif atau berjumlah satu. Ensemble tidak dijamin mengungguli metode tunggal.
Bobot ditentukan dari OOF data latih, bukan data uji.
basis <- c("IDW", "NN", "Local")
rmse_basis <- vapply(oof[basis], function(z) {
sqrt(mean((latih$suhu - z)^2))
}, numeric(1))
stopifnot(all(is.finite(rmse_basis)))
bobot <- 1 / pmax(rmse_basis, 1e-8)
bobot <- bobot / sum(bobot)
knitr::kable(data.frame(metode = basis, RMSE_OOF = rmse_basis,
bobot = as.numeric(bobot)), digits = 4)
| metode | RMSE_OOF | bobot | |
|---|---|---|---|
| IDW | IDW | 1.4236 | 0.2788 |
| NN | NN | 1.0534 | 0.3768 |
| Local | Local | 1.1525 | 0.3444 |
sum(bobot)
## [1] 1
uji_rata <- rowMeans(pred_uji[basis])
uji_bobot <- as.numeric(as.matrix(pred_uji[basis]) %*% bobot)
# Tiap baris prediktor berikut merupakan prediksi OOF.
data_meta <- data.frame(suhu = latih$suhu, oof[basis])
meta <- lm(suhu ~ IDW + NN + Local, data = data_meta)
summary(meta)
##
## Call:
## lm(formula = suhu ~ IDW + NN + Local, data = data_meta)
##
## Residuals:
## Min 1Q Median 3Q Max
## -2.820 -0.425 0.079 0.693 1.399
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -1.312 5.959 -0.22 0.828
## IDW -0.665 0.416 -1.60 0.123
## NN 0.812 0.329 2.47 0.021 *
## Local 0.898 0.425 2.11 0.045 *
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 1.01 on 25 degrees of freedom
## Multiple R-squared: 0.756, Adjusted R-squared: 0.727
## F-statistic: 25.8 on 3 and 25 DF, p-value: 7.92e-08
# Base-model memprediksi uji dari seluruh data latih.
uji_stack <- as.numeric(predict(meta, newdata = pred_uji[basis]))
evaluasi_ensemble <- data.frame(
IDW = pred_uji$IDW, NN = pred_uji$NN, Local = pred_uji$Local,
Rata_rata = uji_rata, Berbobot = uji_bobot, Stacking = uji_stack
)
hasil_akhir <- ringkas_metrik(uji$suhu, evaluasi_ensemble)
hasil_akhir <- hasil_akhir[order(hasil_akhir$RMSE), ]
knitr::kable(hasil_akhir, digits = 3,
caption = "Metode dasar dan ensemble pada tujuh lokasi uji yang sama")
| metode | n | RMSE | MAE | |
|---|---|---|---|---|
| 3 | Local | 7 | 0.858 | 0.746 |
| 1 | IDW | 7 | 0.920 | 0.765 |
| 4 | Rata_rata | 7 | 1.038 | 0.888 |
| 5 | Berbobot | 7 | 1.058 | 0.905 |
| 6 | Stacking | 7 | 1.299 | 1.053 |
| 2 | NN | 7 | 1.402 | 1.155 |
plot(uji$suhu, uji_bobot, pch = 21, bg = "#1495a7",
xlab = "Suhu pengamatan (C)", ylab = "Prediksi ensemble berbobot (C)",
xlim = range(c(uji$suhu, uji_bobot)),
ylim = range(c(uji$suhu, uji_bobot)), asp = 1,
main = "Pengamatan vs prediksi uji")
abline(0, 1, col = "#d44a30", lty = 2, lwd = 2)
Peta ini memakai data latih dan bobot yang sudah ditetapkan. Data uji ditandai agar lokasi evaluasi terlihat.
basis_grid <- data.frame(
IDW = pred_idw(latih, grid),
NN = pred_nn(latih, grid),
Local = pred_local(latih, grid, k = 4)
)
grid_bobot <- as.numeric(as.matrix(basis_grid) %*% bobot)
peta(grid_bobot, "Ensemble berbobot dari data latih")
points(uji$x, uji$y, pch = 4, col = "#194f98", lwd = 2, cex = 1.3)
legend("topleft", c("Stasiun", "Data uji", "Target"),
pch = c(21, 4, 8), pt.bg = "white",
col = c("black", "#194f98", "black"), bty = "n")
Metode dengan RMSE OOF lebih kecil mendapat bobot lebih besar menurut aturan yang dipakai. Bobot adalah kontribusi dalam rumus ensemble, bukan probabilitas bahwa suatu metode benar.
Pada tujuh lokasi uji simulasi, metode dengan RMSE terkecil dalam tabel adalah Local, dengan RMSE 0.858 °C. Hasil ini berlaku pada simulasi dan pembagian tersebut, bukan semua data spasial.
Titik dekat garis merah menunjukkan prediksi mendekati pengamatan. Titik di atas garis menunjukkan prediksi terlalu tinggi; di bawah garis menunjukkan prediksi terlalu rendah.
Koefisien, p-value, dan \(R^2\) pada
summary(meta) menjelaskan penyesuaian meta-model pada data
latih OOF, bukan pengganti evaluasi uji akhir. Galat
ensemble pada baris OOF yang juga dipakai untuk menentukan bobot atau
melatih meta-model tidak digunakan sebagai estimasi kinerja akhir.
Tabel berikut kembali memakai seluruh stasiun, seperti demonstrasi awal. Ensemble tidak dicampurkan karena contoh evaluasinya memakai data latih.
tabel_target <- data.frame(
metode = c("IDW (p=2)", "Nearest neighbor", "Triangulasi linier",
"Thiessen (prediksi titik)", "Local averaging (k=4)"),
suhu_prediksi = c(idw_target, nn_target, tri_target,
thiessen_target, local_target)
)
knitr::kable(tabel_target, digits = 3,
caption = "Prediksi target (5,5) dari semua stasiun")
| metode | suhu_prediksi |
|---|---|
| IDW (p=2) | 26.801 |
| Nearest neighbor | 27.133 |
| Triangulasi linier | 27.243 |
| Thiessen (prediksi titik) | 27.133 |
| Local averaging (k=4) | 26.479 |
Perbedaan hasil berasal dari aturan pemilihan stasiun dan pembobotan. NN dan Thiessen menghasilkan angka sama. Target tidak memiliki pengamatan, sehingga tabel ini tidak dapat menentukan metode paling akurat.
| Metode | Informasi yang digunakan | Ciri keluaran |
|---|---|---|
| IDW | Nilai dan bobot jarak | Titik dekat lebih berpengaruh |
| NN | Satu stasiun terdekat | Konstan dalam daerah pengaruh |
| Triangulasi linier | Tiga simpul segitiga | Linier per segitiga; NA di luar convex hull |
| Thiessen | Daerah pengaruh terdekat | Setara NN untuk titik; luas untuk rata-rata wilayah |
| Local averaging | \(k\) tetangga terdekat | Rata-rata berbobot sama |
| Ensemble | Prediksi beberapa metode | Bergantung pada komponen dan penggabungannya |
Penyesuaian terhadap PDF: hitungan IDW dilengkapi; hasil triangulasi dihitung tanpa pembulatan antara (slide 15); nilai B pada contoh Thiessen diluruskan (slide 19–20); RMSE ditulis eksplisit menggunakan akar kuadrat. Validasi ditempatkan sebelum ensemble agar sumber bobot OOF dapat diikuti.
Rujukan
help("deldir", package = "deldir")
help("triang.list", package = "deldir")
help("tile.list", package = "deldir")
help("html_document", package = "rmarkdown")
# Versi R dan paket untuk menelusuri hasil praktikum
sessionInfo()
## R version 4.5.3 (2026-03-11 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
##
## Matrix products: default
## LAPACK version 3.12.1
##
## locale:
## [1] LC_COLLATE=English_Indonesia.utf8 LC_CTYPE=English_Indonesia.utf8
## [3] LC_MONETARY=English_Indonesia.utf8 LC_NUMERIC=C
## [5] LC_TIME=English_Indonesia.utf8
##
## time zone: Asia/Jakarta
## tzcode source: internal
##
## attached base packages:
## [1] stats graphics grDevices utils datasets methods base
##
## loaded via a namespace (and not attached):
## [1] digest_0.6.37 R6_2.6.1 fastmap_1.2.0 xfun_0.52
## [5] cachem_1.1.0 knitr_1.50 deldir_2.0-4 htmltools_0.5.8.1
## [9] rmarkdown_2.29 lifecycle_1.0.5 cli_3.6.5 sass_0.4.10
## [13] jquerylib_0.1.4 compiler_4.5.3 rstudioapi_0.19.0 tools_4.5.3
## [17] evaluate_1.0.5 bslib_0.9.0 yaml_2.3.10 rlang_1.3.0
## [21] jsonlite_2.0.0