Persiapan Awal RStudio

Menyiapkan Proyek

Sebagai langkah awal, proyek RStudio sebaiknya disiapkan terlebih dahulu agar seluruh berkas praktikum tersusun dalam satu folder kerja yang sama.

  • Buka RStudio.
  • Pilih File → New Project.
  • Lalu New Directory → New Project.
  • Tentukan nama proyek, misalnya praktikum_preprocessing.
  • Simpan file .Rmd dalam folder proyek tersebut.
  • Klik tombol Knit untuk menghasilkan output HTML.

Mengecek Lingkungan Kerja

Pada tahap awal, dua hal penting yang perlu diperiksa adalah lokasi direktori kerja serta versi R yang sedang digunakan.

getwd()
## [1] "C:/Users/ASUS/Downloads"
R.version.string
## [1] "R version 4.5.2 (2025-10-31 ucrt)"

Seluruh tahapan pada modul ini menggunakan fungsi bawaan R (base R), sehingga praktikum dapat langsung dijalankan tanpa instalasi paket tambahan.

Dataset yang Digunakan

Dataset yang digunakan terdiri dari dataset pelanggan dan dataset transaksi. Pada kondisi awal, data masih memiliki beberapa ketidakteraturan sehingga cocok untuk memperagakan berbagai teknik preprocessing.

Data pelanggan berisi atribut seperti customer_id, nama, usia, pendapatan, kota, dan status. Adapun data transaksi memuat informasi yang berkaitan dengan aktivitas transaksi setiap pelanggan.

pelanggan_raw <- data.frame(
  customer_id = c("C001", "C002", "C003", "C004", "C005", "C006",
                  "C007", "C008", "C009", "C010", "C010", "C011"),
  nama = c("Ani", "Budi", "Citra", "Dodi", "Eka", "Fani",
           "Gilang", "Hana", "Indra", "Joko", "Joko", "Kiki"),
  usia = c(21, 25, 23, 150, 27, NA, 31, 29, 22, 35, 35, 28),
  pendapatan = c(4500000, NA, 5200000, 4800000, 4900000, 5100000,
                 500000000, 4700000, 4600000, 5300000, 5300000, NA),
  kota = c("Pekanbaru", " PKU", "PEKANBARU", "Dumai", "pekanbaru",
           "DUMAI", "Pekanbaru ", "Siak", "PKU", "Dumai", "Dumai", NA),
  status = c("Aktif", "aktif", "ACTIVE", "A", "Tidak Aktif", "nonaktif",
             "Aktif", "AKTIF", "A", "Tidak aktif", "Tidak aktif", "Aktif"),
  stringsAsFactors = FALSE
)

transaksi_raw <- data.frame(
  cust_id = c("C001", "C002", "C003", "C004", "C005", "C006",
              "C007", "C008", "C009", "C010", "C012"),
  jumlah_transaksi = c(5, 3, 7, 2, 6, 4, 20, 5, 3, 8, 1),
  total_purchase = c(1500000, 900000, 2700000, 600000, 2100000, 1300000,
                     25000000, 1700000, 800000, 3200000, 250000),
  stringsAsFactors = FALSE
)

Isi kedua tabel tersebut kemudian dapat diperiksa melalui tampilan berikut.

pelanggan_raw
##    customer_id   nama usia pendapatan       kota      status
## 1         C001    Ani   21    4.5e+06  Pekanbaru       Aktif
## 2         C002   Budi   25         NA        PKU       aktif
## 3         C003  Citra   23    5.2e+06  PEKANBARU      ACTIVE
## 4         C004   Dodi  150    4.8e+06      Dumai           A
## 5         C005    Eka   27    4.9e+06  pekanbaru Tidak Aktif
## 6         C006   Fani   NA    5.1e+06      DUMAI    nonaktif
## 7         C007 Gilang   31    5.0e+08 Pekanbaru        Aktif
## 8         C008   Hana   29    4.7e+06       Siak       AKTIF
## 9         C009  Indra   22    4.6e+06        PKU           A
## 10        C010   Joko   35    5.3e+06      Dumai Tidak aktif
## 11        C010   Joko   35    5.3e+06      Dumai Tidak aktif
## 12        C011   Kiki   28         NA       <NA>       Aktif
transaksi_raw
##    cust_id jumlah_transaksi total_purchase
## 1     C001                5        1.5e+06
## 2     C002                3        9.0e+05
## 3     C003                7        2.7e+06
## 4     C004                2        6.0e+05
## 5     C005                6        2.1e+06
## 6     C006                4        1.3e+06
## 7     C007               20        2.5e+07
## 8     C008                5        1.7e+06
## 9     C009                3        8.0e+05
## 10    C010                8        3.2e+06
## 11    C012                1        2.5e+05

Bagian I — Dasar-Dasar Data Preprocessing

Mengenali Struktur Data

Sebelum melakukan proses pembersihan, struktur dasar data perlu dikenali terlebih dahulu, mulai dari ukuran data, nama kolom, hingga tipe datanya.

Dimensi data. Fungsi dim() digunakan untuk mengetahui jumlah baris dan kolom data secara bersamaan.

dim(pelanggan_raw)
## [1] 12  6

Nama Variabel. Untuk melihat atribut atau nama variabel yang tersedia, fungsi names() dapat digunakan.

names(pelanggan_raw)
## [1] "customer_id" "nama"        "usia"        "pendapatan"  "kota"       
## [6] "status"

Struktur Data. Jika ingin memperoleh gambaran struktur secara menyeluruh, termasuk tipe setiap kolom dan contoh nilainya, str() merupakan fungsi yang praktis.

