Diabetes melitus merupakan salah satu penyakit tidak menular yang menjadi permasalahan kesehatan global dengan prevalensi yang terus meningkat secara signifikan dari tahun ke tahun. Berdasarkan laporan International Diabetes Federation (IDF) edisi ke-10 tahun 2021, terdapat sekitar 537 juta orang dewasa (usia 20–79 tahun) yang hidup dengan diabetes di seluruh dunia, dan angka ini diproyeksikan meningkat menjadi 853 juta pada tahun 2050. Indonesia sendiri menempati peringkat kelima negara dengan jumlah penderita diabetes terbanyak di dunia, dengan estimasi 19,47 juta penderita pada tahun 2021.
Menurut WHO, diabetes adalah penyakit kronis yang terjadi ketika pankreas tidak menghasilkan cukup insulin atau ketika tubuh tidak dapat secara efektif menggunakan insulin yang dihasilkannya. Kondisi ini dapat menyebabkan komplikasi serius seperti penyakit jantung, stroke, gagal ginjal, kebutaan, hingga amputasi. Data Riskesdas Kemenkes RI menunjukkan bahwa prevalensi diabetes melitus pada penduduk usia ≥15 tahun meningkat dari 6,9% (2013) menjadi 10,9% (2018).
Untuk menganalisis hubungan antara faktor-faktor risiko tersebut dengan status diabetes, regresi logistik biner merupakan salah satu metode yang paling banyak digunakan dalam pemodelan variabel respons dikotomi. Metode ini memiliki keunggulan dalam menginterpretasikan pengaruh setiap variabel prediktor terhadap probabilitas kejadian suatu peristiwa, serta kemampuannya dalam menangani variabel prediktor campuran (numerik dan kategorik).
Tujuan penelitian ini adalah:
Dataset yang digunakan adalah Early Stage Diabetes Risk Prediction Dataset dari UCI Machine Learning Repository (ID: 529), dikumpulkan dari Rumah Sakit Sylhet, Bangladesh. Dataset terdiri dari 520 observasi dengan 17 variabel: satu variabel kontinu (usia), 15 variabel prediktor kategorik (gejala klinis dan faktor demografi), dan satu variabel respon biner (status diabetes: Positif/Negatif).
Referensi Dataset: Islam, M.M.F. et al. (2020). Likelihood prediction of diabetes at early stage using data mining techniques. https://doi.org/10.24432/C5VG8H
Materi: Analisis ini memerlukan beberapa paket R.
Paket glm (base R) digunakan untuk estimasi model logistik,
MASS untuk seleksi stepwise, car untuk uji
VIF, ResourceSelection untuk uji Hosmer-Lemeshow,
pROC untuk kurva ROC, caret untuk pembagian
data, broom untuk output tidy, dan ggplot2
untuk visualisasi.
required_packages <- c(
"dplyr", "ggplot2", "broom", "knitr", "scales", "tidyr",
"car", "pROC", "ResourceSelection", "MASS", "caret", "lmtest"
)
for (pkg in required_packages) {
if (!require(pkg, character.only = TRUE, quietly = TRUE)) {
install.packages(pkg)
library(pkg, character.only = TRUE)
}
}
# Pastikan select() menggunakan dplyr, bukan MASS
select <- dplyr::select
set.seed(123)Materi: Data dibaca langsung dari UCI Machine
Learning Repository menggunakan URL. Fungsi tryCatch()
digunakan sebagai fallback apabila koneksi internet tidak tersedia —
dalam kondisi tersebut, pengguna diminta memilih file secara manual.
url_data <- "https://archive.ics.uci.edu/ml/machine-learning-databases/00529/diabetes_data_upload.csv"
raw_data <- tryCatch(
read.csv(url(url_data), header = TRUE, stringsAsFactors = FALSE,
check.names = FALSE),
error = function(e) {
message("Gagal mengunduh dari UCI. Silakan pilih file secara manual.")
read.csv(file.choose(), header = TRUE, stringsAsFactors = FALSE,
check.names = FALSE)
}
)knitr::kable(
data.frame(
Informasi = c("Jumlah Baris", "Jumlah Kolom"),
Nilai = c(nrow(raw_data), ncol(raw_data))
),
caption = "Dimensi Dataset"
)| Informasi | Nilai |
|---|---|
| Jumlah Baris | 520 |
| Jumlah Kolom | 17 |
Interpretasi: Dataset berhasil dibaca dengan 520 observasi dan 17 variabel (termasuk variabel respon).
Materi: Sebelum analisis, perlu dipastikan tidak ada
data yang hilang (missing values). Adanya missing values dapat
menyebabkan bias estimasi dan mengurangi ukuran sampel efektif. Fungsi
is.na() dan colSums() digunakan untuk
mendeteksi missing per kolom.
## 'data.frame': 520 obs. of 17 variables:
## $ Age : int 40 58 41 45 60 55 57 66 67 70 ...
## $ Gender : chr "Male" "Male" "Male" "Male" ...
## $ Polyuria : chr "No" "No" "Yes" "No" ...
## $ Polydipsia : chr "Yes" "No" "No" "No" ...
## $ sudden weight loss: chr "No" "No" "No" "Yes" ...
## $ weakness : chr "Yes" "Yes" "Yes" "Yes" ...
## $ Polyphagia : chr "No" "No" "Yes" "Yes" ...
## $ Genital thrush : chr "No" "No" "No" "Yes" ...
## $ visual blurring : chr "No" "Yes" "No" "No" ...
## $ Itching : chr "Yes" "No" "Yes" "Yes" ...
## $ Irritability : chr "No" "No" "No" "No" ...
## $ delayed healing : chr "Yes" "No" "Yes" "Yes" ...
## $ partial paresis : chr "No" "Yes" "No" "No" ...
## $ muscle stiffness : chr "Yes" "No" "Yes" "No" ...
## $ Alopecia : chr "Yes" "Yes" "Yes" "No" ...
## $ Obesity : chr "Yes" "No" "No" "No" ...
## $ class : chr "Positive" "Positive" "Positive" "Positive" ...
missing_counts <- colSums(is.na(raw_data))
knitr::kable(
data.frame(Variabel = names(missing_counts),
`Jumlah Missing` = as.integer(missing_counts),
check.names = FALSE),
caption = "Jumlah Missing Values per Variabel"
)| Variabel | Jumlah Missing |
|---|---|
| Age | 0 |
| Gender | 0 |
| Polyuria | 0 |
| Polydipsia | 0 |
| sudden weight loss | 0 |
| weakness | 0 |
| Polyphagia | 0 |
| Genital thrush | 0 |
| visual blurring | 0 |
| Itching | 0 |
| Irritability | 0 |
| delayed healing | 0 |
| partial paresis | 0 |
| muscle stiffness | 0 |
| Alopecia | 0 |
| Obesity | 0 |
| class | 0 |
Interpretasi: Tidak ditemukan missing values pada dataset ini. Seluruh 520 observasi dapat digunakan dalam analisis selanjutnya tanpa penghapusan baris.
Materi: Rekoding variabel dilakukan agar fungsi
glm() dapat mengidentifikasi kategori referensi secara
eksplisit. Skema rekoding mengacu pada Hosmer & Lemeshow (2013) dan
Agresti (2018):
class):
Negative → 0 (referensi), Positive → 1Female → 0 (referensi),
Male → 1No → 0 (referensi),
Yes → 1Variabel dikodekan numerik 0/1 untuk model GLM, sedangkan versi
berlabel faktor (_f) digunakan hanya untuk tabel dan
visualisasi EDA.
diabetes <- raw_data %>%
transmute(
# Variabel respon — numerik (0/1) untuk model GLM
diabetes_positif = as.integer(class == "Positive"),
# Variabel respon — faktor berlabel untuk tabel dan plot
status_diabetes = factor(class,
levels = c("Negative", "Positive"),
labels = c("Negatif", "Positif")),
# Prediktor kontinu
age = as.numeric(Age),
# Prediktor kategorik dikode 0/1
gender = as.integer(Gender == "Male"),
polyuria = as.integer(Polyuria == "Yes"),
polydipsia = as.integer(Polydipsia == "Yes"),
sudden_wt_loss = as.integer(`sudden weight loss` == "Yes"),
weakness = as.integer(weakness == "Yes"),
polyphagia = as.integer(Polyphagia == "Yes"),
genital_thrush = as.integer(`Genital thrush` == "Yes"),
visual_blurring = as.integer(`visual blurring` == "Yes"),
itching = as.integer(Itching == "Yes"),
irritability = as.integer(Irritability == "Yes"),
delayed_healing = as.integer(`delayed healing` == "Yes"),
partial_paresis = as.integer(`partial paresis` == "Yes"),
muscle_stiffness = as.integer(`muscle stiffness` == "Yes"),
alopecia = as.integer(Alopecia == "Yes"),
obesity = as.integer(Obesity == "Yes")
)
# Versi berlabel faktor untuk EDA
diabetes_plot <- diabetes %>%
mutate(
gender_f = factor(gender, levels = c(0,1), labels = c("0","1")),
polyuria_f = factor(polyuria, levels = c(0,1), labels = c("0","1")),
polydipsia_f = factor(polydipsia, levels = c(0,1), labels = c("0","1")),
sudden_wt_loss_f = factor(sudden_wt_loss, levels = c(0,1), labels = c("0","1")),
weakness_f = factor(weakness, levels = c(0,1), labels = c("0","1")),
polyphagia_f = factor(polyphagia, levels = c(0,1), labels = c("0","1")),
genital_thrush_f = factor(genital_thrush, levels = c(0,1), labels = c("0","1")),
visual_blurring_f = factor(visual_blurring, levels = c(0,1), labels = c("0","1")),
itching_f = factor(itching, levels = c(0,1), labels = c("0","1")),
irritability_f = factor(irritability, levels = c(0,1), labels = c("0","1")),
delayed_healing_f = factor(delayed_healing, levels = c(0,1), labels = c("0","1")),
partial_paresis_f = factor(partial_paresis, levels = c(0,1), labels = c("0","1")),
muscle_stiffness_f = factor(muscle_stiffness, levels = c(0,1), labels = c("0","1")),
alopecia_f = factor(alopecia, levels = c(0,1), labels = c("0","1")),
obesity_f = factor(obesity, levels = c(0,1), labels = c("0","1"))
)Interpretasi: Rekoding selesai. Dataset akhir terdiri dari 520 baris dan 18 kolom. Seluruh prediktor kategorik telah dikodekan sebagai 0/1 dengan kategori referensi yang eksplisit.
Materi: Pembagian data menjadi training set dan test set dilakukan sebelum analisis deskriptif dan pemodelan untuk menghindari data leakage. Model dibangun hanya menggunakan data train, sedangkan data test digunakan untuk evaluasi kinerja out-of-sample.
Metode yang digunakan adalah stratified random
sampling via caret::createDataPartition(), yang
memastikan proporsi kelas positif/negatif terjaga di kedua set.
Referensi: Harrell (2015), Regression Modeling Strategies, 2nd ed.
idx_train <- caret::createDataPartition(diabetes$diabetes_positif,
p = 0.80, list = FALSE)
data_train <- diabetes[idx_train, ]
data_test <- diabetes[-idx_train, ]tbl_split <- data.frame(
Set = c("Training", "Testing", "Total"),
n = c(nrow(data_train), nrow(data_test), nrow(diabetes)),
`% Positif` = c(
round(100 * mean(data_train$diabetes_positif), 1),
round(100 * mean(data_test$diabetes_positif), 1),
round(100 * mean(diabetes$diabetes_positif), 1)
),
`% Negatif` = c(
round(100 * mean(data_train$diabetes_positif == 0), 1),
round(100 * mean(data_test$diabetes_positif == 0), 1),
round(100 * mean(diabetes$diabetes_positif == 0), 1)
),
check.names = FALSE
)
knitr::kable(tbl_split,
caption = "Ringkasan Pembagian Data Training dan Testing (80:20, Stratified)")| Set | n | % Positif | % Negatif |
|---|---|---|---|
| Training | 416 | 60.8 | 39.2 |
| Testing | 104 | 64.4 | 35.6 |
| Total | 520 | 61.5 | 38.5 |
Interpretasi: Data train terdiri dari 416 observasi (80%) dan data test terdiri dari 104 observasi (20%). Stratified sampling berhasil menjaga proporsi kelas positif yang konsisten di kedua subset, memastikan evaluasi model tidak bias akibat ketidakseimbangan distribusi kelas.
Materi: Statistika deskriptif memberikan gambaran awal distribusi data sebelum pemodelan. Untuk variabel kontinu (usia), dihitung rata-rata, median, standar deviasi, nilai minimum, dan maksimum. Kedekatan nilai mean dan median mengindikasikan distribusi yang relatif simetris.
ringkasan_age <- diabetes %>%
summarise(
n = n(),
Rata2_Usia = round(mean(age), 2),
Median = round(median(age), 2),
SD = round(sd(age), 2),
Min = min(age),
Max = max(age)
)
knitr::kable(ringkasan_age, caption = "Statistika Deskriptif Usia Responden")| n | Rata2_Usia | Median | SD | Min | Max |
|---|---|---|---|---|---|
| 520 | 48.03 | 47.5 | 12.15 | 16 | 90 |
ringkasan_age_grup <- diabetes %>%
group_by(status_diabetes) %>%
summarise(
n = n(),
Mean = round(mean(age), 2),
Median = round(median(age), 2),
SD = round(sd(age), 2),
Min = min(age),
Max = max(age),
.groups = "drop"
)
knitr::kable(ringkasan_age_grup,
caption = "Statistika Deskriptif Usia Berdasarkan Status Diabetes")| status_diabetes | n | Mean | Median | SD | Min | Max |
|---|---|---|---|---|---|---|
| Negatif | 200 | 46.36 | 45 | 12.08 | 26 | 72 |
| Positif | 320 | 49.07 | 48 | 12.10 | 16 | 90 |
Interpretasi: Rata-rata usia seluruh responden adalah 48.03 tahun dengan rentang 16–90 tahun. Nilai mean dan median yang berdekatan mengindikasikan distribusi usia relatif simetris. Responden dengan diabetes positif rata-rata lebih tua (~49 tahun) dibandingkan yang negatif (~46 tahun), meskipun perbedaan ini perlu dikonfirmasi dalam analisis multivariat.
ggplot(diabetes, aes(x = age, fill = status_diabetes)) +
geom_histogram(binwidth = 5, color = "white", alpha = 0.8,
position = "dodge") +
scale_fill_manual(values = c("Negatif" = "#5b9bd5", "Positif" = "#e06c75")) +
labs(title = "Distribusi Usia Berdasarkan Status Diabetes",
x = "Usia (tahun)", y = "Frekuensi", fill = "Status Diabetes") +
theme_minimal(base_size = 12) +
theme(legend.position = "top")Distribusi Usia Berdasarkan Status Diabetes
ggplot(diabetes, aes(x = age, fill = status_diabetes)) +
geom_density(alpha = 0.45) +
scale_fill_manual(values = c("Negatif" = "#5b9bd5", "Positif" = "#e06c75")) +
labs(title = "Kurva Densitas Usia Berdasarkan Status Diabetes",
x = "Usia (tahun)", y = "Densitas", fill = "Status Diabetes") +
theme_minimal(base_size = 12)Kurva Densitas Usia Berdasarkan Status Diabetes
jk_tbl <- diabetes %>%
mutate(Jenis_Kelamin = ifelse(gender == 1, "Laki-laki", "Perempuan")) %>%
count(Jenis_Kelamin) %>%
mutate(Proporsi = scales::percent(n / sum(n), accuracy = 0.1)) %>%
rename(Jumlah = n)
knitr::kable(jk_tbl, caption = "Distribusi Jenis Kelamin Responden")| Jenis_Kelamin | Jumlah | Proporsi |
|---|---|---|
| Laki-laki | 328 | 63.1% |
| Perempuan | 192 | 36.9% |
Interpretasi: Mayoritas responden berjenis kelamin laki-laki (63,1%). Distribusi ini mencerminkan komposisi pasien di rumah sakit tempat data dikumpulkan, bukan prevalensi diabetes di populasi umum.
Materi: Pemeriksaan distribusi variabel respon penting untuk mendeteksi class imbalance. Apabila proporsi salah satu kelas < 20% atau > 80%, diperlukan teknik penyeimbangan seperti SMOTE atau oversampling sebelum pemodelan.
distribusi_y <- diabetes %>%
count(status_diabetes) %>%
mutate(Proporsi = scales::percent(n / sum(n), accuracy = 0.1)) %>%
rename(Jumlah = n)
knitr::kable(distribusi_y, caption = "Distribusi Status Diabetes")| status_diabetes | Jumlah | Proporsi |
|---|---|---|
| Negatif | 200 | 38.5% |
| Positif | 320 | 61.5% |
ggplot(diabetes_plot, aes(x = status_diabetes, fill = status_diabetes)) +
geom_bar(width = 0.55, color = "white") +
geom_text(stat = "count", aes(label = after_stat(count)),
vjust = -0.4, size = 4.5) +
scale_fill_manual(values = c("Negatif" = "#5b9bd5", "Positif" = "#e06c75")) +
labs(title = "Distribusi Status Diabetes",
x = "Status Diabetes", y = "Jumlah Observasi") +
theme_minimal(base_size = 12) +
theme(legend.position = "none")Distribusi Status Diabetes
Interpretasi: Dari 520 responden, 320 orang (61,5%) didiagnosis positif diabetes dan 200 orang (38,5%) negatif. Proporsi positif sebesar 61,5% berada dalam rentang 20%–80%, sehingga distribusi kelas dinyatakan tidak terjadi class imbalance yang serius. Model regresi logistik standar dapat diterapkan tanpa teknik penyeimbangan kelas.
var_kategorik <- c(
"gender", "polyuria", "polydipsia", "sudden_wt_loss", "weakness",
"polyphagia", "genital_thrush", "visual_blurring", "itching",
"irritability", "delayed_healing", "partial_paresis",
"muscle_stiffness", "alopecia", "obesity"
)
var_label <- c(
"Gender", "Polyuria", "Polydipsia", "Sudden Weight Loss",
"Weakness", "Polyphagia", "Genital Thrush", "Visual Blurring",
"Itching", "Irritability", "Delayed Healing", "Partial Paresis",
"Muscle Stiffness", "Alopecia", "Obesity"
)
dist_prediktor <- lapply(seq_along(var_kategorik), function(i) {
v <- var_kategorik[i]
lbl <- var_label[i]
n1 <- sum(diabetes[[v]] == 1)
n0 <- sum(diabetes[[v]] == 0)
data.frame(
Variabel = lbl,
`Kode 0 (n)` = n0,
`% Kode 0` = round(100 * n0 / nrow(diabetes), 1),
`Kode 1 (n)` = n1,
`% Kode 1` = round(100 * n1 / nrow(diabetes), 1),
check.names = FALSE
)
}) %>% bind_rows()
knitr::kable(dist_prediktor,
caption = "Distribusi Variabel Prediktor Kategorik (0 = Tidak/Perempuan, 1 = Ya/Laki-laki)")| Variabel | Kode 0 (n) | % Kode 0 | Kode 1 (n) | % Kode 1 |
|---|---|---|---|---|
| Gender | 192 | 36.9 | 328 | 63.1 |
| Polyuria | 262 | 50.4 | 258 | 49.6 |
| Polydipsia | 287 | 55.2 | 233 | 44.8 |
| Sudden Weight Loss | 303 | 58.3 | 217 | 41.7 |
| Weakness | 215 | 41.3 | 305 | 58.7 |
| Polyphagia | 283 | 54.4 | 237 | 45.6 |
| Genital Thrush | 404 | 77.7 | 116 | 22.3 |
| Visual Blurring | 287 | 55.2 | 233 | 44.8 |
| Itching | 267 | 51.3 | 253 | 48.7 |
| Irritability | 394 | 75.8 | 126 | 24.2 |
| Delayed Healing | 281 | 54.0 | 239 | 46.0 |
| Partial Paresis | 296 | 56.9 | 224 | 43.1 |
| Muscle Stiffness | 325 | 62.5 | 195 | 37.5 |
| Alopecia | 341 | 65.6 | 179 | 34.4 |
| Obesity | 432 | 83.1 | 88 | 16.9 |
Interpretasi: Dua gejala paling menonjol secara deskriptif adalah Polyuria (sering buang air kecil berlebihan) dan Polydipsia (rasa haus berlebihan), yang keduanya merupakan tanda klasik diabetes melitus akibat tubuh berusaha mengeluarkan kelebihan glukosa melalui urin sehingga menyebabkan dehidrasi dan haus berlebihan.
for (i in seq_along(var_kategorik)) {
v <- var_kategorik[i]
lbl <- var_label[i]
tbl <- table(Kode = diabetes[[v]], Status = diabetes$status_diabetes)
tbl_prop <- round(prop.table(tbl, margin = 2) * 100, 1)
tbl_df <- as.data.frame.matrix(tbl)
tbl_df$Kode <- rownames(tbl_df)
tbl_df$`% Negatif` <- tbl_prop[, "Negatif"]
tbl_df$`% Positif` <- tbl_prop[, "Positif"]
tbl_df <- tbl_df[, c("Kode", "Negatif", "Positif", "% Negatif", "% Positif")]
cat(sprintf("\n\n#### %s (0 = referensi, 1 = kategori aktif)\n\n", lbl))
print(knitr::kable(tbl_df,
format = "markdown",
col.names = c("Kode", "Negatif", "Positif", "% Negatif", "% Positif"),
caption = paste("Distribusi", lbl, "vs Status Diabetes")))
cat("\n\n")
}| Kode | Negatif | Positif | % Negatif | % Positif | |
|---|---|---|---|---|---|
| 0 | 0 | 19 | 173 | 9.5 | 54.1 |
| 1 | 1 | 181 | 147 | 90.5 | 45.9 |
| Kode | Negatif | Positif | % Negatif | % Positif | |
|---|---|---|---|---|---|
| 0 | 0 | 185 | 77 | 92.5 | 24.1 |
| 1 | 1 | 15 | 243 | 7.5 | 75.9 |
| Kode | Negatif | Positif | % Negatif | % Positif | |
|---|---|---|---|---|---|
| 0 | 0 | 192 | 95 | 96 | 29.7 |
| 1 | 1 | 8 | 225 | 4 | 70.3 |
| Kode | Negatif | Positif | % Negatif | % Positif | |
|---|---|---|---|---|---|
| 0 | 0 | 171 | 132 | 85.5 | 41.2 |
| 1 | 1 | 29 | 188 | 14.5 | 58.8 |
| Kode | Negatif | Positif | % Negatif | % Positif | |
|---|---|---|---|---|---|
| 0 | 0 | 113 | 102 | 56.5 | 31.9 |
| 1 | 1 | 87 | 218 | 43.5 | 68.1 |
| Kode | Negatif | Positif | % Negatif | % Positif | |
|---|---|---|---|---|---|
| 0 | 0 | 152 | 131 | 76 | 40.9 |
| 1 | 1 | 48 | 189 | 24 | 59.1 |
| Kode | Negatif | Positif | % Negatif | % Positif | |
|---|---|---|---|---|---|
| 0 | 0 | 167 | 237 | 83.5 | 74.1 |
| 1 | 1 | 33 | 83 | 16.5 | 25.9 |
| Kode | Negatif | Positif | % Negatif | % Positif | |
|---|---|---|---|---|---|
| 0 | 0 | 142 | 145 | 71 | 45.3 |
| 1 | 1 | 58 | 175 | 29 | 54.7 |
| Kode | Negatif | Positif | % Negatif | % Positif | |
|---|---|---|---|---|---|
| 0 | 0 | 101 | 166 | 50.5 | 51.9 |
| 1 | 1 | 99 | 154 | 49.5 | 48.1 |
| Kode | Negatif | Positif | % Negatif | % Positif | |
|---|---|---|---|---|---|
| 0 | 0 | 184 | 210 | 92 | 65.6 |
| 1 | 1 | 16 | 110 | 8 | 34.4 |
| Kode | Negatif | Positif | % Negatif | % Positif | |
|---|---|---|---|---|---|
| 0 | 0 | 114 | 167 | 57 | 52.2 |
| 1 | 1 | 86 | 153 | 43 | 47.8 |
| Kode | Negatif | Positif | % Negatif | % Positif | |
|---|---|---|---|---|---|
| 0 | 0 | 168 | 128 | 84 | 40 |
| 1 | 1 | 32 | 192 | 16 | 60 |
| Kode | Negatif | Positif | % Negatif | % Positif | |
|---|---|---|---|---|---|
| 0 | 0 | 140 | 185 | 70 | 57.8 |
| 1 | 1 | 60 | 135 | 30 | 42.2 |
| Kode | Negatif | Positif | % Negatif | % Positif | |
|---|---|---|---|---|---|
| 0 | 0 | 99 | 242 | 49.5 | 75.6 |
| 1 | 1 | 101 | 78 | 50.5 | 24.4 |
| Kode | Negatif | Positif | % Negatif | % Positif | |
|---|---|---|---|---|---|
| 0 | 0 | 173 | 259 | 86.5 | 80.9 |
| 1 | 1 | 27 | 61 | 13.5 | 19.1 |
Pada kelompok Negatif, mayoritas berjenis kelamin laki-laki (kode 1) sebesar 90,5%, sedangkan pada kelompok Positif, perempuan (kode 0) mendominasi dengan 54,1%. Hal ini mengindikasikan bahwa perempuan memiliki proporsi kasus positif diabetes yang lebih tinggi dalam dataset ini.
Sebesar 75,9% pasien positif diabetes mengalami polyuria, dibandingkan hanya 7,5% pada kelompok negatif. Polyuria merupakan salah satu gejala klasik diabetes yang paling membedakan antara kedua kelompok.
Sebesar 70,3% pasien positif mengalami polydipsia, sementara hanya 4% pada kelompok negatif. Bersama polyuria, polydipsia menjadi gejala dengan perbedaan distribusi paling mencolok antar kelompok.
Pada kelompok positif, 58,8% mengalami penurunan berat badan mendadak dibandingkan hanya 14,5% pada kelompok negatif. Gejala ini cukup diskriminatif terhadap status diabetes.
Sebesar 68,1% pasien positif melaporkan kelemahan fisik, berbanding 43,5% pada kelompok negatif. Meskipun ada perbedaan, gejala ini juga cukup umum ditemukan pada kelompok negatif.
Sebesar 59,1% pasien positif mengalami polyphagia dibandingkan 24% pada kelompok negatif. Gejala ini cukup membedakan kedua kelompok meskipun tidak setajam polyuria dan polydipsia.
Distribusinya relatif mirip antar kelompok: 25,9% pada positif vs 16,5% pada negatif. Gejala ini memiliki daya pembeda yang relatif lemah dibanding gejala lainnya.
Sebesar 54,7% pasien positif mengalami penglihatan kabur dibandingkan 29% pada kelompok negatif. Gejala ini cukup relevan namun tidak sangat spesifik.
Distribusi hampir merata di kedua kelompok: 49,5% pada negatif dan 48,1% pada positif. Gejala ini tidak diskriminatif dan kemungkinan bukan prediktor yang kuat.
Proporsi yang mengalami iritabilitas pada kelompok positif (34,4%) jauh lebih tinggi dibandingkan negatif (8%), menunjukkan daya pembeda yang cukup baik.
Distribusinya hampir seimbang antara kedua kelompok (positif: 47,8%, negatif: 43%). Gejala ini memiliki daya pembeda yang rendah.
Sebesar 60% pasien positif mengalami partial paresis dibandingkan hanya 16% pada kelompok negatif. Gejala ini cukup diskriminatif terhadap status diabetes.
Proporsi yang mengalami kekakuan otot: 42,2% pada positif dan 30% pada negatif. Daya pembedanya tergolong sedang.
Alopecia lebih banyak ditemukan pada kelompok Negatif (50,5%) dibandingkan Positif (24,4%), mengindikasikan bahwa alopecia berkorelasi negatif dengan status diabetes dalam dataset ini.
Proporsi obesitas tidak jauh berbeda: 19,1% pada positif vs 13,5% pada negatif. Gejala ini memiliki daya pembeda yang paling lemah di antara semua variabel.
Materi: Uji Chi-Square (\(\chi^2\)) digunakan sebagai langkah skrining awal untuk mengidentifikasi asosiasi bivariat antara masing-masing prediktor kategorik dengan variabel respon sebelum analisis multivariat. Uji ini menguji independensi antara dua variabel kategorik.
Hipotesis:
Kriteria: Variabel dinyatakan signifikan apabila \(p < 0{,}05\).
Referensi: Hosmer & Lemeshow (2013), hal. 89–90.
Selain nilai \(p\), dihitung pula Odds Ratio (OR) bivariat dan Confidence Interval 95% untuk mengukur kekuatan dan arah asosiasi.
hasil_chisq <- lapply(seq_along(var_kategorik), function(i) {
v <- var_kategorik[i]
lbl <- var_label[i]
tbl <- table(diabetes[[v]], diabetes$diabetes_positif)
test <- chisq.test(tbl, correct = FALSE)
or_result <- tryCatch({
a <- tbl["1", "1"]; b <- tbl["1", "0"]
cc <- tbl["0", "1"]; d <- tbl["0", "0"]
if (any(c(a, b, cc, d) == 0)) {
a <- a + 0.5; b <- b + 0.5; cc <- cc + 0.5; d <- d + 0.5
}
or_val <- (a * d) / (b * cc)
log_or <- log(or_val)
se_log <- sqrt(1/a + 1/b + 1/cc + 1/d)
ci_low <- exp(log_or - 1.96 * se_log)
ci_high <- exp(log_or + 1.96 * se_log)
c(or_val, ci_low, ci_high)
}, error = function(e) c(NA, NA, NA))
data.frame(
Variabel = lbl,
Chi_square = round(test$statistic, 4),
df = test$parameter,
p_value = round(test$p.value, 4),
Signifikan = ifelse(test$p.value < 0.001, "*** (p<0.001)",
ifelse(test$p.value < 0.01, "** (p<0.01)",
ifelse(test$p.value < 0.05, "* (p<0.05)", "Tidak"))),
OR = round(or_result[1], 4),
CI_95_Low = round(or_result[2], 4),
CI_95_High = round(or_result[3], 4)
)
}) %>% bind_rows()
knitr::kable(hasil_chisq,
caption = "Hasil Uji Chi-Square dan Odds Ratio Bivariat")| Variabel | Chi_square | df | p_value | Signifikan | OR | CI_95_Low | CI_95_High | |
|---|---|---|---|---|---|---|---|---|
| X-squared…1 | Gender | 104.9415 | 1 | 0.0000 | *** (p<0.001) | 0.0892 | 0.0530 | 0.1502 |
| X-squared…2 | Polyuria | 230.5954 | 1 | 0.0000 | *** (p<0.001) | 38.9221 | 21.6773 | 69.8855 |
| X-squared…3 | Polydipsia | 218.8448 | 1 | 0.0000 | *** (p<0.001) | 56.8421 | 26.9362 | 119.9511 |
| X-squared…4 | Sudden Weight Loss | 99.1077 | 1 | 0.0000 | *** (p<0.001) | 8.3981 | 5.3432 | 13.1997 |
| X-squared…5 | Weakness | 30.7750 | 1 | 0.0000 | *** (p<0.001) | 2.7760 | 1.9265 | 4.0000 |
| X-squared…6 | Polyphagia | 61.0006 | 1 | 0.0000 | *** (p<0.001) | 4.5687 | 3.0820 | 6.7725 |
| X-squared…7 | Genital Thrush | 6.3250 | 1 | 0.0119 | * (p<0.05) | 1.7723 | 1.1308 | 2.7777 |
| X-squared…8 | Visual Blurring | 32.8389 | 1 | 0.0000 | *** (p<0.001) | 2.9548 | 2.0278 | 4.3056 |
| X-squared…9 | Itching | 0.0931 | 1 | 0.7602 | Tidak | 0.9465 | 0.6647 | 1.3477 |
| X-squared…10 | Irritability | 46.6339 | 1 | 0.0000 | *** (p<0.001) | 6.0238 | 3.4390 | 10.5513 |
| X-squared…11 | Delayed Healing | 1.1477 | 1 | 0.2840 | Tidak | 1.2145 | 0.8510 | 1.7331 |
| X-squared…12 | Partial Paresis | 97.1737 | 1 | 0.0000 | *** (p<0.001) | 7.8750 | 5.0756 | 12.2184 |
| X-squared…13 | Muscle Stiffness | 7.8000 | 1 | 0.0052 | ** (p<0.01) | 1.7027 | 1.1702 | 2.4776 |
| X-squared…14 | Alopecia | 37.2125 | 1 | 0.0000 | *** (p<0.001) | 0.3159 | 0.2167 | 0.4605 |
| X-squared…15 | Obesity | 2.7087 | 1 | 0.0998 | Tidak | 1.5091 | 0.9224 | 2.4688 |
sig_vars <- hasil_chisq %>% dplyr::filter(p_value < 0.05) %>% dplyr::pull(Variabel)
nonsig_vars <- hasil_chisq %>% dplyr::filter(p_value >= 0.05) %>% dplyr::pull(Variabel)Interpretasi: Dari 15 variabel kategorik yang diuji, 12 variabel menunjukkan asosiasi yang signifikan secara statistik (\(p < 0{,}05\)): Gender, Polyuria, Polydipsia, Sudden Weight Loss, Weakness, Polyphagia, Genital Thrush, Visual Blurring, Irritability, Partial Paresis, Muscle Stiffness, Alopecia. Sementara 3 variabel tidak signifikan: Itching, Delayed Healing, Obesity.
Ketiga variabel yang tidak signifikan secara bivariat tidak serta-merta dikeluarkan dari pertimbangan, mengingat uji Chi-Square hanya berfungsi sebagai skrining awal dan belum mencerminkan hubungan dalam konteks multivariat.
Materi: Sebelum memeriksa asumsi, dibangun terlebih dahulu model regresi logistik penuh yang memuat seluruh 16 prediktor. Model penuh ini digunakan sebagai dasar pemeriksaan asumsi linearitas logit (Box-Tidwell) dan multikolinearitas (VIF). Estimasi menggunakan Maximum Likelihood Estimation (MLE) via algoritma IRLS (Iteratively Reweighted Least Squares).
model_penuh <- glm(
diabetes_positif ~ age + gender + polyuria + polydipsia +
sudden_wt_loss + weakness + polyphagia + genital_thrush +
visual_blurring + itching + irritability + delayed_healing +
partial_paresis + muscle_stiffness + alopecia + obesity,
data = data_train,
family = binomial(link = "logit")
)Materi: Regresi logistik mengasumsikan hubungan linear antara prediktor kontinu dengan log-odds (logit) dari variabel respon. Uji Box-Tidwell (1962) mendeteksi non-linearitas dengan menambahkan interaksi \(X \cdot \ln(X)\) ke dalam model.
\[\text{logit}(P) = \beta_0 + \beta_1 X + \beta_2 \cdot [X \cdot \ln(X)]\]
Hipotesis:
age dan logit bersifat linearKeputusan: Tolak \(H_0\) jika \(p < 0{,}05\)
Referensi: Box & Tidwell (1962), Technometrics, 4(4), 531–550; Hosmer & Lemeshow (2013), hal. 95–96.
data_train_bt <- data_train %>%
mutate(age_log_age = age * log(age))
model_bt <- glm(
diabetes_positif ~ age + age_log_age +
gender + polyuria + polydipsia + sudden_wt_loss + weakness +
polyphagia + genital_thrush + visual_blurring + itching +
irritability + delayed_healing + partial_paresis +
muscle_stiffness + alopecia + obesity,
data = data_train_bt,
family = binomial(link = "logit")
)
bt_coef <- summary(model_bt)$coefficients
pval_bt <- bt_coef["age_log_age", "Pr(>|z|)"]
beta_bt <- bt_coef["age_log_age", "Estimate"]
se_bt <- bt_coef["age_log_age", "Std. Error"]
z_bt <- bt_coef["age_log_age", "z value"]
bt_result <- data.frame(
Koefisien = "age × ln(age)",
Estimasi = round(beta_bt, 4),
SE = round(se_bt, 4),
`z-value` = round(z_bt, 4),
`p-value` = round(pval_bt, 4),
Keputusan = ifelse(pval_bt > 0.05,
"Gagal tolak H0 — Asumsi TERPENUHI",
"Tolak H0 — Asumsi TIDAK terpenuhi"),
check.names = FALSE
)
knitr::kable(bt_result, caption = "Hasil Uji Box-Tidwell untuk Variabel age")| Koefisien | Estimasi | SE | z-value | p-value | Keputusan |
|---|---|---|---|---|---|
| age × ln(age) | -0.1344 | 0.1293 | -1.0398 | 0.2984 | Gagal tolak H0 — Asumsi TERPENUHI |
Interpretasi: Nilai \(p\)-value untuk koefisien interaksi
age × ln(age) = 0.2984 > 0,05, sehingga \(H_0\) gagal ditolak. Tidak
terdapat bukti hubungan non-linear antara variabel age dan
log-odds kejadian diabetes. Dengan demikian, asumsi linearitas
dalam logit terpenuhi dan variabel age dapat
digunakan dalam bentuk aslinya tanpa transformasi lebih lanjut.
Materi: Multikolinearitas terjadi ketika prediktor saling berkorelasi tinggi, sehingga estimasi koefisien menjadi tidak stabil dan standar error membesar. Variance Inflation Factor (VIF) mengukur seberapa besar varians koefisien meningkat akibat kolinearitas:
\[\text{VIF}_j = \frac{1}{1 - R^2_j}\]
di mana \(R^2_j\) adalah koefisien determinasi dari regresi variabel \(X_j\) terhadap prediktor lainnya.
Threshold:
Referensi: Fox & Weisberg (2019), An R Companion to Applied Regression, 3rd ed.
vif_raw <- car::vif(model_penuh)
if (is.matrix(vif_raw)) {
vif_df <- data.frame(
Variabel = rownames(vif_raw),
GVIF = round(vif_raw[, 1], 4),
Df = vif_raw[, 2],
`GVIF^(1/(2*Df))` = round(vif_raw[, 3], 4),
Keterangan = ifelse(vif_raw[, 3] > 2.236, "Masalah!", "OK"),
check.names = FALSE
)
ada_masalah <- any(vif_raw[, 3] > 2.236)
} else {
vif_df <- data.frame(
Variabel = names(vif_raw),
VIF = round(vif_raw, 4),
Keterangan = ifelse(vif_raw > 5, "Masalah (VIF >= 5)!", "OK (VIF < 5)")
)
ada_masalah <- any(vif_raw > 5)
}
knitr::kable(vif_df, caption = "Hasil Uji Multikolinearitas (VIF) — Model Penuh")| Variabel | VIF | Keterangan | |
|---|---|---|---|
| age | age | 2.2125 | OK (VIF < 5) |
| gender | gender | 1.9705 | OK (VIF < 5) |
| polyuria | polyuria | 2.6225 | OK (VIF < 5) |
| polydipsia | polydipsia | 1.7235 | OK (VIF < 5) |
| sudden_wt_loss | sudden_wt_loss | 1.6217 | OK (VIF < 5) |
| weakness | weakness | 1.7699 | OK (VIF < 5) |
| polyphagia | polyphagia | 1.5921 | OK (VIF < 5) |
| genital_thrush | genital_thrush | 1.7476 | OK (VIF < 5) |
| visual_blurring | visual_blurring | 2.7065 | OK (VIF < 5) |
| itching | itching | 3.1867 | OK (VIF < 5) |
| irritability | irritability | 1.3773 | OK (VIF < 5) |
| delayed_healing | delayed_healing | 2.1922 | OK (VIF < 5) |
| partial_paresis | partial_paresis | 1.3957 | OK (VIF < 5) |
| muscle_stiffness | muscle_stiffness | 1.6886 | OK (VIF < 5) |
| alopecia | alopecia | 2.4811 | OK (VIF < 5) |
| obesity | obesity | 1.2828 | OK (VIF < 5) |
Interpretasi: Seluruh variabel prediktor memiliki nilai VIF < 5, yang berarti tidak terdapat multikolinearitas yang mengkhawatirkan antar variabel prediktor dalam model. Estimasi koefisien regresi dinilai stabil dan dapat dipercaya. Seluruh prediktor dapat dipertahankan dalam model penuh.
Materi — Model Regresi Logistik Biner:
Model regresi logistik biner memodelkan probabilitas kejadian suatu peristiwa (dalam hal ini: diabetes positif) sebagai fungsi dari satu atau lebih prediktor:
\[\text{logit}(P) = \ln\left(\frac{P}{1-P}\right) = \beta_0 + \beta_1 X_1 + \cdots + \beta_p X_p\]
Transformasi ke probabilitas: \[P(\text{diabetes positif}) = \frac{1}{1 + e^{-\text{logit}(P)}}\]
Parameter diestimasi menggunakan Maximum Likelihood Estimation (MLE), yang memaksimalkan fungsi log-likelihood: \[\ell(\boldsymbol{\beta}) = \sum_{i=1}^{n} \left[ y_i \ln(\hat{p}_i) + (1 - y_i) \ln(1 - \hat{p}_i) \right]\]
Referensi: Hosmer & Lemeshow (2013); Agresti (2018).
Materi: Model penuh dibangun dengan memasukkan seluruh 16 variabel prediktor secara bersamaan. Pendekatan confirmatory ini bertujuan menguji seluruh variabel yang secara teoritis dan klinis relevan terhadap kejadian diabetes, sekaligus sebagai titik awal dalam seleksi variabel stepwise.
knitr::kable(
data.frame(
Metrik = c("AIC Model Penuh", "Deviasi Residual", "Deviasi Null"),
Nilai = c(round(AIC(model_penuh), 4),
round(deviance(model_penuh), 4),
round(model_penuh$null.deviance, 4)),
df = c(NA, model_penuh$df.residual, model_penuh$df.null)
),
caption = "Ringkasan Statistik Model Penuh"
)| Metrik | Nilai | df |
|---|---|---|
| AIC Model Penuh | 164.8563 | NA |
| Deviasi Residual | 130.8563 | 399 |
| Deviasi Null | 557.0725 | 415 |
hasil_model_penuh <- broom::tidy(model_penuh) %>%
mutate(
OR = exp(estimate),
CI_low = exp(estimate - 1.96 * std.error),
CI_high = exp(estimate + 1.96 * std.error),
Sig = ifelse(p.value < 0.001, "***",
ifelse(p.value < 0.01, "**",
ifelse(p.value < 0.05, "*", "ns")))
) %>%
transmute(
Variabel = term,
Beta = round(estimate, 4),
SE = round(std.error, 4),
`p-value` = round(p.value, 4),
Sig,
`Odds Ratio` = round(OR, 4),
`CI 95% Bawah` = round(CI_low, 4),
`CI 95% Atas` = round(CI_high, 4)
)
knitr::kable(hasil_model_penuh,
caption = "Koefisien dan Odds Ratio Model Penuh (Data Train)")| Variabel | Beta | SE | p-value | Sig | Odds Ratio | CI 95% Bawah | CI 95% Atas |
|---|---|---|---|---|---|---|---|
| (Intercept) | 2.4500 | 1.2311 | 0.0466 | * | 11.5883 | 1.0377 | 129.4061 |
| age | -0.0493 | 0.0294 | 0.0932 | ns | 0.9519 | 0.8987 | 1.0083 |
| gender | -4.2611 | 0.6861 | 0.0000 | *** | 0.0141 | 0.0037 | 0.0541 |
| polyuria | 4.7658 | 0.8582 | 0.0000 | *** | 117.4296 | 21.8401 | 631.3947 |
| polydipsia | 4.4574 | 0.9976 | 0.0000 | *** | 86.2661 | 12.2086 | 609.5581 |
| sudden_wt_loss | 0.2900 | 0.6403 | 0.6507 | ns | 1.3364 | 0.3809 | 4.6882 |
| weakness | 0.8816 | 0.6096 | 0.1481 | ns | 2.4148 | 0.7311 | 7.9757 |
| polyphagia | 1.5003 | 0.6215 | 0.0158 | * | 4.4829 | 1.3260 | 15.1562 |
| genital_thrush | 2.1896 | 0.6465 | 0.0007 | *** | 8.9317 | 2.5156 | 31.7129 |
| visual_blurring | 1.4000 | 0.7855 | 0.0747 | ns | 4.0551 | 0.8698 | 18.9065 |
| itching | -2.8503 | 0.8101 | 0.0004 | *** | 0.0578 | 0.0118 | 0.2830 |
| irritability | 2.5133 | 0.6779 | 0.0002 | *** | 12.3451 | 3.2693 | 46.6165 |
| delayed_healing | -0.6967 | 0.6915 | 0.3137 | ns | 0.4982 | 0.1285 | 1.9321 |
| partial_paresis | 0.8044 | 0.5925 | 0.1746 | ns | 2.2353 | 0.6997 | 7.1403 |
| muscle_stiffness | -0.4132 | 0.6532 | 0.5270 | ns | 0.6615 | 0.1839 | 2.3799 |
| alopecia | -0.0849 | 0.7346 | 0.9080 | ns | 0.9186 | 0.2177 | 3.8767 |
| obesity | -0.4588 | 0.6147 | 0.4554 | ns | 0.6320 | 0.1895 | 2.1084 |
Interpretasi: Dari 16 variabel prediktor dalam model
penuh, terdapat tujuh variabel yang signifikan (\(p < 0{,}05\)): gender,
polyuria, polydipsia, polyphagia,
genital_thrush, itching, dan
irritability. Sembilan variabel lainnya tidak signifikan
secara individual, namun akan dievaluasi kembali dalam proses seleksi
stepwise.
Materi: Pemilihan variabel menggunakan prosedur stepwise bidireksional berbasis Akaike Information Criterion (AIC). AIC menyeimbangkan goodness-of-fit dengan kompleksitas model melalui penalti jumlah parameter:
\[\text{AIC} = -2\ell(\hat{\boldsymbol{\beta}}) + 2k\]
di mana \(\ell(\hat{\boldsymbol{\beta}})\) adalah log-likelihood dan \(k\) adalah jumlah parameter. Model dengan AIC lebih kecil lebih disukai.
Prosedur both-direction menggabungkan:
Referensi: Akaike (1974), IEEE Trans. Automatic Control, 19(6), 716–723; Venables & Ripley (2002).
## Start: AIC=164.86
## diabetes_positif ~ age + gender + polyuria + polydipsia + sudden_wt_loss +
## weakness + polyphagia + genital_thrush + visual_blurring +
## itching + irritability + delayed_healing + partial_paresis +
## muscle_stiffness + alopecia + obesity
##
## Df Deviance AIC
## - alopecia 1 130.87 162.87
## - sudden_wt_loss 1 131.06 163.06
## - muscle_stiffness 1 131.25 163.25
## - obesity 1 131.42 163.42
## - delayed_healing 1 131.88 163.88
## - partial_paresis 1 132.71 164.71
## <none> 130.86 164.86
## - weakness 1 132.98 164.98
## - age 1 133.81 165.81
## - visual_blurring 1 134.28 166.28
## - polyphagia 1 137.02 169.02
## - genital_thrush 1 143.95 175.95
## - itching 1 146.28 178.28
## - irritability 1 146.88 178.88
## - polydipsia 1 167.25 199.25
## - polyuria 1 186.43 218.43
## - gender 1 188.84 220.84
##
## Step: AIC=162.87
## diabetes_positif ~ age + gender + polyuria + polydipsia + sudden_wt_loss +
## weakness + polyphagia + genital_thrush + visual_blurring +
## itching + irritability + delayed_healing + partial_paresis +
## muscle_stiffness + obesity
##
## Df Deviance AIC
## - sudden_wt_loss 1 131.09 161.09
## - muscle_stiffness 1 131.25 161.25
## - obesity 1 131.42 161.42
## - delayed_healing 1 132.11 162.11
## - partial_paresis 1 132.71 162.71
## <none> 130.87 162.87
## - weakness 1 133.00 163.00
## - visual_blurring 1 134.33 164.33
## - age 1 134.67 164.67
## + alopecia 1 130.86 164.86
## - polyphagia 1 137.11 167.11
## - genital_thrush 1 143.95 173.95
## - itching 1 146.84 176.84
## - irritability 1 147.25 177.25
## - polydipsia 1 167.46 197.46
## - polyuria 1 187.62 217.62
## - gender 1 191.32 221.32
##
## Step: AIC=161.09
## diabetes_positif ~ age + gender + polyuria + polydipsia + weakness +
## polyphagia + genital_thrush + visual_blurring + itching +
## irritability + delayed_healing + partial_paresis + muscle_stiffness +
## obesity
##
## Df Deviance AIC
## - obesity 1 131.60 159.60
## - muscle_stiffness 1 131.65 159.65
## - delayed_healing 1 132.29 160.29
## <none> 131.09 161.09
## - partial_paresis 1 133.10 161.10
## - visual_blurring 1 134.55 162.55
## + sudden_wt_loss 1 130.87 162.87
## - weakness 1 134.88 162.88
## - age 1 134.94 162.94
## + alopecia 1 131.06 163.06
## - polyphagia 1 137.62 165.62
## - genital_thrush 1 144.71 172.71
## - irritability 1 147.31 175.31
## - itching 1 148.60 176.60
## - polydipsia 1 173.94 201.94
## - gender 1 197.37 225.37
## - polyuria 1 200.47 228.47
##
## Step: AIC=159.6
## diabetes_positif ~ age + gender + polyuria + polydipsia + weakness +
## polyphagia + genital_thrush + visual_blurring + itching +
## irritability + delayed_healing + partial_paresis + muscle_stiffness
##
## Df Deviance AIC
## - muscle_stiffness 1 132.24 158.24
## - delayed_healing 1 132.80 158.80
## - partial_paresis 1 133.57 159.57
## <none> 131.60 159.60
## + obesity 1 131.09 161.09
## + sudden_wt_loss 1 131.42 161.42
## - weakness 1 135.45 161.45
## - visual_blurring 1 135.46 161.46
## - age 1 135.49 161.49
## + alopecia 1 131.58 161.58
## - polyphagia 1 139.40 165.40
## - genital_thrush 1 145.00 171.00
## - irritability 1 147.33 173.33
## - itching 1 150.72 176.72
## - polydipsia 1 175.13 201.13
## - gender 1 198.59 224.59
## - polyuria 1 200.61 226.61
##
## Step: AIC=158.24
## diabetes_positif ~ age + gender + polyuria + polydipsia + weakness +
## polyphagia + genital_thrush + visual_blurring + itching +
## irritability + delayed_healing + partial_paresis
##
## Df Deviance AIC
## - delayed_healing 1 133.37 157.37
## <none> 132.24 158.24
## - partial_paresis 1 134.56 158.56
## - visual_blurring 1 135.56 159.56
## + muscle_stiffness 1 131.60 159.60
## + obesity 1 131.65 159.65
## - weakness 1 135.81 159.81
## + sudden_wt_loss 1 131.88 159.88
## + alopecia 1 132.24 160.24
## - age 1 136.71 160.71
## - polyphagia 1 139.41 163.41
## - genital_thrush 1 146.61 170.61
## - irritability 1 147.92 171.92
## - itching 1 151.09 175.09
## - polydipsia 1 175.68 199.68
## - gender 1 198.93 222.93
## - polyuria 1 200.62 224.62
##
## Step: AIC=157.37
## diabetes_positif ~ age + gender + polyuria + polydipsia + weakness +
## polyphagia + genital_thrush + visual_blurring + itching +
## irritability + partial_paresis
##
## Df Deviance AIC
## <none> 133.37 157.37
## - partial_paresis 1 135.86 157.86
## - visual_blurring 1 136.17 158.17
## - weakness 1 136.20 158.20
## + delayed_healing 1 132.24 158.24
## + obesity 1 132.79 158.79
## + muscle_stiffness 1 132.80 158.80
## + sudden_wt_loss 1 133.05 159.05
## + alopecia 1 133.26 159.26
## - age 1 139.05 161.05
## - polyphagia 1 142.82 164.82
## - genital_thrush 1 146.83 168.83
## - irritability 1 150.32 172.32
## - itching 1 158.37 180.37
## - polydipsia 1 188.98 210.98
## - polyuria 1 202.33 224.33
## - gender 1 203.38 225.38
##
## Call:
## glm(formula = diabetes_positif ~ age + gender + polyuria + polydipsia +
## weakness + polyphagia + genital_thrush + visual_blurring +
## itching + irritability + partial_paresis, family = binomial(link = "logit"),
## data = data_train)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) 2.72645 1.09442 2.491 0.012730 *
## age -0.05761 0.02493 -2.311 0.020817 *
## gender -4.45055 0.68810 -6.468 9.94e-11 ***
## polyuria 4.70118 0.74793 6.286 3.27e-10 ***
## polydipsia 4.96158 0.90873 5.460 4.76e-08 ***
## weakness 0.83647 0.50288 1.663 0.096240 .
## polyphagia 1.65795 0.56618 2.928 0.003408 **
## genital_thrush 2.15717 0.62440 3.455 0.000551 ***
## visual_blurring 1.19318 0.74389 1.604 0.108720
## itching -3.22018 0.74776 -4.306 1.66e-05 ***
## irritability 2.42881 0.62281 3.900 9.63e-05 ***
## partial_paresis 0.89698 0.57265 1.566 0.117259
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 557.07 on 415 degrees of freedom
## Residual deviance: 133.37 on 404 degrees of freedom
## AIC: 157.37
##
## Number of Fisher Scoring iterations: 8
var_final <- names(coef(model_stepwise))[-1]
var_semua <- names(coef(model_penuh))[-1]
var_dihapus <- setdiff(var_semua, var_final)
perbandingan_aic <- data.frame(
Model = c("Model Penuh (16 prediktor)", "Model Final (Stepwise)"),
`Jumlah Prediktor` = c(16, length(var_final)),
AIC = round(c(AIC(model_penuh), AIC(model_stepwise)), 4),
`Delta AIC` = round(c(AIC(model_penuh) - AIC(model_stepwise), 0), 4),
Keterangan = c("Model lebih kompleks", "Model lebih parsimonious (dipilih)"),
check.names = FALSE
)
knitr::kable(perbandingan_aic, caption = "Perbandingan AIC Model Penuh vs Model Final")| Model | Jumlah Prediktor | AIC | Delta AIC | Keterangan |
|---|---|---|---|---|
| Model Penuh (16 prediktor) | 16 | 164.8563 | 7.4852 | Model lebih kompleks |
| Model Final (Stepwise) | 11 | 157.3711 | 0.0000 | Model lebih parsimonious (dipilih) |
Interpretasi: Proses seleksi stepwise mengeliminasi 5 variabel yaitu: sudden_wt_loss, delayed_healing, muscle_stiffness, alopecia, obesity. Model final memuat 11 variabel prediktor dengan AIC = 157.3711, lebih kecil 7.49 poin dari model penuh (AIC = 164.8563). Penurunan AIC mengindikasikan model stepwise memberikan keseimbangan lebih baik antara ketepatan prediksi dan kesederhanaan model.
hasil_final <- broom::tidy(model_stepwise) %>%
mutate(
OR = exp(estimate),
CI_low = exp(estimate - 1.96 * std.error),
CI_high = exp(estimate + 1.96 * std.error),
Sig = ifelse(p.value < 0.001, "***",
ifelse(p.value < 0.01, "**",
ifelse(p.value < 0.05, "*", "ns")))
) %>%
transmute(
Variabel = term,
Beta = round(estimate, 4),
SE = round(std.error, 4),
`p-value` = round(p.value, 4),
Sig,
`Odds Ratio` = round(OR, 4),
`CI 95% Bawah` = round(CI_low, 4),
`CI 95% Atas` = round(CI_high, 4)
)
knitr::kable(hasil_final,
caption = "Koefisien dan Odds Ratio Model Final (Stepwise)")| Variabel | Beta | SE | p-value | Sig | Odds Ratio | CI 95% Bawah | CI 95% Atas |
|---|---|---|---|---|---|---|---|
| (Intercept) | 2.7265 | 1.0944 | 0.0127 | * | 15.2786 | 1.7885 | 130.5180 |
| age | -0.0576 | 0.0249 | 0.0208 | * | 0.9440 | 0.8990 | 0.9913 |
| gender | -4.4506 | 0.6881 | 0.0000 | *** | 0.0117 | 0.0030 | 0.0450 |
| polyuria | 4.7012 | 0.7479 | 0.0000 | *** | 110.0765 | 25.4123 | 476.8107 |
| polydipsia | 4.9616 | 0.9087 | 0.0000 | *** | 142.8197 | 24.0580 | 847.8445 |
| weakness | 0.8365 | 0.5029 | 0.0962 | ns | 2.3082 | 0.8614 | 6.1849 |
| polyphagia | 1.6580 | 0.5662 | 0.0034 | ** | 5.2486 | 1.7302 | 15.9214 |
| genital_thrush | 2.1572 | 0.6244 | 0.0006 | *** | 8.6466 | 2.5430 | 29.4000 |
| visual_blurring | 1.1932 | 0.7439 | 0.1087 | ns | 3.2976 | 0.7673 | 14.1712 |
| itching | -3.2202 | 0.7478 | 0.0000 | *** | 0.0399 | 0.0092 | 0.1730 |
| irritability | 2.4288 | 0.6228 | 0.0001 | *** | 11.3453 | 3.3471 | 38.4563 |
| partial_paresis | 0.8970 | 0.5726 | 0.1173 | ns | 2.4522 | 0.7982 | 7.5336 |
Materi: Persamaan model logistik final dapat ditulis dalam bentuk logit (log-odds) dan kemudian ditransformasi ke probabilitas menggunakan fungsi sigmoid (inverse logit).
koef <- coef(model_stepwise)
label_map <- c(
"(Intercept)" = "Intercept",
"age" = "Usia",
"gender" = "Jenis Kelamin (Laki-laki vs Perempuan)",
"polyuria" = "Polyuria",
"polydipsia" = "Polydipsia",
"sudden_wt_loss" = "Penurunan BB Mendadak",
"weakness" = "Kelemahan Fisik",
"polyphagia" = "Polyphagia",
"genital_thrush" = "Genital Thrush",
"visual_blurring" = "Penglihatan Kabur",
"itching" = "Gatal-gatal",
"irritability" = "Iritabilitas",
"delayed_healing" = "Penyembuhan Luka Lambat",
"partial_paresis" = "Paresis Parsial",
"muscle_stiffness"= "Kekakuan Otot",
"alopecia" = "Alopesia",
"obesity" = "Obesitas"
)
persamaan <- paste0(
"$$\\text{logit}(P) = ", round(koef[1], 4),
paste(sapply(seq_along(koef)[-1], function(i) {
tanda <- ifelse(koef[i] >= 0, " + ", " - ")
paste0(tanda, abs(round(koef[i], 4)), " \\cdot \\text{", names(koef)[i], "}")
}), collapse = ""),
"$$"
)
cat(persamaan)\[\text{logit}(P) = 2.7265 - 0.0576 \cdot \text{age} - 4.4506 \cdot \text{gender} + 4.7012 \cdot \text{polyuria} + 4.9616 \cdot \text{polydipsia} + 0.8365 \cdot \text{weakness} + 1.658 \cdot \text{polyphagia} + 2.1572 \cdot \text{genital_thrush} + 1.1932 \cdot \text{visual_blurring} - 3.2202 \cdot \text{itching} + 2.4288 \cdot \text{irritability} + 0.897 \cdot \text{partial_paresis}\]
\[P(\text{diabetes positif}) = \frac{1}{1 + e^{-\text{logit}(P)}}\]
Materi: Uji G² (Likelihood Ratio Test) menguji signifikansi model secara keseluruhan dengan membandingkan model final (dengan prediktor) terhadap model null (hanya intercept):
\[G^2 = -2\left[\ell(\hat{\boldsymbol{\beta}}_\text{null}) - \ell(\hat{\boldsymbol{\beta}}_\text{model})\right] = D_\text{null} - D_\text{model}\]
Statistik \(G^2\) mengikuti distribusi \(\chi^2\) dengan derajat bebas = jumlah prediktor dalam model.
Hipotesis:
Referensi: Hosmer & Lemeshow (2013), hal. 10–14.
model_null_final <- glm(diabetes_positif ~ 1,
data = data_train,
family = binomial)
lr_stat <- model_null_final$deviance - model_stepwise$deviance
lr_df <- model_null_final$df.residual - model_stepwise$df.residual
lr_pvalue <- pchisq(lr_stat, df = lr_df, lower.tail = FALSE)
lrt_result <- data.frame(
Model = c("Model Null (intercept only)", "Model Final (Stepwise)"),
Deviance = round(c(model_null_final$deviance, model_stepwise$deviance), 4),
df = c(model_null_final$df.residual, model_stepwise$df.residual)
)
knitr::kable(lrt_result, caption = "Deviasi Model Null vs Model Final")| Model | Deviance | df |
|---|---|---|
| Model Null (intercept only) | 557.0725 | 415 |
| Model Final (Stepwise) | 133.3711 | 404 |
knitr::kable(
data.frame(
Statistik = c("G²", "Derajat Bebas (df)", "p-value", "Keputusan"),
Nilai = c(round(lr_stat, 4), lr_df,
formatC(lr_pvalue, format = "e", digits = 4),
ifelse(lr_pvalue < 0.05, "Tolak H0", "Gagal Tolak H0"))
),
caption = "Hasil Uji G² (Likelihood Ratio Test)"
)| Statistik | Nilai |
|---|---|
| G² | 423.7014 |
| Derajat Bebas (df) | 11 |
| p-value | 5.6490e-84 |
| Keputusan | Tolak H0 |
Interpretasi: Nilai \(G^2\) = 423.7014 dengan df = 11 dan \(p\)-value < 0,0001, sehingga \(H_0\) ditolak. Model final secara keseluruhan signifikan secara statistik — setidaknya satu prediktor berpengaruh signifikan terhadap probabilitas diabetes positif. Model layak untuk dilanjutkan ke tahap interpretasi.
Materi: Uji Hosmer-Lemeshow mengevaluasi kalibrasi model — apakah probabilitas prediksi sesuai dengan frekuensi aktual di data. Observasi dikelompokkan ke dalam \(g = 10\) desil berdasarkan probabilitas prediksi, kemudian dibandingkan frekuensi observasi vs ekspektasi menggunakan statistik: \[\hat{C} = \sum_{g=1}^{G} \left[ \frac{(O_g - E_g)^2}{E_g} + \frac{(O'_g - E'_g)^2}{E'_g} \right] \sim \chi^2(G-2)\]
Keterangan:
Hipotesis:
Keputusan: Gagal tolak \(H_0\) jika \(p > 0{,}05\) → Model layak digunakan.
Referensi: Hosmer & Lemeshow (1980), Communications in Statistics, 9(10), 1043–1069.
hl_test <- ResourceSelection::hoslem.test(
data_train$diabetes_positif,
fitted(model_stepwise),
g = 10
)
hl_result <- data.frame(
Statistik = "Chi-square Hosmer-Lemeshow",
Nilai = round(hl_test$statistic, 4),
df = hl_test$parameter,
`p-value` = round(hl_test$p.value, 4),
Keputusan = ifelse(hl_test$p.value > 0.05,
"Gagal tolak H0 — Model FIT",
"Tolak H0 — Model TIDAK FIT"),
check.names = FALSE
)
knitr::kable(hl_result, caption = "Hasil Uji Hosmer-Lemeshow")| Statistik | Nilai | df | p-value | Keputusan | |
|---|---|---|---|---|---|
| X-squared | Chi-square Hosmer-Lemeshow | 7.8498 | 8 | 0.4483 | Gagal tolak H0 — Model FIT |
Interpretasi: Nilai Chi-square = 7.8498 dengan df = 8 dan \(p\)-value = 0.4483 > 0,05, sehingga \(H_0\) gagal ditolak. Tidak terdapat bukti bahwa model menghasilkan prediksi yang menyimpang secara sistematis dari data aktual. Dengan demikian, model final dinyatakan fit dan layak digunakan.
Materi: Odds Ratio (OR) merupakan ukuran utama dalam regresi logistik yang mengukur kekuatan asosiasi antara prediktor dan variabel respon:
\[\text{OR} = e^{\hat{\beta}_j}\]
Confidence Interval 95%: \[\text{CI 95\%} = \left[e^{\hat{\beta}_j - 1{,}96 \cdot SE_j},\; e^{\hat{\beta}_j + 1{,}96 \cdot SE_j}\right]\]
Apabila CI 95% tidak mencakup nilai 1, maka OR dinyatakan signifikan secara statistik.
hasil_final_label <- hasil_final %>%
mutate(
Variabel_ID = Variabel,
Variabel = dplyr::recode(Variabel, !!!label_map)
) %>%
dplyr::filter(Variabel_ID != "(Intercept)")
final_sig <- hasil_final_label %>% dplyr::filter(`p-value` < 0.05)
final_nonsig <- hasil_final_label %>% dplyr::filter(`p-value` >= 0.05)knitr::kable(
final_sig %>%
dplyr::select(Variabel, Beta, `p-value`, Sig,
`Odds Ratio`, `CI 95% Bawah`, `CI 95% Atas`),
caption = "Faktor Risiko Signifikan dalam Model Final"
)| Variabel | Beta | p-value | Sig | Odds Ratio | CI 95% Bawah | CI 95% Atas |
|---|---|---|---|---|---|---|
| Usia | -0.0576 | 0.0208 | * | 0.9440 | 0.8990 | 0.9913 |
| Jenis Kelamin (Laki-laki vs Perempuan) | -4.4506 | 0.0000 | *** | 0.0117 | 0.0030 | 0.0450 |
| Polyuria | 4.7012 | 0.0000 | *** | 110.0765 | 25.4123 | 476.8107 |
| Polydipsia | 4.9616 | 0.0000 | *** | 142.8197 | 24.0580 | 847.8445 |
| Polyphagia | 1.6580 | 0.0034 | ** | 5.2486 | 1.7302 | 15.9214 |
| Genital Thrush | 2.1572 | 0.0006 | *** | 8.6466 | 2.5430 | 29.4000 |
| Gatal-gatal | -3.2202 | 0.0000 | *** | 0.0399 | 0.0092 | 0.1730 |
| Iritabilitas | 2.4288 | 0.0001 | *** | 11.3453 | 3.3471 | 38.4563 |
Interpretasi Odds Ratio (variabel signifikan):
Polydipsia (OR ≈ 143): Individu dengan polydipsia memiliki odds ~143 kali lebih tinggi untuk diabetes positif — faktor risiko terkuat dalam model. Secara klinis, kadar glukosa tinggi menyebabkan osmolalitas darah meningkat sehingga memicu rasa haus berlebihan.
Polyuria (OR ≈ 110): Individu dengan polyuria memiliki odds ~110 kali lebih tinggi. Glukosa berlebih di darah melebihi ambang ginjal sehingga terbuang bersama urin dalam volume besar.
Irritabilitas (OR ≈ 11): Fluktuasi gula darah memengaruhi fungsi korteks prefrontal yang mengatur pengendalian emosi, sehingga iritabilitas menjadi gejala yang relevan secara klinis.
Genital Thrush (OR ≈ 9): Kadar glukosa tinggi di urin (glikosuria) menciptakan lingkungan ideal bagi pertumbuhan Candida albicans di area genital.
Polyphagia (OR ≈ 5): Meskipun glukosa darah tinggi, sel tubuh tidak dapat menggunakannya akibat defisiensi insulin, sehingga tubuh terus mengirim sinyal lapar.
Gender laki-laki dan Usia (OR < 1): Kedua variabel menunjukkan arah negatif dalam konteks multivariat — temuan ini mencerminkan efek mediasi oleh gejala klinis yang dikontrol dalam model, bukan hubungan biologis langsung.
Itching (OR ≈ 0,04): Arah negatif ini perlu diinterpretasikan hati-hati; kemungkinan mencerminkan pola spesifik dataset, bukan bukti bahwa gatal bersifat protektif terhadap diabetes.
dominan <- final_sig %>%
arrange(desc(`Odds Ratio`)) %>%
mutate(Peringkat = row_number()) %>%
dplyr::select(Peringkat, Variabel, `Odds Ratio`, `CI 95% Bawah`, `CI 95% Atas`,
`p-value`, Sig)
knitr::kable(dominan, caption = "Peringkat Faktor Risiko Dominan Berdasarkan Odds Ratio")| Peringkat | Variabel | Odds Ratio | CI 95% Bawah | CI 95% Atas | p-value | Sig |
|---|---|---|---|---|---|---|
| 1 | Polydipsia | 142.8197 | 24.0580 | 847.8445 | 0.0000 | *** |
| 2 | Polyuria | 110.0765 | 25.4123 | 476.8107 | 0.0000 | *** |
| 3 | Iritabilitas | 11.3453 | 3.3471 | 38.4563 | 0.0001 | *** |
| 4 | Genital Thrush | 8.6466 | 2.5430 | 29.4000 | 0.0006 | *** |
| 5 | Polyphagia | 5.2486 | 1.7302 | 15.9214 | 0.0034 | ** |
| 6 | Usia | 0.9440 | 0.8990 | 0.9913 | 0.0208 | * |
| 7 | Gatal-gatal | 0.0399 | 0.0092 | 0.1730 | 0.0000 | *** |
| 8 | Jenis Kelamin (Laki-laki vs Perempuan) | 0.0117 | 0.0030 | 0.0450 | 0.0000 | *** |
or_plot <- hasil_final_label %>%
mutate(
Variabel = reorder(Variabel, `Odds Ratio`),
Signif = ifelse(Sig %in% c("*", "**", "***"),
"Signifikan (p < 0.05)", "Tidak Signifikan")
)
ggplot(or_plot, aes(x = `Odds Ratio`, y = Variabel, color = Signif)) +
geom_vline(xintercept = 1, linetype = "dashed", color = "gray50",
linewidth = 0.7) +
geom_errorbar(aes(xmin = `CI 95% Bawah`, xmax = `CI 95% Atas`,
y = Variabel),
width = 0.3, linewidth = 0.7,
orientation = "y") +
geom_point(size = 3.5) +
scale_x_log10() +
scale_color_manual(values = c("Signifikan (p < 0.05)" = "#2980b9",
"Tidak Signifikan" = "#95a5a6")) +
labs(
title = "Forest Plot: Odds Ratio Faktor Risiko Diabetes",
subtitle = "Model Final (Stepwise) — Data Train | CI 95%",
x = "Odds Ratio (skala logaritmik)",
y = "Variabel Prediktor",
color = "Signifikansi"
) +
theme_minimal(base_size = 11) +
theme(legend.position = "bottom")Forest Plot Odds Ratio Model Final
Materi: Evaluasi kinerja dilakukan pada data test (out-of-sample) untuk menilai kemampuan generalisasi model terhadap data baru yang tidak digunakan dalam proses pembangunan model.
Materi: Koefisien determinasi \(R^2\) tidak berlaku langsung untuk regresi logistik. Beberapa ukuran pseudo R² digunakan sebagai analogi:
| Ukuran | Formula | Acuan “Baik” |
|---|---|---|
| McFadden R² | \(1 - \ell(\hat{\boldsymbol{\beta}}_\text{model}) / \ell(\hat{\boldsymbol{\beta}}_\text{null})\) | > 0,20 |
| Cox-Snell R² | \(1 - \exp(-G^2/n)\) | Tidak ada acuan baku |
| Nagelkerke R² | Cox-Snell / max(Cox-Snell) | > 0,30 |
Referensi: McFadden (1974); Nagelkerke (1991); Cox & Snell (1989).
r2_mcf <- 1 - model_stepwise$deviance / model_stepwise$null.deviance
r2_cs <- 1 - exp((model_stepwise$deviance - model_stepwise$null.deviance) /
nobs(model_stepwise))
r2_nag <- r2_cs / (1 - exp(-model_stepwise$null.deviance / nobs(model_stepwise)))
pseudo_r2 <- data.frame(
Ukuran = c("McFadden R²", "Cox-Snell R²", "Nagelkerke R²"),
Nilai = round(c(r2_mcf, r2_cs, r2_nag), 4),
`Nilai (%)` = paste0(round(c(r2_mcf, r2_cs, r2_nag) * 100, 2), "%"),
Acuan = c("> 0.20 (Hosmer & Lemeshow, 2013)",
"Tidak ada acuan baku",
"> 0.30 (Nagelkerke, 1991)"),
Interpretasi = c(
ifelse(r2_mcf > 0.20, "Sangat Baik", "Perlu ditingkatkan"),
"-",
ifelse(r2_nag > 0.30, "Sangat Baik", "Perlu ditingkatkan")
),
check.names = FALSE
)
knitr::kable(pseudo_r2, caption = "Pseudo R-Square Model Final (Stepwise, Data Train)")| Ukuran | Nilai | Nilai (%) | Acuan | Interpretasi |
|---|---|---|---|---|
| McFadden R² | 0.7606 | 76.06% | > 0.20 (Hosmer & Lemeshow, 2013) | Sangat Baik |
| Cox-Snell R² | 0.6389 | 63.89% | Tidak ada acuan baku | - |
| Nagelkerke R² | 0.8658 | 86.58% | > 0.30 (Nagelkerke, 1991) | Sangat Baik |
Interpretasi:
McFadden R² = 0.7606: Model menjelaskan ~76.1% informasi relatif terhadap model null. Jauh melampaui threshold 0,20, menunjukkan daya diskriminasi model yang sangat kuat.
Cox-Snell R² = 0.6389: Sekitar 63.9% variabilitas variabel dependen dikaitkan dengan prediktor dalam model.
Nagelkerke R² = 0.8658: Model mampu menjelaskan sekitar 86.6% variabilitas kejadian diabetes — kategori sangat baik.
Ketiga ukuran pseudo R² secara konsisten menunjukkan nilai yang jauh melampaui threshold kelayakan, mengindikasikan kualitas model secara menyeluruh.
Materi: Confusion Matrix merangkum hasil prediksi model dalam empat kategori:
| Prediksi Positif | Prediksi Negatif | |
|---|---|---|
| Aktual Positif | True Positive (TP) | False Negative (FN) |
| Aktual Negatif | False Positive (FP) | True Negative (TN) |
Metrik turunan:
\[\text{Accuracy} = \frac{TP+TN}{n}, \quad \text{Sensitivity} = \frac{TP}{TP+FN}, \quad \text{Specificity} = \frac{TN}{TN+FP}\]
\[\text{Precision} = \frac{TP}{TP+FP}, \quad F_1 = \frac{2 \cdot \text{Precision} \cdot \text{Sensitivity}}{\text{Precision} + \text{Sensitivity}}\]
Catatan klinis: Dalam konteks skrining diabetes, Sensitivity lebih diprioritaskan karena False Negative (sakit diprediksi sehat) lebih berbahaya daripada False Positive.
conf_mat <- table(
Aktual = factor(data_test$diabetes_positif, levels = c(0, 1),
labels = c("Negatif", "Positif")),
Prediksi = factor(data_test$pred_biner, levels = c(0, 1),
labels = c("Negatif", "Positif"))
)
knitr::kable(addmargins(conf_mat),
caption = "Confusion Matrix — Model Final (Data Test, cut-off = 0,5)")| Negatif | Positif | Sum | |
|---|---|---|---|
| Negatif | 32 | 5 | 37 |
| Positif | 5 | 62 | 67 |
| Sum | 37 | 67 | 104 |
tp <- conf_mat["Positif", "Positif"]
tn <- conf_mat["Negatif", "Negatif"]
fp <- conf_mat["Negatif", "Positif"]
fn <- conf_mat["Positif", "Negatif"]
akurasi <- (tp + tn) / sum(conf_mat)
sensitivity <- tp / (tp + fn)
specificity <- tn / (tn + fp)
precision <- tp / (tp + fp)
npv <- tn / (tn + fn)
f1_score <- 2 * precision * sensitivity / (precision + sensitivity)
metrik_eval <- data.frame(
Metrik = c("Accuracy", "Sensitivity (Recall/TPR)",
"Specificity (TNR)", "Precision (PPV)", "NPV", "F1-Score"),
Nilai = round(c(akurasi, sensitivity, specificity,
precision, npv, f1_score), 4),
`Nilai (%)` = paste0(round(c(akurasi, sensitivity, specificity,
precision, npv, f1_score) * 100, 2), "%"),
Interpretasi = c(
"Proporsi prediksi benar secara keseluruhan",
"Kemampuan mendeteksi kasus diabetes positif (prioritas skrining)",
"Kemampuan mendeteksi kasus diabetes negatif",
"Dari prediksi positif, proporsi yang benar-benar positif",
"Dari prediksi negatif, proporsi yang benar-benar negatif",
"Rata-rata harmonis Precision dan Sensitivity"
),
check.names = FALSE
)
knitr::kable(metrik_eval,
caption = "Metrik Evaluasi Kinerja Klasifikasi — Data Test (cut-off = 0,5)")| Metrik | Nilai | Nilai (%) | Interpretasi |
|---|---|---|---|
| Accuracy | 0.9038 | 90.38% | Proporsi prediksi benar secara keseluruhan |
| Sensitivity (Recall/TPR) | 0.9254 | 92.54% | Kemampuan mendeteksi kasus diabetes positif (prioritas skrining) |
| Specificity (TNR) | 0.8649 | 86.49% | Kemampuan mendeteksi kasus diabetes negatif |
| Precision (PPV) | 0.9254 | 92.54% | Dari prediksi positif, proporsi yang benar-benar positif |
| NPV | 0.8649 | 86.49% | Dari prediksi negatif, proporsi yang benar-benar negatif |
| F1-Score | 0.9254 | 92.54% | Rata-rata harmonis Precision dan Sensitivity |
Interpretasi: Model menunjukkan performa yang sangat baik pada data test:
Materi: Cut-off default 0,5 tidak selalu optimal, terutama dalam konteks skrining medis. Youden Index menentukan threshold yang memaksimalkan jumlah Sensitivity dan Specificity:
\[J = \text{Sensitivity} + \text{Specificity} - 1\] Threshold optimal = nilai probabilitas yang menghasilkan \(J\) maksimum.
Referensi: Youden, W.J. (1950), Cancer, 3(1), 32–35.
# Prediksi probabilitas pada data test
data_test$prob_pred <- predict(model_stepwise, newdata = data_test, type = "response")
# Buat objek ROC
roc_obj <- pROC::roc(data_test$diabetes_positif, data_test$prob_pred, quiet = TRUE)
auc_value <- pROC::auc(roc_obj)
ci_auc <- pROC::ci.auc(roc_obj)
# Threshold optimal (Youden)
coords_opt <- pROC::coords(roc_obj, x = "best", best.method = "youden",
ret = c("threshold", "sensitivity", "specificity"))
threshold_opt <- as.numeric(coords_opt["threshold"])
sens_opt <- as.numeric(coords_opt["sensitivity"])
spec_opt <- as.numeric(coords_opt["specificity"])
# Prediksi ulang dengan threshold optimal
data_test$pred_opt <- ifelse(data_test$prob_pred >= threshold_opt, 1, 0)
conf_mat_opt <- table(
Aktual = factor(data_test$diabetes_positif, levels = c(0, 1),
labels = c("Negatif", "Positif")),
Prediksi = factor(data_test$pred_opt, levels = c(0, 1),
labels = c("Negatif", "Positif"))
)
knitr::kable(addmargins(conf_mat_opt),
caption = paste0("Confusion Matrix — Threshold Optimal Youden (",
round(threshold_opt, 4), ")")) | Negatif | Positif | Sum | |
|---|---|---|---|
| Negatif | 34 | 3 | 37 |
| Positif | 5 | 62 | 67 |
| Sum | 39 | 65 | 104 |
tp2 <- conf_mat_opt["Positif", "Positif"]
tn2 <- conf_mat_opt["Negatif", "Negatif"]
fp2 <- conf_mat_opt["Negatif", "Positif"]
fn2 <- conf_mat_opt["Positif", "Negatif"]
akurasi2 <- (tp2 + tn2) / sum(conf_mat_opt)
sensitivity2 <- tp2 / (tp2 + fn2)
specificity2 <- tn2 / (tn2 + fp2)
precision2 <- tp2 / (tp2 + fp2)
npv2 <- tn2 / (tn2 + fn2)
f1_score2 <- 2 * precision2 * sensitivity2 / (precision2 + sensitivity2)
perbandingan_threshold <- data.frame(
Threshold = c("Default (0,5)",
paste0("Optimal Youden (", round(threshold_opt, 4), ")")),
Accuracy = paste0(round(c(akurasi, akurasi2) * 100, 2), "%"),
Sensitivity = paste0(round(c(sensitivity, sensitivity2) * 100, 2), "%"),
Specificity = paste0(round(c(specificity, specificity2) * 100, 2), "%"),
F1_Score = round(c(f1_score, f1_score2), 4)
)
knitr::kable(perbandingan_threshold,
caption = "Perbandingan Kinerja Model: Threshold Default vs Threshold Optimal (Youden)")| Threshold | Accuracy | Sensitivity | Specificity | F1_Score |
|---|---|---|---|---|
| Default (0,5) | 90.38% | 92.54% | 86.49% | 0.9254 |
| Optimal Youden (0.6572) | 92.31% | 92.54% | 91.89% | 0.9394 |
Interpretasi: Threshold optimal sebesar 0.6572 diperoleh menggunakan Youden Index. Dibandingkan cut-off default 0,5, threshold optimal menghasilkan sensitivity yang lebih tinggi (92.54% vs 92.54%), meskipun dengan sedikit penurunan specificity. Dalam konteks skrining diabetes, sensitivity lebih diprioritaskan untuk meminimalkan false negative (kasus positif yang tidak terdeteksi), sehingga threshold optimal lebih disarankan untuk aplikasi klinis.
Materi: Kurva ROC (Receiver Operating Characteristic) menggambarkan trade-off antara Sensitivity (sumbu Y) dan 1 − Specificity (sumbu X) pada berbagai nilai threshold. Area Under the Curve (AUC) merangkum performa diskriminasi model:
| Nilai AUC | Interpretasi |
|---|---|
| 0,90 – 1,00 | Sangat Baik |
| 0,80 – 0,90 | Baik |
| 0,70 – 0,80 | Cukup |
| < 0,70 | Lemah |
AUC juga dikenal sebagai concordance index (c-statistic): probabilitas model memberikan skor probabilitas lebih tinggi pada kasus positif dibanding negatif.
Referensi: Hanley & McNeil (1982), Radiology, 143(1), 29–36.
roc_obj <- pROC::roc(data_test$diabetes_positif,
data_test$prob_pred,
quiet = TRUE)
auc_value <- pROC::auc(roc_obj)
ci_auc <- pROC::ci.auc(roc_obj)
auc_result <- data.frame(
Metrik = c("AUC", "CI 95% Bawah", "CI 95% Atas"),
Nilai = round(c(as.numeric(auc_value), ci_auc[1], ci_auc[3]), 4),
Interpretasi = c(
ifelse(auc_value > 0.90, "Sangat Baik (> 0,90)",
ifelse(auc_value > 0.80, "Baik (0,80–0,90)",
ifelse(auc_value > 0.70, "Cukup (0,70–0,80)", "Lemah (< 0,70)"))),
"-", "-"
)
)
knitr::kable(auc_result, caption = "Area Under the ROC Curve (AUC) — Data Test")| Metrik | Nilai | Interpretasi |
|---|---|---|
| AUC | 0.9685 | Sangat Baik (> 0,90) |
| CI 95% Bawah | 0.9418 | - |
| CI 95% Atas | 0.9952 | - |
Interpretasi: AUC = 0.9685 (CI 95%: 0.9418–0.9952) — termasuk kategori sangat baik (> 0,90). Model mampu membedakan pasien diabetes dan non-diabetes dengan tingkat kebenaran sebesar 96.85%. Seluruh rentang CI 95% berada di atas 0,90, mengonfirmasi konsistensi performa diskriminasi model.
roc_df <- data.frame(
fpr = 1 - roc_obj$specificities,
tpr = roc_obj$sensitivities
)
ggplot(roc_df, aes(x = fpr, y = tpr)) +
geom_line(color = "#2980b9", linewidth = 1.2) +
geom_abline(intercept = 0, slope = 1, linetype = "dashed",
color = "#e74c3c", linewidth = 0.9) +
geom_point(aes(x = 1 - spec_opt, y = sens_opt),
color = "#27ae60", size = 4, shape = 19) +
annotate("text",
x = 1 - spec_opt + 0.04, y = sens_opt - 0.04,
label = paste0("Optimal\n(", round(threshold_opt, 4), ")"),
color = "#27ae60", size = 3.5) +
annotate("text", x = 0.65, y = 0.25,
label = paste0("AUC = ", round(as.numeric(auc_value), 4)),
size = 4.5, color = "#2980b9", fontface = "bold") +
labs(
title = "Kurva ROC — Model Final (Stepwise), Data Test",
subtitle = paste0("AUC = ", round(as.numeric(auc_value), 4),
" | CI 95%: ", round(ci_auc[1], 4),
" - ", round(ci_auc[3], 4)),
x = "1 - Specificity (False Positive Rate)",
y = "Sensitivity (True Positive Rate)"
) +
theme_minimal(base_size = 12) +
coord_equal()Kurva ROC Model Final dengan Threshold Optimal (Youden)
Kemampuan diskriminasi model dievaluasi menggunakan kurva ROC (Receiver Operating Characteristic) dan nilai AUC (Area Under the Curve). Kurva ROC menggambarkan hubungan antara sensitivity dan 1 − specificity pada seluruh kemungkinan nilai threshold, sehingga memberikan gambaran menyeluruh tentang kemampuan model dalam membedakan kelas positif (diabetes) dan negatif (tidak diabetes).
Hasil evaluasi pada data test menunjukkan nilai AUC = 0,9685 dengan interval kepercayaan 95% sebesar 0,9418 – 0,9952. Nilai AUC mendekati 1 mengindikasikan bahwa model memiliki kemampuan diskriminasi yang sangat baik dalam membedakan individu dengan dan tanpa diabetes. Berdasarkan kriteria Hosmer & Lemeshow (2013), nilai AUC > 0,90 dikategorikan sebagai diskriminasi outstanding (luar biasa).
Titik threshold optimal ditentukan menggunakan indeks Youden (J = Sensitivity + Specificity − 1), yang memaksimalkan jumlah sensitivity dan specificity secara bersamaan. Threshold optimal yang diperoleh adalah 0,6572, yang ditandai dengan titik hijau pada kurva. Pada threshold ini, model mencapai sensitivity sebesar 0,9063 dan specificity sebesar 0,9250, artinya model mampu mengidentifikasi dengan benar 90,63% kasus diabetes dan 92,50% kasus non-diabetes pada data test.
Kurva ROC yang terbentuk berada jauh di atas garis diagonal referensi (garis merah putus-putus), yang merepresentasikan model acak (random classifier) dengan AUC = 0,50. Hal ini menegaskan bahwa model regresi logistik stepwise yang dibangun memiliki performa klasifikasi yang jauh lebih baik dibandingkan tebakan acak dan layak digunakan
koef <- coef(model_stepwise)
persamaan2 <- paste0(
"$$\\text{logit}(P) = ", round(koef[1], 4),
paste(sapply(seq_along(koef)[-1], function(i) {
tanda <- ifelse(koef[i] >= 0, " + ", " - ")
paste0(tanda, abs(round(koef[i], 4)), " \\cdot X_{\\text{",
gsub("_", "\\\\_", names(koef)[i]), "}}")
}), collapse = ""),
"$$"
)
cat(persamaan2)\[\text{logit}(P) = 2.7265 - 0.0576 \cdot X_{\text{age}} - 4.4506 \cdot X_{\text{gender}} + 4.7012 \cdot X_{\text{polyuria}} + 4.9616 \cdot X_{\text{polydipsia}} + 0.8365 \cdot X_{\text{weakness}} + 1.658 \cdot X_{\text{polyphagia}} + 2.1572 \cdot X_{\text{genital\_thrush}} + 1.1932 \cdot X_{\text{visual\_blurring}} - 3.2202 \cdot X_{\text{itching}} + 2.4288 \cdot X_{\text{irritability}} + 0.897 \cdot X_{\text{partial\_paresis}}\]
\[P(\text{diabetes positif}) = \frac{1}{1 + e^{-\text{logit}(P)}}\]
ringkasan <- data.frame(
Komponen = c(
"Variabel dalam Model Final",
"Variabel Dieliminasi (Stepwise)",
"AIC Model Penuh",
"AIC Model Final",
"Uji G² (LRT)",
"Uji Hosmer-Lemeshow",
"McFadden R²",
"Nagelkerke R²",
"AUC (Data Test)",
"Accuracy (cut-off 0,5)",
"Sensitivity (cut-off 0,5)",
"F1-Score (cut-off 0,5)",
"Threshold Optimal (Youden)",
"Sensitivity (threshold optimal)"
),
Nilai = c(
length(var_final),
length(var_dihapus),
round(AIC(model_penuh), 4),
round(AIC(model_stepwise), 4),
paste0("G² = ", round(lr_stat, 4), ", p < 0,0001"),
paste0("χ² = ", round(hl_test$statistic, 4),
", p = ", round(hl_test$p.value, 4), " (fit)"),
round(r2_mcf, 4),
round(r2_nag, 4),
paste0(round(as.numeric(auc_value), 4),
" (CI: ", round(ci_auc[1], 4), "–", round(ci_auc[3], 4), ")"),
paste0(round(akurasi * 100, 2), "%"),
paste0(round(sensitivity * 100, 2), "%"),
round(f1_score, 4),
round(threshold_opt, 4),
paste0(round(sensitivity2 * 100, 2), "%")
)
)
knitr::kable(ringkasan, caption = "Ringkasan Hasil Analisis Regresi Logistik Biner")| Komponen | Nilai |
|---|---|
| Variabel dalam Model Final | 11 |
| Variabel Dieliminasi (Stepwise) | 5 |
| AIC Model Penuh | 164.8563 |
| AIC Model Final | 157.3711 |
| Uji G² (LRT) | G² = 423.7014, p < 0,0001 |
| Uji Hosmer-Lemeshow | χ² = 7.8498, p = 0.4483 (fit) |
| McFadden R² | 0.7606 |
| Nagelkerke R² | 0.8658 |
| AUC (Data Test) | 0.9685 (CI: 0.9418–0.9952) |
| Accuracy (cut-off 0,5) | 90.38% |
| Sensitivity (cut-off 0,5) | 92.54% |
| F1-Score (cut-off 0,5) | 0.9254 |
| Threshold Optimal (Youden) | 0.6572 |
| Sensitivity (threshold optimal) | 92.54% |
Penelitian ini berhasil membangun model regresi logistik biner untuk memprediksi risiko diabetes stadium awal menggunakan 11 variabel prediktor yang dipilih melalui seleksi stepwise berbasis AIC. Beberapa simpulan utama:
1. Dari 15 variabel kategorik yang diuji secara bivariat, 12 variabel menunjukkan asosiasi signifikan (\(p < 0{,}05\)) dengan status diabetes. Seluruh asumsi regresi logistik (linearitas logit dan non-multikolinearitas) terpenuhi.
2. Model final memuat 11 prediktor dengan Polydipsia (OR ≈ 143) dan Polyuria (OR ≈ 110) sebagai prediktor terkuat, diikuti Irritabilitas, Genital Thrush, dan Polyphagia.
3. Model terbukti terkalibrasi dengan baik berdasarkan uji Hosmer-Lemeshow (\(p = 0.4483\)) dan memiliki daya penjelasan yang sangat kuat (Nagelkerke R² = 0.8658).
4. Evaluasi pada data uji menghasilkan AUC = 0.9685, accuracy 90.38%, sensitivity 92.54%, dan F1-Score 0.9254 — kategori sangat baik untuk keperluan skrining medis.
5. Model regresi logistik biner ini terbukti valid secara statistik dan memiliki performa diskriminasi yang sangat tinggi, sehingga dapat menjadi alat bantu skrining awal yang interpretatif secara klinis.