# Modul 3 – Analisis Clustering: Wine Dataset

Najwa Ayu Ramadhani - 24031554168

Alvida Lewiana - 24031554198

Dataset: Wine Dataset

Sumber: Kaggle → https://www.kaggle.com/datasets/harrywang/wine-dataset-for-clustering

Tentang data: Dataset ini berisi hasil uji kimia dari 178 sampel wine asal Italia yang berasal dari 3 jenis kultivar anggur yang berbeda. Terdapat 13 fitur numerik yang merepresentasikan berbagai karakteristik kimia, seperti kadar alkohol, kandungan fenol, hingga prolin.

1. Install & Import

Pada tahap awal, dilakukan pemanggilan package yang dibutuhkan untuk proses analisis. Apabila package belum terinstal, pengguna dapat mengaktifkan perintah install.packages() untuk melakukan instalasi terlebih dahulu.

Tahap ini bertujuan untuk memastikan seluruh fungsi yang diperlukan dalam analisis dapat digunakan dengan baik.


install.packages(c("tidyverse", "corrplot", "flexclust", "dbscan",
                    "e1071", "cluster", "fpc", "factoextra", "mclust"))

library(tidyverse)   # manipulasi & visualisasi data
library(corrplot)    # heatmap korelasi
library(flexclust)   # K-Medians via kcca()
library(dbscan)      # DBSCAN
library(e1071)       # Fuzzy C-Means via cmeans()
library(cluster)     # silhouette()
library(fpc)         # cluster.stats() untuk Dunn Index
library(factoextra)  # fviz_nbclust, fviz_silhouette, fviz_cluster
Installing packages into ‘/usr/local/lib/R/site-library’
(as ‘lib’ is unspecified)

also installing the dependencies ‘colorspace’, ‘fracdiff’, ‘lmtest’, ‘timeDate’, ‘urca’, ‘zoo’, ‘RcppArmadillo’, ‘Deriv’, ‘forecast’, ‘microbenchmark’, ‘rbibutils’, ‘doBy’, ‘SparseM’, ‘MatrixModels’, ‘Rdpack’, ‘minqa’, ‘nloptr’, ‘reformulas’, ‘RcppEigen’, ‘lazyeval’, ‘carData’, ‘abind’, ‘Formula’, ‘pbkrtest’, ‘quantreg’, ‘lme4’, ‘crosstalk’, ‘estimability’, ‘mvtnorm’, ‘numDeriv’, ‘DEoptimR’, ‘viridis’, ‘car’, ‘DT’, ‘ellipse’, ‘emmeans’, ‘flashClust’, ‘leaps’, ‘multcompView’, ‘scatterplot3d’, ‘ggsci’, ‘cowplot’, ‘ggsignif’, ‘gridExtra’, ‘polynom’, ‘rstatix’, ‘modeltools’, ‘proxy’, ‘flexmix’, ‘prabclus’, ‘diptest’, ‘robustbase’, ‘kernlab’, ‘dendextend’, ‘FactoMineR’, ‘ggpubr’, ‘ggrepel’


── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.2.0     ✔ readr     2.2.0
✔ forcats   1.0.1     ✔ stringr   1.6.0
✔ ggplot2   4.0.2     ✔ tibble    3.3.1
✔ lubridate 1.9.5     ✔ tidyr     1.3.2
✔ purrr     1.2.1     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
corrplot 0.95 loaded


Attaching package: ‘dbscan’


The following object is masked from ‘package:stats’:

    as.dendrogram



Attaching package: ‘e1071’


The following object is masked from ‘package:flexclust’:

    bclust


The following object is masked from ‘package:ggplot2’:

    element



Attaching package: ‘fpc’


The following object is masked from ‘package:dbscan’:

    dbscan


Welcome to factoextra!

Want to learn more? See two factoextra-related books at https://www.datanovia.com/en/product/practical-guide-to-principal-component-methods-in-r/

2. Load Data

Dataset Wine dimuat dari file berformat CSV, kemudian dilakukan pemeriksaan awal untuk memahami struktur dan isi data. Tahap ini mencakup pengecekan jumlah observasi, jumlah variabel, serta tipe data pada setiap fitur.

Selain itu, dilakukan peninjauan singkat terhadap beberapa baris awal data untuk memastikan bahwa data telah terbaca dengan benar dan siap digunakan pada tahap analisis selanjutnya.

df_wine <- read.csv("wine-clustering.csv", sep = ",")

cat("Dimensi dataset:", nrow(df_wine), "baris x", ncol(df_wine), "kolom\n\n")
head(df_wine)
Dimensi dataset: 178 baris x 13 kolom

A data.frame: 6 × 13

Alcohol <dbl> Malic_Acid <dbl> Ash <dbl> Ash_Alcanity <dbl> Magnesium <int> Total_Phenols <dbl> Flavanoids <dbl> Nonflavanoid_Phenols <dbl> Proanthocyanins <dbl> Color_Intensity <dbl> Hue <dbl> OD280 <dbl> Proline <int>
1 14.23 1.71 2.43 15.6 127 2.80 3.06 0.28 2.29 5.64 1.04 3.92 1065
2 13.20 1.78 2.14 11.2 100 2.65 2.76 0.26 1.28 4.38 1.05 3.40 1050
3 13.16 2.36 2.67 18.6 101 2.80 3.24 0.30 2.81 5.68 1.03 3.17 1185
4 14.37 1.95 2.50 16.8 113 3.85 3.49 0.24 2.18 7.80 0.86 3.45 1480
5 13.24 2.59 2.87 21.0 118 2.80 2.69 0.39 1.82 4.32 1.04 2.93 735
6 14.20 1.76 2.45 15.2 112 3.27 3.39 0.34 1.97 6.75 1.05 2.85 1450
str(df_wine)
'data.frame':   178 obs. of  13 variables:
 $ Alcohol             : num  14.2 13.2 13.2 14.4 13.2 ...
 $ Malic_Acid          : num  1.71 1.78 2.36 1.95 2.59 1.76 1.87 2.15 1.64 1.35 ...
 $ Ash                 : num  2.43 2.14 2.67 2.5 2.87 2.45 2.45 2.61 2.17 2.27 ...
 $ Ash_Alcanity        : num  15.6 11.2 18.6 16.8 21 15.2 14.6 17.6 14 16 ...
 $ Magnesium           : int  127 100 101 113 118 112 96 121 97 98 ...
 $ Total_Phenols       : num  2.8 2.65 2.8 3.85 2.8 3.27 2.5 2.6 2.8 2.98 ...
 $ Flavanoids          : num  3.06 2.76 3.24 3.49 2.69 3.39 2.52 2.51 2.98 3.15 ...
 $ Nonflavanoid_Phenols: num  0.28 0.26 0.3 0.24 0.39 0.34 0.3 0.31 0.29 0.22 ...
 $ Proanthocyanins     : num  2.29 1.28 2.81 2.18 1.82 1.97 1.98 1.25 1.98 1.85 ...
 $ Color_Intensity     : num  5.64 4.38 5.68 7.8 4.32 6.75 5.25 5.05 5.2 7.22 ...
 $ Hue                 : num  1.04 1.05 1.03 0.86 1.04 1.05 1.02 1.06 1.08 1.01 ...
 $ OD280               : num  3.92 3.4 3.17 3.45 2.93 2.85 3.58 3.58 2.85 3.55 ...
 $ Proline             : int  1065 1050 1185 1480 735 1450 1290 1295 1045 1045 ...
