Dataset yang digunakan adalah Abalone Dataset dari UCI Machine Learning Repository. Abalone adalah sejenis siput laut yang umurnya dapat ditentukan dari jumlah cincin (rings) pada cangkangnya — mirip seperti menghitung lingkaran pohon.
Dataset ini memiliki 4.177 observasi dengan 9 variabel:
| Variabel | Keterangan |
|---|---|
| Sex | Jenis kelamin (M/F/I) |
| Length | Panjang cangkang (mm) |
| Diameter | Diameter cangkang (mm) |
| Height | Tinggi cangkang (mm) |
| Whole_weight | Berat total (gram) |
| Shucked_weight | Berat daging (gram) |
| Viscera_weight | Berat isi perut setelah dikeringkan (gram) |
| Shell_weight | Berat cangkang setelah dikeringkan (gram) |
| Rings | Jumlah cincin -> indikator umur |
Menerapkan 2 metode clustering (K-Means dan PAM/K-Medoids) pada data Abalone, kemudian memvalidasi hasilnya menggunakan tiga jenis kriteria validasi:
library(factoextra) # visualisasi clustering
library(ggplot2) # plotting
library(cluster) # algoritma PAM dan silhouette
library(clusterCrit) # Rand Index (extCriteria)
library(fpc) # Shadow Value (cluster.stats)
library(kmed) # distNumeric, clustboot, consensusmatrix, clustheatmapurl <- "https://archive.ics.uci.edu/ml/machine-learning-databases/abalone/abalone.data"
abalone <- read.csv(url, header = FALSE)
colnames(abalone) <- c("Sex", "Length", "Diameter", "Height", "Whole_weight",
"Shucked_weight", "Viscera_weight", "Shell_weight", "Rings")
head(abalone)## Sex Length Diameter Height Whole_weight Shucked_weight Viscera_weight
## 1 M 0.455 0.365 0.095 0.5140 0.2245 0.1010
## 2 M 0.350 0.265 0.090 0.2255 0.0995 0.0485
## 3 F 0.530 0.420 0.135 0.6770 0.2565 0.1415
## 4 M 0.440 0.365 0.125 0.5160 0.2155 0.1140
## 5 I 0.330 0.255 0.080 0.2050 0.0895 0.0395
## 6 I 0.425 0.300 0.095 0.3515 0.1410 0.0775
## Shell_weight Rings
## 1 0.150 15
## 2 0.070 7
## 3 0.210 9
## 4 0.155 10
## 5 0.055 7
## 6 0.120 8
## [1] 4177 9
## Length Diameter Height Whole_weight
## Min. :0.075 Min. :0.0550 Min. :0.0000 Min. :0.0020
## 1st Qu.:0.450 1st Qu.:0.3500 1st Qu.:0.1150 1st Qu.:0.4415
## Median :0.545 Median :0.4250 Median :0.1400 Median :0.7995
## Mean :0.524 Mean :0.4079 Mean :0.1395 Mean :0.8287
## 3rd Qu.:0.615 3rd Qu.:0.4800 3rd Qu.:0.1650 3rd Qu.:1.1530
## Max. :0.815 Max. :0.6500 Max. :1.1300 Max. :2.8255
## Shucked_weight Viscera_weight Shell_weight Rings
## Min. :0.0010 Min. :0.0005 Min. :0.0015 Min. : 1.000
## 1st Qu.:0.1860 1st Qu.:0.0935 1st Qu.:0.1300 1st Qu.: 8.000
## Median :0.3360 Median :0.1710 Median :0.2340 Median : 9.000
## Mean :0.3594 Mean :0.1806 Mean :0.2388 Mean : 9.934
## 3rd Qu.:0.5020 3rd Qu.:0.2530 3rd Qu.:0.3290 3rd Qu.:11.000
## Max. :1.4880 Max. :0.7600 Max. :1.0050 Max. :29.000
Clustering berbasis jarak (seperti K-Means dan PAM) sangat sensitif terhadap perbedaan skala antar variabel. Misalnya, variabel berat dalam gram bisa memiliki nilai jauh lebih besar dibanding panjang dalam mm. Jika tidak di-scale, variabel dengan skala besar akan mendominasi perhitungan jarak.
Oleh karena itu, kita lakukan standardisasi (z-score
scaling) menggunakan fungsi scale(), sehingga
setiap variabel memiliki mean = 0 dan standar deviasi = 1.
Untuk efisiensi komputasi (terutama saat menghitung matriks jarak dan bootstrap), kita ambil sampel 1.000 baris secara acak.
Untuk validasi eksternal, kita butuh label “kelas yang sebenarnya”. Karena Abalone tidak punya label kelas, kita gunakan kolom Rings sebagai proksi umur, lalu dibagi menjadi 3 kelompok:
rings_sample <- abalone$Rings[idx]
true_label <- cut(rings_sample, breaks = c(0, 8, 11, Inf), labels = c(1, 2, 3))
true_label <- as.integer(true_label)
table(true_label)## true_label
## 1 2 3
## 350 424 226
K-Means bekerja dengan cara:
Kelebihan: Cepat dan sederhana
Kekurangan: Sensitif terhadap outlier karena
menggunakan rata-rata
set.seed(123)
kmeans_res <- kmeans(data_sample, centers = 3, nstart = 25)
kmeans_cluster <- kmeans_res$cluster
# Distribusi anggota tiap cluster
table(kmeans_cluster)## kmeans_cluster
## 1 2 3
## 246 320 434
PAM (Partitioning Around Medoids) mirip K-Means, tetapi:
pam_res <- pam(data_sample, k = 3)
pam_cluster <- pam_res$clustering
pam_medoids <- pam_res$id.med
# Distribusi anggota tiap cluster
table(pam_cluster)## pam_cluster
## 1 2 3
## 298 387 315
Karena data memiliki 7 dimensi, kita gunakan PCA (Principal
Component Analysis) untuk mereduksi ke 2 dimensi agar bisa
divisualisasikan. Fungsi fviz_cluster() melakukan ini
secara otomatis.
p1 <- fviz_cluster(kmeans_res, data = data_sample, geom = "point",
ellipse.type = "convex") +
theme_bw() +
labs(title = "Hasil Clustering dengan K-Means (k=3)")
print(p1)p2 <- fviz_cluster(pam_res, geom = "point", ellipse.type = "convex") +
theme_bw() +
labs(title = "Hasil Clustering dengan PAM/K-Medoids (k=3)")
print(p2)Validasi eksternal membandingkan hasil clustering dengan label kelas yang sudah diketahui (gold standard). Dalam kasus ini, kita gunakan pengelompokan umur Abalone berdasarkan Rings sebagai acuan.
Tabel kontingensi menunjukkan berapa banyak objek dari tiap true label yang masuk ke tiap cluster.
tab_kmeans <- table(kmeans_cluster, true_label)
tab_pam <- table(pam_cluster, true_label)
cat("=== Contingency Table K-Means ===\n")## === Contingency Table K-Means ===
## true_label
## kmeans_cluster 1 2 3
## 1 7 138 101
## 2 247 57 16
## 3 96 229 109
##
## === Contingency Table PAM ===
## true_label
## pam_cluster 1 2 3
## 1 234 50 14
## 2 101 191 95
## 3 15 183 117
Akurasi clustering dihitung dengan asumsi nilai diagonal tabel kontingensi adalah pencocokan yang optimal:
\[A = \sum_{i=1}^{k} \frac{n_{ii}}{n_{..}}\]
di mana \(n_{ii}\) adalah nilai diagonal baris ke-\(i\) kolom ke-\(i\), dan \(n_{..}\) adalah total observasi.
accuracy_kmeans <- sum(diag(tab_kmeans)) / sum(tab_kmeans)
accuracy_pam <- sum(diag(tab_pam)) / sum(tab_pam)
cat("Cluster Accuracy K-Means :", round(accuracy_kmeans, 4), "\n")## Cluster Accuracy K-Means : 0.173
## Cluster Accuracy PAM : 0.542
Purity mengukur seberapa homogen tiap cluster terhadap label aslinya. Untuk tiap cluster, diambil kelas mayoritas, lalu dijumlahkan:
\[P = \sum_{i=1}^{k} \frac{n_i}{n_{..}} \left( \max_{j \in K} \frac{n_{ij}}{n_i} \right)\]
Nilai mendekati 1 berarti setiap cluster didominasi oleh satu kelas → clustering bagus.
purity_kmeans <- sum(apply(tab_kmeans, 1, max)) / sum(tab_kmeans)
purity_pam <- sum(apply(tab_pam, 1, max)) / sum(tab_pam)
cat("Cluster Purity K-Means :", round(purity_kmeans, 4), "\n")## Cluster Purity K-Means : 0.614
## Cluster Purity PAM : 0.608
Rand Index mengukur proporsi pasang objek yang dikelompokkan konsisten antara hasil clustering dan true label.
\[RI = \frac{B}{B + D}\]
Nilai mendekati 1 → sangat konsisten dengan true label.
ri_kmeans <- extCriteria(as.integer(kmeans_cluster), as.integer(true_label), "Rand")
ri_pam <- extCriteria(as.integer(pam_cluster), as.integer(true_label), "Rand")
cat("Rand Index K-Means :", round(ri_kmeans$rand, 4), "\n")## Rand Index K-Means : 0.6297
## Rand Index PAM : 0.6296
Validasi internal tidak membutuhkan true label. Kualitas clustering dinilai dari struktur data itu sendiri — seberapa kompak isi tiap cluster dan seberapa jauh antar cluster.
Silhouette value untuk tiap objek \(i\) dihitung sebagai:
\[s(i) = \frac{b_i - a_i}{\max(a_i, b_i)}\]
di mana: - \(a_i\) = rata-rata jarak objek \(i\) ke semua objek di clusternya sendiri (kompakness) - \(b_i\) = rata-rata jarak objek \(i\) ke semua objek di cluster terdekat lainnya (separation)
Interpretasi: - Nilai mendekati +1 → objek berada di cluster yang tepat - Nilai mendekati 0 → objek berada di perbatasan dua cluster - Nilai mendekati −1 → objek mungkin salah cluster
dist_matrix <- dist(data_sample)
# Silhouette K-Means
sil_kmeans <- silhouette(kmeans_cluster, dist_matrix)
avg_sil_kmeans <- mean(sil_kmeans[, 3])
cat("Avg Silhouette Width K-Means :", round(avg_sil_kmeans, 4), "\n")## Avg Silhouette Width K-Means : 0.4505
# Silhouette PAM
sil_pam <- silhouette(pam_cluster, dist_matrix)
avg_sil_pam <- mean(sil_pam[, 3])
cat("Avg Silhouette Width PAM :", round(avg_sil_pam, 4), "\n")## Avg Silhouette Width PAM : 0.4411
Shadow value adalah ukuran yang mirip dengan silhouette, namun berbasis centroid/medoid (bukan rata-rata jarak ke semua anggota cluster). Dihitung sebagai:
\[sh(i) = \frac{2 \cdot d(i, c(i))}{d(i, c(i)) + d(i, \hat{c}(i))}\]
di mana \(c(i)\) adalah centroid terdekat dan \(\hat{c}(i)\) adalah centroid terdekat kedua.
Interpretasi kebalikan dari silhouette: - Nilai mendekati 0 → cluster terpisah dengan baik - Nilai mendekati 1 → pemisahan cluster buruk
shadowval_kmeans <- cluster.stats(dist_matrix, kmeans_cluster)
shadowval_pam <- cluster.stats(dist_matrix, pam_cluster)
cat("Shadow Value K-Means :", round(shadowval_kmeans$avg.silwidth, 4), "\n")## Shadow Value K-Means : 0.4505
## Shadow Value PAM : 0.4411
Validasi relatif menguji stabilitas hasil clustering. Ideanya: jika clustering benar-benar menemukan struktur yang ada di data, maka hasil clustering seharusnya konsisten walaupun datanya sedikit berubah (lewat bootstrap sampling).
Cara kerja: 1. Ambil sampel bootstrap dari data sebanyak 50 kali 2. Lakukan clustering pada tiap sampel 3. Hitung seberapa sering sepasang objek masuk ke cluster yang sama → disimpan dalam consensus matrix (n × n) 4. Visualisasikan dengan heatmap — jika terbentuk blok diagonal yang jelas → clustering stabil
num_mat <- as.matrix(data_sample)
mrwdist <- distNumeric(num_mat, num_mat)
# Fungsi PAM untuk clustboot
pamfunc <- function(x, nclust) {
res <- pam(as.dist(x), k = nclust)
return(res$clustering)
}
# Bootstrap 50 replikasi
set.seed(123)
pam_bootstrap <- clustboot(mrwdist, nclust = 3, nboot = 50, algorithm = pamfunc)
# Ordering dengan Ward linkage
wardorder <- function(x, nclust) {
res <- hclust(as.dist(x), method = "ward.D2")
member <- cutree(res, nclust)
return(member)
}
# Consensus matrix & heatmap
consensus_pam <- consensusmatrix(pam_bootstrap, nclust = 3, wardorder)
clustheatmap(consensus_pam, "Abalone data - PAM, ordered by Ward linkage")Blok diagonal yang jelas pada heatmap menunjukkan bahwa hasil clustering PAM stabil dan konsisten di berbagai bootstrap sample.
hasil <- data.frame(
Metode = c("K-Means", "PAM"),
Accuracy = round(c(accuracy_kmeans, accuracy_pam), 4),
Purity = round(c(purity_kmeans, purity_pam), 4),
Rand_Index = round(c(ri_kmeans$rand, ri_pam$rand), 4),
Avg_Silhouette = round(c(avg_sil_kmeans, avg_sil_pam), 4),
Shadow_Value = round(c(shadowval_kmeans$avg.silwidth, shadowval_pam$avg.silwidth), 4)
)
knitr::kable(hasil, caption = "Perbandingan Hasil Validasi K-Means vs PAM")| Metode | Accuracy | Purity | Rand_Index | Avg_Silhouette | Shadow_Value |
|---|---|---|---|---|---|
| K-Means | 0.173 | 0.614 | 0.6297 | 0.4505 | 0.4505 |
| PAM | 0.542 | 0.608 | 0.6296 | 0.4411 | 0.4411 |
# Skor gabungan (Accuracy, Purity, Rand, Silhouette → makin tinggi makin baik)
skor_kmeans <- mean(c(accuracy_kmeans, purity_kmeans, ri_kmeans$rand, avg_sil_kmeans))
skor_pam <- mean(c(accuracy_pam, purity_pam, ri_pam$rand, avg_sil_pam))
cat("Skor rata-rata K-Means :", round(skor_kmeans, 4), "\n")## Skor rata-rata K-Means : 0.4668
## Skor rata-rata PAM : 0.5552
## === KESIMPULAN ===
if (skor_pam > skor_kmeans) {
cat("PAM (K-Medoids) menghasilkan clustering yang lebih baik\n")
cat("berdasarkan rata-rata skor validasi eksternal dan internal.\n\n")
cat("PAM lebih cocok untuk data Abalone karena lebih robust\n")
cat("terhadap outlier dibanding K-Means.\n")
} else {
cat("K-Means menghasilkan clustering yang lebih baik\n")
cat("berdasarkan rata-rata skor validasi eksternal dan internal.\n\n")
cat("K-Means lebih efisien secara komputasi dan menghasilkan\n")
cat("cluster yang lebih kompak pada data Abalone ini.\n")
}## PAM (K-Medoids) menghasilkan clustering yang lebih baik
## berdasarkan rata-rata skor validasi eksternal dan internal.
##
## PAM lebih cocok untuk data Abalone karena lebih robust
## terhadap outlier dibanding K-Means.
Cara membaca hasil validasi:
| Metrik | Lebih baik jika… |
|---|---|
| Accuracy | Makin tinggi |
| Purity | Makin tinggi |
| Rand Index | Makin tinggi (maks = 1) |
| Avg Silhouette | Makin tinggi (maks = 1) |
| Shadow Value | Makin rendah (min = 0) |