1. Pengenalan Ensemble Models

Apa itu Ensemble Models?

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.

Mengapa Menggunakan Ensemble?

Tujuan utamanya adalah untuk memperbaiki dua masalah umum pada model:

  1. Mengurangi Bias (Prediksi yang kurang akurat): Model yang terlalu sederhana seringkali “kurang pas” dengan data.
  2. Mengurangi Variance (Prediksi yang terlalu sensitif): Model yang terlalu kompleks akan overfitting (terlalu hafal data latihan) dan prediksinya kacau saat bertemu data baru.

Ensemble models menyeimbangkan keduanya. Secara umum, ada dua strategi utama untuk menggabungkan model:

A. Bagging (Bekerja Paralel)

  • Nama Lengkap: Bootstrap Aggregating.
  • Konsep: Model-model “bekerja sendiri-sendiri” secara paralel di saat yang sama.
  • Cara Kerja:
    1. Membuat beberapa “kantong” data acak dari data latihan (disebut bootstrap sample).
    2. Melatih satu model (misal, satu Decision Tree) untuk setiap “kantong” data.
    3. Hasil akhir didapat dari voting (suara terbanyak) dari semua model tersebut.
  • Fokus Utama: Mengurangi Variance. Karena hasilnya adalah rata-rata/voting, model jadi lebih stabil dan tidak overfitting.
  • Contoh Populer: Random Forest.

B. Boosting (Bekerja Sekuensial)

  • Konsep: Model-model “bekerja sama” secara berurutan (sekuensial).
  • Cara Kerja:
    1. Model pertama dibuat dan mencoba memprediksi.
    2. Model kedua dibuat, tapi fokus utamanya adalah belajar dari kesalahan yang dibuat oleh model pertama.
    3. Model ketiga dibuat, fokus belajar dari kesalahan gabungan model 1 dan 2.
    4. Seterusnya, setiap model baru memperbaiki kesalahan model-model sebelumnya.
  • Fokus Utama: Mengurangi Bias. Karena modelnya terus-menerus dikoreksi, hasil akhirnya jadi sangat akurat.
  • Contoh Populer: GBM dan XGBoost.

[Image of Bagging vs Boosting in machine learning]

1.1 Random Forest (RF)

Pengertian

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.

Cara Kerja

  1. Bootstrap: Membuat B sampel bootstrap (kantong data acak dengan penggantian) dari data latihan.
  2. Train Trees: Melatih satu Decision Tree untuk setiap sampel bootstrap. Saat pohon bertumbuh, di setiap cabangnya (split), pohon hanya boleh memilih fitur terbaik dari subset fitur yang diacak (misal, dari 10 fitur, pohon hanya boleh pilih dari 3 fitur acak).
  3. Voting: Untuk memprediksi data baru, data tersebut “ditanyakan” ke setiap pohon di hutan. Jawaban akhir adalah suara terbanyak (voting) dari semua pohon.

Rumus dan Keterangan

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)\}\]

  • \(\hat{y}\) : Prediksi kelas akhir.
  • \(\text{mode}\) : Modus, atau nilai (kelas) yang paling sering muncul.
  • \(T_b(x)\) : Prediksi dari pohon ke-\(b\) (misal, pohon ke-1, ke-2, dst.).
  • \(B\) : Jumlah total pohon dalam forest.

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)\]

  • \(\hat{y}\) : Prediksi nilai akhir.
  • \(B\) : Jumlah total pohon.
  • \(\sum\) : Simbol sigma (penjumlahan).
  • \(\frac{1}{B}\) : Dibagi \(B\) untuk mendapatkan rata-rata.

Karakteristik Utama

  • Kelebihan:
    • Sangat jago mengurangi overfitting.
    • Stabil dan tidak sensitif terhadap outlier atau noise.
    • Cepat dilatih karena pohon-pohonnya bisa dibuat secara paralel.
  • Kelemahan:
    • Sulit dijelaskan (less interpretable). Sulit untuk melacak kenapa 500 pohon mengambil keputusan tertentu.