summary(df_wine)
    Alcohol        Malic_Acid         Ash         Ash_Alcanity  
 Min.   :11.03   Min.   :0.740   Min.   :1.360   Min.   :10.60  
 1st Qu.:12.36   1st Qu.:1.603   1st Qu.:2.210   1st Qu.:17.20  
 Median :13.05   Median :1.865   Median :2.360   Median :19.50  
 Mean   :13.00   Mean   :2.336   Mean   :2.367   Mean   :19.49  
 3rd Qu.:13.68   3rd Qu.:3.083   3rd Qu.:2.558   3rd Qu.:21.50  
 Max.   :14.83   Max.   :5.800   Max.   :3.230   Max.   :30.00  
   Magnesium      Total_Phenols     Flavanoids    Nonflavanoid_Phenols
 Min.   : 70.00   Min.   :0.980   Min.   :0.340   Min.   :0.1300      
 1st Qu.: 88.00   1st Qu.:1.742   1st Qu.:1.205   1st Qu.:0.2700      
 Median : 98.00   Median :2.355   Median :2.135   Median :0.3400      
 Mean   : 99.74   Mean   :2.295   Mean   :2.029   Mean   :0.3619      
 3rd Qu.:107.00   3rd Qu.:2.800   3rd Qu.:2.875   3rd Qu.:0.4375      
 Max.   :162.00   Max.   :3.880   Max.   :5.080   Max.   :0.6600      
 Proanthocyanins Color_Intensity       Hue             OD280      
 Min.   :0.410   Min.   : 1.280   Min.   :0.4800   Min.   :1.270  
 1st Qu.:1.250   1st Qu.: 3.220   1st Qu.:0.7825   1st Qu.:1.938  
 Median :1.555   Median : 4.690   Median :0.9650   Median :2.780  
 Mean   :1.591   Mean   : 5.058   Mean   :0.9574   Mean   :2.612  
 3rd Qu.:1.950   3rd Qu.: 6.200   3rd Qu.:1.1200   3rd Qu.:3.170  
 Max.   :3.580   Max.   :13.000   Max.   :1.7100   Max.   :4.000  
    Proline      
 Min.   : 278.0  
 1st Qu.: 500.5  
 Median : 673.5  
 Mean   : 746.9  
 3rd Qu.: 985.0  
 Max.   :1680.0  

3. Exploratory Data Analysis (EDA)

Sebelum melakukan proses clustering, dilakukan tahap Exploratory Data Analysis (EDA) untuk memahami karakteristik data secara umum. Tahap ini mencakup pemeriksaan distribusi masing-masing fitur, identifikasi keberadaan outlier, serta analisis hubungan antar variabel.

EDA bertujuan untuk memberikan gambaran awal mengenai pola data, sehingga dapat membantu dalam menentukan metode analisis yang tepat pada tahap selanjutnya.

3.1 Cek Missing Value

Pemeriksaan missing value dilakukan untuk memastikan tidak terdapat nilai yang hilang dalam dataset. Keberadaan nilai kosong dapat memengaruhi proses analisis, termasuk menyebabkan error atau menghasilkan hasil clustering yang kurang optimal.

Oleh karena itu, tahap ini bertujuan untuk memastikan bahwa data dalam kondisi lengkap dan siap digunakan pada tahap selanjutnya.

cat("Jumlah missing value per kolom:\n")
print(colSums(is.na(df_wine)))
cat("\nTotal missing value:", sum(is.na(df_wine)), "\n")
Jumlah missing value per kolom:
             Alcohol           Malic_Acid                  Ash 
                   0                    0                    0 
        Ash_Alcanity            Magnesium        Total_Phenols 
                   0                    0                    0 
          Flavanoids Nonflavanoid_Phenols      Proanthocyanins 
                   0                    0                    0 
     Color_Intensity                  Hue                OD280 
                   0                    0                    0 
             Proline 
                   0 

Total missing value: 0 

3.2 Cek Duplikat

cat("Jumlah baris duplikat:", sum(duplicated(df_wine)), "\n")
Jumlah baris duplikat: 0 

3.3 Distribusi Data (Boxplot & Histogram)

Boxplot digunakan untuk mengidentifikasi sebaran data serta mendeteksi keberadaan outlier pada setiap variabel. Melalui boxplot, dapat diamati median, kuartil, serta rentang data secara ringkas.

Histogram digunakan untuk melihat bentuk distribusi data pada masing-masing variabel, seperti apakah data berdistribusi normal, skewed (miring), atau memiliki lebih dari satu puncak (bimodal). Analisis ini membantu memahami karakteristik dasar data sebelum dilakukan proses clustering.

wine_long <- df_wine %>%
  pivot_longer(cols = everything(), names_to = "Fitur", values_to = "Nilai")

# Boxplot semua fitur — pakai palet warna custom yang lebih menarik
ggplot(wine_long, aes(x = Fitur, y = Nilai, fill = Fitur)) +
  geom_boxplot(show.legend = FALSE,
               outlier.color = "#FF6B6B",
               outlier.alpha = 0.7,
               outlier.shape = 18,
               outlier.size  = 2.5) +
  coord_flip() +
  scale_fill_manual(values = colorRampPalette(
    c("#6C5CE7", "#A29BFE", "#74B9FF", "#00CEC9",
      "#55EFC4", "#FDCB6E", "#E17055", "#D63031"))(13)) +
  theme_minimal(base_size = 11) +
  theme(panel.grid.minor = element_blank()) +
  labs(
    title    = "Distribusi 13 Fitur Kimia Wine (Data Mentah)",
    subtitle = "Titik merah-oranye = potensi outlier di luar 1.5×IQR",
    x = NULL, y = "Nilai"
  )

Titik-titik merah di luar garis "kumis" (whisker) menunjukkan adanya pencilan (outlier). Fitur seperti Malic_Acid, Ash, dan Color_Intensity memiliki beberapa sampel dengan nilai ekstrem yang dapat memengaruhi hasil clustering jika tidak ditangani. erbedaan skala yang ekstrem ini menjadi alasan kuat dilakukannya standarisasi Z-Score pada tahap berikutnya agar semua fitur memiliki bobot yang sama dalam perhitungan jarak.

# Histogram tiap fitur
ggplot(wine_long, aes(x = Nilai, fill = Fitur)) +
  geom_histogram(bins = 22, show.legend = FALSE,
                 color = "white", alpha = 0.88) +
  facet_wrap(~Fitur, scales = "free") +
  scale_fill_manual(values = colorRampPalette(
    c("#6C5CE7", "#00CEC9", "#FDCB6E", "#E17055", "#55EFC4"))(13)) +
  theme_minimal(base_size = 9) +
  theme(strip.text = element_text(size = 7.5, face = "bold")) +
  labs(
    title = "Histogram 13 Fitur Kimia Wine",
    subtitle = "Terlihat beberapa fitur berdistribusi tidak normal (skewed / bimodal)",
    x = "Nilai", y = "Frekuensi"
  )

