K-Nearest Neighbour

Pendahuluan

Latar Belakang: Memprediksi Akreditasi (A, B, C) dengan Skor Literasi dan Numerasi

Peringkat Akreditasi (A, B, C) adalah benchmark fundamental untuk menilai mutu sebuah satuan pendidikan. Namun, apa yang mendorong perbedaan peringkat tersebut? Selain faktor infrastruktur dan administratif, faktor yang paling penting seharusnya adalah output pembelajaran siswa.

Dalam analisis ini, kita akan mengeksplorasi sebuah pendekatan machine learning untuk menjawab pertanyaan:

“Bisakah kita memprediksi Peringkat Akreditasi sebuah sekolah (A, B, C) hanya dengan melihat skor Literasi dan Numerasi?”

Kita akan menggunakan empat variabel prediktor kuantitatif yang sangat relevan:

  • Literasi 2023

  • Numerasi 2023

  • Literasi 2024

  • Numerasi 2024

Karena variabel target kita (Peringkat) memiliki tiga kategori (A, B, dan C), ini menjadikannya masalah Klasifikasi Multikelas.

Metodologi: K-Nearest Neighbors (KNN)

Untuk memecahkan masalah ini, kita akan menggunakan algoritma K-Nearest Neighbors (KNN).

Apa itu KNN?

KNN adalah algoritma machine learning yang sederhana namun sangat intuitif. Prinsip dasarnya bekerja seperti pepatah: “Anda dinilai dari dengan siapa Anda bergaul.”

KNN tidak “belajar” sebuah formula rumit. Sebaliknya, ia menghafal seluruh data latih. Ketika kita ingin memprediksi peringkat sekolah baru, KNN melakukan tiga langkah:

  1. Mengukur Jarak: Ia menghitung “jarak” atau “kemiripan” sekolah baru tersebut dengan semua sekolah lain dalam data latih.

  2. Mencari Tetangga (K): Ia menemukan ‘K’ sekolah terdekat (tetangga terdekat) berdasarkan skornya. ‘K’ adalah angka yang kita tentukan (misalnya, 5, 7, atau 10).

  3. Mengambil Suara (Voting): Ia melihat peringkat akreditasi dari ‘K’ tetangga tersebut dan mengambil suara terbanyak (majority vote) untuk menentukan prediksi.

Bagaimana KNN Bekerja untuk Multikelas (A, B, C)?

Inilah keindahan KNN: logikanya tidak berubah sama sekali untuk multikelas.

  • Contoh: Kita ingin memprediksi Sekolah X dan kita menetapkan K = 7.

  • KNN mencari 7 tetangga terdekat berdasarkan skor literasi dan numerasinya.

  • Hasil “voting” dari 7 tetangga tersebut adalah:

    • Peringkat ‘A’: 3 suara

    • Peringkat ‘B’: 2 suara

    • Peringkat ‘C’: 2 suara

  • Keputusan: Suara terbanyak adalah ‘A’ (3 suara). Maka, KNN memprediksi Sekolah X akan mendapatkan Peringkat ‘A’.

Studi Kasus : Provinsi DKI Jakarta, DIY, Bali, Banten

Kami memfokuskan studi kasus pada empat provinsi yang sering dianggap sebagai pusat keunggulan di Indonesia: DKI Jakarta, Daerah Istimewa Yogyakarta (DIY), Bali, dan Banten.

Tujuan RPubs

  1. Eksplorasi Data (EDA): Memvisualisasikan distribusi skor dan melihat korelasi antar variabel prediktor.

  2. Pembagian Data: Membagi data menjadi 80% train dan 20% test.

  3. Optimasi Model (Tuning): Menjalankan cross-validation untuk menemukan nilai ‘K’ optimal.

  4. Pelatihan Model: Melatih model KNN final pada data latih menggunakan ‘K’ optimal tersebut.

  5. Penanganan Imbalance Class : Menyeimbangkan kelas peringkat akreditasi A B dan C dengan SMOTE

  6. Evaluasi Model: Membuat Confusion Matrix multikelas pada data uji untuk menilai Akurasi, Presisi, dan Recall untuk setiap kelas (A, B, dan C).

Hasil dan Pembahasan

1. Persiapan Data

