BAB I PENDAHULUAN

1.1 Latar Belakang

Curah hujan merupakan salah satu unsur iklim yang sangat memengaruhi kegiatan manusia, seperti pertanian, transportasi, dan mitigasi bencana banjir. Prediksi kejadian hujan dapat dilakukan dengan memanfaatkan data pengamatan cuaca, misalnya suhu, kelembapan, keadaan cuaca, dan kecepatan angin, melalui pendekatan machine learning.

Namun, data mentah hasil pengamatan jarang langsung siap digunakan untuk pemodelan. Data sering mengandung nilai hilang (missing value), nilai pencilan (outlier), variabel dengan skala yang berbeda, variabel kategorik berkardinalitas tinggi, serta distribusi kelas target yang tidak seimbang (imbalanced). Kondisi tersebut dapat menurunkan kinerja model sehingga diperlukan tahap data preprocessing sebelum pemodelan (Han et al., 2012; Kuhn & Johnson, 2019).

Pada praktikum ini dilakukan preprocessing pada dataset curah hujan yang meliputi penanganan missing value dengan interpolasi dan mode imputation, reduksi kardinalitas, stratified splitting, penanganan outlier dengan capping, scaling menggunakan Standard Scaler, encoding, serta penanganan data tidak seimbang menggunakan SMOTE.

1.2 Rumusan Masalah

  1. Bagaimana kondisi awal dataset curah hujan, terutama terkait missing value, outlier, dan keseimbangan kelas pada variabel Hujan?
  2. Bagaimana penerapan interpolasi, mode imputation, capping, stratified splitting, Standard Scaler, encoding, dan SMOTE pada dataset tersebut?
  3. Bagaimana perubahan karakteristik data setelah seluruh tahapan preprocessing dilakukan?

1.3 Tujuan Penelitian

  1. Mengetahui kondisi awal dataset curah hujan melalui statistik deskriptif dan pengecekan kualitas data.
  2. Menerapkan tahapan preprocessing data yang terdiri dari penanganan missing value (interpolasi dan mode imputation), reduksi kardinalitas, stratified splitting, capping, Standard Scaler, one-hot encoding, dan SMOTE.
  3. Menginterpretasikan hasil setiap tahapan sehingga diperoleh data latih dan data uji yang siap digunakan untuk pemodelan.

1.4 Manfaat Penelitian

Adapun manfaat pada penelitian ini yaitu:

  1. Bagi penulis

    1. Memberikan informasi serta pengetahuan tentang Feature Engineering dalam Machine Learning.
    2. Mengembangkan dan menerapkan wawasan ilmu yang telah diperoleh dalam bidang statistika khususnya tentang Feature Engineering dengan RStudio.
  2. Bagi pembaca

    Menambah ilmu pengetahuan dan pemahaman yang berkaitan tentang Feature Engineering dengan RStudio.

1.5 Batasan Masalah

Adapun batasan masalah pada praktikum ini yaitu menggunakan dataset data curah hujan.xlsx untuk melakukan preprocessing data. Karena NPM penulis genap (F1F023044), maka missing value ditangani dengan mode imputation, pembagian data menggunakan stratify splitting, dan penskalaan menggunakan Standard Scaler. Selain itu, dilakukan interpolasi pada keadaan cuaca, capping pada data numerik, serta penanganan imbalance data menggunakan SMOTE. Setiap output ditampilkan dan diinterpretasikan.

1.6 Sistematika Penulisan

Adapun sistematika penulisan dari praktikum ini adalah sebagai berikut.

BAB IPENDAHULUAN

Bab ini terdiri dari latar belakang, rumusan masalah, batasan masalah, tujuan penelitian, manfaat penelitian, dan sistematika penulisan.

BAB IITINJAUAN PUSTAKA

Bab ini berisi kajian pustaka yang terdiri dari jurnal, buku, dan informasi pendukung mengenai feature engineering pada program RStudio.

BAB IIIMETODE PENELITIAN

Bab ini terdiri dari jenis dan sumber data, variabel penelitian, algoritma penelitian, dan diagram alir.

BAB IVHASIL DAN PEMBAHASAN

Bab ini berisi hasil yang diperoleh dari analisis data beserta penjelasannya pada bagian pembahasan.

BAB VKESIMPULAN DAN SARAN

Bab ini merupakan bagian penutup laporan yang berisi kesimpulan dari hasil analisis serta saran dari penulis.

DAFTAR PUSTAKA

LAMPIRAN

BAB II TINJAUAN PUSTAKA

2.1 Landasan Teori 1

Missing value adalah kondisi ketika suatu observasi tidak memiliki nilai pada variabel tertentu. Penanganannya dapat dilakukan dengan menghapus baris atau mengisi nilai yang hilang (imputasi). Pada praktikum ini digunakan dua teknik:

  • Interpolasi linear, yaitu mengisi nilai hilang berdasarkan garis lurus antara nilai sebelum dan sesudahnya. Teknik ini cocok untuk data yang berurutan (misalnya deret waktu). Jika nilai hilang berada pada posisi \(t\) di antara titik \((t_a, y_a)\) dan \((t_b, y_b)\), maka:

\[ \hat{y}_t = y_a + \frac{t - t_a}{t_b - t_a}\,(y_b - y_a) \]

  • Mode imputation, yaitu mengganti nilai hilang dengan modus variabel tersebut, yaitu nilai yang paling sering muncul pada data yang tersedia:

\[ \text{Mo}(x) = \arg\max_{v}\; f(v) \]

dengan \(f(v)\) adalah frekuensi kemunculan nilai \(v\). Teknik ini sederhana dan biasa dipakai pada variabel kategorik, tetapi dapat mengecilkan variasi data karena banyak observasi diisi dengan nilai yang sama (Han et al., 2012).

Kardinalitas adalah banyaknya kategori unik pada suatu variabel. Kardinalitas yang tinggi dapat membuat encoding menghasilkan terlalu banyak kolom, sehingga kategori dapat dikelompokkan (binning) menjadi lebih sedikit (Kuhn & Johnson, 2019).

Splitting data membagi data menjadi data latih (train) dan data uji (test). Shuffle splitting membagi data secara acak tanpa mempertimbangkan proporsi kelas, sedangkan stratified splitting membagi data secara acak di dalam setiap kelas sehingga proporsi kelas target pada data latih dan data uji tetap sama dengan proporsi pada data asli. Teknik ini penting ketika kelas target tidak seimbang.

2.2 Landasan Teori 2

Outlier dan capping. Salah satu aturan deteksi outlier adalah aturan Interquartile Range (IQR) (Tukey, 1977). Dengan \(IQR = Q_3 - Q_1\), nilai dianggap outlier jika berada di luar batas:

\[ \text{Batas bawah} = Q_1 - 1{,}5 \times IQR \qquad \text{Batas atas} = Q_3 + 1{,}5 \times IQR \]

Capping (winsorizing) mengganti nilai yang melewati batas dengan nilai batas tersebut sehingga jumlah observasi tidak berkurang.

Standard Scaler. Scaling menyamakan skala antarvariabel. Standard Scaler (standardisasi z-score) mengubah data sehingga memiliki rata-rata 0 dan simpangan baku 1 (Pedregosa et al., 2011):

\[ z = \frac{x - \bar{x}}{s} \]

dengan \(\bar{x}\) adalah rata-rata dan \(s\) adalah simpangan baku. Parameter rata-rata dan simpangan baku dihitung dari data latih saja, kemudian diterapkan pada data latih dan data uji untuk menghindari kebocoran data (data leakage). Karena memakai rata-rata dan simpangan baku, Standard Scaler sensitif terhadap outlier, sehingga capping dilakukan terlebih dahulu.

One-hot encoding mengubah variabel kategorik menjadi beberapa kolom biner (0/1), satu kolom untuk setiap kategori.

SMOTE (Synthetic Minority Over-sampling Technique) menangani data tidak seimbang dengan membuat observasi sintetis pada kelas minoritas. Sebuah titik baru dibentuk di antara satu observasi minoritas \(x_i\) dan salah satu tetangga terdekatnya \(x_{nn}\) (Chawla et al., 2002):