str(pelanggan_raw)
## 'data.frame':    12 obs. of  6 variables:
##  $ customer_id: chr  "C001" "C002" "C003" "C004" ...
##  $ nama       : chr  "Ani" "Budi" "Citra" "Dodi" ...
##  $ usia       : num  21 25 23 150 27 NA 31 29 22 35 ...
##  $ pendapatan : num  4.5e+06 NA 5.2e+06 4.8e+06 4.9e+06 5.1e+06 5.0e+08 4.7e+06 4.6e+06 5.3e+06 ...
##  $ kota       : chr  "Pekanbaru" " PKU" "PEKANBARU" "Dumai" ...
##  $ status     : chr  "Aktif" "aktif" "ACTIVE" "A" ...

Cuplikan data. head() menampilkan beberapa baris awal sehingga bentuk data dapat diperiksa tanpa harus melihat seluruh isi tabel.

head(pelanggan_raw)
##   customer_id  nama usia pendapatan      kota      status
## 1        C001   Ani   21    4500000 Pekanbaru       Aktif
## 2        C002  Budi   25         NA       PKU       aktif
## 3        C003 Citra   23    5200000 PEKANBARU      ACTIVE
## 4        C004  Dodi  150    4800000     Dumai           A
## 5        C005   Eka   27    4900000 pekanbaru Tidak Aktif
## 6        C006  Fani   NA    5100000     DUMAI    nonaktif

Statistik Deskriptif. Terakhir, summary() memberikan ringkasan tiap kolom, baik numerik maupun kategorikal, seperti nilai minimum, maksimum, dan rata-rata pada variabel numerik.

summary(pelanggan_raw)
##  customer_id            nama                usia          pendapatan       
##  Length:12          Length:12          Min.   : 21.00   Min.   :  4500000  
##  Class :character   Class :character   1st Qu.: 24.00   1st Qu.:  4725000  
##  Mode  :character   Mode  :character   Median : 28.00   Median :  5000000  
##                                        Mean   : 38.73   Mean   : 54440000  
##                                        3rd Qu.: 33.00   3rd Qu.:  5275000  
##                                        Max.   :150.00   Max.   :500000000  
##                                        NA's   :1        NA's   :2          
##      kota              status         
##  Length:12          Length:12         
##  Class :character   Class :character  
##  Mode  :character   Mode  :character  
##                                       
##                                       
##                                       
## 

Pengecekan Kualitas Data Awal

Setelah struktur diketahui, kualitas data perlu diperiksa untuk menemukan missing value, duplikasi, kategori yang belum konsisten, maupun nilai numerik yang berada di luar batas kewajaran.

Missing Value

colSums(is.na(pelanggan_raw))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           1           2           1           0

Jika pemeriksaan tersebut dinyatakan dalam persentase, hasilnya dapat dilihat sebagai berikut.

round(colMeans(is.na(pelanggan_raw)) * 100, 2)
## customer_id        nama        usia  pendapatan        kota      status 
##        0.00        0.00        8.33       16.67        8.33        0.00

Duplikasi Data

sum(duplicated(pelanggan_raw))
## [1] 1

Selanjutnya, khusus pada kolom customer_id, perlu diperiksa apakah terdapat ID pelanggan yang tercatat lebih dari sekali.

sum(duplicated(pelanggan_raw$customer_id))
## [1] 1

Konsistensi Kategori

sort(unique(pelanggan_raw$kota))
## [1] " PKU"       "Dumai"      "DUMAI"      "pekanbaru"  "Pekanbaru" 
## [6] "PEKANBARU"  "Pekanbaru " "PKU"        "Siak"

Pemeriksaan yang sama juga dilakukan terhadap kolom status.

sort(unique(pelanggan_raw$status))
## [1] "A"           "ACTIVE"      "aktif"       "Aktif"       "AKTIF"      
## [6] "nonaktif"    "Tidak aktif" "Tidak Aktif"

Pemeriksaan Nilai Numerik

range(pelanggan_raw$usia, na.rm = TRUE)
## [1]  21 150

Hal serupa kemudian diperiksa pada variabel pendapatan.

range(pelanggan_raw$pendapatan, na.rm = TRUE)
## [1] 4.5e+06 5.0e+08

Menyusun Fungsi Ringkasan Kualitas

Menuliskan pemeriksaan yang sama berulang kali akan kurang efisien. Oleh sebab itu, dibuat fungsi audit_data() untuk merangkum indikator kualitas, yaitu tipe data, jumlah dan persentase missing value, serta jumlah nilai unik dalam satu tabel.

audit_data <- function(data) {
  data.frame(
    atribut = names(data),
    tipe = sapply(data, function(x) class(x)[1]),
    jumlah_missing = sapply(data, function(x) sum(is.na(x))),
    persen_missing = round(sapply(data, function(x) mean(is.na(x)) * 100), 2),
    jumlah_unik = sapply(data, function(x) length(unique(x))),
    row.names = NULL
  )
}

audit_awal <- audit_data(pelanggan_raw)
audit_awal
##       atribut      tipe jumlah_missing persen_missing jumlah_unik
## 1 customer_id character              0           0.00          11
## 2        nama character              0           0.00          11
## 3        usia   numeric              1           8.33          11
## 4  pendapatan   numeric              2          16.67          10
## 5        kota character              1           8.33          10
## 6      status character              0           0.00           8

Melalui tabel audit tersebut, atribut yang memiliki missing value tinggi atau jumlah nilai unik yang tidak wajar dapat segera dikenali untuk diperiksa lebih lanjut.

