BAB I PENDAHULUAN

1.1 Latar Belakang Masalah

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 mean imputation, reduksi kardinalitas, shuffle splitting, penanganan outlier dengan capping, scaling menggunakan Robust 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, mean imputation, capping, shuffle splitting, Robust Scaler, encoding, dan SMOTE pada dataset tersebut?
  3. Bagaimana perubahan karakteristik data setelah seluruh tahapan preprocessing dilakukan?

1.3 Tujuan

  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 mean imputation), reduksi kardinalitas, shuffle splitting, capping, Robust Scaler, one-hot encoding, dan SMOTE.
  3. Menginterpretasikan hasil setiap tahapan sehingga diperoleh data latih dan data uji yang siap digunakan untuk pemodelan.

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) \]

  • Mean imputation, yaitu mengganti nilai hilang dengan rata-rata variabel tersebut, \(\bar{x} = \frac{1}{n}\sum_{i=1}^{n} x_i\). Teknik ini sederhana, 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 menjaga proporsi kelas pada kedua subset.

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.

Robust Scaler. Scaling menyamakan skala antarvariabel. Robust Scaler menggunakan median dan IQR sehingga tidak mudah terpengaruh outlier (Pedregosa et al., 2011):

\[ x' = \frac{x - \mathrm{median}(x)}{IQR(x)} \]

Parameter median dan IQR dihitung dari data latih saja, kemudian diterapkan pada data latih dan data uji untuk menghindari kebocoran data (data leakage).

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 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 Langkah-langkah Analisis

  1. Memuat library dan mengimpor data, lalu melihat struktur dan statistik deskriptif.
  2. Menangani missing value: interpolasi pada Keadaan_Cuaca dan mean 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 shuffle splitting.
  5. Menangani outlier pada variabel numerik dengan capping berbasis IQR (batas dari data latih).
  6. Scaling variabel numerik dengan Robust Scaler (parameter dari data latih).
  7. Encoding variabel kategorik dengan one-hot encoding.
  8. Menangani data tidak seimbang dengan SMOTE hanya pada data latih.

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:/Kuliah/Semester 7/Mata Kuliah Semester 7/Praktikum Machine Learning and Modern Prediction/Pertemuan 1/Teladan 1/data curah hujan (1).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. Dataset berisi 743 observasi dan 5 variabel. Seluruh variabel terbaca bertipe numerik (Hujan bertipe integer, sedangkan variabel lainnya bertipe double/integer), sehingga variabel Hujan nantinya perlu diubah menjadi faktor karena berperan sebagai target klasifikasi.

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 menunjukkan ukuran pemusatan (mean, median) dan penyebaran (kuartil, minimum, maksimum) setiap variabel, serta jumlah NA pada variabel yang memiliki data hilang. Informasi ini dibutuhkan untuk menentukan tahap preprocessing yang diperlukan:

  • Variabel Keadaan_Cuaca memiliki mean (15.11) yang jauh lebih besar daripada median (2) dengan nilai maksimum 97. Hal ini menunjukkan sebaran yang menjulur ke kanan dan variabel ini berupa kode, bukan ukuran kontinu, sehingga lebih tepat diperlakukan sebagai kategori.
  • Terdapat NA pada Keadaan_Cuaca dan Kecepatan_Angin, sehingga diperlukan penanganan missing value.
  • Kelas target Hujan tidak seimbang: kelas 2 sebanyak 579 observasi (77.93%) dan kelas 1 sebanyak 164 observasi (22.07%). Karena itu 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. Keadaan_Cuaca memiliki 9 data hilang (1.21%) dan Kecepatan_Angin memiliki 314 data hilang (42.26%). Variabel Hujan, Suhu, dan Kelembapan lengkap. Tabel terakhir menampilkan baris-baris yang Keadaan_Cuaca-nya 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) 
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) Mean imputation pada Kecepatan_Angin
df_imp2 <- df_imp1
mean_angin <- mean(df_imp2$Kecepatan_Angin, na.rm = TRUE)
mean_angin
## [1] 6.655012
df_imp2$Kecepatan_Angin[is.na(df_imp2$Kecepatan_Angin)] <- mean_angin
colSums(is.na(df_imp2))
##           Hujan            Suhu      Kelembapan   Keadaan_Cuaca Kecepatan_Angin 
##               0               0               0               0               0
# Dampak mean 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)),
  SD   = c(sd(data$Kecepatan_Angin, na.rm = TRUE), sd(df_imp2$Kecepatan_Angin))
)
##            Kondisi     Mean       SD
## 1 Sebelum imputasi 6.655012 3.352688
## 2 Sesudah imputasi 6.655012 2.546321
# Data hasil penanganan missing value yang dipakai selanjutnya
df_clean <- df_imp2