library(readxl)
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(tidyr)
library(ggplot2)
## Warning: package 'ggplot2' was built under R version 4.4.3
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ forcats   1.0.0     ✔ readr     2.1.5
## ✔ lubridate 1.9.4     ✔ stringr   1.5.1
## ✔ purrr     1.0.2     ✔ tibble    3.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(tidymodels)
## ── Attaching packages ────────────────────────────────────── tidymodels 1.2.0 ──
## ✔ broom        1.0.7     ✔ rsample      1.2.1
## ✔ dials        1.3.0     ✔ tune         1.2.1
## ✔ infer        1.0.7     ✔ workflows    1.1.4
## ✔ modeldata    1.4.0     ✔ workflowsets 1.1.0
## ✔ parsnip      1.2.1     ✔ yardstick    1.3.2
## ✔ recipes      1.1.0     
## ── Conflicts ───────────────────────────────────────── tidymodels_conflicts() ──
## ✖ scales::discard() masks purrr::discard()
## ✖ dplyr::filter()   masks stats::filter()
## ✖ recipes::fixed()  masks stringr::fixed()
## ✖ dplyr::lag()      masks stats::lag()
## ✖ yardstick::spec() masks readr::spec()
## ✖ recipes::step()   masks stats::step()
## • Learn how to get started at https://www.tidymodels.org/start/
library(DataExplorer)
## Warning: package 'DataExplorer' was built under R version 4.4.3
library(kknn)
## Warning: package 'kknn' was built under R version 4.4.3
df <- read_excel("Data Tugas-2.xlsx")
str(df)
## tibble [6,648 × 7] (S3: tbl_df/tbl/data.frame)
##  $ Nomor               : num [1:6648] 1 2 3 4 5 6 7 8 9 10 ...
##  $ Provinsi            : chr [1:6648] "Prov. Jawa Timur" "Prov. Nusa Tenggara Timur" "Prov. Jawa Timur" "Prov. Sumatera Utara" ...
##  $ Peringkat Akreditasi: chr [1:6648] "A" "B" "A" "A" ...
##  $ Lit_2023            : num [1:6648] 20 80 91.1 57.8 55.6 ...
##  $ Num_2023            : num [1:6648] 40 48.8 82.2 57.8 66.7 ...
##  $ Lit_2024            : num [1:6648] 0 82.2 93.3 64.4 83.3 ...
##  $ Num_2024            : num [1:6648] 0 62.8 93.3 62.2 66.7 ...
df <- df %>% 
  select(-Nomor)
glimpse(df)
## Rows: 6,648
## Columns: 6
## $ Provinsi               <chr> "Prov. Jawa Timur", "Prov. Nusa Tenggara Timur"…
## $ `Peringkat Akreditasi` <chr> "A", "B", "A", "A", "B", "A", "C", "B", "A", "C…
## $ Lit_2023               <dbl> 20.00, 80.00, 91.11, 57.78, 55.56, 51.11, 56.52…
## $ Num_2023               <dbl> 40.00, 48.78, 82.22, 57.78, 66.67, 51.11, 61.90…
## $ Lit_2024               <dbl> 0.000, 82.222, 93.333, 64.444, 83.333, 71.795, …
## $ Num_2024               <dbl> 0.000, 62.791, 93.333, 62.222, 66.667, 55.556, …

Filter provinsi DKI, DIY, Bali, Banten

df <- df %>%
  filter(Provinsi %in% c("Prov. D.K.I. Jakarta",
                         "Prov. D.I. Yogyakarta",
                         "Prov. Bali",
                         "Prov. Banten"))

# cek hasil
df %>% count(Provinsi)
## # A tibble: 4 × 2
##   Provinsi                  n
##   <chr>                 <int>
## 1 Prov. Bali               66
## 2 Prov. Banten            394
## 3 Prov. D.I. Yogyakarta   102
## 4 Prov. D.K.I. Jakarta    229
# hitung jumlah per provinsi
prov_count <- df %>%
  count(Provinsi)

2. Eksplorasi Data