Bagian II — Pembersihan Data

Supaya data mentah (pelanggan_raw) tetap tersedia sebagai pembanding, proses cleaning dilakukan pada salinannya, yaitu objek pelanggan, tanpa mengubah data awal.

pelanggan <- pelanggan_raw

Merapikan Spasi

Kolom teks kota dan status masih menunjukkan perbedaan akibat spasi tambahan serta penggunaan huruf besar dan kecil. Kondisi ini dirapikan terlebih dahulu sebelum standardisasi kategori dilakukan.

Spasi pada bagian awal dan akhir teks dihapus menggunakan trimws():

pelanggan$kota <- trimws(pelanggan$kota)
pelanggan$status <- trimws(pelanggan$status)

Selanjutnya, seluruh teks diubah menjadi huruf kecil agar pencocokan kategori berikutnya tidak dipengaruhi oleh perbedaan kapitalisasi.

pelanggan$kota <- tolower(pelanggan$kota)
pelanggan$status <- tolower(pelanggan$status)

Hasilnya terlihat dari berkurangnya variasi unik pada kedua kolom, walaupun kategorinya belum seluruhnya seragam.

sort(unique(pelanggan$kota))
## [1] "dumai"     "pekanbaru" "pku"       "siak"
sort(unique(pelanggan$status))
## [1] "a"           "active"      "aktif"       "nonaktif"    "tidak aktif"

Menyamakan Kategori

Setelah spasi dan kapitalisasi diperbaiki, masih terdapat singkatan atau istilah berbeda yang sebenarnya memiliki arti sama, misalnya “pku” dan “pekanbaru” serta “active” dan “a”. Karena itu, diperlukan pemetaan kategori secara manual.

Kategori kota “pku” dan “pekanbaru” disatukan menjadi label standar “Pekanbaru”, demikian pula untuk kategori kota lainnya:

pelanggan$kota[pelanggan$kota %in% c("pku", "pekanbaru")] <- "Pekanbaru"
pelanggan$kota[pelanggan$kota == "dumai"] <- "Dumai"
pelanggan$kota[pelanggan$kota == "siak"] <- "Siak"

Langkah serupa diterapkan pada kolom status dengan mengelompokkan seluruh variasi “aktif” dan “tidak aktif” ke label yang sama:

pelanggan$status[pelanggan$status %in% c("aktif", "active", "a")] <- "Aktif"
pelanggan$status[pelanggan$status %in% c("tidak aktif", "nonaktif")] <- "Tidak Aktif"

Pada tahap ini, kedua kolom telah memiliki kategori yang seragam dan tidak lagi memiliki variasi dengan makna yang sama.

sort(unique(pelanggan$kota))
## [1] "Dumai"     "Pekanbaru" "Siak"
sort(unique(pelanggan$status))
## [1] "Aktif"       "Tidak Aktif"

Mengatasi Data Duplikat

Perlu diingat bahwa C010 tercatat dua kali pada customer_id. Sebelum menghapus salah satunya, kedua baris diperiksa terlebih dahulu untuk memastikan apakah isinya identik.

pelanggan[duplicated(pelanggan$customer_id) |
          duplicated(pelanggan$customer_id, fromLast = TRUE), ]
##    customer_id nama usia pendapatan  kota      status
## 10        C010 Joko   35    5300000 Dumai Tidak Aktif
## 11        C010 Joko   35    5300000 Dumai Tidak Aktif

Karena kedua baris terbukti identik, kemunculan kedua dapat dihapus menggunakan duplicated(), kemudian nomor baris disusun kembali.

pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL

dim(pelanggan)
## [1] 11  6

Koreksi Nilai Berdasarkan Domain

Selain duplikasi, nilai yang melanggar aturan logis atau domain juga perlu diperiksa. Sebagai contoh, usia manusia secara umum dapat diperiksa pada rentang 15–100 tahun.

pelanggan[pelanggan$usia < 15 | pelanggan$usia > 100, ]
##    customer_id nama usia pendapatan  kota status
## 4         C004 Dodi  150    4800000 Dumai  Aktif
## NA        <NA> <NA>   NA         NA  <NA>   <NA>

Pemeriksaan domain juga diterapkan pada pendapatan, karena nilai negatif tidak sesuai dengan konteks bisnis pada data ini.

pelanggan[pelanggan$pendapatan < 0, ]
##      customer_id nama usia pendapatan kota status
## NA          <NA> <NA>   NA         NA <NA>   <NA>
## NA.1        <NA> <NA>   NA         NA <NA>   <NA>

Berdasarkan hasil investigasi, usia 150 tahun merupakan kesalahan input dan nilai yang benar adalah 50 tahun. Oleh karena itu, baris dengan customer_id “C004” dikoreksi.

pelanggan$usia[pelanggan$customer_id == "C004"] <- 50

Membuat Log Perubahan Data

Setiap keputusan dalam proses cleaning sebaiknya dicatat, tidak hanya tersimpan di dalam kode. Log perubahan berfungsi sebagai dokumentasi, transparansi, dan jejak audit.

log_perubahan <- data.frame(
  tahap = c("Standardisasi", "Standardisasi", "Deduplikasi", "Koreksi domain"),
  atribut = c("kota", "status", "customer_id", "usia"),
  tindakan = c(
    "PKU dan variasi kapital menjadi Pekanbaru",
    "ACTIVE/A menjadi Aktif; nonaktif menjadi Tidak Aktif",
    "Menghapus kemunculan kedua C010",
    "Usia C004 dikoreksi dari 150 menjadi 50 berdasarkan sumber asli"
  ),
  stringsAsFactors = FALSE
)

