1 Load Dataset

data <- read.csv("data/patient_dataset.csv")
head(data, n = 10)
##    age gender chest_pain_type blood_pressure cholesterol max_heart_rate
## 1   24      1               4            250         139            212
## 2   29      0               4            132         187            147
## 3   46      0               3            271         185            193
## 4   73     NA               2            102         200            125
## 5   49      1               3             91         163            192
## 6   63      1               3             18         154            107
## 7   48      0               3            143         275            165
## 8   37      1               4            263         201            201
## 9   20      0               3            113         127            139
## 10  77      1               1            138         217            201
##    exercise_angina plasma_glucose skin_thickness insulin      bmi
## 1                0            108             33     109 37.99930
## 2                0            202             42      NA 25.58835
## 3                0            149             43     102 37.89203
## 4                0            105             77     165 18.66024
## 5                0            162             31     170 12.76798
## 6                0            103             67     102 22.37385
## 7                0            248             NA     136 27.90071
## 8                0            186             21     180 35.66340
## 9                1            123             NA     120 26.52915
## 10               0            199            100     132 18.39360
##    diabetes_pedigree hypertension heart_disease residence_type smoking_status
## 1          0.4802775            1             1          Urban         Smoker
## 2          0.2839864            1             1          Urban        Unknown
## 3          2.4723086            1             0          Rural     Non-Smoker
## 4          1.4720523            0             1          Rural         Smoker
## 5          0.5376265            1             1          Rural         Smoker
## 6          1.0624109            0             0          Rural     Non-Smoker
## 7          1.0737608            1             1          Rural     Non-Smoker
## 8          0.1512359            0             0          Urban         Smoker
## 9          1.9102780            1             0          Urban     Non-Smoker
## 10         1.8253058            1             0          Rural     Non-Smoker

2 Preprocessing

2.1 Cek Missing Value dan Nilai Kosong

data %>%
  summarise(across(everything(), ~sum(is.na(.)))) %>%
  pivot_longer(everything(), names_to = "Kolom", values_to = "Jumlah_NA") %>%
  mutate(Persen_NA = round((Jumlah_NA / nrow(data)) * 100, 2))
## # A tibble: 16 × 3
##    Kolom             Jumlah_NA Persen_NA
##    <chr>                 <int>     <dbl>
##  1 age                       0      0   
##  2 gender                  472      7.87
##  3 chest_pain_type           0      0   
##  4 blood_pressure            0      0   
##  5 cholesterol               0      0   
##  6 max_heart_rate            0      0   
##  7 exercise_angina           0      0   
##  8 plasma_glucose          609     10.2 
##  9 skin_thickness          614     10.2 
## 10 insulin                 568      9.47
## 11 bmi                       0      0   
## 12 diabetes_pedigree         0      0   
## 13 hypertension              0      0   
## 14 heart_disease             0      0   
## 15 residence_type            0      0   
## 16 smoking_status            0      0
# Cek nilai kosong " " atau spasi pada kolom karakter
sapply(data, function(x) {
  if (is.character(x) || is.factor(x)) sum(trimws(x) == "") else 0
})
##               age            gender   chest_pain_type    blood_pressure 
##                 0                 0                 0                 0 
##       cholesterol    max_heart_rate   exercise_angina    plasma_glucose 
##                 0                 0                 0                 0 
##    skin_thickness           insulin               bmi diabetes_pedigree 
##                 0                 0                 0                 0 
##      hypertension     heart_disease    residence_type    smoking_status 
##                 0                 0               455                 0

Dari hasil cek nilai kosong pada setiap kolom dataset, ditemukan hampir seluruh kolom tidak memiliki nilai kosong, kecuali kolom residence_type terdapat 455 baris data memiliki nilai kosong, yang berarti baris-baris tersebut tidak memiliki informasi tentang jenis tempat tinggal pasien.

2.2 Imputasi Data Menggunakan KNN

list_impute <- c("plasma_glucose", "skin_thickness", "insulin")
data[list_impute] <- kNN(data[list_impute], k = 5)[, list_impute]

2.3 Ubah Kolom Kategorikal ke Numerik

ohe_encode <- function(df, column) {
  dummies <- dummyVars(as.formula(paste("~", column)), data = df)
  ohe <- predict(dummies, newdata = df)
  colnames(ohe) <- gsub("\\.", "_", colnames(ohe))
  ohe_df <- as.data.frame(ohe)
  df <- cbind(df, ohe_df)
  df <- df[, !(names(df) %in% column)]
  return(df)
}

data <- ohe_encode(data, "residence_type")
data <- ohe_encode(data, "gender")
data <- ohe_encode(data, "smoking_status")

2.4 Cek Ulang Missing Value

