library(MASS)        # lda()
library(tidyverse)   # data wrangling
library(ggplot2)     # visualisasi
library(car)         # leveneTest()
library(MVN)         # uji normalitas multivariat (mardia)
library(heplots)     # boxM() — Box's M test
library(caret)       # confusionMatrix
library(knitr)       # kable

Load Data

df_raw <- read.csv("data_mod4.csv", sep = ";")

cat("Dimensi data:", nrow(df_raw), "baris x", ncol(df_raw), "kolom\n")
## Dimensi data: 4424 baris x 37 kolom

Feature Selection

LDA hanya dapat menggunakan variabel prediktor numerik secara langsung. Variabel kategorik dikonversi menjadi numerik (dummy/integer).

df <- df_raw %>%
  dplyr::select(
    Target,
    Previous.qualification..grade.,
    Curricular.units.1st.sem..grade.,
    Curricular.units.2nd.sem..grade.,
    Curricular.units.1st.sem..approved.,
    Age.at.enrollment,
    Marital.status,
    Scholarship.holder,
    Daytime.evening.attendance.,
    Debtor,
    Gender
  )

colnames(df) <- c("Target",
                  "Prev_Grade", "Grade_Sem1", "Grade_Sem2",
                  "Approved_Sem1", "Age",
                  "Marital_Status", "Scholarship", "Attendance",
                  "Debtor", "Gender")

# Konversi Target ke factor
df$Target <- factor(df$Target, levels = c("Dropout", "Enrolled", "Graduate"))

cat("PREVIEW DATA ")
## PREVIEW DATA
print(head(df))
##     Target Prev_Grade Grade_Sem1 Grade_Sem2 Approved_Sem1 Age Marital_Status
## 1  Dropout      122.0    0.00000    0.00000             0  20              1
## 2 Graduate      160.0   14.00000   13.66667             6  19              1
## 3  Dropout      122.0    0.00000    0.00000             0  19              1
## 4 Graduate      122.0   13.42857   12.40000             6  20              1
## 5 Graduate      100.0   12.33333   13.00000             5  45              2
## 6 Graduate      133.1   11.85714   11.50000             5  50              2
##   Scholarship Attendance Debtor Gender
## 1           0          1      0      1
## 2           0          1      0      1
## 3           0          1      0      1
## 4           0          1      0      0
## 5           0          0      0      0
## 6           0          0      1      1
cat("DISTRIBUSI TARGET ")
## DISTRIBUSI TARGET
print(table(df$Target))
## 
##  Dropout Enrolled Graduate 
##     1421      794     2209
cat("PROPORSI TARGET ")
## PROPORSI TARGET
print(round(prop.table(table(df$Target)) * 100, 2))
## 
##  Dropout Enrolled Graduate 
##    32.12    17.95    49.93
barplot(table(df$Target),
        main = "Distribusi Kelas Target",
        xlab = "Status Mahasiswa",
        ylab = "Frekuensi",
        col  = c("#E74C3C", "#3498DB", "#2ECC71"),
        names.arg = c("Dropout", "Enrolled", "Graduate"))


Uji Asumsi

Analisis Diskriminan Linear memiliki tiga asumsi utama yang harus dipenuhi:

  1. Normalitas multivariat — setiap kelas harus berdistribusi normal multivariat
  2. Homogenitas matriks kovarians — matriks kovarians antar kelas harus homogen (Box’s M test)
  3. Tidak ada multikolinearitas — tidak ada korelasi tinggi antar prediktor (VIF)

1. Normalitas Multivariat (Uji Mardia)

Uji Mardia digunakan untuk mendeteksi normalitas multivariat berdasarkan skewness dan kurtosis multivariat.

Hipotesis:

  • H₀ : Data berdistribusi normal multivariat
  • H₁ : Data tidak berdistribusi normal multivariat
num_vars <- c("Prev_Grade", "Grade_Sem1", "Grade_Sem2", "Approved_Sem1", "Age")

hasil_mardia <- data.frame(
  Kelas     = character(),
  Skewness  = numeric(),
  p_Skew    = numeric(),
  Kurtosis  = numeric(),
  p_Kurt    = numeric(),
  Keputusan = character(),
  stringsAsFactors = FALSE
)