summary(df[, c("Lit_2023","Num_2023","Lit_2024","Num_2024")])
##     Lit_2023         Num_2023         Lit_2024         Num_2024     
##  Min.   :  0.00   Min.   :  6.67   Min.   :  0.00   Min.   :  0.00  
##  1st Qu.: 72.78   1st Qu.: 65.89   1st Qu.: 77.78   1st Qu.: 71.11  
##  Median : 91.11   Median : 83.33   Median : 93.33   Median : 87.81  
##  Mean   : 81.80   Mean   : 77.17   Mean   : 83.46   Mean   : 80.11  
##  3rd Qu.: 97.78   3rd Qu.: 93.25   3rd Qu.: 97.78   3rd Qu.: 95.56  
##  Max.   :100.00   Max.   :100.00   Max.   :100.00   Max.   :100.00
df <- df %>% 
      mutate(across(where(is.character),as.factor))

2.1. Cek Missing Value

plot_intro(df,theme_config = theme_classic())

2.2 Jumlah Sekolah Per Provinsi

# pie chart dengan label jumlah
ggplot(prov_count, aes(x = "", y = n, fill = Provinsi)) +
  geom_bar(stat = "identity", width = 1, color = "white") +
  coord_polar(theta = "y") +
  geom_text(aes(label = n),
            position = position_stack(vjust = 0.5),
            color = "black",
            size = 4) +
   scale_fill_brewer(palette = "Oranges") +
  labs(title = "Jumlah Sekolah per Provinsi",
       fill = "Provinsi") +
  theme_void()

Ubah Kolom “Peringkat Akreditasi” jadi Y biar lebih gampang

df <- df %>%
  rename(Y = `Peringkat Akreditasi`)

2.3. Barplot Frekuensi Peringkat Akreditasi

# Hitung frekuensi + proporsi
freq_tab <- df %>%
  count(Y) %>%
  mutate(prop = n / sum(n) * 100)

# Plot
ggplot(freq_tab, aes(x = Y, y = n, fill = Y)) +
  geom_col() +
  geom_text(aes(label = paste0(n, " (", round(prop,1), "%)")),
            vjust = -0.5, size = 4) +
  labs(title = "Frekuensi Peringkat Akreditasi",
       x = "Peringkat Akreditasi",
       y = "Frekuensi") +
  scale_fill_brewer(palette = "Oranges") +
  theme_classic()

hapus observasi pada kelas “Tidak Terakreditasi” karena terlalu sedikit yang akan menyebabkan model kurang mampu menggeneralisir

TTA <- df %>%
  filter(Y == "Tidak Terakreditasi")
TTA
## # A tibble: 2 × 6
##   Provinsi     Y                   Lit_2023 Num_2023 Lit_2024 Num_2024
##   <fct>        <fct>                  <dbl>    <dbl>    <dbl>    <dbl>
## 1 Prov. Banten Tidak Terakreditasi     66.7     66.7     0         100
## 2 Prov. Banten Tidak Terakreditasi     25       18.8     4.54       25
df <- df %>%
  filter(Y != "Tidak Terakreditasi") %>%
  droplevels()
# Hitung frekuensi + proporsi
freq_tab <- df %>%
  count(Y) %>%
  mutate(prop = n / sum(n) * 100)

# Plot
ggplot(freq_tab, aes(x = Y, y = n, fill = Y)) +
  geom_col() +
  geom_text(aes(label = paste0(n, " (", round(prop,1), "%)")),
            vjust = -0.5, size = 4) +
  labs(title = "Frekuensi Peringkat Akreditasi",
       x = "Peringkat Akreditasi",
       y = "Frekuensi") +
  scale_fill_brewer(palette = "Oranges") +
  theme_classic()

Berdasarkan plot sebaran Variabel Respon (Y) dalam hal ini peringkat akreditasi mengalami kelas yang tidak seimbang

2.4 Boxplot sebaran nilai peubah terhadap kelas Y

df_long <- df %>%
  pivot_longer(cols = c(Lit_2023, Num_2023, Lit_2024, Num_2024),
               names_to = "Variabel",
               values_to = "Skor")

ggplot(df_long, aes(x = Y, y = Skor, fill = Y)) +
  geom_boxplot() +
  facet_wrap(~ Variabel, scales = "free_y") +
   scale_fill_brewer(palette = "Oranges") +
  labs(title = "Distribusi Nilai Literasi & Numerasi per Akreditasi",
       x = "Akreditasi", y = "Skor") +
  theme_minimal()

nilai masing-masing peubah terlihat berbeda rentang nilainya yaitu A lebih tinggi dari B C, tapi A banyak outlier

2.4. Frekuensi Peringkat Akreditasi per Provinsi

