Tugas Regresi Spasial Praktikum 9

Persiapan Data

library(sp)
library(sf)
## Linking to GEOS 3.14.1, GDAL 3.12.1, PROJ 9.7.1; sf_use_s2() is TRUE
library(terra)
## terra 1.9.34
library(gstat)
library(tmap)
library(dismo)
## Loading required package: raster
library(ggplot2)
library(viridis)
## Loading required package: viridisLite
data(meuse)
data(meuse.grid)

# Mengubah data menjadi sf
meuse <- st_as_sf(meuse, coords = c("x", "y"), crs = 28992)
meuse.grid <- st_as_sf(meuse.grid, coords = c("x", "y"), crs = 28992)

# Data pengamatan
d <- meuse

# Titik yang akan diprediksi
coop <- meuse.grid

# Memberikan ID pada setiap titik prediksi
coop$id <- seq_len(nrow(coop))

# Membuat batas wilayah yang mencakup data pengamatan dan seluruh titik prediksi
map <- st_as_sfc(
  st_bbox(
    st_union(c(st_geometry(d), st_geometry(coop)))
  )
) |> st_sf()

# Membuat raster template
grid <- rast(
  ext(vect(map)),
  resolution = 40,
  crs = "EPSG:28992"
)

Eksplorasi Data

Persebaran Kandungan Timbal (Lead)

tm_shape(d) +
  tm_dots(
    col = "lead",
    palette = "viridis",
    size = 0.3,
    title = "Lead"
  ) +
  tm_layout(
    title = "Persebaran Kandungan Lead",
    legend.outside = TRUE
  )
## 
## ── tmap v3 code detected ───────────────────────────────────────────────────────
## [v3->v4] `tm_tm_dots()`: migrate the argument(s) related to the scale of the
## map variable `fill` namely 'palette' (rename to 'values') to fill.scale =
## tm_scale(<HERE>).
## [v3->v4] `tm_dots()`: use 'fill' for the fill color of polygons/symbols
## (instead of 'col'), and 'col' for the outlines (instead of 'border.col').
## [tm_dots()] Argument `title` unknown.
## [v3->v4] `tm_layout()`: use `tm_title()` instead of `tm_layout(title = )`

Lokasi Data Pengamatan dan Titik Prediksi

tm_shape(map) +
  tm_borders(col = "black") +
  tm_shape(coop) +
  tm_dots(
    col = "grey70",
    size = 0.02,
    alpha = 0.5
  ) +
  tm_shape(d) +
  tm_dots(
    col = "lead",
    palette = "viridis",
    size = 0.3,
    title = "Lead"
  ) +
  tm_layout(
    title = "Lokasi Data Pengamatan dan Titik Prediksi",
    legend.outside = TRUE
  )
## 
## ── tmap v3 code detected ───────────────────────────────────────────────────────
## [v3->v4] `tm_dots()`: use `fill_alpha` instead of `alpha`.
## [tm_dots()] Argument `title` unknown.
## [v3->v4] `tm_layout()`: use `tm_title()` instead of `tm_layout(title = )`
## [plot mode] fit legend/component: Some legend items or map compoments do not
## fit well, and are therefore rescaled.
## ℹ Set the tmap option `component.autoscale = FALSE` to disable rescaling.

Statistik Deskriptif

summary(d$lead)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    37.0    72.5   123.0   153.4   207.0   654.0
sd(d$lead)
## [1] 111.3201
var(d$lead)
## [1] 12392.15

Distribusi Kandungan Lead

ggplot(d, aes(x = lead)) +
  geom_histogram(
    bins = 20,
    fill = "steelblue",
    color = "black"
  ) +
  labs(
    title = "Distribusi Kandungan Timbal (Lead)",
    x = "Kandungan Lead",
    y = "Frekuensi"
  ) +
  theme_minimal()

Closest Observation

Metode Closest Observation menggunakan diagram Voronoi. Setiap lokasi prediksi memperoleh nilai dari titik pengamatan yang berada pada wilayah Voronoi yang memuat lokasi tersebut.

# Membuat diagram Voronoi
v <- terra::voronoi(
  x = vect(d),
  bnd = vect(map)
)

# Mengubah menjadi sf
v <- st_as_sf(v)

# Menampilkan diagram Voronoi
tm_shape(v) +
  tm_fill(
    col = "lead",
    palette = "viridis",
    alpha = 0.6
  ) +
  tm_layout(title = "Closest Observation - Lead")
