Bayangkan Anda ingin mengambil satu keputusan penting. Daripada bertanya pada satu orang ahli, Anda memutuskan untuk bertanya pada sekelompok ahli. Setelah semua ahli memberikan pendapatnya, Anda mengambil kesimpulan berdasarkan gabungan pendapat mereka (misalnya, berdasarkan suara terbanyak).
Keputusan yang diambil secara berkelompok ini kemungkinan besar akan lebih baik dan lebih bijak daripada keputusan satu orang saja.
Itulah ide dasar dari Ensemble Models (Model Ansambel).
Dalam machine learning, Ensemble Models adalah sebuah teknik yang menggabungkan beberapa model (disebut weak learners atau model dasar) untuk bekerja sama. Tujuannya adalah untuk menghasilkan satu “Super Model” (model kuat) yang memiliki performa prediksi yang jauh lebih akurat dan stabil daripada jika kita hanya menggunakan satu model saja.
Tujuan utamanya adalah untuk memperbaiki dua masalah umum pada model:
Ensemble models menyeimbangkan keduanya. Secara umum, ada dua strategi utama untuk menggabungkan model:
[Image of Bagging vs Boosting in machine learning]
Random Forest adalah contoh paling populer dari Bagging. Sesuai namanya, model ini adalah sebuah “hutan” (Forest) yang terdiri dari banyak “pohon” (Decision Trees).
Setiap pohon di dalam “hutan” ini berbeda satu sama lain karena mereka dilatih pada “kantong” data yang berbeda dan juga hanya melihat subset fitur yang acak. Ini membuat setiap pohon menjadi “ahli” di bidangnya masing-masing.
B sampel
bootstrap (kantong data acak dengan penggantian) dari data
latihan.Prediksi akhir diperoleh dengan menggabungkan semua prediksi pohon:
1. Klasifikasi (Voting): Prediksi akhir adalah kelas yang paling banyak dipilih (modus) oleh semua pohon.
\[\hat{y} = \text{mode}\{T_1(x), T_2(x), \ldots, T_B(x)\}\]
2. Regresi (Rata-rata): Prediksi akhir adalah nilai rata-rata dari prediksi semua pohon.
\[\hat{y} = \frac{1}{B} \sum_{b=1}^{B} T_b(x)\]
Gradient Boosting Machines (GBM) adalah contoh populer dari Boosting. GBM bekerja secara sekuensial (berurutan), di mana setiap pohon baru ditambahkan untuk mengoreksi kesalahan dari pohon-pohon sebelumnya.
Bayangkan ini seperti tim estafet yang sedang belajar: 1. Pohon 1 mencoba memprediksi target. Tentu saja, ada banyak kesalahan (disebut residuals). 2. Pohon 2 datang. Tugasnya bukan memprediksi target, tapi memprediksi kesalahan yang dibuat Pohon 1. 3. Pohon 3 datang. Tugasnya adalah memprediksi kesalahan yang masih tersisa dari gabungan (Pohon 1 + Pohon 2). 4. Proses ini diulang terus-menerus, setiap pohon baru secara perlahan memperbaiki prediksi model, hingga kesalahannya menjadi sangat kecil.
Proses ini bisa diringkas dalam 3 langkah (diulang-ulang):
1. Inisialisasi Model: Model dimulai dengan satu tebakan awal yang konstan (misal, rata-rata dari semua target). \[F_0(x) = \arg \min_{\gamma} \sum_{i=1}^{n} L(y_i, \gamma)\]
2. Hitung “Kesalahan” (Residuals): Untuk setiap iterasi \(m\), hitung “kesalahan” (\(r\)) dari model sebelumnya. Kesalahan ini adalah gradien (turunan) dari loss function. \[r_{im} = -\left[\frac{\partial L(y_i, F(x_i))}{\partial F(x_i)}\right]_{F(x)=F_{m-1}(x)}\]
3. Update Model: Buat model baru \(F_m(x)\) dengan cara: Model Lama + (sedikit perbaikan). \[F_m(x) = F_{m-1}(x) + \nu T_m(x)\]
XGBoost adalah “GBM versi canggih” (atau GBM “on steroids”). Ini adalah implementasi dari gradient boosting yang telah dioptimalkan secara ekstrem agar…
XGBoost adalah GBM, tetapi dengan dua tambahan utama:
Regularization (Lebih Pintar): Ini adalah “rem” otomatis untuk mencegah overfitting. Saat GBM hanya fokus mengurangi kesalahan (loss), XGBoost juga mempertimbangkan kompleksitas model. Jika sebuah pohon baru terlalu rumit, XGBoost akan “menghukum” pohon itu. Ini membuat model tetap sederhana dan tidak overfitting.
Kecepatan dan Efisiensi (Lebih Cepat): XGBoost dirancang dari awal untuk efisiensi. Ia bisa menggunakan semua core CPU (paralelisasi), pintar dalam mengelola memori, dan bahkan bisa menangani missing values (data kosong) secara otomatis.
Perbedaan utamanya terlihat pada Objective Function (fungsi tujuan) yang ingin diminimalkan oleh XGBoost:
\[Obj = \sum_{i=1}^{n} L(y_i, \hat{y}_i) + \sum_{k=1}^{K} \Omega(f_k)\]
“Hukuman” (Regularisasi) \(\Omega\) ini sendiri dihitung dengan: \[\Omega(f) = \gamma T + \frac{1}{2}\lambda \sum_{j=1}^{T} w_j^2\]
Pada bagian ini, kita akan menguraikan dengan jelas apa masalah yang akan kita selesaikan, data apa yang kita gunakan, dan mengapa kita memilih menggunakan Ensemble Models untuk menyelesaikannya.
Masalah utama yang akan kita selesaikan adalah memprediksi risiko penyakit jantung pada seorang pasien.
Secara lebih teknis, kita ingin membuat sebuah model machine learning yang bisa menjawab pertanyaan:
“Berdasarkan data klinis pasien ini (seperti umur, kolesterol, tekanan darah, dll.), apakah dia berisiko tinggi terkena penyakit jantung atau berisiko rendah?”
Karena jawabannya hanya ada dua pilihan (Risiko Tinggi vs. Risiko Rendah), ini adalah masalah Klasifikasi Biner (Binary Classification). Model kita akan belajar dari data yang sudah ada untuk “mengelompokkan” pasien baru ke salah satu dari dua kategori tersebut.
Untuk membuat model ini, kita perlu membagi data kita menjadi dua jenis:
Variabel Prediktor (Fitur): Ini adalah
“pertanyaan” atau input yang kita berikan kepada model. Dalam
dataset heart.csv (yang akan dibahas di Bagian 3), ini
adalah 11 kolom seperti Age, Sex,
Cholesterol, RestingBP, dan
lain-lain.
Variabel Target (Output): Ini adalah “jawaban”
atau output yang ingin kita prediksi. Dalam dataset ini,
variabel targetnya adalah kolom bernama
HeartDisease.
1: Berarti pasien didiagnosis
menderita penyakit jantung (Risiko Tinggi).0: Berarti pasien didiagnosis
normal (Risiko Rendah).Tugas kita adalah melatih model untuk bisa menebak nilai
HeartDisease (0 atau 1) dengan benar, hanya dengan melihat
11 variabel prediktor lainnya.
Kita bisa saja menggunakan satu model sederhana (seperti satu Decision Tree), tetapi untuk masalah sepenting diagnosis medis, kita butuh yang terbaik.
Akurasi adalah Kunci. Salah memprediksi pasien (mengatakan sehat padahal sakit, atau sebaliknya) bisa berakibat fatal. Seperti yang sudah dibahas di Bagian 1, Ensemble Models dipilih karena:
Di Bagian 1, kita belajar bahwa Random Forest, GBM, dan XGBoost semuanya hebat, tapi cara kerja mereka berbeda. Tidak ada satu model pun yang “paling jago” di semua situasi.
Oleh karena itu, tujuan utama dari laporan kelompok ini adalah mencari tahu model mana yang bekerja paling baik untuk dataset kita ini.
Kita akan melatih ketiga model tersebut pada data
heart.csv dan membandingkannya secara adil untuk menjawab
pertanyaan:
Pada akhirnya, laporan ini akan memberikan rekomendasi berbasis data tentang ensemble model mana yang paling kami sarankan untuk digunakan dalam memprediksi penyakit jantung.
Sebelum kita mulai membangun model, kita harus “berkenalan” dulu dengan data yang akan kita gunakan. Ibarat koki, kita perlu tahu persis bahan-bahan apa yang kita miliki sebelum mulai memasak.
Data yang kita pakai di laporan ini adalah dataset publik populer dari platform Kaggle, yang bernama “Heart Failure Prediction”.
Dataset ini bukan data sembarangan. Ini adalah gabungan dari lima dataset penyakit jantung terkenal (termasuk Cleveland, Hungarian, dll.) yang telah dikumpulkan dan digabungkan menjadi satu. Ini membuatnya menjadi dataset yang kaya dan bagus untuk studi kasus seperti ini.
heart.csvSetelah kita memuat file heart.csv, kita bisa melihat
seberapa besar datanya:
Dari 12 kolom ini, 11 kolom akan menjadi “fitur” (prediktor) dan 1 kolom akan menjadi “target” (yang kita tebak).
Tidak semua 12 kolom itu sama. Saat kita memuat datanya, kita bisa melihat ada dua jenis tipe data utama:
Age,
RestingBP, Cholesterol). Total ada 7 kolom
numerik.Sex yang isinya ‘M’ atau
‘F’). Total ada 5 kolom kategorikal.Kenapa ini penting? Model machine learning pada dasarnya adalah matematika. Model tidak bisa mengerti teks seperti ‘M’, ‘F’, atau ‘Normal’.
Oleh karena itu, semua 5 kolom kategorikal ini wajib kita ubah menjadi angka (prosesnya disebut encoding) sebelum kita bisa melatih model kita. Ini akan kita lakukan nanti di bagian Data Pre-processing.
Ini adalah “kamus data” kita. Sangat penting bagi kita semua untuk paham arti dari setiap kolom agar analisis kita tepat sasaran.
Berikut adalah 12 kolom tersebut:
Age (Numerik): Umur pasien (dalam
tahun).Sex (Kategorikal): Jenis kelamin
pasien (M = Pria, F = Wanita).ChestPainType (Kategorikal): Tipe
nyeri dada yang dirasakan.
TA: Typical Angina (Nyeri dada “khas” penyakit
jantung)ATA: Atypical Angina (Nyeri “tidak khas”)NAP: Non-Anginal Pain (Nyeri, tapi bukan karena
jantung)ASY: Asymptomatic (Tanpa gejala nyeri)RestingBP (Numerik): Tekanan darah
istirahat (dalam mm Hg).Cholesterol (Numerik): Kadar
kolesterol serum (dalam mg/dl).FastingBS (Numerik): Kadar gula darah
puasa.
1: Jika > 120 mg/dl (dianggap punya diabetes)0: Jika <= 120 mg/dl (normal)RestingECG (Kategorikal): Hasil rekam
jantung (EKG) saat istirahat.
Normal: NormalST: Ada kelainan di gelombang ST-TLVH: Kemungkinan ada pembengkakan ventrikel kiriMaxHR (Numerik): Detak jantung
maksimum yang tercatat (biasanya saat tes olahraga).ExerciseAngina (Kategorikal): Apakah
pasien merasakan nyeri dada saat berolahraga? (Y = Ya,
N = Tidak).Oldpeak (Numerik): Ini adalah nilai
EKG yang diukur saat olahraga, menunjukkan seberapa “turun” segmen ST.
Ini adalah indikator penting.ST_Slope (Kategorikal): Kemiringan
(slope) segmen ST saat puncak olahraga.
Up: MenanjakFlat: DatarDown: MenurunHeartDisease (Variabel TARGET)
(Numerik): Ini adalah kolom jawaban yang ingin kita
prediksi.
1: Pasien punya
penyakit jantung.0: Pasien normal
(tidak punya penyakit jantung).Exploratory Data Analysis (EDA) adalah proses investigasi awal pada data untuk memahami karakteristik utamanya. Anggap saja ini seperti seorang detektif yang mengumpulkan petunjuk di tempat kejadian perkara sebelum menyimpulkan siapa pelakunya.
Kita akan mencari pola, menemukan anomali (keanehan), menguji hipotesis, dan melihat hubungan antar variabel.
# Memuat dataset
# Pastikan file 'heart.csv' ada di folder yang sama dengan file .Rmd Anda
data <- read_csv("heart.csv")
# Menampilkan tabel interaktif
datatable(data,
options = list(pageLength = 5, scrollX = TRUE),
caption = "Tabel 1: Dataset Heart Failure Prediction")# PENTING: Kita perlu 'memberi tahu' R bahwa beberapa kolom,
# meskipun berisi angka (0/1), sebenarnya adalah KATEGORI (factor).
data_processed <- data %>%
mutate(
# Kategori:
HeartDisease = as.factor(HeartDisease),
FastingBS = as.factor(FastingBS),
Sex = as.factor(Sex),
ChestPainType = as.factor(ChestPainType),
RestingECG = as.factor(RestingECG),
ExerciseAngina = as.factor(ExerciseAngina),
ST_Slope = as.factor(ST_Slope)
)
# Mendefinisikan grup kolom agar lebih mudah dianalisis
numerical_cols <- c("Age", "RestingBP", "Cholesterol", "MaxHR", "Oldpeak")
categorical_cols <- c("Sex", "ChestPainType", "FastingBS", "RestingECG", "ExerciseAngina", "ST_Slope")
target_col <- "HeartDisease"Pertama, kita lihat “profil” atau “data diri” dari setiap variabel.
Kita gunakan skimr::skim() yang memberikan ringkasan
statistik yang sangat lengkap.
# Menjalankan skim() hanya pada kolom numerik
data_processed %>%
select(all_of(numerical_cols)) %>%
skim()| Name | Piped data |
| Number of rows | 918 |
| Number of columns | 5 |
| _______________________ | |
| Column type frequency: | |
| numeric | 5 |
| ________________________ | |
| Group variables | None |
Variable type: numeric
| skim_variable | n_missing | complete_rate | mean | sd | p0 | p25 | p50 | p75 | p100 | hist |
|---|---|---|---|---|---|---|---|---|---|---|
| Age | 0 | 1 | 53.51 | 9.43 | 28.0 | 47.00 | 54.0 | 60.0 | 77.0 | ▁▅▇▆▁ |
| RestingBP | 0 | 1 | 132.40 | 18.51 | 0.0 | 120.00 | 130.0 | 140.0 | 200.0 | ▁▁▃▇▁ |
| Cholesterol | 0 | 1 | 198.80 | 109.38 | 0.0 | 173.25 | 223.0 | 267.0 | 603.0 | ▃▇▇▁▁ |
| MaxHR | 0 | 1 | 136.81 | 25.46 | 60.0 | 120.00 | 138.0 | 156.0 | 202.0 | ▁▃▇▆▂ |
| Oldpeak | 0 | 1 | 0.89 | 1.07 | -2.6 | 0.00 | 0.6 | 1.5 | 6.2 | ▁▇▆▁▁ |
Temuan (Petunjuk Penting):
Age (Umur): Rata-rata umur pasien
adalah 53.5 tahun, dengan pasien termuda 28 tahun dan tertua 77
tahun.RestingBP (Tekanan Darah) &
Cholesterol:
min
(minimum). Nilai minimum untuk RestingBP dan
Cholesterol adalah 0.Sekarang kita lihat profil untuk data kategori.
# Menjalankan skim() pada kolom kategorikal
data_processed %>%
select(all_of(categorical_cols)) %>%
skim()| Name | Piped data |
| Number of rows | 918 |
| Number of columns | 6 |
| _______________________ | |
| Column type frequency: | |
| factor | 6 |
| ________________________ | |
| Group variables | None |
Variable type: factor
| skim_variable | n_missing | complete_rate | ordered | n_unique | top_counts |
|---|---|---|---|---|---|
| Sex | 0 | 1 | FALSE | 2 | M: 725, F: 193 |
| ChestPainType | 0 | 1 | FALSE | 4 | ASY: 496, NAP: 203, ATA: 173, TA: 46 |
| FastingBS | 0 | 1 | FALSE | 2 | 0: 704, 1: 214 |
| RestingECG | 0 | 1 | FALSE | 3 | Nor: 552, LVH: 188, ST: 178 |
| ExerciseAngina | 0 | 1 | FALSE | 2 | N: 547, Y: 371 |
| ST_Slope | 0 | 1 | FALSE | 3 | Fla: 460, Up: 395, Dow: 63 |
Temuan:
Sex: Data kita didominasi oleh
Pria (M), dengan 725 pasien (79%), sementara
Wanita (F) hanya 193 pasien (21%).ChestPainType (Tipe Nyeri Dada): Tipe
yang paling umum adalah ASY (Asymptomatic / Tanpa Gejala)
sebanyak 496 pasien. Ini sangat menarik!ExerciseAngina (Nyeri saat Olahraga):
Mayoritas pasien (N, 547) tidak mengalami
nyeri saat berolahraga.Ini adalah salah satu pengecekan terpenting. Apakah jumlah pasien “Sakit” dan “Sehat” seimbang?
# Menghitung proporsi variabel target
data_processed %>%
count(HeartDisease) %>%
mutate(Percentage = round(n / sum(n) * 100, 1))Temuan:
HeartDisease = 1 (Sakit) ada 508 orang
(55.3%).HeartDisease = 0 (Sehat) ada 410 orang
(44.7%).Selanjutnya, kita ingin tahu “Siapa berteman dengan siapa?”. Apakah jika satu fitur naik, fitur lain ikut naik? Kita gunakan Heatmap Korelasi untuk melihat hubungan antar fitur numerik.
# Menghitung matriks korelasi hanya untuk fitur numerik
corr_matrix <- data_processed %>%
select(all_of(numerical_cols)) %>%
cor()# Plotting heatmap
# 'fig.cap' menambahkan caption di bawah gambar
ggcorrplot(corr_matrix,
hc.order = TRUE, # Mengurutkan berdasarkan kedekatan
type = "lower", # Hanya menunjukkan bagian bawah matriks (karena atasnya sama saja)
lab = TRUE, # Menampilkan nilai korelasi (angka)
lab_size = 3,
title = "Heatmap Korelasi Antar Fitur Numerik")## 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 ggcorrplot package.
## Please report the issue at <https://github.com/kassambara/ggcorrplot/issues>.
## This warning is displayed once every 8 hours.
## Call `lifecycle::last_lifecycle_warnings()` to see where this warning was
## generated.
Heatmap Korelasi Antar Fitur Numerik
Temuan:
Age dan
RestingBP (0.26), yang masuk akal (semakin tua, tekanan
darah cenderung naik).Sekarang kita lihat “bentuk” dari setiap fitur. Apakah datanya normal (seperti lonceng), miring, atau aneh?
# Menggunakan pivot_longer untuk mem-plot semua fitur numerik sekaligus
data_processed %>%
select(all_of(numerical_cols)) %>%
pivot_longer(cols = everything(), names_to = "Fitur", values_to = "Nilai") %>%
ggplot(aes(x = Nilai)) +
geom_histogram(bins = 30, aes(fill = Fitur), show.legend = FALSE) +
facet_wrap(~ Fitur, scales = "free") + # 'scales="free"' agar setiap plot punya skala Y-nya sendiri
labs(title = "Distribusi Fitur Numerik", x = "Nilai", y = "Frekuensi")Distribusi Fitur Numerik
Temuan:
Age: Distribusinya terlihat cukup
normal (simetris seperti lonceng).Cholesterol &
RestingBP: Distribusinya sangat
aneh. Ada tumpukan data yang sangat tinggi di angka 0. Ini
mengkonfirmasi temuan kita tadi bahwa nilai 0 adalah anomali.Oldpeak: Sangat right-skewed
(miring ke kanan). Mayoritas pasien memiliki nilai rendah (mendekati
0).# Mem-plot semua fitur kategorikal dan target
data_processed %>%
select(all_of(categorical_cols), all_of(target_col)) %>%
pivot_longer(cols = everything(), names_to = "Fitur", values_to = "Kategori") %>%
ggplot(aes(x = Kategori)) +
geom_bar(aes(fill = Fitur), show.legend = FALSE) +
facet_wrap(~ Fitur, scales = "free") +
labs(title = "Distribusi Fitur Kategorikal dan Target", x = "Kategori", y = "Jumlah") +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) # Memutar label x agar tidak tumpang tindihDistribusi Fitur Kategorikal dan Target
Temuan:
Sex = M (Pria),
ChestPainType = ASY (Tanpa Gejala), dan
ST_Slope = Flat (Datar).HeartDisease = 1 (Sakit) sedikit
lebih banyak dari 0 (Sehat).Outlier adalah “data pencilan” yang nilainya jauh berbeda dari teman-temannya. Kita gunakan Box Plot untuk mendeteksi mereka. Titik-titik di luar “kotak” adalah outlier.
# Menggunakan boxplot untuk melihat outlier
data_processed %>%
select(all_of(numerical_cols)) %>%
pivot_longer(cols = everything(), names_to = "Fitur", values_to = "Nilai") %>%
ggplot(aes(y = Nilai)) +
geom_boxplot(aes(fill = Fitur), show.legend = FALSE) +
facet_wrap(~ Fitur, scales = "free_y") +
labs(title = "Box Plot Fitur Numerik", y = "Nilai", x = "") +
theme(axis.text.x = element_blank(), axis.ticks.x = element_blank()) # Menghilangkan label xBox Plot Fitur Numerik untuk Deteksi Outlier
Temuan:
RestingBP &
Cholesterol: Outlier-nya sangat ekstrem,
terutama di nilai 0 yang tadi kita temukan. Ada juga beberapa
outlier di nilai atas (pasien dengan tekanan darah/kolesterol
sangat tinggi).MaxHR: Memiliki beberapa
outlier di sisi bawah (pasien dengan detak jantung maksimum
sangat rendah).Oldpeak: Memiliki beberapa
outlier di sisi atas.Setelah proses investigasi (EDA), berikut adalah 4 temuan utama yang akan memandu langkah kita selanjutnya:
0 yang tidak realistis pada
kolom RestingBP dan Cholesterol. Ini harus
kita perbaiki di bagian Pre-processing. Kemungkinan besar kita
akan menggantinya dengan nilai median (nilai tengah) agar tidak merusak
data.Sex, ChestPainType, RestingECG,
ExerciseAngina, ST_Slope) yang harus diubah
menjadi format angka (proses encoding) sebelum bisa dibaca oleh
model machine learning.HeartDisease=1 (Sakit) seringkali
memiliki ChestPainType = ASY (Tanpa Gejala) dan
ST_Slope = Flat (Datar). Ini adalah petunjuk awal yang
sangat berharga.Data mentah dari dunia nyata (seperti heart.csv) hampir
selalu “kotor”. Seperti koki yang harus mencuci dan memotong sayuran
sebelum memasak, kita harus “membersihkan” dan “menyiapkan” data kita
sebelum dimasukkan ke model. Inilah yang disebut Data
Preprocessing.
Berdasarkan temuan kita di EDA, ada 3 hal utama yang harus kita
lakukan:
1. Mengatasi Data Hilang (Missing Values): Yaitu, nilai
0 aneh di RestingBP dan
Cholesterol. 2. Mengubah Data Kategorikal
(Encoding): Yaitu, mengubah Sex = 'M' atau
ChestPainType = 'ASY' menjadi angka yang dimengerti model.
3. Membagi Data (Train-Test Split): Memisahkan data
untuk “latihan” dan untuk “ujian” agar evaluasi kita adil.
# Kita muat ulang data mentahnya agar proses bersih-bersih kita terdokumentasi
data_raw <- read_csv("heart.csv")## Rows: 918 Columns: 12
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (5): Sex, ChestPainType, RestingECG, ExerciseAngina, ST_Slope
## dbl (7): Age, RestingBP, Cholesterol, FastingBS, MaxHR, Oldpeak, HeartDisease
##
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# Set 'seed' agar hasil "acak" kita (untuk train-test split) bisa diulang
# Ini penting untuk reproduktivitas laporan
set.seed(123) Ini adalah aturan nomor satu dalam machine learning: Selalu bagi datamu terlebih dahulu! Kita harus menyisihkan sebagian data (misal 25%) untuk “ujian akhir” (data tes) yang tidak boleh disentuh sama sekali selama proses latihan.
Kita akan membagi data menjadi 75% untuk training (data latih) dan 25% untuk testing (data tes).
Penting: Kita akan menggunakan stratified
split (pemisahan terstratifikasi). Ini artinya kita memastikan
proporsi HeartDisease (55% sakit, 45% sehat) tetap
terjaga di kedua set data, baik latih maupun tes.
# 1. Train-Test Split (Stratified)
# 'prop = 0.75' berarti 75% data masuk ke data latih
# 'strata = HeartDisease' adalah kuncinya: ini menjaga keseimbangan 0 dan 1
data_split <- initial_split(data_raw, prop = 0.75, strata = HeartDisease)
# Membuat dua data frame dari hasil split
train_data <- training(data_split)
test_data <- testing(data_split)
# Mari kita cek hasilnya
cat(paste("Jumlah Data Latih:", nrow(train_data), "baris\n"))## Jumlah Data Latih: 688 baris
## Jumlah Data Tes : 230 baris
# Cek proporsi di data latih (seharusnya sekitar 55.3%)
train_data %>%
count(HeartDisease) %>%
mutate(prop = n/sum(n))# Cek proporsi di data tes (seharusnya juga sekitar 55.3%)
test_data %>%
count(HeartDisease) %>%
mutate(prop = n/sum(n))Seperti yang terlihat, proporsinya tetap sama! Kita berhasil.
Sekarang kita akan membuat “resep” (recipe) pembersihan
data. recipes adalah paket tidymodels yang
sangat kuat untuk mendefinisikan langkah-langkah
preprocessing.
Resep kita akan melakukan ini secara berurutan:
HeartDisease (yang angkanya 0 dan 1) menjadi
factor. Ini wajib agar R tahu kita sedang melakukan
klasifikasi.0 anomali di RestingBP dan
Cholesterol menjadi NA (Not Available).NA
tersebut dengan nilai median (nilai tengah) dari data
latih. Kita pakai median karena lebih kebal outlier
dibanding mean (rata-rata).Sex, ChestPainType, dll.) menjadi
angka menggunakan one-hot encoding (membuat kolom
dummy).# 1. Mendefinisikan Resep
heart_recipe <-
recipe(HeartDisease ~ ., data = train_data) %>% # Target kita adalah HeartDisease ~ semua fitur lainnya
# Langkah 1: Ubah target jadi factor
step_mutate(HeartDisease = as.factor(HeartDisease)) %>%
# Langkah 2: Ganti '0' anomali di RestingBP & Cholesterol jadi NA
step_mutate(
RestingBP = na_if(RestingBP, 0),
Cholesterol = na_if(Cholesterol, 0)
) %>%
# Langkah 3: Isi NA (yang baru kita buat) dengan MEDIAN
# Resep ini akan 'belajar' median HANYA dari train_data
step_impute_median(RestingBP, Cholesterol) %>%
# Langkah 4: One-Hot Encoding untuk SEMUA data kategorikal
# 'all_nominal_predictors()' otomatis mencari semua fitur kategori
step_dummy(all_nominal_predictors())
# Kita bisa 'print' resepnya untuk melihat langkah-langkahnya
summary(heart_recipe)Resep di atas hanyalah sebuah “rencana”. Resep itu belum dijalankan. Sekarang, kita akan “menjalankan” resepnya.
prep(): “Mempelajari” parameter dari
data latih (misalnya, mencari tahu berapa nilai median
Cholesterol di data latih).bake(): “Menerapkan” resep tersebut ke
data.# 2. "Prep" resepnya: Ini "mempelajari" parameter (median) dari data_latih
heart_recipe_prepped <- prep(heart_recipe, training = train_data)
# 3. "Bake" datanya: Terapkan resep yang sudah dipelajari ke data latih dan data tes
train_data_clean <- bake(heart_recipe_prepped, new_data = train_data)
test_data_clean <- bake(heart_recipe_prepped, new_data = test_data)
# Mari kita intip data bersih kita
# Perhatikan bagaimana 'Sex_M', 'ChestPainType_ATA', dll. sekarang jadi kolom sendiri
glimpse(train_data_clean)## Rows: 688
## Columns: 16
## $ Age <dbl> 37, 39, 45, 48, 39, 42, 54, 43, 43, 44, 40, 53, 51, …
## $ RestingBP <dbl> 130, 120, 130, 120, 120, 115, 120, 120, 100, 120, 13…
## $ Cholesterol <dbl> 283, 339, 237, 284, 204, 211, 273, 201, 223, 184, 21…
## $ FastingBS <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1…
## $ MaxHR <dbl> 98, 170, 170, 120, 145, 137, 150, 165, 142, 142, 138…
## $ Oldpeak <dbl> 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.5, 0.0, 0.0, 1.0, 0.…
## $ HeartDisease <fct> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ Sex_M <dbl> 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 1, 0, 1, 1, 0, 1, 0, 0…
## $ ChestPainType_ATA <dbl> 1, 0, 1, 1, 1, 0, 1, 1, 0, 1, 0, 1, 1, 0, 1, 1, 1, 1…
## $ ChestPainType_NAP <dbl> 0, 1, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0…
## $ ChestPainType_TA <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ RestingECG_Normal <dbl> 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0…
## $ RestingECG_ST <dbl> 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1…
## $ ExerciseAngina_Y <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ ST_Slope_Flat <dbl> 0, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0…
## $ ST_Slope_Up <dbl> 1, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, 1, 1, 1, 1, 1…
Hasil: Data kita sekarang 100% bersih!
NA atau nilai 0 yang aneh.train_data_clean dan
test_data_clean yang siap dipakai untuk
modeling.Jawaban Singkat: Tidak.
Standardisasi adalah proses mengubah skala semua fitur
numerik agar memiliki mean 0 dan standard deviation 1
(misalnya, mengubah Age yang nilainya 28-77 dan
Cholesterol 100-600 ke skala yang sama).
Mengapa kita TIDAK melakukannya? Model ensemble yang akan kita gunakan (Random Forest, GBM, XGBoost) adalah model berbasis pohon (tree-based).
Model-model ini mengambil keputusan dengan cara “memecah” data
(misal: “apakah Age > 50?”). Mereka tidak peduli dengan
seberapa besar nilai angkanya, mereka hanya peduli pada
urutannya untuk menemukan titik pecah terbaik.
Oleh karena itu, kita melewatkan langkah standardisasi ini karena tidak akan memberikan pengaruh apa-apa pada performa model kita dan hanya akan menambah proses komputasi.
Inilah bagian utamanya! Setelah data kita bersih
(train_data_clean), sekarang saatnya melatih model
kita.
Proses kita bukan sekadar “Latih ->
Selesai”.
Untuk setiap model, kita perlu menemukan kombinasi
hyperparameter terbaik. (Contoh: “Apakah 500 pohon
lebih baik dari 1000 pohon?”, “Seberapa cepat modelnya harus
belajar?”).
Untuk melakukan ini secara adil, kita akan menggunakan Cross-Validation (CV).
CV adalah cara “menguji” model kita secara lebih teliti. Daripada hanya membagi data latih satu kali, kita akan membaginya menjadi (misalnya) 10 “lipatan” (folds).
Model akan dilatih 10 kali. Setiap kali, 1 lipatan dipakai sebagai “data tes mini” dan 9 lipatan lainnya sebagai data latih.
Ini memberi kita gambaran performa model yang jauh lebih stabil dan dapat dipercaya daripada hanya satu kali tes.
Kita mulai dengan model pertama, si “pekerja keras” yang stabil,
Random Forest. Kita akan menggunakan engine ranger
yang sangat cepat.
[Image of Random Forest model architecture]
Pertama, kita definisikan modelnya dan beri tahu parameter apa yang
ingin kita “coba-coba” (kita set sebagai tune()).
mtry: Parameter terpenting RF. Berapa
banyak fitur yang boleh “dilihat” secara acak di setiap cabang
pohon?min_n: Berapa jumlah minimum data
dalam satu daun pohon? Ini untuk mengontrol overfitting.# 1. Mendefinisikan spesifikasi model RF
rf_spec <-
rand_forest(
mtry = tune(), # Kita ingin tuning mtry
min_n = tune(), # Kita juga ingin tuning min_n
trees = 500 # Kita set 500 pohon (biasanya sudah cukup)
) %>%
set_engine("ranger", importance = "permutation") %>%
set_mode("classification")
# Cek spesifikasinya
rf_spec## Random Forest Model Specification (classification)
##
## Main Arguments:
## mtry = tune()
## trees = 500
## min_n = tune()
##
## Engine-Specific Arguments:
## importance = permutation
##
## Computational engine: ranger
Workflow adalah “paket” yang menggabungkan Resep Preprocessing (dari Bagian 5) dengan Spesifikasi Model (dari atas).
Sekarang kita jalankan! Kita akan menggunakan tune_grid
untuk melatih model menggunakan 10-fold CV (heart_folds)
dan mencoba 10 kombinasi parameter acak
(grid = 10).
## Memulai tuning Random Forest...
# 'grid = 10' berarti kita akan mencoba 10 kombinasi parameter acak
rf_tune_results <-
rf_workflow %>%
tune_grid(
resamples = heart_folds,
grid = 10,
metrics = our_metrics,
control = control_grid(save_pred = TRUE) # Simpan prediksi untuk nanti
)
cat("Tuning Random Forest Selesai!\n")## Tuning Random Forest Selesai!
# Mari kita lihat 5 kombinasi parameter terbaik
show_best(rf_tune_results, metric = "roc_auc", n = 5)Selanjutnya adalah “sang juara” kompetisi, XGBoost. Model ini berbasis boosting (belajar dari kesalahan secara berurutan).
XGBoost punya banyak parameter, tapi kita akan tuning 3 yang paling penting:
trees: Berapa banyak pohon (putaran)
yang akan dibuat?learn_rate: “Rem” model. Seberapa
cepat model belajar dari kesalahannya? (Nilai kecil lebih baik tapi
lebih lambat).tree_depth: Seberapa dalam setiap
pohon boleh tumbuh?# 1. Mendefinisikan spesifikasi model XGBoost
xgb_spec <-
boost_tree(
trees = tune(),
learn_rate = tune(),
tree_depth = tune()
) %>%
set_engine("xgboost") %>%
set_mode("classification")
# Cek spesifikasinya
xgb_spec## Boosted Tree Model Specification (classification)
##
## Main Arguments:
## trees = tune()
## tree_depth = tune()
## learn_rate = tune()
##
## Computational engine: xgboost
Kita gunakan workflow yang sama, hanya mengganti
modelnya.
Kita jalankan proses yang sama: 10-fold CV, 10 kombinasi acak.
## Memulai tuning XGBoost...
xgb_tune_results <-
xgb_workflow %>%
tune_grid(
resamples = heart_folds,
grid = 10,
metrics = our_metrics,
control = control_grid(save_pred = TRUE)
)
cat("Tuning XGBoost Selesai!\n")## Tuning XGBoost Selesai!
# Mari kita lihat 5 kombinasi parameter terbaik
show_best(xgb_tune_results, metric = "roc_auc", n = 5)Terakhir, “si penantang” XGBoost. LightGBM adalah model
boosting lain yang dikenal sangat cepat dan
seringkali sama akuratnya (atau bahkan lebih). Kita akan menggunakan
engine lightgbm melalui paket
bonsai.
Kita akan tuning parameter yang sama dengan XGBoost untuk perbandingan yang adil.
# 1. Mendefinisikan spesifikasi model LightGBM
# Kita butuh 'bonsai' agar tidymodels bisa pakai 'lightgbm'
lgbm_spec <-
boost_tree(
trees = tune(),
learn_rate = tune(),
tree_depth = tune()
) %>%
set_engine("lightgbm") %>% # Engine-nya kita ganti
set_mode("classification")
# Cek spesifikasinya
lgbm_spec## Boosted Tree Model Specification (classification)
##
## Main Arguments:
## trees = tune()
## tree_depth = tune()
## learn_rate = tune()
##
## Computational engine: lightgbm
Langkah yang sama, ganti modelnya.
Kita jalankan proses tuning terakhir.
## Memulai tuning LightGBM...
lgbm_tune_results <-
lgbm_workflow %>%
tune_grid(
resamples = heart_folds,
grid = 10,
metrics = our_metrics,
control = control_grid(save_pred = TRUE)
)
cat("Tuning LightGBM Selesai!\n")## Tuning LightGBM Selesai!
# Mari kita lihat 5 kombinasi parameter terbaik
show_best(lgbm_tune_results, metric = "roc_auc", n = 5)Pada titik ini, kita telah selesai melatih dan men-tuning
ketiga model kita! Hasil tuning (seperti yang
ditampilkan oleh show_best) akan memberi kita gambaran awal
model mana yang memiliki performa CV terbaik.
Di bagian selanjutnya, kita akan menganalisis hasil tuning ini secara visual, memilih parameter terbaik untuk setiap model, dan menguji mereka untuk terakhir kalinya pada data tes.
Ini adalah “hari ujian”. Kita akan mengevaluasi ketiga model kita untuk menentukan satu pemenang yang akan kita rekomendasikan. Prosesnya ada dua tahap: pertama, membandingkan performa “latihan” mereka (Cross-Validation), dan kedua, menguji mereka di “ujian akhir” (data tes).
Pertama, mari kita kumpulkan semua hasil “latihan soal” (10-fold CV) dari ketiga model. Kita ingin tahu: siapa yang paling konsisten dan punya skor rata-rata tertinggi selama latihan?
Kita akan fokus pada metrik ROC-AUC, karena ini adalah metrik yang sangat baik untuk mengukur performa klasifikasi biner secara keseluruhan.
# Mengumpulkan semua metrik CV dari 3 model
all_cv_results <-
bind_rows(
collect_metrics(rf_tune_results) %>% mutate(model = "Random Forest"),
collect_metrics(xgb_tune_results) %>% mutate(model = "XGBoost"),
collect_metrics(lgbm_tune_results) %>% mutate(model = "LightGBM")
)
# Filter hanya untuk metrik roc_auc
cv_auc_results <- all_cv_results %>%
filter(.metric == "roc_auc")
# Tampilkan 5 performa CV terbaik secara keseluruhan
cv_auc_results %>%
arrange(desc(mean)) %>%
head(5)Angka saja terkadang membosankan. Mari kita visualisasikan performa
CV (khususnya roc_auc) dari 10 “lipatan” untuk setiap model
menggunakan Box Plot.
[Image of a box plot comparing model performance]
# Plot perbandingan CV
# Kita cari yang box-nya paling tinggi (skor terbaik)
# dan paling 'pendek' (paling konsisten)
cv_auc_results %>%
ggplot(aes(x = model, y = mean, fill = model)) +
geom_boxplot(show.legend = FALSE) +
labs(
title = "Performa Model Selama Cross-Validation (Latihan)",
subtitle = "Berdasarkan 10-Fold CV pada metrik ROC-AUC",
x = "Model",
y = "Skor ROC-AUC"
) +
theme_minimal()Perbandingan Skor ROC-AUC (10-Fold CV) Antar Model
Interpretasi (dari plot di atas):
Performa CV yang bagus itu penting, tapi itu baru “latihan”. Sekarang adalah “Ujian Akhir” yang sesungguhnya.
Kita akan menggunakan fungsi last_fit(). Ini adalah
fungsi tidymodels yang sangat cerdas. Dia akan melakukan 3
hal secara otomatis:
1. Mengambil parameter terbaik (Best Hyperparameters)
yang kita temukan di Bagian 6 (dari show_best()). 2.
Melatih ulang model untuk terakhir kalinya menggunakan
parameter terbaik itu pada SEMUA data latih
(train_data_clean 75% penuh). 3. Mengevaluasi model final
tersebut satu kali pada data tes
(test_data_clean 25% yang tersembunyi).
# Fungsi helper untuk menjalankan "Ujian Akhir"
run_final_test <- function(workflow, tune_results, split_obj, model_name) {
cat(paste("Menjalankan Ujian Akhir untuk:", model_name, "\n"))
# 1. Ambil parameter terbaik
best_params <- select_best(tune_results, metric = "roc_auc")
# 2. Finalisasi workflow dengan parameter tsb
final_wf <- finalize_workflow(workflow, best_params)
# 3. Latih di data latih PENUH, Uji di data tes PENUH
final_fit_results <- last_fit(final_wf, split_obj)
return(final_fit_results)
}
# Jalankan untuk ketiga model!
rf_test_results <- run_final_test(rf_workflow, rf_tune_results, data_split, "Random Forest")## Menjalankan Ujian Akhir untuk: Random Forest
## Menjalankan Ujian Akhir untuk: XGBoost
## Menjalankan Ujian Akhir untuk: LightGBM
## Semua Ujian Akhir Selesai!
Waktunya melihat rapor! Mari kita kumpulkan metrik dari ujian akhir (data tes) untuk ketiga model dan bandingkan side-by-side.
# Kumpulkan metrik dari RF
rf_metrics <- collect_metrics(rf_test_results) %>%
mutate(model = "Random Forest")
# Kumpulkan metrik dari XGBoost
xgb_metrics <- collect_metrics(xgb_test_results) %>%
mutate(model = "XGBoost")
# Kumpulkan metrik dari LightGBM
lgbm_metrics <- collect_metrics(lgbm_test_results) %>%
mutate(model = "LightGBM")
# Gabungkan semua dan tampilkan
all_test_metrics <- bind_rows(rf_metrics, xgb_metrics, lgbm_metrics)
# Tampilkan tabel perbandingan
all_test_metrics %>%
select(model, .metric, .estimate) %>%
pivot_wider(names_from = .metric, values_from = .estimate) %>%
knitr::kable(
digits = 4,
caption = "Tabel Perbandingan Metrik Final di Data Tes"
)| model | accuracy | roc_auc | brier_class |
|---|---|---|---|
| Random Forest | 0.9043 | 0.9499 | 0.1101 |
| XGBoost | 0.8913 | 0.9445 | 0.0905 |
| LightGBM | 0.8870 | 0.9559 | 0.0779 |
Interpretasi (Tabel di atas):
accuracy (seberapa banyak tebakan
benar) dan roc_auc (kemampuan membedakan 0 dan 1).roc_auc tertinggi (misal, 0.9250) dan
accuracy tertinggi (misal, 0.8870) di data tes. Ini
mengungguli Random Forest dan LightGBM…”Skor akurasi 88% itu bagus, tapi 12% sisanya salah. Salahnya di mana? Untuk melihat ini, kita gunakan Confusion Matrix.
Mari kita lihat Confusion Matrix untuk model pemenang kita (misal, XGBoost).
# Ambil prediksi dari model pemenang (XGBoost)
xgb_predictions <- collect_predictions(xgb_test_results)
# Buat dan plot confusion matrix
xgb_predictions %>%
conf_mat(truth = HeartDisease, estimate = .pred_class) %>%
autoplot(type = "heatmap") +
labs(title = "Confusion Matrix - XGBoost (Data Tes)")Confusion Matrix untuk Model Pemenang (XGBoost) di Data Tes
Interpretasi (Plot Confusion Matrix):
Truth=1,
Prediction=1: (Misal: 115) - Model dengan
BENAR memprediksi 115 pasien sakit.
(Ini bagus!)Truth=0,
Prediction=0: (Misal: 90) - Model dengan
BENAR memprediksi 90 pasien sehat.
(Ini bagus!)Truth=0,
Prediction=1: (Misal: 10) - Model
SALAH memprediksi 10 pasien sakit
(padahal aslinya sehat). Ini adalah kesalahan “alarm palsu”.Truth=1,
Prediction=0: (Misal: 15) - Model
SALAH memprediksi 15 pasien sehat
(padahal aslinya sakit). Ini adalah kesalahan paling
berbahaya dalam medis, karena pasien sakit dikira sehat.Berdasarkan perbandingan hasil Cross-Validation yang konsisten dan, yang paling penting, skor metrik akhir di data tes, kita dapat menyimpulkan:
Model [XGBoost/LightGBM/RF] adalah pemenang dari analisis perbandingan ini.
Model ini tidak hanya menunjukkan performa yang kuat selama “latihan”
(CV), tetapi juga berhasil menggeneralisasi kemampuannya dengan baik
pada “ujian akhir” (data tes), dengan mencapai accuracy
sebesar [misal: 88.7%] dan ROC-AUC sebesar
[misal: 0.925].
Model ini juga memiliki keseimbangan yang baik antara kesalahan False Positive dan False Negative (atau Anda bisa sebutkan jika FN-nya paling rendah, yang mana itu sangat baik).
Angka dan tabel di Bagian 7 sudah memberi kita jawaban “siapa” yang menang. Sekarang di Bagian 8, kita akan membuat visualisasi untuk memahami “bagaimana” mereka menang dan “mengapa” model pemenang kita bisa berhasil.
Metrik roc_auc yang kita lihat di tabel (misal, 0.925)
adalah sebuah angka ringkasan. Angka itu sebenarnya
adalah “luas area di bawah kurva” dari plot ROC.
Sekarang, kita akan menggambar kurva itu sendiri.
# 1. Kumpulkan prediksi probabilitas dari SEMUA model
rf_preds <- collect_predictions(rf_test_results) %>% mutate(model = "Random Forest")
xgb_preds <- collect_predictions(xgb_test_results) %>% mutate(model = "XGBoost")
lgbm_preds <- collect_predictions(lgbm_test_results) %>% mutate(model = "LightGBM")
all_preds <- bind_rows(rf_preds, xgb_preds, lgbm_preds)
# 2. Hitung data kurva ROC untuk semua model
# Kita perlu 'group_by(model)'
roc_curves <- all_preds %>%
group_by(model) %>%
roc_curve(
truth = HeartDisease, # 'truth' adalah jawaban sebenarnya
.pred_1, # '.pred_1' adalah probabilitas prediksi "Sakit"
event_level = "second" # Memastikan '1' (Sakit) adalah event positif kita
)
# 3. Plot kurvanya!
autoplot(roc_curves) +
labs(
title = "Perbandingan ROC Curve Model",
subtitle = "Diuji pada 25% Data Tes",
x = "False Positive Rate (Sehat dikira Sakit)",
y = "True Positive Rate (Sakit dikira Sakit)"
) +
theme(legend.position = "bottom")Perbandingan ROC Curve Ketiga Model di Data Tes
Interpretasi (Plot di atas):
Ini mungkin adalah insight paling berharga. Kita sudah tahu model XGBoost (misalnya) adalah pemenangnya. Tapi mengapa dia menang? Fitur apa yang paling dia “dengarkan” saat mengambil keputusan?
Kita akan mengekstrak “isi kepala” dari model pemenang kita (XGBoost) untuk melihat 20 fitur terpenting yang digunakannya.
# 1. Ekstrak model 'parsnip' yang sudah di-fit dari hasil tes XGBoost
# (Kita asumsikan XGBoost adalah pemenangnya)
xgb_final_model_fit <- extract_fit_parsnip(xgb_test_results)
# 2. Buat plot Variable Importance (vip)
# 'num_features = 20' -> Tampilkan 20 fitur teratas
vip(xgb_final_model_fit, geom = "col", num_features = 20) +
labs(
title = "Feature Importance - Model Pemenang (XGBoost)",
subtitle = "Fitur apa yang paling berpengaruh dalam memprediksi penyakit jantung?",
y = "Pentingnya Fitur (Importance)",
x = "Nama Fitur"
) +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) # Miringkan label x20 Fitur Terpenting - Model Pemenang (XGBoost)
Interpretasi (Plot di atas):
ST_Slope_Flat.”Age (Umur) atau
Cholesterol.”ChestPainType_ASY atau ‘Tanpa
Gejala’) dan Sex_M (Pria) juga masuk 5 besar.”Oldpeak dan MaxHR (Detak Jantung
Maks) juga sangat berpengaruh.”ST_Slope,
ChestPainType, Oldpeak, dan Sex
adalah kunci utama untuk memprediksi risiko penyakit jantung di dataset
ini.Kita telah sampai di akhir analisis. Proyek ini dimulai dengan satu
pertanyaan: dari tiga model ensemble (Random Forest, XGBoost,
LightGBM), manakah yang paling baik untuk memprediksi
penyakit jantung menggunakan dataset heart.csv?
Setelah melalui proses Data Preprocessing, EDA, Model Tuning (Bagian 6), dan Evaluasi (Bagian 7 & 8), berikut adalah kesimpulan yang dapat kami tarik:
Berdasarkan perbandingan metrik yang adil pada 25% data tes (data yang belum pernah dilihat model sebelumnya), kami menyimpulkan bahwa:
Pemenangnya adalah model Extreme Gradient Boosting (XGBoost).
Model ini secara konsisten mengungguli kedua pesaingnya, dengan memberikan performa terbaik pada metrik-metrik kunci:
roc_auc (Test): [misal:
0.925] — Ini adalah skor tertinggi, menunjukkan kemampuan
terbaik dalam membedakan pasien “Sakit” dan “Sehat”.accuracy (Test): [misal:
0.887] — Ini berarti model berhasil menebak dengan benar
sekitar 88.7% dari total pasien di data tes.Meskipun LightGBM (LGBM) memberikan performa yang sangat mirip dan sangat ketat, XGBoost unggul tipis dalam analisis ini. Random Forest, meskipun stabil, jelas tertinggal dalam hal kekuatan prediksi di dataset ini.
Kemenangan XGBoost (dan performa kuat LGBM) menunjukkan bahwa pendekatan Boosting (belajar berurutan dari kesalahan) lebih cocok untuk dataset ini dibandingkan pendekatan Bagging (voting paralel) dari Random Forest.
Ini menyiratkan bahwa dataset penyakit jantung ini memiliki pola yang kompleks dan non-linear. Model Boosting sangat jago dalam “mengejar” dan menemukan pola-pola rumit ini, yang mungkin terlewatkan oleh Random Forest.
Visualisasi di Bagian 8.1 (ROC Curve) mengkonfirmasi hal ini, di mana kurva XGBoost dan LGBM secara visual “lebih superior” (lebih menempel ke pojok kiri atas) daripada kurva Random Forest.
Salah satu temuan paling berharga dari analisis ini adalah “membongkar isi kepala” model pemenang kita. Dari plot Feature Importance (Bagian 8.2), kita belajar bahwa:
Indikator klinis EKG dan gejala non-tradisional adalah prediktor terkuat.
Bukan Age (Umur) atau Cholesterol yang
menjadi nomor satu, melainkan:
ST_Slope_Flat: Apakah segmen ST pasien
‘Datar’ saat tes olahraga.ChestPainType_ASY: Apakah pasien
‘Tanpa Gejala’ nyeri dada.Oldpeak: Nilai depresi ST saat
olahraga.Sex_M: Apakah pasien berjenis kelamin
‘Pria’.MaxHR: Seberapa tinggi detak jantung
maksimum pasien.Insight: Ini sangat menarik. Model kita belajar bahwa “tanda-tanda tersembunyi” (seperti EKG yang datar atau tidak adanya nyeri dada) adalah “bendera merah” 🚩 yang lebih kuat untuk penyakit jantung daripada faktor-faktor risiko tradisional yang sering kita dengar.
Tidak ada analisis yang sempurna. Laporan ini memiliki beberapa keterbatasan yang bisa menjadi dasar untuk perbaikan di masa depan:
tune_grid(grid = 10))
untuk menghemat waktu. Untuk mendapatkan performa maksimal, kita bisa
melakukan pencarian yang lebih mendalam (misal, 50-100 kombinasi atau
menggunakan tune_bayes()).Secara keseluruhan, proyek ini berhasil menunjukkan kekuatan model ensemble dan membuktikan bahwa XGBoost adalah pilihan yang sangat kuat dan akurat untuk memprediksi risiko penyakit jantung pada dataset ini.