Machine Learning Model: Predicting Heart Disease Risk Using Clinical Variables
1 Introduction
Prediksi penyakit jantung memberikan wawasan penting mengenai hubungan antara faktor risiko dan kesehatan jantung. Pada kasus ini, akan dilakukan evaluasi bagaimana setiap karakterisik saling berinteraksi untuk menentukan tingkat risiko individu dalam mengembangkan masalah kardiovaskular yang dapat menyebabkan gagal jantung atau stroke. Dengan prediksi ini, dapat diciptakan strategi pencegahan kepada pasein penyakit jantung.
Pada artikel ini akan dibangun model prediktif yang dapat memprediksi
pasien dengan kriteria apa saja apakah pasien telah didiagnosa dengan
penyakit jantung atau tidak menggunakan data heart_disease.
Algoritma yang akan digunakan adalah Naive Bayes, Decision Tree,
dan Random Forest.
Data Dictionary
library(readxl)
dictionary_data <- read_excel("data_dictionary.xlsx")
dictionary_data#> # A tibble: 14 × 3
#> Variable Description Type
#> <chr> <chr> <chr>
#> 1 Age The age of the patient Nume…
#> 2 Sex The gender of the patient Cate…
#> 3 Chest Pain Type The type of chest pain experienced by the pati… Cate…
#> 4 BP The blood pressure level of the patient Nume…
#> 5 Cholesterol The cholesterol level of the patient Nume…
#> 6 FBS over 120 The fasting blood sugar test results over 120 … Nume…
#> 7 EKG Results The electrocardiagram results of the patient Cate…
#> 8 Max HR The maximum heart rate levels achieved during … Nume…
#> 9 Exercise Angina The angina experienced during exercise testing Cate…
#> 10 ST depression The ST depression on an Electrocardiogram Nume…
#> 11 Slope of ST The slope of ST segment electrocardiogram read… Cate…
#> 12 Number of Vessels Fluro The amount vessels seen in Fluoroscopy images Nume…
#> 13 Thallium The Thallium Stress test findings Cate…
#> 14 Heart Disease Whether or not the patient has been diagnosed … Cate…
2 Data Preparation
2.1 Importing Libraries
Langkah pertama adalah memanggil library yang akan
digunakan dalam pemodelan.
library(dplyr)
library(gtools)
library(ggplot2)
library(class)
library(tidyr)
library(tidyverse)
library(plotly)
library(tidymodels)
library(gridExtra)
library(ggstatsplot)
library(grid)
library(tidymodels)
library(caret)
library(GGally)2.2 Importing Datasets
Selanjutnya, dilakukan import data Heart Disease dalam
bentuk CSV yang disimpan dalam data frame yang disimpan
dengan nama heart_disease. Data yang diimport merupakan
data train dimana dari data tersebut akan digunakan untuk
training model.
heart_disease <- read.csv("heart_disease_prediction.csv", sep = ',', header = TRUE)
str(heart_disease)#> 'data.frame': 270 obs. of 15 variables:
#> $ index : int 0 1 2 3 4 5 6 7 8 9 ...
#> $ Age : int 70 67 57 64 74 65 56 59 60 63 ...
#> $ Sex : int 1 0 1 1 0 1 1 1 1 0 ...
#> $ Chest.pain.type : int 4 3 2 4 2 4 3 4 4 4 ...
#> $ BP : int 130 115 124 128 120 120 130 110 140 150 ...
#> $ Cholesterol : int 322 564 261 263 269 177 256 239 293 407 ...
#> $ FBS.over.120 : int 0 0 0 0 0 0 1 0 0 0 ...
#> $ EKG.results : int 2 2 0 0 2 0 2 2 2 2 ...
#> $ Max.HR : int 109 160 141 105 121 140 142 142 170 154 ...
#> $ Exercise.angina : int 0 0 0 1 1 0 1 1 0 0 ...
#> $ ST.depression : num 2.4 1.6 0.3 0.2 0.2 0.4 0.6 1.2 1.2 4 ...
#> $ Slope.of.ST : int 2 2 1 2 1 1 2 2 2 2 ...
#> $ Number.of.vessels.fluro: int 3 0 0 1 1 0 1 1 2 3 ...
#> $ Thallium : int 3 7 7 7 3 7 6 7 7 7 ...
#> $ Heart.Disease : chr "Presence" "Absence" "Presence" "Absence" ...
Data di atas memiliki 270 observasi dan 15 variables. Variabel
index merupakan variabel dengan unique identifier untuk
setiap pasien. Target variabel dalam case ini adalah
Heart.Disease. Dari seluruh variabel pada data, terdapat
beberapa variabel yang memiliki tipe data tidak sesuai. Oleh karena itu
perlu dilakukan penyesuaian tipe data.
2.3 Data Types
Variabel yang memiliki tipe data tidak sesuai akan mempengaruhi hasil pemodelan bahkan tidak bisa diproses dalam pemodelan.
heart_disease$Sex <- as.factor(heart_disease$Sex)
heart_disease$Chest.pain.type <- as.factor(heart_disease$Chest.pain.type)
heart_disease$EKG.results <- as.factor(heart_disease$EKG.results)
heart_disease$Exercise.angina <- as.factor(heart_disease$Exercise.angina)
heart_disease$Thallium <- as.factor(heart_disease$Thallium)
heart_disease$Heart.Disease <- as.factor(heart_disease$Heart.Disease)
heart_disease <- heart_disease %>% mutate(across(Heart.Disease, as_factor))
glimpse(heart_disease)#> Rows: 270
#> Columns: 15
#> $ index <int> 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, …
#> $ Age <int> 70, 67, 57, 64, 74, 65, 56, 59, 60, 63, 59, 53…
#> $ Sex <fct> 1, 0, 1, 1, 0, 1, 1, 1, 1, 0, 1, 1, 1, 1, 0, 0…
#> $ Chest.pain.type <fct> 4, 3, 2, 4, 2, 4, 3, 4, 4, 4, 4, 4, 3, 1, 4, 4…
#> $ BP <int> 130, 115, 124, 128, 120, 120, 130, 110, 140, 1…
#> $ Cholesterol <int> 322, 564, 261, 263, 269, 177, 256, 239, 293, 4…
#> $ FBS.over.120 <int> 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0…
#> $ EKG.results <fct> 2, 2, 0, 0, 2, 0, 2, 2, 2, 2, 0, 2, 2, 0, 2, 0…
#> $ Max.HR <int> 109, 160, 141, 105, 121, 140, 142, 142, 170, 1…
#> $ Exercise.angina <fct> 0, 0, 0, 1, 1, 0, 1, 1, 0, 0, 0, 1, 0, 0, 0, 0…
#> $ ST.depression <dbl> 2.4, 1.6, 0.3, 0.2, 0.2, 0.4, 0.6, 1.2, 1.2, 4…
#> $ Slope.of.ST <int> 2, 2, 1, 2, 1, 1, 2, 2, 2, 2, 2, 1, 1, 2, 1, 2…
#> $ Number.of.vessels.fluro <int> 3, 0, 0, 1, 1, 0, 1, 1, 2, 3, 0, 0, 0, 2, 1, 0…
#> $ Thallium <fct> 3, 7, 7, 7, 3, 7, 6, 7, 7, 7, 7, 7, 3, 3, 3, 3…
#> $ Heart.Disease <fct> Presence, Absence, Presence, Absence, Absence,…
3 Exploratory Data Analysis
Sebelum melakukan pemodelan, kita harus memastikan bahwa data yang digunakan adalah data yang bersih dan useful.
3.1 Duplicates
Langkah pertama dilakukan pengecekan data duplikat.
dup <- sum(duplicated(heart_disease))
dup#> [1] 0
Berdasarkan hasil pengecekan, data heart_disease tidak
memiliki data duplikat.
3.2 Missing Values
Selanjutnya yaitu melakukan pengecekan missing values. Missing values akan menyebabkan data tidak valid saat dilakukan pemodelan. Terdapat beberapa uji yang tidak resisten terhadap missing values.
mv <- colSums(is.na(x=heart_disease))
mv#> index Age Sex
#> 0 0 0
#> Chest.pain.type BP Cholesterol
#> 0 0 0
#> FBS.over.120 EKG.results Max.HR
#> 0 0 0
#> Exercise.angina ST.depression Slope.of.ST
#> 0 0 0
#> Number.of.vessels.fluro Thallium Heart.Disease
#> 0 0 0
Berdasarkab hasil pengecekan missing values, data
heart_disease tidak memiliki missing values untuk seluruh
variabel, sehingga tidak perlu dilakukan penanganan missing values.
3.3 Proporsi Data
Sebelum dilakukan pemodelan, perlu dilihat proporsi dari target
variabel yang dimiliki yaitu pada variabel
Heart.Disease.
prop.table(table(heart_disease$Heart.Disease))#>
#> Absence Presence
#> 0.5555556 0.4444444
Jika dilihat dari proporsi kedua kelas, proporsi kedua kelas dianggap seimbang.
3.4 Korelasi antar Variabel
Korelasi digunakan untuk melihat hubungan antar variabel. Korelasi ini dapat digunakan sebagai filter awal dalam memilih variabel independen (X). Namun, korelasi memiliki beberapa kekurangan. Korelasi hanya dapat melihat seberapa besar kekuatan hubungan kedua variabel, namun tidak dapat melihat peran yang mempengaruhi dan dipengaruhi.
GGally::ggcorr(heart_disease[,-12], hjust = 1, layout.exp = 2, label = T, label_size = 2.9)
Korelasi menunjukkan bahwa mayoritas, antar variabel memiliki korelasi
yang rendah (<0,5). Mayoritas antar variabel memiliki korelasi yang
positif. Dalam hal ini ketika satu variabel meningkat maka variabel
lainnya juga akan mengalami peningkatan.
3.5 Splitting Train-Test
Selanjutnya dilakukan split data train dan test data dengan tujuan pada data train akan digunakan sebagai modeling, dan pada data test akan digukanan sebagai pengujian model.
set.seed(240899)
intrain <- sample(nrow(heart_disease), nrow(heart_disease)*0.7)
hd_train <- heart_disease[intrain,]
hd_test <- heart_disease[-intrain,]
heart_disease$Heart.Disease %>% levels()#> [1] "Absence" "Presence"
4 Modelling
Setelah dilakukan preprocessing data, selanjutnya dilakukan pemodelan.
4.1 Naive Bayes
💡 Beberapa karakteristik Naive Bayes
Mengasumsikan bahwa semua fitur dalam dataset memiliki tingkat kepentingan yang sama dan saling independen. Hal ini memungkinkan Naive Bayes untuk melakukan perhitungan lebih cepat (algoritma ini cukup sederhana).
Rentan terhadap bias akibat kelangkaan data. Dalam beberapa kasus, data mungkin memiliki distribusi di mana pengamatan yang langka menyebabkan probabilitas mendekati 0 atau 1, yang mengintroduksi bias berat ke dalam model yang dapat menyebabkan kinerja buruk pada data yang belum pernah dilihat.
Lebih cocok untuk data dengan prediktor kategorikal. Hal ini karena Naive Bayes sensitif terhadap kelangkaan data. Sementara itu, variabel kontinu mungkin mengandung pengamatan yang sangat langka atau bahkan hanya satu pengamatan untuk nilai tertentu.
Menerapkan estimator Laplace/perataan untuk masalah kelangkaan data. Estimator Laplace mengusulkan penambahan angka kecil (biasanya 1) ke setiap hitungan dalam tabel frekuensi. Ini memastikan bahwa setiap kombinasi kelas-fitur memiliki probabilitas non-nol untuk terjadi.
Berdasarkan karakteristik tersebut, data heart_disease
cocok untuk Naive Bayes. Dari deskripsi data, seluruh prediktor tidak
memiliki korelasi tinggi satu sama lain.
Model Building and Fitting
# Model Building
library(e1071)
naive<- naiveBayes(hd_train[-15], hd_train$Heart.Disease, laplace = 1)# Model Fitting
# Class Prediction
naive_pred <- predict(naive, hd_test, type = "class")
# Probability
naive_prob <- predict(naive, hd_test, type = "raw")
# Result
naive_tab <- table(naive_pred,hd_test$Heart.Disease)
naive_cm <- caret::confusionMatrix(naive_tab)
naive_cm#> Confusion Matrix and Statistics
#>
#>
#> naive_pred Absence Presence
#> Absence 35 10
#> Presence 4 32
#>
#> Accuracy : 0.8272
#> 95% CI : (0.727, 0.9022)
#> No Information Rate : 0.5185
#> P-Value [Acc > NIR] : 0.00000000649
#>
#> Kappa : 0.6557
#>
#> Mcnemar's Test P-Value : 0.1814
#>
#> Sensitivity : 0.8974
#> Specificity : 0.7619
#> Pos Pred Value : 0.7778
#> Neg Pred Value : 0.8889
#> Prevalence : 0.4815
#> Detection Rate : 0.4321
#> Detection Prevalence : 0.5556
#> Balanced Accuracy : 0.8297
#>
#> 'Positive' Class : Absence
#>
4.2 Decision Tree
Model Decision Tree (tree-based models) adalah model yang sangat kuat, sangat serbaguna, dan kemungkinan merupakan pilihan paling populer untuk kasus pembelajaran mesin (machine learning). Model Decision tree memiliki manfaat utama yaitu keterpahaman (interpretability) dan algoritma yang akan membuat sekumpulan aturan yang divisualisasikan dalam diagram yang menyerupai pohon.
Model Building and Fitting
# Model Building
library(rpart)
library(rattle)
library(rpart.plot)
dtree <- rpart(formula = Heart.Disease ~., data = hd_train, method = "class" )
fancyRpartPlot(dtree, sub = NULL)
Decision tree terdiri dari beberapa bagian. Kotak pertama di bagian atas
plot adalah root node (simpul akar). Akar ini akan membelah dan membuat
cabang-cabang berdasarkan aturan tertentu. Setiap cabang berakhir dengan
sebuah simpul. Beberapa simpul membelah lagi menjadi simpul-simpul lain
dan disebut simpul internal (internal node). Simpul yang tidak membelah
lagi atau yang muncul di akhir pohon disebut simpul terminal (terminal
node) atau simpul daun (leaf node), seperti daun pada pohon.
💡 Setiap simpul menunjukkan:
Kelas yang diprediksi (Excellent/Poor-Normal).
Probabilitas kelas Excellent/Poor-Normal.
Persentase pengamatan dalam simpul tersebut.
Simpul akar dan simpul internal juga menunjukkan aturan (variabel dengan ambang/batas nilai) yang akan membagi setiap pengamatan.
💡 Bagaimana pohon menghasilkan simpul?
Pohon akan memilih untuk membelah data dengan cara sehingga simpul-simpul yang dihasilkan akan mengandung titik data dengan kelas yang serupa sebanyak mungkin (homogen). Salah satu ukuran homogeneity/kemurnian dalam grup adalah entropi atau ukuran ketidakteraturan. Entropi yang mendekati 0 berarti sebagian besar pengamatan termasuk dalam kelas yang sama (homogen). Entropi yang mendekati 1 menunjukkan kebalikannya (heterogen).
Pohon keputusan dibangun dengan cara top-down. Simpul akar akan dipilih dari sebuah variabel dan aturan kondisional yang memberikan entropi tertinggi. Akar akan dibagi (split) menjadi simpul-simpul dengan setiap bagian memiliki entropi yang berbeda. Untuk referensi lebih lanjut tentang perhitungan entropi, Anda dapat membaca artikel ini. Perbedaan entropi sebelum dan setelah pemisahan disebut sebagai information gain. Pohon akan lebih memilih untuk melakukan pemisahan menggunakan variabel dan aturan yang menghasilkan information gain yang lebih tinggi (dari entropi yang tinggi menjadi entropi yang lebih rendah).
💡 Karakteristik Decision Tree
Berkinerja baik pada variabel numerik dan kategorikal.
Semua prediktor diasumsikan berinteraksi.
Cukup robust terhadap masalah multikolinearitas. Sebuah pohon keputusan akan memilih variabel yang memiliki information gain tertinggi dalam satu pemisahan, sementara metode seperti regresi logistik akan menggunakan keduanya.
Robust dan tidak sensitif terhadap outlier. Pemisahan akan terjadi pada kondisi yang memaksimalkan homogenitas dalam grup yang dihasilkan. Outlier akan memiliki pengaruh yang sedikit terhadap proses pemisahan.
# Model Fitting
# Class Prediction
dtree_pred <- predict(dtree, hd_test, type = "class")
# Probability
dtree_prob <- predict(dtree, hd_test, type = "prob")
# Result
dtree_tab <- table(dtree_pred,hd_test$Heart.Disease)
dtree_cm <- caret::confusionMatrix(dtree_tab)
dtree_cm#> Confusion Matrix and Statistics
#>
#>
#> dtree_pred Absence Presence
#> Absence 34 12
#> Presence 5 30
#>
#> Accuracy : 0.7901
#> 95% CI : (0.6854, 0.8727)
#> No Information Rate : 0.5185
#> P-Value [Acc > NIR] : 0.0000003951
#>
#> Kappa : 0.5823
#>
#> Mcnemar's Test P-Value : 0.1456
#>
#> Sensitivity : 0.8718
#> Specificity : 0.7143
#> Pos Pred Value : 0.7391
#> Neg Pred Value : 0.8571
#> Prevalence : 0.4815
#> Detection Rate : 0.4198
#> Detection Prevalence : 0.5679
#> Balanced Accuracy : 0.7930
#>
#> 'Positive' Class : Absence
#>
4.3 Random Forest
Random Forest adalah salah satu contoh algoritma berbasis ensemble yang dibangun berdasarkan metode pohon keputusan dan dikenal karena keserbagunaannya serta kinerjanya. Algoritma berbasis ensemble sendiri sebenarnya adalah gabungan dari beberapa teknik pembelajaran mesin yang digabungkan menjadi satu model prediktif, yang dibangun untuk mengurangi kesalahan, bias, dan meningkatkan prediksi.
💡 Langkah Pembangunan Random Forest
Melakukan bagging (bootstrap aggregation), yaitu membuat subset data pelatihan melalui sampling acak dengan penggantian untuk melatih beberapa model prediktif (dalam hal ini banyak pohon keputusan).
Melakukan boosting, yaitu melatih beberapa model prediktif untuk menghasilkan model yang lebih baik. Hal ini dapat mengatasi masalah overfitting pada pohon keputusan karena kita akan mempertimbangkan lebih dari 1 pohon keputusan yang sebelumnya dilatih menggunakan variabel dan pengamatan acak.
Mengklasifikasikan kelas berdasarkan mekanisme voting setelah banyak pohon keputusan dilatih dan setiap pohon memberikan prediksi (nilai yang dipasang). Prediksi akhir dihasilkan melalui voting mayoritas (untuk klasifikasi) atau rata-rata output (jika regresi).
💡 K-fold Cross-Validation
Teknik ini melakukan cross-validation dengan membagi data kita
menjadi k grup sampel yang memiliki ukuran yang sama (bin)
dan menggunakan salah satu bin sebagai data uji, sementara sisanya
menjadi data latih. Proses ini diulang sebanyak k kali
(fold). Hal ini membuat setiap pengamatan memiliki kesempatan untuk
digunakan sebagai data latih dan uji, dan dengan demikian juga dapat
mengatasi masalah overfitting dari pohon keputusan.
Model Building and Fitting
library(randomForest)
# Model Building
set.seed(240899)
ctrl <- trainControl(method="repeatedcv", number=4, repeats=4) # k-fold cross validation
forest <- train(Heart.Disease ~ ., data=hd_train, method="rf", trControl = ctrl)
forest#> Random Forest
#>
#> 189 samples
#> 14 predictor
#> 2 classes: 'Absence', 'Presence'
#>
#> No pre-processing
#> Resampling: Cross-Validated (4 fold, repeated 4 times)
#> Summary of sample sizes: 142, 142, 141, 142, 142, 142, ...
#> Resampling results across tuning parameters:
#>
#> mtry Accuracy Kappa
#> 2 0.8389789 0.6599735
#> 10 0.8163388 0.6154517
#> 18 0.7977760 0.5777074
#>
#> Accuracy was used to select the optimal model using the largest value.
#> The final value used for the model was mtry = 2.
Dari model yang telah dibangun, dapat diketahui bahwa jumlah variabel optimum yang dapat dipertimbangkan untuk pemisahan disetiap sampul pohon adalah 2. Selanjutnya akan dilakukan pengecekan pengaruh dari setiap variabel yang digunakan dalam model.
# Importance Features
varImp(forest)#> rf variable importance
#>
#> Overall
#> Max.HR 100.00
#> ST.depression 97.52
#> Number.of.vessels.fluro 97.02
#> Thallium7 90.12
#> Chest.pain.type4 78.60
#> Cholesterol 68.43
#> BP 64.49
#> Age 62.91
#> index 61.89
#> Exercise.angina1 48.63
#> Slope.of.ST 44.44
#> Sex1 31.38
#> Chest.pain.type3 22.48
#> EKG.results2 19.82
#> Chest.pain.type2 13.27
#> FBS.over.120 10.44
#> Thallium6 5.60
#> EKG.results1 0.00
# Out-of-bag Estimates
plot(forest$finalModel)
legend("topright", colnames(forest$finalModel$err.rate), col = 1:6, cex = 0.8, fill = 1:6)# Model Fitting
# Class Prediction
forest_pred <- predict(forest, hd_test, type = "raw")
# Probability
forest_prob <- predict(forest, hd_test, type = "prob")
# Result
forest_tab <- table(forest_pred,hd_test$Heart.Disease)
forest_cm <- caret::confusionMatrix(forest_tab)
forest_cm#> Confusion Matrix and Statistics
#>
#>
#> forest_pred Absence Presence
#> Absence 37 12
#> Presence 2 30
#>
#> Accuracy : 0.8272
#> 95% CI : (0.727, 0.9022)
#> No Information Rate : 0.5185
#> P-Value [Acc > NIR] : 0.00000000649
#>
#> Kappa : 0.657
#>
#> Mcnemar's Test P-Value : 0.01616
#>
#> Sensitivity : 0.9487
#> Specificity : 0.7143
#> Pos Pred Value : 0.7551
#> Neg Pred Value : 0.9375
#> Prevalence : 0.4815
#> Detection Rate : 0.4568
#> Detection Prevalence : 0.6049
#> Balanced Accuracy : 0.8315
#>
#> 'Positive' Class : Absence
#>
5 Model Evaluation
Langkah terakhir yaitu membandingkan efektifitas model menggunakan , , , dan untuk mendapatkan model terbaik.
📌 Keterangan
Re-call/Sensitivity = dari semua data aktual yang positif, seberapa mampu proporsi model saya menebak benar.
Specificity = dari semua data aktual yang negatif, seberapa mampu proporsi model saya menebak yang benar.
Accuracy = seberapa mampu model saya menebak dengan benar target Y.
Precision = dari semua hasil prediksi, seberapa mampu model saya dapat menebak benar kelas positif.
#*Model Evaluation Naive Bayes*
eval_naive <- data_frame(Accuracy = naive_cm$overall[1],
Recall = naive_cm$byClass[1],
Specificity = naive_cm$byClass[2],
Precision = naive_cm$byClass[3])
#*Model Evaluation Decision Tree*
eval_dtree <- data_frame(Accuracy = dtree_cm$overall[1],
Recall = dtree_cm$byClass[1],
Specificity = dtree_cm$byClass[2],
Precision = dtree_cm$byClass[3])
#*Model Evaluation Random Forest*
eval_forest <- data_frame(Accuracy = forest_cm$overall[1],
Recall = forest_cm$byClass[1],
Specificity = forest_cm$byClass[2],
Precision = forest_cm$byClass[3])
rbind("1 Naive Bayes" = eval_naive, "2 Decision Tree" = eval_dtree, "3 Random Forest" = eval_forest)#> # A tibble: 3 × 4
#> Accuracy Recall Specificity Precision
#> * <dbl> <dbl> <dbl> <dbl>
#> 1 0.827 0.897 0.762 0.778
#> 2 0.790 0.872 0.714 0.739
#> 3 0.827 0.949 0.714 0.755
6 Conclusion
Berdasarkan tabel matriks evaluasi di atas, model prediktif yang dibangun menggunakan algoritma Random Forest memberikan hasil terbaik. Model ini memberikan akurasi yang tinggi sama dengan model Naive Bayes yaitu 83%, namun Random Forest lebih unggul di spesifikasi dan presisi di atas. Oleh karena itu, model terbaik untuk memprediksi apakah pasien didiagnosa dengan penyakit jantung atau tidak adalah model Random Forest.