df %>%
  count(Provinsi, Y) %>%
  arrange(Provinsi, desc(n)) %>%
  ggplot(aes(x = Provinsi, y = n, fill = Y)) +
  geom_col(position = "stack") +
  scale_fill_brewer(palette = "Oranges") +
  labs(title = "Distribusi Peringkat Akreditasi per Provinsi",
       x = "Provinsi",
       y = "Frekuensi") +
  theme_minimal() +  # background putih polos, no grid
  theme(axis.text.x = element_text(angle = 45, hjust = 1),
        panel.background = element_blank(),
        plot.background = element_blank(),
        legend.background = element_blank())

2.5 Korelasi

library(corrplot)
## corrplot 0.95 loaded
cor_mat <- df %>%
  select(Lit_2023, Num_2023, Lit_2024, Num_2024) %>%
  cor(use = "complete.obs")

corrplot(cor_mat, method = "color", type = "upper", 
         addCoef.col = "white", number.cex = 0.7, 
         tl.col = "black", tl.srt = 45)

library(GGally)
## Warning: package 'GGally' was built under R version 4.4.3
## Registered S3 method overwritten by 'GGally':
##   method from   
##   +.gg   ggplot2
library(ggplot2)

df %>%
  select(Lit_2023, Num_2023, Lit_2024, Num_2024) %>%
  ggpairs(
    aes(alpha = 0.6),
    upper = list(continuous = wrap("cor", size = 4, stars = FALSE)),  # hanya angka korelasi
    lower = list(continuous = wrap("smooth", 
                                   color = "black", alpha = 0.6, se = FALSE)), 
    diag  = list(continuous = wrap("densityDiag", 
                                   fill = "orange", alpha = 0.6))
  ) +
  theme_classic() 

3. Model KNN

Untuk analisis klasifikasi ini, kita akan fokus murni pada skor rapor pendidikan. Oleh karena itu, kolom Provinsi tidak akan digunakan sebagai variabel prediktor dan kita keluarkan dari data.

df<-df %>% 
select(-Provinsi)

Tantangan utama dalam dataset ini adalah data yang tidak seimbang (imbalanced). Kemungkinan besar, jumlah sekolah dengan akreditasi ‘A’ atau ‘B’ jauh lebih banyak daripada ‘C’. Jika kita melatih model pada data ini, model akan cenderung “malas” dan lebih sering menebak kelas mayoritas, sehingga kinerjanya buruk pada kelas minoritas (‘C’).

Untuk mengatasi ini, kita akan merancang sebuah eksperimen untuk membandingkan dua pendekatan:

  1. Workflow 1 (no_prep): Melatih KNN pada data mentah (asli).

  2. Workflow 2 (smote_rec): Melatih KNN pada data yang telah kita seimbangkan menggunakan SMOTE dan kita normalisasi.

KNN wajib menggunakan normalisasi/standardisasi karena ia berbasis jarak. step_smote akan membuat data sintetis (buatan) untuk kelas minoritas agar jumlahnya seimbang.

3.1 Pembagian data

Pertama, kita bagi data kita menjadi data latih (80%) dan data uji (20%). Kita gunakan strata = Y agar proporsi kelas A, B, dan C tetap sama di kedua set data.

set.seed(123)
holdout_split <- initial_split(df,
                               #sampel acak berdasarkan kelompok
                               strata = Y,
                               # proporsi untuk training data
                               prop = 0.8)
train_data <- training(holdout_split)
test_data <- testing(holdout_split)
library(patchwork) # biar bisa gabung plot

# distribusi train
p_train <- train_data %>%
  count(Y) %>%
  ggplot(aes(x = Y, y = n, fill = Y)) +
  geom_col() +
  scale_fill_brewer(palette = "Oranges") +
  labs(title = "Distribusi Y - Training Set", x = "Kelas", y = "Jumlah") +
  theme_minimal()

# distribusi test
p_test <- test_data %>%
  count(Y) %>%
  ggplot(aes(x = Y, y = n, fill = Y)) +
  geom_col() +
  labs(title = "Distribusi Y - Testing Set", x = "Kelas", y = "Jumlah") +
  scale_fill_brewer(palette = "Oranges") +
  theme_minimal()

# gabungkan plot
p_train + p_test

Plot di atas untuk memperlihatkan bahwa pembagian data training dan testing sesuai dengan strata Y sehingga proporsi kelas sama untuk train dan test set