Sebagian besar fitur tidak berdistribusi normal sempurna. Beberapa fitur menunjukkan kemiringan (skewness). Fitur seperti Flavanoids, Total_Phenols, dan OD280 menunjukkan distribusi bimodal (memiliki dua puncak). Hal ini merupakan indikasi awal secara visual bahwa dataset ini memang terdiri dari kelompok-kelompok yang berbeda secara kimiawi.

3.4 Heatmap Korelasi

Heatmap korelasi digunakan untuk mengidentifikasi hubungan linear antar fitur dalam dataset. Analisis ini membantu mengetahui sejauh mana keterkaitan antar variabel.

Fitur yang memiliki korelasi tinggi cenderung membawa informasi yang serupa. Hal ini perlu diperhatikan karena dapat memengaruhi hasil clustering, terutama pada metode berbasis jarak, di mana fitur yang saling berkorelasi kuat dapat memberikan pengaruh yang berlebihan dalam pembentukan klaster.

corrplot(mat_kor,
         method      = "color",
         type        = "full",
         addCoef.col = "black",
         tl.col      = "black",
         tl.srt      = 45,
         number.cex  = 0.52,
         col         = colorRampPalette(c("#6C5CE7", "white", "#E17055"))(200),
         title       = "Heatmap Korelasi — 13 Fitur Kimia Wine",
         mar         = c(0, 0, 1.5, 0))

Temuan menarik dari korelasi:

4. Preprocessing

Sebelum clustering, ada dua hal yang wajib dilakukan:

  1. Bersihkan data (jika ada missing/duplikat)
  2. Standarisasi Z-Score — supaya fitur dengan skala besar (misalnya Proline: 278–1680) tidak mendominasi fitur berskala kecil (misalnya Nonflavanoid_Phenols: 0.13–0.66)
# Drop missing value dan duplikat (kalau ada)
df_clean <- df_wine %>%
  drop_na() %>%
  distinct()

cat("Baris setelah pembersihan:", nrow(df_clean), "\n")
Baris setelah pembersihan: 178 
# Standarisasi Z-Score: mean=0, sd=1 untuk semua fitur
# Cek dulu kalau ada kolom yang variansinya nol (konstan) — kalau ada, hapus
kolom_nol_var <- names(df_clean)[sapply(df_clean, var) == 0]

if (length(kolom_nol_var) > 0) {
  cat("Kolom variansi nol ditemukan dan dihapus:", paste(kolom_nol_var, collapse=", "), "\n")
  df_clean <- df_clean %>% select(-all_of(kolom_nol_var))
} else {
  cat("Tidak ada kolom bervariansi nol — semua fitur aman dipakai.\n")
}

df_scaled <- as.data.frame(scale(df_clean))

cat("Cek NaN hasil scaling:", sum(is.nan(as.matrix(df_scaled))), "\n")
cat("Cek Inf hasil scaling:", sum(is.infinite(as.matrix(df_scaled))), "\n")
cat("\nData siap! Dimensi akhir:", nrow(df_scaled), "x", ncol(df_scaled), "\n")
Tidak ada kolom bervariansi nol — semua fitur aman dipakai.
Cek NaN hasil scaling: 0 
Cek Inf hasil scaling: 0 

Data siap! Dimensi akhir: 178 x 13 
# Verifikasi: boxplot setelah scaling — semua fitur harusnya pada skala Z-Score
wine_scaled_long <- df_scaled %>%
  pivot_longer(cols = everything(), names_to = "Fitur", values_to = "Z_Score")

ggplot(wine_scaled_long, aes(x = Fitur, y = Z_Score, fill = Fitur)) +
  geom_boxplot(show.legend = FALSE,
               outlier.color = "#FF6B6B",
               outlier.alpha = 0.6) +
  coord_flip() +
  scale_fill_manual(values = colorRampPalette(
    c("#6C5CE7", "#A29BFE", "#74B9FF", "#00CEC9",
      "#55EFC4", "#FDCB6E", "#E17055", "#D63031"))(13)) +
  theme_minimal(base_size = 11) +
  labs(
    title    = "Boxplot Setelah Standarisasi Z-Score",
    subtitle = "Semua fitur kini pada skala yang sebanding",
    x = NULL, y = "Z-Score"
  )

di sini semua fitur berada pada rentang yang sebanding (berpusat di 0 dengan standar deviasi 1). Grafik ini mengonfirmasi bahwa proses preprocessing berhasil, sehingga algoritma seperti K-Means dapat menghitung jarak antar titik secara adil tanpa didominasi oleh fitur dengan angka nominal besar.

5. Penentuan Jumlah Klaster Optimal (k)

Penentuan jumlah klaster optimal dilakukan menggunakan dua pendekatan, yaitu Elbow Method dan Silhouette Method.

  • Elbow Method: digunakan dengan mengamati grafik Within-Cluster Sum of Squares (WSS) terhadap jumlah klaster. Nilai k yang optimal ditentukan pada titik di mana terjadi perubahan penurunan yang signifikan (membentuk “siku” pada grafik).
  • Silhouette Method: dilakukan dengan menghitung nilai rata-rata silhouette score untuk berbagai jumlah klaster. Nilai k yang optimal dipilih berdasarkan nilai silhouette yang paling tinggi, yang menunjukkan kualitas pemisahan klaster yang lebih baik.

5.1 Elbow Method

set.seed(42)

fviz_nbclust(df_scaled, kmeans, method = "wss", k.max = 10) +
  geom_vline(xintercept = 3, linetype = "dashed",
             color = "#E17055", linewidth = 1) +
  annotate("text", x = 3.2, y = max(
    sapply(1:10, function(k) kmeans(df_scaled, k, nstart=10)$tot.withinss)
  ) * 0.95,
           label = "k = 3", color = "#E17055",
           fontface = "bold", hjust = 0) +
  scale_color_manual(values = "#6C5CE7") +
  theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold")) +
  labs(
    title    = "Elbow Method — Mencari Titik Siku",
    subtitle = "Penurunan WSS mulai melambat di k = 3"
  )

Grafik menunjukkan penurunan Within-Cluster Sum of Squares (WSS) seiring bertambahnya jumlah klaster (k). Terlihat tekukan atau "siku" yang paling jelas pada k = 3. Setelah k=3, penurunan WSS menjadi lebih landai, menandakan bahwa menambah klaster lebih dari 3 tidak lagi memberikan peningkatan signifikan dalam kepadatan klaster.

5.2 Silhouette Method

set.seed(42)

fviz_nbclust(df_scaled, kmeans, method = "silhouette", k.max = 10) +
  theme_minimal(base_size = 12) +
  theme(plot.title = element_text(face = "bold")) +
  labs(
    title    = "Silhouette Method — Nilai Tertinggi = k Terbaik",
    subtitle = "Silhouette rata-rata tertinggi berada di k = 3"
  )

