Analisis Cluster pada Data Mahasiswa Berdasarkan Nilai Akademik dan Kehadiran

Dalam konteks evaluasi akademik, mahasiswa tidak selalu hanya berbeda pada nilai ujian, tetapi juga pada pola kehadiran, partisipasi praktikum, dan performa akademik sebelumnya. Oleh karena itu, pendekatan clustering dapat digunakan untuk mengidentifikasi kelompok mahasiswa dengan karakteristik yang mirip, sehingga dapat membantu memahami pola performa belajar secara lebih menyeluruh.

Analisis ini bertujuan untuk mengelompokkan mahasiswa berdasarkan karakteristik performa akademik dan partisipasi pembelajaran, menggunakan beberapa metode clustering agar dapat dibandingkan metode mana yang paling sesuai dengan struktur data.

LOAD DATA

# Cek apakah file sudah ada
if (!file.exists("student_data.csv")) {
  file_id <- "1Mzqami9CEzT8nGVMV323fmk0Q-mYKCpd"
  url <- paste0("https://drive.google.com/uc?id=", file_id)

  download.file(url, destfile = "student_data.csv", mode = "wb")
}

# Load data
df <- read.csv("student_data.csv")

# Validasi
if (nrow(df) == 0) {
  stop("Data gagal dimuat!")
}

head(df)
##   student_id             name age gender quiz1_marks quiz2_marks quiz3_marks
## 1          1 Kristina Vaughan  19   Male         8.0         5.7         7.4
## 2          2   Rodney Daniels  21   Male        10.0         7.9         4.1
## 3          3        Jose Nash  19 Female         7.5         1.2         0.3
## 4          4    Nicole Martin  21   Male         5.2         2.5         9.9
## 5          5     Shelby Smith  21 Female         5.9         6.3         2.0
## 6          6   Austin Griffin  22   Male         5.8        10.0         5.5
##   total_assignments assignments_submitted midterm_marks final_marks
## 1                 5                    NA          30.0        36.5
## 2                 5                    NA          25.4        33.0
## 3                 5                    NA          14.4        24.8
## 4                 5                    NA          17.7        41.0
## 5                 5                    NA          23.8        31.0
## 6                 5                    NA           9.4        50.0
##   previous_gpa total_lectures lectures_attended total_lab_sessions
## 1         2.57             12                 4                  6
## 2         2.40             12                 1                  6
## 3         2.99             12                 0                  6
## 4         1.68             12                 9                  6
## 5         2.53             12                 7                  6
## 6         1.83             12                11                  6
##   labs_attended
## 1             1
## 2             5
## 3             0
## 4             0
## 5             4
## 6             3

LOAD LIBRARY

library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr     1.2.1     ✔ readr     2.2.0
## ✔ forcats   1.0.1     ✔ stringr   1.6.0
## ✔ ggplot2   4.0.2     ✔ tibble    3.3.1
## ✔ lubridate 1.9.5     ✔ tidyr     1.3.2
## ✔ purrr     1.2.1     
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(GGally)
library(corrplot)
## corrplot 0.95 loaded
library(skimr)
library(flexclust)
library(dbscan)
## 
## Attaching package: 'dbscan'
## 
## The following object is masked from 'package:stats':
## 
##     as.dendrogram
library(meanShiftR)
library(e1071)
## 
## Attaching package: 'e1071'
## 
## The following object is masked from 'package:flexclust':
## 
##     bclust
## 
## The following object is masked from 'package:ggplot2':
## 
##     element
library(cluster)
library(fpc)
## 
## Attaching package: 'fpc'
## 
## The following object is masked from 'package:dbscan':
## 
##     dbscan
library(mclust)
## Package 'mclust' version 6.1.2
## Type 'citation("mclust")' for citing this R package in publications.
## 
## Attaching package: 'mclust'
## 
## The following object is masked from 'package:dplyr':
## 
##     count
## 
## The following object is masked from 'package:purrr':
## 
##     map
library(rsconnect)

EXPLORASI DATA

EDA awal

