📍 Pendahuluan

Analisis ini bertujuan untuk: - Mengelompokkan kabupaten/kota di Provinsi Kepulauan Bangka Belitung berdasarkan struktur PDRB menurut lapangan usaha. - Menentukan jumlah klaster optimal dengan Elbow, Silhouette, dan Gap Statistic. - Menafsirkan hasil klasterisasi untuk memahami struktur ekonomi wilayah.


⚙️ Setup Awal dan Pengambilan Data

# === Library utama ===
library(jsonlite)   # untuk membaca JSON dari API BPS
library(dplyr)      # manipulasi data (filter, mutate, summarise, dll)
library(purrr)      # iterasi fungsional (map/imap)
library(tidyr)      # reshaping data (pivot_wider, pivot_longer)
library(stringr)    # pembersihan teks (regex)
library(ggplot2)    # visualisasi data
library(viridis)    # palet warna modern
library(factoextra) # fungsi K-Means dan visualisasi cluster
library(cluster)    # Gap Statistic
library(sf)         # data spasial shapefile
library(patchwork)  # menggabungkan plot
library(grid)

🛰️ Pengambilan Data API BPS

# Daftar API PDRB per daerah (contoh tahun 2023/2024)
urls <- list(
  'Bangka'              = "https://webapi.bps.go.id/v1/api/list/model/data/lang/ind/domain/1901/var/45/th/124/key/e8629b07af3f4067624b0515d06a1ad0",
  'Belitung'            = "https://webapi.bps.go.id/v1/api/list/model/data/lang/ind/domain/1902/var/35/th/124/key/e8629b07af3f4067624b0515d06a1ad0",
  'Bangka Barat'        = "https://webapi.bps.go.id/v1/api/list/model/data/lang/ind/domain/1903/var/30/th/124/key/e8629b07af3f4067624b0515d06a1ad0",
  'Bangka Tengah'       = "https://webapi.bps.go.id/v1/api/list/model/data/lang/ind/domain/1904/var/46/th/124/key/e8629b07af3f4067624b0515d06a1ad0",
  'Bangka Selatan'      = "https://webapi.bps.go.id/v1/api/list/model/data/lang/ind/domain/1905/var/8/th/124/key/e8629b07af3f4067624b0515d06a1ad0",
  'Belitung Timur'      = "https://webapi.bps.go.id/v1/api/list/model/data/lang/ind/domain/1906/var/58/th/124/key/e8629b07af3f4067624b0515d06a1ad0",
  'Kota Pangkal Pinang' = "https://webapi.bps.go.id/v1/api/list/model/data/lang/ind/domain/1971/var/48/th/124/key/e8629b07af3f4067624b0515d06a1ad0"
)

# Fungsi ambil JSON API
get_raw_bps <- function(url, daerah) {
  cat("📦 Mengambil data:", daerah, "\n")
  res <- tryCatch(fromJSON(url), error = function(e) NULL)
  if (is.null(res)) {
    warning(paste("❌ Gagal mengambil data untuk", daerah))
    return(NULL)
  }
  return(res)
}

# Ambil semua data
raw_list <- imap(urls, get_raw_bps)
## 📦 Mengambil data: Bangka 
## 📦 Mengambil data: Belitung 
## 📦 Mengambil data: Bangka Barat 
## 📦 Mengambil data: Bangka Tengah 
## 📦 Mengambil data: Bangka Selatan 
## 📦 Mengambil data: Belitung Timur 
## 📦 Mengambil data: Kota Pangkal Pinang

Penjelasan:

fromJSON() membaca API dan mengonversinya menjadi list di R.

imap() dari purrr digunakan agar bisa mengambil data sekaligus menyimpan nama daerah.

🧹 Parsing dan Pembersihan Data

# Fungsi parsing tiap kabupaten/kota
parse_pdrb_daerah <- function(res, daerah) {
  if (is.null(res) || is.null(res$datacontent)) return(NULL)
  vervar_df <- res$vervar %>% mutate(val = as.character(val))
  var_val <- as.character(res$var$val)
  turvar_val <- as.character(res$turvar$val)
  tahun_val <- as.character(res$tahun$val)
  turtahun_val <- as.character(res$turtahun$val)

  key_combos <- expand.grid(vervar = vervar_df$val, var = var_val,
                            turvar = turvar_val, tahun = tahun_val,
                            turtahun = turtahun_val, stringsAsFactors = FALSE) %>%
    mutate(key = paste0(vervar, var, turvar, tahun, turtahun))
  
  df_raw <- tibble(key = names(res$datacontent), nilai = unlist(res$datacontent))
  df_final <- key_combos %>%
    left_join(df_raw, by = "key") %>%
    left_join(vervar_df, by = c("vervar" = "val")) %>%
    transmute(daerah, lapangan_usaha = label, nilai = as.numeric(nilai))
  return(df_final)
}

