BAB I PENDAHULUAN

1.1 Latar Belakang Masalah

Perkembangan teknologi informasi menghasilkan data dalam jumlah besar dan beragam, sehingga machine learning semakin banyak digunakan untuk prediksi dan pengambilan keputusan di berbagai bidang. Namun, kinerja model tidak hanya ditentukan oleh algoritma yang dipilih, tetapi juga oleh kualitas data yang digunakan. Data mentah umumnya belum siap dimodelkan karena sering mengandung nilai hilang, pencilan, skala antarvariabel yang berbeda, serta variabel kategorik yang tidak dapat langsung diproses oleh algoritma. Jika kondisi ini diabaikan, model berpotensi menghasilkan prediksi yang kurang akurat dan sulit diinterpretasikan.

Feature engineering hadir sebagai tahapan untuk mengatasi masalah tersebut. Proses ini berfokus pada transformasi data mentah menjadi masukan yang sesuai bagi algoritma machine learning (Teixeira & Cavique, 2023). Secara umum, feature engineering dapat dibagi menjadi empat proses, yaitu pembentukan ide fitur, pembangkitan fitur, transformasi fitur, dan seleksi fitur (Chatzimparmpas et al., 2022). Pada praktiknya, tahapan ini mencakup penanganan data hilang, pengkodean variabel kategorik, normalisasi atau standardisasi, pembuatan fitur baru dari variabel yang ada, dan pemilihan fitur yang paling relevan.

Penerapan feature engineering yang tepat dapat meningkatkan akurasi model, mengurangi dimensi data, mempercepat komputasi, dan mengurangi risiko overfitting. Fitur yang informatif juga membuat hasil model lebih mudah dijelaskan. Oleh karena itu, feature engineering menjadi bagian penting dalam alur kerja analisis prediktif, dan pemahaman tentang konsep serta tekniknya diperlukan sebelum membangun model machine learning.

1.2 Rumusan Masalah

  1. Bagaimana cara memahami konsep dari berbagai jenis feature engineering pada RStudio?
  2. Bagaimana cara melakukan teknik feature engineering di program RStudio?

1.3 Tujuan Praktikum

  1. Mahasiswa memahami konsep dari berbagai jenis feature engineering pada RStudio.
  2. Mahasiswa dapat melakukan teknik feature engineering di program RStudio.

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 Jenis dan Sumber Data

Jenis penelitian ini adalah penelitian terapan yang mengaplikasikan materi yang telah diberikan untuk mengatasi permasalahan pendekatan kuantitatif, yaitu menggunakan data numerik yang akan dianalisis secara statistik menggunakan program R. Data yang digunakan dalam penelitian ini yaitu sekunder data curah hujan data tersebut diperoleh dari Asisten Praktikum

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. Gunakan library yqng dibutuhkan dan lakukan impor data curah hujan, lalu lihat struktur dan statistik deskriptif.
  2. Mengecek dan Menangani missing value jika ada: 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 training (80%) dan data testing (20%) dengan shuffle splitting.
  5. Menangani outlier pada variabel numerik dengan capping berbasis IQR (batas dari data training).
  6. Scaling variabel numerik dengan Robust Scaler (parameter dari data training).
  7. Encoding variabel kategorik dengan one-hot encoding.
  8. Menangani data tidak seimbang dengan SMOTE hanya pada data training

BAB IV HASIL DAN PEMBAHASAN

4.1 Menjalankan library dan Mengimpor 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("C:/Users/amand/Downloads/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: Berdasarkan hasil dataset berisi 743 observasi dan 5 variabel. Seluruh variabel terbaca dan bertipe numerik (Hujan bertipe integer, sedangkan variabel lainnya bertipe double/integer), sehingga variabel Hujan akan 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  
##                                                   NA's   :9      
##  Kecepatan_Angin 
##  Min.   : 2.000  
##  1st Qu.: 4.000  
##  Median : 6.000  
##  Mean   : 6.655  
##  3rd Qu.: 9.000  
##  Max.   :21.000  
##  NA's   :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: Ringkasan statistik deskriptif memperlihatkan gambaran umum tiap variabel, yaitu nilai tengah (rata-rata dan median), sebaran data (kuartil, nilai minimum, dan nilai maksimum), serta banyaknya NA pada variabel yang memiliki data hilang. Gambaran ini menjadi dasar untuk menentukan langkah preprocessing yang perlu dilakukan sebelum data dimodelkan. 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 Pengecekan dan Penanganan 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: Berdasarkan hasil 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:

  1. 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.
  2. 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.
  3. Mean imputation mengisi 314 data hilang pada Kecepatan_Angin dengan rata-rata 6.655. Setelah imputasi, seluruh kolom tidak lagi memiliki NA.
  4. 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: Berdasarkan hasil 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 hingga 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

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: Berdasarkan hasil 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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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: Berdasarkan hasil Robust Scaler mengubah setiap nilai menjadi \((x - \text{median}) / IQR\). Median data training 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 training 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: Berdasarkan hasil 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:

  1. 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.
  2. 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.
  3. SMOTE hanya diterapkan pada data latih. Data uji (149 observasi) tidak diubah sehingga evaluasi model nanti tetap mencerminkan kondisi data yang sebenarnya.
  4. 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

Konsep berbagai jenis feature engineering dapat dipahami di RStudio dengan mempraktikkannya langsung pada data, mulai dari eksplorasi data melalui statistik deskriptif dan visualisasi untuk mengenali data hilang, pencilan, kemencengan, dan perbedaan skala. Temuan tersebut kemudian dihubungkan dengan teknik yang sesuai, yaitu imputasi untuk data hilang, encoding untuk variabel kategorik, penskalaan dan transformasi untuk skala serta sebaran data, pembangkitan fitur untuk variabel baru, dan seleksi fitur untuk memilih variabel yang relevan. Membandingkan data sebelum dan sesudah tiap teknik, misalnya melalui histogram, membuat fungsi dan dampak masing-masing teknik lebih mudah dipahami.

Teknik feature engineering di RStudio dilakukan secara bertahap sesuai alur pengolahan data. Imputasi dilakukan dengan is.na() dan median(), one-hot encoding dengan model.matrix(), normalisasi min-max dan standardisasi z-score dengan perhitungan langsung atau fungsi scale(), transformasi logaritma dengan log1p(), fitur baru dengan operasi antarvariabel, dan seleksi fitur dengan cor(). Seluruh langkah ini ditulis dalam satu dokumen RMarkdown sehingga prosesnya terdokumentasi, dapat diulang, dan dapat dipublikasikan melalui RPubs.

Berdasarkan batasan masalah 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). 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. Kardinalitas Keadaan_Cuaca berhasil direduksi dari 23 nilai unik menjadi 7 kategori terisi, lalu di-encode dengan one-hot encoding. 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). SMOTE menyeimbangkan kelas Hujan pada data latih menjadi 466 : 466, sehingga data latih dan data uji siap digunakan pada tahap pemodelan.

DAFTAR PUSTAKA

Chatzimparmpas, A., Martins, R. M., Kucher, K., & Kerren, A. (2022). FeatureEnVi: Visual analytics for feature engineering using stepwise selection and semi-automatic extraction approaches. IEEE Transactions on Visualization and Computer Graphics.

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.

Teixeira, M., & Cavique, L. (2023). Feature engineering: Techniques and applications. Repositório Aberto, Universidade Aberta.

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