# Struktur data
str(df)
## 'data.frame':    300 obs. of  16 variables:
##  $ student_id           : int  1 2 3 4 5 6 7 8 9 10 ...
##  $ name                 : chr  "Kristina Vaughan" "Rodney Daniels" "Jose Nash" "Nicole Martin" ...
##  $ age                  : int  19 21 19 21 21 22 23 23 23 18 ...
##  $ gender               : chr  "Male" "Male" "Female" "Male" ...
##  $ quiz1_marks          : num  8 10 7.5 5.2 5.9 5.8 7.4 6.8 7.7 9.1 ...
##  $ quiz2_marks          : num  5.7 7.9 1.2 2.5 6.3 10 1.1 5.2 1.6 8.3 ...
##  $ quiz3_marks          : num  7.4 4.1 0.3 9.9 2 5.5 1.5 1.1 6.5 3 ...
##  $ total_assignments    : int  5 5 5 5 5 5 5 5 5 5 ...
##  $ assignments_submitted: logi  NA NA NA NA NA NA ...
##  $ midterm_marks        : num  30 25.4 14.4 17.7 23.8 9.4 22 12.8 16.1 16.9 ...
##  $ final_marks          : num  36.5 33 24.8 41 31 50 50 33.1 29.8 45 ...
##  $ previous_gpa         : num  2.57 2.4 2.99 1.68 2.53 1.83 2.88 3.54 3.21 3.48 ...
##  $ total_lectures       : int  12 12 12 12 12 12 12 12 12 12 ...
##  $ lectures_attended    : int  4 1 0 9 7 11 3 6 4 1 ...
##  $ total_lab_sessions   : int  6 6 6 6 6 6 6 6 6 6 ...
##  $ labs_attended        : int  1 5 0 0 4 3 6 0 6 3 ...
# Statistik awal
summary(df)
##    student_id         name                age           gender         
##  Min.   :  1.00   Length:300         Min.   :18.00   Length:300        
##  1st Qu.: 75.75   Class :character   1st Qu.:20.00   Class :character  
##  Median :150.50   Mode  :character   Median :22.00   Mode  :character  
##  Mean   :150.50                      Mean   :21.55                     
##  3rd Qu.:225.25                      3rd Qu.:23.00                     
##  Max.   :300.00                      Max.   :25.00                     
##   quiz1_marks      quiz2_marks      quiz3_marks     total_assignments
##  Min.   : 2.800   Min.   : 0.000   Min.   : 0.000   Min.   :5        
##  1st Qu.: 6.075   1st Qu.: 4.000   1st Qu.: 3.600   1st Qu.:5        
##  Median : 7.300   Median : 5.800   Median : 5.600   Median :5        
##  Mean   : 7.255   Mean   : 5.867   Mean   : 5.469   Mean   :5        
##  3rd Qu.: 8.400   3rd Qu.: 7.700   3rd Qu.: 7.200   3rd Qu.:5        
##  Max.   :10.000   Max.   :10.000   Max.   :10.000   Max.   :5        
##  assignments_submitted midterm_marks    final_marks     previous_gpa  
##  Mode:logical          Min.   : 0.00   Min.   : 0.00   Min.   :0.610  
##  NA's:300              1st Qu.:14.22   1st Qu.:29.88   1st Qu.:2.328  
##                        Median :21.35   Median :39.60   Median :2.800  
##                        Mean   :19.97   Mean   :37.38   Mean   :2.806  
##                        3rd Qu.:27.43   3rd Qu.:49.55   3rd Qu.:3.320  
##                        Max.   :30.00   Max.   :50.00   Max.   :4.000  
##  total_lectures lectures_attended total_lab_sessions labs_attended 
##  Min.   :12     Min.   : 0.000    Min.   :6          Min.   :0.00  
##  1st Qu.:12     1st Qu.: 3.000    1st Qu.:6          1st Qu.:1.00  
##  Median :12     Median : 6.000    Median :6          Median :3.00  
##  Mean   :12     Mean   : 6.013    Mean   :6          Mean   :3.11  
##  3rd Qu.:12     3rd Qu.: 9.000    3rd Qu.:6          3rd Qu.:5.00  
##  Max.   :12     Max.   :12.000    Max.   :6          Max.   :6.00
# Missing value
colSums(is.na(df))
##            student_id                  name                   age 
##                     0                     0                     0 
##                gender           quiz1_marks           quiz2_marks 
##                     0                     0                     0 
##           quiz3_marks     total_assignments assignments_submitted 
##                     0                     0                   300 
##         midterm_marks           final_marks          previous_gpa 
##                     0                     0                     0 
##        total_lectures     lectures_attended    total_lab_sessions 
##                     0                     0                     0 
##         labs_attended 
##                     0

Interpretasi

Berdasarkan pemeriksaan struktur data, dataset memiliki 300 mahasiswa dan 16 variabel. Namun, tidak semua variabel layak langsung digunakan dalam analisis clustering. Variabel seperti student_id dan name hanya berfungsi sebagai identitas, sehingga tidak memiliki makna analitis dalam pembentukan kelompok. Selain itu, variabel assignments_submitted memiliki nilai hilang pada seluruh observasi, sehingga tidak dapat digunakan dalam analisis lanjutan.

Preprocessing & Feature Engineering

# preprocessing
df_clean <- df %>%
  mutate(
    attendance_rate = lectures_attended / total_lectures,
    lab_rate = labs_attended / total_lab_sessions
  ) %>%
  select(
    age,
    quiz1_marks, quiz2_marks, quiz3_marks,
    midterm_marks, final_marks,
    previous_gpa,
    attendance_rate,
    lab_rate
  )

colnames(df_clean)
## [1] "age"             "quiz1_marks"     "quiz2_marks"     "quiz3_marks"    
## [5] "midterm_marks"   "final_marks"     "previous_gpa"    "attendance_rate"
## [9] "lab_rate"
# hapus kolom konstan
sd_check <- sapply(df_clean, function(x) sd(x, na.rm = TRUE))
sd_check[is.na(sd_check)] <- 0