# Eksekusi parsing
pdrb_kabkota <- imap_dfr(raw_list, parse_pdrb_daerah)

🧭 Klasifikasi Lapangan Usaha & Pivot ke Format Wide

# Fungsi bersihkan label lapangan usaha
clean_lapangan_usaha <- function(label) {
  label_clean <- label %>%
    str_remove_all("<.*?>") %>%
    str_trim() %>% str_squish() %>% str_to_lower()
  
  case_when(
    str_detect(label_clean, "pertanian|kehutanan|perikanan") ~ "A. Pertanian, Kehutanan, dan Perikanan",
    str_detect(label_clean, "pertambangan") ~ "B. Pertambangan dan Penggalian",
    str_detect(label_clean, "industri pengolahan") ~ "C. Industri Pengolahan",
    str_detect(label_clean, "listrik") ~ "D. Pengadaan Listrik dan Gas",
    str_detect(label_clean, "air|sampah|limbah") ~ "E. Pengadaan Air, Pengelolaan Sampah, Limbah, dan Daur Ulang",
    str_detect(label_clean, "konstruksi") ~ "F. Konstruksi",
    str_detect(label_clean, "perdagangan") ~ "G. Perdagangan Besar dan Eceran; Reparasi Mobil dan Sepeda Motor",
    str_detect(label_clean, "transportasi") ~ "H. Transportasi dan Pergudangan",
    str_detect(label_clean, "akomodasi|makan minum") ~ "I. Penyediaan Akomodasi dan Makan Minum",
    str_detect(label_clean, "informasi|komunikasi") ~ "J. Informasi dan Komunikasi",
    str_detect(label_clean, "keuangan|asuransi") ~ "K. Jasa Keuangan dan Asuransi",
    str_detect(label_clean, "real estat") ~ "L. Real Estat",
    str_detect(label_clean, "perusahaan") ~ "M,N. Jasa Perusahaan",
    str_detect(label_clean, "administrasi|pemerintahan|jaminan sosial") ~ "O. Administrasi Pemerintahan, Pertahanan, dan Jaminan Sosial Wajib",
    str_detect(label_clean, "pendidikan") ~ "P. Jasa Pendidikan",
    str_detect(label_clean, "kesehatan") ~ "Q. Jasa Kesehatan dan Kegiatan Sosial",
    str_detect(label_clean, "lainnya") ~ "R,S,T,U. Jasa Lainnya",
    str_detect(label_clean, "pdrb|produk domestik regional bruto") ~ "Total PDRB",
    TRUE ~ NA_character_
  )
}

pdrb_clean <- pdrb_kabkota %>%
  mutate(kategori = clean_lapangan_usaha(lapangan_usaha)) %>%
  filter(!is.na(kategori)) %>%
  group_by(daerah, kategori) %>%
  summarise(nilai = sum(nilai, na.rm = TRUE), .groups = "drop") %>%
  pivot_wider(names_from = kategori, values_from = nilai, values_fill = 0)

pdrb_clean <- pdrb_clean %>%
  mutate(across(
    where(is.numeric),
    ~ if_else(daerah == "Bangka Tengah", .x / 1000, .x)
  ))

pdrb_scaled <- pdrb_clean %>%
  group_by(daerah) %>%
  ungroup() %>%
  mutate(across(where(is.numeric), scale))

🔢 Klasterisasi K-Means

# Standarisasi agar tiap variabel setara
data_scaled <- scale(select(pdrb_clean, -daerah))

# Menentukan jumlah cluster optimal
max_k <- min(10, nrow(data_scaled) - 1) # aman untuk data kecil

# --- (a) Elbow Method ---
set.seed(123)
fviz_elbow <- fviz_nbclust(data_scaled, kmeans, method = "wss", k.max = max_k)
elbow_data <- fviz_elbow$data
k_elbow <- which(diff(diff(elbow_data$y)) == min(diff(diff(elbow_data$y)))) + 1
if (length(k_elbow) == 0 || is.na(k_elbow)) k_elbow <- 2

# --- (b) Silhouette Method ---
set.seed(123)
sil_result <- fviz_nbclust(data_scaled, kmeans, method = "silhouette", k.max = max_k)
k_silhouette <- sil_result$data$clusters[which.max(sil_result$data$y)] %>% as.numeric()