data %>%
  summarise(across(everything(), ~sum(is.na(.)))) %>%
  pivot_longer(everything(), names_to = "Kolom", values_to = "Jumlah_NA") %>%
  mutate(Persen_NA = round((Jumlah_NA / nrow(data)) * 100, 2))
## # A tibble: 18 × 3
##    Kolom                    Jumlah_NA Persen_NA
##    <chr>                        <int>     <dbl>
##  1 age                              0         0
##  2 chest_pain_type                  0         0
##  3 blood_pressure                   0         0
##  4 cholesterol                      0         0
##  5 max_heart_rate                   0         0
##  6 exercise_angina                  0         0
##  7 plasma_glucose                   0         0
##  8 skin_thickness                   0         0
##  9 insulin                          0         0
## 10 bmi                              0         0
## 11 diabetes_pedigree                0         0
## 12 hypertension                     0         0
## 13 heart_disease                    0         0
## 14 residence_typeRural              0         0
## 15 residence_typeUrban              0         0
## 16 smoking_statusNon-Smoker         0         0
## 17 smoking_statusSmoker             0         0
## 18 smoking_statusUnknown            0         0

2.5 Scaling

scaler <- preProcess(data, method = c("center", "scale"))
data_scaled <- predict(scaler, data)

3 DBSCAN tanpa PCA

3.1 DBSCAN

set.seed(123)
kNNdistplot(data_scaled, k = 4)
abline(h = 3, col = "red", lty = 2)

db_result <- dbscan(data_scaled, eps = 3, minPts = 4)
fviz_cluster(list(data = data_scaled, cluster = db_result$cluster),
             main = "DBSCAN Clustering")

Parameter ε (epsilon) pada DBSCAN ditentukan menggunakan kNN distance plot dengan k = 4 sehingga minPts = 4. Untuk grafiknya menunjukkan elbow atau tekukan di nilai 3, sehingga ditetapkan eps = 3. Garis merah putus-putus pada grafik menandai ambang epsilon yang digunakan dalam algoritma.

Setelah parameter ditentukan, dilanjutkan klasterisasi menggunakan DBSCAN. Hasil visualisasi menunjukkan bahwa algoritma berhasil mengidentifikasi beberapa klaster yang terpisah, namun masih cukup banyak data yang diklasifikasikan sebagai noise yang ditandai dengan label 0 dan simbol merah. Hal ini mengindikasikan bahwa data asli masih cukup kompleks dan tumpang tindih antar grup pasien.

3.2 Evaluasi DBSCAN

silhouette_dbscan <- silhouette(db_result$cluster, dist(data_scaled))
silhouette_score_dbscan <- mean(silhouette_dbscan[, 3])  # Nilai rata-rata Silhouette

# Hitung jumlah noise points dengan label 0
noise_points_dbscan <- sum(db_result$cluster == 0)

cat("Silhouette Score DBSCAN tanpa PCA:", silhouette_score_dbscan, "\n")
## Silhouette Score DBSCAN tanpa PCA: 0.07383255
cat("Number of Noise Points DBSCAN tanpa PCA:", noise_points_dbscan, "\n")
## Number of Noise Points DBSCAN tanpa PCA: 342

Pada bagian ini didapatkan Silhouette Score DBSCAN sebesar 0.074, yang menandakan kualitas klaster masih tergolong rendah. Hal ini terjadi karena dimensi data yang tinggi, serta tumpang tindih antar kelompok

Sebanyak 342 data atau sekitar 5.7% dari total 6000 observasi diklasifikasikan sebagai noise karena tidak masuk ke dalam klaster manapun.

4 DBSCAN dengan PCA

4.1 PCA

pca_res <- prcomp(data_scaled, center = TRUE, scale. = TRUE)
fviz_eig(pca_res)

summary(pca_res)$importance[3, ]  # variansi kumulatif
##     PC1     PC2     PC3     PC4     PC5     PC6     PC7     PC8     PC9    PC10 
## 0.10366 0.20654 0.27089 0.33078 0.38998 0.44797 0.50503 0.56139 0.61739 0.67246 
##    PC11    PC12    PC13    PC14    PC15    PC16    PC17    PC18 
## 0.72705 0.78132 0.83493 0.88817 0.94046 0.99219 1.00000 1.00000
# Hitung variansi kumulatif
cum_var <- summary(pca_res)$importance[3, ]
n_components <- which(cum_var >= 0.8)[1]
pca_data <- as.data.frame(pca_res$x[, 1:n_components])

Hasil visualisasi scree plot menunjukkan bahwa komponen utama pertama PC1 dan PC2 menyumbang variasi yang besar dibandingkan komponen lainnya. Berdasarkan output variansi kumulatif, diketahui bahwa:

  • 10 komponen utama pertama telah menjelaskan sekitar 67.25% dari total variansi data.

  • Sementara lanjutannya hingga PC13, data telah menjelaskan 83.49% variansi kumulatif.