important_cols <- c("attendance_rate", "lab_rate")

sd_check <- sapply(df_clean, function(x) sd(x, na.rm = TRUE))
sd_check[is.na(sd_check)] <- 0

valid_cols <- (sd_check > 0) | (names(sd_check) %in% important_cols)

df_clean <- df_clean[, valid_cols]

# validasi
print(colnames(df_clean))
## [1] "age"             "quiz1_marks"     "quiz2_marks"     "quiz3_marks"    
## [5] "midterm_marks"   "final_marks"     "previous_gpa"    "attendance_rate"
## [9] "lab_rate"
print(dim(df_clean))
## [1] 300   9
if (ncol(df_clean) == 0) {
  stop("Data kosong setelah preprocessing!")
}


colSums(is.na(df_clean))
##             age     quiz1_marks     quiz2_marks     quiz3_marks   midterm_marks 
##               0               0               0               0               0 
##     final_marks    previous_gpa attendance_rate        lab_rate 
##               0               0               0               0

Interpretasi

Pada tahap preprocessing, variabel identitas seperti student_id dan name tidak digunakan karena tidak mencerminkan karakteristik akademik mahasiswa. Variabel kategorik gender juga tidak dimasukkan ke dalam clustering utama karena metode yang digunakan berbasis jarak Euclidean dan lebih sesuai untuk data numerik. Variabel total seperti total_lectures dan total_lab_sessions tidak digunakan karena nilainya konstan untuk seluruh mahasiswa, sehingga tidak memiliki variasi yang dapat membantu proses pengelompokan. Sebagai gantinya, dibuat fitur turunan berupa attendance_rate dan lab_rate agar tingkat partisipasi mahasiswa dapat direpresentasikan dengan lebih proporsional.

EDA lanjutan

distribusi

Alasan

Digunakan untuk melihat bentuk distribusi masing-masing variabel, apakah cenderung simetris, miring, atau memiliki penumpukan pada nilai tertentu. Informasi ini penting karena beberapa metode clustering sensitif terhadap bentuk distribusi dan keberadaan data ekstrem.

df_clean %>%
  pivot_longer(cols = everything()) %>%
  ggplot(aes(x = value)) +
  geom_histogram(bins = 20, fill = "skyblue") +
  facet_wrap(~name, scales = "free")

Interpretasi

  • age tampak tersebar pada rentang sempit dan relatif merata,
  • attendance_rate dan lab_rate bersifat diskret/bertingkat karena berasal dari pembagian jumlah hadir dengan total sesi tetap,
  • final_marks tampak menumpuk di nilai tinggi,
  • beberapa nilai kuis dan ujian terlihat menyebar cukup lebar.

Temuan ini menunjukkan bahwa data memiliki variasi antarmahasiswa yang cukup untuk dianalisis lebih lanjut dengan clustering, meskipun bentuk distribusinya tidak sepenuhnya simetris.

deteksi outlier

Alasan

Digunakan untuk mendeteksi kemungkinan outlier. Dalam clustering, outlier dapat menarik pusat cluster dan membuat hasil pengelompokan menjadi kurang stabil, terutama pada metode K-Means.

df_clean %>%
  pivot_longer(cols = everything()) %>%
  ggplot(aes(y = value)) +
  geom_boxplot(fill = "orange") +
  facet_wrap(~name, scales = "free")

Interpretasi

Boxplot menunjukkan bahwa beberapa variabel memiliki nilai yang berada di luar rentang kuartil, namun secara umum tidak tampak outlier ekstrem dalam jumlah besar. struktur data masih relatif wajar untuk dianalisis menggunakan metode clustering. Namun tetap perlu diperhatikan karena metode seperti K-Means cukup sensitif terhadap outlier.

korelasi antar fitur

Alasan

Digunakan untuk melihat kekuatan hubungan linear antarvariabel. Jika ada dua variabel yang terlalu berkorelasi tinggi, maka keduanya bisa memberi informasi yang tumpang tindih dalam pembentukan cluster.

cor_matrix <- cor(df_clean)
corrplot(cor_matrix, method = "color", tl.cex = 0.8)

nilai <- df_clean %>%
  select(quiz1_marks, quiz2_marks, quiz3_marks, midterm_marks, final_marks, previous_gpa, attendance_rate, lab_rate)