1.2 Gradient Boosting Machines (GBM)

Pengertian

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.

Cara Kerja

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.

Rumus dan Keterangan

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)\]

  • \(F_m(x)\) : Model gabungan pada iterasi ke-\(m\).
  • \(F_{m-1}(x)\) : Model gabungan dari iterasi sebelumnya.
  • \(T_m(x)\) : Pohon baru yang dilatih untuk memprediksi kesalahan \(r_{im}\).
  • \(\nu\) (nu) : Learning Rate. Ini adalah “rem” agar model belajar pelan-pelan (biasanya nilainya kecil, misal 0.1).

Karakteristik Utama

  • Kelebihan:
    • Sangat akurat (seringkali lebih akurat dari Random Forest).
    • Jago mengurangi bias.
  • Kelemahan:
    • Gampang overfitting jika jumlah pohonnya terlalu banyak atau learning rate terlalu besar.
    • Sensitif terhadap hyperparameter.
    • Pelatihan lebih lambat karena harus berurutan (tidak bisa paralel).

1.3 Extreme Gradient Boosting (XGBoost)

Pengertian

XGBoost adalah “GBM versi canggih” (atau GBM “on steroids”). Ini adalah implementasi dari gradient boosting yang telah dioptimalkan secara ekstrem agar…

  1. Lebih Cepat (Sangat cepat).
  2. Lebih Pintar (Punya pencegah overfitting bawaan).

Perbedaan Utama dari GBM

XGBoost adalah GBM, tetapi dengan dua tambahan utama:

  1. 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.

  2. 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.

Rumus dan Keterangan

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)\]

  • \(Obj\) : Fungsi tujuan yang ingin diminimalkan.
  • \(\sum L(...)\) : Training Loss (Bagian 1). Seberapa besar kesalahan model? (Sama seperti GBM).
  • \(\sum \Omega(...)\) : Regularization Term (Bagian 2). Seberapa rumit modelnya? (Ini yang baru!).

“Hukuman” (Regularisasi) \(\Omega\) ini sendiri dihitung dengan: \[\Omega(f) = \gamma T + \frac{1}{2}\lambda \sum_{j=1}^{T} w_j^2\]

  • \(\Omega(f)\) : Hukuman untuk satu pohon \(f\).
  • \(T\) : Jumlah daun di pohon.
  • \(w_j\) : Skor/bobot prediksi di daun ke-\(j\).
  • \(\gamma\) (gamma) : Hukuman untuk jumlah daun (semakin banyak daun, semakin dihukum).
  • \(\lambda\) (lambda) : Hukuman untuk bobot daun (semakin ekstrem skornya, semakin dihukum).

Karakteristik Utama

  • Kelebihan:
    • Sangat Cepat dan efisien.
    • Sangat jago mencegah overfitting berkat regularization (\(\gamma\) dan \(\lambda\)) bawaan.
    • Bisa menangani missing values secara otomatis.
    • Sangat populer dan sering menjadi pemenang di kompetisi data science (seperti Kaggle).
  • Kelemahan:
    • Meskipun sudah canggih, tetap memerlukan hyperparameter tuning yang cermat untuk mendapatkan hasil maksimal.

2. Identifikasi Masalah

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.

2.1 Apa yang Ingin Diprediksi?

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.

2.2 Variabel Target

Untuk membuat model ini, kita perlu membagi data kita menjadi dua jenis:

  1. 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.

  2. Variabel Target (Output): Ini adalah “jawaban” atau output yang ingin kita prediksi. Dalam dataset ini, variabel targetnya adalah kolom bernama HeartDisease.

    • Nilai 1: Berarti pasien didiagnosis menderita penyakit jantung (Risiko Tinggi).
    • Nilai 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.