\[ x_{new} = x_i + \lambda\,(x_{nn} - x_i), \qquad \lambda \in [0,1] \]

BAB III METODE PENELITIAN

3.1 Jenis dan Sumber Data

Data yang digunakan adalah dataset praktikum Machine Learning berupa file data curah hujan.xlsx yang terdiri dari 743 observasi dan 5 variabel.

3.2 Variabel Penelitian

Variabel Jenis Keterangan
Hujan Kategorik biner (target) Kode kelas kejadian hujan (nilai 1 dan 2)
Suhu Numerik kontinu Suhu udara hasil pengamatan
Kelembapan Numerik Kelembapan udara hasil pengamatan
Keadaan_Cuaca Numerik berupa kode Kode keadaan cuaca; dikelompokkan menjadi kategori Keadaan_Cuaca_reduced
Kecepatan_Angin Numerik Kecepatan angin hasil pengamatan

3.3 Algoritma Penelitian

  1. Memuat library dan mengimpor data, lalu melihat struktur dan statistik deskriptif.
  2. Menangani missing value: interpolasi pada Keadaan_Cuaca dan mode imputation pada Kecepatan_Angin.
  3. Mereduksi kardinalitas Keadaan_Cuaca menjadi kategori Keadaan_Cuaca_reduced.
  4. Membagi data menjadi data latih (80%) dan data uji (20%) dengan stratified splitting berdasarkan variabel Hujan.
  5. Menangani outlier pada variabel numerik dengan capping berbasis IQR (batas dari data latih).
  6. Scaling variabel numerik dengan Standard Scaler (rata-rata dan simpangan baku dari data latih).
  7. Encoding variabel kategorik dengan one-hot encoding.
  8. Menangani data tidak seimbang dengan SMOTE hanya pada data latih.

3.4 Diagram Alir

Diagram alir pada penelitian ini menggunakan simbol standar: oval untuk terminator (mulai dan selesai), jajargenjang untuk input dan output, serta persegi panjang untuk proses.

Diagram Alir Penelitian

Diagram Alir Penelitian

Interpretasi. Diagram alir memperlihatkan urutan tahapan preprocessing yang dilakukan. Proses diawali dari terminator Mulai, lalu input berupa impor data data curah hujan.xlsx. Data kemudian melalui proses statistik deskriptif, penanganan missing value, reduksi kardinalitas, pembagian data secara stratifikasi, capping, scaling, encoding, dan diakhiri SMOTE yang hanya diterapkan pada data latih. Output berupa data latih dan data uji yang siap dimodelkan, kemudian proses berakhir pada terminator Selesai. Urutan ini menjaga agar seluruh parameter transformasi berasal dari data latih sehingga tidak terjadi kebocoran data.

BAB IV HASIL DAN PEMBAHASAN

4.1 Library dan Import Data

library(zoo) 
library(readxl)
library(tidyverse)
library(rsample)
library(e1071)
library(DescTools)
library(dplyr)
library(ggplot2)
library(gridExtra)
library(recipes)
library(themis)
library(caret)

data <- read_excel("D:/SEMESTER 7/PRAK SPASIAL/data curah hujan.xlsx")
head(data)
## # A tibble: 6 × 5
##   Hujan  Suhu Kelembapan Keadaan_Cuaca Kecepatan_Angin
##   <dbl> <dbl>      <dbl>         <dbl>           <dbl>
## 1     1  23           95             5              NA
## 2     1  24           90             1              NA
## 3     1  26.8         77             1              NA
## 4     1  29.6         62             2               2
## 5     1  30.8         56             1               7
## 6     1  31           55             1               7
str(data)
## tibble [743 × 5] (S3: tbl_df/tbl/data.frame)
##  $ Hujan          : num [1:743] 1 1 1 1 1 1 1 2 2 2 ...
##  $ Suhu           : num [1:743] 23 24 26.8 29.6 30.8 31 30.4 30.9 30.2 29.7 ...
##  $ Kelembapan     : num [1:743] 95 90 77 62 56 55 57 58 62 62 ...
##  $ Keadaan_Cuaca  : num [1:743] 5 1 1 2 1 1 3 2 2 2 ...
##  $ Kecepatan_Angin: num [1:743] NA NA NA 2 7 7 9 10 8 7 ...

Interpretasi. Data berhasil diimpor ke dalam R dan berisi 743 observasi dengan 5 variabel, yaitu Hujan, Suhu, Kelembapan, Keadaan_Cuaca, Kecepatan_Angin. Enam baris pertama (head) memberikan gambaran awal bahwa setiap baris mewakili satu pengamatan cuaca, sedangkan fungsi str menunjukkan tipe data setiap kolom. Seluruh variabel terbaca bertipe numerik (Hujan bertipe integer, sedangkan variabel lainnya bertipe double/integer). Hal ini penting diperhatikan karena variabel Hujan sebenarnya adalah kode kelas (1 dan 2), bukan ukuran kuantitatif, sehingga nantinya harus diubah menjadi faktor agar diperlakukan sebagai target klasifikasi. Demikian pula Keadaan_Cuaca yang berupa kode keadaan cuaca perlu dipertimbangkan untuk diperlakukan sebagai kategori, bukan angka kontinu. Pada tahap ini juga terlihat bahwa sebagian sel pada kolom tertentu kosong (NA), yang akan dibahas pada sub-bab berikutnya.

Statistik Deskriptif

summary(data)
##      Hujan            Suhu         Kelembapan     Keadaan_Cuaca  
##  Min.   :1.000   Min.   :22.60   Min.   : 52.00   Min.   : 1.00  
##  1st Qu.:2.000   1st Qu.:24.30   1st Qu.: 75.00   1st Qu.: 2.00  
##  Median :2.000   Median :26.00   Median : 86.00   Median : 2.00  
##  Mean   :1.779   Mean   :26.54   Mean   : 83.88   Mean   :15.11  
##  3rd Qu.:2.000   3rd Qu.:28.90   3rd Qu.: 94.00   3rd Qu.:15.00  
##  Max.   :2.000   Max.   :32.00   Max.   :100.00   Max.   :97.00  
##                                                   NAs    :9      
##  Kecepatan_Angin 
##  Min.   : 2.000  
##  1st Qu.: 4.000  
##  Median : 6.000  
##  Mean   : 6.655  
##  3rd Qu.: 9.000  
##  Max.   :21.000  
##  NAs    :314
cat("\nDistribusi kelas Hujan:\n")
## 
## Distribusi kelas Hujan:
table(data$Hujan)
## 
##   1   2 
## 164 579
round(prop.table(table(data$Hujan)) * 100, 2)
## 
##     1     2 
## 22.07 77.93

Interpretasi. Statistik deskriptif memberikan gambaran awal tentang pemusatan dan penyebaran setiap variabel sebelum dilakukan preprocessing. Informasi ini dipakai untuk menentukan perlakuan yang tepat pada tahap-tahap selanjutnya:

  • Suhu berkisar dari 22.6 sampai 32 dengan rata-rata 26.54 dan median 26. Kedekatan nilai rata-rata dan median menjadi petunjuk awal tentang apakah sebaran data cenderung simetris atau menjulur ke salah satu sisi.
  • Kelembapan berkisar dari 52 sampai 100 dengan rata-rata 83.88 dan median 86. Perbedaan rentang nilai antara Suhu dan Kelembapan menunjukkan bahwa kedua variabel memiliki skala yang berbeda, sehingga scaling diperlukan.
  • Kecepatan_Angin (tanpa memperhitungkan NA) berkisar dari 2 sampai 21 dengan rata-rata 6.66 dan median 6.
  • Keadaan_Cuaca memiliki mean (15.11) yang jauh lebih besar daripada median (2) dengan nilai maksimum 97. Selisih yang besar ini menunjukkan sebaran yang menjulur ke kanan, yaitu sebagian besar nilai kecil dan hanya sebagian kecil bernilai sangat besar. Karena variabel ini berupa kode keadaan cuaca (bukan ukuran kontinu), lebih tepat jika diperlakukan sebagai kategori dan dikelompokkan terlebih dahulu.
  • Missing value. Terdapat NA pada Keadaan_Cuaca dan Kecepatan_Angin, sehingga penanganan missing value wajib dilakukan sebelum pemodelan karena sebagian besar algoritma tidak dapat memproses nilai kosong.
  • Kelas target Hujan tidak seimbang: kelas 2 sebanyak 579 observasi (77.93%) dan kelas 1 sebanyak 164 observasi (22.07%). Perbandingan kelas mayoritas terhadap minoritas adalah sekitar 3.53 : 1. Jika data dibiarkan demikian, model berisiko lebih sering memprediksi kelas mayoritas dan kurang mampu mengenali kelas minoritas, sehingga diperlukan penanganan imbalance dengan SMOTE.

