# === Library utama ===
library(jsonlite) # Baca data JSON dari API
library(dplyr) # Manipulasi data
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(purrr) # Iterasi dan pemetaan fungsi
##
## Attaching package: 'purrr'
## The following object is masked from 'package:jsonlite':
##
## flatten
library(tidyr) # Ubah bentuk data
library(stringr) # Olah teks dan regex
library(ggplot2) # Visualisasi data
library(viridis) # Palet warna modern
## Loading required package: viridisLite
library(factoextra) # Visualisasi dan analisis clustering
## Welcome! Want to learn more? See two factoextra-related books at https://goo.gl/ve3WBa
library(cluster) # Analisis cluster lanjut
library(sf) # Data spasial (shapefile)
## Linking to GEOS 3.13.1, GDAL 3.11.0, PROJ 9.6.0; sf_use_s2() is TRUE
library(patchwork) # Gabungkan beberapa plot
library(grid) # Atur tata letak grafis
# === Daftar API PDRB per daerah (contoh tahun 2023/2024) ===
# Setiap elemen berisi URL API BPS untuk masing-masing kabupaten/kota di Provinsi Babel.
urls <- list(
'Bangka' = "https://webapi.bps.go.id/v1/api/list/model/data/lang/ind/domain/1901/var/45/th/110/key/dec6ad5937bd46f1da5da3dbbf15cd4d",
'Belitung' = "https://webapi.bps.go.id/v1/api/list/model/data/lang/ind/domain/1902/var/35/th/110/key/dec6ad5937bd46f1da5da3dbbf15cd4d",
'Bangka Barat' = "https://webapi.bps.go.id/v1/api/list/model/data/lang/ind/domain/1903/var/30/th/110/key/dec6ad5937bd46f1da5da3dbbf15cd4d",
'Bangka Tengah' = "https://webapi.bps.go.id/v1/api/list/model/data/lang/ind/domain/1904/var/46/th/110/key/dec6ad5937bd46f1da5da3dbbf15cd4d",
'Bangka Selatan' = "https://webapi.bps.go.id/v1/api/list/model/data/lang/ind/domain/1905/var/8/th/110/key/dec6ad5937bd46f1da5da3dbbf15cd4d",
'Belitung Timur' = "https://webapi.bps.go.id/v1/api/list/model/data/lang/ind/domain/1906/var/58/th/110/key/dec6ad5937bd46f1da5da3dbbf15cd4d",
'Kota Pangkal Pinang' = "https://webapi.bps.go.id/v1/api/list/model/data/lang/ind/domain/1971/var/48/th/110/key/dec6ad5937bd46f1da5da3dbbf15cd4d"
)
# === Fungsi untuk mengambil data JSON dari API BPS ===
# Fungsi ini:
# - Menampilkan nama daerah yang sedang diambil
# - Mengambil data dari URL menggunakan fromJSON()
# - Menangani error jika koneksi gagal atau format salah
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)
}
# === Mengambil semua data dari daftar URL ===
# Menggunakan imap() dari purrr agar bisa iterasi dengan nama daerah
# Hasil akhirnya berupa list berisi JSON mentah untuk tiap kabupaten/kota
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
# === Fungsi untuk parsing data PDRB per kabupaten/kota ===
# Tujuan:
# - Mengambil dan menyusun data PDRB dari hasil JSON API BPS
# - Menghubungkan kode variabel dengan label lapangan usaha
# - Menghasilkan data frame: daerah, lapangan_usaha, nilai
parse_pdrb_daerah <- function(res, daerah) {
# Abaikan jika data kosong
if (is.null(res) || is.null(res$datacontent)) return(NULL)
# Ambil komponen data dari struktur JSON
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)
# Buat kombinasi unik key berdasarkan struktur BPS API
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))
# Gabungkan nilai data dengan label variabel
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 untuk semua kabupaten/kota ===
# Fungsi imap_dfr() akan menjalankan parse_pdrb_daerah() untuk setiap elemen raw_list
# dan langsung menggabungkannya menjadi satu data frame besar.
pdrb_kabkota <- imap_dfr(raw_list, parse_pdrb_daerah)
# === Fungsi pembersihan label lapangan usaha ===
# Tujuan:
# - Membersihkan teks label dari karakter HTML
# - Menstandarkan format teks (huruf kecil, tanpa spasi berlebih)
# - Mengelompokkan ke dalam kategori lapangan usaha resmi (A–U)
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_
)
}
# === Membersihkan dan mengelompokkan data PDRB ===
# Langkah-langkah:
# 1. Bersihkan label menjadi kategori resmi
# 2. Hilangkan data tanpa kategori (NA)
# 3. Agregasi nilai per kategori per daerah
# 4. Ubah format ke bentuk wide (kolom = kategori)
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)
# === Penyesuaian satuan data tertentu ===
# Contoh: untuk Bangka Tengah, satuan nilainya 1000 kali lebih besar, jadi dibagi 1000
pdrb_clean <- pdrb_clean %>%
mutate(across(
where(is.numeric),
~ if_else(daerah == "Bangka Tengah", .x / 1000, .x)
))
# === Standardisasi (scaling) data untuk analisis cluster ===
# Tujuan: menormalkan nilai numerik agar tidak bias terhadap skala besar/kecil
# Setiap variabel diubah menjadi skala (mean = 0, sd = 1)
pdrb_scaled <- pdrb_clean %>%
group_by(daerah) %>%
ungroup() %>%
mutate(across(where(is.numeric), scale))
# === Standarisasi data ===
# Skala semua variabel numerik agar setara (mean = 0, sd = 1)
data_scaled <- scale(select(pdrb_clean, -daerah))
# === Tentukan jumlah cluster maksimum ===
# Batasi sampai 10 atau sesuai banyaknya observasi - 1
max_k <- min(10, nrow(data_scaled) - 1)
# --- (a) Elbow Method ---
# Cari titik "tekukan" grafik WSS (Within Sum of Squares)
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 # fallback
# --- (b) Silhouette Method ---
# Mengukur seberapa baik tiap observasi cocok dengan clusternya
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 ---
# Bandingkan variasi antar cluster dengan data acak
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 dari ketiga metode ===
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 jumlah cluster final ===
# Urutan prioritas: Silhouette → Gap → Elbow
k_optimal <- if (!is.na(k_silhouette)) k_silhouette else if (!is.na(k_gap)) k_gap else k_elbow
# === Visualisasi 3 Metode Penentuan Jumlah Cluster ===
# --- (1) Elbow Method ---
# Menampilkan grafik WSS dengan garis vertikal di titik 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)
# --- (2) Silhouette Method ---
# Menampilkan nilai rata-rata silhouette untuk tiap jumlah cluster
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)
# --- (3) Gap Statistic Method ---
# Menampilkan perbandingan variasi antar cluster dengan data acak
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)
## Warning: `aes_string()` was deprecated in ggplot2 3.0.0.
## ℹ Please use tidy evaluation idioms with `aes()`.
## ℹ See also `vignette("ggplot2-in-packages")` for more information.
## ℹ The deprecated feature was likely used in the factoextra package.
## Please report the issue at <https://github.com/kassambara/factoextra/issues>.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
# --- Gabungkan ketiga plot dalam satu tampilan ---
(p1 | p2 | p3) +
plot_annotation(
title = "Penentuan Jumlah Cluster Optimal",
subtitle = "Metode: Elbow, Silhouette, dan Gap Statistic"
)