2.3 Alasan Menggunakan Ensemble Learning

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:

  1. Akurasi Superior: Dengan “berdiskusi”, gabungan banyak model (ensemble) hampir selalu lebih akurat daripada satu model tunggal.
  2. Stabilitas (Anti-Overfitting): Model ensemble (terutama Random Forest) sangat jago menghindari overfitting. Mereka lebih stabil dan bisa diandalkan saat bertemu data baru, tidak hanya “menghafal” data latihan.
  3. Menangani Pola Rumit: Penyakit jantung adalah masalah kompleks. Model Boosting (GBM & XGBoost) sangat hebat dalam menemukan hubungan atau pola yang rumit dan tersembunyi di dalam data, yang mungkin terlewat oleh model lain.

2.4 Tujuan Perbandingan Tiga Model Ensemble

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:

  • Model mana yang memberikan akurasi tertinggi?
  • Model mana yang paling cepat?
  • Bagaimana performa mereka jika dilihat dari metrik lain (seperti Precision dan Recall)?

Pada akhirnya, laporan ini akan memberikan rekomendasi berbasis data tentang ensemble model mana yang paling kami sarankan untuk digunakan dalam memprediksi penyakit jantung.

3. Data Understanding

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.

3.1 Sumber Data

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.

3.2 Jumlah Baris dan Kolom

Setelah kita memuat file heart.csv, kita bisa melihat seberapa besar datanya:

  • Jumlah Baris (Observasi): Ada 918 baris. Ini berarti kita memiliki catatan data dari 918 pasien yang berbeda.
  • Jumlah Kolom (Variabel): Ada 12 kolom. Ini berarti ada 12 jenis informasi (seperti umur, kolesterol, dll.) yang dicatat untuk setiap pasien.

Dari 12 kolom ini, 11 kolom akan menjadi “fitur” (prediktor) dan 1 kolom akan menjadi “target” (yang kita tebak).

3.3 Tipe Data

Tidak semua 12 kolom itu sama. Saat kita memuat datanya, kita bisa melihat ada dua jenis tipe data utama:

  1. Numerik (Angka): Ini adalah kolom yang berisi angka murni yang bisa dihitung (seperti Age, RestingBP, Cholesterol). Total ada 7 kolom numerik.
  2. Kategorikal (Teks/Kategori): Ini adalah kolom yang berisi teks atau kategori (seperti 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.

3.4 Penjelasan Masing-Masing Variabel

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:

  1. Age (Numerik): Umur pasien (dalam tahun).
  2. Sex (Kategorikal): Jenis kelamin pasien (M = Pria, F = Wanita).
  3. 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)
  4. RestingBP (Numerik): Tekanan darah istirahat (dalam mm Hg).
  5. Cholesterol (Numerik): Kadar kolesterol serum (dalam mg/dl).
  6. FastingBS (Numerik): Kadar gula darah puasa.
    • 1: Jika > 120 mg/dl (dianggap punya diabetes)
    • 0: Jika <= 120 mg/dl (normal)
  7. RestingECG (Kategorikal): Hasil rekam jantung (EKG) saat istirahat.
    • Normal: Normal
    • ST: Ada kelainan di gelombang ST-T
    • LVH: Kemungkinan ada pembengkakan ventrikel kiri
  8. MaxHR (Numerik): Detak jantung maksimum yang tercatat (biasanya saat tes olahraga).
  9. ExerciseAngina (Kategorikal): Apakah pasien merasakan nyeri dada saat berolahraga? (Y = Ya, N = Tidak).
  10. Oldpeak (Numerik): Ini adalah nilai EKG yang diukur saat olahraga, menunjukkan seberapa “turun” segmen ST. Ini adalah indikator penting.
  11. ST_Slope (Kategorikal): Kemiringan (slope) segmen ST saat puncak olahraga.
    • Up: Menanjak
    • Flat: Datar
    • Down: Menurun
  12. HeartDisease (Variabel TARGET) (Numerik): Ini adalah kolom jawaban yang ingin kita prediksi.
    • 1: Pasien punya penyakit jantung.
    • 0: Pasien normal (tidak punya penyakit jantung).