4.2 Handling Missing Value

# Cek Missing Value
colSums(is.na(data)) 
##           Hujan            Suhu      Kelembapan   Keadaan_Cuaca Kecepatan_Angin 
##               0               0               0               9             314
round(colMeans(is.na(data)) * 100, 2)
##           Hujan            Suhu      Kelembapan   Keadaan_Cuaca Kecepatan_Angin 
##            0.00            0.00            0.00            1.21           42.26
data %>% filter(is.na(Keadaan_Cuaca)) 
## # A tibble: 9 × 5
##   Hujan  Suhu Kelembapan Keadaan_Cuaca Kecepatan_Angin
##   <dbl> <dbl>      <dbl>         <dbl>           <dbl>
## 1     2  26           92            NA              NA
## 2     1  23.8         98            NA               5
## 3     1  23.3         97            NA              NA
## 4     2  28.8         79            NA              12
## 5     2  24.6         92            NA              NA
## 6     1  22.8         98            NA               4
## 7     2  25.5         96            NA              NA
## 8     2  27           80            NA               4
## 9     2  31           57            NA               6

Interpretasi. Pengecekan missing value menunjukkan bahwa Keadaan_Cuaca memiliki 9 data hilang (1.21%) dan Kecepatan_Angin memiliki 314 data hilang (42.26%), sedangkan Hujan, Suhu, dan Kelembapan lengkap tanpa NA. Proporsi data hilang pada Kecepatan_Angin jauh lebih besar dibandingkan Keadaan_Cuaca, sehingga dampak teknik imputasi yang dipilih akan lebih terasa pada Kecepatan_Angin. Tabel terakhir menampilkan baris-baris yang Keadaan_Cuaca-nya hilang; baris-baris ini diperlukan untuk memeriksa nilai di sekitarnya, karena interpolasi bergantung pada nilai sebelum dan sesudah baris yang hilang.

# (a) Hapus baris NA (hanya sebagai pembanding, tidak dipakai)
data_dropna <- data %>% drop_na() 
cat("Jumlah baris sebelum drop_na :", nrow(data), "\n")
## Jumlah baris sebelum drop_na : 743
cat("Jumlah baris sesudah drop_na :", nrow(data_dropna), "\n")
## Jumlah baris sesudah drop_na : 424
# (b) Interpolasi pada Keadaan_Cuaca
df_imp1 <- data
df_imp1$Keadaan_Cuaca <- na.approx(df_imp1$Keadaan_Cuaca, na.rm = FALSE, rule = 2) 
idx_na <- which(is.na(data$Keadaan_Cuaca))
data.frame(Baris = idx_na, 
           Hasil_Interpolasi = df_imp1$Keadaan_Cuaca[idx_na])
##   Baris Hasil_Interpolasi
## 1    42               2.0
## 2   207              61.0
## 3   310              62.0
## 4   512              49.0
## 5   598              21.0
## 6   639              56.0
## 7   675               2.0
## 8   723               2.0
## 9   725               1.5
colSums(is.na(df_imp1))
##           Hujan            Suhu      Kelembapan   Keadaan_Cuaca Kecepatan_Angin 
##               0               0               0               0             314
# (c) Mode imputation pada Kecepatan_Angin
# Fungsi modus: nilai yang paling sering muncul (NA diabaikan)
hitung_modus <- function(x) {
  x  <- x[!is.na(x)]
  ux <- unique(x)
  ux[which.max(tabulate(match(x, ux)))]
}

# Tabel frekuensi nilai yang paling sering muncul
head(sort(table(data$Kecepatan_Angin), decreasing = TRUE), 5)
## 
##  3  5  8  6  4 
## 57 55 47 40 39
df_imp2 <- df_imp1
modus_angin <- hitung_modus(df_imp2$Kecepatan_Angin)
modus_angin
## [1] 3
df_imp2$Kecepatan_Angin[is.na(df_imp2$Kecepatan_Angin)] <- modus_angin
colSums(is.na(df_imp2))
##           Hujan            Suhu      Kelembapan   Keadaan_Cuaca Kecepatan_Angin 
##               0               0               0               0               0
# Dampak mode imputation terhadap Kecepatan_Angin
data.frame(
  Kondisi = c("Sebelum imputasi", "Sesudah imputasi"),
  Mean   = c(mean(data$Kecepatan_Angin, na.rm = TRUE), mean(df_imp2$Kecepatan_Angin)),
  Median = c(median(data$Kecepatan_Angin, na.rm = TRUE), median(df_imp2$Kecepatan_Angin)),
  SD     = c(sd(data$Kecepatan_Angin, na.rm = TRUE), sd(df_imp2$Kecepatan_Angin))
)
##            Kondisi     Mean Median       SD
## 1 Sebelum imputasi 6.655012      6 3.352688
## 2 Sesudah imputasi 5.110363      3 3.122165
# Data hasil penanganan missing value yang dipakai selanjutnya
df_clean <- df_imp2

Interpretasi.

  • Penghapusan baris (drop_na) sebagai pembanding. Jika seluruh baris yang mengandung NA dihapus, data tersisa hanya 424 dari 743 baris, atau kehilangan 42.93% informasi. Kehilangan data sebesar ini tergolong besar dan dapat membuat model kehilangan banyak pola penting serta memperkecil ukuran sampel. Karena itu penghapusan baris tidak dipilih dan digantikan dengan imputasi.
  • Interpolasi pada Keadaan_Cuaca. Sebanyak 9 data hilang berhasil diisi berdasarkan nilai sebelum dan sesudahnya, sehingga jumlah NA pada Keadaan_Cuaca menjadi 0. Tabel hasil interpolasi memperlihatkan nilai pengisi pada setiap baris yang hilang. Nilai ini dapat berupa bilangan desimal karena merupakan titik di antara dua kode cuaca di sekitarnya. Hal ini bukan masalah, sebab pada tahap berikutnya variabel ini akan dikelompokkan ke dalam interval (binning) sehingga nilai desimal akan masuk ke kategori yang sesuai.
  • Mode imputation pada Kecepatan_Angin. Nilai yang paling sering muncul pada Kecepatan_Angin adalah 3, dan nilai inilah yang dipakai untuk mengisi 314 data hilang. Tabel frekuensi menampilkan lima nilai yang paling sering muncul sebagai dasar penentuan modus. Setelah imputasi, seluruh kolom tidak lagi memiliki NA, sehingga data siap diproses pada tahap selanjutnya.
  • Dampak mode imputation. Rata-rata Kecepatan_Angin berubah dari 6.655 menjadi 5.11, median dari 6 menjadi 3, dan simpangan baku dari 3.353 menjadi 3.122. Berbeda dengan mean imputation yang menjaga rata-rata tetap, mode imputation dapat menggeser rata-rata ke arah nilai modus, dan karena sebagian besar data hilang diisi dengan satu nilai yang sama, variasi data berkurang. Hal ini merupakan konsekuensi dari teknik modus yang perlu disadari, terutama karena persentase data hilang pada Kecepatan_Angin cukup besar.

4.3 Kardinalitas