log_perubahan
##            tahap     atribut
## 1  Standardisasi        kota
## 2  Standardisasi      status
## 3    Deduplikasi customer_id
## 4 Koreksi domain        usia
##                                                          tindakan
## 1                       PKU dan variasi kapital menjadi Pekanbaru
## 2            ACTIVE/A menjadi Aktif; nonaktif menjadi Tidak Aktif
## 3                                 Menghapus kemunculan kedua C010
## 4 Usia C004 dikoreksi dari 150 menjadi 50 berdasarkan sumber asli

Bagian III — Penanganan Missing Values

Mengidentifikasi Missing Value

Setelah kategori dan duplikasi dibersihkan, tahap berikutnya adalah menangani nilai yang hilang. Langkah awalnya adalah mengetahui kolom mana saja yang mengandung NA.

colSums(is.na(pelanggan))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           1           2           1           0

Mengetahui kolom yang memiliki NA saja belum memadai; baris yang terdampak juga perlu diperiksa secara langsung menggunakan complete.cases().

pelanggan[!complete.cases(pelanggan), ]
##    customer_id nama usia pendapatan      kota      status
## 2         C002 Budi   25         NA Pekanbaru       Aktif
## 6         C006 Fani   NA    5100000     Dumai Tidak Aktif
## 11        C011 Kiki   28         NA      <NA>       Aktif

Penghapusan Baris

Alternatif paling sederhana adalah menghapus baris yang memiliki nilai hilang (listwise deletion). Pada praktikum ini langkah tersebut hanya sebagai simulasi; objek pelanggan tetap dipertahankan dan hasilnya disimpan sebagai pelanggan_complete.

pelanggan_complete <- pelanggan[complete.cases(pelanggan), ]
nrow(pelanggan)
## [1] 11
nrow(pelanggan_complete)
## [1] 8

Persentase data yang hilang jika strategi tersebut diterapkan dapat dihitung sebagai berikut.

round((1 - nrow(pelanggan_complete) / nrow(pelanggan)) * 100, 2)
## [1] 27.27

Nilai persentase tersebut menunjukkan konsekuensi penghapusan baris; semakin besar angkanya, semakin besar pula risiko kehilangan informasi yang masih dapat dimanfaatkan.

Imputasi Mean dan Median

Pilihan lain yang dapat mempertahankan lebih banyak data adalah imputasi, yaitu mengganti nilai yang hilang dengan suatu estimasi. Dua pendekatan umum yang dibandingkan adalah mean dan median.

Karena variabel pendapatan mengandung nilai ekstrem (outlier), mean dan median perlu dibandingkan terlebih dahulu untuk memilih nilai pengganti yang paling representatif.

mean_pendapatan <- mean(pelanggan$pendapatan, na.rm = TRUE)
median_pendapatan <- median(pelanggan$pendapatan, na.rm = TRUE)

mean_pendapatan
## [1] 59900000
median_pendapatan
## [1] 4900000

Perbedaan keduanya cukup besar, yang menunjukkan adanya pengaruh outlier terhadap mean. Oleh sebab itu, median dipilih karena relatif lebih tahan terhadap nilai ekstrem.

pelanggan$pendapatan_imputasi <- pelanggan$pendapatan
pelanggan$pendapatan_imputasi[is.na(pelanggan$pendapatan_imputasi)] <-
  median_pendapatan

Metode yang sama juga diterapkan pada kolom usia agar pendekatan imputasinya tetap konsisten.

median_usia <- median(pelanggan$usia, na.rm = TRUE)
pelanggan$usia_imputasi <- pelanggan$usia
pelanggan$usia_imputasi[is.na(pelanggan$usia_imputasi)] <- median_usia

Nilai sebelum dan sesudah imputasi dapat dibandingkan secara berdampingan melalui tampilan berikut.

pelanggan[, c("customer_id", "usia", "usia_imputasi",
              "pendapatan", "pendapatan_imputasi")]
##    customer_id usia usia_imputasi pendapatan pendapatan_imputasi
## 1         C001   21          21.0    4.5e+06             4.5e+06
## 2         C002   25          25.0         NA             4.9e+06
## 3         C003   23          23.0    5.2e+06             5.2e+06
## 4         C004   50          50.0    4.8e+06             4.8e+06
## 5         C005   27          27.0    4.9e+06             4.9e+06
## 6         C006   NA          27.5    5.1e+06             5.1e+06
## 7         C007   31          31.0    5.0e+08             5.0e+08
## 8         C008   29          29.0    4.7e+06             4.7e+06
## 9         C009   22          22.0    4.6e+06             4.6e+06
## 10        C010   35          35.0    5.3e+06             5.3e+06
## 11        C011   28          28.0         NA             4.9e+06

Imputasi Nilai Kategorik

Variabel kategorik seperti kota tidak selalu tepat jika langsung diisi dengan modus karena dapat menghasilkan asumsi lokasi yang belum tentu benar. Karena itu, nilai yang hilang diberi label eksplisit “Tidak diketahui” agar status missing tetap dapat ditelusuri.

pelanggan$kota_imputasi <- pelanggan$kota
pelanggan$kota_imputasi[is.na(pelanggan$kota_imputasi)] <- "Tidak diketahui"

table(pelanggan$kota_imputasi, useNA = "ifany")
## 
##           Dumai       Pekanbaru            Siak Tidak diketahui 
##               3               6               1               1

Indikator Missing

