1 Pendahuluan

Halo temen-temen, jadi dikesempatan kali ini aku mau berbagi pengalaman mempraktikkan data preprocessing memakai R dan RStudio.

Nah ceritanya begini: aku berperan sebagai analis data di sebuah perusahaan e-commerce fiktif dan aku punya dua sumber data:

  1. Data pelanggan: Berisi identitas, usia, kota, pendapatan, dan status keanggotaan.
  2. Data transaksi: Berisi jumlah transaksi dan total pembelian tiap pelanggan.

Masalahnya, kedua data ini masih “kotor”. Ada nilai yang hilang, penulisan kategori yang tidak konsisten, data yang terduplikasi, angka yang kelihatan aneh (outlier), sampai nama kolom identifier yang berbeda antara satu tabel dengan tabel lainnya. Semua itu harus aku beresin dulu sebelum datanya layak dianalisis.

Okeh langsung, ikuti langkah-langkahnya bareng yah.

2 Menyiapkan RStudio

Sebelum mulai ngoding, aku menyiapkan dulu proyeknya di RStudio:

  1. Buka RStudio.
  2. Klik File → New Project → New Directory → New Project.
  3. Terus ku kasih nama proyeknya, misalnya praktikum_preprocessing.
  4. File R Markdown ini aku simpan di dalam folder proyek tadi.
  5. Kalau semua kode sudah aku tulis, aku tinggal klik tombol Knit untuk menghasilkan laporan HTML dan file HTML inilah yang nantinya aku unggah ke RPubs.
getwd()
## [1] "D:/SEMESTER 5/DATA MINING"
R.version.string
## [1] "R version 4.4.2 (2024-10-31 ucrt)"

3 Menyiapkan Data yang Sengaja “Kotor”

Karena ini latihan, aku sengaja membuat datanya sendiri dan membuatnya kotor — supaya aku bisa mempraktikkan semua teknik pembersihan data dari awal sampai akhir.

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
)

pelanggan_raw
transaksi_raw

Coba perhatikan baik-baik data di atas. Aku sengaja menyelipkan beberapa “jebakan” supaya temen-temen bisa lihat sendiri kenapa data ini belum siap dianalisis:

  • Ada usia 150 tahun yang jelas tidak masuk akal.
  • Ada pendapatan 500000000 yang jauh di atas pendapatan pelanggan lain.
  • Nama kota ditulis dengan variasi yang berbeda-beda: "PKU", " PKU", "pekanbaru", "PEKANBARU", "Pekanbaru ".
  • Status keanggotaan juga bervariasi: "Aktif", "aktif", "ACTIVE", "A".
  • Ada baris yang terduplikasi persis (C010 muncul dua kali).
  • Ada beberapa nilai yang hilang (NA).
  • ID pelanggan C011 tidak punya transaksi, dan C012 di data transaksi tidak punya data pelanggan.

4 Bagian I: Memahami Kondisi Data Sebelum Diapa-apakan

4.1 Melihat Struktur Data

Sebelum aku ubah apa pun, aku selalu memeriksa dulu bentuk datanya: berapa baris, berapa kolom, tipe datanya apa saja, dan seperti apa isinya.

dim(pelanggan_raw)
## [1] 12  6
names(pelanggan_raw)
## [1] "customer_id" "nama"        "usia"        "pendapatan"  "kota"       
## [6] "status"
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" ...
head(pelanggan_raw)
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  
##                                       
##                                       
##                                       
## 

4.2 Mengukur Kualitas Data di Awal

Menurut Han, Kamber, dan Pei (2012) kalau kualitas data itu bisa dilihat dari enam dimensi: accuracy, completeness, consistency, timeliness, believability, dan interpretability. Tapi tidak semuanya bisa aku ukur langsung dari tabel — misalnya accuracy butuh pembanding dengan kondisi aslinya, dan timeliness butuh informasi kapan data terakhir diperbarui.

Yang bisa aku cek langsung dari tabel ini adalah kelengkapan data, konsistensi kategori, dan duplikasi. Ini yang aku lakukan:

# Berapa banyak nilai hilang di tiap kolom?
colSums(is.na(pelanggan_raw))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           1           2           1           0
# Berapa persen nilai yang hilang di tiap kolom?
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
# Ada berapa baris yang terduplikasi penuh?
sum(duplicated(pelanggan_raw))
## [1] 1
# Ada berapa customer_id yang terduplikasi?
sum(duplicated(pelanggan_raw$customer_id))
## [1] 1
# Kategori apa saja yang ada di kolom kota?
sort(unique(pelanggan_raw$kota))
## [1] " PKU"       "Dumai"      "DUMAI"      "pekanbaru"  "Pekanbaru" 
## [6] "PEKANBARU"  "Pekanbaru " "PKU"        "Siak"
# Kategori apa saja yang ada di kolom status?
sort(unique(pelanggan_raw$status))
## [1] "A"           "ACTIVE"      "aktif"       "Aktif"       "AKTIF"      
## [6] "nonaktif"    "Tidak aktif" "Tidak Aktif"
# Rentang nilai usia dan pendapatan
range(pelanggan_raw$usia, na.rm = TRUE)
## [1]  21 150
range(pelanggan_raw$pendapatan, na.rm = TRUE)
## [1] 4.5e+06 5.0e+08

Dari sini aku sudah bisa menebak beberapa hal: ada nilai hilang di kolom usia, pendapatan, dan kota; kategori kota dan status berantakan; dan ada satu baris duplikat.

4.3 Membuat Fungsi Ringkasan Kualitas Sendiri

Karena aku akan sering mengecek kualitas data, baik sebelum maupun sesudah dibersihkan, aku membuat satu fungsi khusus supaya tidak perlu menulis ulang kode yang sama:

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

Penting buat aku catat di sini: hasil audit ini bukan keputusan pembersihan data. Audit hanya memberi tahu aku kolom mana saja yang perlu aku perhatikan lebih lanjut, keputusan tetap aku ambil sendiri setelah memahami konteksnya.

5 Bagian II: Membersihkan Data

5.1 Membuat Salinan Kerja

Ini kebiasaan yang selalu aku terapkan: jangan pernah mengubah data mentah secara langsung. Aku selalu membuat salinannya dulu, supaya kalau ada yang salah, aku masih punya data aslinya untuk dibandingkan.

pelanggan <- pelanggan_raw

5.2 Merapikan Spasi dan Huruf Kapital

Langkah pertama yang aku lakukan adalah hal paling sederhana: menghapus spasi berlebih di awal/akhir teks, lalu menyeragamkan semuanya jadi huruf kecil supaya lebih mudah dicocokkan.

# Menghapus spasi di awal dan akhir
pelanggan$kota <- trimws(pelanggan$kota)
pelanggan$status <- trimws(pelanggan$status)

# Menyeragamkan huruf menjadi huruf kecil untuk proses pencocokan
pelanggan$kota <- tolower(pelanggan$kota)
pelanggan$status <- tolower(pelanggan$status)

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

Lihat, kategorinya jadi jauh lebih sedikit variasinya sekarang.

5.3 Menyamakan Penulisan Kategori

Langkah berikutnya, aku menyatukan kategori-kategori yang sebenarnya merujuk pada hal yang sama. Aturan yang aku pakai di sini aku tentukan sendiri berdasarkan pemahaman terhadap datanya dalam pekerjaan nyata, aturan semacam ini sebaiknya aku konfirmasikan dulu ke pemilik data.

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

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

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

Sekarang kolom kota hanya punya 3 kategori dan status hanya punya 2 kategori jauh lebih rapi dibanding sebelumnya.

5.4 Mencari dan Menghapus Data yang Terduplikasi

Ingat tadi aku sempat lihat ada customer_id yang muncul dua kali? Aku cek dulu baris mana saja yang terlibat sebelum aku hapus:

pelanggan[duplicated(pelanggan$customer_id) |
          duplicated(pelanggan$customer_id, fromLast = TRUE), ]

Karena baris C010 benar-benar identik di semua kolomnya, aku putuskan untuk menyimpan kemunculan pertamanya saja dan membuang duplikatnya:

pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL
dim(pelanggan)
## [1] 11  6

Aku selalu ingat satu catatan penting di sini: menghapus duplikasi hanya aman kalau setiap customer_id memang seharusnya cuma mewakili satu pelanggan. Kalau ternyata satu pelanggan boleh punya banyak baris (misalnya untuk mencatat banyak transaksi), maka menghapus baris seperti ini justru salah langkah.

5.5 Memeriksa Aturan Bisnis (Domain Rules)

Selanjutnya aku memeriksa apakah ada nilai yang melanggar aturan bisnis yang wajar. Misalnya, aku tetapkan usia pelanggan yang masuk akal itu antara 15–100 tahun, dan pendapatan tidak boleh negatif.