set.seed(200)
head(df_clean, 10)
## # A tibble: 10 × 5
##    Hujan  Suhu Kelembapan Keadaan_Cuaca Kecepatan_Angin
##    <dbl> <dbl>      <dbl>         <dbl>           <dbl>
##  1     1  23           95             5               3
##  2     1  24           90             1               3
##  3     1  26.8         77             1               3
##  4     1  29.6         62             2               2
##  5     1  30.8         56             1               7
##  6     1  31           55             1               7
##  7     1  30.4         57             3               9
##  8     2  30.9         58             2              10
##  9     2  30.2         62             2               8
## 10     2  29.7         62             2               7
sort(unique(df_clean$Keadaan_Cuaca))
##  [1]  1.0  1.5  2.0  3.0  5.0 10.0 13.0 14.0 15.0 16.0 17.0 21.0 29.0 49.0 56.0
## [16] 60.0 61.0 62.0 63.0 65.0 91.0 95.0 97.0
bins <- c(0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100)
labels <- 0:9

df_clean$Keadaan_Cuaca_reduced <- cut(
  df_clean$Keadaan_Cuaca,
  breaks = bins,
  labels = labels,
  right = TRUE,
  include.lowest = TRUE
)

cat("--- Hasil Perbandingan ---\n")
## --- Hasil Perbandingan ---
print(df_clean[sample(nrow(df_clean), 10), ])
## # A tibble: 10 × 6
##    Hujan  Suhu Kelembapan Keadaan_Cuaca Kecepatan_Angin Keadaan_Cuaca_reduced
##    <dbl> <dbl>      <dbl>         <dbl>           <dbl> <fct>                
##  1     1  23.7         97            61               3 6                    
##  2     2  31           68             2              10 0                    
##  3     2  26.4         81             3               5 0                    
##  4     2  31.3         57             2               9 0                    
##  5     2  29.1         78             2               3 0                    
##  6     2  27.8         79             2               3 0                    
##  7     1  23.7         97            61               4 6                    
##  8     1  27.2         83            60               7 5                    
##  9     2  27.2         86             2               6 0                    
## 10     2  25.7         90             2               4 0
cat("\n--- Pengecekan Kardinalitas ---\n")
## 
## --- Pengecekan Kardinalitas ---
cat('Jumlah kategori di "Keadaan_Cuaca" asli    :', length(unique(df_clean$Keadaan_Cuaca)), "\n")
## Jumlah kategori di "Keadaan_Cuaca" asli    : 23
cat('Jumlah kategori di "Keadaan_Cuaca_reduced" :', length(unique(df_clean$Keadaan_Cuaca_reduced)), "\n")
## Jumlah kategori di "Keadaan_Cuaca_reduced" : 7
cat("\nKategori unik yang baru (reduced):\n")
## 
## Kategori unik yang baru (reduced):
print(sort(unique(df_clean$Keadaan_Cuaca_reduced)))
## [1] 0 1 2 4 5 6 9
## Levels: 0 1 2 3 4 5 6 7 8 9
cat("\nFrekuensi tiap kategori (reduced):\n")
## 
## Frekuensi tiap kategori (reduced):
table(df_clean$Keadaan_Cuaca_reduced)
## 
##   0   1   2   3   4   5   6   7   8   9 
## 523  48  46   0   1  28  71   0   0  26

Interpretasi.

  • Kondisi awal. Variabel Keadaan_Cuaca memiliki 23 nilai unik, yang berarti kardinalitasnya tinggi. Jika langsung di-one-hot encoding, akan terbentuk 23 kolom biner yang sebagian besar jarang bernilai 1, sehingga dimensi data membengkak dan model mudah mengalami overfitting.
  • Proses reduksi. Nilai dikelompokkan ke dalam interval berlebar 10 (0-10, 10-20, …, 90-100) dan diberi label 0 sampai 9. Tabel “Hasil Perbandingan” memperlihatkan sepuluh baris acak yang membandingkan nilai asli Keadaan_Cuaca dengan kategori barunya, sehingga terlihat bahwa nilai-nilai yang berdekatan dipetakan ke kategori yang sama.
  • Hasil. Kategori yang benar-benar terisi tinggal 7, dan terdapat 3 kategori yang kosong (tidak memiliki observasi). Tabel frekuensi menunjukkan bahwa kategori 0 paling dominan, yaitu 523 observasi (70.39%). Artinya sebagian besar kode cuaca bernilai kecil, konsisten dengan sebaran menjulur ke kanan yang tampak pada statistik deskriptif.
  • Catatan teknis. Kategori kosong tetap tercatat sebagai level faktor agar kolom one-hot pada data latih dan data uji konsisten. Pengelompokan ini menyederhanakan variabel, tetapi konsekuensinya sebagian detail informasi hilang karena kode cuaca yang berbeda dalam satu interval dianggap setara.

4.4 Splitting Data (Stratified Splitting)

df_clean$Hujan <- factor(df_clean$Hujan)

set.seed(200)   
split_strat <- initial_split(df_clean, prop = 0.8, strata = Hujan)   # strata = stratified splitting

X_train <- training(split_strat) %>% dplyr::select(-Hujan)
X_test  <- testing(split_strat)  %>% dplyr::select(-Hujan)
y_train <- training(split_strat)$Hujan
y_test  <- testing(split_strat)$Hujan

cat("Dimensi X_train :", dim(X_train), "\n")
## Dimensi X_train : 594 5
cat("Dimensi X_test  :", dim(X_test), "\n")
## Dimensi X_test  : 149 5
## samakan level Keadaan_Cuaca_reduced
all_levels <- levels(df_clean$Keadaan_Cuaca_reduced)
X_train$Keadaan_Cuaca_reduced <- factor(X_train$Keadaan_Cuaca_reduced, levels = all_levels)
X_test$Keadaan_Cuaca_reduced  <- factor(X_test$Keadaan_Cuaca_reduced,  levels = all_levels)

cat("\nDistribusi kelas data asli:\n")
## 
## Distribusi kelas data asli:
table(df_clean$Hujan)
## 
##   1   2 
## 164 579
round(prop.table(table(df_clean$Hujan)) * 100, 2)
## 
##     1     2 
## 22.07 77.93
cat("\nDistribusi kelas data latih:\n")
## 
## Distribusi kelas data latih:
table(y_train)
## y_train
##   1   2 
## 131 463
round(prop.table(table(y_train)) * 100, 2)
## y_train
##     1     2 
## 22.05 77.95
cat("\nDistribusi kelas data uji:\n")
## 
## Distribusi kelas data uji:
table(y_test)
## y_test
##   1   2 
##  33 116
round(prop.table(table(y_test)) * 100, 2)
## y_test
##     1     2 
## 22.15 77.85

Interpretasi.

  • Ukuran data. Data dibagi dengan stratified splitting berdasarkan variabel Hujan (set.seed(200)) menjadi 594 observasi data latih (79.9%) dan 149 observasi data uji (20.1%). Dimensi X_train dan X_test memiliki jumlah kolom yang sama, yaitu 5 variabel prediktor (target Hujan dipisahkan sebagai y_train dan y_test).
  • Proporsi kelas. Pada data asli, kelas 2 sebesar 77.93%. Pada data latih kelas 2 sebesar 77.95% (463 observasi) dan kelas 1 sebesar 22.05% (131 observasi). Pada data uji kelas 2 sebesar 77.85% (116 observasi) dan kelas 1 sebesar 22.15% (33 observasi). Ketiga proporsi ini sangat berdekatan, yang membuktikan bahwa stratifikasi berhasil menjaga komposisi kelas.
  • Keunggulan stratifikasi. Dibandingkan shuffle splitting yang proporsi kelasnya dapat bergeser secara kebetulan, stratified splitting menjamin kelas minoritas tetap terwakili pada data uji. Hal ini penting agar evaluasi model tidak bias dan metrik pada kelas minoritas dapat dihitung secara andal.
  • Ketidakseimbangan tetap ada. Kedua subset sama-sama didominasi kelas 2, sehingga masalah imbalance belum teratasi dan akan ditangani dengan SMOTE hanya pada data latih. Seluruh tahap berikutnya (capping, scaling, encoding, SMOTE) menggunakan parameter yang dihitung dari data latih saja agar tidak terjadi kebocoran data (data leakage) dari data uji.