4. Exploratory Data Analysis (EDA)

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"

4.1 Statistik Deskriptif (Profil Data)

Pertama, kita lihat “profil” atau “data diri” dari setiap variabel.

Fitur Numerik (Angka)

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()
Data summary
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:
    • AHA! MASALAH! Lihat di bagian min (minimum). Nilai minimum untuk RestingBP dan Cholesterol adalah 0.
    • Secara medis, tidak mungkin seseorang memiliki tekanan darah 0 atau kolesterol 0 (kecuali mereka sudah meninggal, yang mana data ini tidak mencatatnya).
    • Ini adalah missing data (data hilang) yang salah diisi sebagai ‘0’. Ini adalah temuan krusial. Kita harus mencatat ini untuk diperbaiki pada tahap Pre-processing.

Fitur Kategorikal

Sekarang kita lihat profil untuk data kategori.

# Menjalankan skim() pada kolom kategorikal
data_processed %>%
  select(all_of(categorical_cols)) %>%
  skim()
Data summary
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.

Distribusi Variabel Target (Seimbang atau TIdak?)

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:

  • Pasien HeartDisease = 1 (Sakit) ada 508 orang (55.3%).
  • Pasien HeartDisease = 0 (Sehat) ada 410 orang (44.7%).
  • Karena datanya seimbang, kita tidak perlu repot-repot menggunakan teknik sampling (seperti SMOTE atau undersampling) yang rumit. Metrik Akurasi juga akan cukup relevan untuk evaluasi model.

4.2 Korelasi (Hubungan Antar Fitur)

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

Heatmap Korelasi Antar Fitur Numerik

Temuan:

  • Kita mencari angka yang mendekati +1 (korelasi positif, sama-sama naik) atau -1 (korelasi negatif, satu naik satu turun).
  • Secara umum, tidak ada korelasi yang sangat kuat (misalnya, di atas 0.8) antar fitur.
  • Korelasi terkuat yang terlihat adalah antara Age dan RestingBP (0.26), yang masuk akal (semakin tua, tekanan darah cenderung naik).
  • Kesimpulan: Tidak ada masalah multicollinearity (fitur yang tumpang tindih) yang serius. Semua fitur numerik ini bisa kita pakai.

4.3 Distribusi Fitur (Melihat Bentuk Data)

Sekarang kita lihat “bentuk” dari setiap fitur. Apakah datanya normal (seperti lonceng), miring, atau aneh?

Distribusi Fitur Numerik (Histogram)

# 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

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).

Distribusi Fitur Kategorikal (Bar Chart)

# 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 tindih
Distribusi Fitur Kategorikal dan Target

Distribusi Fitur Kategorikal dan Target

Temuan:

  • Plot ini memvisualisasikan apa yang kita lihat di statistik deskriptif.
  • Kita bisa lihat dengan jelas dominasi Sex = M (Pria), ChestPainType = ASY (Tanpa Gejala), dan ST_Slope = Flat (Datar).
  • Kita juga bisa lihat HeartDisease = 1 (Sakit) sedikit lebih banyak dari 0 (Sehat).

4.4 Analisis Outlier (Mencari Data Aneh)

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 x
Box Plot Fitur Numerik untuk Deteksi Outlier

Box 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.
  • Kesimpulan: Data kita memiliki outlier. Model seperti Decision Tree (dan turunannya seperti Random Forest) cukup kebal terhadap outlier. Namun, nilai 0 yang anomali tetap harus kita tangani.

4.5 Insight Utama (Ringkasan Investigasi)

