Perkembangan teknologi membuat data yang tersedia semakin banyak, dan machine learning menjadi salah satu cara yang sering digunakan untuk mengolah data tersebut. Dalam machine learning, model belajar dari data yang diberikan sehingga hasil yang diperoleh sangat bergantung pada kualitas data (Géron, 2019). Pada kenyataannya, data mentah jarang bisa langsung digunakan karena masih memiliki berbagai masalah. Oleh karena itu, diperlukan tahap feature engineering, yaitu langkah-langkah untuk mengubah data mentah menjadi fitur-fitur yang dapat digunakan dalam algoritma machine learning (Zheng & Casari, 2018). Kuhn dan Johnson (2019) menyebutkan bahwa cara prediktor disajikan ke dalam model dapat berpengaruh besar terhadap performa model.
Salah satu masalah yang paling sering ditemui adalah missing value atau data yang hilang. Menurut Emmanuel dkk. (2021), data yang hilang dapat terjadi karena gangguan sistem saat pengumpulan data maupun kesalahan manusia saat mengolah data. Data curah hujan juga sering memiliki data yang kosong sehingga perlu diisi terlebih dahulu sebelum digunakan (Vidal-Paz, Rodríguez-Gómez, & Orosa, 2023). Selain itu, variabel kategorik yang memiliki banyak kategori atau kardinalitas tinggi juga dapat menyulitkan model. Pargent dkk. (2022) menemukan bahwa fitur dengan kardinalitas tinggi tidak cukup ditangani dengan metode encoding biasa seperti one-hot encoding.
Masalah lain yang perlu diperhatikan adalah outlier dan perbedaan skala antarvariabel. Outlier dapat memengaruhi hasil analisis sehingga perlu dideteksi dan ditangani sebelum data digunakan untuk klasifikasi (Dash dkk., 2023). Perbedaan skala juga perlu ditangani karena de Amorim, Cavalcanti, dan Cruz (2023) menemukan bahwa pemilihan teknik scaling berpengaruh terhadap performa model klasifikasi. Di samping itu, banyak algoritma hanya dapat menerima input berupa angka sehingga variabel kategorik perlu diubah melalui encoding (Kuhn & Johnson, 2019).
Sebelum model dibangun, data juga perlu dibagi menjadi data training dan data testing agar kemampuan model dapat diuji pada data baru (Muraina, 2022). Pembagian ini harus dilakukan dengan benar karena proses preprocessing yang ikut menggunakan data testing dapat menyebabkan data leakage (Kapoor & Narayanan, 2023). Kemudian, data dengan jumlah kelas yang tidak seimbang dapat membuat model lebih condong ke kelas mayoritas (Chen dkk., 2024). Salah satu cara yang banyak digunakan untuk mengatasinya adalah SMOTE, yaitu metode yang membuat data sintetis baru pada kelas minoritas (Elreedy, Atiya, & Kamalov, 2024).
Berdasarkan latar belakang di atas, rumusan masalah dalam praktikum ini adalah sebagai berikut.
Berdasarkan rumusan masalah di atas, tujuan praktikum ini adalah sebagai berikut.
Adapun manfaat pada penelitian ini yaitu:
Adapun batasan masalah pada praktikum ini yaitu gunakan dataset data curah hujan.xlsx untuk melakukan preprocessing data. NPM ganjil menggunakan mean imputation, shuffle splitting, dan Robust Scaler, sedangkan NPM genap menggunakan mode imputation, stratify splitting, dan Standard Scaler. Keduanya melakukan interpolasi pada keadaan cuaca, capping pada data numerik, serta penanganan imbalance data menggunakan SMOTE. Tampilkan dan interpretasikan setiap output!
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 alur.
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
Machine learning adalah bidang ilmu yang membuat komputer dapat belajar dari data tanpa harus diprogram secara khusus untuk setiap tugas (Géron, 2019). Berdasarkan cara belajarnya, machine learning dibagi menjadi beberapa jenis, di antaranya supervised learning, unsupervised learning, dan reinforcement learning. Pada supervised learning, data yang digunakan sudah memiliki label atau variabel target. Jika variabel targetnya berupa kategori, misalnya hujan dan tidak hujan, maka masalahnya disebut klasifikasi (Géron, 2019).
Feature engineering adalah langkah-langkah untuk mengubah data mentah menjadi fitur-fitur yang dapat digunakan dalam algoritma machine learning. Pada proses ini, variabel atau fitur yang paling berpengaruh dipilih lalu direkayasa agar lebih sesuai untuk pemodelan. Proses ini membutuhkan pemahaman yang baik karena melibatkan analisis data, pengetahuan tentang data, dan sedikit intuisi (Zheng & Casari, 2018). Tujuan feature engineering adalah menyederhanakan dan mempercepat transformasi data sekaligus meningkatkan akurasi model. Kuhn dan Johnson (2019) menjelaskan bahwa cara prediktor disajikan ke dalam model dapat sangat memengaruhi performa model, sehingga kualitas fitur sering kali lebih penting daripada seberapa canggih algoritma yang dipilih. Tahapan feature engineering yang digunakan pada praktikum ini adalah penanganan missing value, kardinalitas, splitting data, penanganan outlier, scaling, encoding, dan penanganan imbalanced data.
Missing value adalah kondisi ketika tidak ada nilai yang tersimpan pada observasi tertentu dalam suatu variabel. Missing value dapat terjadi karena alat pencatat rusak, sistem mengalami gangguan saat pengumpulan data, atau terjadi kesalahan manusia saat memasukkan dan mengolah data (Emmanuel dkk., 2021). Sebagian besar algoritma machine learning tidak dapat memproses data yang kosong sehingga missing value harus ditangani terlebih dahulu.
Little dan Rubin (2019) membagi mekanisme terjadinya missing value menjadi tiga jenis, yaitu:
Secara umum, missing value dapat ditangani dengan dua cara. Cara pertama adalah menghapus baris yang memiliki data kosong (deletion). Cara ini mudah dilakukan, tetapi dapat membuang banyak informasi jika jumlah data kosongnya besar. Cara kedua adalah imputasi, yaitu mengisi data yang kosong dengan nilai dugaan (Little & Rubin, 2019). Beberapa metode imputasi sederhana yang sering digunakan adalah imputasi rata-rata (mean), median, modus (mode), forward fill, dan interpolasi. Pada praktikum ini, metode yang digunakan adalah interpolasi linier dan mode imputation.
Interpolasi linier adalah cara menduga nilai di antara dua titik data yang sudah diketahui dengan menghubungkan kedua titik tersebut menggunakan garis lurus (Chapra & Canale, 2015). Dalam penanganan missing value, interpolasi cocok digunakan pada data yang tersusun berurutan, seperti data cuaca dari waktu ke waktu, karena nilai pada suatu waktu biasanya tidak jauh berbeda dengan nilai sebelum dan sesudahnya. Jika nilai pada urutan ke-\(t\) kosong, sedangkan nilai terdekat yang diketahui berada pada urutan ke-\(a\) dan ke-\(b\), maka nilai dugaannya adalah
\[ \hat{x}_t = x_a + \frac{t-a}{b-a}\,(x_b - x_a) \]
dengan \(x_a\) adalah nilai sebelum data kosong dan \(x_b\) adalah nilai sesudah data kosong. Jika hanya ada satu data kosong yang diapit oleh dua nilai, hasil interpolasinya sama dengan rata-rata kedua nilai tersebut. Di R, interpolasi linier dapat dilakukan menggunakan fungsi na.approx() dari paket zoo.
Modus adalah nilai yang paling sering muncul dalam suatu data. Mode imputation mengisi data yang kosong dengan modus dari variabel tersebut. Metode ini sederhana dan dapat digunakan untuk variabel kategorik maupun numerik diskrit. Kekurangannya, jika data yang kosong cukup banyak, nilai modus akan muncul terlalu sering sehingga sebaran data berubah dan keragaman data menjadi lebih kecil dari yang seharusnya (Little & Rubin, 2019).
Kardinalitas adalah jumlah nilai unik atau label yang terdapat dalam suatu variabel kategorik. Kardinalitas digunakan untuk melihat seberapa beragam isi suatu fitur. Variabel dengan sedikit kategori, misalnya jenis kelamin, memiliki kardinalitas rendah (low cardinality), sedangkan variabel dengan banyak kategori, misalnya kode pos atau kode wilayah, memiliki kardinalitas tinggi (high cardinality).
Kardinalitas berpengaruh terhadap performa model dan waktu komputasi. Variabel dengan kardinalitas tinggi dapat menyebabkan curse of dimensionality jika diubah menggunakan one-hot encoding, karena setiap kategori akan menjadi satu kolom baru. Akibatnya, data menjadi sangat jarang (sparse) dan proses komputasi menjadi lebih berat. Selain itu, banyak kategori yang hanya muncul beberapa kali di data training sehingga model cenderung menghafal pola dari kategori yang jarang muncul dan berisiko mengalami overfitting. Pargent dkk. (2022) menemukan bahwa untuk fitur dengan kardinalitas tinggi, regularized target encoding memberikan hasil yang lebih baik dibandingkan metode encoding tradisional seperti one-hot encoding.
Salah satu cara sederhana untuk mengurangi kardinalitas adalah binning, yaitu mengelompokkan nilai-nilai yang berdekatan ke dalam beberapa interval. Jika data dibagi ke dalam \(k\) interval dengan lebar yang sama, lebar setiap interval adalah
\[ w = \frac{x_{\max} - x_{\min}}{k} \]
Setiap nilai kemudian diberi label sesuai interval tempat nilai tersebut berada. Cara lain yang juga sering digunakan adalah menggabungkan kategori yang jarang muncul menjadi satu kategori, misalnya kategori “lainnya”.
Splitting data adalah proses membagi data menjadi dua bagian, yaitu data training dan data testing. Data training digunakan untuk melatih model, sedangkan data testing digunakan untuk menguji kemampuan model pada data yang belum pernah dilihat sebelumnya. Pembagian ini penting agar model yang dibangun dapat digeneralisasi dengan baik pada data baru (Muraina, 2022).
Proporsi pembagian data bersifat fleksibel. Rasio yang umum digunakan adalah 80% untuk training dan 20% untuk testing, tetapi rasio ini dapat disesuaikan dengan ukuran dataset (Géron, 2019). Joseph (2022) menjelaskan bahwa rasio pembagian yang optimal sebenarnya bergantung pada banyaknya parameter dalam model. Ada tiga cara yang umum digunakan untuk membagi data, yaitu:
Hal lain yang perlu diperhatikan adalah data leakage, yaitu kondisi ketika informasi dari data testing ikut terpakai saat melatih model. Kapoor dan Narayanan (2023) menyebutkan bahwa preprocessing yang dilakukan pada gabungan data training dan data testing termasuk salah satu penyebab data leakage. Oleh karena itu, parameter preprocessing seperti batas outlier dan nilai scaling sebaiknya dihitung dari data training saja.
Outlier atau pencilan adalah observasi yang nilainya menyimpang jauh dari sebagian besar data lain. Outlier dapat muncul karena kesalahan pengukuran, kesalahan input data (misinput), kontaminasi data, atau memang merupakan kejadian langka yang terjadi dalam populasi. Dalam pemodelan machine learning, outlier perlu diperhatikan karena dapat memberikan pengaruh yang besar terhadap hasil sehingga sebaiknya dideteksi dan ditangani sebelum data digunakan (Dash dkk., 2023).
Salah satu metode deteksi outlier yang sering digunakan adalah metode Interquartile Range (IQR) yang diperkenalkan oleh Tukey (1977). IQR adalah selisih antara kuartil ketiga (\(Q_3\)) dan kuartil pertama (\(Q_1\)). Suatu data dianggap sebagai outlier jika nilainya lebih kecil dari batas bawah (lower bound) atau lebih besar dari batas atas (upper bound).
\[ IQR = Q_3 - Q_1 \]
\[ LB = Q_1 - 1{,}5 \times IQR \qquad\qquad UB = Q_3 + 1{,}5 \times IQR \]
Pada boxplot, data yang berada di luar batas tersebut digambarkan sebagai titik di luar whisker.
Skewness atau kemiringan adalah ukuran yang menunjukkan seberapa tidak simetris sebaran suatu data. Salah satu rumus skewness adalah
\[ g_1 = \frac{\frac{1}{n}\sum_{i=1}^{n}(x_i-\bar{x})^3}{\left(\frac{1}{n}\sum_{i=1}^{n}(x_i-\bar{x})^2\right)^{3/2}} \]
Jika nilai skewness mendekati 0, sebaran data cenderung simetris. Nilai positif berarti sebaran menceng ke kanan (ekor panjang di sisi nilai besar), sedangkan nilai negatif berarti sebaran menceng ke kiri (ekor panjang di sisi nilai kecil). Pada praktikum ini, kolom dengan nilai skewness antara −0,5 sampai 0,5 dianggap memiliki sebaran normal atau mendekati simetris.
Setelah outlier terdeteksi, ada dua cara yang umum digunakan untuk menanganinya. Cara pertama adalah trimming, yaitu membuang observasi yang menjadi outlier. Cara kedua adalah capping atau winsorizing, yaitu mengganti nilai outlier dengan nilai batasnya. Dash dkk. (2023) juga menggunakan metode IQR untuk mendeteksi outlier dan winsorizing untuk menanganinya sebelum data digunakan pada tugas klasifikasi. Capping dapat dituliskan sebagai berikut.
\[ x_i^{*} = \begin{cases} LB, & x_i < LB \\ x_i, & LB \le x_i \le UB \\ UB, & x_i > UB \end{cases} \]
Kelebihan capping dibandingkan trimming adalah jumlah observasi tidak berkurang, sehingga informasi dari variabel lain pada baris yang sama tetap bisa digunakan.
Scaling data adalah langkah preprocessing untuk mengubah nilai variabel ke dalam rentang skala yang sebanding. Tujuannya agar tidak ada variabel yang mendominasi proses pembelajaran hanya karena rentang nilainya lebih besar dibandingkan variabel lain. Hal ini terutama penting untuk algoritma yang menggunakan perhitungan jarak, seperti k-nearest neighbor. de Amorim, Cavalcanti, dan Cruz (2023) membandingkan beberapa teknik scaling pada banyak dataset dan menemukan bahwa pilihan teknik scaling dapat memengaruhi performa model klasifikasi. Tiga metode scaling yang umum digunakan adalah sebagai berikut.
Parameter scaling, seperti rata-rata dan simpangan baku, dihitung dari data training saja, kemudian digunakan juga untuk mentransformasi data testing agar tidak terjadi data leakage (Kapoor & Narayanan, 2023). Perbedaan hasil ketiga metode tersebut dapat dilihat pada Gambar 1.
Gambar 1. Ilustrasi sebaran dua variabel sebelum dan sesudah scaling
Pada Gambar 1, sebelum scaling variabel x1 dan x2 berada pada posisi yang berjauhan. Setelah Robust Scaler dan Standard Scaler, kedua variabel bergeser ke sekitar nol sehingga dapat dibandingkan. Pada Min-Max Scaler, adanya outlier membuat sebagian besar data terdesak ke salah satu sisi rentang 0 sampai 1.
Encoding adalah proses mengubah fitur kategorik atau non-numerik menjadi bentuk numerik agar dapat digunakan sebagai input algoritma machine learning. Proses ini diperlukan karena banyak algoritma machine learning hanya dapat menerima input berupa angka (Kuhn & Johnson, 2019). Dua teknik encoding yang umum digunakan adalah one-hot encoding dan ordinal encoding.
One-hot encoding mengubah setiap kategori menjadi satu kolom biner. Kolom bernilai 1 jika observasi termasuk kategori tersebut dan bernilai 0 jika tidak. Variabel yang memiliki \(k\) kategori akan menghasilkan \(k\) kolom baru, dan setiap baris hanya memiliki satu nilai 1. Metode ini cocok untuk variabel nominal, yaitu variabel yang kategorinya tidak memiliki urutan, karena tidak ada urutan yang dipaksakan di antara kategori. Pada model regresi, biasanya hanya digunakan \(k-1\) kolom untuk menghindari dummy variable trap (Kuhn & Johnson, 2019). Contoh one-hot encoding dapat dilihat pada Gambar 2.
Gambar 2. Contoh one-hot encoding pada variabel kategorik
Ordinal encoding mengganti setiap kategori dengan angka berurutan, misalnya 0, 1, 2, dan seterusnya. Metode ini cocok untuk variabel ordinal, yaitu variabel yang kategorinya memiliki tingkatan, seperti tingkat pendidikan (SD, SMP, SMA) atau ukuran pakaian (S, M, L). Jika digunakan pada variabel nominal, ordinal encoding dapat membuat model menganggap ada urutan di antara kategori padahal sebenarnya tidak ada.
Imbalanced dataset adalah dataset yang jumlah observasi pada setiap kelas variabel targetnya tidak seimbang. Kelas dengan jumlah observasi lebih banyak disebut kelas mayoritas, sedangkan kelas dengan jumlah observasi lebih sedikit disebut kelas minoritas. Tingkat ketidakseimbangan biasanya dinyatakan dengan imbalance ratio (IR), yaitu
\[ IR = \frac{n_{mayoritas}}{n_{minoritas}} \]
Semakin besar nilai IR, semakin tidak seimbang datanya. Dalam praktiknya, data yang tidak seimbang lebih sering dijumpai daripada data yang seimbang. Hal ini tidak selalu menjadi masalah jika ketidakseimbangannya kecil, tetapi dapat menjadi masalah jika perbandingannya besar, misalnya 10:1. Model yang dilatih pada data seperti ini cenderung lebih banyak menebak kelas mayoritas sehingga kemampuannya dalam mengenali kelas minoritas menjadi rendah (Chen dkk., 2024).
Penanganan imbalanced data pada level data dapat dilakukan dengan undersampling, yaitu mengurangi jumlah data kelas mayoritas, atau oversampling, yaitu menambah jumlah data kelas minoritas (Chen dkk., 2024). Salah satu metode oversampling yang paling banyak digunakan adalah Synthetic Minority Over-sampling Technique (SMOTE). Berbeda dengan random oversampling yang hanya menggandakan data yang sudah ada, SMOTE membuat data sintetis baru di antara observasi kelas minoritas yang saling berdekatan (Elreedy, Atiya, & Kamalov, 2024). Langkah-langkah SMOTE adalah sebagai berikut.
Karena data baru dibentuk di sepanjang garis antara \(x_i\) dan \(x_{zi}\), data sintetis tetap berada di sekitar wilayah kelas minoritas. Ilustrasinya dapat dilihat pada Gambar 3.
Gambar 3. Ilustrasi pembentukan data sintetis dengan SMOTE
Pada Gambar 3, titik oranye adalah observasi kelas minoritas dan garis putus-putus menghubungkan \(x_i\) dengan tiga tetangga terdekatnya. Segitiga hijau adalah data sintetis (\(x_{new}\)) yang terbentuk di antara \(x_i\) dan tetangganya.
Data yang digunakan adalah data sekunder dari file data curah hujan.xlsx yang diberikan oleh asisten praktikum pada praktikum Machine Learning pertemuan 1. Data ini terdiri dari 743 observasi dan 5 variabel, yaitu hujan, suhu, kelembapan, keadaan cuaca, dan kecepatan angin. Data tersusun berurutan berdasarkan waktu pengamatan.
Variabel yang digunakan terdiri dari satu variabel target dan empat variabel fitur. Variabel target (Y) adalah hujan dengan kode 1 untuk hujan dan 2 untuk tidak hujan. Variabel fitur (X) terdiri dari suhu (°C), kelembapan (%), dan kecepatan angin yang merupakan data numerik, serta keadaan cuaca yang merupakan data kategorik berupa kode keadaan cuaca (1 sampai 97).
Langkah-langkah analisis yang dilakukan adalah sebagai berikut.
Library yang digunakan sama dengan sintaks praktikum. Paket zoo digunakan untuk interpolasi, readxl untuk membaca file Excel, tidyverse dan dplyr untuk mengolah data, rsample untuk splitting data, e1071 untuk menghitung skewness, DescTools untuk capping dengan fungsi Winsorize(), ggplot2 dan gridExtra untuk visualisasi, recipes untuk encoding, themis untuk SMOTE, dan caret sebagai paket pendukung pemodelan.
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 dibaca menggunakan fungsi read_excel(). File Rmd ini dan file data curah hujan.xlsx disimpan dalam satu folder yang sama sehingga cukup dituliskan nama file-nya saja.
data <- read_excel("D:/SEMESTER 7 STATISTIKA/MATKUL ML & MP/praktikum/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
## 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 ...
Berdasarkan output head(), dapat dilihat enam baris pertama dari data curah hujan. Pada tiga baris pertama, variabel kecepatan angin bernilai NA yang menandakan adanya data kosong. Output str() menunjukkan bahwa data terdiri dari 743 baris dan 5 kolom, dan semua variabel bertipe numerik (num). Walaupun bertipe numerik, variabel hujan dan keadaan cuaca sebenarnya merupakan data kategorik karena nilainya berupa kode.
## hujan suhu kelembapan keadaan_cuaca kecepatan_angin
## Min. :1.000 Min. :22.60 Min. : 52.00 Min. : 1.00 Min. : 2.000
## 1st Qu.:2.000 1st Qu.:24.30 1st Qu.: 75.00 1st Qu.: 2.00 1st Qu.: 4.000
## Median :2.000 Median :26.00 Median : 86.00 Median : 2.00 Median : 6.000
## Mean :1.779 Mean :26.54 Mean : 83.88 Mean :15.11 Mean : 6.655
## 3rd Qu.:2.000 3rd Qu.:28.90 3rd Qu.: 94.00 3rd Qu.:15.00 3rd Qu.: 9.000
## Max. :2.000 Max. :32.00 Max. :100.00 Max. :97.00 Max. :21.000
## NAs :9 NAs :314
##
## 1 2
## 164 579
##
## 1 2
## 22.07 77.93
Berdasarkan output di atas, dapat diketahui nilai minimum, kuartil pertama, median, rata-rata, kuartil ketiga, nilai maksimum, dan jumlah data kosong (NA’s) dari setiap variabel. suhu memiliki nilai terkecil 22,6 °C dan nilai terbesar 32 °C dengan rata-rata 26,54 °C. Nilai rata-rata ini tidak jauh berbeda dengan mediannya (26 °C), sehingga data suhu cenderung simetris. kelembapan memiliki nilai terkecil 52% dan nilai terbesar 100% dengan rata-rata 83,88% dan median 86%. Hal ini menunjukkan bahwa sebagian besar pengamatan memiliki kelembapan yang tinggi.
Pada variabel keadaan cuaca, nilai mediannya 2 sedangkan nilai maksimumnya 97. Artinya, sebagian besar pengamatan berada pada kondisi tidak hujan, sedangkan kode hujan lebih jarang muncul. Variabel ini memiliki 9 data kosong. kecepatan angin memiliki nilai terkecil 2 dan nilai terbesar 21 dengan rata-rata 6,655, serta memiliki 314 data kosong. Nilai maksimum 21 cukup jauh dari kuartil ketiga yaitu 9, sehingga kemungkinan terdapat outlier pada variabel ini. Untuk variabel hujan dan keadaan cuaca, nilai rata-ratanya tidak perlu diartikan karena kedua variabel tersebut berupa kode.
Pada variabel hujan, kelas 1 (hujan) berjumlah 164 observasi (22,07%) dan kelas 2 (tidak hujan) berjumlah 579 observasi (77,93%). Jumlah kedua kelas ini berbeda cukup jauh, dengan perbandingan sekitar 1 : 3,5, sehingga data termasuk tidak seimbang. Berdasarkan hasil tersebut, data perlu diolah terlebih dahulu sebelum digunakan, yaitu dengan menangani data kosong, menyederhanakan kode keadaan cuaca, mengecek outlier, melakukan scaling, melakukan encoding, dan menyeimbangkan kelas pada variabel hujan.
Pada tahap ini dilakukan penanganan missing value yang terdiri dari tiga langkah. Langkah pertama adalah mengecek jumlah data kosong pada setiap variabel. Langkah kedua adalah mengisi data kosong pada variabel keadaan cuaca menggunakan interpolasi. Langkah terakhir adalah mengisi data kosong pada variabel kecepatan angin menggunakan mode imputation.
## hujan suhu kelembapan keadaan_cuaca kecepatan_angin
## 0 0 0 9 314
## hujan suhu kelembapan keadaan_cuaca kecepatan_angin
## 0.00 0.00 0.00 1.21 42.26
## # 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
# jumlah baris yang tersisa jika semua baris ber-NA dihapus
data_dropna <- data %>% drop_na()
nrow(data_dropna)## [1] 424
Berdasarkan output di atas, variabel hujan, suhu, dan kelembapan tidak memiliki data kosong. Data kosong hanya terdapat pada keadaan cuaca sebanyak 9 data (1,21%) dan kecepatan angin sebanyak 314 data (42,26%). Hasil filter() menampilkan 9 baris yang nilai keadaan cuaca nya kosong, dan 4 baris di antaranya juga tidak memiliki nilai kecepatan angin. Jika semua baris yang memiliki data kosong dihapus, data hanya tersisa 424 baris sehingga ada 319 baris atau sekitar 43% data yang hilang. Jumlah ini terlalu banyak untuk dibuang, sehingga data kosong ditangani dengan imputasi. Sesuai soal, keadaan cuaca diisi dengan interpolasi dan kecepatan angin diisi dengan mode imputation.
df_imp4 <- data
idx_na <- which(is.na(data$keadaan_cuaca)) # posisi baris yang kosong
# Interpolasi linier
df_imp4$keadaan_cuaca <- na.approx(df_imp4$keadaan_cuaca, na.rm = FALSE)
# bandingkan nilai pengapit dengan hasil interpolasi
data.frame(Baris = idx_na,
Nilai_Sebelumnya = data$keadaan_cuaca[idx_na - 1],
Hasil_Interpolasi = df_imp4$keadaan_cuaca[idx_na],
Nilai_Sesudahnya = data$keadaan_cuaca[idx_na + 1])## Baris Nilai_Sebelumnya Hasil_Interpolasi Nilai_Sesudahnya
## 1 42 2 2.0 2
## 2 207 61 61.0 61
## 3 310 63 62.0 61
## 4 512 3 49.0 95
## 5 598 21 21.0 21
## 6 639 95 56.0 17
## 7 675 2 2.0 2
## 8 723 2 2.0 2
## 9 725 2 1.5 1
## hujan suhu kelembapan keadaan_cuaca kecepatan_angin
## 0 0 0 0 314
Tabel di atas menunjukkan nilai sebelum data kosong, hasil interpolasi, dan nilai sesudah data kosong. Pada baris 42, 207, 598, 675, dan 723, nilai sebelum dan sesudahnya sama sehingga hasil interpolasinya juga sama, misalnya baris 207 diisi dengan kode 61. Pada baris 310, 512, 639, dan 725, nilai sebelum dan sesudahnya berbeda sehingga hasil interpolasinya adalah nilai tengah dari kedua nilai tersebut, yaitu 62, 49, 56, dan 1,5. Contohnya, baris 512 diapit oleh kode 3 dan 95 sehingga hasil interpolasinya adalah (3 + 95)/2 = 49.
Karena keadaan cuaca berupa kode, hasil interpolasi tidak selalu sesuai dengan kode cuaca yang sebenarnya. Nilai 1,5 tidak ada dalam daftar kode, sedangkan nilai 49 dan 56 pada tabel kode WMO berarti kabut yang membentuk lapisan es dan gerimis beku, padahal suhu pada data ini selalu di atas 20 °C. Oleh karena itu, hasil interpolasi pada variabel ini hanya merupakan nilai pendekatan, dan pengaruhnya akan dikurangi pada tahap kardinalitas karena kode-kode tersebut dikelompokkan ke dalam interval. Setelah interpolasi, keadaan cuaca sudah tidak memiliki data kosong, sedangkan kecepatan angin masih memiliki 314 data kosong.
# frekuensi nilai kecepatan angin sebelum imputasi
sort(table(df_imp4$kecepatan_angin), decreasing = TRUE)##
## 3 5 8 6 4 2 10 9 7 11 12 14 15 13 21
## 57 55 47 40 39 38 35 29 27 27 18 9 5 2 1
## [1] "3"
df_imp4$kecepatan_angin[is.na(df_imp4$kecepatan_angin)] <- as.numeric(modus)
colSums(is.na(df_imp4))## hujan suhu kelembapan keadaan_cuaca kecepatan_angin
## 0 0 0 0 0
# perbandingan statistik sebelum dan sesudah imputasi
data.frame(
Keterangan = c("Sebelum imputasi", "Sesudah imputasi"),
Mean = c(mean(data$kecepatan_angin, na.rm = TRUE), mean(df_imp4$kecepatan_angin)),
Median = c(median(data$kecepatan_angin, na.rm = TRUE), median(df_imp4$kecepatan_angin)),
SD = c(sd(data$kecepatan_angin, na.rm = TRUE), sd(df_imp4$kecepatan_angin))
)## Keterangan Mean Median SD
## 1 Sebelum imputasi 6.655012 6 3.352688
## 2 Sesudah imputasi 5.110363 3 3.122165
p_sebelum <- ggplot(data %>% filter(!is.na(kecepatan_angin)),
aes(x = factor(kecepatan_angin))) +
geom_bar(fill = "#008080", color = "black") +
labs(title = "Sebelum Imputasi", x = "kecepatan angin", y = "Frekuensi") +
theme_minimal()
p_sesudah <- ggplot(df_imp4, aes(x = factor(kecepatan_angin))) +
geom_bar(fill = "#008080", color = "black") +
labs(title = "Sesudah Mode Imputation", x = "kecepatan angin", y = "Frekuensi") +
theme_minimal()
grid.arrange(p_sebelum, p_sesudah, ncol = 2)Gambar 4. Frekuensi nilai kecepatan angin sebelum dan sesudah mode imputation
Berdasarkan tabel frekuensi, nilai kecepatan angin yang paling sering muncul adalah 3, yaitu sebanyak 57 kali, sehingga modus yang digunakan untuk imputasi adalah 3. Setelah 314 data kosong diisi dengan nilai 3, output colSums() menunjukkan bahwa sudah tidak ada data kosong pada semua variabel.
Perbandingan statistik sebelum dan sesudah imputasi menunjukkan bahwa rata-rata kecepatan angin turun dari 6,66 menjadi 5,11, median turun dari 6 menjadi 3, dan simpangan baku turun dari 3,35 menjadi 3,12. Pada Gambar 4 terlihat bahwa sebelum imputasi nilai kecepatan angin tersebar pada rentang 2 sampai 12 dengan frekuensi tertinggi 57. Setelah imputasi, frekuensi nilai 3 naik menjadi 371 atau hampir setengah dari seluruh data. Perubahan ini terjadi karena data kosong pada kecepatan angin cukup banyak (42,26%), sehingga semua data kosong yang diisi dengan modus membuat sebaran data menumpuk di nilai 3.
## # 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
## [1] 5.0 1.0 2.0 3.0 14.0 60.0 61.0 21.0 29.0 10.0 16.0 62.0 63.0 65.0 15.0 17.0 13.0
## [18] 95.0 91.0 49.0 97.0 56.0 1.5
## [1] 23
Sepuluh baris pertama menunjukkan bahwa data sudah lengkap setelah imputasi, misalnya kecepatan angin pada tiga baris pertama yang sebelumnya NA sekarang bernilai 3. Variabel keadaan cuaca memiliki 23 nilai unik, yang terdiri dari 20 kode asli dan 3 nilai baru hasil interpolasi (1,5; 49; dan 56). Jika langsung dilakukan one-hot encoding, variabel ini akan menghasilkan 23 kolom baru, padahal beberapa nilai hanya muncul satu kali. Oleh karena itu, kardinalitas keadaan cuaca perlu dikurangi.
bins <- c(0, 10, 20, 30, 40, 50, 60, 70, 80, 90, 100)
labels <- 0:9
df_imp4$keadaan_cuaca_reduced <- cut(
df_imp4$keadaan_cuaca,
breaks = bins,
labels = labels,
right = TRUE,
include.lowest = TRUE
)
set.seed(123)
cat("--- Hasil Perbandingan ---\n")
print(df_imp4[sample(nrow(df_imp4), 10), ])
cat("\n--- Pengecekan Kardinalitas ---\n")
cat('Jumlah kategori di "keadaan cuaca" asli :', length(unique(df_imp4$keadaan_cuaca)), "\n")
cat('Jumlah kategori di "keadaan cuaca_reduced" :', length(unique(df_imp4$keadaan_cuaca_reduced)), "\n")
cat("\nKategori unik yang baru (reduced):\n")
print(unique(df_imp4$keadaan_cuaca_reduced))
cat("\nJumlah observasi tiap kategori:\n")
table(df_imp4$keadaan_cuaca_reduced)## --- Hasil Perbandingan ---
## # A tibble: 10 × 6
## hujan suhu kelembapan keadaan_cuaca kecepatan_angin keadaan_cuaca_reduced
## <dbl> <dbl> <dbl> <dbl> <dbl> <fct>
## 1 2 31.6 60 2 10 0
## 2 2 28.9 80 15 6 1
## 3 1 25.1 94 61 10 6
## 4 2 24.5 89 2 3 0
## 5 2 24.6 91 2 2 0
## 6 2 24.4 93 1 3 0
## 7 2 29.3 76 3 8 0
## 8 2 27.4 76 1 6 0
## 9 2 28.9 65 3 10 0
## 10 1 26.5 84 3 3 0
##
## --- Pengecekan Kardinalitas ---
## Jumlah kategori di "keadaan cuaca" asli : 23
## Jumlah kategori di "keadaan cuaca_reduced" : 7
##
## Kategori unik yang baru (reduced):
## [1] 0 1 5 6 2 9 4
## Levels: 0 1 2 3 4 5 6 7 8 9
##
## Jumlah observasi tiap kategori:
##
## 0 1 2 3 4 5 6 7 8 9
## 523 48 46 0 1 28 71 0 0 26
Kode keadaan cuaca dikelompokkan ke dalam 10 interval dengan lebar 10 menggunakan fungsi cut(). Interval (0, 10] diberi label 0, interval (10, 20] diberi label 1, dan seterusnya sampai interval (90, 100] diberi label 9. Pada 10 baris acak yang ditampilkan, kode 1, 2, dan 3 masuk ke kategori 0, kode 15 masuk ke kategori 1, dan kode 61 masuk ke kategori 6.
Setelah pengelompokan, jumlah kategori berkurang dari 23 menjadi 7, yaitu kategori 0, 1, 2, 4, 5, 6, dan 9. Kategori 3, 7, dan 8 tidak memiliki anggota karena tidak ada kode yang berada pada interval tersebut, tetapi tetap disimpan sebagai level faktor. Kategori 0 berisi kode 1 sampai 10 (kondisi tanpa hujan) dan merupakan kategori terbanyak dengan 523 observasi. Kategori 6 berisi kode hujan 61 sampai 65 sebanyak 71 observasi, kategori 1 berisi kode 13 sampai 17 (hujan atau petir yang terlihat dari kejauhan) sebanyak 48 observasi, kategori 2 berisi kode 21 dan 29 (hujan atau badai petir pada satu jam sebelumnya) sebanyak 46 observasi, kategori 5 berisi kode 60 dan nilai 56 sebanyak 28 observasi, kategori 9 berisi kode 91 sampai 97 (hujan disertai badai petir) sebanyak 26 observasi, dan kategori 4 hanya berisi 1 observasi, yaitu nilai 49 hasil interpolasi. Karena interval dibuat tertutup di kanan (right = TRUE), kode 60 masuk ke kategori 5 dan terpisah dari kode hujan lain yang berada di kategori 6.
set.seed(200)
split_stratify <- initial_split(df_imp4, prop = 0.8, strata = hujan)
X_train <- training(split_stratify) %>% select(-hujan)
X_test <- testing(split_stratify) %>% select(-hujan)
y_train <- training(split_stratify)$hujan
y_test <- testing(split_stratify)$hujan
dim(X_train)## [1] 594 5
## [1] 149 5
## y_train
## 1 2
## 131 463
## y_test
## 1 2
## 33 116
round(rbind("Data awal" = prop.table(table(df_imp4$hujan)),
"Data training" = prop.table(table(y_train)),
"Data testing" = prop.table(table(y_test))) * 100, 2)## 1 2
## Data awal 22.07 77.93
## Data training 22.05 77.95
## Data testing 22.15 77.85
## samakan level keadaan cuaca_reduced
all_levels <- levels(df_imp4$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)
table(X_train$keadaan_cuaca_reduced)##
## 0 1 2 3 4 5 6 7 8 9
## 422 35 39 0 0 18 56 0 0 24
##
## 0 1 2 3 4 5 6 7 8 9
## 101 13 7 0 1 10 15 0 0 2
Berdasarkan output di atas, data dibagi menjadi 594 baris data training dan 149 baris data testing dengan 5 kolom fitur, karena variabel hujan sudah dipisahkan menjadi y_train dan y_test. Data training berisi 131 observasi kelas 1 dan 463 observasi kelas 2, sedangkan data testing berisi 33 observasi kelas 1 dan 116 observasi kelas 2. Jumlah ini sesuai dengan rumus stratified split, yaitu 0,8 × 164 = 131,2 yang dibulatkan ke bawah menjadi 131 dan 0,8 × 579 = 463,2 yang dibulatkan ke bawah menjadi 463.
Persentase kelas 1 pada data awal, data training, dan data testing hampir sama, yaitu 22,07%, 22,05%, dan 22,15%. Hal ini menunjukkan bahwa stratified split berhasil mempertahankan proporsi kelas pada kedua bagian data. Dua tabel terakhir menunjukkan jumlah observasi setiap kategori keadaan cuaca_reduced. Kategori 4 tidak ada pada data training, tetapi ada 1 observasi pada data testing. Karena level faktornya sudah disamakan, kategori 4 tetap dapat dikenali pada tahap encoding.
Pada tahap ini dilakukan penanganan outlier pada variabel numerik, yaitu suhu, kelembapan, dan kecepatan angin, melalui tiga langkah. Langkah pertama adalah mendeteksi outlier menggunakan metode IQR berdasarkan data training. Langkah kedua adalah mengecek nilai skewness untuk melihat bentuk sebaran data. Langkah terakhir adalah menangani outlier dengan capping, yaitu mengganti nilai yang melewati batas dengan nilai batasnya.
list_num <- c("suhu", "kelembapan", "kecepatan_angin")
list_outlier <- c()
list_lower_bound <- c()
list_upper_bound <- c()
for (i in list_num) {
Q1 <- quantile(X_train[[i]], 0.25, na.rm = TRUE)
Q3 <- 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)
total_outliers <- num_outliers_lower + num_outliers_upper
list_outlier <- c(list_outlier, total_outliers)
}
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
Tabel outliers berisi jumlah outlier, batas bawah, dan batas atas setiap variabel numerik yang dihitung dari data training. suhu memiliki batas bawah 17,46 dan batas atas 35,76, sedangkan nilai suhu berada pada rentang 22,6 sampai 32,0, sehingga tidak ada outlier. kelembapan memiliki batas bawah 46,5 dan batas atas 122,5, sedangkan nilai kelembapan terendah adalah 52, sehingga juga tidak ada outlier. kecepatan angin memiliki \(Q_1 = 3\) dan \(Q_3 = 7\), sehingga \(IQR = 4\), batas bawahnya \(3 - 1{,}5(4) = -3\), dan batas atasnya \(7 + 1{,}5(4) = 13\). Pada variabel ini terdapat 12 outlier, yaitu nilai 14 dan 15 yang berada di atas batas atas. Nilai \(Q_1\) sama dengan modus (3) karena pengaruh mode imputation pada tahap sebelumnya.
df_num <- X_train[, list_num]
nilai_skew <- c()
nilai_skew_normal <- c()
for (i in colnames(df_num)) {
skew_val <- skewness(X_train[[i]], na.rm = TRUE)
if (skew_val >= -0.5 && skew_val <= 0.5) {
nilai_skew_normal <- c(nilai_skew_normal, i)
} else {
nilai_skew <- c(nilai_skew, i)
}
}
round(sapply(df_num, skewness), 3)## suhu kelembapan kecepatan_angin
## 0.288 -0.552 1.237
## kolom yang mempunyai nilai skewness sedang : kelembapan kecepatan_angin
## kolom yang mempunyai nilai skewness normal : suhu
Nilai skewness suhu sebesar 0,288 berada di antara −0,5 dan 0,5 sehingga sebarannya termasuk normal atau mendekati simetris. kelembapan memiliki skewness −0,552 yang berarti sebarannya sedikit menceng ke kiri, sedangkan kecepatan angin memiliki skewness 1,237 yang berarti sebarannya menceng ke kanan. Pada sintaks contoh, hasil skewness digunakan untuk memilih metode scaling. Namun, sesuai soal untuk NPM genap, semua variabel numerik di-scaling menggunakan Standard Scaler.
# Capping pada seluruh kolom numerik dengan batas IQR dari data training
X_train_capped <- X_train
X_test_capped <- X_test
for (i in seq_along(list_num)) {
kolom <- list_num[i]
batas <- c(outliers$Lower_Bound[i], outliers$Upper_Bound[i])
X_train_capped[[kolom]] <- Winsorize(X_train[[kolom]], val = batas)
X_test_capped[[kolom]] <- Winsorize(X_test[[kolom]], val = batas)
}
# jumlah nilai yang berubah dan nilai maksimum sebelum-sesudah capping
data.frame(
Kolom = list_num,
Berubah_Training = sapply(list_num, function(k) sum(X_train_capped[[k]] != X_train[[k]])),
Berubah_Testing = sapply(list_num, function(k) sum(X_test_capped[[k]] != X_test[[k]])),
Maks_Sebelum = sapply(list_num, function(k) max(X_train[[k]])),
Maks_Sesudah = sapply(list_num, function(k) max(X_train_capped[[k]])),
row.names = NULL
)## Kolom Berubah_Training Berubah_Testing Maks_Sebelum Maks_Sesudah
## 1 suhu 0 0 32 32
## 2 kelembapan 0 0 100 100
## 3 kecepatan_angin 12 3 15 13
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() +
theme(axis.text.x = element_blank())
list(p1, p2)
}
for (col in list_num) {
sebelum <- diagnostic_plots(X_train, col, "Sebelum Capping")
sesudah <- diagnostic_plots(X_train_capped, col, "Sesudah Capping")
grid.arrange(sebelum[[1]], sebelum[[2]], sesudah[[1]], sesudah[[2]],
ncol = 2, top = col)
}Gambar 5. Histogram dan boxplot suhu sebelum dan sesudah capping
Gambar 6. Histogram dan boxplot kelembapan sebelum dan sesudah capping
Gambar 7. Histogram dan boxplot kecepatan angin sebelum dan sesudah capping
Sesuai soal, capping dilakukan pada ketiga variabel numerik menggunakan batas IQR dari data training, lalu batas yang sama juga digunakan pada data testing. Berdasarkan tabel di atas, nilai suhu dan kelembapan tidak berubah karena kedua variabel ini tidak memiliki outlier. Perubahan hanya terjadi pada kecepatan angin, yaitu 12 nilai pada data training dan 3 nilai pada data testing yang diganti menjadi 13. Nilai maksimum kecepatan angin pada data training turun dari 15 menjadi 13.
Gambar 5 dan Gambar 6 menunjukkan histogram dan boxplot suhu serta kelembapan yang sama persis sebelum dan sesudah capping. Histogram suhu memiliki dua puncak, yaitu sekitar 23–25 °C dan 29–30 °C, yang kemungkinan menggambarkan perbedaan suhu pada malam dan siang hari. Histogram kelembapan menumpuk pada nilai 90–100% dengan ekor ke arah kiri, sesuai dengan nilai skewness-nya yang negatif. Pada Gambar 7, boxplot kecepatan angin sebelum capping masih memiliki titik di atas whisker, yaitu nilai 14 dan 15. Setelah capping, titik tersebut tidak ada lagi karena nilainya sudah diganti menjadi 13. Garis median pada boxplot kecepatan angin juga berimpit dengan sisi bawah kotak karena median dan kuartil pertamanya sama-sama bernilai 3.
# parameter Standard Scaler dihitung dari data training
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
# terapkan ke data training dan data testing
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]] - mean_val[col]) / sd_val[col]
X_test_scale[[col]] <- (X_test_capped[[col]] - mean_val[col]) / 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
# cek rata-rata dan simpangan baku setelah scaling
round(rbind("Mean training" = sapply(X_train_scale[list_num], mean),
"SD training" = sapply(X_train_scale[list_num], sd),
"Mean testing" = sapply(X_test_scale[list_num], mean),
"SD testing" = sapply(X_test_scale[list_num], sd)), 4)## suhu kelembapan kecepatan_angin
## Mean training 0.0000 0.0000 0.0000
## SD training 1.0000 1.0000 1.0000
## Mean testing -0.0331 0.0535 0.0083
## SD testing 1.0027 1.0067 1.0371
sebelum_scale <- X_train_capped %>%
select(all_of(list_num)) %>%
pivot_longer(everything(), names_to = "Variabel", values_to = "Nilai") %>%
mutate(Tahap = "Sebelum Scaling")
sesudah_scale <- X_train_scale %>%
select(all_of(list_num)) %>%
pivot_longer(everything(), names_to = "Variabel", values_to = "Nilai") %>%
mutate(Tahap = "Sesudah Standard Scaler")
bind_rows(sebelum_scale, sesudah_scale) %>%
mutate(Tahap = factor(Tahap, levels = c("Sebelum Scaling", "Sesudah Standard Scaler"))) %>%
ggplot(aes(x = Variabel, y = Nilai)) +
geom_boxplot(fill = "#008080") +
facet_wrap(~ Tahap, scales = "free_y") +
theme_minimal()Gambar 8. Boxplot data numerik sebelum dan sesudah Standard Scaler
Output pertama menunjukkan parameter Standard Scaler yang dihitung dari data training setelah capping. Rata-rata suhu adalah 26,56 dengan simpangan baku 2,56, rata-rata kelembapan 83,76 dengan simpangan baku 11,55, dan rata-rata kecepatan angin 5,07 dengan simpangan baku 2,99. Parameter ini digunakan untuk mentransformasi data training dan data testing.
Pada output head(), suhu pada baris pertama bernilai 23 °C sehingga hasil scaling-nya adalah \((23 - 26{,}56)/2{,}56 \approx -1{,}39\). Nilai negatif berarti nilai tersebut berada di bawah rata-rata, sedangkan nilai positif berarti berada di atas rata-rata. Kolom keadaan cuaca dan keadaan cuaca_reduced tidak di-scaling karena merupakan data kategorik.
Setelah scaling, ketiga variabel numerik pada data training memiliki rata-rata 0 dan simpangan baku 1. Pada data testing, rata-ratanya berada antara −0,033 sampai 0,054 dan simpangan bakunya antara 1,003 sampai 1,037. Nilai ini tidak tepat 0 dan 1 karena parameter yang digunakan berasal dari data training, dan hal ini wajar untuk menghindari data leakage. Gambar 8 menunjukkan bahwa sebelum scaling, kelembapan memiliki skala yang jauh lebih besar dibandingkan suhu dan kecepatan angin. Setelah Standard Scaler, ketiga variabel berada pada skala yang sama.
## 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>
## # 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>
## [1] 594 10
## [1] 149 10
## keadaan_cuaca_reduced_X0 keadaan_cuaca_reduced_X1 keadaan_cuaca_reduced_X2
## 422 35 39
## keadaan_cuaca_reduced_X3 keadaan_cuaca_reduced_X4 keadaan_cuaca_reduced_X5
## 0 0 18
## keadaan_cuaca_reduced_X6 keadaan_cuaca_reduced_X7 keadaan_cuaca_reduced_X8
## 56 0 0
## keadaan_cuaca_reduced_X9
## 24
## keadaan_cuaca_reduced_X0 keadaan_cuaca_reduced_X1 keadaan_cuaca_reduced_X2
## 101 13 7
## keadaan_cuaca_reduced_X3 keadaan_cuaca_reduced_X4 keadaan_cuaca_reduced_X5
## 0 1 10
## keadaan_cuaca_reduced_X6 keadaan_cuaca_reduced_X7 keadaan_cuaca_reduced_X8
## 15 0 0
## keadaan_cuaca_reduced_X9
## 2
##
## 1
## 594
Fungsi step_dummy() dengan one_hot = TRUE mengubah keadaan cuaca_reduced menjadi 10 kolom dummy, yaitu keadaan cuaca_reduced_X0 sampai keadaan cuaca_reduced_X9. Setiap kolom bernilai 1 jika observasi termasuk kategori tersebut dan 0 jika tidak. Pada output terlihat bahwa sepuluh baris pertama data training bernilai 1 pada kolom X0 karena semuanya termasuk kategori 0.
Hasil encoding pada data training berukuran 594 × 10, sedangkan pada data testing berukuran 149 × 10. Jumlah angka 1 pada setiap kolom sama dengan jumlah observasi setiap kategori pada tahap splitting, misalnya kolom X0 berisi 422 angka 1 pada data training. Output table(rowSums()) menunjukkan bahwa semua baris (594 baris) hanya memiliki satu angka 1 sehingga setiap observasi hanya masuk ke satu kategori. Kolom X3, X4, X7, dan X8 pada data training seluruhnya bernilai 0 karena kategorinya tidak memiliki anggota, sehingga kolom tersebut tidak memberikan informasi bagi model. Kolom X4 pada data testing berisi satu angka 1 karena ada satu observasi kategori 4 pada data testing.
# gabungkan fitur numerik hasil scaling dengan kolom dummy hasil encoding
X_train_final <- bind_cols(X_train_scale %>% select(all_of(list_num)), X_train_encoded)
X_test_final <- bind_cols(X_test_scale %>% select(all_of(list_num)), X_test_encoded)
dim(X_train_final)## [1] 594 13
## [1] 149 13
## Rows: 594
## Columns: 13
## $ suhu <dbl> -1.39130628, -1.00026060, 0.09466729, 1.18959518, 1.658…
## $ kelembapan <dbl> 0.97328758, 0.54042383, -0.58502192, -1.88361317, -2.40…
## $ kecepatan_angin <dbl> -0.6925895, -0.6925895, -0.6925895, -1.0273317, 0.64637…
## $ keadaan_cuaca_reduced_X0 <dbl> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0…
## $ keadaan_cuaca_reduced_X1 <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0…
## $ keadaan_cuaca_reduced_X2 <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ keadaan_cuaca_reduced_X3 <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ keadaan_cuaca_reduced_X4 <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ keadaan_cuaca_reduced_X5 <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0…
## $ keadaan_cuaca_reduced_X6 <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 1…
## $ keadaan_cuaca_reduced_X7 <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ keadaan_cuaca_reduced_X8 <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
## $ keadaan_cuaca_reduced_X9 <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
Hasil encoding kemudian digabungkan dengan tiga variabel numerik yang sudah di-scaling, sehingga data training akhir berukuran 594 × 13 dan data testing akhir berukuran 149 × 13. Output glimpse() menunjukkan ketiga belas fitur tersebut, yaitu suhu, kelembapan, kecepatan angin, dan 10 kolom dummy keadaan cuaca, dengan tipe data dbl (numerik). Kolom keadaan cuaca asli tidak digunakan lagi karena informasinya sudah diwakili oleh kolom dummy. Semua fitur sudah berbentuk numerik dan tidak ada data kosong, sehingga data dapat digunakan pada tahap SMOTE.
## y_train
## 1 2
## 131 463
train_full <- X_train_final %>%
mutate(hujan = y_train)
train_full$hujan <- factor(train_full$hujan) # SMOTE butuh target berupa factor
set.seed(42)
resep_smote <- recipe(hujan ~ ., data = train_full) %>%
step_smote(hujan, over_ratio = 1, neighbors = 5)
resep_smote_prep <- prep(resep_smote, training = train_full)
train_balanced <- bake(resep_smote_prep, new_data = NULL)
# Pisahkan lagi jadi X dan y
X_train_balanced <- train_balanced %>% select(-hujan)
y_train_balanced <- train_balanced$hujan
table(y_train_balanced)## y_train_balanced
## 1 2
## 463 463
## [1] 926 14
## y_test
## 1 2
## 33 116
label_hujan <- c("1" = "1 (hujan)", "2" = "2 (Tidak hujan)")
p_awal <- ggplot(data.frame(hujan = factor(y_train)), aes(x = hujan, fill = hujan)) +
geom_bar() +
geom_text(stat = "count", aes(label = after_stat(count)), vjust = -0.4) +
scale_x_discrete(labels = label_hujan) +
scale_fill_manual(values = c("1" = "#E07B00", "2" = "#008080")) +
ylim(0, 520) +
labs(title = "Sebelum SMOTE", x = "hujan", y = "Jumlah") +
theme_minimal() + theme(legend.position = "none")
p_smote <- ggplot(data.frame(hujan = y_train_balanced), aes(x = hujan, fill = hujan)) +
geom_bar() +
geom_text(stat = "count", aes(label = after_stat(count)), vjust = -0.4) +
scale_x_discrete(labels = label_hujan) +
scale_fill_manual(values = c("1" = "#E07B00", "2" = "#008080")) +
ylim(0, 520) +
labs(title = "Sesudah SMOTE", x = "hujan", y = "Jumlah") +
theme_minimal() + theme(legend.position = "none")
grid.arrange(p_awal, p_smote, ncol = 2)Gambar 9. Jumlah kelas target pada data training sebelum dan sesudah SMOTE
Sebelum SMOTE, data training terdiri dari 131 observasi kelas 1 (hujan) dan 463 observasi kelas 2 (tidak hujan) dengan imbalance ratio sebesar 463/131 = 3,53. SMOTE dijalankan dengan over_ratio = 1 sehingga jumlah kelas minoritas ditambah sampai sama dengan kelas mayoritas, dan neighbors = 5 sehingga data sintetis dibentuk berdasarkan 5 tetangga terdekat. Setelah SMOTE, kedua kelas memiliki jumlah yang sama, yaitu 463 observasi, sehingga data training menjadi 926 baris dan 14 kolom (13 fitur dan 1 target). Jumlah data sintetis yang ditambahkan adalah 463 − 131 = 332 data, seperti terlihat pada Gambar 9.
Pada sintaks contoh, SMOTE dijalankan pada X_train_encoded yang hanya berisi kolom dummy. Pada laporan ini, SMOTE dijalankan pada X_train_final yang berisi variabel numerik hasil scaling dan kolom dummy, agar semua fitur ikut digunakan saat mencari tetangga terdekat. SMOTE hanya diterapkan pada data training, sedangkan data testing tetap berisi 33 observasi kelas 1 dan 116 observasi kelas 2 supaya evaluasi model nantinya sesuai dengan kondisi data yang sebenarnya.
## Rows: 5
## Columns: 14
## $ suhu <dbl> -1.088785, -1.096800, -1.254800, -1.168689, -1.272640
## $ kelembapan <dbl> 1.169270, 1.146433, 1.188922, 1.233006, 1.149428
## $ kecepatan_angin <dbl> -0.6925895, -0.6925895, -0.6925895, -0.6925895, -0.6925…
## $ keadaan_cuaca_reduced_X0 <dbl> 0, 0, 0, 0, 0
## $ keadaan_cuaca_reduced_X1 <dbl> 0, 0, 0, 0, 0
## $ keadaan_cuaca_reduced_X2 <dbl> 0, 0, 0, 0, 0
## $ keadaan_cuaca_reduced_X3 <dbl> 0, 0, 0, 0, 0
## $ keadaan_cuaca_reduced_X4 <dbl> 0, 0, 0, 0, 0
## $ keadaan_cuaca_reduced_X5 <dbl> 1.0000000, 0.5312384, 0.0000000, 0.0000000, 0.0000000
## $ keadaan_cuaca_reduced_X6 <dbl> 0.0000000, 0.4687616, 1.0000000, 1.0000000, 1.0000000
## $ keadaan_cuaca_reduced_X7 <dbl> 0, 0, 0, 0, 0
## $ keadaan_cuaca_reduced_X8 <dbl> 0, 0, 0, 0, 0
## $ keadaan_cuaca_reduced_X9 <dbl> 0, 0, 0, 0, 0
## $ hujan <fct> 1, 1, 1, 1, 1
# cek kolom dummy pada data sintetis: adakah yang bernilai pecahan (bukan 0/1)?
dummy_sintetis <- train_balanced %>%
slice((nrow(train_full) + 1):n()) %>%
select(starts_with("keadaan_cuaca_reduced"))
n_pecahan <- sum(rowSums(dummy_sintetis > 0 & dummy_sintetis < 1) > 0)
nrow(dummy_sintetis) # jumlah data sintetis## [1] 332
## [1] 20
Output glimpse() menampilkan lima baris terakhir data training yang merupakan data sintetis hasil SMOTE. Kelima baris tersebut memiliki label hujan = 1. Nilai fiturnya tidak sama persis dengan data asli karena dibentuk di antara dua observasi kelas 1 yang berdekatan menggunakan rumus SMOTE pada subbab 2.8.
Dari 332 data sintetis, terdapat 20 baris (sekitar 6%) yang kolom dummy-nya bernilai pecahan, yaitu di antara 0 dan 1. Hal ini terjadi karena SMOTE juga melakukan interpolasi pada kolom dummy ketika observasi acuan dan tetangganya berasal dari kategori cuaca yang berbeda. Jumlahnya tidak banyak, tetapi perlu diperhatikan karena kolom dummy seharusnya hanya bernilai 0 atau 1.
Feature engineering merupakan tahap untuk mengubah data mentah menjadi fitur yang siap digunakan dalam algoritma machine learning. Tahapan ini terdiri dari beberapa jenis, yaitu penanganan missing value untuk mengisi data yang kosong, reduksi kardinalitas untuk menyederhanakan variabel kategorik yang memiliki banyak kategori, splitting data untuk membagi data menjadi data training dan data testing, penanganan outlier untuk mengatasi nilai yang terlalu ekstrem, scaling untuk menyamakan skala antarvariabel, encoding untuk mengubah data kategorik menjadi numerik, dan penanganan imbalance data untuk menyeimbangkan jumlah kelas pada variabel target.
Teknik feature engineering di RStudio dilakukan dengan bantuan beberapa paket dan fungsi. Interpolasi dilakukan menggunakan fungsi na.approx() dari paket zoo, sedangkan mode imputation dilakukan dengan mencari nilai yang paling sering muncul menggunakan fungsi table(). Reduksi kardinalitas dilakukan dengan fungsi cut(), dan stratified split dilakukan dengan fungsi initial_split() dari paket rsample. Deteksi outlier dilakukan dengan metode IQR menggunakan fungsi quantile(), lalu capping dilakukan dengan fungsi Winsorize() dari paket DescTools. Standard Scaler dihitung menggunakan rata-rata dan simpangan baku dari data training, one-hot encoding dilakukan dengan fungsi step_dummy() dari paket recipes, dan SMOTE dilakukan dengan fungsi step_smote() dari paket themis.
Berdasarkan hasil praktikum, data kosong pada keadaan cuaca sebanyak 9 data diisi dengan interpolasi, sedangkan data kosong pada kecepatan angin sebanyak 314 data diisi dengan modus, yaitu 3. Kardinalitas keadaan cuaca berhasil dikurangi dari 23 nilai unik menjadi 7 kategori. Data kemudian dibagi dengan stratified split menjadi 594 data training dan 149 data testing dengan proporsi kelas yang tetap sama. Outlier hanya ditemukan pada kecepatan angin dan sudah diganti dengan batas atas 13 melalui capping. Setelah itu, ketiga variabel numerik di-scaling dengan Standard Scaler dan keadaan cuaca diubah menjadi 10 kolom dummy dengan one-hot encoding. Terakhir, SMOTE membuat jumlah kelas hujan dan tidak hujan pada data training menjadi seimbang, yaitu masing-masing 463 observasi. Dengan demikian, data sudah bersih dan siap digunakan untuk tahap pemodelan.
Chapra, S. C., & Canale, R. P. (2015). Numerical methods for engineers (7th ed.). McGraw-Hill Education.
Chen, W., Yang, K., Yu, Z., Shi, Y., & Chen, C. L. P. (2024). A survey on imbalanced learning: Latest research, applications and future directions. Artificial Intelligence Review, 57(6), 137. https://doi.org/10.1007/s10462-024-10759-6
Dash, C. S. K., Behera, A. K., Dehuri, S., & Ghosh, A. (2023). An outliers detection and elimination framework in classification task of data mining. Decision Analytics Journal, 6, 100164. https://doi.org/10.1016/j.dajour.2023.100164
de Amorim, L. B. V., Cavalcanti, G. D. C., & Cruz, R. M. O. (2023). The choice of scaling technique matters for classification performance. Applied Soft Computing, 133, 109924.
Elreedy, D., Atiya, A. F., & Kamalov, F. (2024). A theoretical distribution analysis of synthetic minority oversampling technique (SMOTE) for imbalanced learning. Machine Learning, 113, 4903–4923. https://doi.org/10.1007/s10994-022-06296-4
Emmanuel, T., Maupong, T., Mpoeleng, D., Semong, T., Mphago, B., & Tabona, O. (2021). A survey on missing data in machine learning. Journal of Big Data, 8, 140. https://doi.org/10.1186/s40537-021-00516-9
Géron, A. (2019). Hands-on machine learning with Scikit-Learn, Keras, and TensorFlow (2nd ed.). O’Reilly Media.
Joseph, V. R. (2022). Optimal ratio for data splitting. Statistical Analysis and Data Mining: The ASA Data Science Journal, 15(4), 531–538.
Kapoor, S., & Narayanan, A. (2023). Leakage and the reproducibility crisis in machine-learning-based science. Patterns, 4(9), 100804. https://doi.org/10.1016/j.patter.2023.100804
Kuhn, M., & Johnson, K. (2019). Feature engineering and selection: A practical approach for predictive models. CRC Press.
Little, R. J. A., & Rubin, D. B. (2019). Statistical analysis with missing data (3rd ed.). Wiley.
Muraina, I. O. (2022). Ideal dataset splitting ratios in machine learning algorithms: General concerns for data scientists and data analysts. Dalam Proceedings of the 7th International Mardin Artuklu Scientific Research Conference (hlm. 496–504).
Pargent, F., Pfisterer, F., Thomas, J., & Bischl, B. (2022). Regularized target encoding outperforms traditional methods in supervised machine learning with high cardinality features. Computational Statistics, 37(5), 2671–2692. https://doi.org/10.1007/s00180-022-01207-6
Tukey, J. W. (1977). Exploratory data analysis. Addison-Wesley.
Vidal-Paz, J., Rodríguez-Gómez, B. A., & Orosa, J. A. (2023). A comparison of different methods for rainfall imputation: A Galician case study. Applied Sciences, 13(22), 12260. https://doi.org/10.3390/app132212260
WMO. (2019). Manual on codes: International codes, Volume I.1, Part A – Alphanumeric codes (WMO-No. 306). World Meteorological Organization.
Zheng, A., & Casari, A. (2018). Feature engineering for machine learning: Principles and techniques for data scientists. O’Reilly Media.