# --- (c) Gap Statistic ---
set.seed(123)
gap_stat <- clusGap(data_scaled, FUN = kmeans, nstart = 25, K.max = max_k, B = 100)
k_gap <- maxSE(gap_stat$Tab[, "gap"], gap_stat$Tab[, "SE.sim"], method = "firstSEmax")

# === Ringkasan Hasil ===
cat("📊 K Optimal (Elbow):", k_elbow, "\n")
## 📊 K Optimal (Elbow): 4
cat("📊 K Optimal (Silhouette):", k_silhouette, "\n")
## 📊 K Optimal (Silhouette): 2
cat("📊 K Optimal (Gap Statistic):", k_gap, "\n")
## 📊 K Optimal (Gap Statistic): 1
# Pilih K Final (prioritas: Silhouette → Gap → Elbow)
k_optimal <- if (!is.na(k_silhouette)) k_silhouette else if (!is.na(k_gap)) k_gap else k_elbow
cat("\n✅ Jumlah Cluster Final:", k_optimal, "\n")
## 
## ✅ Jumlah Cluster Final: 2

Penjelasan Fungsi:

fviz_nbclust(…, method=“wss”) → mencari titik “tekukan” (Elbow) antara jumlah cluster & total variasi.

fviz_nbclust(…, method=“silhouette”) → mengukur seberapa baik tiap observasi cocok dalam cluster-nya.

clusGap() → menghitung Gap Statistic (pembeda signifikan antar cluster).

Plot Penentuan K Optimal

p1 <- fviz_nbclust(data_scaled, kmeans, method = "wss", k.max = max_k) +
  geom_vline(xintercept = k_elbow, linetype = "dashed", color = "#E4572E") +
  annotate("text", x = k_elbow, y = max(elbow_data$y), label = paste("K =", k_elbow),
           vjust = -1, color = "#E4572E", size = 3.5, fontface = "bold") +
  ggtitle("Elbow Method (WSS)") +
  theme_minimal(base_size = 11)

p2 <- fviz_nbclust(data_scaled, kmeans, method = "silhouette", k.max = max_k) +
  geom_vline(xintercept = k_silhouette, linetype = "dashed", color = "#4CAF50") +
  annotate("text", x = k_silhouette, y = 0, label = paste("K =", k_silhouette),
           vjust = -1, color = "#4CAF50", size = 3.5, fontface = "bold") +
  ggtitle("Silhouette Method") +
  theme_minimal(base_size = 11)

p3 <- fviz_gap_stat(gap_stat) +
  geom_vline(xintercept = k_gap, linetype = "dashed", color = "yellow") +
  annotate("text", x = k_gap, y = max(gap_stat$Tab[,"gap"]), label = paste("K =", k_gap),
           vjust = -1, color = "yellow", size = 3.5, fontface = "bold") +
  ggtitle("Gap Statistic Method") +
  theme_minimal(base_size = 11)

(p1 | p2 | p3) +
  plot_annotation(
    title = "Penentuan Jumlah Cluster Optimal",
    subtitle = "Metode: Elbow, Silhouette, dan Gap Statistic"
  )

🧩 Hasil Akhir K-Means

set.seed(123)
km <- kmeans(data_scaled, centers = k_optimal, nstart = 25)
pdrb_scaled$cluster <- km$cluster

pdrb_clean$cluster <- km$cluster
fviz_cluster(km, data = data_scaled, geom = "point", ellipse.type = "norm") +
  labs(title = paste("K-Means Clustering (K =", k_optimal, ")"))
## Too few points to calculate an ellipse

pdrb_scaled %>%
  group_by(cluster) %>%
  summarise(across(where(is.numeric), mean, na.rm = TRUE)) %>%
  pivot_longer(cols = -cluster, names_to = "Sektor", values_to = "Rata2") %>%
  group_by(cluster) %>%
  slice_max(Rata2, n = 1, with_ties = FALSE) %>%
  ungroup()
faktor_dominan <- pdrb_scaled %>%
  group_by(cluster) %>%
  summarise(across(where(is.numeric), mean, na.rm = TRUE)) %>%
  pivot_longer(cols = -cluster, names_to = "Sektor", values_to = "Rata2") %>%
  group_by(cluster) %>%
  slice_max(Rata2, n = 1, with_ties = FALSE) %>%
  ungroup()

pastel_colors <- c("#FFB3BA", "#BAFFC9", "#FFF5BA", "#D5BAFF", "#FFE4BA", "#B4E1FF", "#D1BAFF", "#BAFFD5", "#FFD1BA", "#F5FFBA")