cor(nilai)
##                  quiz1_marks  quiz2_marks  quiz3_marks midterm_marks
## quiz1_marks      1.000000000 -0.043697917 -0.053417101   0.002247129
## quiz2_marks     -0.043697917  1.000000000 -0.018054015  -0.057367460
## quiz3_marks     -0.053417101 -0.018054015  1.000000000  -0.021707029
## midterm_marks    0.002247129 -0.057367460 -0.021707029   1.000000000
## final_marks      0.035708710  0.172477432  0.085784724  -0.063032977
## previous_gpa     0.077042642 -0.105887553  0.001000300   0.013604204
## attendance_rate -0.142728255 -0.002700816  0.006231715   0.012045722
## lab_rate         0.004496065 -0.004400874  0.046679767  -0.019408259
##                 final_marks previous_gpa attendance_rate     lab_rate
## quiz1_marks      0.03570871   0.07704264    -0.142728255  0.004496065
## quiz2_marks      0.17247743  -0.10588755    -0.002700816 -0.004400874
## quiz3_marks      0.08578472   0.00100030     0.006231715  0.046679767
## midterm_marks   -0.06303298   0.01360420     0.012045722 -0.019408259
## final_marks      1.00000000   0.10108920    -0.018898478  0.108077448
## previous_gpa     0.10108920   1.00000000    -0.022005049 -0.038860102
## attendance_rate -0.01889848  -0.02200505     1.000000000 -0.017853515
## lab_rate         0.10807745  -0.03886010    -0.017853515  1.000000000

Interpretasi

Matriks korelasi menunjukkan bahwa sebagian besar pasangan variabel memiliki hubungan linear yang lemah. Nilai korelasi yang muncul umumnya mendekati nol, dan tidak ada pasangan variabel dengan korelasi tinggi yang mengindikasikan redundansi kuat. Artinya, performa mahasiswa pada satu jenis penilaian belum tentu sejalan dengan performa pada penilaian lainnya.

hubungan attendance vs GPA

Alasan

Digunakan untuk mengecek apakah ada pola hubungan sederhana antara kehadiran dan prestasi akademik sebelumnya.

ggplot(df_clean, aes(x = attendance_rate, y = previous_gpa)) +
  geom_point(alpha = 0.6) +
  geom_smooth(method = "lm")
## `geom_smooth()` using formula = 'y ~ x'

Interpretasi

Scatter plot antara attendance_rate dan previous_gpa tidak menunjukkan pola linear yang kuat. Garis regresi yang hampir datar mengindikasikan bahwa tingkat kehadiran pada data ini tidak memiliki hubungan yang jelas dengan GPA sebelumnya. Artinya, kehadiran kemungkinan menjadi dimensi yang relatif independen dalam proses pembentukan cluster.

multivariate analysis

Alasan

Digunakan sebagai eksplorasi multivariat awal untuk melihat pola hubungan pasangan variabel secara lebih menyeluruh.

ggpairs(df_clean)

Interpretasi

  • sebaran hubungan antarvariabel cenderung menyebar,
  • tidak ada pemisahan visual yang sangat jelas,
  • pola cluster belum langsung tampak dari pasangan variabel sederhana.

Karena itu, penggunaan metode clustering multivariat menjadi relevan.

SCALING

Standardisasi dilakukan agar seluruh variabel memiliki skala yang sebanding. Langkah ini penting karena rentang nilai antarvariabel berbeda, misalnya final_marks berada pada skala 0–50, sedangkan previous_gpa berada pada skala sekitar 0–4. Tanpa scaling, variabel dengan rentang yang lebih besar akan lebih dominan dalam perhitungan jarak, sehingga hasil clustering dapat menjadi bias.

# scaling data
df_scaled <- scale(df_clean)

# cek hasil scaling
head(df_scaled)
##             age quiz1_marks quiz2_marks quiz3_marks midterm_marks final_marks
## [1,] -1.1376395   0.4624255 -0.06901922  0.71226143     1.2126115 -0.07417282
## [2,] -0.2453732   1.7032809  0.84203448 -0.50496421     0.6565341 -0.37086409
## [3,] -1.1376395   0.1522116 -1.93253816 -1.90661797    -0.6732164 -1.06596935
## [4,] -0.2453732  -1.2747722 -1.39418824  1.63440206    -0.2742912  0.30728739
## [5,] -0.2453732  -0.8404728  0.17944997 -1.27956234     0.4631158 -0.54040196
## [6,]  0.2007599  -0.9025155  1.71167666  0.01143454    -1.2776484  1.07020780
##      previous_gpa attendance_rate    lab_rate
## [1,]   -0.3221446      -0.5402881 -1.06421836
## [2,]   -0.5541652      -1.3453531  0.95325721
## [3,]    0.2510826      -1.6137081 -1.56858726
## [4,]   -1.5368405       0.8014870 -1.56858726
## [5,]   -0.3767377       0.2647769  0.44888831
## [6,]   -1.3321164       1.3381970 -0.05548058
colMeans(df_scaled)
##             age     quiz1_marks     quiz2_marks     quiz3_marks   midterm_marks 
##   -3.160435e-16   -2.476029e-16    1.879515e-16   -1.211647e-16   -1.378643e-16 
##     final_marks    previous_gpa attendance_rate        lab_rate 
##   -2.798687e-18    2.496036e-16    1.618887e-16    2.553513e-17
apply(df_scaled, 2, sd)
##             age     quiz1_marks     quiz2_marks     quiz3_marks   midterm_marks 
##               1               1               1               1               1 
##     final_marks    previous_gpa attendance_rate        lab_rate 
##               1               1               1               1