for (kelas in levels(df$Target)) {
  subset_data <- df[df$Target == kelas, num_vars]
  mv_test <- mvn(data = subset_data, mvn_test = "mardia")
  
  tbl <- mv_test$multivariate_normality
  
  skew_stat  <- as.numeric(tbl$Statistic[1])
  kurt_stat  <- as.numeric(tbl$Statistic[2])
  skew_p_num <- as.numeric(tbl$p.value[1])
  kurt_p_num <- as.numeric(tbl$p.value[2])
  
  keputusan <- ifelse(
  is.na(skew_p_num) | is.na(kurt_p_num) |
  skew_p_num < 0.05 | kurt_p_num < 0.05,
  "Tidak Normal Multivariat",
  "Normal Multivariat"
)
  
  hasil_mardia <- rbind(hasil_mardia, data.frame(
    Kelas     = kelas,
    Skewness  = round(skew_stat, 4),
    p_Skew    = round(skew_p_num, 4),
    Kurtosis  = round(kurt_stat, 4),
    p_Kurt    = round(kurt_p_num, 4),
    Keputusan = keputusan  
  ))
}

print(hasil_mardia, row.names = FALSE)
##     Kelas  Skewness p_Skew Kurtosis p_Kurt                Keputusan
##   Dropout  3065.634     NA   36.875     NA Tidak Normal Multivariat
##  Enrolled  2505.339     NA   58.361     NA Tidak Normal Multivariat
##  Graduate 13608.783     NA  177.627     NA Tidak Normal Multivariat

Apabila p-value uji Mardia < 0,05 maka asumsi normalitas multivariat tidak terpenuhi. Meskipun demikian, LDA dikenal cukup robust terhadap pelanggaran normalitas multivariat apabila ukuran sampel tiap kelas cukup besar (n > 30), sehingga analisis dapat tetap dilanjutkan.


2. Homogenitas Matriks Kovarians (Box’s M Test)

Box’s M test digunakan untuk menguji apakah matriks kovarians antar kelas bersifat homogen — asumsi kunci LDA.

Hipotesis:

  • H₀ : Matriks kovarians antar kelas homogen (Σ₁ = Σ₂ = Σ₃)
  • H₁ : Minimal ada satu matriks kovarians yang berbeda
boxM_result <- boxM(df[, num_vars], df$Target)
print(boxM_result)
## 
##  Box's M-test for Homogeneity of Covariance Matrices 
## 
## data:  df[, num_vars] by df$Target 
## Chi-Sq (approx.) = 4806.255, df = 30, p-value = < 2.2e-16
cat(sprintf("\nChi-Square hitung : %.4f\n", boxM_result$statistic))
## 
## Chi-Square hitung : 4806.2553
cat(sprintf("Derajat bebas     : %d\n", boxM_result$parameter))
## Derajat bebas     : 30
cat(sprintf("P-value           : %.6f\n", boxM_result$p.value))
## P-value           : 0.000000
cat(sprintf("Keputusan         : %s\n",
            ifelse(boxM_result$p.value < 0.05,
                   "Tolak H0 — Matriks kovarians tidak homogen",
                   "Gagal Tolak H0 — Matriks kovarians homogen")))
## Keputusan         : Tolak H0 — Matriks kovarians tidak homogen

Apabila Box’s M test signifikan (p-value < 0,05), asumsi homogenitas matriks kovarians tidak terpenuhi. Dalam kasus ini, Quadratic Discriminant Analysis (QDA) lebih tepat digunakan sebagai alternatif LDA karena QDA tidak mengasumsikan kesamaan matriks kovarians antar kelas. Namun untuk tujuan pembelajaran, LDA tetap dijalankan.


3. Multikolinearitas (VIF)

model_lm <- lm(as.numeric(Target) ~ Prev_Grade + Grade_Sem1 + Grade_Sem2 +
                 Approved_Sem1 + Age, data = df)

vif_result <- vif(model_lm)
cat("NILAI VIF ")
## NILAI VIF
print(round(vif_result, 3))
##    Prev_Grade    Grade_Sem1    Grade_Sem2 Approved_Sem1           Age 
##         1.015         3.701         3.627         2.106         1.054
cat(sprintf("Kesimpulan: %s\n",
            ifelse(all(vif_result < 10),
                   "Tidak ada multikolinearitas (semua VIF < 10)",
                   "Terdeteksi multikolinearitas pada beberapa variabel")))
## Kesimpulan: Tidak ada multikolinearitas (semua VIF < 10)

Nilai VIF > 10 mengindikasikan adanya multikolinearitas yang perlu ditangani. Jika semua VIF < 10, asumsi ini terpenuhi dan seluruh prediktor dapat dimasukkan ke dalam model.


Pemodelan Analisis Diskriminan Linear (LDA)

Model LDA dibangun menggunakan fungsi lda() dari package MASS. LDA mencari kombinasi linear dari variabel prediktor yang memaksimalkan separasi antar kelas.

model_lda <- lda(
  Target ~ Prev_Grade + Grade_Sem1 + Grade_Sem2 + Approved_Sem1 + Age +
           Marital_Status + Scholarship + Attendance + Debtor + Gender,
  data = df
)