Setelah proses investigasi (EDA), berikut adalah 4 temuan utama yang akan memandu langkah kita selanjutnya:

  1. MASALAH KUALITAS DATA: Temuan terbesar adalah adanya nilai 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.
  2. DATA TARGET SEIMBANG: Berita bagus! Dataset kita cukup seimbang (55% vs 45%). Ini berarti metrik Akurasi bisa kita jadikan salah satu patokan evaluasi utama, selain Precision, Recall, dan F1-Score.
  3. FITUR KATEGORIKAL: Ada 5 kolom kategorikal (Sex, ChestPainType, RestingECG, ExerciseAngina, ST_Slope) yang harus diubah menjadi format angka (proses encoding) sebelum bisa dibaca oleh model machine learning.
  4. PROFIL AWAL: Kita mulai mendapat gambaran awal bahwa pasien dengan HeartDisease=1 (Sakit) seringkali memiliki ChestPainType = ASY (Tanpa Gejala) dan ST_Slope = Flat (Datar). Ini adalah petunjuk awal yang sangat berharga.

5. Data Preprocessing

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) 

5.1 Train-Test Split (Langkah Paling Awal!)

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
cat(paste("Jumlah Data Tes  :", nrow(test_data), "baris\n"))
## 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.

5.2 Penanganan Missing Values dan Data Kategorik

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:

  1. Langkah 1 (Target): Mengubah HeartDisease (yang angkanya 0 dan 1) menjadi factor. Ini wajib agar R tahu kita sedang melakukan klasifikasi.
  2. Langkah 2 (Missing Values): Mengubah nilai 0 anomali di RestingBP dan Cholesterol menjadi NA (Not Available).
  3. Langkah 3 (Imputasi): Mengisi NA tersebut dengan nilai median (nilai tengah) dari data latih. Kita pakai median karena lebih kebal outlier dibanding mean (rata-rata).
  4. Langkah 4 (Encoding): Mengubah semua fitur kategorikal (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.

  1. prep(): “Mempelajari” parameter dari data latih (misalnya, mencari tahu berapa nilai median Cholesterol di data latih).
  2. 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!

  • Tidak ada lagi NA atau nilai 0 yang aneh.
  • Semua kolom sudah dalam format numerik.
  • Kita punya train_data_clean dan test_data_clean yang siap dipakai untuk modeling.

5.3 Standardisasi (Apakah Diperlukan?)

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.

  • Model seperti Regresi Logistik atau SVM wajib distandardisasi.
  • Model Pohon (RF, GBM, XGBoost) tidak perlu distandardisasi.

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.

6. Ensemble Modelling

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).

Apa itu Cross-Validation?

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.

6.1 Random Forest (RF)

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]

1. Spesifikasi Model & Parameter

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

2. Membangun Workflow

Workflow adalah “paket” yang menggabungkan Resep Preprocessing (dari Bagian 5) dengan Spesifikasi Model (dari atas).

# 2. Membuat workflow
# Ini menggabungkan resep (cara bersih-bersih) dan spec (cara modeling)
rf_workflow <- 
  workflow() %>% 
  add_recipe(heart_recipe) %>% # Menggunakan resep yang SAMA dari Bagian 5
  add_model(rf_spec)

3. Proses Pelatihan (Tuning)

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).

# 3. Menjalankan Tuning
# Ini mungkin butuh beberapa saat
cat("Memulai tuning Random Forest...\n")
## 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)

6.2 Extreme Gradient Boosting (XGBoost)

Selanjutnya adalah “sang juara” kompetisi, XGBoost. Model ini berbasis boosting (belajar dari kesalahan secara berurutan).

1. Spesifikasi Model & Parameter

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

2. Membangun Workflow

Kita gunakan workflow yang sama, hanya mengganti modelnya.

# 2. Membuat workflow
# Resepnya tetap sama, modelnya kita ganti
xgb_workflow <- 
  workflow() %>% 
  add_recipe(heart_recipe) %>% 
  add_model(xgb_spec)

3. Proses Pelatihan (Tuning)

Kita jalankan proses yang sama: 10-fold CV, 10 kombinasi acak.

# 3. Menjalankan Tuning
cat("Memulai tuning XGBoost...\n")
## 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)