ggplot(faktor_dominan, aes(x = factor(cluster), y = Rata2, fill = Sektor)) +
  geom_col(width = 0.7, show.legend = FALSE) +
  geom_text(aes(label = Sektor), vjust = -0.8, size = 3.5, fontface = "bold") +
  scale_fill_manual(values = pastel_colors) +
  labs(
    title = "Faktor Dominan Tiap Cluster",
    x = "Cluster",
    y = "Rata-rata Z-score"
  ) +
  theme_minimal(base_size = 12) +
  theme(
    plot.title = element_text(face = "bold", hjust = 0.5, size = 14)
  )

🗺️ Peta Klasterisasi

sf_use_s2(FALSE)
## Spherical geometry (s2) switched off
map <- st_read(dsn = params$shp_path, quiet = TRUE) %>%
  rename(daerah = WADMKK) %>%
  mutate(daerah = str_to_title(daerah))

map_clustered <- map %>%
  left_join(pdrb_clean %>% mutate(cluster = factor(cluster)), by = "daerah")

ggplot(map_clustered) +
  geom_sf(aes(fill = cluster), color = "white", size = 0.3) +
  labs(title = "Peta Klasterisasi Ekonomi Bangka Belitung") +
  theme_void()

# Buat data frame PCA
pca_res <- prcomp(data_scaled, scale. = FALSE)
pca_df <- data.frame(pca_res$x[, 1:2]) %>%
  mutate(daerah = pdrb_scaled$daerah, cluster = factor(km$cluster))

ggplot(pca_df, aes(x = PC1, y = PC2, color = cluster)) +
  geom_point(size = 3) +
  geom_text(aes(label = daerah), size = 3, vjust = -0.6, check_overlap = TRUE) +
  stat_ellipse(aes(fill = cluster), geom = "polygon", alpha = 0.12) +
  labs(title = paste("PCA - Proyeksi 2D (K =", k_optimal, ")")) +
  theme_minimal()
## Too few points to calculate an ellipse

🧠 Interpretasi Hasil ⚙️ Ringkasan Hasil

Jumlah cluster: 2

Ukuran cluster:

Cluster 1: 1 kabupaten/kota

Cluster 2: 6 kabupaten/kota

Proporsi variasi antar cluster: r round(km\(betweenss / km\)totss * 100, 1)%

→ Artinya, sekitar r round(km\(betweenss / km\)totss * 100, 1)% variasi data dijelaskan oleh dua cluster ini — pemisahan cukup moderat dan jelas terlihat.

🧩 Interpretasi Tiap Cluster 🌆 Cluster 1 — Ekonomi Jasa dan Perkotaan

Ciri:

Z-score sangat tinggi di sektor tersier: perdagangan, jasa keuangan, real estat, pendidikan, kesehatan.

Nilai negatif di sektor primer: pertanian, pertambangan.

Makna:

Mewakili daerah service-oriented dan modern, pusat aktivitas ekonomi jasa dan administrasi.

Kemungkinan besar: Kota Pangkalpinang.

🌾 Cluster 2 — Ekonomi Primer dan Tradisional

Ciri:

Z-score sedikit positif di sektor pertanian dan pertambangan.

Negatif pada jasa dan industri tersier.

Makna:

Menggambarkan kabupaten dengan basis ekonomi primer dan sekunder (agraris dan tambang).

Kemungkinan besar: Kabupaten Bangka, Bangka Tengah, Bangka Barat, Bangka Selatan, Belitung, Belitung Timur.

🧭 Kesimpulan Umum Aspek Cluster 1 Cluster 2 Jumlah daerah 1 6 Tipe ekonomi Jasa dan perkotaan Primer & sekunder (agraris-tambang) Contoh daerah Kota Pangkalpinang Kabupaten Bangka, Belitung, dkk Ciri ekonomi Terdiversifikasi, modern, tersier dominan Tradisional, berbasis SDA Arah kebijakan Penguatan sektor jasa, digitalisasi Diversifikasi ke industri & jasa

💡 Kesimpulan ringkas: Hasil K-Means menunjukkan dualisme struktur ekonomi di Provinsi Kep. Bangka Belitung — kota utama (Pangkalpinang) berorientasi jasa, sedangkan kabupaten lainnya masih berbasis sumber daya alam.

💾 Ekspor Hasil write.csv(pdrb_clean, file.path(params$out_dir, “hasil_cluster.csv”), row.names = FALSE)

✅ Analisis selesai. Silakan buka output HTML untuk eksplorasi visualisasi dan hasil interpretasi.