cat("HASIL MODEL LDA ")
## HASIL MODEL LDA
cat("Prior Probabilities (Proporsi tiap kelas) ")
## Prior Probabilities (Proporsi tiap kelas)
print(round(model_lda$prior, 4))
##  Dropout Enrolled Graduate 
##   0.3212   0.1795   0.4993
cat("Group Means (Rata-rata tiap kelas per prediktor) ")
## Group Means (Rata-rata tiap kelas per prediktor)
print(round(model_lda$means, 4))
##          Prev_Grade Grade_Sem1 Grade_Sem2 Approved_Sem1     Age Marital_Status
## Dropout    131.1141     7.2567     5.8993        2.5517 26.0690         1.2611
## Enrolled   131.2084    11.1253    11.1174        4.3186 22.3690         1.1524
## Graduate   134.0827    12.6437    12.6973        6.2322 21.7836         1.1349
##          Scholarship Attendance Debtor Gender
## Dropout       0.0943     0.8543 0.2196 0.4933
## Enrolled      0.1637     0.9055 0.1134 0.3866
## Graduate      0.3780     0.9090 0.0457 0.2481
cat("Koefisien Fungsi Diskriminan ")
## Koefisien Fungsi Diskriminan
print(round(model_lda$scaling, 4))
##                    LD1     LD2
## Prev_Grade      0.0061  0.0187
## Grade_Sem1     -0.0385 -0.0236
## Grade_Sem2      0.1498 -0.1962
## Approved_Sem1   0.1732  0.3081
## Age            -0.0392  0.0406
## Marital_Status  0.0776 -0.1429
## Scholarship     0.6846  1.1669
## Attendance     -0.1185 -0.0143
## Debtor         -0.8902 -0.0871
## Gender         -0.2601 -0.3389
cat(" Proporsi Trace (Variansi yang dijelaskan tiap fungsi) ")
##  Proporsi Trace (Variansi yang dijelaskan tiap fungsi)
prop_trace <- model_lda$svd^2 / sum(model_lda$svd^2)
for (i in seq_along(prop_trace)) {
  cat(sprintf("  LD%d : %.4f (%.2f%%)\n", i, prop_trace[i], prop_trace[i] * 100))
}
##   LD1 : 0.9424 (94.24%)
##   LD2 : 0.0576 (5.76%)

LDA dengan 3 kelas menghasilkan 2 fungsi diskriminan (LD1 dan LD2), karena jumlah fungsi = min(jumlah kelas − 1, jumlah prediktor). Proporsi trace menunjukkan seberapa besar variansi antar kelas yang dijelaskan oleh masing-masing fungsi diskriminan.


Visualisasi Fungsi Diskriminan

# Proyeksikan data ke ruang diskriminan
lda_pred_train <- predict(model_lda, df)
lda_scores     <- as.data.frame(lda_pred_train$x)
lda_scores$Target <- df$Target

# Plot LD1 vs LD2
ggplot(lda_scores, aes(x = LD1, y = LD2, color = Target)) +
  geom_point(alpha = 0.4, size = 1.5) +
  stat_ellipse(level = 0.90, linewidth = 1) +
  scale_color_manual(values = c("#E74C3C", "#3498DB", "#2ECC71")) +
  labs(title = "Proyeksi LDA: LD1 vs LD2",
       x = "Fungsi Diskriminan 1 (LD1)",
       y = "Fungsi Diskriminan 2 (LD2)",
       color = "Status Mahasiswa") +
  theme_minimal(base_size = 13)

# Distribusi LD1 per kelas
ggplot(lda_scores, aes(x = LD1, fill = Target)) +
  geom_density(alpha = 0.5) +
  scale_fill_manual(values = c("#E74C3C", "#3498DB", "#2ECC71")) +
  labs(title = "Distribusi Skor LD1 per Kelas",
       x = "Skor LD1", y = "Densitas",
       fill = "Status Mahasiswa") +
  theme_minimal(base_size = 13)

Plot LD1 vs LD2 menunjukkan sejauh mana ketiga kelas dapat dipisahkan di ruang diskriminan. Ellips 90% menggambarkan sebaran tiap kelas. Semakin sedikit tumpang tindih antar ellips, semakin baik kemampuan separasi model.


Evaluasi Model

Confusion Matrix dan Akurasi

# Prediksi pada data training
pred_class <- lda_pred_train$class

