Tujuan: Memuat dataset dan mengambil sampel acak representatif dari keseluruhan data.
library(readxl) -> Memanggil package readxl yang memungkinkan R membaca file Excel (.xlsx). Package ini perlu diinstal terlebih dahulu jika belum ada.
read_excel(…) -> Membaca file heart_attack_prediction_indonesia.xlsx dan menyimpannya ke objek Datajantung. Dataset ini berisi data pasien dari Indonesia dengan berbagai fitur klinis dan demografis terkait risiko serangan jantung.
set.seed(…) -> Menetapkan “benih” angka acak agar hasil pengambilan sampel selalu konsisten setiap kali kode dijalankan ulang. Tanpa ini, sampel yang terpilih akan berbeda setiap kali dijalankan.
sample(1:nrow(Datajantung), 9407) -> Membuat vektor indeks baris secara acak sebanyak 9.407 dari total baris dataset. Mengingat jumlah data yang sangat besar (158.355 data) sehingga memerlukan komputasi yang berat, maka dilakukan pengambilan sampel menggunakan rumus Slovin dengan tingkat kesalahan sebesar 1% sebagai berikut:
\[n = \frac{N}{1 + N(e)^2} = \frac{158.355}{1 + 158.355(0,01)^2} = \frac{158.355}{16,8355} \approx 9.407 \text{ data}\]
Berdasarkan perhitungan tersebut, diperoleh jumlah sampel sebanyak 9.407 data yang diambil secara acak (random sampling) dari keseluruhan dataset untuk memastikan sampel yang diperoleh bersifat representatif terhadap populasi data.
Mengapa perlu sampling? Jika dataset asli sangat besar, pemodelan pada seluruh data bisa memakan waktu dan sumber daya komputasi yang besar. Sampling membantu efisiensi tanpa mengorbankan representativitas data secara signifikan.
library(readxl)
Datajantung <- read_excel("heart_attack_prediction_indonesia.xlsx")
set.seed(36)
sample_index <- sample(
1:nrow(Datajantung),
9407
)
Data_sample <- Datajantung[sample_index, ]
Tujuan: Mengubah variabel-variabel kategorikal dari tipe karakter/numerik menjadi tipe factor.
Decision Tree membutuhkan variabel kategorikal dalam format factor agar bisa diproses dengan benar oleh R.
Sebanyak 18 variabel dikonversi, mulai dari gender, region, hypertension, hingga heart_attack sebagai variabel target.
Secara default, R mungkin membaca variabel seperti hypertension (bernilai 0/1) sebagai numerik, bukan kategori. Jika dibiarkan numerik, algoritma rpart akan memperlakukannya sebagai data kontinu dan mencari titik split angka, bukan sebagai dua kategori berbeda (Ya/Tidak). Konversi ke factor memastikan algoritma memahami konteks semantik variabel tersebut.
library(caret)
## Warning: package 'caret' was built under R version 4.5.3
## Loading required package: ggplot2
## Loading required package: lattice
library(tidyverse)
## Warning: package 'tidyverse' was built under R version 4.5.3
## Warning: package 'lubridate' was built under R version 4.5.3
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.2.0 ✔ readr 2.1.6
## ✔ forcats 1.0.1 ✔ stringr 1.6.0
## ✔ lubridate 1.9.5 ✔ tibble 3.3.1
## ✔ purrr 1.2.1 ✔ tidyr 1.3.2
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ✖ purrr::lift() masks caret::lift()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(dplyr)
selected_data_preprocessed_factors <- Data_sample %>%
mutate(gender = factor(gender),
region = factor(region),
income_level = factor(income_level),
hypertension = factor(hypertension),
diabetes = factor(diabetes),
obesity = factor(obesity),
family_history = factor(family_history),
smoking_status = factor(smoking_status),
alcohol_consumption = factor(alcohol_consumption),
physical_activity = factor(physical_activity),
dietary_habits = factor(dietary_habits),
air_pollution_exposure = factor(air_pollution_exposure),
stress_level = factor(stress_level),
EKG_results = factor(EKG_results),
previous_heart_disease = factor(previous_heart_disease),
medication_usage = factor(medication_usage),
participated_in_free_screening = factor(participated_in_free_screening),
heart_attack = factor(heart_attack)
)
Tujuan: Memisahkan variabel dependen (target) dan variabel independen (fitur).
y berisi label kelas (heart_attack: 0 atau 1), sedangkan X berisi semua fitur prediktor.
Pemisahan ini merupakan langkah standar dalam persiapan supervised learning. Penggunaan dplyr::select() dengan prefix namespace dplyr:: dilakukan untuk menghindari konflik nama fungsi, karena beberapa package lain (seperti MASS) juga memiliki fungsi select(). Ini adalah praktik pemrograman yang baik dalam R.
y <- selected_data_preprocessed_factors$heart_attack
X <- selected_data_preprocessed_factors %>%
dplyr::select(-heart_attack)
Tujuan: Membagi data menjadi dua subset -> training (80%) dan testing (20%) -> untuk melatih dan mengevaluasi model secara objektif.
Mengapa 80:20? Ini adalah rasio yang umum digunakan dalam machine learning. Data training yang besar memberi model cukup informasi untuk belajar, sementara data testing yang memadai memberikan estimasi performa yang cukup stabil.
Stratified Partitioning: createDataPartition() dari package caret membagi data secara stratified, artinya proporsi kelas heart_attack (0 dan 1) dijaga sama antara data training dan testing. Ini sangat penting ketika distribusi kelas tidak seimbang -> tanpa stratifikasi, bisa terjadi data testing hanya berisi satu kelas saja.
set.seed(36)
train_index <- createDataPartition(y, p = 0.8, list = FALSE)
Pisahkan data menjadi set training dan testing
train_data <- selected_data_preprocessed_factors[train_index, ]
test_data <- selected_data_preprocessed_factors[-train_index, ]
Tujuan: Melakukan validasi visual terhadap struktur data setelah preprocessing untuk memastikan tidak ada kesalahan konversi tipe data.
glimpse() menampilkan untuk setiap kolom: nama variabel, tipe data
(
Yang perlu diverifikasi:
Semua 18 variabel kategorikal sudah bertipe
Variabel numerik seperti age, cholesterol_level, bmi tetap
bertipe
Tidak ada kolom yang hilang (missing) secara tidak sengaja
message("Struktur data training setelah konversi:")
## Struktur data training setelah konversi:
glimpse(train_data)
## Rows: 7,526
## Columns: 28
## $ age <dbl> 64, 44, 36, 75, 57, 67, 66, 59, 34, 38,…
## $ gender <fct> Male, Female, Female, Male, Male, Femal…
## $ region <fct> Urban, Urban, Urban, Urban, Urban, Rura…
## $ income_level <fct> Low, Middle, Low, Middle, High, Low, Mi…
## $ hypertension <fct> 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, …
## $ diabetes <fct> 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, …
## $ cholesterol_level <dbl> 261, 230, 292, 217, 193, 252, 100, 258,…
## $ obesity <fct> 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 0, …
## $ waist_circumference <dbl> 105, 108, 93, 100, 117, 85, 80, 71, 80,…
## $ family_history <fct> 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, …
## $ smoking_status <fct> Past, Never, Never, Never, Never, Never…
## $ alcohol_consumption <fct> None, None, None, None, None, Moderate,…
## $ physical_activity <fct> Moderate, Low, Moderate, Low, Moderate,…
## $ dietary_habits <fct> Unhealthy, Unhealthy, Healthy, Unhealth…
## $ air_pollution_exposure <fct> Low, Low, Moderate, Moderate, High, Mod…
## $ stress_level <fct> Moderate, Moderate, Moderate, Moderate,…
## $ sleep_hours <dbl> 5.504516, 6.668030, 8.058486, 7.335143,…
## $ blood_pressure_systolic <dbl> 125, 138, 114, 130, 129, 114, 137, 117,…
## $ blood_pressure_diastolic <dbl> 74, 93, 76, 98, 87, 76, 101, 95, 90, 67…
## $ fasting_blood_sugar <dbl> 116, 111, 84, 147, 176, 70, 131, 75, 75…
## $ cholesterol_hdl <dbl> 46, 66, 44, 46, 40, 46, 41, 48, 51, 59,…
## $ cholesterol_ldl <dbl> 185, 121, 107, 156, 109, 107, 107, 131,…
## $ triglycerides <dbl> 145, 243, 140, 192, 223, 193, 168, 118,…
## $ EKG_results <fct> Abnormal, Normal, Normal, Normal, Norma…
## $ previous_heart_disease <fct> 1, 0, 0, 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, …
## $ medication_usage <fct> 0, 1, 1, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1, …
## $ participated_in_free_screening <fct> 0, 0, 0, 1, 1, 0, 1, 0, 1, 1, 1, 1, 0, …
## $ heart_attack <fct> 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 0, …
message("\nStruktur data testing setelah konversi:")
##
## Struktur data testing setelah konversi:
glimpse(test_data)
## Rows: 1,881
## Columns: 28
## $ age <dbl> 46, 49, 72, 47, 70, 41, 51, 47, 63, 60,…
## $ gender <fct> Female, Female, Male, Male, Male, Male,…
## $ region <fct> Urban, Urban, Rural, Urban, Urban, Rura…
## $ income_level <fct> Low, Middle, Middle, High, High, Low, H…
## $ hypertension <fct> 0, 1, 0, 0, 1, 1, 1, 0, 0, 0, 0, 0, 1, …
## $ diabetes <fct> 1, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, …
## $ cholesterol_level <dbl> 199, 165, 243, 148, 253, 189, 205, 174,…
## $ obesity <fct> 0, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, …
## $ waist_circumference <dbl> 80, 115, 91, 84, 124, 56, 100, 103, 99,…
## $ family_history <fct> 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 1, …
## $ smoking_status <fct> Never, Past, Never, Current, Never, Nev…
## $ alcohol_consumption <fct> High, None, None, Moderate, None, Moder…
## $ physical_activity <fct> Low, Low, Moderate, Moderate, Moderate,…
## $ dietary_habits <fct> Healthy, Healthy, Unhealthy, Healthy, U…
## $ air_pollution_exposure <fct> Moderate, Low, Moderate, Moderate, High…
## $ stress_level <fct> Moderate, Moderate, Low, Moderate, Low,…
## $ sleep_hours <dbl> 9.000000, 9.000000, 3.000000, 7.998115,…
## $ blood_pressure_systolic <dbl> 148, 145, 120, 132, 130, 117, 127, 119,…
## $ blood_pressure_diastolic <dbl> 82, 88, 75, 92, 70, 84, 71, 81, 79, 74,…
## $ fasting_blood_sugar <dbl> 85, 138, 96, 117, 103, 175, 83, 137, 10…
## $ cholesterol_hdl <dbl> 51, 46, 45, 34, 47, 56, 62, 53, 52, 57,…
## $ cholesterol_ldl <dbl> 176, 70, 50, 167, 157, 71, 123, 130, 14…
## $ triglycerides <dbl> 107, 178, 125, 188, 204, 189, 75, 146, …
## $ EKG_results <fct> Normal, Normal, Normal, Abnormal, Norma…
## $ previous_heart_disease <fct> 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 1, …
## $ medication_usage <fct> 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 1, 0, 0, …
## $ participated_in_free_screening <fct> 1, 1, 1, 1, 1, 0, 0, 1, 1, 1, 0, 1, 1, …
## $ heart_attack <fct> 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0, 1, …
Tujuan: Mengecek apakah distribusi kelas target seimbang antara data training dan testing, sekaligus mendeteksi potensi class imbalance.
Jika proporsi kelas sangat tidak seimbang (misalnya 95% “tidak serangan” dan 5% “serangan”), model bisa menjadi bias. Jika proporsi kelas 0 dan 1 mendekati sama di keduanya -> stratified split berhasil, tidak perlu penanganan khusus.
ini memastikan distribusi kelas serupa antara training dan testing -> bukti bahwa stratified split berhasil.
message("Proporsi 'heart_attack' di data training:")
## Proporsi 'heart_attack' di data training:
prop_train <- prop.table(table(train_data$heart_attack))
print(prop_train)
##
## 0 1
## 0.5932766 0.4067234
message("\nProporsi 'heart_attack' di data testing:")
##
## Proporsi 'heart_attack' di data testing:
prop_test <- prop.table(table(test_data$heart_attack))
print(prop_test)
##
## 0 1
## 0.5933014 0.4066986
Tujuan: Menyiapkan konfigurasi validasi silang (cross-validation) untuk evaluasi model yang lebih robust.
Cara kerja 5-Fold Cross-Validation -> Data training dibagi menjadi 5 bagian (fold) yang sama besar.
Model dilatih sebanyak 5 kali:
Iterasi 1: Fold 2-5 untuk training, Fold 1 untuk validasi
Iterasi 2: Fold 1, 3-5 untuk training, Fold 2 untuk validasi
… dan seterusnya
Akurasi akhir adalah rata-rata dari 5 iterasi tersebut, menghasilkan estimasi performa yang lebih stabil dan tidak bergantung pada satu pembagian data saja.
Ini menghasilkan estimasi performa model yang lebih stabil dan tidak overfitting.
train_control <- trainControl(method = "cv", number = 5)
Tujuan: Mendefinisikan rentang nilai hyperparameter yang akan diuji secara sistematis melalui Grid Search.
Penjelasan hyperparameter:
cp (Complexity Parameter): Mengontrol seberapa agresif pruning (pemangkasan cabang) dilakukan pada pohon. Nilai kecil (0.001) membiarkan pohon tumbuh lebih dalam dan kompleks -> risiko overfitting. Nilai besar (0.1) memangkas banyak cabang -> pohon lebih sederhana, risiko underfitting.
minsplit: Jumlah minimum observasi yang harus ada di suatu node agar node tersebut boleh dipecah menjadi dua cabang. Nilai kecil (10) memungkinkan split bahkan pada node yang sedikit datanya -> pohon lebih detail. Nilai besar (30) membuat pohon lebih konservatif.
maxdepth: Kedalaman maksimum pohon dari root (akar) ke leaf (daun). Nilai 3 menghasilkan pohon sangat sederhana (mudah diinterpretasi), nilai 7 menghasilkan pohon yang lebih kompleks dan berpotensi lebih akurat namun sulit dibaca.
Total kombinasi yang diuji: 3 × 3 × 3 = 27 model
library(rpart)
library(caret)
set.seed(36)
results <- data.frame()
cp_list <- c(0.001, 0.01, 0.1)
minsplit_list <- c(10, 20, 30)
maxdepth_list <- c(3, 5, 7)
Tujuan: Melakukan Grid Search secara manual -> melatih dan mengevaluasi 27 model Decision Tree dengan kombinasi hyperparameter yang berbeda, lalu merekam akurasi masing-masing.
Alur kerja iterasi:
Bangun model rpart dengan kombinasi cp, minsplit, maxdepth saat itu
Prediksi label kelas pada test_data
Evaluasi dengan confusionMatrix() dan ekstrak nilai akurasi
Simpan kombinasi parameter beserta akurasinya ke dataframe results
rbind(results, …) menambahkan baris baru ke dataframe results di setiap iterasi, sehingga pada akhir loop results berisi 27 baris -> satu per kombinasi hyperparameter.
Hasil setiap kombinasi disimpan ke dataframe results untuk dibandingkan.
for (cp in cp_list) {
for (minsplit in minsplit_list) {
for (maxdepth in maxdepth_list) {
# Model
model_loop <- rpart(
heart_attack ~ .,
data = train_data,
method = "class",
control = rpart.control(
cp = cp,
minsplit = minsplit,
maxdepth = maxdepth
)
)
# Prediksi
pred <- predict(
model_loop,
newdata = test_data,
type = "class"
)
# Evaluasi
cm <- confusionMatrix(pred, test_data$heart_attack)
acc <- cm$overall["Accuracy"]
# Simpan hasil
results <- rbind(results, data.frame(
cp = cp,
minsplit = minsplit,
maxdepth = maxdepth,
accuracy = acc
))
}
}
}
Tujuan: Menampilkan tabel lengkap berisi 27 kombinasi hyperparameter beserta akurasi masing-masing untuk perbandingan.
Tabel ini memudahkan analisis pola: misalnya, apakah maxdepth yang lebih besar selalu menghasilkan akurasi lebih tinggi? Atau apakah cp kecil justru menurunkan performa karena overfitting?
print(results)
## cp minsplit maxdepth accuracy
## Accuracy 0.001 10 3 0.6916534
## Accuracy1 0.001 10 5 0.7166401
## Accuracy2 0.001 10 7 0.7256778
## Accuracy3 0.001 20 3 0.6916534
## Accuracy4 0.001 20 5 0.7166401
## Accuracy5 0.001 20 7 0.7256778
## Accuracy6 0.001 30 3 0.6916534
## Accuracy7 0.001 30 5 0.7166401
## Accuracy8 0.001 30 7 0.7246146
## Accuracy9 0.010 10 3 0.6916534
## Accuracy10 0.010 10 5 0.7129187
## Accuracy11 0.010 10 7 0.7129187
## Accuracy12 0.010 20 3 0.6916534
## Accuracy13 0.010 20 5 0.7129187
## Accuracy14 0.010 20 7 0.7129187
## Accuracy15 0.010 30 3 0.6916534
## Accuracy16 0.010 30 5 0.7129187
## Accuracy17 0.010 30 7 0.7129187
## Accuracy18 0.100 10 3 0.6549708
## Accuracy19 0.100 10 5 0.6549708
## Accuracy20 0.100 10 7 0.6549708
## Accuracy21 0.100 20 3 0.6549708
## Accuracy22 0.100 20 5 0.6549708
## Accuracy23 0.100 20 7 0.6549708
## Accuracy24 0.100 30 3 0.6549708
## Accuracy25 0.100 30 5 0.6549708
## Accuracy26 0.100 30 7 0.6549708
Tujuan: Secara otomatis memilih kombinasi hyperparameter yang menghasilkan akurasi tertinggi pada data testing.
which.max(results$accuracy) -> mengembalikan nomor baris (indeks) dengan nilai akurasi tertinggi
results[indeks, ] -> mengambil seluruh baris tersebut, berisi nilai cp, minsplit, maxdepth, dan accuracy
Kombinasi terpilih inilah yang dianggap sebagai konfigurasi optimal dan akan digunakan untuk membangun model final.
best_param <- results[which.max(results$accuracy), ]
cat("\nPARAMETER TERBAIK:\n")
##
## PARAMETER TERBAIK:
print(best_param)
## cp minsplit maxdepth accuracy
## Accuracy2 0.001 10 7 0.7256778
Tujuan: Melatih ulang model Decision Tree menggunakan seluruh data training dengan hyperparameter terbaik hasil Grid Search.
Penjelasan komponen:
heart_attack ~ . -> Formula: prediksi heart_attack menggunakan semua variabel lainnya (. berarti semua kolom)
method = “class” -> Menentukan ini adalah masalah klasifikasi, bukan regresi
rpart.control(…) -> Menetapkan hyperparameter optimal dari best_param
Model ini (dt_modelT) adalah model final yang siap dievaluasi dan diinterpretasi.
dt_modelT <- rpart(
heart_attack ~ .,
data = train_data,
method = "class",
control = rpart.control(
cp = best_param$cp,
minsplit = best_param$minsplit,
maxdepth = best_param$maxdepth
)
)
Tujuan: Mengaplikasikan model final ke data testing untuk menghasilkan prediksi kelas.
newdata = test_data -> Model memprediksi baris-baris yang belum pernah “dilihat” selama training
type = “class” -> Output berupa label kelas (0 atau 1), bukan probabilitas. Jika ingin probabilitas, gunakan type = “prob”
Hasil pred_dtT adalah vektor faktor berisi prediksi untuk setiap baris di test_data.
pred_dtT <- predict(dt_modelT, newdata = test_data, type = "class")
Tujuan: Mengukur performa model final secara komprehensif.
Confusion Matrix menghasilkan metrik seperti:
Accuracy -> proporsi prediksi benar secara keseluruhan
Sensitivity (Recall) -> kemampuan model mendeteksi kasus serangan jantung (kelas positif = 1)
Specificity -> kemampuan model mengidentifikasi yang tidak serangan jantung
Kappa -> akurasi yang dikoreksi terhadap peluang acak
message("\nConfusion Matrix DT:")
##
## Confusion Matrix DT:
confusionMatrix(
pred_dtT,
test_data$heart_attack,
positive = "1"
)
## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 893 293
## 1 223 472
##
## Accuracy : 0.7257
## 95% CI : (0.7049, 0.7457)
## No Information Rate : 0.5933
## P-Value [Acc > NIR] : < 2.2e-16
##
## Kappa : 0.4233
##
## Mcnemar's Test P-Value : 0.002385
##
## Sensitivity : 0.6170
## Specificity : 0.8002
## Pos Pred Value : 0.6791
## Neg Pred Value : 0.7530
## Prevalence : 0.4067
## Detection Rate : 0.2509
## Detection Prevalence : 0.3695
## Balanced Accuracy : 0.7086
##
## 'Positive' Class : 1
##
Tujuan: Menampilkan struktur pohon keputusan secara visual.
extra = 111 menampilkan probabilitas kelas, proporsi kelas, dan jumlah observasi di setiap node.
fallen.leaves = TRUE menempatkan semua daun (leaf node) di baris bawah untuk keterbacaan.
cex = 0.6 mengecilkan ukuran teks agar pohon tidak terlalu padat.
Visualisasi ini memungkinkan interpretasi faktor-faktor apa yang paling berpengaruh terhadap prediksi serangan jantung.
message("\nVisualisasi Decision Tree:")
##
## Visualisasi Decision Tree:
# rpart.plot(dt_modelT, extra = 111, fallen.leaves = TRUE, cex = 0.6)