Selanjutnya, kita siapkan “resep” untuk kedua workflow kita.

library(tidymodels)
library(themis)      # Untuk step_smote
## Warning: package 'themis' was built under R version 4.4.3
library(workflowsets) # Untuk membandingkan workflow

set.seed(123)

# Recipe 1: Tanpa preprocessing
no_prep <- recipe(Y ~ ., data = train_data)

# Recipe 2: SMOTE + Normalisasi
# Kita terapkan SMOTE untuk menyeimbangkan kelas
# Kita terapkan Normalisasi (centering dan scaling) karena KNN sensitif terhadap skala
smote_rec <- recipe(Y ~ ., data = train_data) %>%
  step_smote(Y) %>%
  step_normalize(all_numeric_predictors())

3.2. Penentuan Model dan Tuning Grid

Kita akan menggunakan model KNN. Parameter terpenting dalam KNN adalah neighbors (jumlah tetangga, ‘k’). Kita tidak tahu nilai ‘k’ terbaik, jadi kita akan memberitahu tidymodels untuk mencarinya (tuning).

Kita akan menguji 50 nilai ‘k’ yang berbeda, dari k=2 sampai k=50.

# Model: KNN dengan K yang bisa di-tuning
knn_tune <- nearest_neighbor(
  neighbors = tune(), # 'tune()' berarti parameter ini akan kita cari
  weight_func = "rectangular",
  dist_power = 2 # Jarak Euclidean
) %>%
  set_engine("kknn") %>%
  set_mode("classification")

# Grid (daftar) nilai 'k' yang akan diuji
knn_grid <- grid_regular(neighbors(range = c(2, 50)), levels = 50)

3.3. Validasi Silang (Cross-Validation)

Untuk menemukan ‘k’ terbaik, kita tidak bisa menggunakan test_data (karena itu curang). Kita akan menggunakan 10-Fold Cross-Validation pada train_data. Data latih akan dibagi 10, lalu model akan dilatih 10 kali.

Kita gunakan workflow_set untuk menggabungkan kedua resep (no_prep dan smote_rec) dengan model (knn_tune).

# 1. Buat workflow set
wfst <- workflow_set(
  preproc = list(no_prep = no_prep,
                 smote_rec = smote_rec),
  models = list(knn = knn_tune)
)

# 2. Siapkan 10-fold CV (stratified)
set.seed(345)
folds <- vfold_cv(train_data, v = 10, strata = Y)

# 3. Jalankan tuning!
# Ini akan menguji 2 workflow x 50 nilai K x 10 folds
knn_tune_cv <- wfst %>%
  workflow_map(
    fn = "tune_grid",
    verbose = TRUE,
    seed = 2045,
    resamples = folds,
    grid = knn_grid,
    metrics = metric_set(accuracy),
    control = control_resamples(save_pred = TRUE)
  )
## i 1 of 2 tuning:     no_prep_knn
## ✔ 1 of 2 tuning:     no_prep_knn (7.2s)
## i 2 of 2 tuning:     smote_rec_knn
## ✔ 2 of 2 tuning:     smote_rec_knn (7.9s)

3.4. Hasil Tuning: SMOTE vs Data Asli

Sekarang kita plot hasil eksperimen CV kita. Kita ingin tahu:

  1. Workflow mana yang lebih baik (biru vs oranye)?

  2. Berapa nilai ‘k’ terbaik untuk masing-masing workflow?

# Kumpulkan hasil tuning (metrics) untuk keduanya
neighbors_result_no_prep <- knn_tune_cv %>%
  extract_workflow_set_result("no_prep_knn") %>%
  collect_metrics() %>%
  mutate(workflow = "No SMOTE")

neighbors_result_smote <- knn_tune_cv %>%
  extract_workflow_set_result("smote_rec_knn") %>%
  collect_metrics() %>%
  mutate(workflow = "SMOTE")

# Gabungkan hasil
neighbors_result <- bind_rows(neighbors_result_no_prep,
                              neighbors_result_smote)