6.3 LightGBM (LGBM)

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.

1. Spesifikasi Model & Parameter

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

2. Membangun Workflow

Langkah yang sama, ganti modelnya.

# 2. Membuat workflow
lgbm_workflow <- 
  workflow() %>% 
  add_recipe(heart_recipe) %>% 
  add_model(lgbm_spec)

3. Proses Pelatihan (Tuning)

Kita jalankan proses tuning terakhir.

# 3. Menjalankan Tuning
cat("Memulai tuning LightGBM...\n")
## 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.

7. Evaluasi Model

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).

7.1 Perbandingan Hasil “Latihan” (Cross-Validation)

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)

Visualisasi Performa CV

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

Perbandingan Skor ROC-AUC (10-Fold CV) Antar Model

Interpretasi (dari plot di atas):

  • (Di sini Anda akan menginterpretasi plot yang dihasilkan).
  • Contoh: “Dari plot di atas, kita bisa lihat bahwa XGBoost dan LightGBM memiliki rata-rata (garis tengah kotak) yang lebih tinggi daripada Random Forest. Kotak XGBoost juga terlihat ‘pendek’ dan stabil, menunjukkan performa yang konsisten selama 10 kali latihan.”

7.2 Momen Kebenaran: Ujian Akhir di Data Tes

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
xgb_test_results <- run_final_test(xgb_workflow, xgb_tune_results, data_split, "XGBoost")
## Menjalankan Ujian Akhir untuk: XGBoost
lgbm_test_results <- run_final_test(lgbm_workflow, lgbm_tune_results, data_split, "LightGBM")
## Menjalankan Ujian Akhir untuk: LightGBM
cat("Semua Ujian Akhir Selesai!\n")
## Semua Ujian Akhir Selesai!

7.3 Hasil Ujian: Tabel Perbandingan Metrik

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"
  )
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):

  • Dari tabel ini, kita bisa melihat dengan jelas siapa pemenangnya. Kita bandingkan nilai accuracy (seberapa banyak tebakan benar) dan roc_auc (kemampuan membedakan 0 dan 1).
  • (Contoh): “Terlihat bahwa XGBoost mendapatkan skor roc_auc tertinggi (misal, 0.9250) dan accuracy tertinggi (misal, 0.8870) di data tes. Ini mengungguli Random Forest dan LightGBM…”

7.4 Analisis Kesalahan: Confusion Matrix

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

Confusion Matrix untuk Model Pemenang (XGBoost) di Data Tes

Interpretasi (Plot Confusion Matrix):

  • (Anda akan menginterpretasi plot yang dihasilkan. Angka bisa bervariasi)
  • True Positive (TP) - Truth=1, Prediction=1: (Misal: 115) - Model dengan BENAR memprediksi 115 pasien sakit. (Ini bagus!)
  • True Negative (TN) - Truth=0, Prediction=0: (Misal: 90) - Model dengan BENAR memprediksi 90 pasien sehat. (Ini bagus!)
  • False Positive (FP) - Truth=0, Prediction=1: (Misal: 10) - Model SALAH memprediksi 10 pasien sakit (padahal aslinya sehat). Ini adalah kesalahan “alarm palsu”.
  • False Negative (FN) - 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.

7.5 Kesimpulan Evaluasi: Siapa Pemenangnya? 🥇

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).

8. Visualisasi Hasil

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.

8.1 Perbandingan ROC Curve (Siapa yang Paling Jago Membedakan?)

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.

  • Apa itu ROC Curve? Ini adalah plot yang menunjukkan seberapa jago sebuah model membedakan antara kelas “0” (Sehat) dan “1” (Sakit).
  • Cara Membacanya: Model yang “sempurna” akan memiliki kurva yang langsung “menempel” ke pojok kiri atas.
  • Tujuan Kita: Kita ingin melihat model mana yang kurvanya paling dekat ke pojok kiri atas.
# 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

Perbandingan ROC Curve Ketiga Model di Data Tes