4.5 Handling Outlier

list_num <- c("Suhu", "Kelembapan", "Kecepatan_Angin")

list_outlier      <- c()
list_lower_bound  <- c()
list_upper_bound  <- c()

for (i in list_num) {
  Q1  <- unname(quantile(X_train[[i]], 0.25, na.rm = TRUE))
  Q3  <- unname(quantile(X_train[[i]], 0.75, na.rm = TRUE))
  IQR_val <- Q3 - Q1
  
  lower_bound <- Q1 - 1.5 * IQR_val
  upper_bound <- Q3 + 1.5 * IQR_val
  
  list_lower_bound <- c(list_lower_bound, lower_bound)
  list_upper_bound <- c(list_upper_bound, upper_bound)
  
  num_outliers_lower <- sum(X_train[[i]] < lower_bound, na.rm = TRUE)
  num_outliers_upper <- sum(X_train[[i]] > upper_bound, na.rm = TRUE)
  
  list_outlier <- c(list_outlier, num_outliers_lower + num_outliers_upper)
}

outliers <- data.frame(
  Kolom = list_num,
  Jumlah_Outlier = list_outlier,
  Lower_Bound = list_lower_bound,
  Upper_Bound = list_upper_bound
)
outliers
##             Kolom Jumlah_Outlier Lower_Bound Upper_Bound
## 1            Suhu              0     17.4625     35.7625
## 2      Kelembapan              0     46.5000    122.5000
## 3 Kecepatan_Angin             12     -3.0000     13.0000
# Skewness sebelum capping
skew_sebelum <- sapply(X_train[list_num], skewness, na.rm = TRUE)
round(skew_sebelum, 3)
##            Suhu      Kelembapan Kecepatan_Angin 
##           0.288          -0.552           1.237
# Capping (batas dari data latih, diterapkan ke data latih dan data uji)
X_train_capped <- X_train
X_test_capped  <- X_test

for (k in seq_along(list_num)) {
  kol <- list_num[k]
  batas <- c(list_lower_bound[k], list_upper_bound[k])
  X_train_capped[[kol]] <- Winsorize(X_train[[kol]], val = batas)
  X_test_capped[[kol]]  <- Winsorize(X_test[[kol]],  val = batas)
}

# Cek jumlah outlier setelah capping (data latih)
outlier_sesudah <- sapply(seq_along(list_num), function(k) {
  x <- X_train_capped[[list_num[k]]]
  sum(x < list_lower_bound[k] | x > list_upper_bound[k])
})
data.frame(Kolom = list_num, Outlier_Sebelum = list_outlier, Outlier_Sesudah = outlier_sesudah)
##             Kolom Outlier_Sebelum Outlier_Sesudah
## 1            Suhu               0               0
## 2      Kelembapan               0               0
## 3 Kecepatan_Angin              12               0
# Ringkasan sebelum vs sesudah capping
summary(X_train[list_num])
##       Suhu         Kelembapan     Kecepatan_Angin 
##  Min.   :22.60   Min.   : 52.00   Min.   : 2.000  
##  1st Qu.:24.32   1st Qu.: 75.00   1st Qu.: 3.000  
##  Median :26.00   Median : 86.00   Median : 3.000  
##  Mean   :26.56   Mean   : 83.76   Mean   : 5.098  
##  3rd Qu.:28.90   3rd Qu.: 94.00   3rd Qu.: 7.000  
##  Max.   :32.00   Max.   :100.00   Max.   :15.000
summary(X_train_capped[list_num])
##       Suhu         Kelembapan     Kecepatan_Angin 
##  Min.   :22.60   Min.   : 52.00   Min.   : 2.000  
##  1st Qu.:24.32   1st Qu.: 75.00   1st Qu.: 3.000  
##  Median :26.00   Median : 86.00   Median : 3.000  
##  Mean   :26.56   Mean   : 83.76   Mean   : 5.069  
##  3rd Qu.:28.90   3rd Qu.: 94.00   3rd Qu.: 7.000  
##  Max.   :32.00   Max.   :100.00   Max.   :13.000
# Skewness sesudah capping
skew_sesudah <- sapply(X_train_capped[list_num], skewness, na.rm = TRUE)
round(skew_sesudah, 3)
##            Suhu      Kelembapan Kecepatan_Angin 
##           0.288          -0.552           1.126
# Visualisasi sebelum dan sesudah capping
diagnostic_plots <- function(df, variable, judul) {
  p1 <- ggplot(df, aes(x = .data[[variable]])) +
    geom_histogram(bins = 30, fill = "#008080", color = "black") +
    ggtitle(paste("Histogram -", judul)) +
    theme_minimal()
  
  p2 <- ggplot(df, aes(y = .data[[variable]])) +
    geom_boxplot(fill = "#008080") +
    ggtitle(paste("Boxplot -", judul)) +
    theme_minimal()
  
  grid.arrange(p1, p2, ncol = 2)
}

for (col in list_num) {
  diagnostic_plots(X_train, col, paste(col, "(Sebelum Capping)"))
  diagnostic_plots(X_train_capped, col, paste(col, "(Sesudah Capping)"))
}

Interpretasi.

  • Tabel batas dan jumlah outlier. Batas bawah dan batas atas dihitung dengan aturan IQR (\(Q_1 - 1{,}5 \times IQR\) dan \(Q_3 + 1{,}5 \times IQR\)) dari data latih. Jumlah outlier pada Suhu adalah 0 (0%), Kelembapan 0 (0%), dan Kecepatan_Angin 12 (2.02%). Variabel dengan outlier terbanyak adalah Kecepatan_Angin, sehingga variabel inilah yang paling terdampak oleh capping. Batas bawah dan batas atas masing-masing variabel adalah: Suhu (17.46 sampai 35.76), Kelembapan (46.5 sampai 122.5), dan Kecepatan_Angin (-3 sampai 13).
  • Pengaruh imputasi terhadap Kecepatan_Angin. Sebanyak 42.26% data Kecepatan_Angin diisi dengan nilai modus yang sama. Penumpukan nilai pada satu titik cenderung menyempitkan rentang antarkuartil (IQR), sehingga batas atas menjadi rendah (13) dan nilai kecepatan angin yang relatif tinggi lebih mudah terdeteksi sebagai outlier. Dengan kata lain, sebagian outlier yang terdeteksi merupakan akibat dari teknik imputasi, bukan semata-mata nilai ekstrem alami dari pengamatan.
  • Capping. Nilai di luar batas diganti dengan nilai batas (winsorizing), tanpa menghapus observasi, sehingga jumlah baris data latih tetap 594. Setelah capping, jumlah outlier pada ketiga variabel menjadi 0, yang menunjukkan bahwa seluruh nilai ekstrem telah ditarik ke dalam batas. Nilai maksimum Kecepatan_Angin berubah dari 15 menjadi 13. Batas yang dihitung dari data latih juga dipakai pada data uji agar tidak ada kebocoran data.
  • Skewness. Sebelum capping, Suhu memiliki skewness 0.288 (mendekati simetris), Kelembapan -0.552 (agak miring menjulur ke kiri), dan Kecepatan_Angin 1.237 (sangat miring menjulur ke kanan). Setelah capping, nilainya menjadi 0.288 untuk Suhu (mendekati simetris), -0.552 untuk Kelembapan (agak miring menjulur ke kiri), dan 1.126 untuk Kecepatan_Angin (sangat miring menjulur ke kanan). Penurunan nilai mutlak skewness berarti distribusi menjadi lebih simetris karena ekor ekstrem terpotong; perubahan terbesar terjadi pada variabel yang paling banyak di-capping.
  • Visualisasi. Pada boxplot sebelum capping, titik-titik di luar whisker menunjukkan keberadaan outlier, sedangkan pada boxplot sesudah capping titik tersebut sudah tidak ada dan whisker mencakup seluruh data. Histogram variabel yang tidak memiliki outlier praktis tidak berubah, sedangkan histogram variabel yang di-capping tampak memiliki tumpukan nilai pada ujung batas, yang merupakan ciri khas hasil winsorizing.