Interpretasi.

  • Jika seluruh baris yang mengandung NA dihapus (drop_na), data tersisa hanya 424 dari 743 baris atau kehilangan 42.93% informasi. Karena itu penghapusan baris tidak dipilih.
  • Interpolasi mengisi 9 data hilang pada Keadaan_Cuaca berdasarkan nilai sebelum dan sesudahnya. Setelah interpolasi, jumlah NA pada Keadaan_Cuaca menjadi 0. Hasil interpolasi dapat berupa bilangan desimal karena merupakan nilai di antara dua kode cuaca.
  • Mean imputation mengisi 314 data hilang pada Kecepatan_Angin dengan rata-rata 6.655. Setelah imputasi, seluruh kolom tidak lagi memiliki NA.
  • Rata-rata Kecepatan_Angin tidak berubah, tetapi simpangan baku turun dari 3.353 menjadi 2.546. Ini adalah konsekuensi mean imputation: banyak data diisi dengan nilai yang sama sehingga variasi data mengecil.

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            6.66
##  2     1  24           90             1            6.66
##  3     1  26.8         77             1            6.66
##  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            6.66 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            6.66 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. Variabel Keadaan_Cuaca awalnya memiliki 23 nilai unik (kardinalitas tinggi). Nilai tersebut dikelompokkan ke dalam interval lebar 10 (0-10, 10-20, …, 90-100) menjadi kategori berlabel 0 sampai 9, sehingga kategori yang benar-benar terisi tinggal 7. Tabel frekuensi menunjukkan sebagian besar observasi berada pada kategori 0 (kode cuaca kecil), sedangkan beberapa kategori (misalnya 3, 7, dan 8) tidak memiliki observasi. Kategori kosong ini tetap tercatat sebagai level faktor agar kolom one-hot pada data latih dan data uji konsisten.

4.4 Splitting Data (Shuffle Splitting)

df_clean$Hujan <- factor(df_clean$Hujan)

set.seed(200)   
split_shuffle <- initial_split(df_clean, prop = 0.8)   # tanpa strata = shuffle (acak)

X_train <- training(split_shuffle) %>% dplyr::select(-Hujan)
X_test  <- testing(split_shuffle)  %>% dplyr::select(-Hujan)
y_train <- training(split_shuffle)$Hujan
y_test  <- testing(split_shuffle)$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 latih:\n")
## 
## Distribusi kelas data latih:
table(y_train)
## y_train
##   1   2 
## 128 466
round(prop.table(table(y_train)) * 100, 2)
## y_train
##     1     2 
## 21.55 78.45
cat("\nDistribusi kelas data uji:\n")
## 
## Distribusi kelas data uji:
table(y_test)
## y_test
##   1   2 
##  36 113
round(prop.table(table(y_test)) * 100, 2)
## y_test
##     1     2 
## 24.16 75.84