## 
## ── tmap v3 code detected ───────────────────────────────────────────────────────
## [v3->v4] `tm_tm_polygons()`: migrate the argument(s) related to the scale of
## the map variable `fill` namely 'palette' (rename to 'values') to fill.scale =
## tm_scale(<HERE>).
## [v3->v4] `tm_polygons()`: use `fill_alpha` instead of `alpha`.
## [v3->v4] `tm_layout()`: use `tm_title()` instead of `tm_layout(title = )`

# Mengambil nilai lead pada setiap titik prediksi
resp_co <- st_intersection(v, coop)
## Warning: attribute variables are assumed to be spatially constant throughout
## all geometries
# Menyimpan hasil prediksi
resp_co$pred <- resp_co$lead

# Membuat raster hasil prediksi
pred_co <- rasterize(
  vect(resp_co),
  grid,
  field = "pred",
  fun = "mean"
)

c1 <- tm_shape(pred_co) +
  tm_raster(
    alpha = 0.6,
    palette = "viridis",
    title = "Prediksi Lead"
  ) +
  tm_layout(title = "Closest Observation")
## 
## ── tmap v3 code detected ───────────────────────────────────────────────────────
## [v3->v4] `tm_tm_raster()`: migrate the argument(s) related to the scale of the
## map variable `col` namely 'palette' (rename to 'values') to col.scale =
## tm_scale(<HERE>).[v3->v4] `tm_raster()`: use `col_alpha` instead of `alpha`.[v3->v4] `tm_raster()`: migrate the argument(s) related to the legend of the
## map variable `col` namely 'title' to 'col.legend = tm_legend(<HERE>)'[v3->v4] `tm_layout()`: use `tm_title()` instead of `tm_layout(title = )`
c1

Inverse Distance Weighting (IDW)

# Membentuk model IDW
gs <- gstat(
  formula = lead ~ 1,
  locations = d,
  nmax = nrow(d),
  set = list(idp = 1)
)

# Melakukan prediksi
resp_idw <- predict(gs, coop)
## [inverse distance weighted interpolation]
# Menyimpan hasil prediksi
resp_idw$pred <- resp_idw$var1.pred

# Membuat raster hasil prediksi
pred_idw <- rasterize(
  vect(resp_idw),
  grid,
  field = "pred",
  fun = "mean"
)

c2 <- tm_shape(pred_idw) +
  tm_raster(
    alpha = 0.6,
    palette = "viridis",
    title = "Prediksi Lead"
  ) +
  tm_layout(title = "Inverse Distance Weighting")
## 
## ── tmap v3 code detected ───────────────────────────────────────────────────────
## [v3->v4] `tm_tm_raster()`: migrate the argument(s) related to the scale of the
## map variable `col` namely 'palette' (rename to 'values') to col.scale =
## tm_scale(<HERE>).
## [v3->v4] `tm_raster()`: use `col_alpha` instead of `alpha`.
## [v3->v4] `tm_raster()`: migrate the argument(s) related to the legend of the
## map variable `col` namely 'title' to 'col.legend = tm_legend(<HERE>)'
## [v3->v4] `tm_layout()`: use `tm_title()` instead of `tm_layout(title = )`
c2

Nearest Neighbors

Metode Nearest Neighbors menggunakan lima titik pengamatan terdekat. Dengan idp = 0, bobot setiap titik tetangga dibuat sama.

# Membentuk model Nearest Neighbors
nn <- gstat(
  formula = lead ~ 1,
  locations = d,
  nmax = 5,
  set = list(idp = 0)
)

# Melakukan prediksi
resp_nn <- predict(nn, coop)
## [inverse distance weighted interpolation]
# Menyimpan hasil prediksi
resp_nn$pred <- resp_nn$var1.pred

# Membuat raster hasil prediksi
pred_nn <- rasterize(
  vect(resp_nn),
  grid,
  field = "pred",
  fun = "mean"
)

c3 <- tm_shape(pred_nn) +
  tm_raster(
    alpha = 0.6,
    palette = "viridis",
    title = "Prediksi Lead"
  ) +
  tm_layout(title = "Nearest Neighbors")
