Kesehatan ibu hamil merupakan salah satu isu penting dalam pembangunan kesehatan karena komplikasi selama kehamilan dan persalinan dapat meningkatkan risiko kematian ibu maupun bayi. Oleh karena itu, identifikasi risiko kesehatan ibu hamil perlu dilakukan secara cepat, objektif, dan konsisten. Pendekatan data mining dapat digunakan untuk membantu mengenali pola risiko berdasarkan indikator fisiologis seperti usia, tekanan darah, kadar gula darah, suhu tubuh, dan denyut jantung.
Penelitian ini membandingkan dua algoritma klasifikasi, yaitu Decision Tree dan Naïve Bayes, dalam mendeteksi tingkat risiko kesehatan ibu hamil. Evaluasi dilakukan menggunakan metrik accuracy, precision, recall, dan F1-score. Selain itu, dilakukan optimasi hyperparameter dengan pendekatan Random Search untuk melihat apakah proses tuning mampu meningkatkan performa model.
library(readxl)
library(dplyr)
library(tidyr)
library(ggplot2)
library(caret)
library(rpart)
library(rpart.plot)
library(e1071)
library(klaR)
library(pROC)
library(knitr)
# Letakkan file data Excel di folder yang sama dengan file R Markdown ini.
# Jika nama file berbeda, ganti bagian data_path sesuai nama file Anda.
library(readxl)
data_path <- "data uas_maternal risk.xlsx"
Risk <- read_excel(data_path)
Risk$RiskLevel <- as.factor(Risk$RiskLevel)
head(Risk)
Interpretasi: Dataset yang digunakan adalah
Maternal Health Risk dengan variabel prediktor berupa indikator
fisiologis ibu hamil dan variabel target berupa RiskLevel.
Variabel target perlu diubah menjadi faktor karena analisis yang
dilakukan merupakan klasifikasi, bukan regresi.
info_tipe <- data.frame(
Variabel = names(Risk),
Jumlah_Observasi = sapply(Risk, function(x) sum(!is.na(x))),
Tipe_Data = sapply(Risk, class)
)
kable(info_tipe, caption = "Informasi Tipe Data")
| Variabel | Jumlah_Observasi | Tipe_Data | |
|---|---|---|---|
| Age | Age | 1014 | numeric |
| SystolicBP | SystolicBP | 1014 | numeric |
| DiastolicBP | DiastolicBP | 1014 | numeric |
| BS | BS | 1014 | numeric |
| BodyTemp | BodyTemp | 1014 | numeric |
| HeartRate | HeartRate | 1014 | numeric |
| RiskLevel | RiskLevel | 1014 | factor |
Interpretasi: Dataset terdiri atas enam variabel
prediktor, yaitu Age, SystolicBP,
DiastolicBP, BS, BodyTemp, dan
HeartRate, serta satu variabel target, yaitu
RiskLevel. Variabel prediktor berbentuk numerik, sedangkan
RiskLevel berbentuk kategorik. Struktur ini sudah sesuai
untuk proses klasifikasi menggunakan Decision Tree dan Naïve Bayes.
library(dplyr)
library(tidyr)
data_num <- Risk %>%
dplyr::select(Age, SystolicBP, DiastolicBP, BS, BodyTemp, HeartRate)
statdesk <- data_num %>%
summarise(across(
everything(),
list(
Min = min,
Max = max,
Mean = mean,
SD = sd
),
na.rm = TRUE
)) %>%
pivot_longer(
cols = everything(),
names_to = c("Variabel", ".value"),
names_sep = "_"
)
kable(statdesk, digits = 2, caption = "Statistik Deskriptif Variabel Numerik")
| Variabel | Min | Max | Mean | SD |
|---|---|---|---|---|
| Age | 10 | 70 | 29.87 | 13.47 |
| SystolicBP | 70 | 160 | 113.20 | 18.40 |
| DiastolicBP | 49 | 100 | 76.46 | 13.89 |
| BS | 6 | 19 | 8.73 | 3.29 |
| BodyTemp | 98 | 103 | 98.67 | 1.37 |
| HeartRate | 7 | 90 | 74.30 | 8.09 |
Interpretasi: Statistik deskriptif menunjukkan bahwa
data memiliki variasi karakteristik kesehatan ibu hamil. Rentang usia,
tekanan darah, kadar gula darah, suhu tubuh, dan denyut jantung
menunjukkan adanya perbedaan kondisi fisiologis antarobservasi. Namun,
nilai minimum pada HeartRate perlu diperhatikan karena
terdapat nilai yang sangat rendah dan berpotensi menjadi anomali
data.
library(dplyr)
dist_target <- Risk %>%
count(RiskLevel) %>%
mutate(Persentase = round(n / sum(n) * 100, 2))
kable(dist_target, caption = "Distribusi Variabel Target RiskLevel")
| RiskLevel | n | Persentase |
|---|---|---|
| High Risk | 272 | 26.82 |
| Low Risk | 406 | 40.04 |
| Mid Risk | 336 | 33.14 |
ggplot(dist_target, aes(x = RiskLevel, y = n, fill = RiskLevel)) +
geom_col() +
geom_text(aes(label = paste0(n, " (", Persentase, "%)")), vjust = -0.3) +
labs(
title = "Distribusi Data Variabel Target",
x = "RiskLevel",
y = "Jumlah Observasi"
) +
theme_minimal() +
theme(
plot.title = element_text(hjust = 0.5, face = "bold"),
legend.position = "none"
)
Interpretasi: Distribusi RiskLevel
digunakan untuk melihat apakah terdapat ketidakseimbangan kelas. Apabila
jumlah setiap kelas relatif mendekati, maka data tidak mengalami
class imbalance yang berat. Kondisi ini penting karena data
yang terlalu tidak seimbang dapat membuat model cenderung memprediksi
kelas mayoritas dan mengabaikan kelas minoritas.
missing_value <- data.frame(
Variabel = names(Risk),
Jumlah_Missing = colSums(is.na(Risk))
)
kable(missing_value, caption = "Pengecekan Missing Value")
| Variabel | Jumlah_Missing | |
|---|---|---|
| Age | Age | 0 |
| SystolicBP | SystolicBP | 0 |
| DiastolicBP | DiastolicBP | 0 |
| BS | BS | 0 |
| BodyTemp | BodyTemp | 0 |
| HeartRate | HeartRate | 0 |
| RiskLevel | RiskLevel | 0 |
Interpretasi: Hasil pengecekan menunjukkan apakah terdapat nilai kosong pada setiap variabel. Jika seluruh variabel memiliki jumlah missing value sebesar nol, maka data dapat langsung dilanjutkan ke tahap berikutnya tanpa imputasi. Jika terdapat nilai kosong, maka perlu dilakukan penanganan agar proses pemodelan tidak terganggu.
hitung_outlier <- function(x){
Q1 <- quantile(x, 0.25, na.rm = TRUE)
Q3 <- quantile(x, 0.75, na.rm = TRUE)
IQR_val <- IQR(x, na.rm = TRUE)
batas_bawah <- Q1 - 1.5 * IQR_val
batas_atas <- Q3 + 1.5 * IQR_val
sum(x < batas_bawah | x > batas_atas, na.rm = TRUE)
}
tabel_outlier <- data.frame(
Variabel = names(data_num),
Jumlah_Outlier = sapply(data_num, hitung_outlier)
)
kable(tabel_outlier, caption = "Jumlah Outlier Berdasarkan Metode IQR")
| Variabel | Jumlah_Outlier | |
|---|---|---|
| Age | Age | 1 |
| SystolicBP | SystolicBP | 10 |
| DiastolicBP | DiastolicBP | 0 |
| BS | BS | 210 |
| BodyTemp | BodyTemp | 210 |
| HeartRate | HeartRate | 2 |
Interpretasi: Deteksi outlier dilakukan untuk melihat apakah terdapat pengamatan yang berada jauh dari sebaran utama data. Tidak semua outlier harus dihapus karena dalam konteks kesehatan, nilai ekstrem dapat mencerminkan kondisi medis tertentu. Oleh karena itu, keputusan penghapusan perlu dilakukan secara hati-hati, terutama jika nilai tersebut masih masuk akal secara fisiologis.
data_long <- Risk %>%
dplyr::select(Age, SystolicBP, DiastolicBP, BS, BodyTemp, HeartRate) %>%
pivot_longer(
cols = everything(),
names_to = "Variabel",
values_to = "Nilai"
)
ggplot(data_long, aes(x = Variabel, y = Nilai, fill = Variabel)) +
geom_boxplot() +
labs(
title = "Boxplot Maternal Health Risk",
x = "Variabel",
y = "Nilai"
) +
theme_minimal() +
theme(
plot.title = element_text(hjust = 0.5, face = "bold"),
axis.text.x = element_text(angle = 45, hjust = 1),
legend.position = "none"
)
Interpretasi: Boxplot memperlihatkan pola sebaran
setiap variabel numerik. Beberapa variabel memiliki outlier, tetapi
sebagian besar masih dapat dianggap sebagai variasi kondisi kesehatan.
Perhatian utama diberikan pada HeartRate karena terdapat
nilai yang sangat jauh dari rentang denyut jantung normal, sehingga
perlu diperiksa lebih lanjut.
jumlah_hr_7 <- sum(Risk$HeartRate == 7)
jumlah_hr_7
## [1] 2
data_hr_7 <- Risk %>%
filter(HeartRate == 7)
kable(data_hr_7, caption = "Observasi dengan HeartRate sebesar 7")
| Age | SystolicBP | DiastolicBP | BS | BodyTemp | HeartRate | RiskLevel |
|---|---|---|---|---|---|---|
| 16 | 120 | 75 | 7.9 | 98 | 7 | Low Risk |
| 16 | 120 | 75 | 7.9 | 98 | 7 | Low Risk |
Interpretasi: Observasi dengan
HeartRate sebesar 7 perlu diperlakukan sebagai anomali
karena nilainya tidak realistis untuk denyut jantung manusia dalam
kondisi normal. Jika observasi tersebut juga memiliki karakteristik yang
sama, maka terdapat indikasi duplikasi sekaligus kesalahan input. Oleh
karena itu, penghapusan observasi ini dapat diterima untuk menjaga
kualitas data.
Risk <- Risk %>%
filter(HeartRate != 7)
jumlah_setelah_hapus <- sum(Risk$HeartRate == 7)
jumlah_data_bersih <- nrow(Risk)
jumlah_setelah_hapus
## [1] 0
jumlah_data_bersih
## [1] 1012
Interpretasi: Setelah observasi dengan
HeartRate sebesar 7 dihapus, dataset menjadi lebih bersih
dan lebih representatif untuk proses pemodelan. Penghapusan ini tidak
dilakukan terhadap seluruh outlier, melainkan hanya terhadap nilai yang
secara substansi tidak wajar.
set.seed(123)
index_train <- createDataPartition(
Risk$RiskLevel,
p = 0.70,
list = FALSE
)
train_data <- Risk[index_train, ]
test_data <- Risk[-index_train, ]
train_data$RiskLevel <- factor(train_data$RiskLevel)
test_data$RiskLevel <- factor(test_data$RiskLevel)
split_table <- data.frame(
Data = c("Training", "Testing"),
Jumlah = c(nrow(train_data), nrow(test_data)),
Persentase = c(
round(nrow(train_data) / nrow(Risk) * 100, 2),
round(nrow(test_data) / nrow(Risk) * 100, 2)
)
)
kable(split_table, caption = "Pembagian Data 70:30")
| Data | Jumlah | Persentase |
|---|---|---|
| Training | 710 | 70.16 |
| Testing | 302 | 29.84 |
Interpretasi: Data dibagi menggunakan rasio 70:30,
yaitu 70% untuk pelatihan model dan 30% untuk pengujian. Pembagian ini
memberikan keseimbangan antara kebutuhan model untuk mempelajari pola
dari data training dan kebutuhan evaluasi pada data testing. Penggunaan
createDataPartition() juga membantu mempertahankan proporsi
kelas pada data training dan testing.
evaluasi_model <- function(actual, pred){
actual <- factor(actual)
pred <- factor(pred, levels = levels(actual))
cm <- confusionMatrix(pred, actual)
accuracy <- cm$overall["Accuracy"]
precision <- mean(cm$byClass[, "Precision"], na.rm = TRUE)
recall <- mean(cm$byClass[, "Recall"], na.rm = TRUE)
f1 <- mean(cm$byClass[, "F1"], na.rm = TRUE)
data.frame(
Accuracy = round(as.numeric(accuracy), 4),
Precision = round(precision, 4),
Recall = round(recall, 4),
F1_Score = round(f1, 4)
)
}
Interpretasi: Fungsi evaluasi digunakan untuk menghitung empat metrik utama, yaitu accuracy, precision, recall, dan F1-score. Karena target memiliki lebih dari dua kelas, precision, recall, dan F1-score dihitung per kelas lalu dirata-ratakan. Pendekatan ini lebih informatif daripada hanya melihat accuracy karena setiap kelas risiko perlu diperhatikan.
set.seed(123)
dt_baseline <- rpart(
RiskLevel ~ .,
data = train_data,
method = "class"
)
rpart.plot(dt_baseline)
pred_dt <- predict(
dt_baseline,
test_data,
type = "class"
)
hasil_dt <- evaluasi_model(
test_data$RiskLevel,
pred_dt
)
kable(hasil_dt, caption = "Evaluasi Model Decision Tree Sebelum Tuning")
| Accuracy | Precision | Recall | F1_Score |
|---|---|---|---|
| 0.6523 | 0.6423 | 0.6626 | 0.6459 |
Interpretasi: Model Decision Tree sebelum tuning digunakan sebagai model dasar. Model ini membentuk pohon keputusan berdasarkan pola hubungan antara indikator kesehatan ibu hamil dan tingkat risiko. Nilai evaluasi pada tahap ini menjadi acuan awal untuk melihat apakah tuning mampu meningkatkan performa model.
set.seed(123)
n_iter <- 1000
hasil_tuning <- data.frame()
for(i in 1:n_iter){
cp_rand <- runif(1, 0.0001, 0.05)
maxdepth_rand <- sample(2:30, 1)
minsplit_rand <- sample(2:100, 1)
minbucket_rand <- sample(1:100, 1)
model <- rpart(
RiskLevel ~ .,
data = train_data,
method = "class",
control = rpart.control(
cp = cp_rand,
maxdepth = maxdepth_rand,
minsplit = minsplit_rand,
minbucket = minbucket_rand
)
)
pred <- predict(model, test_data, type = "class")
cm <- confusionMatrix(
factor(pred, levels = levels(test_data$RiskLevel)),
test_data$RiskLevel
)
accuracy <- as.numeric(cm$overall["Accuracy"])
hasil_tuning <- rbind(
hasil_tuning,
data.frame(
cp = cp_rand,
maxdepth = maxdepth_rand,
minsplit = minsplit_rand,
minbucket = minbucket_rand,
Accuracy = accuracy
)
)
}
best_param <- hasil_tuning[which.max(hasil_tuning$Accuracy), ]
kable(best_param, digits = 4, caption = "Parameter Terbaik Decision Tree")
| cp | maxdepth | minsplit | minbucket | Accuracy | |
|---|---|---|---|---|---|
| 201 | 0.0022 | 20 | 3 | 3 | 0.7715 |
Interpretasi: Tuning Decision Tree dilakukan dengan
mencari kombinasi terbaik dari cp, maxdepth,
minsplit, dan minbucket. Parameter tersebut
mengontrol kompleksitas pohon. Nilai parameter terbaik dipilih
berdasarkan accuracy tertinggi pada data testing. Namun, hasil tuning
tetap perlu dibaca kritis karena pemilihan parameter langsung
berdasarkan testing dapat membuat evaluasi menjadi terlalu optimistis.
Idealnya, tuning dilakukan pada data training melalui validasi silang,
lalu performa akhir diuji pada data testing.
dt_tuning <- rpart(
RiskLevel ~ .,
data = train_data,
method = "class",
control = rpart.control(
cp = best_param$cp,
maxdepth = best_param$maxdepth,
minsplit = best_param$minsplit,
minbucket = best_param$minbucket
)
)
pred_tuning <- predict(
dt_tuning,
test_data,
type = "class"
)
hasil_dt_tuning <- evaluasi_model(
test_data$RiskLevel,
pred_tuning
)
parameter_terbaik <- data.frame(
Parameter = c("cp", "maxdepth", "minsplit", "minbucket"),
Nilai = c(
best_param$cp,
best_param$maxdepth,
best_param$minsplit,
best_param$minbucket
)
)
kable(parameter_terbaik, digits = 4, caption = "Parameter Final Decision Tree")
| Parameter | Nilai |
|---|---|
| cp | 0.0022 |
| maxdepth | 20.0000 |
| minsplit | 3.0000 |
| minbucket | 3.0000 |
kable(hasil_dt_tuning, caption = "Evaluasi Model Decision Tree Setelah Tuning")
| Accuracy | Precision | Recall | F1_Score |
|---|---|---|---|
| 0.7715 | 0.7722 | 0.7782 | 0.7743 |
Interpretasi: Setelah menggunakan parameter terbaik, model Decision Tree hasil tuning dievaluasi kembali. Jika nilai accuracy, precision, recall, dan F1-score meningkat dibandingkan model baseline, maka tuning berhasil memperbaiki performa. Jika peningkatannya kecil atau tidak terjadi pada semua metrik, maka tuning tidak dapat diklaim selalu memperbaiki model secara menyeluruh.
tabel_hasil_dt <- rbind(
data.frame(Model = "Decision Tree Baseline", hasil_dt),
data.frame(Model = "Decision Tree + Tuning", hasil_dt_tuning)
)
kable(tabel_hasil_dt, caption = "Perbandingan Evaluasi Decision Tree")
| Model | Accuracy | Precision | Recall | F1_Score |
|---|---|---|---|---|
| Decision Tree Baseline | 0.6523 | 0.6423 | 0.6626 | 0.6459 |
| Decision Tree + Tuning | 0.7715 | 0.7722 | 0.7782 | 0.7743 |
Interpretasi: Tabel perbandingan menunjukkan perubahan performa Decision Tree sebelum dan sesudah tuning. Model yang lebih baik tidak hanya dilihat dari accuracy, tetapi juga dari precision, recall, dan F1-score. Dalam konteks risiko kesehatan ibu hamil, recall penting karena model perlu mampu mengenali sebanyak mungkin kasus pada setiap kelas risiko, terutama kelas risiko tinggi.
nb_baseline <- naiveBayes(
RiskLevel ~ .,
data = train_data
)
pred_nb <- predict(
nb_baseline,
test_data
)
hasil_nb <- evaluasi_model(
test_data$RiskLevel,
pred_nb
)
kable(hasil_nb, caption = "Evaluasi Model Naïve Bayes Sebelum Tuning")
| Accuracy | Precision | Recall | F1_Score |
|---|---|---|---|
| 0.6258 | 0.6252 | 0.6086 | 0.5799 |
Interpretasi: Model Naïve Bayes baseline dibangun berdasarkan pendekatan probabilistik. Model ini menghitung peluang setiap kelas risiko berdasarkan nilai prediktor. Kelebihan Naïve Bayes adalah sederhana dan cepat, tetapi kelemahannya terletak pada asumsi independensi antarvariabel yang sering kali sulit terpenuhi pada data kesehatan.
set.seed(123)
n_iter <- 1000
hasil_tuning_nb <- data.frame()
for(i in 1:n_iter){
fL_rand <- runif(1, min = 0, max = 5)
usekernel_rand <- sample(c(TRUE, FALSE), 1)
adjust_rand <- runif(1, min = 0.1, max = 5)
model <- NaiveBayes(
RiskLevel ~ .,
data = train_data,
fL = fL_rand,
usekernel = usekernel_rand,
adjust = adjust_rand
)
pred <- predict(model, test_data)$class
cm <- confusionMatrix(
factor(pred, levels = levels(test_data$RiskLevel)),
test_data$RiskLevel
)
accuracy <- as.numeric(cm$overall["Accuracy"])
hasil_tuning_nb <- rbind(
hasil_tuning_nb,
data.frame(
fL = fL_rand,
usekernel = usekernel_rand,
adjust = adjust_rand,
Accuracy = accuracy
)
)
}
best_param_nb <- hasil_tuning_nb[which.max(hasil_tuning_nb$Accuracy), ]
kable(best_param_nb, digits = 4, caption = "Parameter Terbaik Naïve Bayes")
| fL | usekernel | adjust | Accuracy | |
|---|---|---|---|---|
| 440 | 1.2755 | TRUE | 0.2661 | 0.6887 |
Interpretasi: Tuning Naïve Bayes dilakukan pada
parameter fL, usekernel, dan
adjust. Parameter fL berkaitan dengan koreksi
Laplace, usekernel menentukan penggunaan estimasi kernel,
sedangkan adjust mengatur tingkat kehalusan estimasi
distribusi. Sama seperti Decision Tree, hasil tuning perlu ditafsirkan
dengan hati-hati karena pemilihan parameter sebaiknya berbasis validasi
silang agar tidak terlalu menyesuaikan data testing.
nb_tuning <- NaiveBayes(
RiskLevel ~ .,
data = train_data,
fL = best_param_nb$fL,
usekernel = best_param_nb$usekernel,
adjust = best_param_nb$adjust
)
pred_nb_tuning <- predict(
nb_tuning,
test_data
)$class
hasil_nb_tuning <- evaluasi_model(
test_data$RiskLevel,
pred_nb_tuning
)
parameter_nb <- data.frame(
Parameter = c("fL", "usekernel", "adjust"),
Nilai = c(
best_param_nb$fL,
best_param_nb$usekernel,
best_param_nb$adjust
)
)
kable(parameter_nb, digits = 4, caption = "Parameter Final Naïve Bayes")
| Parameter | Nilai |
|---|---|
| fL | 1.2755 |
| usekernel | 1.0000 |
| adjust | 0.2661 |
kable(hasil_nb_tuning, caption = "Evaluasi Model Naïve Bayes Setelah Tuning")
| Accuracy | Precision | Recall | F1_Score |
|---|---|---|---|
| 0.6887 | 0.7174 | 0.6773 | 0.6887 |
Interpretasi: Model Naïve Bayes setelah tuning menunjukkan apakah penyesuaian parameter mampu memperbaiki performa dibandingkan baseline. Jika performa masih lebih rendah daripada Decision Tree, maka hal tersebut dapat terjadi karena Naïve Bayes mengasumsikan setiap prediktor saling independen, sedangkan indikator kesehatan seperti tekanan darah sistolik, diastolik, kadar gula darah, dan usia dapat memiliki hubungan tertentu.
tabel_hasil_nb <- rbind(
data.frame(Model = "Naïve Bayes Baseline", hasil_nb),
data.frame(Model = "Naïve Bayes + Tuning", hasil_nb_tuning)
)
kable(tabel_hasil_nb, caption = "Perbandingan Evaluasi Naïve Bayes")
| Model | Accuracy | Precision | Recall | F1_Score |
|---|---|---|---|---|
| Naïve Bayes Baseline | 0.6258 | 0.6252 | 0.6086 | 0.5799 |
| Naïve Bayes + Tuning | 0.6887 | 0.7174 | 0.6773 | 0.6887 |
Interpretasi: Tabel perbandingan Naïve Bayes menunjukkan perubahan performa sebelum dan sesudah tuning. Jika peningkatan hanya terjadi pada accuracy tetapi tidak pada recall atau F1-score, maka perbaikan model belum tentu substansial. Pada kasus klasifikasi risiko kesehatan, keseimbangan antar metrik lebih penting dibandingkan peningkatan accuracy semata.
tabel_semua_model <- rbind(
data.frame(Model = "Decision Tree Baseline", hasil_dt),
data.frame(Model = "Decision Tree + Tuning", hasil_dt_tuning),
data.frame(Model = "Naïve Bayes Baseline", hasil_nb),
data.frame(Model = "Naïve Bayes + Tuning", hasil_nb_tuning)
)
tabel_semua_model <- tabel_semua_model %>%
arrange(desc(Accuracy), desc(F1_Score), desc(Recall), desc(Precision))
kable(tabel_semua_model, caption = "Perbandingan Seluruh Model")
| Model | Accuracy | Precision | Recall | F1_Score |
|---|---|---|---|---|
| Decision Tree + Tuning | 0.7715 | 0.7722 | 0.7782 | 0.7743 |
| Naïve Bayes + Tuning | 0.6887 | 0.7174 | 0.6773 | 0.6887 |
| Decision Tree Baseline | 0.6523 | 0.6423 | 0.6626 | 0.6459 |
| Naïve Bayes Baseline | 0.6258 | 0.6252 | 0.6086 | 0.5799 |
Interpretasi: Model terbaik dipilih berdasarkan performa pada data testing dengan mempertimbangkan accuracy, precision, recall, dan F1-score. Pemilihan tidak sebaiknya hanya menggunakan accuracy karena pada konteks kesehatan, kesalahan dalam mengenali risiko dapat memiliki dampak serius. Model yang ideal adalah model dengan performa tinggi sekaligus seimbang pada seluruh metrik.
model_terbaik <- tabel_semua_model[1, ]
kable(model_terbaik, caption = "Model Terbaik Berdasarkan Evaluasi Testing")
| Model | Accuracy | Precision | Recall | F1_Score |
|---|---|---|---|---|
| Decision Tree + Tuning | 0.7715 | 0.7722 | 0.7782 | 0.7743 |
Interpretasi: Berdasarkan hasil perbandingan, model pada baris pertama merupakan model dengan performa terbaik secara keseluruhan. Jika model terbaik adalah Decision Tree + Tuning, maka model tersebut dapat dianggap lebih sesuai karena mampu menangkap pola pemisahan kelas melalui struktur pohon yang mudah diinterpretasikan. Namun, kesimpulan tetap perlu dibatasi pada dataset yang digunakan dan tidak langsung digeneralisasikan ke seluruh populasi ibu hamil.
cm_dt_tuning <- confusionMatrix(
factor(pred_tuning, levels = levels(test_data$RiskLevel)),
test_data$RiskLevel
)
cm_dt_tuning$table
## Reference
## Prediction High Risk Low Risk Mid Risk
## High Risk 73 3 11
## Low Risk 3 96 25
## Mid Risk 5 22 64
Interpretasi: Confusion matrix menunjukkan jumlah prediksi benar dan salah pada setiap kelas risiko. Nilai diagonal menunjukkan data yang berhasil diklasifikasikan dengan benar, sedangkan nilai di luar diagonal menunjukkan kesalahan klasifikasi. Bagian ini penting karena dapat memperlihatkan kelas mana yang paling sering tertukar oleh model.
classification_report <- data.frame(
Class = rownames(cm_dt_tuning$byClass),
Precision = round(cm_dt_tuning$byClass[, "Precision"], 4),
Recall = round(cm_dt_tuning$byClass[, "Recall"], 4),
F1_Score = round(cm_dt_tuning$byClass[, "F1"], 4),
Support = as.numeric(table(test_data$RiskLevel))
)
kable(classification_report, caption = "Classification Report Model Decision Tree + Tuning")
| Class | Precision | Recall | F1_Score | Support | |
|---|---|---|---|---|---|
| Class: High Risk | Class: High Risk | 0.8391 | 0.9012 | 0.8690 | 81 |
| Class: Low Risk | Class: Low Risk | 0.7742 | 0.7934 | 0.7837 | 121 |
| Class: Mid Risk | Class: Mid Risk | 0.7033 | 0.6400 | 0.6702 | 100 |
Interpretasi: Classification report memperlihatkan performa model pada masing-masing kelas. Precision menunjukkan ketepatan prediksi pada suatu kelas, recall menunjukkan kemampuan model mengenali seluruh data aktual pada kelas tersebut, sedangkan F1-score menunjukkan keseimbangan antara precision dan recall. Jika terdapat kelas dengan recall rendah, maka model masih kurang mampu mengenali kelas tersebut secara optimal.
prob_dt <- predict(
dt_tuning,
test_data,
type = "prob"
)
kelas <- levels(test_data$RiskLevel)
roc_list <- list()
auc_values <- c()
for(i in seq_along(kelas)){
roc_obj <- roc(
response = ifelse(test_data$RiskLevel == kelas[i], 1, 0),
predictor = prob_dt[, i],
quiet = TRUE
)
roc_list[[i]] <- roc_obj
auc_values[i] <- auc(roc_obj)
}
mean_auc <- mean(auc_values)
roc_df <- data.frame()
for(i in seq_along(kelas)){
roc_temp <- data.frame(
FPR = 1 - roc_list[[i]]$specificities,
TPR = roc_list[[i]]$sensitivities,
Class = kelas[i]
)
roc_df <- rbind(roc_df, roc_temp)
}
ggplot(roc_df, aes(x = FPR, y = TPR, color = Class)) +
geom_line(linewidth = 1.2) +
geom_abline(slope = 1, intercept = 0, linetype = "dashed") +
annotate(
"text",
x = 0.65,
y = 0.15,
label = paste("Mean AUC =", round(mean_auc, 4)),
size = 5,
fontface = "bold"
) +
labs(
title = "Kurva ROC Multiclass Model Decision Tree + Tuning",
x = "False Positive Rate (1 - Specificity)",
y = "True Positive Rate (Sensitivity)",
color = "Class"
) +
theme_bw(base_size = 14)
auc_table <- data.frame(
Class = kelas,
AUC = round(as.numeric(auc_values), 4)
)
kable(auc_table, caption = "Nilai AUC per Kelas")
| Class | AUC |
|---|---|
| High Risk | 0.9576 |
| Low Risk | 0.8790 |
| Mid Risk | 0.8402 |
Interpretasi: Kurva ROC dan nilai AUC digunakan untuk melihat kemampuan model dalam membedakan setiap kelas risiko melalui pendekatan one-vs-rest. Nilai AUC yang semakin mendekati 1 menunjukkan kemampuan diskriminasi yang semakin baik. Namun, AUC pada multiclass perlu dibaca bersama metrik lain karena model tetap dapat memiliki AUC cukup baik tetapi masih melakukan kesalahan pada kelas tertentu.
importance_dt <- data.frame(
Variabel = names(dt_tuning$variable.importance),
Importance = as.numeric(dt_tuning$variable.importance)
)
importance_dt <- importance_dt %>%
arrange(desc(Importance)) %>%
mutate(Gain = Importance / sum(Importance)) %>%
arrange(desc(Gain))
kable(importance_dt, digits = 4, caption = "Feature Importance Model Decision Tree")
| Variabel | Importance | Gain |
|---|---|---|
| BS | 178.9486 | 0.3209 |
| SystolicBP | 112.9588 | 0.2026 |
| DiastolicBP | 85.5745 | 0.1535 |
| Age | 84.3011 | 0.1512 |
| HeartRate | 60.7845 | 0.1090 |
| BodyTemp | 34.9991 | 0.0628 |
ggplot(
importance_dt,
aes(x = reorder(Variabel, Gain), y = Gain)
) +
geom_col(fill = "#4E79A7") +
geom_text(aes(label = round(Gain, 3)), hjust = -0.15, size = 4) +
coord_flip() +
expand_limits(y = max(importance_dt$Gain) * 1.15) +
labs(
title = "Feature Importance Model Decision Tree",
x = "Variabel",
y = "Gain"
) +
theme_bw(base_size = 14) +
theme(
plot.title = element_text(hjust = 0.5, face = "bold", size = 16),
axis.title = element_text(face = "bold"),
panel.grid.major.y = element_blank()
)
Interpretasi: Feature importance menunjukkan variabel yang paling berkontribusi dalam pembentukan model Decision Tree. Variabel dengan nilai gain terbesar memiliki peran paling kuat dalam memisahkan kelas risiko. Dalam konteks kesehatan ibu hamil, variabel yang dominan perlu ditafsirkan secara substantif, bukan hanya secara statistik, karena dapat menunjukkan indikator fisiologis yang paling membantu dalam deteksi risiko.
Berdasarkan hasil analisis, Decision Tree dan Naïve Bayes dapat digunakan untuk mengklasifikasikan tingkat risiko kesehatan ibu hamil berdasarkan indikator fisiologis. Secara umum, model terbaik dipilih berdasarkan keseimbangan nilai accuracy, precision, recall, dan F1-score pada data testing. Apabila Decision Tree + Tuning menjadi model dengan performa tertinggi, maka model tersebut lebih sesuai digunakan karena selain menghasilkan evaluasi yang baik, struktur pohonnya juga lebih mudah diinterpretasikan.
Namun, hasil penelitian tetap perlu dibaca secara kritis. Pertama, tuning sebaiknya dilakukan menggunakan validasi silang pada data training agar pemilihan parameter tidak terlalu dipengaruhi oleh data testing. Kedua, hasil model hanya berlaku pada dataset yang digunakan sehingga penerapan pada data ibu hamil di Indonesia membutuhkan validasi tambahan. Ketiga, dalam konteks kesehatan, recall pada kelas risiko tinggi perlu menjadi perhatian utama karena kesalahan mendeteksi ibu hamil berisiko tinggi dapat berdampak lebih serius dibandingkan kesalahan pada kelas risiko rendah.
Penelitian selanjutnya dapat menggunakan validasi silang, menambahkan metrik AUC multiclass, serta membandingkan metode lain seperti Random Forest, XGBoost, atau Support Vector Machine. Selain itu, penggunaan data yang lebih kontekstual dengan kondisi ibu hamil di Indonesia juga penting agar model yang dibangun lebih relevan untuk kebutuhan deteksi dini risiko kehamilan.