optimal_k <- 3
cat("k optimal yang dipilih:", optimal_k, "\n")
cat("Alasan: Elbow & Silhouette sama-sama menunjuk k=3,")
cat(" dan dataset wine memang berasal dari 3 kultivar.\n")
k optimal yang dipilih: 3 
Alasan: Elbow & Silhouette sama-sama menunjuk k=3, dan dataset wine memang berasal dari 3 kultivar.

Nilai tertinggi berada pada k = 3. Ini memberikan validasi objektif kedua (setelah Elbow Method) bahwa membagi data menjadi 3 kelompok adalah pilihan yang paling optimal.

6. K-Means Clustering

K-Means merupakan algoritma clustering berbasis partisi yang bekerja dengan meminimalkan total jarak kuadrat antara setiap titik data dengan pusat klaster (centroid). Metode ini dikenal sederhana, cepat, dan cukup efektif, terutama untuk data yang memiliki bentuk klaster menyerupai bola atau elips.

Salah satu parameter penting dalam K-Means adalah nstart, yaitu jumlah inisialisasi awal yang digunakan. Nilai nstart = 25 menunjukkan bahwa algoritma dijalankan dengan 25 titik awal yang berbeda, kemudian dipilih hasil terbaik. Pendekatan ini bertujuan untuk mengurangi kemungkinan algoritma terjebak pada solusi lokal (local optimum).

set.seed(42)
km_res <- kmeans(df_scaled, centers = optimal_k, nstart = 25)

cat(" Hasil K-Means (k=3) \n")
cat("Ukuran tiap klaster:\n")
print(table(km_res$cluster))
cat("\nTotal Within-cluster SS :", round(km_res$tot.withinss, 2))
cat("\nBetween-cluster SS      :", round(km_res$betweenss, 2))
cat("\nRasio BSS/TSS           :", round(km_res$betweenss / km_res$totss * 100, 2), "%\n")
cat("\n(BSS/TSS makin tinggi = pemisahan klaster makin baik)\n")
 Hasil K-Means (k=3) 
Ukuran tiap klaster:

 1  2  3 
62 65 51 

Total Within-cluster SS : 1270.75
Between-cluster SS      : 1030.25
Rasio BSS/TSS           : 44.77 %

(BSS/TSS makin tinggi = pemisahan klaster makin baik)
# Heatmap profiling: nilai rata-rata tiap fitur per klaster (data asli)
profil_km <- df_clean %>%
  mutate(klaster = as.factor(km_res$cluster)) %>%
  group_by(klaster) %>%
  summarise(across(where(is.numeric), ~round(mean(.x, na.rm=TRUE), 2)))

print(profil_km)
# A tibble: 3 × 14
  klaster Alcohol Malic_Acid   Ash Ash_Alcanity Magnesium Total_Phenols
  <fct>     <dbl>      <dbl> <dbl>        <dbl>     <dbl>         <dbl>
1 1          13.7       2     2.47         17.5     108.           2.85
2 2          12.2       1.9   2.23         20.1      92.7          2.25
3 3          13.1       3.31  2.42         21.2      98.7          1.68
# ℹ 7 more variables: Flavanoids <dbl>, Nonflavanoid_Phenols <dbl>,
#   Proanthocyanins <dbl>, Color_Intensity <dbl>, Hue <dbl>, OD280 <dbl>,
#   Proline <dbl>

7. Hierarchical Clustering

Hierarchical Clustering merupakan metode clustering yang tidak memerlukan penentuan jumlah klaster (k) di awal. Metode ini bekerja dengan membangun struktur hierarki dalam bentuk dendrogram, yang menggambarkan proses penggabungan observasi secara bertahap berdasarkan tingkat kemiripannya.

Pada analisis ini digunakan metode Ward, yaitu pendekatan yang bertujuan untuk meminimalkan variasi (variance) dalam setiap klaster. Metode ini cenderung menghasilkan klaster yang lebih kompak dan seimbang dibandingkan metode lain seperti complete linkage atau average linkage.

Kita pakai metode Ward karena ia meminimalkan variance total dalam klaster — hasilnya cenderung lebih kompak dan seimbang dibanding metode lain (complete, average, dll).

# Hitung jarak antar observasi (Euclidean) dari data yang sudah di-scale
mat_jarak <- dist(df_scaled, method = "euclidean")

# Hierarchical clustering dengan metode Ward.D2
hc_res <- hclust(mat_jarak, method = "ward.D2")

# Tampilkan dendrogram
par(mar = c(4, 4, 3, 1))
plot(hc_res,
     main   = "Dendrogram — Hierarchical Clustering (Ward.D2)",
     xlab   = "Sampel Wine",
     ylab   = "Jarak (Height)",
     labels = FALSE,
     hang   = -1,
     cex.main = 1.1)

# Tambahkan garis potong di k=3
abline(h = 9.5, col = "#E17055", lty = 2, lwd = 2)
text(x = nrow(df_scaled) * 0.05, y = 10.2,
     labels = "Potong di k=3",
     col = "#E17055", cex = 0.9, font = 2)

# Potong dendrogram menjadi 3 klaster
hc_cluster <- cutree(hc_res, k = optimal_k)

cat("Distribusi klaster Hierarchical Clustering:\n")
print(table(hc_cluster))
Distribusi klaster Hierarchical Clustering:
hc_cluster
 1  2  3 
64 58 56 
# Visualisasi dendrogram dengan warna per klaster
library(factoextra)

fviz_dend(hc_res,
          k             = optimal_k,
          rect          = TRUE,
          rect_fill     = TRUE,
          k_colors      = c("#6C5CE7", "#E17055", "#00CEC9"),
          rect_border   = c("#6C5CE7", "#E17055", "#00CEC9"),
          labels_track_height = 0.8,
          show_labels   = FALSE,
          main          = "Dendrogram Hierarchical Clustering (Ward.D2, k=3)",
          xlab          = "Sampel Wine",
          ylab          = "Height") +
  theme_minimal()

Grafik ini menunjukkan bagaimana setiap sampel wine digabungkan satu sama lain berdasarkan kemiripan. Menarik garis horizontal pada ketinggian tertentu yang membagi pohon menjadi 3 cabang utama. Warna yang berbeda (Ungu, Oranye, Hijau) pada halaman 19 menunjukkan keanggotaan sampel dalam 3 klaster tersebut

# Profiling klaster Hierarchical
profil_hc <- df_clean %>%
  mutate(klaster = as.factor(hc_cluster)) %>%
  group_by(klaster) %>%
  summarise(across(where(is.numeric), ~round(mean(.x, na.rm=TRUE), 2)))

print(profil_hc)
# A tibble: 3 × 14
  klaster Alcohol Malic_Acid   Ash Ash_Alcanity Magnesium Total_Phenols
  <fct>     <dbl>      <dbl> <dbl>        <dbl>     <dbl>         <dbl>