Imputasi dapat menghilangkan jejak bahwa suatu nilai sebelumnya merupakan data hilang. Untuk mempertahankan informasi tersebut, dibuat sebuah indikator biner.

pelanggan$pendapatan_missing <- as.integer(is.na(pelanggan$pendapatan))
table(pelanggan$pendapatan_missing)
## 
## 0 1 
## 9 2

Membandingkan Distribusi Sebelum dan Sesudah Imputasi

Nilai numerik saja belum tentu cukup untuk melihat dampak imputasi. Histogram yang ditampilkan berdampingan dapat membantu mengevaluasi perubahan bentuk distribusi setelah pengisian missing value.

par(mfrow = c(1, 2))
hist(pelanggan$pendapatan,
     main = "Sebelum Imputasi",
     xlab = "Pendapatan", col = "skyblue", breaks = 8)
hist(pelanggan$pendapatan_imputasi,
     main = "Sesudah Imputasi Median",
     xlab = "Pendapatan", col = "lightgreen", breaks = 8)

par(mfrow = c(1, 1))

Histogram menunjukkan bahwa imputasi membuat sebagian nilai terkonsentrasi di sekitar median sehingga variasi data sedikit berkurang. Karena itu, evaluasi tetap perlu dilakukan meskipun seluruh NA sudah terisi.

Bagian IV — Penanganan Outlier

Visualisasi dengan Boxplot

Sebelum menentukan batas outlier secara matematis, boxplot dapat digunakan untuk memperoleh gambaran visual mengenai nilai ekstrem pada pendapatan.

boxplot(pelanggan$pendapatan_imputasi,
        horizontal = TRUE,
        col = "lightblue",
        main = "Boxplot Pendapatan",
        xlab = "Pendapatan")

Metode IQR

Untuk menentukan batas outlier secara lebih objektif, metode IQR (Interquartile Range) digunakan dalam menghitung batas bawah dan batas atas.

q1 <- quantile(pelanggan$pendapatan_imputasi, 0.25)
q3 <- quantile(pelanggan$pendapatan_imputasi, 0.75)
iqr <- IQR(pelanggan$pendapatan_imputasi)

batas_bawah <- q1 - 1.5 * iqr
batas_atas <- q3 + 1.5 * iqr

c(Q1 = q1, Q3 = q3, IQR = iqr,
  batas_bawah = batas_bawah,
  batas_atas = batas_atas)
##          Q1.25%          Q3.75%             IQR batas_bawah.25%  batas_atas.75% 
##         4750000         5150000          400000         4150000         5750000

Menandai Outlier

Setiap nilai yang berada di luar rentang [batas_bawah, batas_atas] ditandai sebagai kandidat outlier melalui sebuah kolom logika.

pelanggan$outlier_pendapatan <-
  pelanggan$pendapatan_imputasi < batas_bawah |
  pelanggan$pendapatan_imputasi > batas_atas

pelanggan[pelanggan$outlier_pendapatan,
          c("customer_id", "nama", "pendapatan_imputasi")]
##   customer_id   nama pendapatan_imputasi
## 7        C007 Gilang               5e+08

Winsorizing

Keberadaan outlier tidak selalu berarti data harus dihapus. Setidaknya ada tiga kemungkinan penyebab yang perlu dipertimbangkan terlebih dahulu:

  • Kesalahan input — koreksi jika nilai yang benar dapat diverifikasi.
  • Observasi valid tetapi ekstrem — dipertahankan, atau ditangani dengan metode yang lebih robust.
  • Berasal dari populasi berbeda — misalnya pelanggan korporasi tercampur dengan pelanggan ritel, sehingga perlu dipisah sebagai segmen tersendiri.

Pada kasus ini outlier tetap dipertahankan. Namun, untuk melihat pengaruh metode lain, teknik winsorizing digunakan sebagai perbandingan dengan cara membatasi nilai ekstrem tanpa menghapus observasinya.

pelanggan$pendapatan_winsor <- pmin(
  pmax(pelanggan$pendapatan_imputasi, batas_bawah),
  batas_atas
)

pelanggan[pelanggan$outlier_pendapatan,
          c("customer_id", "pendapatan_imputasi", "pendapatan_winsor")]
##   customer_id pendapatan_imputasi pendapatan_winsor
## 7        C007               5e+08           5750000

Perlu diperhatikan bahwa winsorizing pada bagian ini hanya merupakan demonstrasi tambahan. Metode tersebut sebaiknya digunakan sesuai tujuan analisis dan selalu didokumentasikan agar nilai yang telah dibatasi tidak dianggap sebagai nilai asli.

Bagian V — Transformasi Data

Normalisasi Min–Max

Metode pertama untuk mentransformasi variabel numerik adalah normalisasi min–maks, yang mengubah nilai ke dalam rentang \([0,1]\) dengan rumus:

\[ x' = \frac{x-\min(x)}{\max(x)-\min(x)}. \]

minmax <- function(x) {
  if (all(is.na(x))) return(rep(NA_real_, length(x)))
  rentang <- max(x, na.rm = TRUE) - min(x, na.rm = TRUE)
  if (rentang == 0) return(rep(0, length(x)))
  (x - min(x, na.rm = TRUE)) / rentang
}

pelanggan$usia_minmax <- minmax(pelanggan$usia_imputasi)
pelanggan$pendapatan_minmax <- minmax(pelanggan$pendapatan_imputasi)

pelanggan[, c("customer_id", "usia", "usia_minmax",
              "pendapatan_imputasi", "pendapatan_minmax")]
##    customer_id usia usia_minmax pendapatan_imputasi pendapatan_minmax
## 1         C001   21  0.00000000             4.5e+06      0.0000000000
## 2         C002   25  0.13793103             4.9e+06      0.0008072654
## 3         C003   23  0.06896552             5.2e+06      0.0014127144
## 4         C004   50  1.00000000             4.8e+06      0.0006054490
## 5         C005   27  0.20689655             4.9e+06      0.0008072654
## 6         C006   NA  0.22413793             5.1e+06      0.0012108981
## 7         C007   31  0.34482759             5.0e+08      1.0000000000
## 8         C008   29  0.27586207             4.7e+06      0.0004036327
## 9         C009   22  0.03448276             4.6e+06      0.0002018163
## 10        C010   35  0.48275862             5.3e+06      0.0016145308
## 11        C011   28  0.24137931             4.9e+06      0.0008072654

Standarisasi Z-Score

Jika min–maks menghasilkan rentang [0,1], z-score menunjukkan posisi suatu nilai terhadap rata-rata dalam satuan standar deviasi:

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

pelanggan$usia_z <- as.numeric(scale(pelanggan$usia_imputasi))
pelanggan$pendapatan_z <- as.numeric(scale(pelanggan$pendapatan_imputasi))

round(pelanggan[, c("usia_z", "pendapatan_z")], 3)
##    usia_z pendapatan_z
## 1  -0.984       -0.304
## 2  -0.489       -0.301
## 3  -0.737       -0.299
## 4   2.604       -0.302
## 5  -0.242       -0.301
## 6  -0.180       -0.300
## 7   0.253        3.015
## 8   0.006       -0.303
## 9  -0.860       -0.303
## 10  0.748       -0.299
## 11 -0.118       -0.301

Decimal Scaling

Metode ketiga adalah decimal scaling. Prinsipnya cukup sederhana, yaitu membagi nilai dengan pangkat 10 tertentu hingga hasilnya berada dalam rentang [-1, 1].

decimal_scale <- function(x) {
  maks <- max(abs(x), na.rm = TRUE)
  if (maks == 0) return(x)
  j <- ceiling(log10(maks + 1))
  x / (10 ^ j)
}

pelanggan$pendapatan_decimal <-
  decimal_scale(pelanggan$pendapatan_imputasi)

range(pelanggan$pendapatan_decimal, na.rm = TRUE)
## [1] 0.0045 0.5000

Membandingkan Metode Transformasi

Ketiga metode tersebut menghasilkan skala numerik yang berbeda meskipun diterapkan pada variabel yang sama. Menampilkannya berdampingan memudahkan perbandingan hasil transformasi.

transformasi <- pelanggan[, c(
  "customer_id", "pendapatan_imputasi", "pendapatan_minmax",
  "pendapatan_z", "pendapatan_decimal"
)]

transformasi
##    customer_id pendapatan_imputasi pendapatan_minmax pendapatan_z
## 1         C001             4.5e+06      0.0000000000   -0.3041235
## 2         C002             4.9e+06      0.0008072654   -0.3014440
## 3         C003             5.2e+06      0.0014127144   -0.2994343
## 4         C004             4.8e+06      0.0006054490   -0.3021138
## 5         C005             4.9e+06      0.0008072654   -0.3014440
## 6         C006             5.1e+06      0.0012108981   -0.3001042
## 7         C007             5.0e+08      1.0000000000    3.0151095
## 8         C008             4.7e+06      0.0004036327   -0.3027837
## 9         C009             4.6e+06      0.0002018163   -0.3034536
## 10        C010             5.3e+06      0.0016145308   -0.2987645
## 11        C011             4.9e+06      0.0008072654   -0.3014440
##    pendapatan_decimal
## 1              0.0045
## 2              0.0049
## 3              0.0052
## 4              0.0048
## 5              0.0049
## 6              0.0051
## 7              0.5000
## 8              0.0047
## 9              0.0046
## 10             0.0053
## 11             0.0049

Dampak Outlier Terhadap Normalisasi

pelanggan$pendapatan_winsor_minmax <- minmax(pelanggan$pendapatan_winsor)

plot(pelanggan$pendapatan_minmax,
     pelanggan$pendapatan_winsor_minmax,
     pch = 19, col = "navy",
     xlab = "Min–maks Data Asli",
     ylab = "Min–maks Data Winsorized",
     main = "Dampak Penanganan Outlier terhadap Normalisasi")
abline(0, 1, col = "red", lty = 2)

Titik yang jauh dari garis diagonal merah memperlihatkan pengaruh outlier dalam membuat sebagian besar nilai min–maks berada pada rentang yang sempit. Karena itu, pemeriksaan outlier sebaiknya dilakukan sebelum menentukan metode transformasi.

Bagian VI — Integrasi Data

Pemeriksaan Kunci

Sebelum kedua tabel digabungkan, kolom kunci penghubung, yaitu customer_id pada data pelanggan dan cust_id pada data transaksi, perlu diperiksa agar tidak mengandung duplikasi dan dapat dipasangkan dengan benar.

sum(duplicated(pelanggan$customer_id))
## [1] 0
sum(duplicated(transaksi_raw$cust_id))
## [1] 0

Selanjutnya, perlu diketahui pelanggan mana yang belum memiliki data transaksi.

setdiff(pelanggan$customer_id, transaksi_raw$cust_id)
## [1] "C011"

Sebaliknya, perlu diperiksa apakah terdapat transaksi yang berasal dari pelanggan yang tidak tercatat pada tabel pelanggan.

setdiff(transaksi_raw$cust_id, pelanggan$customer_id)
## [1] "C012"

Kedua hasil setdiff() tersebut menunjukkan bahwa relasi kedua tabel tidak sepenuhnya 1-ke-1: terdapat pelanggan tanpa transaksi dan terdapat transaksi yang tidak memiliki pasangan pelanggan yang valid.

Menyamakan Nama Identifier

Agar merge() dapat menggunakan kunci yang sama, kolom cust_id pada tabel transaksi terlebih dahulu diubah namanya menjadi customer_id, mengikuti tabel pelanggan.

transaksi <- transaksi_raw
names(transaksi)[names(transaksi) == "cust_id"] <- "customer_id"

Melakukan Penggabungan Dataset ()

Setelah nama kolom kunci diseragamkan, kedua dataset dapat digabungkan menggunakan merge().

data_terintegrasi <- merge(
  pelanggan,
  transaksi,
  by = "customer_id",
  all.x = TRUE
)

data_terintegrasi[, c(
  "customer_id", "nama", "jumlah_transaksi", "total_purchase"
)]
##    customer_id   nama jumlah_transaksi total_purchase
## 1         C001    Ani                5        1.5e+06
## 2         C002   Budi                3        9.0e+05
## 3         C003  Citra                7        2.7e+06
## 4         C004   Dodi                2        6.0e+05
## 5         C005    Eka                6        2.1e+06
## 6         C006   Fani                4        1.3e+06
## 7         C007 Gilang               20        2.5e+07
## 8         C008   Hana                5        1.7e+06
## 9         C009  Indra                3        8.0e+05
## 10        C010   Joko                8        3.2e+06
## 11        C011   Kiki               NA             NA

Validasi Hasil Integrasi

Hasil join perlu divalidasi dan tidak langsung dianggap benar. Beberapa pemeriksaan berikut digunakan untuk memastikan proses penggabungan berjalan sesuai harapan.

Apakah jumlah baris mengalami perubahan setelah proses penggabungan?

c(sebelum = nrow(pelanggan), sesudah = nrow(data_terintegrasi))
## sebelum sesudah 
##      11      11

Apakah customer_id masih unik setelah penggabungan, atau muncul duplikasi baru akibat relasi many-to-many?

sum(duplicated(data_terintegrasi$customer_id))
## [1] 0

Apakah terdapat missing value baru akibat pelanggan yang tidak memiliki pasangan transaksi?

colSums(is.na(data_terintegrasi[, c("jumlah_transaksi", "total_purchase")]))
## jumlah_transaksi   total_purchase 
##                1                1

Pelanggan mana saja yang tidak mempunyai pasangan transaksi tersebut?

data_terintegrasi[is.na(data_terintegrasi$jumlah_transaksi),
                  c("customer_id", "nama")]
##    customer_id nama
## 11        C011 Kiki

Menangani Missing setelah Integrasi

Hal ini perlu diperhatikan karena NA pada kolom transaksi setelah join dapat memiliki dua arti yang berbeda.

NA hanya boleh diubah menjadi 0 jika telah dipastikan bahwa artinya memang tidak ada transaksi. Tanpa verifikasi, penggantian tersebut dapat menutupi masalah pada proses pencocokan data.

# Contoh jika telah dikonfirmasi bahwa NA berarti belum pernah bertransaksi
data_terintegrasi$jumlah_transaksi_final <- data_terintegrasi$jumlah_transaksi
data_terintegrasi$total_purchase_final <- data_terintegrasi$total_purchase

data_terintegrasi$jumlah_transaksi_final[
  is.na(data_terintegrasi$jumlah_transaksi_final)
] <- 0

data_terintegrasi$total_purchase_final[
  is.na(data_terintegrasi$total_purchase_final)
] <- 0

Dataset Akhir

Dari seluruh variabel yang terbentuk selama preprocessing, termasuk variabel perantara seperti usia, kota, dan pendapatan mentah, hanya variabel hasil yang diperlukan yang dipertahankan dalam dataset final dengan nama yang lebih sederhana.

data_final <- data_terintegrasi[, c(
  "customer_id", "nama", "usia_imputasi", "kota_imputasi", "status",
  "pendapatan_imputasi", "pendapatan_missing", "outlier_pendapatan",
  "usia_minmax", "pendapatan_minmax", "jumlah_transaksi_final",
  "total_purchase_final"
)]

names(data_final)[names(data_final) == "kota_imputasi"] <- "kota"
names(data_final)[names(data_final) == "usia_imputasi"] <- "usia"

data_final
##    customer_id   nama usia            kota      status pendapatan_imputasi
## 1         C001    Ani 21.0       Pekanbaru       Aktif             4.5e+06
## 2         C002   Budi 25.0       Pekanbaru       Aktif             4.9e+06
## 3         C003  Citra 23.0       Pekanbaru       Aktif             5.2e+06
## 4         C004   Dodi 50.0           Dumai       Aktif             4.8e+06
## 5         C005    Eka 27.0       Pekanbaru Tidak Aktif             4.9e+06
## 6         C006   Fani 27.5           Dumai Tidak Aktif             5.1e+06
## 7         C007 Gilang 31.0       Pekanbaru       Aktif             5.0e+08
## 8         C008   Hana 29.0            Siak       Aktif             4.7e+06
## 9         C009  Indra 22.0       Pekanbaru       Aktif             4.6e+06
## 10        C010   Joko 35.0           Dumai Tidak Aktif             5.3e+06
## 11        C011   Kiki 28.0 Tidak diketahui       Aktif             4.9e+06
##    pendapatan_missing outlier_pendapatan usia_minmax pendapatan_minmax
## 1                   0              FALSE  0.00000000      0.0000000000
## 2                   1              FALSE  0.13793103      0.0008072654
## 3                   0              FALSE  0.06896552      0.0014127144
## 4                   0              FALSE  1.00000000      0.0006054490
## 5                   0              FALSE  0.20689655      0.0008072654
## 6                   0              FALSE  0.22413793      0.0012108981
## 7                   0               TRUE  0.34482759      1.0000000000
## 8                   0              FALSE  0.27586207      0.0004036327
## 9                   0              FALSE  0.03448276      0.0002018163
## 10                  0              FALSE  0.48275862      0.0016145308
## 11                  1              FALSE  0.24137931      0.0008072654
##    jumlah_transaksi_final total_purchase_final
## 1                       5              1.5e+06
## 2                       3              9.0e+05
## 3                       7              2.7e+06
## 4                       2              6.0e+05
## 5                       6              2.1e+06
## 6                       4              1.3e+06
## 7                      20              2.5e+07
## 8                       5              1.7e+06
## 9                       3              8.0e+05
## 10                      8              3.2e+06
## 11                      0              0.0e+00

Audit Akhir

Seperti pemeriksaan awal, dataset yang telah melalui seluruh tahapan preprocessing perlu diaudit kembali untuk memastikan missing value dan duplikasi telah ditangani.

Fungsi audit_data() yang telah dibuat pada Bagian I digunakan kembali pada tahap ini:

audit_akhir <- audit_data(data_final)
audit_akhir
##                   atribut      tipe jumlah_missing persen_missing jumlah_unik
## 1             customer_id character              0              0          11
## 2                    nama character              0              0          11
## 3                    usia   numeric              0              0          11
## 4                    kota character              0              0           4
## 5                  status character              0              0           2
## 6     pendapatan_imputasi   numeric              0              0           9
## 7      pendapatan_missing   integer              0              0           2
## 8      outlier_pendapatan   logical              0              0           2
## 9             usia_minmax   numeric              0              0          11
## 10      pendapatan_minmax   numeric              0              0           9
## 11 jumlah_transaksi_final   numeric              0              0           9
## 12   total_purchase_final   numeric              0              0          11

Selanjutnya dilakukan pemeriksaan duplikasi pada customer_id:

sum(duplicated(data_final$customer_id))
## [1] 0

Terakhir, jumlah nilai hilang pada setiap kolom dirangkum sebagai berikut:

colSums(is.na(data_final))
##            customer_id                   nama                   usia 
##                      0                      0                      0 
##                   kota                 status    pendapatan_imputasi 
##                      0                      0                      0 
##     pendapatan_missing     outlier_pendapatan            usia_minmax 
##                      0                      0                      0 
##      pendapatan_minmax jumlah_transaksi_final   total_purchase_final 
##                      0                      0                      0

Perbandingan Kondisi Sebelum dan Sesudah

Untuk mengevaluasi dampak preprocessing secara keseluruhan, beberapa indikator utama dari data mentah dan data akhir dibandingkan dalam satu tabel ringkas.

perbandingan <- data.frame(
  indikator = c(
    "Jumlah baris",
    "Duplikasi customer_id",
    "Total missing value",
    "Kategori kota unik",
    "Kategori status unik"
  ),
  sebelum = c(
    nrow(pelanggan_raw),
    sum(duplicated(pelanggan_raw$customer_id)),
    sum(is.na(pelanggan_raw)),
    length(unique(pelanggan_raw$kota)),
    length(unique(pelanggan_raw$status))
  ),
  sesudah = c(
    nrow(data_final),
    sum(duplicated(data_final$customer_id)),
    sum(is.na(data_final)),
    length(unique(data_final$kota)),
    length(unique(data_final$status))
  )
)

perbandingan
##               indikator sebelum sesudah
## 1          Jumlah baris      12      11
## 2 Duplikasi customer_id       1       0
## 3   Total missing value       4       0
## 4    Kategori kota unik      10       4
## 5  Kategori status unik       8       2

Menyimpan Hasil

Sebagai tahap terakhir, dataset final dan log seluruh perubahan disimpan dalam format CSV sehingga dapat digunakan kembali untuk analisis berikutnya tanpa mengulang preprocessing dari awal.

write.csv(data_final,
          "data_pelanggan_setelah_preprocessing.csv",
          row.names = FALSE)

write.csv(log_perubahan,
          "log_perubahan_preprocessing.csv",
          row.names = FALSE)

Ringkasan

Secara umum, tahapan preprocessing yang dilakukan dalam praktikum ini dapat dirangkum sebagai berikut:

  1. Mengenali Struktur Data dan menilai kualitas awalnya;
  2. Membuat salinan kerja, merapikan spasi dan kapitalisasi, menyeragamkan kategori, mendeteksi serta menghapus duplikasi, dan memeriksa pelanggaran aturan domain;
  3. Menangani nilai hilang (missing values) dengan strategi yang sesuai untuk masing-masing tipe data;
  4. Mendeteksi dan menangani outlier;
  5. Melakukan transformasi data melalui normalisasi dan scaling; dan
  6. Menggabungkan (mengintegrasikan) data dari dua sumber serta memvalidasi hasil penggabungannya.

Kualitas hasil preprocessing pada akhirnya ditentukan oleh kemampuan analis memahami konteks data dan tujuan analisis. R berperan sebagai alat untuk menjalankan keputusan tersebut, bukan sebagai penentu keputusan.

Daftar Pustaka

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