# Plot perbandingan
neighbors_result %>%
  filter(.metric == "accuracy") %>%
  ggplot(aes(x = neighbors, y = mean, color = workflow)) +
  geom_errorbar(aes(ymin = mean - std_err,
                    ymax = mean + std_err),
                width = 0.3, alpha = 0.6) +
  geom_point(size = 2) +
  geom_line(alpha = 0.7) +
  ylab("Accuracy (Rata-rata CV)") +
  xlab("Neighbors (k)") +
  scale_color_manual(values = c("No SMOTE" = "#03A9F4",
                                "SMOTE" = "#f44e03"),
                     name = "Workflow") +
  labs(title = "Perbandingan Kinerja Akurasi KNN",
       subtitle = "Data Asli vs. SMOTE + Normalisasi") +
  theme_bw() +
  theme(legend.position = "top")

Interpretasi Plot: Dari plot di atas, kita dapat melihat bahwa workflow SMOTE (garis oranye) secara konsisten memberikan akurasi yang lebih tinggi daripada workflow No SMOTE (garis biru).

Sekarang kita pilih nilai ‘k’ terbaik untuk masing-masing:

# 'k' terbaik untuk No SMOTE
best_neighbors <- knn_tune_cv %>%
  extract_workflow_set_result("no_prep_knn") %>%
  select_best(metric = "accuracy")
best_neighbors
## # A tibble: 1 × 2
##   neighbors .config              
##       <int> <chr>                
## 1        33 Preprocessor1_Model32
# 'k' terbaik untuk SMOTE
best_neighbors_smote <- knn_tune_cv %>%
  extract_workflow_set_result("smote_rec_knn") %>%
  select_best(metric = "accuracy")
best_neighbors_smote
## # A tibble: 1 × 2
##   neighbors .config              
##       <int> <chr>                
## 1        50 Preprocessor1_Model49

Berdasarkan hasil tuning CV

  • No SMOTE = k terbaik berdasarkan accuracy 33

  • SMOTE = k terbaik berdasarkan accuracy 50

3.5. Evaluasi Akhir pada Data Uji

Kita telah menemukan nilai ‘k’ optimal untuk kedua workflow (data asli dan SMOTE) menggunakan cross-validation pada data latih.

Kini saatnya menguji kedua model final tersebut pada test_data—data yang belum pernah tersentuh sama sekali. Fungsi last_fit() sangat ideal untuk ini: ia akan melatih model final pada seluruh train_data dan mengevaluasinya satu kali pada test_data.

3.5.1. Finalisasi Kedua Workflow

Pertama, kita siapkan dua workflow final menggunakan nilai ‘k’ terbaik yang sudah kita temukan untuk masing-masing.

# 1. Finalize workflow untuk non-SMOTE (menggunakan 'k' terbaiknya)
wf_no_prep <- workflow() %>%
  add_model(knn_tune %>% finalize_model(best_neighbors)) %>%
  add_recipe(no_prep)

# 2. Finalize workflow untuk SMOTE (menggunakan 'k' terbaiknya)
wf_smote <- workflow() %>%
  add_model(knn_tune %>% finalize_model(best_neighbors_smote)) %>%
  add_recipe(smote_rec)

3.5.2. Menjalankan last_fit()

Sekarang kita jalankan last_fit() pada kedua workflow menggunakan holdout_split yang sama.

Fungsi last_fit() dirancang khusus untuk langkah “final” ini.

  1. Ia mengambil workflow final (misal: wf_no_prep dan wf_smote).

  2. Ia melatihnya (fit) satu kali pada data latih dari holdout_split (yaitu train_data).

  3. Ia mengevaluasinya (evaluate) satu kali pada data uji dari holdout_split (yaitu test_data).

# Menjalankan 'last_fit' untuk workflow No SMOTE
set.seed(1234)
final_no_prep <- last_fit(wf_no_prep, split = holdout_split)

# Menjalankan 'last_fit' untuk workflow SMOTE
set.seed(1234)
final_smote   <- last_fit(wf_smote, split = holdout_split)

3.6. Hasil Kinerja Model: SMOTE vs Data Asli

Kita sekarang memiliki dua hasil akhir. Mari kita kumpulkan metrik dan confusion matrix dari keduanya untuk perbandingan head-to-head.

3.6.1. Perbandingan Metrik Performa

Metrik ini memberi tahu kita kinerja keseluruhan model di data uji.

# Ambil metrik (Accuracy & ROC AUC) untuk No SMOTE
metrics_no_prep <- final_no_prep %>%
  collect_metrics() %>% 
  mutate(workflow = "No SMOTE")