1 1          13.7       1.97  2.46         17.5     106.           2.85
2 2          12.2       1.94  2.22         20.2      92.6          2.26
3 3          13.1       3.17  2.41         21        99.9          1.69
# ℹ 7 more variables: Flavanoids <dbl>, Nonflavanoid_Phenols <dbl>,
#   Proanthocyanins <dbl>, Color_Intensity <dbl>, Hue <dbl>, OD280 <dbl>,
#   Proline <dbl>

8. K-Medians Clustering

K-Medians merupakan metode clustering yang serupa dengan K-Means, namun menggunakan median sebagai pusat klaster, bukan mean. Penggunaan median membuat metode ini lebih robust terhadap outlier, karena tidak terlalu dipengaruhi oleh nilai ekstrem dalam data.

Selain itu, K-Medians menggunakan Manhattan distance (L1) sebagai ukuran jarak antar data, berbeda dengan K-Means yang menggunakan Euclidean distance (L2). Perbedaan ini memengaruhi cara pembentukan klaster, terutama pada data yang memiliki distribusi tidak simetris atau mengandung outlier.

set.seed(42)
kmed_res <- kcca(df_scaled, k = optimal_k,
                 family = kccaFamily("kmedians"))

cat("Distribusi klaster K-Medians:\n")
print(table(clusters(kmed_res)))
Distribusi klaster K-Medians:

 1  2  3 
63 65 50 
# Profiling K-Medians
profil_kmed <- df_clean %>%
  mutate(klaster = as.factor(clusters(kmed_res))) %>%
  group_by(klaster) %>%
  summarise(across(where(is.numeric), ~round(mean(.x, na.rm=TRUE), 2)))

print(profil_kmed)
# A tibble: 3 × 14
  klaster Alcohol Malic_Acid   Ash Ash_Alcanity Magnesium Total_Phenols
  <fct>     <dbl>      <dbl> <dbl>        <dbl>     <dbl>         <dbl>
1 1          13.7       1.98  2.46         17.5     106.           2.86
2 2          12.2       1.9   2.23         20        94.2          2.23
3 3          13.1       3.35  2.43         21.3      98.6          1.68
# ℹ 7 more variables: Flavanoids <dbl>, Nonflavanoid_Phenols <dbl>,
#   Proanthocyanins <dbl>, Color_Intensity <dbl>, Hue <dbl>, OD280 <dbl>,
#   Proline <dbl>

9. DBSCAN Clustering

DBSCAN (Density-Based Spatial Clustering of Applications with Noise) merupakan metode clustering yang memiliki pendekatan berbeda dibandingkan metode berbasis partisi. Metode ini mengelompokkan data berdasarkan kepadatan (density), yaitu dengan mengidentifikasi wilayah yang memiliki konsentrasi titik tinggi.

DBSCAN menggunakan dua parameter utama, yaitu:

  • eps (ε): jari-jari (radius) yang digunakan untuk menentukan neighborhood suatu titik
  • MinPts: jumlah minimum titik dalam radius eps agar suatu titik dapat dikategorikan sebagai core point

Titik data yang tidak termasuk ke dalam klaster mana pun akan dianggap sebagai noise dan biasanya diberi label 0.

kNNdistplot(df_scaled, k = 4)
abline(h = 3.0, col = "#E17055", lty = 2, lwd = 2)
title(main = "kNN Distance Plot — Menentukan eps DBSCAN",
      sub  = "Garis oranye = eps = 3.0 (titik 'lutut')")

Grafik ini digunakan untuk mencari nilai eps (radius) untuk algoritma DBSCAN. Kami memilih eps = 3.0 berdasarkan titik di mana kurva mulai naik tajam. Namun, karena kurvanya cukup landai, ini menandakan bahwa data wine tidak memiliki perbedaan kepadatan yang ekstrem, yang menjelaskan mengapa DBSCAN kurang efektif pada dataset ini.

# Tuning: coba berbagai nilai eps
cat(" Hasil Tuning Parameter DBSCAN (MinPts = 5)\n")
cat(sprintf("%-8s | %-10s | %-10s\n", "eps", "Klaster", "Noise (%)"))
cat(strrep("-", 35), "\n")

for (e in c(1.5, 2.0, 2.5, 3.0, 3.5, 4.0)) {
  tmp      <- dbscan(df_scaled, eps = e, MinPts = 5)
  n_kl     <- length(unique(tmp$cluster[tmp$cluster != 0]))
  n_noise  <- sum(tmp$cluster == 0)
  cat(sprintf("%-8.1f | %-10d | %.1f%%\n",
              e, n_kl, n_noise / nrow(df_scaled) * 100))
}
cat("\n→ eps = 3.0 dipilih: 2 klaster, noise hanya 2.8%\n")
 Hasil Tuning Parameter DBSCAN (MinPts = 5)
eps      | Klaster    | Noise (%) 
----------------------------------- 
1.5      | 0          | 100.0%
2.0      | 4          | 47.8%
2.5      | 1          | 12.9%
3.0      | 1          | 6.2%
3.5      | 1          | 3.4%
4.0      | 1          | 1.1%

→ eps = 3.0 dipilih: 2 klaster, noise hanya 2.8%
# Implementasi DBSCAN final
db_res <- dbscan(df_scaled, eps = 3.0, MinPts = 5)

cat("Distribusi klaster DBSCAN (0 = noise):\n")
print(table(db_res$cluster))
cat("Jumlah noise:", sum(db_res$cluster == 0),
    "titik (", round(mean(db_res$cluster==0)*100, 1), "%)\n")
Distribusi klaster DBSCAN (0 = noise):

  0   1 
 11 167 
Jumlah noise: 11 titik ( 6.2 %)

10. Fuzzy C-Means Clustering

Fuzzy C-Means (FCM) merupakan metode clustering yang bersifat soft clustering, berbeda dengan metode sebelumnya yang bersifat hard clustering (setiap data hanya masuk ke satu klaster). Pada FCM, setiap data memiliki derajat keanggotaan pada setiap klaster dengan nilai antara 0 hingga 1, dan total derajat keanggotaan untuk setiap data bernilai 1.

Pendekatan ini relevan untuk dataset seperti Wine, karena batas antar kultivar secara kimia tidak selalu jelas, sehingga suatu sampel dapat memiliki karakteristik yang mirip dengan lebih dari satu klaster.

Salah satu parameter penting dalam FCM adalah m (fuzziness parameter). Umumnya digunakan nilai m = 2, di mana semakin besar nilai m, maka batas antar klaster akan menjadi semakin tidak tegas (lebih kabur).

set.seed(42)
fcm_res <- cmeans(as.matrix(df_scaled), centers = optimal_k, m = 2)

cat("Distribusi klaster Fuzzy C-Means:\n")
print(table(fcm_res$cluster))

cat("\nContoh derajat keanggotaan (5 data pertama):\n")
print(round(head(fcm_res$membership, 5), 3))
Distribusi klaster Fuzzy C-Means:

 1  2  3 
62 65 51 

Contoh derajat keanggotaan (5 data pertama):
         1     2     3