4.6 Scaling Data (Standard Scaler)

# Parameter Standard Scaler dihitung dari data latih
mean_val <- sapply(X_train_capped[list_num], mean, na.rm = TRUE)
sd_val   <- sapply(X_train_capped[list_num], sd, na.rm = TRUE)

rbind(Mean = mean_val, SD = sd_val)
##           Suhu Kelembapan Kecepatan_Angin
## Mean 26.557912   83.75758        5.069024
## SD    2.557246   11.55098        2.987374
X_train_scale <- X_train_capped
X_test_scale  <- X_test_capped

for (col in list_num) {
  X_train_scale[[col]] <- (X_train_capped[[col]] - unname(mean_val[col])) / unname(sd_val[col])
  X_test_scale[[col]]  <- (X_test_capped[[col]]  - unname(mean_val[col])) / unname(sd_val[col])
}

head(X_train_scale)
## # A tibble: 6 × 5
##      Suhu Kelembapan Keadaan_Cuaca Kecepatan_Angin Keadaan_Cuaca_reduced
##     <dbl>      <dbl>         <dbl>           <dbl> <fct>                
## 1 -1.39        0.973             5          -0.693 0                    
## 2 -1.00        0.540             1          -0.693 0                    
## 3  0.0947     -0.585             1          -0.693 0                    
## 4  1.19       -1.88              2          -1.03  0                    
## 5  1.66       -2.40              1           0.646 0                    
## 6  1.50       -2.32              3           1.32  0
cat("\nMean & SD data latih setelah scaling:\n")
## 
## Mean & SD data latih setelah scaling:
round(rbind(Mean = sapply(X_train_scale[list_num], mean),
            SD   = sapply(X_train_scale[list_num], sd)), 3)
##      Suhu Kelembapan Kecepatan_Angin
## Mean    0          0               0
## SD      1          1               1
cat("\nMean & SD data uji setelah scaling:\n")
## 
## Mean & SD data uji setelah scaling:
round(rbind(Mean = sapply(X_test_scale[list_num], mean),
            SD   = sapply(X_test_scale[list_num], sd)), 3)
##        Suhu Kelembapan Kecepatan_Angin
## Mean -0.033      0.054           0.008
## SD    1.003      1.007           1.037
cat("\nRingkasan data latih setelah scaling:\n")
## 
## Ringkasan data latih setelah scaling:
summary(X_train_scale[list_num])
##       Suhu           Kelembapan      Kecepatan_Angin  
##  Min.   :-1.5477   Min.   :-2.7493   Min.   :-1.0273  
##  1st Qu.:-0.8732   1st Qu.:-0.7582   1st Qu.:-0.6926  
##  Median :-0.2182   Median : 0.1941   Median :-0.6926  
##  Mean   : 0.0000   Mean   : 0.0000   Mean   : 0.0000  
##  3rd Qu.: 0.9159   3rd Qu.: 0.8867   3rd Qu.: 0.6464  
##  Max.   : 2.1281   Max.   : 1.4062   Max.   : 2.6548
cat("\nRingkasan data uji setelah scaling:\n")
## 
## Ringkasan data uji setelah scaling:
summary(X_test_scale[list_num])
##       Suhu            Kelembapan       Kecepatan_Angin    
##  Min.   :-1.54772   Min.   :-2.48962   Min.   :-1.027332  
##  1st Qu.:-0.88295   1st Qu.:-0.75817   1st Qu.:-0.692589  
##  Median :-0.21817   Median : 0.28071   Median :-0.692589  
##  Mean   :-0.03314   Mean   : 0.05352   Mean   : 0.008347  
##  3rd Qu.: 0.75944   3rd Qu.: 0.88671   3rd Qu.: 0.646379  
##  Max.   : 1.97169   Max.   : 1.40615   Max.   : 2.654832
X_train_scale %>%
  dplyr::select(all_of(list_num)) %>%
  pivot_longer(everything(), names_to = "Variabel", values_to = "Nilai") %>%
  ggplot(aes(x = Variabel, y = Nilai, fill = Variabel)) +
  geom_boxplot() +
  labs(title = "Boxplot Data Latih Setelah Standard Scaler") +
  theme_minimal() +
  theme(legend.position = "none")

Interpretasi.

  • Parameter scaling. Standard Scaler mengubah setiap nilai menjadi \(z = (x - \bar{x}) / s\). Parameter dihitung dari data latih (setelah capping): rata-rata Suhu, Kelembapan, dan Kecepatan_Angin berturut-turut 26.558, 83.758, dan 5.069, dengan simpangan baku 2.557, 11.551, dan 2.987. Perbedaan yang mencolok antarvariabel pada rata-rata dan simpangan baku ini menegaskan bahwa ketiga variabel semula berada pada skala yang berbeda.
  • Hasil pada data latih. Setelah scaling, rata-rata ketiga variabel pada data latih menjadi 0, 0, dan 0 (praktis 0), dan simpangan bakunya menjadi 1, 1, dan 1 (tepat 1). Dengan demikian, ketiga variabel berada pada skala yang sebanding sehingga tidak ada variabel yang mendominasi perhitungan jarak atau gradien pada algoritma seperti KNN, SVM, atau regresi logistik.
  • Hasil pada data uji. Rata-rata data uji setelah scaling adalah -0.033 (Suhu), 0.054 (Kelembapan), dan 0.008 (Kecepatan_Angin), dengan simpangan baku 1.003, 1.007, dan 1.037. Nilainya mendekati, tetapi tidak harus persis, 0 dan 1 karena data uji hanya ditransformasi menggunakan parameter data latih. Inilah praktik yang benar untuk mencegah kebocoran data.
  • Arti nilai hasil scaling. Nilai negatif berarti pengamatan berada di bawah rata-rata data latih dan nilai positif berarti di atas rata-rata, dengan satuan berupa simpangan baku. Misalnya nilai 1 berarti satu simpangan baku di atas rata-rata.
  • Hubungan dengan capping. Standard Scaler sensitif terhadap outlier karena rata-rata dan simpangan baku mudah tertarik oleh nilai ekstrem. Oleh sebab itu capping dilakukan lebih dahulu sehingga parameter scaling tidak terdistorsi. Boxplot data latih setelah scaling memperlihatkan ketiga variabel berpusat di sekitar nol dengan rentang yang sebanding.

4.7 Encoding

## Encoding - One Hot Encoder
list_cat <- c("Keadaan_Cuaca_reduced")

resep_encode <- recipe(~ Keadaan_Cuaca_reduced, data = X_train_scale) %>%
  step_dummy(all_of(list_cat), one_hot = TRUE) %>%
  prep(training = X_train_scale)

X_train_encoded <- bake(resep_encode, new_data = NULL)
X_test_encoded  <- bake(resep_encode, new_data = X_test_scale)