# Confusion Matrix
cm <- confusionMatrix(pred_class, df$Target)
print(cm)
## Confusion Matrix and Statistics
## 
##           Reference
## Prediction Dropout Enrolled Graduate
##   Dropout      917      137      121
##   Enrolled     125      108       38
##   Graduate     379      549     2050
## 
## Overall Statistics
##                                           
##                Accuracy : 0.6951          
##                  95% CI : (0.6813, 0.7086)
##     No Information Rate : 0.4993          
##     P-Value [Acc > NIR] : < 2.2e-16       
##                                           
##                   Kappa : 0.4628          
##                                           
##  Mcnemar's Test P-Value : < 2.2e-16       
## 
## Statistics by Class:
## 
##                      Class: Dropout Class: Enrolled Class: Graduate
## Sensitivity                  0.6453         0.13602          0.9280
## Specificity                  0.9141         0.95510          0.5810
## Pos Pred Value               0.7804         0.39852          0.6884
## Neg Pred Value               0.8449         0.83482          0.8900
## Prevalence                   0.3212         0.17948          0.4993
## Detection Rate               0.2073         0.02441          0.4634
## Detection Prevalence         0.2656         0.06126          0.6731
## Balanced Accuracy            0.7797         0.54556          0.7545
cat("RINGKASAN EVALUASI ")
## RINGKASAN EVALUASI
cat(sprintf("Akurasi keseluruhan : %.4f (%.2f%%)\n",
            cm$overall["Accuracy"], cm$overall["Accuracy"] * 100))
## Akurasi keseluruhan : 0.6951 (69.51%)
cat(sprintf("Kappa               : %.4f\n", cm$overall["Kappa"]))
## Kappa               : 0.4628
cm_table <- as.data.frame(cm$table)
colnames(cm_table) <- c("Prediksi", "Aktual", "Frekuensi")

ggplot(cm_table, aes(x = Aktual, y = Prediksi, fill = Frekuensi)) +
  geom_tile(color = "white") +
  geom_text(aes(label = Frekuensi), size = 5, fontface = "bold") +
  scale_fill_gradient(low = "#D6EAF8", high = "#1A5276") +
  labs(title = "Confusion Matrix - LDA",
       x = "Kelas Aktual", y = "Kelas Prediksi") +
  theme_minimal(base_size = 13)


Kesimpulan

prop_trace <- model_lda$svd^2 / sum(model_lda$svd^2)

cat(" RINGKASAN HASIL ANALISIS DISKRIMINAN LINEAR ")
##  RINGKASAN HASIL ANALISIS DISKRIMINAN LINEAR
cat("1. UJI ASUMSI\n")
## 1. UJI ASUMSI
cat("   - Normalitas Multivariat : Dievaluasi per kelas menggunakan Uji Mardia\n")
##    - Normalitas Multivariat : Dievaluasi per kelas menggunakan Uji Mardia
cat("   - Homogenitas Kovarians  : Dievaluasi menggunakan Box's M Test\n")
##    - Homogenitas Kovarians  : Dievaluasi menggunakan Box's M Test
cat("   - Multikolinearitas      : Dievaluasi menggunakan VIF\n\n")
##    - Multikolinearitas      : Dievaluasi menggunakan VIF
cat("2. FUNGSI DISKRIMINAN\n")
## 2. FUNGSI DISKRIMINAN
cat(sprintf("   Jumlah fungsi diskriminan : %d\n", length(model_lda$svd)))
##    Jumlah fungsi diskriminan : 2
for (i in seq_along(prop_trace)) {
  cat(sprintf("   LD%d menjelaskan : %.2f%% variansi antar kelas\n",
              i, prop_trace[i] * 100))
}
##    LD1 menjelaskan : 94.24% variansi antar kelas
##    LD2 menjelaskan : 5.76% variansi antar kelas
cat("\n")
cat("3. EVALUASI MODEL\n")
## 3. EVALUASI MODEL
cat(sprintf("   Akurasi : %.2f%%\n", cm$overall["Accuracy"] * 100))
##    Akurasi : 69.51%
cat(sprintf("   Kappa   : %.4f\n", cm$overall["Kappa"]))
##    Kappa   : 0.4628
cat(sprintf("   Interpretasi Kappa: %s\n",
            ifelse(cm$overall["Kappa"] >= 0.8, "Sangat Baik",
            ifelse(cm$overall["Kappa"] >= 0.6, "Baik (Substantial)",
            ifelse(cm$overall["Kappa"] >= 0.4, "Sedang (Moderate)",
                                               "Lemah")))))
##    Interpretasi Kappa: Sedang (Moderate)

Analisis Diskriminan Linear berhasil membangun fungsi diskriminan yang memisahkan status mahasiswa (Dropout, Enrolled, Graduate) berdasarkan variabel prediktor akademik dan demografis. Fungsi LD1 menjelaskan sebagian besar variansi antar kelas. Nilai akurasi dan Kappa mencerminkan kemampuan model dalam mengklasifikasikan data secara keseluruhan.