# Usia yang di luar rentang wajar (15–100 tahun)
pelanggan[pelanggan$usia < 15 | pelanggan$usia > 100, ]
# Pendapatan yang bernilai negatif
pelanggan[pelanggan$pendapatan < 0, ]

Ternyata ada satu pelanggan dengan usia 150 tahun jelas melanggar aturan. Di kondisi nyata, aku akan mengecek ulang ke formulir aslinya. Untuk latihan ini, anggap saja setelah aku cek, nilai yang benar adalah 50 tahun, jadi aku koreksi:

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

5.6 Mencatat Semua Perubahan yang Sudah Aku Lakukan

Ini kebiasaan yang menurutku sangat penting tapi sering dilewatkan orang: mencatat log perubahan. Dengan catatan ini, siapa pun yang membaca laporan ini (termasuk aku sendiri di kemudian hari) bisa tahu persis apa saja yang sudah diubah, dan kenapa.

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

6 Bagian III: Menangani Nilai yang Hilang (Missing Values)

6.1 Mencari Tahu di Mana Saja Nilai yang Hilang

colSums(is.na(pelanggan))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           1           2           1           0
# Baris mana saja yang punya setidaknya satu nilai hilang?
pelanggan[!complete.cases(pelanggan), ]

Aku punya tiga pilihan strategi untuk menangani nilai hilang ini, dan aku akan coba semuanya satu per satu supaya temen-temen bisa lihat perbandingannya.

6.2 Strategi 1: Menghapus Baris yang Tidak Lengkap

Ini strategi paling sederhana, tapi aku hanya mendemonstrasikannya di sini aku tidak langsung menerapkannya ke data utamaku.

pelanggan_complete <- pelanggan[complete.cases(pelanggan), ]

nrow(pelanggan)
## [1] 11
nrow(pelanggan_complete)
## [1] 8
# Berapa persen data yang hilang kalau semua baris tidak lengkap dibuang?
round((1 - nrow(pelanggan_complete) / nrow(pelanggan)) * 100, 2)
## [1] 27.27

Ternyata kalau aku pakai strategi ini, aku akan kehilangan lebih dari seperempat datanya. Itu terlalu banyak untuk dataset sekecil ini, jadi aku memilih strategi lain.

6.3 Strategi 2: Mengisi dengan Mean atau Median

Untuk kolom numerik seperti pendapatan, aku bandingkan dulu nilai mean dan median-nya:

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

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

Perhatikan bedanya jauh sekali! Ini karena ada satu nilai pendapatan yang sangat ekstrem (500 juta) yang menarik rata-ratanya ke atas. Karena itu, aku memilih median untuk mengisi nilai yang hilang di kolom pendapatan, karena median lebih tahan terhadap nilai ekstrem seperti ini.

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

Untuk kolom usia, aku pakai pendekatan yang sama:

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

pelanggan[, c("customer_id", "usia", "usia_imputasi",
              "pendapatan", "pendapatan_imputasi")]

6.4 Strategi 3: Mengisi Kategori yang Hilang

Untuk kolom kategorik seperti kota, aku sengaja tidak mengisinya dengan modus (kategori yang paling sering muncul), karena itu bisa menyembunyikan fakta bahwa datanya memang tidak diketahui. Aku lebih suka jujur dengan label eksplisit "Tidak diketahui":

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

6.5 Menyimpan Jejak Nilai yang Tadinya Hilang

Ada satu trik yang aku suka pakai: menambahkan kolom penanda (indikator) supaya aku tetap tahu baris mana saja yang nilai aslinya hilang, walaupun sudah diisi.

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

6.6 Membandingkan Sebaran Data Sebelum dan Sesudah Diisi

Aku juga ingin memastikan proses imputasi tadi tidak mengubah bentuk sebaran datanya secara drastis. Makanya aku bandingkan lewat histogram:

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

Dari sini aku belajar satu hal penting: imputasi bisa mengubah sebaran data dan mengecilkan variasinya. Jadi pekerjaanku belum selesai hanya karena semua NA sudah hilang aku tetap perlu mengevaluasi dampaknya.

7 Bagian IV: Menangani Nilai Ekstrem (Outlier)

7.1 Melihat Outlier Lewat Boxplot

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

Kelihatan jelas ada satu titik yang jauh terpisah dari yang lain.