X_train_encoded
## # A tibble: 594 × 10
##    Keadaan_Cuaca_reduced_X0 Keadaan_Cuaca_reduced_X1 Keadaan_Cuaca_reduced_X2
##                       <dbl>                    <dbl>                    <dbl>
##  1                        1                        0                        0
##  2                        1                        0                        0
##  3                        1                        0                        0
##  4                        1                        0                        0
##  5                        1                        0                        0
##  6                        1                        0                        0
##  7                        1                        0                        0
##  8                        1                        0                        0
##  9                        1                        0                        0
## 10                        1                        0                        0
## # ℹ 584 more rows
## # ℹ 7 more variables: Keadaan_Cuaca_reduced_X3 <dbl>,
## #   Keadaan_Cuaca_reduced_X4 <dbl>, Keadaan_Cuaca_reduced_X5 <dbl>,
## #   Keadaan_Cuaca_reduced_X6 <dbl>, Keadaan_Cuaca_reduced_X7 <dbl>,
## #   Keadaan_Cuaca_reduced_X8 <dbl>, Keadaan_Cuaca_reduced_X9 <dbl>
X_test_encoded
## # A tibble: 149 × 10
##    Keadaan_Cuaca_reduced_X0 Keadaan_Cuaca_reduced_X1 Keadaan_Cuaca_reduced_X2
##                       <dbl>                    <dbl>                    <dbl>
##  1                        1                        0                        0
##  2                        1                        0                        0
##  3                        1                        0                        0
##  4                        1                        0                        0
##  5                        1                        0                        0
##  6                        1                        0                        0
##  7                        1                        0                        0
##  8                        1                        0                        0
##  9                        1                        0                        0
## 10                        0                        0                        0
## # ℹ 139 more rows
## # ℹ 7 more variables: Keadaan_Cuaca_reduced_X3 <dbl>,
## #   Keadaan_Cuaca_reduced_X4 <dbl>, Keadaan_Cuaca_reduced_X5 <dbl>,
## #   Keadaan_Cuaca_reduced_X6 <dbl>, Keadaan_Cuaca_reduced_X7 <dbl>,
## #   Keadaan_Cuaca_reduced_X8 <dbl>, Keadaan_Cuaca_reduced_X9 <dbl>
cat("Dimensi X_train_encoded :", dim(X_train_encoded), "\n")
## Dimensi X_train_encoded : 594 10
cat("Dimensi X_test_encoded  :", dim(X_test_encoded), "\n")
## Dimensi X_test_encoded  : 149 10
colnames(X_train_encoded)
##  [1] "Keadaan_Cuaca_reduced_X0" "Keadaan_Cuaca_reduced_X1"
##  [3] "Keadaan_Cuaca_reduced_X2" "Keadaan_Cuaca_reduced_X3"
##  [5] "Keadaan_Cuaca_reduced_X4" "Keadaan_Cuaca_reduced_X5"
##  [7] "Keadaan_Cuaca_reduced_X6" "Keadaan_Cuaca_reduced_X7"
##  [9] "Keadaan_Cuaca_reduced_X8" "Keadaan_Cuaca_reduced_X9"

Interpretasi.

  • Struktur hasil. One-hot encoding mengubah Keadaan_Cuaca_reduced menjadi 10 kolom biner, satu kolom untuk setiap kategori (0 sampai 9). Pada setiap baris tepat satu kolom bernilai 1 dan sisanya 0, sehingga model tidak mengira ada urutan atau jarak numerik antarkategori, yang akan terjadi jika label 0 sampai 9 dipakai langsung sebagai angka.
  • Dimensi. Data latih menghasilkan 594 baris dan data uji 149 baris, dengan jumlah kolom yang sama (10 kolom). Kesamaan struktur ini wajib agar model yang dilatih pada data latih dapat langsung dipakai pada data uji.
  • Kolom aktif dan kosong. Pada data latih terdapat 6 kolom yang memiliki minimal satu nilai 1 (kategori muncul) dan 4 kolom yang seluruhnya 0 (kategori tidak muncul). Kolom kosong tidak membawa informasi, tetapi sengaja dipertahankan agar struktur data latih dan data uji tetap seragam.
  • Dampak reduksi kardinalitas. Berkat pengelompokan pada tahap sebelumnya, jumlah kolom hasil encoding hanya 10, jauh lebih sedikit dibandingkan jika 23 nilai unik asli di-encode langsung. Ini menjaga dimensi data tetap ringkas dan mengurangi risiko overfitting.

4.8 Handling Imbalance Data (SMOTE)

# Gabungkan fitur numerik (sudah di-scaling) + fitur hasil encoding + target
# (variabel Keadaan_Cuaca asli tidak ikut karena sudah diwakili hasil encoding)
X_train_final <- bind_cols(X_train_scale[list_num], X_train_encoded) %>%
  mutate(Hujan = y_train)

X_test_final <- bind_cols(X_test_scale[list_num], X_test_encoded) %>%
  mutate(Hujan = y_test)

cat("Distribusi kelas data latih SEBELUM SMOTE:\n")
## Distribusi kelas data latih SEBELUM SMOTE:
table(X_train_final$Hujan)
## 
##   1   2 
## 131 463
round(prop.table(table(X_train_final$Hujan)) * 100, 2)
## 
##     1     2 
## 22.05 77.95
# SMOTE hanya pada data latih
set.seed(200)
resep_smote <- recipe(Hujan ~ ., data = X_train_final) %>%
  step_smote(Hujan, over_ratio = 1, neighbors = 5) %>%
  prep(training = X_train_final)

train_smote <- bake(resep_smote, new_data = NULL)

cat("\nDistribusi kelas data latih SESUDAH SMOTE:\n")
## 
## Distribusi kelas data latih SESUDAH SMOTE:
table(train_smote$Hujan)
## 
##   1   2 
## 463 463
round(prop.table(table(train_smote$Hujan)) * 100, 2)
## 
##  1  2 
## 50 50
cat("\nDimensi data latih sebelum & sesudah SMOTE:\n")
## 
## Dimensi data latih sebelum & sesudah SMOTE:
cat("Sebelum:", dim(X_train_final), "| Sesudah:", dim(train_smote), "\n")
## Sebelum: 594 14 | Sesudah: 926 14
cat("\nDistribusi kelas data uji (tidak diubah):\n")
## 
## Distribusi kelas data uji (tidak diubah):
table(X_test_final$Hujan)
## 
##   1   2 
##  33 116
head(train_smote)
## # A tibble: 6 × 14
##      Suhu Kelembapan Kecepatan_Angin Keadaan_Cuaca_reduced_X0
##     <dbl>      <dbl>           <dbl>                    <dbl>
## 1 -1.39        0.973          -0.693                        1
## 2 -1.00        0.540          -0.693                        1
## 3  0.0947     -0.585          -0.693                        1
## 4  1.19       -1.88           -1.03                         1
## 5  1.66       -2.40            0.646                        1
## 6  1.50       -2.32            1.32                         1
## # ℹ 10 more variables: Keadaan_Cuaca_reduced_X1 <dbl>,
## #   Keadaan_Cuaca_reduced_X2 <dbl>, Keadaan_Cuaca_reduced_X3 <dbl>,
## #   Keadaan_Cuaca_reduced_X4 <dbl>, Keadaan_Cuaca_reduced_X5 <dbl>,
## #   Keadaan_Cuaca_reduced_X6 <dbl>, Keadaan_Cuaca_reduced_X7 <dbl>,
## #   Keadaan_Cuaca_reduced_X8 <dbl>, Keadaan_Cuaca_reduced_X9 <dbl>, Hujan <fct>
# Visualisasi
dist_kelas <- bind_rows(
  data.frame(Tahap = "Sebelum SMOTE", Hujan = X_train_final$Hujan),
  data.frame(Tahap = "Sesudah SMOTE", Hujan = train_smote$Hujan)
)
dist_kelas$Tahap <- factor(dist_kelas$Tahap, levels = c("Sebelum SMOTE", "Sesudah SMOTE"))

ggplot(dist_kelas, aes(x = Hujan, fill = Hujan)) +
  geom_bar() +
  geom_text(stat = "count", aes(label = after_stat(count)), vjust = -0.3) +
  facet_wrap(~ Tahap) +
  labs(title = "Distribusi Kelas Hujan pada Data Latih", x = "Hujan", y = "Frekuensi") +
  theme_minimal() +
  theme(legend.position = "none")

cat("\nDimensi akhir:\n")
## 
## Dimensi akhir:
cat("Data latih (SMOTE):", dim(train_smote), "\n")
## Data latih (SMOTE): 926 14
cat("Data uji          :", dim(X_test_final), "\n")
## Data uji          : 149 14