## 
## ── tmap v3 code detected ───────────────────────────────────────────────────────
## [v3->v4] `tm_tm_raster()`: migrate the argument(s) related to the scale of the
## map variable `col` namely 'palette' (rename to 'values') to col.scale =
## tm_scale(<HERE>).
## [v3->v4] `tm_raster()`: use `col_alpha` instead of `alpha`.
## [v3->v4] `tm_raster()`: migrate the argument(s) related to the legend of the
## map variable `col` namely 'title' to 'col.legend = tm_legend(<HERE>)'
## [v3->v4] `tm_layout()`: use `tm_title()` instead of `tm_layout(title = )`
c3

Ensemble

Metode Ensemble menggabungkan hasil prediksi dari Closest Observation, IDW, dan Nearest Neighbors dengan bobot yang sama, yaitu masing-masing sepertiga (1/3)

# Menentukan bobot yang sama
weights <- c(1/3, 1/3, 1/3)

# Closest Observation
# Mencocokkan prediksi berdasarkan ID titik prediksi
v2 <- v
v2$pred_co <- v2$lead

resp_co_ensemble <- st_intersection(
  v2["pred_co"],
  coop["id"]
)
## Warning: attribute variables are assumed to be spatially constant throughout
## all geometries
p1 <- resp_co_ensemble$pred_co[
  match(coop$id, resp_co_ensemble$id)
]

# IDW
gs <- gstat(
  formula = lead ~ 1,
  locations = d,
  nmax = nrow(d),
  set = list(idp = 1)
)

p2 <- predict(gs, coop)$var1.pred
## [inverse distance weighted interpolation]
# Nearest Neighbors
nn <- gstat(
  formula = lead ~ 1,
  locations = d,
  nmax = 5,
  set = list(idp = 0)
)

p3 <- predict(nn, coop)$var1.pred
## [inverse distance weighted interpolation]
# Memastikan semua titik memperoleh prediksi
stopifnot(
  length(p1) == nrow(coop),
  !anyNA(p1),
  !anyNA(p2),
  !anyNA(p3)
)

# Menggabungkan hasil prediksi
p4 <- p1 * weights[1] +
      p2 * weights[2] +
      p3 * weights[3]

# Menyimpan hasil prediksi ke data titik
resp_ensemble <- coop
resp_ensemble$pred <- p4

# Membuat raster hasil prediksi
pred_ensemble <- rasterize(
  vect(resp_ensemble),
  grid,
  field = "pred",
  fun = "mean"
)

c4 <- tm_shape(pred_ensemble) +
  tm_raster(
    alpha = 0.6,
    palette = "viridis",
    title = "Prediksi Lead"
  ) +
  tm_layout(title = "Ensemble")
## 
## ── tmap v3 code detected ───────────────────────────────────────────────────────
## [v3->v4] `tm_tm_raster()`: migrate the argument(s) related to the scale of the
## map variable `col` namely 'palette' (rename to 'values') to col.scale =
## tm_scale(<HERE>).
## [v3->v4] `tm_raster()`: use `col_alpha` instead of `alpha`.
## [v3->v4] `tm_raster()`: migrate the argument(s) related to the legend of the
## map variable `col` namely 'title' to 'col.legend = tm_legend(<HERE>)'
## [v3->v4] `tm_layout()`: use `tm_title()` instead of `tm_layout(title = )`
c4

Kriging

Menghitung Experimental Variogram

# Menghitung experimental variogram
v_kriging <- variogram(lead ~ 1, d)

# Menampilkan experimental variogram
plot(
  v_kriging,
  main = "Experimental Variogram Lead"
)

Fitting Model Variogram

Model variogram yang digunakan adalah Spherical. Nilai awal sill dan nugget ditentukan berdasarkan varians data lead, sedangkan range awal ditetapkan sebesar 900 meter.

# Menentukan nilai awal berdasarkan varians lead
nilai_var <- var(d$lead)

# Menyesuaikan model variogram Spherical
fv <- fit.variogram(
  object = v_kriging,
  model = vgm(
    psill = 0.8 * nilai_var,
    model = "Sph",
    range = 900,
    nugget = 0.2 * nilai_var
  )
)

# Menampilkan hasil fitting
fv
##   model     psill    range
## 1   Nug  3024.205   0.0000
## 2   Sph 12412.918 906.6834
# Menampilkan plot experimental variogram dan model
plot(
  v_kriging,
  fv,
  main = "Fitting Variogram Lead"
)

Membentuk Model Ordinary Kriging dan Prediksi

# Membentuk model Ordinary Kriging
model_kriging <- gstat(
  formula = lead ~ 1,
  data = d,
  model = fv
)