Interpretasi. Data dibagi secara acak (shuffle splitting, set.seed(200)) menjadi 594 observasi data latih (80%) dan 149 observasi data uji (20%). Karena tidak memakai stratifikasi, proporsi kelas pada data latih (kelas 2: 78.45%) dan data uji (kelas 2: 75.84%) hanya mendekati proporsi data asli (77.93%) dan dapat sedikit berbeda antar subset. Kedua subset sama-sama didominasi kelas 2, sehingga ketidakseimbangan kelas tetap ada dan akan ditangani dengan SMOTE. Seluruh tahap berikutnya menggunakan parameter yang dihitung dari data latih saja.

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.4        35.8
## 2      Kelembapan              0        46.5       122.5
## 3 Kecepatan_Angin             50         2.0        10.0
# Skewness sebelum capping
skew_sebelum <- sapply(X_train[list_num], skewness, na.rm = TRUE)
round(skew_sebelum, 3)
##            Suhu      Kelembapan Kecepatan_Angin 
##           0.279          -0.558           0.837
# 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              50               0
# Ringkasan sebelum vs sesudah capping
summary(X_train[list_num])
##       Suhu         Kelembapan  Kecepatan_Angin 
##  Min.   :22.60   Min.   : 52   Min.   : 2.000  
##  1st Qu.:24.30   1st Qu.: 75   1st Qu.: 5.000  
##  Median :26.00   Median : 86   Median : 6.655  
##  Mean   :26.54   Mean   : 84   Mean   : 6.642  
##  3rd Qu.:28.90   3rd Qu.: 94   3rd Qu.: 7.000  
##  Max.   :31.60   Max.   :100   Max.   :21.000
summary(X_train_capped[list_num])
##       Suhu         Kelembapan  Kecepatan_Angin 
##  Min.   :22.60   Min.   : 52   Min.   : 2.000  
##  1st Qu.:24.30   1st Qu.: 75   1st Qu.: 5.000  
##  Median :26.00   Median : 86   Median : 6.655  
##  Mean   :26.54   Mean   : 84   Mean   : 6.447  
##  3rd Qu.:28.90   3rd Qu.: 94   3rd Qu.: 7.000  
##  Max.   :31.60   Max.   :100   Max.   :10.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.279          -0.558          -0.163
# 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 dari data latih. Jumlah outlier pada Suhu adalah 0, Kelembapan 0, dan Kecepatan_Angin 50. Artinya Suhu dan Kelembapan tidak bermasalah, sedangkan outlier terkonsentrasi pada Kecepatan_Angin.
  • Penyebab outlier pada Kecepatan_Angin. Karena sekitar 42% data Kecepatan_Angin diisi dengan nilai rata-rata yang sama, rentang antarkuartil (IQR) menjadi sempit, yaitu batas atas hanya 10. Akibatnya nilai kecepatan angin yang tinggi (di atas batas tersebut) terdeteksi sebagai outlier.
  • Capping. Nilai di luar batas diganti dengan nilai batas, tanpa menghapus observasi (jumlah baris data latih tetap 594). Setelah capping, jumlah outlier pada ketiga variabel menjadi 0, dan nilai maksimum Kecepatan_Angin turun dari 21 menjadi 10. Batas yang sama dipakai pada data uji agar tidak ada kebocoran data.
  • Skewness. Nilai skewness Suhu, Kelembapan, dan Kecepatan_Angin sebelum capping berturut-turut 0.279, -0.558, dan 0.837; setelah capping menjadi 0.279, -0.558, dan -0.163. Perubahan terbesar terjadi pada variabel yang di-capping.
  • Visualisasi. Pada boxplot sebelum capping Kecepatan_Angin terlihat titik-titik di luar whisker; pada boxplot sesudah capping titik tersebut sudah tidak ada. Histogram Suhu dan Kelembapan tidak berubah karena tidak ada nilai yang di-capping.

4.6 Scalling Data (Robust Scaler)

# Parameter Robust Scaler dihitung dari data latih
median_val <- sapply(X_train_capped[list_num], median, na.rm = TRUE)
iqr_val    <- sapply(X_train_capped[list_num], IQR, na.rm = TRUE)