7.2 Menghitung Batas Wajar dengan Metode IQR

Aku pakai metode Interquartile Range (IQR) untuk menentukan batas bawah dan batas atas nilai yang masih dianggap wajar:

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

7.3 Menandai Siapa Saja yang Jadi Kandidat Outlier

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

pelanggan[pelanggan$outlier_pendapatan,
          c("customer_id", "nama", "pendapatan_imputasi")]

Benar saja, pelanggan dengan pendapatan 500 juta itu terkonfirmasi sebagai outlier.

7.4 Memutuskan Apa yang Harus Aku Lakukan dengan Outlier Ini

Nah, di sinilah aku harus berpikir dulu outlier tidak boleh langsung aku hapus begitu saja. Aku mempertimbangkan tiga kemungkinan:

  • Kesalahan input: kalau memang salah ketik, aku koreksi ke nilai yang benar.
  • Observasi valid tapi memang ekstrem: kalau datanya benar, aku pertahankan atau pakai metode yang tahan terhadap outlier.
  • Berasal dari populasi berbeda: misalnya kalau ternyata ini pelanggan korporasi yang tercampur dengan data pelanggan ritel, sebaiknya aku pisahkan segmennya.

Untuk latihan ini, aku memutuskan mempertahankan nilai aslinya, tapi aku juga membuat versi winsorized (nilai ekstrem “dipangkas” ke batas wajar) hanya untuk membandingkan dampaknya:

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

pelanggan[pelanggan$outlier_pendapatan,
          c("customer_id", "pendapatan_imputasi", "pendapatan_winsor")]

Winsorizing ini cuma aku pakai sebagai demonstrasi tambahan kalau temen-temen mau menerapkannya di data asli, pastikan itu memang sesuai dengan tujuan analisismu, dan jangan lupa dokumentasikan bahwa nilai ekstremnya sudah dibatasi.

8 Bagian V: Mentransformasikan Data Numerik

8.1 Normalisasi Min–Maks

Rumus normalisasi min–maks yang aku pakai untuk membawa nilai ke rentang \([0, 1]\) adalah:

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

Aku buat fungsinya sendiri supaya bisa dipakai berulang:

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

8.2 Normalisasi Z-Score

Rumus z-score yang aku pakai:

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

8.3 Decimal Scaling

Cara lain yang lebih sederhana adalah membagi nilai dengan pangkat 10 yang sesuai:

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

8.4 Membandingkan Ketiga Metode Transformasi

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

transformasi

8.5 Melihat Bagaimana Outlier Memengaruhi Hasil 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)

Ini yang aku maksud tadi soal pentingnya menangani outlier sebelum melakukan normalisasi: satu nilai ekstrem saja bisa menekan hampir semua nilai lain ke rentang yang sangat sempit setelah dinormalisasi min–maks.

9 Bagian VI: Menggabungkan Data Pelanggan dan Data Transaksi

9.1 Memeriksa Kunci Penghubung di Kedua Tabel

Sebelum menggabungkan dua tabel, aku pastikan dulu kolom kuncinya (customer_id dan cust_id) unik dan aku cek juga siapa saja yang “tidak punya pasangan”:

sum(duplicated(pelanggan$customer_id))
## [1] 0
sum(duplicated(transaksi_raw$cust_id))
## [1] 0
# Pelanggan yang tidak punya data transaksi
setdiff(pelanggan$customer_id, transaksi_raw$cust_id)
## [1] "C011"
# Transaksi yang tidak punya data pelanggan
setdiff(transaksi_raw$cust_id, pelanggan$customer_id)
## [1] "C012"

Ternyata pelanggan C011 belum pernah bertransaksi, sementara transaksi milik C012 tidak punya data pelanggan yang cocok.

9.2 Menyamakan Nama Kolom Identifier

Karena nama kolomnya berbeda (customer_id vs cust_id), aku samakan dulu supaya bisa digabungkan:

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

9.3 Menggabungkan dengan Left Join

Aku pakai merge() dengan all.x = TRUE supaya semua pelanggan tetap ada di hasil akhir, walaupun sebagian di antaranya tidak punya data transaksi:

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

data_terintegrasi[, c(
  "customer_id", "nama", "jumlah_transaksi", "total_purchase"
)]

9.4 Memvalidasi Hasil Penggabungan

Setelah digabungkan, aku selalu mengecek ulang supaya yakin prosesnya berjalan seperti yang aku harapkan:

# Apakah jumlah baris berubah secara tidak wajar?
c(sebelum = nrow(pelanggan), sesudah = nrow(data_terintegrasi))
## sebelum sesudah 
##      11      11
# Apakah customer_id masih tetap unik?
sum(duplicated(data_terintegrasi$customer_id))
## [1] 0
# Ada berapa nilai hilang baru akibat tidak ketemu pasangan transaksi?
colSums(is.na(data_terintegrasi[, c("jumlah_transaksi", "total_purchase")]))
## jumlah_transaksi   total_purchase 
##                1                1
# Siapa saja pelanggan yang tidak punya pasangan transaksi?
data_terintegrasi[is.na(data_terintegrasi$jumlah_transaksi),
                  c("customer_id", "nama")]

9.5 Mengisi Nol atau Membiarkan NA?

Ini pertanyaan yang menurutku penting untuk direnungkan: nilai NA pada kolom transaksi itu bisa berarti dua hal yang beda:

  • pelanggan tersebut memang belum pernah bertransaksi sama sekali, atau
  • data transaksinya tidak tersedia / gagal dipadankan karena alasan teknis.

Aku hanya boleh mengubah NA menjadi 0 kalau aku sudah yakin betul yang terjadi adalah kemungkinan pertama. Untuk latihan ini, anggap saja aku sudah mengonfirmasinya:

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

10 Menyusun Dataset Akhir dan Mengevaluasinya

10.1 Memilih Kolom yang Benar-Benar Aku Butuhkan

Setelah semua tahap di atas, aku rapikan lagi dan hanya menyimpan kolom-kolom yang relevan untuk analisis lanjutan:

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

10.2 Audit Terakhir

audit_akhir <- audit_data(data_final)
audit_akhir
sum(duplicated(data_final$customer_id))
## [1] 0
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

Semua nilai hilang sudah tertangani, dan tidak ada lagi customer_id yang terduplikasi.

10.3 Membandingkan Kondisi “Sebelum” vs “Sesudah”

Supaya kelihatan jelas seberapa besar perubahan yang sudah aku lakukan, aku ringkas perbandingannya di sini:

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

Lihat sendiri betapa jauh bedanya jumlah kategori kota turun dari 10 jadi 4, kategori status turun dari 8 jadi 2, dan semua nilai hilang sudah tertangani.

10.4 Menyimpan Hasil Akhir

Terakhir, aku simpan dataset yang sudah bersih dan log perubahannya ke dalam file CSV, supaya bisa dipakai lagi di analisis selanjutnya:

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

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

11 Pertanyaan yang Aku Tanyakan ke Diri Sendiri

Sebagai bahan renungan, ini beberapa pertanyaan yang selalu aku tanyakan ke diri sendiri setiap kali selesai melakukan preprocessing:

  1. Apakah dataset tanpa nilai hilang otomatis lebih berkualitas?
  2. Kenapa outlier tidak boleh langsung dihapus begitu saja?
  3. Bagaimana proses preprocessing justru bisa menimbulkan bias baru?
  4. Kenapa parameter imputasi dan transformasi sebaiknya dihitung dari data latih (training data) saja?
  5. Apa risikonya kalau identifier yang dipakai untuk integrasi data ternyata tidak unik?

12 Yang Bisa Aku Simpulkan

Kalau aku rangkum, ini alur yang sudah aku lalui dari awal sampai akhir:

  1. Memahami struktur dan kualitas awal data.
  2. Membersihkan kategori, duplikasi, dan pelanggaran aturan bisnis.
  3. Menangani nilai yang hilang tanpa menyembunyikan ketidakpastiannya.
  4. Mendeteksi dan mengevaluasi outlier tanpa langsung menghapusnya.
  5. Mentransformasikan atribut numerik ke skala yang lebih seragam.
  6. Menggabungkan dua sumber data dan memvalidasi hasilnya.

Satu hal yang aku pegang sampai sekarang: preprocessing itu pada dasarnya adalah proses pengambilan keputusan. Kode R hanya menjalankan keputusan yang sudah aku buat kualitas hasil akhirnya tetap bergantung pada seberapa paham aku terhadap data itu sendiri, apa tujuan analisisnya, dan seberapa rapi aku mendokumentasikan setiap perubahan yang aku lakukan.

Semoga tulisan ini membantu temen-temen yang baru belajar data preprocessing di R yaa!

13 Referensi

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