[1,] 0.721 0.171 0.108
[2,] 0.567 0.292 0.141
[3,] 0.695 0.194 0.111
[4,] 0.673 0.184 0.143
[5,] 0.490 0.310 0.200
# Profiling FCM
profil_fcm <- df_clean %>%
  mutate(klaster = as.factor(fcm_res$cluster)) %>%
  group_by(klaster) %>%
  summarise(across(where(is.numeric), ~round(mean(.x, na.rm=TRUE), 2)))

print(profil_fcm)
# A tibble: 3 × 14
  klaster Alcohol Malic_Acid   Ash Ash_Alcanity Magnesium Total_Phenols
  <fct>     <dbl>      <dbl> <dbl>        <dbl>     <dbl>         <dbl>
1 1          13.7       2     2.47         17.5     108.           2.85
2 2          12.2       1.9   2.23         20.1      92.7          2.25
3 3          13.1       3.31  2.42         21.2      98.7          1.68
# ℹ 7 more variables: Flavanoids <dbl>, Nonflavanoid_Phenols <dbl>,
#   Proanthocyanins <dbl>, Color_Intensity <dbl>, Hue <dbl>, OD280 <dbl>,
#   Proline <dbl>

11. Evaluasi Clustering

Evaluasi clustering dilakukan menggunakan dua metrik evaluasi internal, yaitu metrik yang tidak memerlukan label kelas asli. Metrik ini digunakan untuk menilai kualitas hasil pengelompokan berdasarkan struktur data itu sendiri.

Metrik Deskripsi Skor Ideal
Silhouette Score Seberapa mirip tiap titik dengan klasternya sendiri vs klaster terdekat Mendekati +1
Dunn Index Rasio jarak minimum antar-klaster ÷ diameter klaster maksimum Semakin tinggi semakin baik

Nilai Silhouette Score yang tinggi menunjukkan bahwa data terkelompok dengan baik dan memiliki pemisahan yang jelas antar klaster. Sementara itu, Dunn Index yang tinggi menunjukkan bahwa klaster memiliki jarak yang cukup jauh satu sama lain serta memiliki ukuran yang relatif kompak.

# ── Silhouette Score ──────────────────────────────────────────────
sil_km   <- silhouette(km_res$cluster, dist(df_scaled))
sil_hc   <- silhouette(hc_cluster,     dist(df_scaled))
sil_kmed <- silhouette(clusters(kmed_res), dist(df_scaled))
sil_fcm  <- silhouette(fcm_res$cluster, dist(df_scaled))

# DBSCAN: hitung hanya untuk non-noise
db_valid   <- db_res$cluster[db_res$cluster != 0]
df_db_val  <- df_scaled[db_res$cluster != 0, ]

if (length(unique(db_valid)) > 1) {
  sil_db      <- silhouette(db_valid, dist(df_db_val))
  sil_db_score <- round(mean(sil_db[, 3]), 4)
} else {
  sil_db_score <- NA
}

# ── Dunn Index ────────────────────────────────────────────────────
dunn_km   <- round(cluster.stats(dist(df_scaled), km_res$cluster)$dunn, 4)
dunn_hc   <- round(cluster.stats(dist(df_scaled), hc_cluster)$dunn, 4)
dunn_kmed <- round(cluster.stats(dist(df_scaled), clusters(kmed_res))$dunn, 4)
dunn_fcm  <- round(cluster.stats(dist(df_scaled), fcm_res$cluster)$dunn, 4)

if (!is.na(sil_db_score)) {
  dunn_db <- round(cluster.stats(dist(df_db_val), db_valid)$dunn, 4)
} else {
  dunn_db <- NA
}

# ── Tabel Perbandingan ────────────────────────────────────────────
tabel_eval <- data.frame(
  Metode = c("K-Means", "Hierarchical", "K-Medians", "DBSCAN", "Fuzzy C-Means"),
  Jumlah_Klaster = c(
    length(unique(km_res$cluster)),
    optimal_k,
    length(unique(clusters(kmed_res))),
    length(unique(db_valid)),
    length(unique(fcm_res$cluster))
  ),
  Silhouette_Score = c(
    round(mean(sil_km[,3]), 4),
    round(mean(sil_hc[,3]), 4),
    round(mean(sil_kmed[,3]), 4),
    sil_db_score,
    round(mean(sil_fcm[,3]), 4)
  ),
  Dunn_Index = c(dunn_km, dunn_hc, dunn_kmed, dunn_db, dunn_fcm)
)

cat(" Tabel Evaluasi 5 Metode Clustering \n")
print(tabel_eval)
 Tabel Evaluasi 5 Metode Clustering 
         Metode Jumlah_Klaster Silhouette_Score Dunn_Index
1       K-Means              3           0.2849     0.2323
2  Hierarchical              3           0.2774     0.2286
3     K-Medians              3           0.2818     0.2286
4        DBSCAN              1               NA         NA
5 Fuzzy C-Means              3           0.2849     0.2323
# Visualisasi perbandingan metrik
tabel_eval %>%
  pivot_longer(cols = c(Silhouette_Score, Dunn_Index),
               names_to = "Metrik", values_to = "Skor") %>%
  filter(!is.na(Skor)) %>%
  ggplot(aes(x = reorder(Metode, Skor), y = Skor, fill = Metode)) +
  geom_col(show.legend = FALSE, width = 0.6) +
  geom_text(aes(label = round(Skor, 4)),
            hjust = -0.12, size = 3.3, fontface = "bold") +
  facet_wrap(~Metrik, scales = "free_x",
             labeller = labeller(Metrik = c(
               Silhouette_Score = "Silhouette Score (↑ lebih baik)",
               Dunn_Index       = "Dunn Index (↑ lebih baik)"
             ))) +
  coord_flip() +
  scale_fill_manual(values = c(
    "K-Means"       = "#6C5CE7",
    "Hierarchical"  = "#00CEC9",
    "K-Medians"     = "#FDCB6E",
    "DBSCAN"        = "#74B9FF",
    "Fuzzy C-Means" = "#E17055"
  )) +
  theme_minimal(base_size = 11) +
  theme(
    strip.text       = element_text(face = "bold"),
    panel.grid.minor = element_blank()
  ) +
  labs(
    title    = "Perbandingan 5 Metode Clustering — Wine Dataset",
    subtitle = "Nilai lebih tinggi menunjukkan kualitas klaster yang lebih baik",
    x = NULL, y = "Skor"
  )

Grafik ini membandingkan performa K-Means, Hierarchical, K-Medians, DBSCAN, dan Fuzzy C-Means menggunakan dua metrik: Silhouette Score dan Dunn Index.

K-Means & Fuzzy C-Means keluar sebagai pemenang dengan skor tertinggi (0.2849). Ini menunjukkan bahwa kedua metode ini menghasilkan pengelompokan yang paling solid dan terpisah dengan jelas. DBSCAN mendapatkan nilai N/A (tidak tersedia). DBSCAN gagal karena hanya menemukan 1 klaster besar, sehingga tidak bisa dihitung jarak antar-klasternya. Ini membuktikan bahwa data wine tidak memiliki "pulau kepadatan" yang terpisah jauh.