rbind(Median = median_val, IQR = iqr_val)
##        Suhu Kelembapan Kecepatan_Angin
## Median 26.0         86        6.655012
## IQR     4.6         19        2.000000
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(median_val[col])) / unname(iqr_val[col])
  X_test_scale[[col]]  <- (X_test_capped[[col]]  - unname(median_val[col])) / unname(iqr_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 -0.500       0.579            61           0     6                    
## 2  1.09       -0.947             2           1.67  0                    
## 3  0.0870     -0.263             3          -0.828 0                    
## 4  1.15       -1.53              2           1.17  0                    
## 5  0.674      -0.421             2           0     0                    
## 6  0.391      -0.368             2          -1.83  0
cat("\nMedian & IQR data latih setelah scaling:\n")
## 
## Median & IQR data latih setelah scaling:
rbind(Median = sapply(X_train_scale[list_num], median),
      IQR    = sapply(X_train_scale[list_num], IQR))
##        Suhu Kelembapan Kecepatan_Angin
## Median    0          0               0
## IQR       1          1               1
cat("\nRingkasan data latih setelah scaling:\n")
## 
## Ringkasan data latih setelah scaling:
summary(X_train_scale[list_num])
##       Suhu           Kelembapan      Kecepatan_Angin  
##  Min.   :-0.7391   Min.   :-1.7895   Min.   :-2.3275  
##  1st Qu.:-0.3696   1st Qu.:-0.5789   1st Qu.:-0.8275  
##  Median : 0.0000   Median : 0.0000   Median : 0.0000  
##  Mean   : 0.1177   Mean   :-0.1053   Mean   :-0.1041  
##  3rd Qu.: 0.6304   3rd Qu.: 0.4211   3rd Qu.: 0.1725  
##  Max.   : 1.2174   Max.   : 0.7368   Max.   : 1.6725
cat("\nRingkasan data uji setelah scaling:\n")
## 
## Ringkasan data uji setelah scaling:
summary(X_test_scale[list_num])
##       Suhu            Kelembapan      Kecepatan_Angin   
##  Min.   :-0.69565   Min.   :-1.6316   Min.   :-2.32751  
##  1st Qu.:-0.34783   1st Qu.:-0.6842   1st Qu.:-0.32751  
##  Median :-0.04348   Median : 0.0000   Median : 0.00000  
##  Mean   : 0.11716   Mean   :-0.1363   Mean   :-0.05804  
##  3rd Qu.: 0.65217   3rd Qu.: 0.4211   3rd Qu.: 0.67249  
##  Max.   : 1.30435   Max.   : 0.7368   Max.   : 1.67249
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 Robust Scaler") +
  theme_minimal() +
  theme(legend.position = "none")

Interpretasi. Robust Scaler mengubah setiap nilai menjadi \((x - \text{median}) / IQR\). Median data latih sebelum scaling untuk Suhu, Kelembapan, dan Kecepatan_Angin berturut-turut 26, 86, dan 6.655, dengan IQR 4.6, 19, dan 2. Setelah scaling, median setiap variabel pada data latih menjadi 0 dan IQR menjadi 1, sehingga ketiga variabel berada pada skala yang sebanding. Nilai negatif berarti di bawah median dan nilai positif berarti di atas median. Pada data uji, median dan IQR tidak harus persis 0 dan 1 karena data uji hanya ditransformasi memakai parameter data latih. Robust Scaler dipilih karena memakai median dan IQR yang tahan terhadap outlier, dan boxplot di atas memperlihatkan ketiga variabel sudah berada pada 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                        0                        0                        0
##  2                        1                        0                        0
##  3                        1                        0                        0
##  4                        1                        0                        0
##  5                        1                        0                        0
##  6                        1                        0                        0
##  7                        0                        0                        0
##  8                        0                        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                        1                        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. One-hot encoding mengubah Keadaan_Cuaca_reduced menjadi 10 kolom biner (satu kolom per kategori 0 sampai 9); pada setiap baris tepat satu kolom bernilai 1 dan lainnya 0. Data latih menghasilkan 594 baris dan data uji 149 baris dengan jumlah kolom yang sama. Kolom untuk kategori yang tidak muncul di data (misalnya kategori 3, 7, dan 8) berisi 0 seluruhnya; kolom ini tidak membawa informasi, tetapi dibiarkan agar struktur data latih dan data uji tetap sama.

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 
## 128 466
round(prop.table(table(X_train_final$Hujan)) * 100, 2)
## 
##     1     2 
## 21.55 78.45
# 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 
## 466 466
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: 932 14
cat("\nDistribusi kelas data uji (tidak diubah):\n")
## 
## Distribusi kelas data uji (tidak diubah):
table(X_test_final$Hujan)
## 
##   1   2 
##  36 113
head(train_smote)
## # A tibble: 6 × 14
##      Suhu Kelembapan Kecepatan_Angin Keadaan_Cuaca_reduced_X0
##     <dbl>      <dbl>           <dbl>                    <dbl>
## 1 -0.500       0.579           0                            0
## 2  1.09       -0.947           1.67                         1
## 3  0.0870     -0.263          -0.828                        1
## 4  1.15       -1.53            1.17                         1
## 5  0.674      -0.421           0                            1
## 6  0.391      -0.368          -1.83                         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): 932 14
cat("Data uji          :", dim(X_test_final), "\n")
## Data uji          : 149 14

Interpretasi.

  • Sebelum SMOTE, data latih terdiri dari 466 observasi kelas 2 (mayoritas) dan 128 observasi kelas 1 (minoritas), atau proporsi 78.45% : 21.55%. Ketidakseimbangan ini membuat model berisiko condong memprediksi kelas mayoritas.
  • Sesudah SMOTE, kedua kelas masing-masing berjumlah 466 (kelas 2) dan 466 (kelas 1), sehingga proporsinya seimbang (50% : 50%). Jumlah baris data latih bertambah dari 594 menjadi 932 karena dibuat 338 observasi sintetis kelas minoritas dengan interpolasi antar tetangga terdekat (K = 5). Jumlah kelas mayoritas tidak berubah.
  • SMOTE hanya diterapkan pada data latih. Data uji (149 observasi) tidak diubah sehingga evaluasi model nanti tetap mencerminkan kondisi data yang sebenarnya.
  • Hasil head(train_smote) menunjukkan data akhir berisi tiga variabel numerik yang sudah di-scaling, kolom one-hot Keadaan_Cuaca_reduced, dan target Hujan. Kolom one-hot pada observasi sintetis dapat bernilai pecahan karena dibentuk dari interpolasi, bukan dari kategori asli.

BAB V KESIMPULAN

5.1 Kesimpulan

  1. Dataset curah hujan berisi 743 observasi dengan missing value pada Keadaan_Cuaca (9 data) dan Kecepatan_Angin (314 data), serta kelas target yang tidak seimbang (77.93% kelas 2 dan 22.07% kelas 1).
  2. Interpolasi berhasil mengisi data hilang pada Keadaan_Cuaca dan mean imputation mengisi data hilang pada Kecepatan_Angin tanpa menghilangkan observasi, tetapi mean imputation mengecilkan variasi Kecepatan_Angin.
  3. Kardinalitas Keadaan_Cuaca berhasil direduksi dari 23 nilai unik menjadi 7 kategori terisi, lalu di-encode dengan one-hot encoding.
  4. Shuffle splitting menghasilkan 594 data latih dan 149 data uji. Capping berbasis IQR menghilangkan outlier terutama pada Kecepatan_Angin tanpa mengurangi jumlah data, dan Robust Scaler menyamakan skala variabel numerik (median 0 dan IQR 1 pada data latih).
  5. SMOTE menyeimbangkan kelas Hujan pada data latih menjadi 466 : 466, sehingga data latih dan data uji siap digunakan pada tahap pemodelan.

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.