Interpretasi

Standardisasi dilakukan agar seluruh variabel berada pada skala yang sebanding. Langkah ini penting karena variabel seperti final_marks memiliki rentang nilai yang jauh lebih besar dibandingkan previous_gpa atau attendance_rate. Tanpa scaling, variabel dengan rentang besar akan mendominasi perhitungan jarak dan membuat hasil clustering menjadi bias. Hasil pengecekan menunjukkan bahwa rata-rata variabel hasil scaling mendekati 0 dan simpangan bakunya mendekati 1, sehingga standardisasi berhasil dilakukan.

PCA (struktur data)

df_scaled <- scale(na.omit(df_clean))

pca <- prcomp(df_scaled)

summary(pca)
## Importance of components:
##                           PC1    PC2    PC3    PC4    PC5    PC6    PC7     PC8
## Standard deviation     1.1211 1.1018 1.0379 1.0266 1.0073 0.9844 0.9494 0.90884
## Proportion of Variance 0.1396 0.1349 0.1197 0.1171 0.1127 0.1077 0.1002 0.09178
## Cumulative Proportion  0.1396 0.2745 0.3942 0.5113 0.6241 0.7318 0.8319 0.92369
##                            PC9
## Standard deviation     0.82874
## Proportion of Variance 0.07631
## Cumulative Proportion  1.00000
pca_df <- as.data.frame(pca$x)

ggplot(pca_df, aes(PC1, PC2)) +
  geom_point(alpha = 0.6)

Interpretasi

CLUSTERING

K-means dan K-median

Penentuan jumlah cluster

Elbow Method

wss <- sapply(1:10, function(k){
  kmeans(df_scaled, centers = k, nstart = 20)$tot.withinss
})

par(mfrow = c(1, 1))
plot(1:10, wss, type = "b", pch = 19, frame = FALSE,
     xlab = "Number of clusters K",
     ylab = "Total within-clusters sum of squares",
     main = "Elbow Method")

Interpretasi

Pada data ini, titik siku tidak terlihat sangat tegas, sehingga Elbow Method perlu dipertimbangkan bersama ukuran lain, yaitu silhouette.

Silhouette Analysis

set.seed(42)

avg_sil <- function(k) {
  kmeans_model <- kmeans(df_scaled, centers = k, nstart = 25)
  ss <- silhouette(kmeans_model$cluster, dist(df_scaled))
  mean(ss[, 3])
}

k_values <- 2:10
avg_sil_values <- sapply(k_values, avg_sil)

par(mfrow = c(1, 1))
plot(k_values, avg_sil_values, type = "b", pch = 19, frame = FALSE,
     xlab = "Number of clusters K",
     ylab = "Average Silhouette Width",
     main = "Silhouette Analysis")

Interpretasi

Berdasarkan silhouette, jumlah cluster terbaik adalah yang memiliki nilai silhouette rata-rata tertinggi, kalau dilihat dari grafik puncaknya sekitar 7.

k <- 7

kmeans_model <- kmeans(df_scaled, centers = k, nstart = 25)
kmedian_model <- pam(df_scaled, k = k)

sil_kmeans <- silhouette(kmeans_model$cluster, dist(df_scaled))
mean(sil_kmeans[,3])
## [1] 0.1051238
sil_kmedian <- silhouette(kmedian_model$cluster, dist(df_scaled))
mean(sil_kmedian[,3])
## [1] 0.06714033

Interpretasi

  • Nilai silhouette rata-rata masih relatif rendah.
  • Hal ini menunjukkan bahwa pemisahan antarcluster belum kuat dan masih terjadi tumpang tindih antar data.
  • Nilai silhouette K-Means lebih tinggi dibanding K-Median (PAM).
  • Ini menunjukkan bahwa K-Means menghasilkan cluster yang lebih kompak dan lebih terpisah.
  • Meskipun K-Median lebih tahan terhadap outlier, pada dataset ini tidak memberikan kualitas cluster yang lebih baik.

1. K-Means

k <- 7
kmeans_model <- kmeans(df_scaled, centers = k, nstart = 25)

Simpan Hasil Assignment Cluster ke dataframe asli

df_features <- as.data.frame(df_scaled)
df_features$cluster_kmeans <- as.factor(kmeans_model$cluster)

Melihat jumlah mahasiswa di tiap Cluster

table(df_features$cluster_kmeans)
## 
##  1  2  3  4  5  6  7 
## 47 37 28 46 48 44 50

Mekanisme: Algoritma ini mengoptimalkan fungsi objektif \(J = \sum_{i=1}^{k} \sum_{x \in C_i} ||x - \mu_i||^2\).