Interpretasi (Plot di atas):

  • (Anda akan menginterpretasi plot yang dihasilkan).
  • Contoh: “Dari plot ‘foto finish’ ini, kita bisa lihat dengan jelas bahwa kurva XGBoost (biru) dan LightGBM (hijau) hampir berimpitan dan keduanya jauh lebih”nempel” ke pojok kiri atas dibandingkan kurva Random Forest (merah).”
  • “Ini mengkonfirmasi temuan kita di Bagian 7: model boosting (XGB & LGBM) jauh lebih superior dalam membedakan pasien sakit dan sehat di dataset ini.”

8.2 Feature Importance (Fitur Apa yang Paling Penting?)

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 x
20 Fitur Terpenting - Model Pemenang (XGBoost)

20 Fitur Terpenting - Model Pemenang (XGBoost)

Interpretasi (Plot di atas):

  • (Anda akan menginterpretasi plot yang dihasilkan. Hasil bisa bervariasi).
  • Contoh: “Hasilnya sangat menarik! Ternyata, fitur yang dianggap paling penting oleh model adalah ST_Slope_Flat.”
  • “Ini berarti, apakah kemiringan ST pasien ‘Datar’ atau tidak, adalah prediktor nomor satu penyakit jantung (menurut model kita). Ini bahkan lebih penting daripada Age (Umur) atau Cholesterol.”
  • “Kita juga bisa lihat bahwa fitur-fitur yang sudah di-encoding (seperti ChestPainType_ASY atau ‘Tanpa Gejala’) dan Sex_M (Pria) juga masuk 5 besar.”
  • “Fitur Oldpeak dan MaxHR (Detak Jantung Maks) juga sangat berpengaruh.”
  • Kesimpulan Insight: Model kita tidak hanya menebak. Ia telah belajar bahwa kombinasi dari ST_Slope, ChestPainType, Oldpeak, dan Sex adalah kunci utama untuk memprediksi risiko penyakit jantung di dataset ini.

9. Kesimpulan

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:

9.1 Model Ensemble Terbaik 🥇

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:

  • Skor roc_auc (Test): [misal: 0.925] — Ini adalah skor tertinggi, menunjukkan kemampuan terbaik dalam membedakan pasien “Sakit” dan “Sehat”.
  • Skor 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.

9.2 Kenapa Model Ini Menang?

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.

9.3 Variabel Paling Berpengaruh 💡

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:

  1. ST_Slope_Flat: Apakah segmen ST pasien ‘Datar’ saat tes olahraga.
  2. ChestPainType_ASY: Apakah pasien ‘Tanpa Gejala’ nyeri dada.
  3. Oldpeak: Nilai depresi ST saat olahraga.
  4. Sex_M: Apakah pasien berjenis kelamin ‘Pria’.
  5. 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.

9.4 Keterbatasan dan Saran Perbaikan 📈

Tidak ada analisis yang sempurna. Laporan ini memiliki beberapa keterbatasan yang bisa menjadi dasar untuk perbaikan di masa depan:

  1. Hyperparameter Tuning: Kita hanya mencoba 10 kombinasi acak (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()).
  2. Ukuran Data: Dataset kita (918 baris) tergolong kecil. Model yang dilatih pada data yang lebih besar dan lebih beragam (lebih banyak pasien wanita, lebih banyak etnis) akan jauh lebih dapat diandalkan di dunia nyata.
  3. Metodologi Lain: Kita hanya membandingkan 3 model. Untuk perbaikan, kita bisa mencoba:
    • Stacking: Sebuah teknik ensemble canggih di mana kita “menumpuk” ketiga model kita, dan menggunakan satu model lagi (misal, Regresi Logistik) untuk “mempelajari” cara terbaik menggabungkan prediksi mereka.
    • Neural Networks: Mencoba model deep learning untuk melihat apakah neural network sederhana bisa mengalahkan ensemble pohon.

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.