Penyakit kardiovaskular, termasuk gagal jantung, merupakan penyebab utama kematian di seluruh dunia. Organisasi Kesehatan Dunia (WHO) melaporkan bahwa penyakit jantung bertanggung jawab atas sekitar 17,9 juta kematian setiap tahunnya, mewakili 31% dari seluruh kematian global. Gagal jantung adalah kondisi medis serius di mana jantung tidak mampu memompa darah secara efektif untuk memenuhi kebutuhan tubuh, sehingga menyebabkan berbagai komplikasi dan berpotensi fatal.
Pemahaman mendalam mengenai faktor-faktor risiko yang memengaruhi kematian akibat gagal jantung sangat penting untuk pengembangan strategi pencegahan dan penanganan medis yang tepat. Regresi logistik biner merupakan salah satu metode statistika yang paling tepat digunakan untuk memodelkan hubungan antara peubah respon dikotomis (biner) dengan satu atau lebih peubah prediktor. Interpretasi koefisien melalui nilai odds ratio memberikan pemahaman langsung mengenai besaran pengaruh setiap peubah prediktor terhadap peubah respon.
Tujuan penelitian:
Data yang digunakan merupakan data sekunder dari dataset rekam medis klinis pasien gagal jantung yang dipublikasikan oleh Chicco dan Jurman (2020) pada UCI Machine Learning Repository. Dataset mencakup 299 pasien yang didiagnosis menderita gagal jantung dan dirawat di Faisalabad, Pakistan, antara April–Desember 2015, dengan masa tindak lanjut (follow-up) bervariasi antara 4 hingga 285 hari.
Tautan dataset: https://archive.ics.uci.edu/dataset/519/heart+failure+clinical+records
Peubah respon (\(Y\)) adalah status
kematian pasien selama masa tindak lanjut
(DEATH_EVENT):
Terdapat 12 peubah prediktor yang mencakup kondisi klinis dan demografis pasien:
| Peubah | Keterangan | Satuan | Skala |
|---|---|---|---|
| X1 | Usia (Age) | Tahun | Rasio |
| X2 | Anemia (Anaemia) | 0/1 | Nominal |
| X3 | Kreatin Fosfokinase (CPK) | mcg/L | Rasio |
| X4 | Diabetes | 0/1 | Nominal |
| X5 | Fraksi Ejeksi (Ejection Fraction) | % | Rasio |
| X6 | Tekanan Darah Tinggi | 0/1 | Nominal |
| X7 | Trombosit (Platelets) | kiloplatelets/mL | Rasio |
| X8 | Kreatinin Serum | mg/dL | Rasio |
| X9 | Natrium Serum | mEq/L | Rasio |
| X10 | Jenis Kelamin | 0=P, 1=L | Nominal |
| X11 | Merokok | 0/1 | Nominal |
| X12 | Waktu Tindak Lanjut | Hari | Rasio |
Logit dari model logistik berganda dituliskan sebagai:
\[ g(x) = \ln\left(\frac{\pi(x)}{1 - \pi(x)}\right) = \beta_0 + \beta_1 x_1 + \beta_2 x_2 + \cdots + \beta_p x_p \]
Model regresi logistik berganda:
\[ \pi(x) = \frac{e^{g(x)}}{1 + e^{g(x)}} \]
dengan \(\pi(x)\) adalah peluang kelas \(Y = 1\), \(\beta_0\) intersep, \(\beta_j\) koefisien regresi ke-\(j\), dan \(p\) banyaknya peubah prediktor.
# Daftar package yang digunakan
packages <- c(
"tidyverse", "psych", "corrplot", "car", "MASS",
"ResourceSelection", "pscl", "caret", "pROC",
"patchwork", "broom", "knitr", "gt", "ggcorrplot", "kableExtra"
)
# Menginstall package yang belum terpasang (hanya berjalan jika ada yang kurang)
paket_belum_ada <- packages[!sapply(packages, requireNamespace, quietly = TRUE)]
if (length(paket_belum_ada) > 0) {
install.packages(paket_belum_ada, repos = "https://cloud.r-project.org")
}
# Memuat seluruh package
invisible(
lapply(packages, library, character.only = TRUE)
)
alpha <- 0.05# Membaca dataset
# Catatan: sesuaikan path dengan lokasi file Anda.
# Saat menjalankan interaktif, dapat juga menggunakan: read.csv(file.choose())
data <- read.csv(file.choose())
glimpse(data)## Rows: 299
## Columns: 13
## $ age <int> 75, 55, 65, 50, 65, 90, 75, 60, 65, 80, 75, 6…
## $ anaemia <int> 0, 0, 0, 1, 1, 1, 1, 1, 0, 1, 1, 0, 1, 1, 1, …
## $ creatinine_phosphokinase <int> 582, 7861, 146, 111, 160, 47, 246, 315, 157, …
## $ diabetes <int> 0, 0, 0, 0, 1, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, …
## $ ejection_fraction <int> 20, 38, 20, 20, 20, 40, 15, 60, 65, 35, 38, 2…
## $ high_blood_pressure <int> 1, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, …
## $ platelets <dbl> 265000, 263358, 162000, 210000, 327000, 20400…
## $ serum_creatinine <dbl> 1.90, 1.10, 1.30, 1.90, 2.70, 2.10, 1.20, 1.1…
## $ serum_sodium <int> 130, 136, 129, 137, 116, 132, 137, 131, 138, …
## $ sex <int> 1, 1, 1, 1, 0, 1, 1, 1, 0, 1, 1, 1, 1, 1, 0, …
## $ smoking <int> 0, 0, 1, 0, 0, 1, 0, 1, 0, 1, 1, 1, 0, 0, 0, …
## $ time <int> 4, 6, 7, 7, 8, 8, 10, 10, 10, 10, 10, 10, 11,…
## $ DEATH_EVENT <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, …
## age anaemia creatinine_phosphokinase diabetes
## Min. :40.00 Min. :0.0000 Min. : 23.0 Min. :0.0000
## 1st Qu.:51.00 1st Qu.:0.0000 1st Qu.: 116.5 1st Qu.:0.0000
## Median :60.00 Median :0.0000 Median : 250.0 Median :0.0000
## Mean :60.83 Mean :0.4314 Mean : 581.8 Mean :0.4181
## 3rd Qu.:70.00 3rd Qu.:1.0000 3rd Qu.: 582.0 3rd Qu.:1.0000
## Max. :95.00 Max. :1.0000 Max. :7861.0 Max. :1.0000
## ejection_fraction high_blood_pressure platelets serum_creatinine
## Min. :14.00 Min. :0.0000 Min. : 25100 Min. :0.500
## 1st Qu.:30.00 1st Qu.:0.0000 1st Qu.:212500 1st Qu.:0.900
## Median :38.00 Median :0.0000 Median :262000 Median :1.100
## Mean :38.08 Mean :0.3512 Mean :263358 Mean :1.394
## 3rd Qu.:45.00 3rd Qu.:1.0000 3rd Qu.:303500 3rd Qu.:1.400
## Max. :80.00 Max. :1.0000 Max. :850000 Max. :9.400
## serum_sodium sex smoking time
## Min. :113.0 Min. :0.0000 Min. :0.0000 Min. : 4.0
## 1st Qu.:134.0 1st Qu.:0.0000 1st Qu.:0.0000 1st Qu.: 73.0
## Median :137.0 Median :1.0000 Median :0.0000 Median :115.0
## Mean :136.6 Mean :0.6488 Mean :0.3211 Mean :130.3
## 3rd Qu.:140.0 3rd Qu.:1.0000 3rd Qu.:1.0000 3rd Qu.:203.0
## Max. :148.0 Max. :1.0000 Max. :1.0000 Max. :285.0
## DEATH_EVENT
## Min. :0.0000
## 1st Qu.:0.0000
## Median :0.0000
## Mean :0.3211
## 3rd Qu.:1.0000
## Max. :1.0000
Catatan: Untuk menjalankan secara interaktif di RStudio, ganti baris
read.csv(...)dengandata <- read.csv(file.choose())lalu pilih file dataset secara manual.
death_summary <- data %>%
count(DEATH_EVENT) %>%
mutate(
Persentase = round(n / sum(n) * 100, 2),
Status = c("Tidak Meninggal", "Meninggal")
)
death_summaryggplot(death_summary, aes(x = Status, y = n, fill = Status)) +
geom_col(width = 0.65, show.legend = FALSE) +
geom_text(
aes(label = paste0(n, "\n(", Persentase, "%)")),
size = 5, vjust = 1.2
) +
labs(
title = "Distribusi Status Kematian Pasien",
x = "", y = "Jumlah Pasien"
) +
theme_minimal(base_size = 14)Dari 299 observasi tanpa missing value, sebanyak 203 pasien (67,89%) bertahan hidup dan 96 pasien (32,11%) meninggal.
kontinu <- data %>%
dplyr::select(
age, creatinine_phosphokinase, ejection_fraction,
platelets, serum_creatinine, serum_sodium, time
)
describe(kontinu)Untuk peubah biner: 129 pasien (43,1%) mengalami anemia, 125 (41,8%) menderita diabetes, 105 (35,1%) memiliki tekanan darah tinggi, 194 (64,9%) berjenis kelamin laki-laki, dan 96 (32,1%) merupakan perokok.
cor_matrix <- cor(
data %>% dplyr::select(-DEATH_EVENT),
use = "complete.obs"
)
nama_variabel <- c(
"X1_Usia", "X2_Anemia", "X3_Kr.Fosfokinase", "X4_Diabetes",
"X5_Fraksi Ejeksi", "X6_Tek.Darah Tinggi", "X7_Trombosit",
"X8_Kr.Serum", "X9_Natrium Serum", "X10_Jenis Kelamin",
"X11_Merokok", "X12_Waktu"
)
colnames(cor_matrix) <- nama_variabel
rownames(cor_matrix) <- nama_variabel
corrplot(
cor_matrix, method = "color", type = "upper",
addCoef.col = "black", number.cex = 0.7,
tl.col = "black", tl.pos = "td", tl.cex = 0.9, diag = TRUE,
col = colorRampPalette(c("#B2182B", "white", "#2166AC"))(200)
)
mtext("Plot Korelasi Peubah Prediktor", side = 3, line = 1, cex = 1.4, font = 3)Tidak ditemukan pasangan peubah dengan korelasi di atas 0,70 (ambang batas kritis). Nilai korelasi tertinggi terdapat antara jenis kelamin (X10) dan merokok (X11) sebesar 0,446, masih di bawah ambang batas, sehingga seluruh peubah dipertahankan.
model_full_vif <- glm(
DEATH_EVENT ~ age + anaemia + creatinine_phosphokinase + diabetes +
ejection_fraction + high_blood_pressure + platelets +
serum_creatinine + serum_sodium + sex + smoking + time,
family = binomial(link = "logit"), data = data
)
round(vif(model_full_vif), 2)## age anaemia creatinine_phosphokinase
## 1.10 1.11 1.09
## diabetes ejection_fraction high_blood_pressure
## 1.05 1.17 1.06
## platelets serum_creatinine serum_sodium
## 1.05 1.10 1.07
## sex smoking time
## 1.38 1.28 1.15
Seluruh peubah memiliki nilai VIF < 10 (berkisar 1,05–1,38), sehingga tidak terdeteksi masalah multikolinieritas. Semua peubah prediktor tetap dipertahankan dalam pemodelan.
model_full <- glm(
DEATH_EVENT ~ age + anaemia + creatinine_phosphokinase + diabetes +
ejection_fraction + high_blood_pressure + platelets +
serum_creatinine + serum_sodium + sex + smoking + time,
family = binomial(link = "logit"), data = data
)
summary(model_full)##
## Call:
## glm(formula = DEATH_EVENT ~ age + anaemia + creatinine_phosphokinase +
## diabetes + ejection_fraction + high_blood_pressure + platelets +
## serum_creatinine + serum_sodium + sex + smoking + time, family = binomial(link = "logit"),
## data = data)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 1.019e+01 5.656e+00 1.802 0.071588 .
## age 4.729e-02 1.579e-02 2.995 0.002749 **
## anaemia -6.938e-03 3.604e-01 -0.019 0.984644
## creatinine_phosphokinase 2.220e-04 1.779e-04 1.248 0.212035
## diabetes 1.452e-01 3.512e-01 0.414 0.679158
## ejection_fraction -7.663e-02 1.633e-02 -4.694 2.68e-06 ***
## high_blood_pressure -1.025e-01 3.587e-01 -0.286 0.775006
## platelets -1.198e-06 1.889e-06 -0.634 0.525992
## serum_creatinine 6.660e-01 1.815e-01 3.670 0.000242 ***
## serum_sodium -6.699e-02 3.973e-02 -1.686 0.091801 .
## sex -5.330e-01 4.139e-01 -1.288 0.197795
## smoking -1.376e-02 4.126e-01 -0.033 0.973389
## time -2.104e-02 3.014e-03 -6.981 2.94e-12 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 375.35 on 298 degrees of freedom
## Residual deviance: 219.60 on 286 degrees of freedom
## AIC: 245.6
##
## Number of Fisher Scoring iterations: 6
## [1] 245.6024
null_model <- glm(DEATH_EVENT ~ 1, family = binomial, data = data)
model_forward <- stepAIC(
null_model,
scope = list(lower = null_model, upper = model_full),
direction = "forward", trace = FALSE
)
summary(model_forward)##
## Call:
## glm(formula = DEATH_EVENT ~ time + ejection_fraction + serum_creatinine +
## age + serum_sodium, family = binomial, data = data)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 9.499662 5.405559 1.757 0.07885 .
## time -0.020891 0.002916 -7.165 7.76e-13 ***
## ejection_fraction -0.073404 0.015782 -4.651 3.30e-06 ***
## serum_creatinine 0.685916 0.174031 3.941 8.10e-05 ***
## age 0.042352 0.015023 2.819 0.00482 **
## serum_sodium -0.064562 0.038375 -1.682 0.09249 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 375.35 on 298 degrees of freedom
## Residual deviance: 223.53 on 293 degrees of freedom
## AIC: 235.53
##
## Number of Fisher Scoring iterations: 6
## [1] 235.5283
model_backward <- stepAIC(model_full, direction = "backward", trace = FALSE)
summary(model_backward)##
## Call:
## glm(formula = DEATH_EVENT ~ age + ejection_fraction + serum_creatinine +
## serum_sodium + time, family = binomial(link = "logit"), data = data)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 9.499662 5.405559 1.757 0.07885 .
## age 0.042352 0.015023 2.819 0.00482 **
## ejection_fraction -0.073404 0.015782 -4.651 3.30e-06 ***
## serum_creatinine 0.685916 0.174031 3.941 8.10e-05 ***
## serum_sodium -0.064562 0.038375 -1.682 0.09249 .
## time -0.020891 0.002916 -7.165 7.76e-13 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 375.35 on 298 degrees of freedom
## Residual deviance: 223.53 on 293 degrees of freedom
## AIC: 235.53
##
## Number of Fisher Scoring iterations: 6
## [1] 235.5283
model_significant <- glm(
DEATH_EVENT ~ age + ejection_fraction + serum_creatinine + time,
family = binomial(link = "logit"), data = data
)
summary(model_significant)##
## Call:
## glm(formula = DEATH_EVENT ~ age + ejection_fraction + serum_creatinine +
## time, family = binomial(link = "logit"), data = data)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 0.610280 1.036011 0.589 0.55582
## age 0.043214 0.014865 2.907 0.00365 **
## ejection_fraction -0.074778 0.015552 -4.808 1.52e-06 ***
## serum_creatinine 0.719735 0.174593 4.122 3.75e-05 ***
## time -0.020607 0.002881 -7.153 8.50e-13 ***
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 375.35 on 298 degrees of freedom
## Residual deviance: 226.34 on 294 degrees of freedom
## AIC: 236.34
##
## Number of Fisher Scoring iterations: 5
## [1] 236.3443
aic_table <- data.frame(
Model = c("Model Penuh", "Model Forward", "Model Backward", "Model Signifikan"),
AIC = c(
AIC(model_full), AIC(model_forward),
AIC(model_backward), AIC(model_significant)
)
)
aic_table$AIC <- round(aic_table$AIC, 5)
kable(aic_table, caption = "Perbandingan Nilai AIC Antarmodel") %>%
kable_styling(
bootstrap_options = c("striped", "hover"),
full_width = FALSE, position = "center"
) %>%
row_spec(0, bold = TRUE, color = "white", background = "#1a5276")| Model | AIC |
|---|---|
| Model Penuh | 245.6024 |
| Model Forward | 235.5283 |
| Model Backward | 235.5283 |
| Model Signifikan | 236.3443 |
model_list <- list(
"Penuh" = model_full,
"Forward" = model_forward,
"Backward" = model_backward,
"Peubah Signifikan" = model_significant
)
hasil_model <- lapply(names(model_list), function(nama_model) {
model <- model_list[[nama_model]]
prob <- predict(model, type = "response")
pred <- ifelse(prob > 0.5, 1, 0)
cm <- confusionMatrix(
factor(pred), factor(data$DEATH_EVENT), positive = "1"
)
data.frame(
Model = nama_model,
AIC = round(AIC(model), 5),
A = round(100 * cm$overall["Accuracy"], 2),
S = round(100 * cm$byClass["Sensitivity"], 2),
Sp = round(100 * cm$byClass["Specificity"], 2),
BA = round(100 * cm$byClass["Balanced Accuracy"], 2),
Peubah = paste0(length(coef(model)) - 1, " dari 12")
)
})
tabel_model <- do.call(rbind, hasil_model)
rownames(tabel_model) <- NULL
kable(
tabel_model, digits = 2,
caption = "Perbandingan Kinerja Kandidat Model Regresi Logistik Biner"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE, position = "center"
) %>%
row_spec(0, bold = TRUE, color = "white", background = "#1a5276") %>%
row_spec(2, bold = TRUE, background = "#e9f9f4") # baris model forward (terpilih)| Model | AIC | A | S | Sp | BA | Peubah |
|---|---|---|---|---|---|---|
| Penuh | 245.60 | 85.62 | 71.88 | 92.12 | 82.00 | 12 dari 12 |
| Forward | 235.53 | 83.61 | 68.75 | 90.64 | 79.70 | 5 dari 12 |
| Backward | 235.53 | 83.61 | 68.75 | 90.64 | 79.70 | 5 dari 12 |
| Peubah Signifikan | 236.34 | 83.28 | 68.75 | 90.15 | 79.45 | 4 dari 12 |
Model forward dan backward menghasilkan AIC terkecil yang sama, yaitu 235,49, dengan akurasi, sensitivitas, spesifisitas, dan akurasi seimbang yang identik. Berdasarkan prinsip parsimoni, model forward dipilih sebagai model akhir. Model ini memuat lima peubah: usia (X1), fraksi ejeksi (X5), kreatinin serum (X8), natrium serum (X9), dan waktu tindak lanjut (X12).
\[ \text{Logit}[P(Y=1)] = 9{,}4930 + 0{,}0425\,X_1 - 0{,}0734\,X_5 + 0{,}6860\,X_8 - 0{,}0646\,X_9 - 0{,}0209\,X_{12} \]
Nilai \(G^2 = 151{,}8625 > \chi^2_{(0,05; 5)} = 11{,}070\), maka \(H_0\) ditolak. Dengan demikian, minimal terdapat satu peubah prediktor yang berpengaruh signifikan terhadap status kematian pasien.
##
## Call:
## glm(formula = DEATH_EVENT ~ time + ejection_fraction + serum_creatinine +
## age + serum_sodium, family = binomial, data = data)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 9.499662 5.405559 1.757 0.07885 .
## time -0.020891 0.002916 -7.165 7.76e-13 ***
## ejection_fraction -0.073404 0.015782 -4.651 3.30e-06 ***
## serum_creatinine 0.685916 0.174031 3.941 8.10e-05 ***
## age 0.042352 0.015023 2.819 0.00482 **
## serum_sodium -0.064562 0.038375 -1.682 0.09249 .
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 375.35 on 298 degrees of freedom
## Residual deviance: 223.53 on 293 degrees of freedom
## AIC: 235.53
##
## Number of Fisher Scoring iterations: 6
Peubah usia (X1), fraksi ejeksi (X5), kreatinin serum (X8), dan waktu tindak lanjut (X12) memiliki nilai-p < 0,05 sehingga berpengaruh signifikan. Peubah natrium serum (X9) memiliki nilai-p = 0,0925 (> 0,05) sehingga tidak signifikan secara parsial, namun tetap dipertahankan karena meningkatkan kualitas model berdasarkan AIC.
##
## Hosmer and Lemeshow goodness of fit (GOF) test
##
## data: data$DEATH_EVENT, fitted(best_model)
## X-squared = 12.959, df = 8, p-value = 0.1133
Nilai-p uji Hosmer–Lemeshow lebih besar dari 0,05, sehingga \(H_0\) tidak ditolak. Model regresi logistik biner yang terpilih layak digunakan pada taraf nyata 5%.
OR <- exp(coef(best_model))
CI <- exp(confint(best_model))
OR_table <- data.frame(
Variabel = names(OR),
OddsRatio = round(OR, 4),
CI_Lower = round(CI[, 1], 4),
CI_Upper = round(CI[, 2], 4)
)
rownames(OR_table) <- NULL
kable(OR_table, caption = "Odds Ratio Model Terpilih") %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE, position = "center"
) %>%
row_spec(0, bold = TRUE, color = "white", background = "#1a5276")| Variabel | OddsRatio | CI_Lower | CI_Upper |
|---|---|---|---|
| (Intercept) | 13355.2162 | 0.3365 | 6.980819e+08 |
| time | 0.9793 | 0.9734 | 9.846000e-01 |
| ejection_fraction | 0.9292 | 0.8995 | 9.572000e-01 |
| serum_creatinine | 1.9856 | 1.4214 | 2.874100e+00 |
| age | 1.0433 | 1.0137 | 1.075500e+00 |
| serum_sodium | 0.9375 | 0.8681 | 1.011000e+00 |
prob <- predict(best_model, type = "response")
pred <- ifelse(prob > 0.5, 1, 0)
cm <- confusionMatrix(
factor(pred), factor(data$DEATH_EVENT), positive = "1"
)
cm## Confusion Matrix and Statistics
##
## Reference
## Prediction 0 1
## 0 184 30
## 1 19 66
##
## Accuracy : 0.8361
## 95% CI : (0.7892, 0.8762)
## No Information Rate : 0.6789
## P-Value [Acc > NIR] : 5.259e-10
##
## Kappa : 0.6124
##
## Mcnemar's Test P-Value : 0.1531
##
## Sensitivity : 0.6875
## Specificity : 0.9064
## Pos Pred Value : 0.7765
## Neg Pred Value : 0.8598
## Prevalence : 0.3211
## Detection Rate : 0.2207
## Detection Prevalence : 0.2843
## Balanced Accuracy : 0.7970
##
## 'Positive' Class : 1
##
Model menghasilkan akurasi 83,61%, sensitivitas 68,75%, spesifisitas 90,64%, dan akurasi seimbang 79,70%.
Visualisasi kurva sigmoid untuk keempat peubah signifikan dalam model terbaik.
plot_logistic_curve <- function(var_name) {
label_id <- c(
age = "Usia",
ejection_fraction = "Fraksi Ejeksi",
serum_creatinine = "Kreatinin Serum",
time = "Waktu Tindak Lanjut"
)
newdata <- data.frame(
age = median(data$age),
anaemia = median(data$anaemia),
creatinine_phosphokinase = median(data$creatinine_phosphokinase),
diabetes = median(data$diabetes),
ejection_fraction = median(data$ejection_fraction),
high_blood_pressure = median(data$high_blood_pressure),
platelets = median(data$platelets),
serum_creatinine = median(data$serum_creatinine),
serum_sodium = median(data$serum_sodium),
sex = median(data$sex),
smoking = median(data$smoking),
time = median(data$time)
)
newdata <- newdata[rep(1, 100), ]
newdata[[var_name]] <- seq(
min(data[[var_name]]), max(data[[var_name]]), length.out = 100
)
pred_curve <- predict(
best_model, newdata = newdata, type = "link", se.fit = TRUE
)
newdata$prob <- plogis(pred_curve$fit)
newdata$lower <- plogis(pred_curve$fit - 1.96 * pred_curve$se.fit)
newdata$upper <- plogis(pred_curve$fit + 1.96 * pred_curve$se.fit)
ggplot() +
geom_jitter(
data = data, aes(x = .data[[var_name]], y = DEATH_EVENT),
width = 0, height = 0.03, color = "#6B7280", alpha = 0.35, size = 2
) +
geom_ribbon(
data = newdata, aes(x = .data[[var_name]], ymin = lower, ymax = upper),
fill = "#60A5FA", alpha = 0.25
) +
geom_line(
data = newdata, aes(x = .data[[var_name]], y = prob),
linewidth = 1.5, color = "#1D4ED8"
) +
geom_hline(
yintercept = 0.5, linetype = "dashed", linewidth = 0.8, color = "#DC2626"
) +
labs(
title = paste("Kurva Sigmoid:", label_id[var_name]),
x = label_id[var_name], y = "Probabilitas Kematian"
) +
scale_y_continuous(limits = c(0, 1), breaks = seq(0, 1, 0.2)) +
theme_minimal(base_size = 13) +
theme(
plot.title = element_text(face = "bold", size = 14, hjust = 0.5),
axis.title = element_text(face = "bold"),
panel.grid.minor = element_blank()
)
}p1 <- plot_logistic_curve("age")
p2 <- plot_logistic_curve("ejection_fraction")
p3 <- plot_logistic_curve("serum_creatinine")
p4 <- plot_logistic_curve("time")
(p1 + p2) / (p3 + p4)Model terbaik untuk memprediksi risiko kematian akibat gagal jantung diperoleh melalui metode forward selection dengan nilai AIC terkecil 235,49 dan akurasi 83,61%. Uji Hosmer–Lemeshow menunjukkan model layak digunakan.
Sejalan dengan Chicco dan Jurman (2020), fraksi ejeksi jantung dan kreatinin serum merupakan dua parameter krusial. Kreatinin serum menjadi prediktor risiko tertinggi — setiap kenaikan satu satuan meningkatkan kemungkinan kematian hampir dua kali lipat, mendukung konsep medis kardio-renal syndrome. Faktor demografis seperti usia dan lama waktu tindak lanjut juga berpengaruh signifikan. Natrium serum, meski tidak signifikan secara parsial, tetap dipertahankan karena memperbaiki kualitas prediksi model secara keseluruhan.
Saran penelitian lanjutan: menggunakan data dengan observasi lebih besar dan periode tindak lanjut lebih panjang; menambahkan peubah klinis lain (BNP, riwayat rawat inap, kelas NYHA, tekanan darah sistolik); serta membandingkan dengan metode klasifikasi lain seperti Random Forest, Gradient Boosting, SVM, atau XGBoost.