Untuk memenuhi ambang minimal 80% variansi, dipilih 13 komponen utama pertama sebagai representasi data yang direduksi, yang ditandai pada kode n_components = 13. Dengan demikian, PCA dapat menyederhanakan data dari 18 fitur menjadi 13 komponen dan tetap mempertahankan informasi penting, sehingga dapat digunakan untuk klasterisasi yang lebih optimal dan cepat secara komputasi.

4.2 DBSCAN

set.seed(123)
kNNdistplot(pca_data, k = 4)
abline(h = 3, col = "red", lty = 2)

db_result <- dbscan(pca_data, eps = 3, minPts = 4)
fviz_cluster(list(data = pca_data, cluster = db_result$cluster),
             main = "DBSCAN Clustering")

Setelah data direduksi dimensinya menggunakan PCA menjadi 13 komponen utama, selanjutnya dilakukan klasterisasi menggunakan DBSCAN. Untuk menentukan nilai epsilon yang optimal, penggunaan kNN distance plot dengan k = 4, titik elbow 3 pada grafik dipertahankan untuk konsistensi.

Hasil visualisasi klaster menunjukkan adanya dua klaster utama yang terbentuk dengan jelas, yaitu:

  • Cluster 1 warna hijau

  • Cluster 2 warna biru

Selain itu, ada sedikit data yang diklasifikasikan sebagai noise yang berwarna merah atau cluster 0, yang jumlahnya lebih sedikit dibandingkan saat DBSCAN dijalankan tanpa PCA

4.3 Evaluasi DBSCAN dengan PCA

silhouette_dbscan_pca <- silhouette(db_result$cluster, dist(pca_data))
silhouette_score_dbscan <- mean(silhouette_dbscan_pca[, 3])
noise_points_dbscan <- sum(db_result$cluster == 0)

cat("Silhouette Score DBSCAN dengan PCA:", silhouette_score_dbscan, "\n")
## Silhouette Score DBSCAN dengan PCA: 0.1826298
cat("Number of Noise Points DBSCAN dengan PCA:", noise_points_dbscan, "\n")
## Number of Noise Points DBSCAN dengan PCA: 7

Hasil klasterisasi menunjukkan adanya peningkatan yang signifikan dibandingkan hasil klaster tanpa PCA. Jumlah data yang dikategorikan sebagai noise hanya sebanyak 7 data, jauh lebih sedikit dibandingkan dengan hasil tanpa PCA.

Selain itu, nilai Silhouette Score meningkat menjadi 0,1826, yang menunjukkan bahwa pembentukan klaster menjadi lebih baik, dengan pemisahan antar klaster yang lebih jelas serta kohesi yang lebih kuat di dalam klaster

5 Visualisasi Pebandingan

5.1 Barplot Silhoutte Score

silhouette_df <- data.frame(
  Metode = c("Tanpa PCA", "Dengan PCA"),
  Silhouette = c(silhouette_score_dbscan, noise_points_dbscan)
)

ggplot(silhouette_df, aes(x = Metode, y = Silhouette, fill = Metode)) +
  geom_bar(stat = "identity", width = 0.4) +
  ggtitle("Perbandingan Silhouette Score") +
  theme_minimal()

5.2 Silhouette Plot – DBSCAN tanpa PCA

fviz_silhouette(silhouette_dbscan) +
  ggtitle("Silhouette Plot - DBSCAN (Tanpa PCA)")
##   cluster size ave.sil.width
## 0       0  342         -0.18
## 1       1 5333          0.09
## 2       2  146          0.03
## 3       3  169         -0.01
## 4       4    6          0.31
## 5       5    4          0.44

Hasil silhouette plot tanpa PCA menunjukkan bahwa sebagian besar klaster memiliki nilai average silhouette width yang rendah, bahkan negatif pada klaster 0 dan 3, yang menunjukkan adanya tumpang tindih antar klaster dan pemisahan yang kurang optimal. Secara keseluruhan, kualitas klasterisasi tanpa PCA belum memuaskan.

5.3 Silhouette Plot – DBSCAN dengan PCA

fviz_silhouette(silhouette_dbscan_pca) +
  ggtitle("Silhouette Plot - DBSCAN (Dengan PCA)")
##   cluster size ave.sil.width
## 0       0    7         -0.20
## 1       1 5522          0.18
## 2       2  471          0.21

Hasil silhouette plot dengan PCA menunjukkan nilai average silhouette width meningkat, khususnya pada klaster 1 dan 2, yang masing-masing memiliki nilai 0,18 dan 0,21. Ini menunjukkan bahwa setelah reduksi dimensi, pemisahan antar klaster menjadi lebih jelas dan kohesi antar anggota klaster meningkat dengan hanya 7 data yang tergolong noise dengan nilai silhouette negatif