aggregate(df_clean, by = list(cluster = kmeans_model$cluster), mean)
##   cluster      age quiz1_marks quiz2_marks quiz3_marks midterm_marks
## 1       1 22.65957    8.238298    6.987234    3.648936     22.104255
## 2       2 22.00000    5.562162    7.624324    6.516216     18.500000
## 3       3 19.71429    7.357143    5.914286    3.557143     13.235714
## 4       4 20.95652    7.678261    3.882609    4.193478     25.334783
## 5       5 21.04167    7.381250    4.331250    8.368750     20.560417
## 6       6 22.34091    7.050000    5.956818    5.620455      9.827273
## 7       7 21.54000    7.194000    6.706000    5.732000     26.240000
##   final_marks previous_gpa attendance_rate  lab_rate
## 1    42.25957     2.591489       0.1897163 0.6276596
## 2    31.17027     2.342973       0.5653153 0.2072072
## 3    34.90000     3.535357       0.3244048 0.2678571
## 4    22.08913     2.534565       0.5760870 0.4528986
## 5    41.65833     3.082500       0.2656250 0.6597222
## 6    42.08182     2.511591       0.7102273 0.7348485
## 7    44.57000     3.185400       0.8183333 0.5200000

Interpretasi

  • Terbentuk 7 cluster mahasiswa dengan karakteristik performa yang berbeda-beda.
  • Cluster 5 memiliki performa akademik terbaik, terlihat dari midterm dan final tinggi, serta attendance rate tinggi (~0.85).
  • Cluster 3 memiliki nilai midterm paling rendah (~9.66), meskipun nilai final cukup tinggi, menunjukkan ketidakkonsistenan performa.
  • Cluster 2 memiliki nilai quiz2 tertinggi (7.57) dan final cukup tinggi, tetapi attendance rendah (~0.20).
  • Cluster 1 dan 6 memiliki quiz3 relatif rendah/tinggi berbeda, namun keduanya memiliki midterm cukup tinggi, menunjukkan variasi pola belajar.
  • Cluster 4 memiliki quiz2 dan quiz3 tinggi, tetapi final relatif lebih rendah, sehingga performa akhir kurang optimal.
  • Cluster 7 memiliki previous GPA tertinggi (~3.27), namun attendance rendah, menunjukkan mahasiswa dengan kemampuan akademik baik tetapi partisipasi rendah.
  • Secara umum, cluster terbentuk berdasarkan kombinasi nilai akademik, GPA, dan tingkat kehadiran, bukan hanya satu faktor saja.
plot_cluster <- function(cluster_assignments, title) {
  ggplot(pca_df, aes(PC1, PC2, color = cluster_assignments)) +
    geom_point(alpha = 0.6) +
    labs(title = title, color = "Cluster") +
    theme_minimal()
}

plot_cluster(df_features$cluster_kmeans, "K-Means")

Interpretasi

  • Data tersebar di bidang PC1–PC2 tanpa batas cluster yang jelas.
  • Beberapa cluster saling tumpang tindih, menunjukkan pemisahan tidak terlalu kuat.
  • Ada kecenderungan beberapa cluster mendominasi area tertentu, tetapi tidak terpisah tegas.
  • Hal ini sejalan dengan nilai silhouette yang rendah, sehingga struktur cluster bersifat lemah/eksploratif.

2. K-Median

set.seed(42)

kmedian_model <- pam(df_scaled, k = k)

df_features$cluster_kmedian <- as.factor(kmedian_model$cluster)

Mekanisme: Meminimalkan absolute error: \(\sum_{i=1}^{k} \sum_{x \in C_i} ||x - m_i||_1\) di mana \(m_i\) adalah median dari cluster.

plot_cluster(df_features$cluster_kmedian, "K-Medians")

## Interpretasi

Hasil K-Median menunjukkan bahwa data terbagi menjadi tujuh cluster sesuai dengan jumlah cluster yang ditentukan. Namun, secara visual terlihat bahwa sebaran antar cluster masih saling tumpang tindih, terutama pada area tengah, sehingga batas antar kelompok tidak terbentuk secara jelas. Meskipun K-Median dikenal lebih tahan terhadap outlier, pada dataset ini metode tersebut belum mampu menghasilkan pemisahan cluster yang kuat dan mudah diinterpretasikan.

3. DBSCAN

  • DBSCAN digunakan untuk mengelompokkan data berdasarkan kepadatan.
  • Titik yang tidak memiliki cukup tetangga dalam radius epsilon akan dilabeli sebagai noise.
# Menentukan kandidat eps
kNNdistplot(df_scaled, k = 5)
abline(h = 2.6, col = "red", lty = 2)
title(main = "kNN Distance Plot untuk Menentukan Epsilon")

# Uji beberapa nilai eps
eps_values <- c(2.6, 2.8, 3.0, 3.2)

for (e in eps_values) {
  model <- dbscan(df_scaled, eps = e, MinPts = 5)
  cat("\nEps =", e, "\n")
  print(table(model$cluster))
}
## 
## Eps = 2.6 
## 
##   0   1 
##  14 286 
## 
## Eps = 2.8 
## 
##   0   1 
##   3 297 
## 
## Eps = 3 
## 
##   0   1 
##   1 299 
## 
## Eps = 3.2 
## 
##   1 
## 300