Interpretasi.

  • Sebelum SMOTE. Data latih terdiri dari 463 observasi kelas 2 (mayoritas) dan 131 observasi kelas 1 (minoritas), atau proporsi 77.95% : 22.05%. Selisih kedua kelas sebesar 332 observasi. Ketimpangan ini membuat model berisiko condong memprediksi kelas mayoritas dan kurang peka terhadap kelas minoritas, sehingga akurasi yang tinggi dapat menyesatkan.
  • Sesudah SMOTE. Kedua kelas masing-masing berjumlah 463 (kelas 2) dan 463 (kelas 1), sehingga proporsinya seimbang (50% : 50%). Jumlah baris data latih bertambah dari 594 menjadi 926 karena dibentuk 332 observasi sintetis kelas minoritas melalui interpolasi antara observasi minoritas dan lima tetangga terdekatnya (K = 5). Jumlah kelas mayoritas tidak berubah, sehingga tidak ada informasi asli yang dibuang.
  • Data uji tidak diubah. SMOTE hanya diterapkan pada data latih. Data uji (149 observasi) dibiarkan apa adanya, dengan 116 observasi kelas 2 dan 33 observasi kelas 1, sehingga evaluasi model tetap mencerminkan kondisi data yang sebenarnya dan tidak terkontaminasi data sintetis.
  • Visualisasi. Diagram batang menunjukkan perbedaan tinggi batang kedua kelas yang mencolok sebelum SMOTE dan menjadi setara setelah SMOTE.
  • Struktur data akhir. Hasil head(train_smote) menunjukkan data akhir berisi tiga variabel numerik yang sudah di-scaling, 10 kolom one-hot Keadaan_Cuaca_reduced, dan target Hujan (total 14 kolom). Kolom one-hot pada observasi sintetis dapat bernilai pecahan karena dibentuk dari interpolasi, bukan dari kategori asli; hal ini wajar dalam SMOTE standar dan perlu diingat saat menginterpretasikan data sintetis.

BAB V KESIMPULAN

5.1 Kesimpulan

  1. Kondisi awal data. Dataset curah hujan berisi 743 observasi dan 5 variabel. Data memiliki missing value pada Keadaan_Cuaca (9 data atau 1.21%) dan Kecepatan_Angin (314 data atau 42.26%), skala antarvariabel yang berbeda, kardinalitas tinggi pada Keadaan_Cuaca (23 nilai unik), serta kelas target yang tidak seimbang (77.93% kelas 2 dan 22.07% kelas 1). Kondisi ini menegaskan bahwa data belum layak langsung dipakai untuk pemodelan.
  2. Penanganan missing value. Menghapus seluruh baris yang mengandung NA hanya menyisakan 424 dari 743 baris, sehingga imputasi dipilih. Interpolasi mengisi data hilang pada Keadaan_Cuaca, dan mode imputation mengisi data hilang pada Kecepatan_Angin dengan nilai 3. Seluruh observasi dipertahankan, tetapi mode imputation menggeser rata-rata (6.655 menjadi 5.11) dan mengecilkan simpangan baku (3.353 menjadi 3.122), sehingga variasi Kecepatan_Angin berkurang.
  3. Reduksi kardinalitas dan encoding. Kardinalitas Keadaan_Cuaca berhasil direduksi dari 23 nilai unik menjadi 7 kategori terisi (dari 10 kategori yang tersedia), lalu di-encode dengan one-hot encoding menjadi 10 kolom biner dengan struktur yang sama pada data latih dan data uji.
  4. Stratified splitting. Data terbagi menjadi 594 data latih dan 149 data uji dengan proporsi kelas Hujan yang terjaga (kelas 2 sebesar 77.95% pada data latih dan 77.85% pada data uji, dibandingkan 77.93% pada data asli), sehingga evaluasi model kelak lebih adil dan representatif.
  5. Capping dan Standard Scaler. Capping berbasis IQR menangani outlier (total 12 nilai ekstrem pada data latih menjadi 0) tanpa mengurangi jumlah data, dan menurunkan kemencengan distribusi terutama pada variabel yang memiliki outlier. Standard Scaler kemudian menyamakan skala variabel numerik sehingga rata-rata data latih menjadi 0 dan simpangan baku menjadi 1, dengan parameter yang hanya dihitung dari data latih untuk mencegah kebocoran data.
  6. Penanganan data tidak seimbang. SMOTE menyeimbangkan kelas Hujan pada data latih menjadi 463 : 463 (50% : 50%) dengan menambahkan 332 observasi sintetis, sementara data uji tetap utuh. Hasil akhirnya adalah data latih berukuran 926 baris dan data uji berukuran 149 baris dengan 14 kolom, yang siap digunakan pada tahap pemodelan klasifikasi.
  7. Catatan dan saran. Urutan tahapan (imputasi, reduksi kardinalitas, splitting, capping, scaling, encoding, lalu SMOTE) penting dijaga agar parameter transformasi hanya berasal dari data latih dan tidak terjadi kebocoran data. Pengisian dengan mode imputation pada variabel dengan persentase data hilang yang besar perlu dicermati karena dapat mengurangi variasi data. Pada penelitian selanjutnya dapat dibandingkan teknik imputasi lain atau teknik penyeimbangan kelas lain, kemudian dievaluasi dampaknya terhadap kinerja model menggunakan metrik yang peka terhadap ketidakseimbangan kelas seperti recall, F1-score, dan AUC.

DAFTAR PUSTAKA

Chawla, N. V., Bowyer, K. W., Hall, L. O., & Kegelmeyer, W. P. (2002). SMOTE: Synthetic Minority Over-sampling Technique. Journal of Artificial Intelligence Research, 16, 321-357.

Han, J., Kamber, M., & Pei, J. (2012). Data Mining: Concepts and Techniques (3rd ed.). Morgan Kaufmann.

Kuhn, M., & Johnson, K. (2019). Feature Engineering and Selection: A Practical Approach for Predictive Models. CRC Press.

Pedregosa, F., et al. (2011). Scikit-learn: Machine Learning in Python. Journal of Machine Learning Research, 12, 2825-2830.

Tukey, J. W. (1977). Exploratory Data Analysis. Addison-Wesley.

LAMPIRAN

Lampiran 1. Dataset data curah hujan.xlsx (20 baris pertama)

Dataset Curah Hujan (20 Baris Pertama)
Hujan Suhu Kelembapan Keadaan_Cuaca Kecepatan_Angin
1 23.0 95 5 NA
1 24.0 90 1 NA
1 26.8 77 1 NA
1 29.6 62 2 2
1 30.8 56 1 7
1 31.0 55 1 7
1 30.4 57 3 9
2 30.9 58 2 10
2 30.2 62 2 8
2 29.7 62 2 7
2 29.2 68 3 5
2 28.6 73 1 5
2 27.6 79 1 2
1 26.5 84 3 3
1 26.2 83 2 3
1 25.7 86 2 2
1 25.0 89 2 NA
1 24.6 90 2 NA
2 24.2 90 2 NA
2 23.9 90 2 NA

Lampiran 2. Data latih akhir setelah SMOTE (10 baris pertama)

Data Latih Setelah SMOTE (10 Baris Pertama)
Suhu Kelembapan Kecepatan_Angin Keadaan_Cuaca_reduced_X0 Keadaan_Cuaca_reduced_X1 Keadaan_Cuaca_reduced_X2 Keadaan_Cuaca_reduced_X3 Keadaan_Cuaca_reduced_X4 Keadaan_Cuaca_reduced_X5 Keadaan_Cuaca_reduced_X6 Keadaan_Cuaca_reduced_X7 Keadaan_Cuaca_reduced_X8 Keadaan_Cuaca_reduced_X9 Hujan
-1.391 0.973 -0.693 1 0 0 0 0 0 0 0 0 0 1
-1.000 0.540 -0.693 1 0 0 0 0 0 0 0 0 0 1
0.095 -0.585 -0.693 1 0 0 0 0 0 0 0 0 0 1
1.190 -1.884 -1.027 1 0 0 0 0 0 0 0 0 0 1
1.659 -2.403 0.646 1 0 0 0 0 0 0 0 0 0 1
1.502 -2.316 1.316 1 0 0 0 0 0 0 0 0 0 1
-0.023 0.021 -0.693 1 0 0 0 0 0 0 0 0 0 1
-0.140 -0.066 -0.693 1 0 0 0 0 0 0 0 0 0 1
-0.335 0.194 -1.027 1 0 0 0 0 0 0 0 0 0 1
-0.609 0.454 -0.693 1 0 0 0 0 0 0 0 0 0 1