# Melakukan prediksi pada titik prediksi
kpred <- predict(model_kriging, coop)
## [using ordinary kriging]
# Menyimpan hasil prediksi
kpred$pred <- kpred$var1.pred

# Membuat raster hasil prediksi
pred_kriging <- rasterize(
  vect(kpred),
  grid,
  field = "pred",
  fun = "mean"
)

c5 <- tm_shape(pred_kriging) +
  tm_raster(
    alpha = 0.6,
    palette = "viridis",
    title = "Prediksi Lead"
  ) +
  tm_layout(title = "Ordinary Kriging")
## 
## ── tmap v3 code detected ───────────────────────────────────────────────────────
## [v3->v4] `tm_tm_raster()`: migrate the argument(s) related to the scale of the
## map variable `col` namely 'palette' (rename to 'values') to col.scale =
## tm_scale(<HERE>).
## [v3->v4] `tm_raster()`: use `col_alpha` instead of `alpha`.
## [v3->v4] `tm_raster()`: migrate the argument(s) related to the legend of the
## map variable `col` namely 'title' to 'col.legend = tm_legend(<HERE>)'
## [v3->v4] `tm_layout()`: use `tm_title()` instead of `tm_layout(title = )`
c5

Varians Prediksi Kriging

# Menyimpan varians prediksi
kpred$varians <- kpred$var1.var

# Membuat raster varians prediksi
var_kriging <- rasterize(
  vect(kpred),
  grid,
  field = "varians",
  fun = "mean"
)

tm_shape(var_kriging) +
  tm_raster(
    palette = "viridis",
    title = "Varians"
  ) +
  tm_layout(
    title = "Varians Prediksi Kriging",
    legend.outside = TRUE
  )
## 
## ── tmap v3 code detected ───────────────────────────────────────────────────────
## [v3->v4] `tm_tm_raster()`: migrate the argument(s) related to the scale of the
## map variable `col` namely 'palette' (rename to 'values') to col.scale =
## tm_scale(<HERE>).
## [v3->v4] `tm_raster()`: migrate the argument(s) related to the legend of the
## map variable `col` namely 'title' to 'col.legend = tm_legend(<HERE>)'
## [v3->v4] `tm_layout()`: use `tm_title()` instead of `tm_layout(title = )`

Perbandingan Peta Hasil Interpolasi

tmap_arrange(
  c1, c2, c3, c4, c5,
  ncol = 2
)
## [plot mode] fit legend/component: Some legend items or map compoments do not
## fit well, and are therefore rescaled.
## ℹ Set the tmap option `component.autoscale = FALSE` to disable rescaling.

Cross Validation

Cross Validation dilakukan menggunakan 5-Fold Cross Validation. Pada setiap iterasi, empat fold digunakan sebagai data training dan satu fold digunakan sebagai data testing. Evaluasi dilakukan menggunakan Root Mean Square Error (RMSE).

Persiapan Cross Validation

# Menentukan seed
set.seed(123)

# Fungsi RMSE
RMSE <- function(observed, predicted) {
  sqrt(mean((observed - predicted)^2))
}

# Membagi data menjadi 5 fold
kf <- dismo::kfold(nrow(d), k = 5)

# Menyimpan nilai RMSE setiap metode
rmse1 <- rep(NA, 5) # Closest Observation
rmse2 <- rep(NA, 5) # IDW
rmse3 <- rep(NA, 5) # Nearest Neighbors
rmse4 <- rep(NA, 5) # Ensemble
rmse5 <- rep(NA, 5) # Kriging

Proses 5-Fold Cross Validation