Interpretasi

  • Berdasarkan beberapa percobaan nilai eps, parameter 2.6 dipilih karena masih mempertahankan satu cluster utama dan sejumlah kecil noise.
  • Dengan demikian, DBSCAN kurang optimal untuk menemukan beberapa kelompok yang terpisah pada dataset ini dibandingkan metode seperti K-Means.

eps: radius pencarian titik tetangga

minPts: jumlah minimum titik dalam radius eps agar menjadi ‘core point’

# Model final
dbscan_model <- dbscan(df_scaled, eps = 2.6, MinPts = 5)
df_features$cluster_dbscan <- as.factor(dbscan_model$cluster)

# Frekuensi cluster
table(df_features$cluster_dbscan)
## 
##   0   1 
##  14 286
cluster_dbscan_plot <- ifelse(df_features$cluster_dbscan == 0, "Noise", "Cluster 1")
plot_cluster(cluster_dbscan_plot, "DBSCAN")

Interpretasi

  • DBSCAN menghasilkan 1 cluster utama dan beberapa noise (outlier).
  • Sebagian besar data berada dalam satu kelompok, menunjukkan struktur data cukup homogen.
  • Tidak terlihat pemisahan cluster yang jelas selain cluster utama.
  • Hal ini menunjukkan bahwa DBSCAN kurang mampu menemukan banyak cluster pada dataset ini.

4. Mean Shift Clustering

Mekanisme singkat: Berbasis pada estimasi kepadatan kernel (Kernel Density Estimation / KDE). Algoritma menghitung vektor pergeseran rata-rata (mean shift vector) dan menggerakkan titik data menuju puncak kepadatan lokal.

# ubah data ke matrix
data_matrix <- as.matrix(df_scaled)

# coba beberapa nilai bandwidth
bandwidth_values <- c(0.8, 0.9, 1.0, 1.1, 1.2, 1.3, 1.4, 1.5)

for (bw in bandwidth_values) {
  model <- meanShift(data_matrix, bandwidth = rep(bw, ncol(data_matrix)))
  cat("\nBandwidth =", bw, "\n")
  print(table(model$assignment))
}
## 
## Bandwidth = 0.8 
## 
##   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20 
##   3   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
##  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   2   1   1   1   1   1   1 
##  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
##  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
##  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 
##   1   1   1   1   2   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 
##   1   1   1   2   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   2   1 
## 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 
##   1   1   1   1   1   1   1   1   1   1   1   1   2   1   1   1   1   1   1   1 
## 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 281 282 283 284 285 286 287 288 289 290 291 292 293 
##   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 
## Bandwidth = 0.9 
## 
##   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
##  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
##  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
##  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   2   1   1 
##  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 
##   1   1   1   1   2   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 
##   1   1   1   1   1   1   1   2   1   1   1   1   1   1   1   1   1   1   1   1 
## 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 
##   2   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   2   1   1   1   1   1   1 
## 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 
## Bandwidth = 1 
## 
##   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
##  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   2   1   1   1   1   1   1 
##  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
##  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   2   1   1 
##  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 
##   1   1   1   1   1   1   2   1   1   1   1   1   1   1   1   1   1   1   1   2 
## 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 
##   1   1   1   1   1   1   1   1   1   1   1   1   2   1   1   1   1   1   1   1 
## 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   2 
## 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   2   1   1   1   1   1 
## 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 281 282 283 284 285 286 287 288 289 290 291 292 293 
##   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 
## Bandwidth = 1.1 
## 
##   1   2   3   4   5   6   7   8   9  10  11  12  13  14  15  16  17  18  19  20 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   2   2   3   1   1   2 
##  21  22  23  24  25  26  27  28  29  30  31  32  33  34  35  36  37  38  39  40 
##   1   1   1   1   1   1   1   1   5   2   2   1   1   1   1   1   1   1   1   1 
##  41  42  43  44  45  46  47  48  49  50  51  52  53  54  55  56  57  58  59  60 
##   1   1   1   1   1   1   1   1   1   1   1   2   1   1   1   1   1   1   2   2 
##  61  62  63  64  65  66  67  68  69  70  71  72  73  74  75  76  77  78  79  80 
##   1   1   1   1   1   1   1   1   1   2   1   1   1   1   2   1   1   1   1   1 
##  81  82  83  84  85  86  87  88  89  90  91  92  93  94  95  96  97  98  99 100 
##   1   1   1   1   1   1   1   1   1   1   1   1   2   3   1   1   1   1   1   1 
## 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   3   1 
## 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 
##   1   1   2   1   1   2   1   2   1   2   1   1   1   1   1   3   1   1   1   1 
## 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 
##   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   1   2   1   2 
## 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 
##   1   1   1   1   2   1   1   1   1   1   2   1   1   1   1   2   1   1   1   3 
## 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 
##   1   1   1   1   1   1   1   1   1   2   1   1   1   1   1   1   1   1   2   1 
## 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 
##   1   1   1   1   1   1   1   1   2   1   1   1   1   1   1   1   1   1   1   1 
## 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 
##   1   1   1   1   1   1   1   2   1   1   1   2   1   1   1   1   1   1   1   1 
## 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 
##   1   1   1   1   1   2   1   1   1   1   1   1   1   1   1   1   1   1   1   1 
## 
## Bandwidth = 1.2 
## 
##  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 
##  7  7  1 10 12  4 13  6  9  4  8  5 14  5 13 11  2  8  4  9 15  6  1  4  1  5 
## 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 
##  1  8  1  2  4  4  3  8  1  4  1  3  2  3  1  4 10  1  1  1  5  1  1 14  1  1 
## 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 
##  5  1  1  2  3  1  1  2  1  3  2  1  1  1  1  2  1  1 
## 
## Bandwidth = 1.3 
## 
##  1  2  3  4  5  6  7  8  9 
## 92 60 78  9 21 15 16  8  1 
## 
## Bandwidth = 1.4 
## 
##   1 
## 300 
## 
## Bandwidth = 1.5 
## 
##   1 
## 300