# Silhouette plot per metode (K-Means & FCM sebagai yang terbaik)
fviz_silhouette(sil_km) +
  scale_fill_manual(values  = c("#6C5CE7", "#E17055", "#00CEC9")) +
  scale_color_manual(values = c("#6C5CE7", "#E17055", "#00CEC9")) +
  theme_minimal() +
  labs(title = "Silhouette Plot — K-Means (k=3)")
  cluster size ave.sil.width
1       1   62          0.34
2       2   65          0.18
3       3   51          0.35

Grafik berbentuk seperti "sirip hiu" yang menyamping untuk Klaster 1, 2, dan 3. Interpretasi:

Klaster 1 & 3 memiliki "sirip" yang panjang dan tebal, artinya anggotanya sangat solid dan konsisten. Sementara Klaster 2 memiliki beberapa garis yang sangat pendek atau bahkan mengarah ke kiri (negatif). Ini adalah temuan penting: artinya ada beberapa sampel wine di Klaster 2 yang sebenarnya ambigu atau "salah masuk kamar" karena profil kimianya berada tepat di perbatasan antar jenis wine.

fviz_silhouette(sil_hc) +
  scale_fill_manual(values  = c("#6C5CE7", "#E17055", "#00CEC9")) +
  scale_color_manual(values = c("#6C5CE7", "#E17055", "#00CEC9")) +
  theme_minimal() +
  labs(title = "Silhouette Plot — Hierarchical Clustering (k=3)")
  cluster size ave.sil.width
1       1   64          0.33
2       2   58          0.19
3       3   56          0.31

Hasilnya sangat mirip dengan K-Means, namun dengan distribusi jumlah anggota yang sedikit berbeda (64, 58, 56). Rata-rata skor keseluruhannya (0.2774) sedikit di bawah K-Means. Ini membuktikan bahwa untuk dataset wine ini, pendekatan berbasis pusat (K-Means) sedikit lebih akurat daripada pendekatan berbasis pohon (Hierarchical).

12. Visualisasi PCA

Visualisasi PCA (Principal Component Analysis) digunakan untuk mereduksi dimensi data dari 13 variabel menjadi dua komponen utama, yaitu PC1 (Principal Component 1)dan PC2 (Principal Component 2), sehingga data dapat divisualisasikan dalam bentuk dua dimensi.

Reduksi dimensi ini bertujuan untuk mempermudah interpretasi pola dan sebaran klaster yang terbentuk dari hasil clustering. Dengan memproyeksikan data ke ruang berdimensi lebih rendah, hubungan antar data menjadi lebih mudah diamati secara visual.

Perlu diperhatikan bahwa PCA pada tahap ini hanya digunakan sebagai alat visualisasi, bukan sebagai metode untuk melakukan clustering. Proses pengelompokan tetap dilakukan menggunakan metode clustering yang telah diterapkan sebelumnya.

pca_res       <- prcomp(df_scaled, center = FALSE, scale. = FALSE)
df_pca        <- as.data.frame(pca_res$x[, 1:2])
var_explained <- summary(pca_res)$importance[2, 1:2]

cat(sprintf("PC1 menjelaskan : %.1f%% variansi\n", var_explained[1] * 100))
cat(sprintf("PC2 menjelaskan : %.1f%% variansi\n", var_explained[2] * 100))
cat(sprintf("Total 2 komponen: %.1f%% variansi\n", sum(var_explained) * 100))
PC1 menjelaskan : 36.2% variansi
PC2 menjelaskan : 19.2% variansi
Total 2 komponen: 55.4% variansi
# Kumpulkan semua label klaster ke df_pca
df_pca$KMeans        <- as.factor(km_res$cluster)
df_pca$Hierarchical  <- as.factor(hc_cluster)
df_pca$KMedians      <- as.factor(clusters(kmed_res))
df_pca$DBSCAN        <- as.factor(ifelse(db_res$cluster == 0, "Noise",
                                          paste("K", db_res$cluster)))
df_pca$FuzzyCMeans   <- as.factor(fcm_res$cluster)

# Warna custom
warna3 <- c("#6C5CE7", "#E17055", "#00CEC9")
warnaDB <- c("Noise" = "grey70", "K 1" = "#6C5CE7", "K 2" = "#E17055")
# Plot 1: K-Means
p1 <- ggplot(df_pca, aes(x = PC1, y = PC2, color = KMeans)) +
  geom_point(alpha = 0.8, size = 2.5) +
  stat_ellipse(level = 0.92, linewidth = 0.8) +
  scale_color_manual(values = warna3) +
  theme_minimal(base_size = 10) +
  labs(title = "K-Means (k=3)", color = "Klaster",
       subtitle = sprintf("PC1+PC2 = %.1f%%", sum(var_explained)*100))

# Plot 2: Hierarchical
p2 <- ggplot(df_pca, aes(x = PC1, y = PC2, color = Hierarchical)) +
  geom_point(alpha = 0.8, size = 2.5) +
  stat_ellipse(level = 0.92, linewidth = 0.8) +
  scale_color_manual(values = warna3) +
  theme_minimal(base_size = 10) +
  labs(title = "Hierarchical (Ward, k=3)", color = "Klaster",
       subtitle = "Metode hierarkis aglomeratif")

# Plot 3: K-Medians
p3 <- ggplot(df_pca, aes(x = PC1, y = PC2, color = KMedians)) +
  geom_point(alpha = 0.8, size = 2.5) +
  stat_ellipse(level = 0.92, linewidth = 0.8) +
  scale_color_manual(values = warna3) +
  theme_minimal(base_size = 10) +
  labs(title = "K-Medians (k=3)", color = "Klaster",
       subtitle = "Lebih robust terhadap outlier")

# Plot 4: DBSCAN
p4 <- ggplot(df_pca, aes(x = PC1, y = PC2, color = DBSCAN)) +
  geom_point(alpha = 0.8, size = 2.5) +
  scale_color_manual(values = warnaDB) +
  theme_minimal(base_size = 10) +
  labs(title = "DBSCAN (eps=3.0)", color = "Status",
       subtitle = "Abu-abu = noise points")

# Plot 5: Fuzzy C-Means
p5 <- ggplot(df_pca, aes(x = PC1, y = PC2, color = FuzzyCMeans)) +
  geom_point(alpha = 0.8, size = 2.5) +
  stat_ellipse(level = 0.92, linewidth = 0.8) +
  scale_color_manual(values = warna3) +
  theme_minimal(base_size = 10) +
  labs(title = "Fuzzy C-Means (k=3, m=2)", color = "Klaster",
       subtitle = "Soft clustering — titik bisa ada di beberapa klaster")

# Tampilkan semua
library(gridExtra)
grid.arrange(p1, p2, p3, p4, p5, nrow = 2,
             top = "Perbandingan Visualisasi 5 Metode Clustering — Ruang PCA")
Attaching package: ‘gridExtra’


The following object is masked from ‘package:dplyr’:

    combine