for(k in 1:5){

  # Membagi data training dan testing
  test <- d[kf == k, ]
  train <- d[kf != k, ]

  # Memberikan ID pada data testing
  test$id <- seq_len(nrow(test))


  # 1. Closest Observation
  v_cv <- terra::voronoi(
    x = vect(train),
    bnd = vect(map)
  )

  v_cv <- st_as_sf(v_cv)

  # Menyimpan nilai lead sebagai prediksi
  v_cv$pred_co <- v_cv$lead

  # Mengambil prediksi pada lokasi data testing
  resp_cv <- st_intersection(
    v_cv["pred_co"],
    test["id"]
  )

  # Mencocokkan prediksi berdasarkan ID data testing
  p1 <- resp_cv$pred_co[
    match(test$id, resp_cv$id)
  ]

  # Memastikan seluruh data testing memperoleh prediksi
  stopifnot(
    length(p1) == nrow(test),
    !anyNA(p1)
  )

  rmse1[k] <- RMSE(test$lead, p1)


  # 2. IDW
  gs <- gstat(
    formula = lead ~ 1,
    locations = train,
    nmax = nrow(train),
    set = list(idp = 1)
  )

  p2 <- predict(gs, test)$var1.pred

  rmse2[k] <- RMSE(test$lead, p2)


  # 3. Nearest Neighbors
  nn <- gstat(
    formula = lead ~ 1,
    locations = train,
    nmax = 5,
    set = list(idp = 0)
  )

  p3 <- predict(nn, test)$var1.pred

  rmse3[k] <- RMSE(test$lead, p3)


  # 4. Ensemble (Equal Weights)
  weights <- c(1/3, 1/3, 1/3)

  p4 <- p1 * weights[1] +
        p2 * weights[2] +
        p3 * weights[3]

  rmse4[k] <- RMSE(test$lead, p4)


  # 5. Kriging
  vgram <- variogram(lead ~ 1, train)

  nilai_var_train <- var(train$lead)

  fv_cv <- fit.variogram(
    object = vgram,
    model = vgm(
      psill = 0.8 * nilai_var_train,
      model = "Sph",
      range = 900,
      nugget = 0.2 * nilai_var_train
    )
  )

  model_kriging_cv <- gstat(
    formula = lead ~ 1,
    data = train,
    model = fv_cv
  )

  p5 <- predict(model_kriging_cv, test)$var1.pred

  rmse5[k] <- RMSE(test$lead, p5)

}
## Warning: attribute variables are assumed to be spatially constant throughout
## all geometries
## [inverse distance weighted interpolation]
## [inverse distance weighted interpolation]
## [using ordinary kriging]
## Warning: attribute variables are assumed to be spatially constant throughout
## all geometries
## [inverse distance weighted interpolation]
## [inverse distance weighted interpolation]
## [using ordinary kriging]
## Warning: attribute variables are assumed to be spatially constant throughout
## all geometries
## [inverse distance weighted interpolation]
## [inverse distance weighted interpolation]
## [using ordinary kriging]
## Warning: attribute variables are assumed to be spatially constant throughout
## all geometries
## [inverse distance weighted interpolation]
## [inverse distance weighted interpolation]
## [using ordinary kriging]
## Warning: attribute variables are assumed to be spatially constant throughout
## all geometries
## [inverse distance weighted interpolation]
## [inverse distance weighted interpolation]
## [using ordinary kriging]

RMSE Setiap Fold

hasil_rmse <- data.frame(
  Fold = 1:5,
  Closest_Observation = rmse1,
  IDW = rmse2,
  Nearest_Neighbors = rmse3,
  Ensemble = rmse4,
  Kriging = rmse5
)

hasil_rmse
##   Fold Closest_Observation       IDW Nearest_Neighbors  Ensemble   Kriging
## 1    1           105.11729  81.66033          81.45935  78.82838  71.46859
## 2    2           128.23290  92.91140          95.95009  94.40308  88.59842
## 3    3           109.12659 147.16314         127.58901 121.44324 112.53338
## 4    4            74.41817  90.20712          75.37686  68.78230  66.52148
## 5    5            81.93270  84.51001          69.80773  69.93459  62.85488

Rata-rata RMSE Setiap Metode

rata_rmse <- data.frame(
  Metode = c(
    "Closest Observation",
    "IDW",
    "Nearest Neighbors",
    "Ensemble",
    "Kriging"
  ),
  RMSE = c(
    mean(rmse1),
    mean(rmse2),
    mean(rmse3),
    mean(rmse4),
    mean(rmse5)
  )
)

# Mengurutkan metode berdasarkan RMSE terkecil
rata_rmse <- rata_rmse[
  order(rata_rmse$RMSE),
]

rata_rmse
##                Metode     RMSE
## 5             Kriging 80.39535
## 4            Ensemble 86.67831
## 3   Nearest Neighbors 90.03661
## 2                 IDW 99.29040
## 1 Closest Observation 99.76553

Metode Kriging merupakan metode yang paling baik untuk mengestimasi nilai lead pada data meuse karena menghasilkan nilai RMSE paling kecil. Sementara itu, metode Closest Observation menghasilkan RMSE terbesar, sehingga memiliki tingkat kesalahan prediksi paling tinggi. Metode Ensemble juga menunjukkan hasil yang cukup baik karena menempati posisi kedua setelah Kriging