Interpretasi

Berdasarkan percobaan berbagai nilai bandwidth, nilai bandwidth = 1.3 dipilih karena menghasilkan jumlah cluster yang tidak terlalu banyak maupun terlalu sedikit.

Namun, struktur cluster yang terbentuk masih kurang stabil dan cenderung menghasilkan beberapa cluster kecil. Oleh karena itu, metode ini kurang optimal dibandingkan K-Means dalam kasus ini.

meanshift_model <- meanShift(data_matrix, bandwidth = rep(1.3, ncol(data_matrix)))

# Simpan hasil cluster
df_features$cluster_meanshift <- as.factor(meanshift_model$assignment)

# Lihat jumlah anggota tiap cluster
table(df_features$cluster_meanshift)
## 
##  1  2  3  4  5  6  7  8  9 
## 92 60 78  9 21 15 16  8  1
plot_cluster(df_features$cluster_meanshift, "Mean Shift")

Interpretasi

  • cluster (sekitar 9 kelompok).
  • Data terbagi ke dalam beberapa kelompok, tetapi batas antar cluster tidak terlalu jelas.
  • Banyak titik dari cluster berbeda saling tumpang tindih, terutama di area tengah.
  • Beberapa cluster terlihat lebih dominan (banyak anggota), sementara lainnya kecil.
  • Tidak ada pemisahan yang tegas antar kelompok, sehingga struktur cluster kurang kuat.
  • Hasil ini menunjukkan bahwa Mean Shift mampu membagi data, tetapi belum menghasilkan cluster yang benar-benar jelas dan terpisah.

5. Fuzzy C-Means

set.seed(42)

# Menggunakan jumlah cluster yang sama dengan K-Means
fcm_model <- cmeans(df_scaled, centers = k, iter.max = 100, method = "cmeans")

# Simpan hasil cluster (hard assignment dari membership terbesar)
df_features$cluster_fcm <- as.factor(fcm_model$cluster)

# Lihat membership
head(fcm_model$membership)
##           1         2         3         4         5         6         7
## 1 0.1428668 0.1428488 0.1428672 0.1428523 0.1428660 0.1428516 0.1428474
## 2 0.1428714 0.1428642 0.1428358 0.1428503 0.1428655 0.1428738 0.1428390
## 3 0.1428710 0.1428480 0.1428683 0.1428479 0.1428673 0.1428551 0.1428423
## 4 0.1428491 0.1428523 0.1428729 0.1428604 0.1428515 0.1428485 0.1428652
## 5 0.1428419 0.1428426 0.1429059 0.1428526 0.1428512 0.1428408 0.1428649
## 6 0.1428321 0.1428661 0.1428608 0.1428639 0.1428403 0.1428599 0.1428769

Interpretasi

  • Nilai membership berada di sekitar 0.142 untuk semua cluster.
  • Nilai yang hampir sama ini menunjukkan bahwa tidak ada cluster yang dominan untuk tiap observasi.
  • Hal ini menunjukkan bahwa pemisahan cluster sangat lemah.
  • Struktur data tidak membentuk kelompok yang jelas, sehingga Fuzzy C-Means sulit membedakan cluster.
plot_cluster(df_features$cluster_fcm, "Fuzzy C-Means")

PERBANDINGAN METODE

KESIMPULAN AKHIR

Berdasarkan seluruh proses analisis, dapat disimpulkan bahwa data mahasiswa memiliki struktur yang cukup kompleks dan tidak menunjukkan pemisahan cluster yang sangat kuat. Hal ini terlihat dari nilai silhouette yang relatif rendah serta visualisasi cluster yang masih saling tumpang tindih.

Dari berbagai metode yang digunakan, K-Means merupakan metode yang paling sesuai untuk dataset ini. Meskipun tidak menghasilkan pemisahan yang sangat tegas, K-Means mampu memberikan pembagian cluster yang relatif lebih stabil, kompak, dan masih dapat diinterpretasikan secara akademik.