# Ambil metrik (Accuracy & ROC AUC) untuk SMOTE
metrics_smote <- final_smote %>%
  collect_metrics() %>% 
  mutate(workflow = "SMOTE")

# Gabungkan dan tampilkan
bind_rows(metrics_no_prep, metrics_smote)
## # A tibble: 6 × 5
##   .metric     .estimator .estimate .config              workflow
##   <chr>       <chr>          <dbl> <chr>                <chr>   
## 1 accuracy    multiclass     0.772 Preprocessor1_Model1 No SMOTE
## 2 roc_auc     hand_till      0.825 Preprocessor1_Model1 No SMOTE
## 3 brier_class multiclass     0.156 Preprocessor1_Model1 No SMOTE
## 4 accuracy    multiclass     0.804 Preprocessor1_Model1 SMOTE   
## 5 roc_auc     hand_till      0.878 Preprocessor1_Model1 SMOTE   
## 6 brier_class multiclass     0.152 Preprocessor1_Model1 SMOTE

Analisis Awal: Dari tabel ringkasan di atas, kita bisa melihat perbandingan langsung Accuracy dan ROC AUC (rata-rata) pada data uji. Workflow SMOTE menunjukkan performa lebih baik secara keseluruhan.

3.6.2. Perbandingan Metrik per Kelas (Lebih Detail)

Akurasi saja tidak cukup untuk data imbalanced. Kita perlu melihat metrik per kelas, terutama Balanced Accuracy, Precision, dan Recall.

# === Metrik Detail untuk No SMOTE ===
class_metrics_no_prep <- final_no_prep %>%
  collect_predictions() %>%
  yardstick::metric_set(accuracy, bal_accuracy, precision, recall, f_meas)(truth = Y, estimate = .pred_class) %>% 
  mutate(workflow = "No SMOTE")
## Warning: While computing multiclass `precision()`, some levels had no predicted events
## (i.e. `true_positive + false_positive = 0`).
## Precision is undefined in this case, and those levels will be removed from the
## averaged result.
## Note that the following number of true events actually occurred for each
## problematic event level:
## 'C': 8
## While computing multiclass `precision()`, some levels had no predicted events
## (i.e. `true_positive + false_positive = 0`).
## Precision is undefined in this case, and those levels will be removed from the
## averaged result.
## Note that the following number of true events actually occurred for each
## problematic event level:
## 'C': 8
print("--- Hasil Model: No SMOTE (Data Asli) ---")
## [1] "--- Hasil Model: No SMOTE (Data Asli) ---"
class_metrics_no_prep
## # A tibble: 5 × 4
##   .metric      .estimator .estimate workflow
##   <chr>        <chr>          <dbl> <chr>   
## 1 accuracy     multiclass     0.772 No SMOTE
## 2 bal_accuracy macro          0.624 No SMOTE
## 3 precision    macro          0.718 No SMOTE
## 4 recall       macro          0.454 No SMOTE
## 5 f_meas       macro          0.679 No SMOTE
# === Metrik Detail untuk SMOTE ===
class_metrics_smote <- final_smote %>%
  collect_predictions() %>%
  yardstick::metric_set(accuracy, bal_accuracy, precision, recall, f_meas)(truth = Y, estimate = .pred_class) %>% 
  mutate(workflow = "SMOTE")

print("--- Hasil Model: SMOTE + Normalisasi ---")
## [1] "--- Hasil Model: SMOTE + Normalisasi ---"
class_metrics_smote
## # A tibble: 5 × 4
##   .metric      .estimator .estimate workflow
##   <chr>        <chr>          <dbl> <chr>   
## 1 accuracy     multiclass     0.804 SMOTE   
## 2 bal_accuracy macro          0.788 SMOTE   
## 3 precision    macro          0.700 SMOTE   
## 4 recall       macro          0.725 SMOTE   
## 5 f_meas       macro          0.701 SMOTE

Analisis Mendalam:

  • Balanced Accuracy: Ini adalah metrik yang paling adil. Model SMOTE memiliki 0.787 lebih tinggi dari No SMOTE yaitu 0.624 , menunjukkan SMOTE memiliki kemampuannya yang lebih dalam menangani kelas minoritas.

  • Recall (Kelas C): Lihat recall untuk kelas ‘C’ (jika itu minoritas). Model No SMOTE mencapai 0.454, sementara SMOTE mencapai 0.725. Ini membuktikan bahwa SMOTE berhasil meningkatkan kemampuan model untuk “menemukan” kelas minoritas.