# === Langkah Klasterisasi K-Means ===
# Menetapkan nilai acak awal agar hasil dapat direplikasi
set.seed(123)
# --- Proses K-Means ---
# data_scaled : data yang sudah distandarisasi
# centers : jumlah cluster optimal (hasil dari tahap sebelumnya)
# nstart : jumlah percobaan awal acak untuk mencari hasil terbaik
km <- kmeans(data_scaled, centers = k_optimal, nstart = 25)
# --- Menambahkan hasil cluster ke dataset utama ---
# Menyimpan nomor cluster tiap daerah ke dalam kolom baru "cluster"
pdrb_scaled$cluster <- km$cluster
# --- Menampilkan hasil ---
# Data sudah berisi kolom tambahan 'cluster' yang menunjukkan kelompok tiap daerah
pdrb_scaled
## # A tibble: 7 × 20
## daerah A. Pertanian, Kehuta…¹ B. Pertambangan dan …² C. Industri Pengolah…³
## <chr> <dbl> <dbl> <dbl>
## 1 Bangka 0.758 0.775 0.373
## 2 Bangka B… -0.164 0.376 1.99
## 3 Bangka S… 1.38 1.16 -1.05
## 4 Bangka T… -1.03 0.366 -0.189
## 5 Belitung 0.568 -0.754 -0.664
## 6 Belitung… -0.0522 -0.133 -0.549
## 7 Kota Pan… -1.46 -1.79 0.0936
## # ℹ abbreviated names: ¹`A. Pertanian, Kehutanan, dan Perikanan`[,1],
## # ²`B. Pertambangan dan Penggalian`[,1], ³`C. Industri Pengolahan`[,1]
## # ℹ 16 more variables: `D. Pengadaan Listrik dan Gas` <dbl[,1]>,
## # `E. Pengadaan Air, Pengelolaan Sampah, Limbah, dan Daur Ulang` <dbl[,1]>,
## # `F. Konstruksi` <dbl[,1]>,
## # `G. Perdagangan Besar dan Eceran; Reparasi Mobil dan Sepeda Motor` <dbl[,1]>,
## # `H. Transportasi dan Pergudangan` <dbl[,1]>, …
# === Menambahkan hasil cluster ke data asli ===
pdrb_clean$cluster <- km$cluster
# === Peta Klasterisasi Ekonomi Bangka Belitung ===
sf_use_s2(FALSE) # Matikan validasi geometri agar peta bisa dibaca tanpa error
## Spherical geometry (s2) switched off
map <- st_read(dsn = "C:/Users/LOQ/Downloads/babel_shp/babel_shp", quiet = TRUE) %>% # params$shp_path -> sesuaikan dengan tempat folder peta
rename(daerah = WADMKK) %>%
mutate(daerah = str_to_title(daerah))
# Gabungkan data peta dengan hasil klasterisasi
map_clustered <- map %>%
left_join(pdrb_clean %>% mutate(cluster = factor(cluster)), by = "daerah")
# Visualisasi peta
ggplot(map_clustered) +
geom_sf(aes(fill = cluster), color = "white", size = 0.3) +
labs(title = "Peta Klasterisasi Ekonomi Bangka Belitung") +
theme_void()

# === Visualisasi PCA (proyeksi 2D hasil klasterisasi) ===
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

# === Identifikasi Faktor Dominan Tiap Cluster ===
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) %>%
ungroup()
## Warning: There was 1 warning in `summarise()`.
## ℹ In argument: `across(where(is.numeric), mean, na.rm = TRUE)`.
## ℹ In group 1: `cluster = 1`.
## Caused by warning:
## ! The `...` argument of `across()` is deprecated as of dplyr 1.1.0.
## Supply arguments directly to `.fns` through an anonymous function instead.
##
## # Previously
## across(a:b, mean, na.rm = TRUE)
##
## # Now
## across(a:b, \(x) mean(x, na.rm = TRUE))
# === Visualisasi sektor dominan ===
# Konfigurasi warna pada plot
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))