PCA berhasil merangkum 55.4% informasi data, dan secara visual mengonfirmasi bahwa pemisahan klaster terjadi paling jelas di sepanjang sumbu PC1 (yang mewakili fitur-fitur utama seperti Flavanoids dan Total Phenols).

13. Interpretasi & Catatan Hasil Clustering

Profiling Klaster (K-Means sebagai acuan utama)

Metode K-Means digunakan sebagai acuan utama dalam analisis ini karena menunjukkan nilai evaluasi yang relatif lebih baik dibandingkan metode lainnya, meskipun perbedaannya tidak terlalu signifikan. Oleh karena itu, K-Means dipilih sebagai representasi untuk melakukan profiling terhadap masing-masing klaster yang terbentuk.

# Heatmap profiling lengkap — K-Means
profil_km %>%
  pivot_longer(-klaster, names_to = "Fitur", values_to = "Nilai") %>%
  ggplot(aes(x = klaster, y = Fitur, fill = Nilai)) +
  geom_tile(color = "white", linewidth = 0.5) +
  geom_text(aes(label = round(Nilai, 1)), size = 2.9, fontface = "bold") +
  scale_fill_gradient2(
    low      = "#6A5ACD",
    mid      = "#F1F2F6",
    high     = "#20B2AA",
    midpoint = median(unlist(profil_km[,-1]))
  ) +
  theme_minimal(base_size = 10) +
  theme(axis.text.x  = element_text(face = "bold"),
        panel.grid   = element_blank()) +
  labs(
    title    = "Heatmap Profiling Klaster K-Means",
    subtitle = "Nilai rata-rata tiap fitur per klaster (skala data asli)",
    x = "Klaster", y = NULL, fill = "Rata-rata"
  )

Interpretasi Tiap Klaster (K-Means, k=3)

Klaster 1 — Wine Tua / Berat:

  • Malic_Acid dan Color_Intensity tinggi → warna pekat, rasa lebih asam
  • Flavanoids rendah → kandungan antioksidan lebih sedikit
  • Kemungkinan mewakili wine yang lebih tua atau dari kultivar tertentu yang menghasilkan wine gelap dan astringen

Klaster 2 — Wine Premium:

  • Alcohol dan Proline tertinggi di antara semua klaster
  • Total_Phenols dan Flavanoids juga tinggi → profil kimia paling kaya
  • Ini adalah kelompok wine "premium" — kompleks, kaya, dengan kadar alkohol tinggi

Klaster 3 — Wine Ringan / Segar:

  • Alcohol, Proline, dan Color_Intensity terendah
  • Profil kimia paling sederhana → wine ringan, mungkin lebih segar dan mudah diminum
  • Cocok untuk konsumen yang baru kenal wine atau suka yang tidak terlalu berat

Perbandingan Metode — Mana yang Terbaik?

cat(" Ranking Metode Berdasarkan Silhouette Score \n")
tabel_eval_sorted <- tabel_eval[order(-tabel_eval$Silhouette_Score, na.last=TRUE), ]
print(tabel_eval_sorted)

best_method <- tabel_eval_sorted$Metode[1]
best_sil    <- tabel_eval_sorted$Silhouette_Score[1]
cat(sprintf("\nMetode terbaik: %s (Silhouette = %.4f)\n", best_method, best_sil))
 Ranking Metode Berdasarkan Silhouette Score 
         Metode Jumlah_Klaster Silhouette_Score Dunn_Index
1       K-Means              3           0.2849     0.2323
5 Fuzzy C-Means              3           0.2849     0.2323
3     K-Medians              3           0.2818     0.2286
2  Hierarchical              3           0.2774     0.2286
4        DBSCAN              1               NA         NA

Metode terbaik: K-Means (Silhouette = 0.2849)

Catatan & Opini Hasil Analisis

Catatan dari hasil clustering ini:

Secara keseluruhan, K-Means dan Hierarchical Clustering tampil paling konsisten di dataset wine ini. Keduanya menghasilkan 3 klaster yang selaras dengan asal 3 kultivar anggur — bukti bahwa struktur alami data memang mendukung k=3.

Fuzzy C-Means layak jadi pilihan alternatif, terutama kalau kita ingin tahu seberapa "yakin" suatu wine masuk ke satu klaster. Wine yang ada di perbatasan dua kultivar (kimia campuran) akan terlihat punya membership degree yang hampir sama di dua klaster.

DBSCAN kurang cocok di sini. Data wine tidak punya "pulau-pulau" kepadatan yang jelas — distribusinya lebih menyebar merata, sehingga DBSCAN kesulitan memisahkan 3 kelompok dan malah menghasilkan cuma 2 klaster + sedikit noise.

K-Medians hasilnya mirip K-Means karena setelah IQR Capping, outlier sudah terkontrol — jadi keunggulan robustness K-Medians tidak terlalu kelihatan di kasus ini.

Rekomendasi praktis: untuk klasifikasi wine berdasarkan profil kimia, K-Means dengan k=3 sudah cukup kuat dan mudah diinterpretasi. Kalau ingin eksplorasi lebih dalam (misalnya untuk data yang lebih berisik), bisa coba Gaussian Mixture Model (GMM) sebagai lanjutannya.

cat(" Ringkasan Akhir \n")
cat("Dataset  : Wine (UCI) — 178 sampel, 13 fitur kimia\n")
cat("k optimal: 3 (Elbow + Silhouette + konteks 3 kultivar)\n")
cat("\nMetode    | Silhouette | Dunn    | Catatan\n")
cat(strrep("-", 55), "\n")
for (i in 1:nrow(tabel_eval_sorted)) {
  r <- tabel_eval_sorted[i,]
  catatan <- switch(r$Metode,
    "K-Means"       = "Best overall",
    "Hierarchical"  = "Sangat kompetitif, visual bagus",
    "K-Medians"     = "Mirip K-Means, lebih robust outlier",
    "DBSCAN"        = "Kurang cocok, data terlalu kontinu",
    "Fuzzy C-Means" = "Baik untuk data ambigu"
  )
  cat(sprintf("%-14s| %-10s | %-7s | %s\n",
              r$Metode,
              ifelse(is.na(r$Silhouette_Score), "N/A", r$Silhouette_Score),
              ifelse(is.na(r$Dunn_Index), "N/A", r$Dunn_Index),
              catatan))
}
 Ringkasan Akhir 
Dataset  : Wine (UCI) — 178 sampel, 13 fitur kimia
k optimal: 3 (Elbow + Silhouette + konteks 3 kultivar)

Metode    | Silhouette | Dunn    | Catatan
------------------------------------------------------- 
K-Means       | 0.2849     | 0.2323  | Best overall
Fuzzy C-Means | 0.2849     | 0.2323  | Baik untuk data ambigu
K-Medians     | 0.2818     | 0.2286  | Mirip K-Means, lebih robust outlier
Hierarchical  | 0.2774     | 0.2286  | Sangat kompetitif, visual bagus
DBSCAN        | N/A        | N/A     | Kurang cocok, data terlalu kontinu