required_packages <- c("dplyr", "ggplot2", "broom", "knitr", "scales", "tidyr")
missing_packages <- required_packages[
!vapply(required_packages, requireNamespace, logical(1), quietly = TRUE)
]
if (length(missing_packages) > 0) {
stop(
"Paket berikut belum tersedia: ",
paste(missing_packages, collapse = ", "),
". Silakan install terlebih dahulu."
)
}
invisible(lapply(required_packages, library, character.only = TRUE))Putus studi (dropout) mahasiswa merupakan persoalan serius bagi institusi pendidikan tinggi karena berdampak pada efisiensi akademik, reputasi institusi, dan masa depan mahasiswa yang bersangkutan. Kemampuan memprediksi secara dini mahasiswa yang berisiko tidak menyelesaikan studi memberikan peluang bagi institusi untuk melakukan intervensi tepat waktu.
Tujuan analisis ini adalah membangun model regresi logistik biner untuk memprediksi apakah seorang mahasiswa akan putus studi (dropout) atau lulus (graduate) berdasarkan karakteristik demografis, akademik, dan ekonomi.
Kasus ini sesuai dianalisis dengan regresi logistik biner karena variabel respons bersifat biner, setelah menyaring hanya dua kelas dari dataset:
Model yang digunakan adalah:
\[ \log\left(\frac{p_i}{1 - p_i}\right) = \beta_0 + \beta_1 X_{1i} + \beta_2 X_{2i} + \cdots + \beta_k X_{ki} \]
di mana \(p_i = P(Y_i = 1 \mid X_i)\) adalah peluang mahasiswa ke-\(i\) mengalami putus studi.
Dataset yang digunakan adalah Predict Students’ Dropout and Academic Success yang tersedia di Kaggle:
Sumber: https://www.kaggle.com/datasets/hamnawaseem112222222/predict-students-dropout-and-academic-success
Dataset ini berasal dari sebuah perguruan tinggi di Portugal dan mencakup informasi mahasiswa dari berbagai program studi. Dataset asli terdiri dari tiga kelas target: Graduate, Dropout, dan Enrolled. Pada analisis ini, observasi dengan status Enrolled dikeluarkan karena status akhir studi mereka belum diketahui, sehingga pemodelan hanya mencakup mahasiswa yang sudah memiliki status akhir yang jelas.
# Sesuaikan path ke lokasi file CSV Anda
raw_data <- read.csv(file.choose(),
sep = ";",
stringsAsFactors = FALSE,
check.names = FALSE
)
cat(sprintf(
"Ukuran dataset asli : %d observasi, %d variabel\n",
nrow(raw_data), ncol(raw_data)
))## Ukuran dataset asli : 4424 observasi, 37 variabel
cat(sprintf(
"Distribusi Target awal:\n Graduate : %d\n Dropout : %d\n Enrolled : %d\n",
sum(raw_data$Target == "Graduate"),
sum(raw_data$Target == "Dropout"),
sum(raw_data$Target == "Enrolled")
))## Distribusi Target awal:
## Graduate : 2209
## Dropout : 1421
## Enrolled : 794
Berikut variabel yang digunakan dalam analisis. Variabel dengan informasi redundan atau bersifat pengenal (ID) dikeluarkan dari model.
kamus <- data.frame(
`Nama variabel` = c(
"Marital status", "Application mode", "Application order", "Course",
"Daytime/evening attendance", "Previous qualification",
"Previous qualification (grade)", "Nacionality",
"Mother's qualification", "Father's qualification",
"Mother's occupation", "Father's occupation",
"Admission grade", "Displaced", "Educational special needs",
"Debtor", "Tuition fees up to date", "Gender",
"Scholarship holder", "Age at enrollment", "International",
"Curricular units 1st sem (credited)", "Curricular units 1st sem (enrolled)",
"Curricular units 1st sem (evaluations)", "Curricular units 1st sem (approved)",
"Curricular units 1st sem (grade)", "Curricular units 1st sem (without evaluations)",
"Curricular units 2nd sem (credited)", "Curricular units 2nd sem (enrolled)",
"Curricular units 2nd sem (evaluations)", "Curricular units 2nd sem (approved)",
"Curricular units 2nd sem (grade)", "Curricular units 2nd sem (without evaluations)",
"Unemployment rate", "Inflation rate", "GDP", "Target"
),
Keterangan = c(
"Status pernikahan mahasiswa",
"Jalur pendaftaran masuk perguruan tinggi",
"Urutan pilihan program studi saat mendaftar",
"Program studi yang diambil",
"Kelas siang (1) atau malam (0)",
"Kualifikasi pendidikan sebelumnya",
"Nilai kualifikasi pendidikan sebelumnya (0–200)",
"Kebangsaan mahasiswa",
"Kualifikasi pendidikan ibu",
"Kualifikasi pendidikan ayah",
"Pekerjaan ibu",
"Pekerjaan ayah",
"Nilai masuk perguruan tinggi (0–200)",
"Mahasiswa pengungsi atau terlantar (1 = ya)",
"Mahasiswa dengan kebutuhan pendidikan khusus (1 = ya)",
"Mahasiswa memiliki tunggakan biaya (1 = ya)",
"Biaya kuliah sudah terbayar tepat waktu (1 = ya)",
"Jenis kelamin (1 = pria, 0 = wanita)",
"Penerima beasiswa (1 = ya)",
"Usia saat mendaftar (tahun)",
"Mahasiswa internasional (1 = ya)",
"SKS dikreditkan semester 1",
"SKS yang diambil semester 1",
"Jumlah evaluasi semester 1",
"SKS lulus semester 1",
"Nilai rata-rata SKS lulus semester 1 (0–20)",
"SKS tanpa evaluasi semester 1",
"SKS dikreditkan semester 2",
"SKS yang diambil semester 2",
"Jumlah evaluasi semester 2",
"SKS lulus semester 2",
"Nilai rata-rata SKS lulus semester 2 (0–20)",
"SKS tanpa evaluasi semester 2",
"Tingkat pengangguran nasional (%)",
"Tingkat inflasi nasional (%)",
"Produk Domestik Bruto (PDB)",
"Status akhir studi: Graduate / Dropout / Enrolled"
),
Tipe = c(
"Kategorik", "Kategorik", "Numerik", "Kategorik",
"Biner", "Kategorik",
"Numerik", "Kategorik",
"Kategorik", "Kategorik",
"Kategorik", "Kategorik",
"Numerik", "Biner", "Biner",
"Biner", "Biner", "Biner",
"Biner", "Numerik", "Biner",
"Numerik", "Numerik",
"Numerik", "Numerik",
"Numerik", "Numerik",
"Numerik", "Numerik",
"Numerik", "Numerik",
"Numerik", "Numerik",
"Numerik", "Numerik", "Numerik",
"Respons"
),
check.names = FALSE
)
knitr::kable(kamus, caption = "Kamus variabel dataset Students' Dropout and Academic Success")| Nama variabel | Keterangan | Tipe |
|---|---|---|
| Marital status | Status pernikahan mahasiswa | Kategorik |
| Application mode | Jalur pendaftaran masuk perguruan tinggi | Kategorik |
| Application order | Urutan pilihan program studi saat mendaftar | Numerik |
| Course | Program studi yang diambil | Kategorik |
| Daytime/evening attendance | Kelas siang (1) atau malam (0) | Biner |
| Previous qualification | Kualifikasi pendidikan sebelumnya | Kategorik |
| Previous qualification (grade) | Nilai kualifikasi pendidikan sebelumnya (0–200) | Numerik |
| Nacionality | Kebangsaan mahasiswa | Kategorik |
| Mother’s qualification | Kualifikasi pendidikan ibu | Kategorik |
| Father’s qualification | Kualifikasi pendidikan ayah | Kategorik |
| Mother’s occupation | Pekerjaan ibu | Kategorik |
| Father’s occupation | Pekerjaan ayah | Kategorik |
| Admission grade | Nilai masuk perguruan tinggi (0–200) | Numerik |
| Displaced | Mahasiswa pengungsi atau terlantar (1 = ya) | Biner |
| Educational special needs | Mahasiswa dengan kebutuhan pendidikan khusus (1 = ya) | Biner |
| Debtor | Mahasiswa memiliki tunggakan biaya (1 = ya) | Biner |
| Tuition fees up to date | Biaya kuliah sudah terbayar tepat waktu (1 = ya) | Biner |
| Gender | Jenis kelamin (1 = pria, 0 = wanita) | Biner |
| Scholarship holder | Penerima beasiswa (1 = ya) | Biner |
| Age at enrollment | Usia saat mendaftar (tahun) | Numerik |
| International | Mahasiswa internasional (1 = ya) | Biner |
| Curricular units 1st sem (credited) | SKS dikreditkan semester 1 | Numerik |
| Curricular units 1st sem (enrolled) | SKS yang diambil semester 1 | Numerik |
| Curricular units 1st sem (evaluations) | Jumlah evaluasi semester 1 | Numerik |
| Curricular units 1st sem (approved) | SKS lulus semester 1 | Numerik |
| Curricular units 1st sem (grade) | Nilai rata-rata SKS lulus semester 1 (0–20) | Numerik |
| Curricular units 1st sem (without evaluations) | SKS tanpa evaluasi semester 1 | Numerik |
| Curricular units 2nd sem (credited) | SKS dikreditkan semester 2 | Numerik |
| Curricular units 2nd sem (enrolled) | SKS yang diambil semester 2 | Numerik |
| Curricular units 2nd sem (evaluations) | Jumlah evaluasi semester 2 | Numerik |
| Curricular units 2nd sem (approved) | SKS lulus semester 2 | Numerik |
| Curricular units 2nd sem (grade) | Nilai rata-rata SKS lulus semester 2 (0–20) | Numerik |
| Curricular units 2nd sem (without evaluations) | SKS tanpa evaluasi semester 2 | Numerik |
| Unemployment rate | Tingkat pengangguran nasional (%) | Numerik |
| Inflation rate | Tingkat inflasi nasional (%) | Numerik |
| GDP | Produk Domestik Bruto (PDB) | Numerik |
| Target | Status akhir studi: Graduate / Dropout / Enrolled | Respons |
# Filter hanya Graduate dan Dropout
df_bin <- raw_data %>%
filter(Target %in% c("Graduate", "Dropout")) %>%
transmute(
# Variabel respons
dropout = as.integer(Target == "Dropout"),
do_label = factor(Target, levels = c("Graduate", "Dropout")),
# Variabel numerik
age = `Age at enrollment`,
prev_grade = `Previous qualification (grade)`,
admission_grade= `Admission grade`,
cu1_approved = `Curricular units 1st sem (approved)`,
cu1_grade = `Curricular units 1st sem (grade)`,
cu2_approved = `Curricular units 2nd sem (approved)`,
cu2_grade = `Curricular units 2nd sem (grade)`,
unemp_rate = `Unemployment rate`,
inflation_rate = `Inflation rate`,
gdp = GDP,
# Variabel biner (0/1)
gender = factor(Gender, levels = c(0, 1),
labels = c("Wanita", "Pria")),
scholarship = factor(`Scholarship holder`, levels = c(0, 1),
labels = c("Tidak", "Ya")),
debtor = factor(Debtor, levels = c(0, 1),
labels = c("Tidak", "Ya")),
tuition_ok = factor(`Tuition fees up to date`, levels = c(0, 1),
labels = c("Belum lunas", "Lunas")),
displaced = factor(Displaced, levels = c(0, 1),
labels = c("Tidak", "Ya")),
international = factor(International, levels = c(0, 1),
labels = c("Tidak", "Ya")),
special_needs = factor(`Educational special needs`, levels = c(0, 1),
labels = c("Tidak", "Ya")),
day_attendance = factor(`Daytime/evening attendance\t`, levels = c(0, 1),
labels = c("Malam", "Siang"))
)
cat(sprintf(
"Jumlah observasi setelah filter: %d\n Graduate (Y=0): %d\n Dropout (Y=1): %d\n",
nrow(df_bin),
sum(df_bin$dropout == 0),
sum(df_bin$dropout == 1)
))## Jumlah observasi setelah filter: 3630
## Graduate (Y=0): 2209
## Dropout (Y=1): 1421
Interpretasi: Setelah menyaring hanya kelas Graduate dan Dropout, diperoleh 3630 observasi. Status Enrolled sengaja dikeluarkan karena mahasiswa yang masih aktif belum memiliki status akhir, sehingga memasukkannya akan mengaburkan pola yang dipelajari model.
missing_df <- data.frame(
Variabel = names(df_bin),
`Jumlah NA` = sapply(df_bin, function(x) sum(is.na(x))),
check.names = FALSE
) %>% filter(`Jumlah NA` > 0)
if (nrow(missing_df) > 0) {
knitr::kable(missing_df, caption = "Variabel dengan nilai hilang")
} else {
cat("Tidak ditemukan nilai hilang (missing values) pada seluruh variabel yang digunakan.\n")
}## Tidak ditemukan nilai hilang (missing values) pada seluruh variabel yang digunakan.
kelas_summary <- df_bin %>%
count(do_label, name = "Jumlah") %>%
mutate(Proporsi = scales::percent(Jumlah / sum(Jumlah), accuracy = 0.1)) %>%
rename(Status = do_label)
knitr::kable(kelas_summary, caption = "Distribusi kelas respons (Graduate vs Dropout)")| Status | Jumlah | Proporsi |
|---|---|---|
| Graduate | 2209 | 60.9% |
| Dropout | 1421 | 39.1% |
ggplot(df_bin, aes(x = do_label, fill = do_label)) +
geom_bar(width = 0.55, color = "white", linewidth = 0.8) +
geom_text(
stat = "count",
aes(label = after_stat(count)),
vjust = -0.4, fontface = "bold", size = 4.5
) +
scale_fill_manual(values = c("Graduate" = "#2a9d8f", "Dropout" = "#e76f51")) +
labs(
title = "Distribusi Status Akhir Studi Mahasiswa",
x = NULL,
y = "Jumlah mahasiswa"
) +
theme_minimal(base_size = 12) +
theme(legend.position = "none")Interpretasi: Dataset bersifat tidak seimbang (imbalanced) dengan jumlah mahasiswa lulus (2209 orang, 60.9%) lebih banyak dari mahasiswa putus studi (1421 orang, 39.1%). Ketidakseimbangan ini wajar dalam konteks perguruan tinggi, namun perlu diperhatikan dalam interpretasi metrik evaluasi model—khususnya sensitivity, yang mengukur kemampuan mendeteksi mahasiswa yang benar-benar putus studi.
num_summary <- df_bin %>%
group_by(do_label) %>%
summarise(
N = n(),
`Rata-rata usia` = round(mean(age), 2),
`Nilai masuk (median)` = round(median(admission_grade), 2),
`SKS lulus sem-1 (mean)` = round(mean(cu1_approved), 2),
`Nilai SKS sem-1 (mean)` = round(mean(cu1_grade), 2),
`SKS lulus sem-2 (mean)` = round(mean(cu2_approved), 2),
`Nilai SKS sem-2 (mean)` = round(mean(cu2_grade), 2),
.groups = "drop"
) %>%
rename(Status = do_label)
knitr::kable(num_summary, caption = "Perbandingan variabel akademik dan demografis menurut status studi")| Status | N | Rata-rata usia | Nilai masuk (median) | SKS lulus sem-1 (mean) | Nilai SKS sem-1 (mean) | SKS lulus sem-2 (mean) | Nilai SKS sem-2 (mean) |
|---|---|---|---|---|---|---|---|
| Graduate | 2209 | 21.78 | 127.4 | 6.23 | 12.64 | 6.18 | 12.7 |
| Dropout | 1421 | 26.07 | 123.6 | 2.55 | 7.26 | 1.94 | 5.9 |
Interpretasi: Perbedaan yang sangat mencolok terlihat pada variabel kinerja akademik semester. Mahasiswa yang lulus rata-rata menyelesaikan sekitar 6.2 SKS di semester 2, sementara mahasiswa putus studi hanya sekitar 1.9 SKS. Demikian pula nilai rata-rata SKS: mahasiswa lulus memiliki nilai yang jauh lebih tinggi dibanding mahasiswa dropout. Perbedaan ini mengindikasikan bahwa kinerja akademik pada semester awal merupakan sinyal prediktif yang sangat kuat. Mahasiswa yang putus studi juga rata-rata berusia lebih tua saat mendaftar (26.1 tahun) dibanding yang lulus (21.8 tahun), yang mungkin mencerminkan tekanan dari tanggung jawab di luar kampus.
ggplot(df_bin, aes(x = do_label, y = age, fill = do_label)) +
geom_boxplot(alpha = 0.75, outlier.shape = 21, outlier.fill = "white",
outlier.size = 1.5) +
scale_fill_manual(values = c("Graduate" = "#2a9d8f", "Dropout" = "#e76f51")) +
labs(
title = "Distribusi Usia Saat Mendaftar Menurut Status Studi",
x = NULL,
y = "Usia saat mendaftar (tahun)"
) +
theme_minimal(base_size = 12) +
theme(legend.position = "none")Interpretasi: Boxplot memperlihatkan bahwa mahasiswa dropout memiliki median usia yang lebih tinggi dan variabilitas yang lebih besar dibanding mahasiswa lulus. Terdapat sejumlah outlier usia yang sangat tinggi (di atas 40 tahun) pada kelompok dropout, yang menunjukkan bahwa mahasiswa dewasa yang masuk kembali ke perguruan tinggi setelah lama meninggalkan pendidikan menghadapi risiko putus studi lebih besar—kemungkinan karena konflik antara studi, pekerjaan, dan tanggung jawab keluarga.
prop_df <- bind_rows(
df_bin %>% group_by(Variabel = "Jenis Kelamin",
Kategori = as.character(gender)) %>%
summarise(dropout_rate = mean(dropout), n = n(), .groups="drop"),
df_bin %>% group_by(Variabel = "Penerima Beasiswa",
Kategori = as.character(scholarship)) %>%
summarise(dropout_rate = mean(dropout), n = n(), .groups="drop"),
df_bin %>% group_by(Variabel = "Biaya Kuliah Lunas",
Kategori = as.character(tuition_ok)) %>%
summarise(dropout_rate = mean(dropout), n = n(), .groups="drop"),
df_bin %>% group_by(Variabel = "Memiliki Tunggakan",
Kategori = as.character(debtor)) %>%
summarise(dropout_rate = mean(dropout), n = n(), .groups="drop")
)
ggplot(prop_df, aes(x = Kategori, y = dropout_rate, fill = Kategori)) +
geom_col(width = 0.55, color = "white", show.legend = FALSE) +
geom_text(aes(label = scales::percent(dropout_rate, accuracy = 0.1)),
vjust = -0.4, size = 3.5) +
scale_y_continuous(labels = scales::percent_format(), limits = c(0, 1)) +
scale_fill_brewer(palette = "Set2") +
facet_wrap(~Variabel, scales = "free_x") +
labs(
title = "Proporsi Dropout Berdasarkan Variabel Kategorik Kunci",
x = NULL,
y = "Proporsi dropout"
) +
theme_minimal(base_size = 11)Interpretasi: Empat variabel kategorik mengungkap pola yang bermakna. Mahasiswa dengan tunggakan biaya (debtor) memiliki proporsi dropout yang jauh lebih tinggi dibanding yang tidak memiliki tunggakan. Mahasiswa yang biaya kuliahnya belum lunas (tuition fees not up to date) juga menunjukkan tingkat dropout yang sangat tinggi—kondisi finansial yang tidak stabil menjadi faktor penghambat penyelesaian studi. Penerima beasiswa memiliki proporsi dropout yang lebih rendah, mengindikasikan bahwa dukungan finansial berperan dalam mempertahankan mahasiswa. Adapun jenis kelamin menunjukkan perbedaan yang lebih moderat, dengan mahasiswa pria sedikit lebih berisiko dibanding wanita.
Data dibagi secara stratified dengan proporsi 80% training dan 20% testing agar komposisi kelas pada kedua subset mencerminkan proporsi data keseluruhan.
stratified_split <- function(y, prop = 0.8) {
idx_by_class <- split(seq_along(y), y)
train_idx <- lapply(
idx_by_class,
function(idx) sample(idx, size = floor(length(idx) * prop))
)
unlist(train_idx, use.names = FALSE)
}
set.seed(42)
train_id <- stratified_split(df_bin$dropout, prop = 0.8)
train_data <- df_bin[train_id, ]
test_data <- df_bin[-train_id, ]
split_summary <- bind_rows(
train_data %>% count(do_label) %>% mutate(Data = "Training"),
test_data %>% count(do_label) %>% mutate(Data = "Testing")
) %>%
group_by(Data) %>%
mutate(Proporsi = scales::percent(n / sum(n), accuracy = 0.1)) %>%
ungroup() %>%
rename(Status = do_label, Jumlah = n) %>%
select(Data, Status, Jumlah, Proporsi)
knitr::kable(split_summary, caption = "Distribusi kelas pada data training dan testing")| Data | Status | Jumlah | Proporsi |
|---|---|---|---|
| Training | Graduate | 1767 | 60.9% |
| Training | Dropout | 1136 | 39.1% |
| Testing | Graduate | 442 | 60.8% |
| Testing | Dropout | 285 | 39.2% |
Interpretasi: Pembagian stratified memastikan proporsi Graduate dan Dropout pada data training dan testing seimbang dan representatif. Data training digunakan untuk mengestimasi parameter model, sementara data testing digunakan untuk mengevaluasi performa model pada data yang tidak pernah dilihat selama proses estimasi.
Misalkan \(Y_i\) menyatakan status akhir mahasiswa ke-\(i\):
\[ Y_i = \begin{cases} 1, & \text{jika mahasiswa putus studi (Dropout),} \\ 0, & \text{jika mahasiswa lulus (Graduate).} \end{cases} \]
Peluang mahasiswa putus studi dinotasikan:
\[p_i = P(Y_i = 1 \mid X_i).\]
Model logistik memodelkan log-odds putus studi sebagai fungsi linear dari prediktor:
\[ \text{logit}(p_i) = \ln\!\left(\frac{p_i}{1 - p_i}\right) = \beta_0 + \sum_{j=1}^{k} \beta_j X_{ji}. \]
Peluang prediksi diperoleh melalui transformasi balik:
\[ \hat{p}_i = \frac{1}{1 + \exp\!\left(-\hat{\eta}_i\right)}. \]
Interpretasi odds ratio: Untuk prediktor numerik \(X_j\), kenaikan satu satuan pada \(X_j\) mengubah odds dropout sebesar faktor \(\exp(\hat\beta_j)\), dengan asumsi prediktor lain tetap:
heart_fit <- glm(
dropout ~ age + prev_grade + admission_grade +
cu1_approved + cu1_grade + cu2_approved + cu2_grade +
unemp_rate + inflation_rate + gdp +
gender + scholarship + debtor + tuition_ok +
displaced + international + day_attendance,
data = train_data,
family = binomial(link = "logit")
)
ringkasan_model <- data.frame(
Keterangan = c(
"Jumlah observasi training",
"Null deviance",
"Residual deviance",
"Derajat bebas residual",
"AIC"
),
Nilai = c(
nobs(heart_fit),
round(heart_fit$null.deviance, 3),
round(heart_fit$deviance, 3),
heart_fit$df.residual,
round(AIC(heart_fit), 3)
)
)
knitr::kable(ringkasan_model, caption = "Ringkasan kecocokan model regresi logistik biner")| Keterangan | Nilai |
|---|---|
| Jumlah observasi training | 2903.000 |
| Null deviance | 3886.157 |
| Residual deviance | 1877.557 |
| Derajat bebas residual | 2885.000 |
| AIC | 1913.557 |
Interpretasi: Residual deviance (1877.6) yang jauh lebih kecil dari null deviance (3886.2) menunjukkan bahwa prediktor yang dimasukkan secara bersama-sama memberikan kontribusi nyata dalam menjelaskan variasi status studi mahasiswa. Selisih deviance sebesar 2008.6 poin mengindikasikan perbaikan kecocokan model yang substansial dibandingkan model tanpa prediktor sama sekali. Nilai AIC digunakan sebagai ukuran keseimbangan antara kecocokan model dan jumlah parameter; semakin kecil nilai AIC, semakin efisien model.
coef_table <- broom::tidy(heart_fit) %>%
filter(term != "(Intercept)") %>%
mutate(
odds_ratio = exp(estimate),
ci_low = exp(estimate - 1.96 * std.error),
ci_high = exp(estimate + 1.96 * std.error),
signif = case_when(
p.value < 0.001 ~ "***",
p.value < 0.01 ~ "**",
p.value < 0.05 ~ "*",
TRUE ~ ""
)
) %>%
arrange(p.value) %>%
transmute(
`Variabel / level` = term,
`Odds ratio` = round(odds_ratio, 3),
`IK 95% bawah` = round(ci_low, 3),
`IK 95% atas` = round(ci_high, 3),
`p-value` = signif(p.value, 3),
`Signifikansi` = signif
)
knitr::kable(
coef_table,
caption = "Koefisien model regresi logistik — diurutkan dari p-value terkecil (*** p<0.001, ** p<0.01, * p<0.05)"
)| Variabel / level | Odds ratio | IK 95% bawah | IK 95% atas | p-value | Signifikansi |
|---|---|---|---|---|---|
| cu2_approved | 0.509 | 0.456 | 0.569 | 0.000000 | *** |
| tuition_okLunas | 0.056 | 0.032 | 0.098 | 0.000000 | *** |
| scholarshipYa | 0.277 | 0.203 | 0.377 | 0.000000 | *** |
| age | 1.042 | 1.024 | 1.061 | 0.000005 | *** |
| debtorYa | 2.283 | 1.488 | 3.503 | 0.000157 | *** |
| admission_grade | 0.983 | 0.974 | 0.993 | 0.000680 | *** |
| cu1_approved | 1.179 | 1.071 | 1.297 | 0.000767 | *** |
| genderPria | 1.481 | 1.159 | 1.893 | 0.001670 | ** |
| internationalYa | 0.274 | 0.113 | 0.661 | 0.003990 | ** |
| cu2_grade | 0.934 | 0.885 | 0.986 | 0.013700 | * |
| unemp_rate | 1.030 | 0.983 | 1.079 | 0.215000 | |
| inflation_rate | 1.051 | 0.965 | 1.144 | 0.251000 | |
| prev_grade | 0.994 | 0.984 | 1.005 | 0.316000 | |
| cu1_grade | 1.018 | 0.965 | 1.074 | 0.510000 | |
| gdp | 0.982 | 0.928 | 1.038 | 0.518000 | |
| day_attendanceSiang | 1.131 | 0.764 | 1.673 | 0.539000 | |
| displacedYa | 0.961 | 0.743 | 1.242 | 0.759000 |
Interpretasi tabel koefisien:
Prediktor dengan pengaruh paling dominan terhadap peluang putus studi berdasarkan tabel adalah sebagai berikut.
Biaya kuliah yang belum lunas (tuition fees not up to date) menunjukkan odds ratio yang jauh di atas 1. Artinya, mahasiswa yang pembayaran biaya kuliahnya tidak tepat waktu memiliki kecenderungan putus studi yang jauh lebih tinggi dibanding mahasiswa yang biayanya sudah lunas, dengan asumsi faktor lain konstan. Kondisi finansial yang tidak stabil secara langsung mengancam kelangsungan studi mahasiswa.
SKS yang lulus di semester 2
(cu2_approved) memiliki odds ratio di bawah 1 dan sangat
signifikan. Setiap tambahan satu SKS yang berhasil diselesaikan di
semester 2 secara substansial menurunkan odds putus studi. Ini
mengkonfirmasi bahwa keberhasilan akademik di awal studi merupakan
pelindung utama terhadap dropout.
Nilai rata-rata SKS semester 2
(cu2_grade) juga menunjukkan arah yang sama: nilai yang
lebih tinggi berasosiasi dengan odds dropout yang lebih rendah,
mencerminkan bahwa performa akademik yang baik secara konsisten
mengurangi risiko putus studi.
Memiliki tunggakan (debtor) meningkatkan odds dropout secara signifikan. Mahasiswa dengan tunggakan keuangan kepada institusi menghadapi tekanan finansial yang dapat memaksa mereka meninggalkan studi.
Penerima beasiswa (scholarship holder) memiliki odds ratio di bawah 1, artinya beasiswa menurunkan peluang dropout. Dukungan finansial formal dari institusi terbukti menjadi faktor protektif yang penting.
Usia saat mendaftar memiliki odds ratio di atas 1—setiap tambahan satu tahun usia saat mendaftar meningkatkan odds dropout. Mahasiswa yang mendaftar pada usia lebih tua kemungkinan memiliki komitmen eksternal (pekerjaan, keluarga) yang bersaing dengan kewajiban akademik.
or_plot_data <- broom::tidy(heart_fit) %>%
filter(term != "(Intercept)") %>%
mutate(
odds_ratio = exp(estimate),
ci_low = exp(estimate - 1.96 * std.error),
ci_high = exp(estimate + 1.96 * std.error),
arah = ifelse(odds_ratio >= 1, "Meningkatkan odds dropout", "Menurunkan odds dropout"),
label_term = dplyr::recode(term,
"age" = "Usia saat mendaftar",
"prev_grade" = "Nilai kualifikasi sebelumnya",
"admission_grade" = "Nilai masuk",
"cu1_approved" = "SKS lulus sem-1",
"cu1_grade" = "Nilai SKS sem-1",
"cu2_approved" = "SKS lulus sem-2",
"cu2_grade" = "Nilai SKS sem-2",
"unemp_rate" = "Tingkat pengangguran",
"inflation_rate" = "Inflasi",
"gdp" = "PDB",
"genderPria" = "Jenis kelamin: Pria",
"scholarshipYa" = "Beasiswa: Ya",
"debtorYa" = "Tunggakan: Ya",
"tuition_okLunas" = "Biaya kuliah: Lunas",
"displacedYa" = "Displaced: Ya",
"internationalYa" = "Internasional: Ya",
"day_attendanceSiang" = "Kelas siang"
)
) %>%
arrange(odds_ratio)
ggplot(or_plot_data,
aes(x = odds_ratio,
y = reorder(label_term, odds_ratio),
color = arah)) +
geom_vline(xintercept = 1, linetype = "dashed", color = "#6c757d") +
geom_errorbarh(aes(xmin = ci_low, xmax = ci_high), height = 0.3, linewidth = 0.7) +
geom_point(size = 3) +
scale_color_manual(values = c(
"Meningkatkan odds dropout" = "#e76f51",
"Menurunkan odds dropout" = "#2a9d8f"
)) +
scale_x_log10() +
labs(
title = "Odds Ratio Prediktor Regresi Logistik Biner",
subtitle = "Prediksi putus studi (Dropout vs Graduate) — skala logaritmik",
x = "Odds ratio (log scale) + Interval Kepercayaan 95%",
y = NULL,
color = NULL
) +
theme_minimal(base_size = 11) +
theme(legend.position = "bottom")Interpretasi: Grafik forest plot menyajikan odds ratio setiap prediktor beserta interval kepercayaan 95%-nya pada skala logaritmik. Prediktor berwarna oranye (odds ratio > 1) meningkatkan odds putus studi, sedangkan yang berwarna hijau (odds ratio < 1) menurunkannya. Prediktor yang interval kepercayaannya tidak melewati garis putus-putus pada nilai 1 dinyatakan signifikan secara statistik. Visualisasi ini memperlihatkan bahwa faktor akademik semester awal (SKS lulus dan nilai) serta faktor finansial (biaya lunas, tunggakan, beasiswa) mendominasi model sebagai prediktor paling informatif.
Kelas prediksi ditentukan berdasarkan threshold \(c\):
\[ \hat{Y}_i = \begin{cases} 1 \;(\text{Dropout}), & \text{jika } \hat{p}_i \ge c, \\ 0 \;(\text{Graduate}), & \text{jika } \hat{p}_i < c. \end{cases} \]
Dari confusion matrix diperoleh:
\[\text{Akurasi} = \frac{TP + TN}{TP + TN + FP + FN}\]
\[\text{Sensitivity} = \frac{TP}{TP + FN} \quad \text{(kemampuan mendeteksi dropout)}\]
\[\text{Specificity} = \frac{TN}{TN + FP} \quad \text{(kemampuan mengidentifikasi lulusan)}\]
\[\text{Presisi} = \frac{TP}{TP + FP}\]
\[\text{F1-score} = \frac{2 \times \text{Presisi} \times \text{Sensitivity}}{\text{Presisi} + \text{Sensitivity}}\]
\[\text{Balanced accuracy} = \frac{\text{Sensitivity} + \text{Specificity}}{2}\]
Dalam konteks prediksi dropout, sensitivity menjadi metrik prioritas karena merepresentasikan proporsi mahasiswa dropout yang berhasil diidentifikasi oleh model. Mahasiswa dropout yang tidak terdeteksi (false negative) akan kehilangan kesempatan mendapat intervensi dini.
safe_div <- function(num, den) ifelse(den == 0, NA_real_, num / den)
classification_metrics <- function(actual, prob, threshold = 0.5) {
pred <- as.integer(prob >= threshold)
tp <- sum(pred == 1 & actual == 1)
tn <- sum(pred == 0 & actual == 0)
fp <- sum(pred == 1 & actual == 0)
fn <- sum(pred == 0 & actual == 1)
sens <- safe_div(tp, tp + fn)
spec <- safe_div(tn, tn + fp)
prec <- safe_div(tp, tp + fp)
data.frame(
threshold = threshold,
accuracy = safe_div(tp + tn, tp + tn + fp + fn),
error_rate = 1 - safe_div(tp + tn, tp + tn + fp + fn),
sensitivity = sens,
specificity = spec,
precision = prec,
npv = safe_div(tn, tn + fn),
f1_score = safe_div(2 * prec * sens, prec + sens),
balanced_accuracy = (sens + spec) / 2,
fpr = 1 - spec,
fnr = 1 - sens
)
}
confusion_matrix_tbl <- function(actual, prob, threshold = 0.5) {
pred_lbl <- factor(ifelse(prob >= threshold, "Prediksi Dropout","Prediksi Graduate"),
levels = c("Prediksi Dropout","Prediksi Graduate"))
actual_lbl <- factor(ifelse(actual == 1, "Aktual Dropout","Aktual Graduate"),
levels = c("Aktual Dropout","Aktual Graduate"))
addmargins(table(actual_lbl, pred_lbl))
}cm_default <- confusion_matrix_tbl(test_data$dropout, p_test, 0.5)
m_default <- classification_metrics(test_data$dropout, p_test, 0.5)
knitr::kable(cm_default,
caption = "Confusion matrix data testing — threshold 0,50")| Prediksi Dropout | Prediksi Graduate | Sum | |
|---|---|---|---|
| Aktual Dropout | 233 | 52 | 285 |
| Aktual Graduate | 28 | 414 | 442 |
| Sum | 261 | 466 | 727 |
knitr::kable(
m_default %>% mutate(across(where(is.numeric), round, 3)) %>%
rename(Threshold = threshold, Akurasi = accuracy, `Error rate` = error_rate,
Sensitivity = sensitivity, Specificity = specificity,
Presisi = precision, NPV = npv, `F1-score` = f1_score,
`Balanced accuracy` = balanced_accuracy, FPR = fpr, FNR = fnr),
caption = "Metrik evaluasi data testing pada threshold 0,50")| Threshold | Akurasi | Error rate | Sensitivity | Specificity | Presisi | NPV | F1-score | Balanced accuracy | FPR | FNR |
|---|---|---|---|---|---|---|---|---|---|---|
| 0.5 | 0.89 | 0.11 | 0.818 | 0.937 | 0.893 | 0.888 | 0.853 | 0.877 | 0.063 | 0.182 |
Interpretasi: Pada threshold 0,50, model mengklasifikasikan mahasiswa sebagai dropout jika peluang prediksinya minimal 50%. Nilai akurasi keseluruhan menggambarkan proporsi prediksi yang benar dari seluruh observasi. Namun dalam konteks prediksi dropout, akurasi saja tidak cukup—perlu melihat sensitivity (proporsi mahasiswa dropout yang berhasil dideteksi) dan specificity (proporsi mahasiswa lulus yang diidentifikasi dengan benar). Jika sensitivity pada threshold ini masih rendah, berarti banyak mahasiswa yang benar-benar berisiko dropout tidak terdeteksi oleh model.
Kurva ROC (Receiver Operating Characteristic) dibangun dengan menghitung pasangan (FPR, TPR) untuk setiap nilai threshold yang mungkin, di mana:
\[TPR(c) = \text{Sensitivity}(c), \quad FPR(c) = 1 - \text{Specificity}(c).\]
Nilai AUC (Area Under Curve) dihitung sebagai:
\[AUC = \int_0^1 TPR(FPR)\, d(FPR).\]
AUC merepresentasikan probabilitas bahwa model memberikan skor dropout yang lebih tinggi pada mahasiswa yang benar-benar dropout dibandingkan mahasiswa yang lulus, jika diambil satu pasang secara acak.
Threshold optimal dipilih menggunakan Indeks Youden:
\[J = \max_c \{\text{Sensitivity}(c) + \text{Specificity}(c) - 1\}.\]
roc_points <- function(actual, prob) {
thresholds <- c(Inf, sort(unique(prob), decreasing = TRUE), -Inf)
out <- lapply(thresholds, function(th) {
pred <- as.integer(prob >= th)
tp <- sum(pred == 1 & actual == 1)
tn <- sum(pred == 0 & actual == 0)
fp <- sum(pred == 1 & actual == 0)
fn <- sum(pred == 0 & actual == 1)
sens <- safe_div(tp, tp + fn)
spec <- safe_div(tn, tn + fp)
data.frame(threshold = th, sensitivity = sens, specificity = spec,
fpr = 1 - spec, youden = sens + spec - 1)
})
bind_rows(out)
}
auc_value <- function(roc_df) {
r <- roc_df %>% arrange(fpr, sensitivity)
sum(diff(r$fpr) * (head(r$sensitivity,-1) + tail(r$sensitivity,-1)) / 2)
}
roc_train <- roc_points(train_data$dropout, p_train) %>% mutate(Data = "Training")
roc_test <- roc_points(test_data$dropout, p_test) %>% mutate(Data = "Testing")
auc_train <- auc_value(roc_train)
auc_test <- auc_value(roc_test)
optimal_train <- roc_train %>%
filter(is.finite(threshold)) %>%
arrange(desc(youden), desc(sensitivity)) %>%
slice(1)
threshold_opt <- optimal_train$threshold[1]
test_at_opt <- roc_points(test_data$dropout, p_test) %>%
filter(is.finite(threshold)) %>%
slice_min(abs(threshold - threshold_opt), n = 1, with_ties = FALSE)
auc_tbl <- data.frame(
Data = c("Training", "Testing"),
AUC = round(c(auc_train, auc_test), 3)
)
knitr::kable(auc_tbl, caption = "Nilai AUC pada data training dan testing")| Data | AUC |
|---|---|
| Training | 0.922 |
| Testing | 0.945 |
youden_tbl <- optimal_train %>%
transmute(
`Threshold optimal` = round(threshold, 3),
Sensitivity = round(sensitivity, 3),
Specificity = round(specificity, 3),
`Indeks Youden (J)` = round(youden, 3)
)
knitr::kable(youden_tbl, caption = "Threshold optimal berdasarkan Indeks Youden (dari ROC training)")| Threshold optimal | Sensitivity | Specificity | Indeks Youden (J) |
|---|---|---|---|
| 0.444 | 0.828 | 0.925 | 0.754 |
Interpretasi AUC: AUC training sebesar 0.922 dan AUC testing sebesar 0.945 menunjukkan kemampuan diskriminasi model yang sangat baik (excellent). Kedekatan nilai AUC training dan testing mengindikasikan model tidak mengalami overfitting yang signifikan—performa yang diperoleh pada data training mampu digeneralisasi ke data baru dengan baik.
Threshold optimal 0.444 diperoleh dari titik pada kurva ROC yang memaksimalkan Indeks Youden, yaitu titik dengan kombinasi sensitivity dan specificity terbaik secara bersamaan.
roc_all <- bind_rows(roc_train, roc_test)
ggplot(roc_all, aes(x = fpr, y = sensitivity, color = Data)) +
geom_path(linewidth = 1.1) +
geom_abline(intercept = 0, slope = 1, linetype = "dashed", color = "#6c757d") +
geom_point(data = optimal_train,
aes(x = fpr, y = sensitivity), inherit.aes = FALSE,
color = "#ffb703", fill = "#fb8500",
shape = 21, size = 4.5, stroke = 1.4) +
geom_point(data = test_at_opt,
aes(x = fpr, y = sensitivity), inherit.aes = FALSE,
color = "#8338ec", fill = "#3a86ff",
shape = 24, size = 4.5, stroke = 1.4) +
annotate("text", x = optimal_train$fpr + 0.07, y = optimal_train$sensitivity - 0.04,
label = paste0("Opt. Training\n(", round(threshold_opt,3),")"),
size = 3, color = "#fb8500") +
coord_equal() +
scale_color_manual(values = c("Training" = "#0077b6", "Testing" = "#e76f51")) +
labs(
title = "Kurva ROC — Prediksi Putus Studi Mahasiswa",
subtitle = paste0("AUC training = ", round(auc_train,3),
" | AUC testing = ", round(auc_test,3),
" | Threshold optimal = ", round(threshold_opt,3)),
x = "False Positive Rate (1 – Specificity)",
y = "Sensitivity (True Positive Rate)",
color = "Data"
) +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom")Interpretasi kurva ROC: Kurva ROC yang jauh di atas garis diagonal (model acak) menunjukkan bahwa model memiliki kemampuan diskriminasi yang kuat. Kurva training dan testing yang hampir berhimpit mengkonfirmasi stabilitas model. Titik kuning pada kurva training dan segitiga ungu pada kurva testing menandai posisi threshold optimal—titik di mana trade-off antara mendeteksi mahasiswa dropout dan tidak salah mengklasifikasikan mahasiswa lulus mencapai keseimbangan terbaik.
Threshold optimal dari ROC training (0.444) diterapkan pada data testing. Dengan threshold ini, mahasiswa diklasifikasikan dropout jika peluang prediksinya \(\geq\) 0.444.
cm_opt <- confusion_matrix_tbl(test_data$dropout, p_test, threshold_opt)
m_opt <- classification_metrics(test_data$dropout, p_test, threshold_opt)
knitr::kable(cm_opt,
caption = paste0("Confusion matrix data testing — threshold optimal (",
round(threshold_opt,3),")"))| Prediksi Dropout | Prediksi Graduate | Sum | |
|---|---|---|---|
| Aktual Dropout | 240 | 45 | 285 |
| Aktual Graduate | 32 | 410 | 442 |
| Sum | 272 | 455 | 727 |
compare_df <- bind_rows(
m_default %>% mutate(Aturan = "Threshold 0,50"),
m_opt %>% mutate(Aturan = paste0("Threshold optimal (", round(threshold_opt,3),")"))
) %>%
mutate(across(where(is.numeric), round, 3)) %>%
select(Aturan, threshold, accuracy, sensitivity, specificity,
f1_score, balanced_accuracy) %>%
rename(
`Aturan klasifikasi` = Aturan, Threshold = threshold,
Akurasi = accuracy, Sensitivity = sensitivity,
Specificity = specificity, `F1-score` = f1_score,
`Balanced accuracy` = balanced_accuracy
)
knitr::kable(compare_df,
caption = "Perbandingan metrik evaluasi: threshold 0,50 vs threshold optimal")| Aturan klasifikasi | Threshold | Akurasi | Sensitivity | Specificity | F1-score | Balanced accuracy |
|---|---|---|---|---|---|---|
| Threshold 0,50 | 0.500 | 0.890 | 0.818 | 0.937 | 0.853 | 0.877 |
| Threshold optimal (0.444) | 0.444 | 0.894 | 0.842 | 0.928 | 0.862 | 0.885 |
Interpretasi perbandingan threshold:
Pergeseran dari threshold 0,50 ke threshold optimal mengubah keseimbangan antara sensitivity dan specificity. Dengan threshold yang lebih rendah, model menjadi lebih “waspada” dalam menandai mahasiswa sebagai berisiko dropout—artinya lebih banyak mahasiswa dropout yang berhasil terdeteksi (sensitivity meningkat), meskipun sebagian mahasiswa yang sebenarnya lulus mungkin ikut terflag (specificity sedikit turun).
Dalam konteks intervensi akademik, peningkatan sensitivity lebih berharga: lebih baik memberikan perhatian ekstra kepada mahasiswa lulus yang tidak memerlukannya (false positive) daripada melewatkan mahasiswa dropout yang benar-benar memerlukan bantuan (false negative). Nilai balanced accuracy dan F1-score memberikan gambaran kinerja model yang lebih seimbang dibandingkan akurasi biasa, terutama pada data yang tidak seimbang.
test_with_prob <- test_data %>% mutate(prob_dropout = p_test)
ggplot(test_with_prob, aes(x = prob_dropout, fill = do_label)) +
geom_density(alpha = 0.60, color = "white", linewidth = 0.7) +
geom_vline(xintercept = 0.5, color = "#6c757d",
linewidth = 0.9, linetype = "dotted") +
geom_vline(xintercept = threshold_opt, color = "#fb8500",
linewidth = 1.2, linetype = "dashed") +
annotate("label", x = threshold_opt, y = Inf,
label = paste0("Threshold optimal\n", round(threshold_opt,3)),
vjust = 1.5, fill = "#fff3b0", color = "#5f370e", label.size = 0, size = 3.2) +
annotate("label", x = 0.5, y = Inf,
label = "Threshold\n0,50",
vjust = 3.5, fill = "#f0f0f0", color = "#444444", label.size = 0, size = 3.2) +
scale_fill_manual(values = c("Graduate" = "#2a9d8f", "Dropout" = "#e76f51")) +
labs(
title = "Distribusi Peluang Prediksi Dropout — Data Testing",
subtitle = "Dua garis vertikal menunjukkan threshold default (0,50) dan threshold optimal",
x = "Peluang prediksi putus studi",
y = "Kepadatan",
fill = "Status aktual"
) +
theme_minimal(base_size = 12)Interpretasi: Grafik kepadatan memperlihatkan bahwa model mampu memisahkan distribusi peluang kedua kelompok dengan cukup baik. Distribusi mahasiswa Graduate (hijau) terkonsentrasi di peluang rendah (mendekati 0), sementara distribusi Dropout (oranye) terkonsentrasi di peluang tinggi (mendekati 1). Tumpang tindih yang relatif kecil di tengah mencerminkan ketidakpastian model pada kasus-kasus yang memiliki karakteristik campuran. Threshold optimal (garis oranye putus-putus) terletak di titik yang memisahkan dua distribusi secara lebih tepat dibandingkan threshold default 0,50 (garis abu-abu).
m_final <- classification_metrics(test_data$dropout, p_test, threshold_opt)
ringkasan_final <- data.frame(
Metrik = c(
"AUC (Testing)",
"Threshold optimal (Indeks Youden)",
"Akurasi (threshold optimal)",
"Sensitivity — deteksi dropout (threshold optimal)",
"Specificity — identifikasi lulus (threshold optimal)",
"F1-score (threshold optimal)",
"Balanced accuracy (threshold optimal)"
),
Nilai = c(
round(auc_test, 3),
round(threshold_opt, 3),
round(m_final$accuracy, 3),
round(m_final$sensitivity, 3),
round(m_final$specificity, 3),
round(m_final$f1_score, 3),
round(m_final$balanced_accuracy, 3)
)
)
knitr::kable(ringkasan_final, caption = "Ringkasan performa model regresi logistik biner pada data testing")| Metrik | Nilai |
|---|---|
| AUC (Testing) | 0.945 |
| Threshold optimal (Indeks Youden) | 0.444 |
| Akurasi (threshold optimal) | 0.894 |
| Sensitivity — deteksi dropout (threshold optimal) | 0.842 |
| Specificity — identifikasi lulus (threshold optimal) | 0.928 |
| F1-score (threshold optimal) | 0.862 |
| Balanced accuracy (threshold optimal) | 0.885 |
Interpretasi ringkasan: Model regresi logistik biner yang dibangun berhasil memisahkan mahasiswa dropout dan lulus dengan performa yang sangat baik berdasarkan nilai AUC sebesar 0.945. Pada threshold optimal 0.444, model mencapai sensitivity 0.842 dan specificity 0.928, yang berarti model mampu mengidentifikasi 84.2% mahasiswa yang benar-benar dropout dan mengidentifikasi 92.8% mahasiswa yang benar-benar lulus dengan tepat.
Faktor-faktor yang paling menentukan status studi mahasiswa dalam model ini adalah: kinerja akademik semester awal (jumlah SKS yang lulus dan nilai rata-rata di semester 1 dan 2), kondisi finansial (biaya kuliah terbayar tepat waktu, ada tidaknya tunggakan, status penerima beasiswa), dan usia saat mendaftar. Ketiga dimensi ini—akademik, finansial, dan demografis—secara bersama-sama membentuk profil risiko putus studi yang komprehensif dan dapat digunakan sebagai dasar pengambilan keputusan intervensi akademik sejak dini.