3.6.3. Perbandingan Confusion Matrix

Visualisasi adalah cara terbaik untuk melihat di mana letak kesalahan model.

# === Confusion Matrix: No SMOTE ===
conf_mat_no_prep <- final_no_prep %>%
  collect_predictions() %>%
  conf_mat(truth = Y, estimate = .pred_class)

autoplot(conf_mat_no_prep, type = "heatmap") +
  labs(title = "Confusion Matrix: Model KNN (Data Asli)") +
  scale_fill_gradient2(low = "#E3F2FD", mid = "#64B5F6", high = "#1565C0")
## Scale for fill is already present.
## Adding another scale for fill, which will replace the existing scale.

# === Confusion Matrix: SMOTE ===
conf_mat_smote <- final_smote %>%
  collect_predictions() %>%
  conf_mat(truth = Y, estimate = .pred_class)

autoplot(conf_mat_smote, type = "heatmap") +
  labs(title = "Confusion Matrix: Model KNN (SMOTE + Normalisasi)") +
  scale_fill_gradient2(low = "#FFF3E0", mid = "#FFB74D", high = "#EF6C00")
## Scale for fill is already present.
## Adding another scale for fill, which will replace the existing scale.

3.7. Kesimpulan Model KNN

# 1. Kumpulkan metrik dari kedua model
metrics_no_prep <- final_no_prep %>%
  collect_metrics() %>% 
  mutate(workflow = "No SMOTE")

metrics_smote <- final_smote %>%
  collect_metrics() %>% 
  mutate(workflow = "SMOTE")

# 2. Gabungkan, filter hanya akurasi, dan format
perbandingan_akurasi <- bind_rows(metrics_no_prep, metrics_smote) %>%
  filter(.metric == "accuracy") %>%
  select(Workflow = workflow, Akurasi = .estimate) %>%
  # (Opsional) Format angka jadi persentase
  mutate(Akurasi = scales::percent(Akurasi, accuracy = 0.01))

# 3. Tampilkan tabel dengan judul
knitr::kable(
  perbandingan_akurasi,
  caption = "Perbandingan Akurasi Final pada Data Uji"
)
Perbandingan Akurasi Final pada Data Uji
Workflow Akurasi
No SMOTE 77.22%
SMOTE 80.38%

Berdasarkan perbandingan langsung pada data uji, workflow SMOTE terbukti menjadi pemenang yang jelas.

Meskipun akurasi keseluruhan beda sedikit, model SMOTE menunjukkan Balanced Accuracy yang jauh lebih superior dan Recall yang jauh lebih baik untuk kelas minoritas ‘C’. Ini membuktikan bahwa untuk dataset ini, [penanganan imbalance (SMOTE) dan normalisasi] sangat krusial untuk menciptakan model yang adil dan akurat.

Kesimpulan

Metode K-Nearest Neighbour dengan penanganan imbalance SMOTE (ditambah normalisasi) memiliki kemampuan yang lebih superior dan seimbang (adil) dalam mengklasifikasikan Peringkat Akreditasi (A, B, dan C) dibandingkan model yang dilatih pada data asli (No SMOTE).

Evaluasi akhir menunjukkan bahwa model SMOTE adalah pemenang yang jelas karena:

  1. Kinerja Kelas Minoritas Jauh Lebih Baik: Model SMOTE menunjukkan nilai Balanced Accuracy dan Recall (untuk kelas minoritas ‘C’) yang jauh lebih tinggi. Ini membuktikan bahwa model tersebut berhasil “belajar” untuk mengidentifikasi kelas yang sulit dan tidak hanya menebak kelas mayoritas.

  2. Model yang Lebih Fungsional: Model No SMOTE mungkin memiliki akurasi yang tampak tinggi, tetapi ini adalah “akurasi palsu” yang didapat dari kegagalannya mengenali kelas minoritas. Model SMOTE menghasilkan model yang secara fungsional jauh lebih berguna dan adil di dunia nyata.

Untuk studi kasus ini, terbukti bahwa penanganan imbalance data menggunakan SMOTE dan normalisasi data adalah langkah yang wajib dan krusial untuk menghasilkan model KNN yang valid.