1 Pengantar

Pada kesempatan kali ini, saya ingin melakukan data preprocessing menggunakan R dan RStudio.

Disini saya berperan sebagai analis data di sebuah perusahaan e-commerce fiktif dengan 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.

Kedua data tersebut masih “kotor”. Ada nilai yang hilang, penulisan kategori yang tidak konsisten, data yang terduplikasi, angka yang tampak tidak wajar (outlier), sampai nama kolom identifier yang berbeda antara satu tabel dengan tabel lainnya. Sehingga, data tersebut harus dilakukan data preprocessing terlebih dahulu sebelum data layak dianalisis.

Prinsip dalam data preprocessing Perubahan atau penghapusan data tidak pernah dilakukan hanya karena “kelihatannya aneh”. Setiap perubahan harus memiliki alasan, aturan yang jelas, dan tercatat.

Berikut langkah-langkah dalam melakukan data preprocessing pada aplikasi RStudio:

2 Menyiapkan RStudio

Sebelum mulai menulis kode, proyek disiapkan terlebih dahulu di RStudio dengan langkah berikut:

  1. Buka RStudio.
  2. Klik File → New Project → New Directory → New Project.
  3. Beri nama proyek, misalnya praktikum_preprocessing.
  4. Simpan file R Markdown ini di dalam folder proyek tersebut.
  5. Setelah seluruh kode selesai ditulis, klik tombol Knit untuk menghasilkan laporan HTML — file inilah yang selanjutnya diunggah ke RPubs.

Direktori kerja dan versi R yang digunakan juga selalu diperiksa terlebih dahulu, supaya apabila terjadi kendala di kemudian hari, kondisi awal lingkungan kerja dapat ditelusuri dengan pasti.

getwd()
## [1] "D:/UR Z/SEMS 5 bismillah chan/Data Mining"
R.version.string
## [1] "R version 4.4.2 (2024-10-31 ucrt)"

Catatan Seluruh teknik dalam tulisan ini menggunakan fungsi dasar R (base R) saja, sehingga tidak diperlukan pemasangan paket tambahan apa pun untuk mengikutinya.

3 Menyiapkan Data yang Sengaja “Dikotorkan”

Karena ini merupakan latihan, data dibuat secara mandiri dan sengaja “dikotorkan” agar seluruh teknik pembersihan data dapat dipraktikkan dari awal hingga akhir.

Berikut data yang digunakan dalam proses data preprocessing ini :

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

Perhatikan data di atas dengan saksama. Beberapa hal sengaja disalahkan agar terlihat jelas mengapa data ini belum siap dianalisis :

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

4 Bagian I — Memahami Kondisi Data Sebelum Diproses

4.1 Melihat Struktur Data

Sebelum melakukan perubahan apa pun, bentuk data diperiksa terlebih dahulu: berapa baris, berapa kolom, tipe data 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

Berdasarkan Han, Kamber, dan Pei (2012), kualitas data dapat dilihat dari enam dimensi : accuracy, completeness, consistency, timeliness, believability, dan interpretability. Tidak semua dimensi tersebut dapat diukur langsung dari tabel, misalnya accuracy membutuhkan pembanding dengan kondisi aslinya, dan timeliness membutuhkan informasi kapan data terakhir diperbarui.

Dimensi yang dapat diperiksa langsung dari tabel ini adalah kelengkapan data, konsistensi kategori, dan duplikasi. Berikut pemeriksaannya :

# 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 hasil ini, dapat diidentifikasi beberapa hal : terdapat nilai hilang pada kolom usia, pendapatan, dan kota. Kategori kota dan status masih berantakan, serta terdapat satu baris duplikat.

4.3 Membuat Fungsi Ringkasan Kualitas

Karena pemeriksaan kualitas data akan sering dilakukan, baik sebelum maupun sesudah dibersihkan, satu fungsi khusus dibuat agar tidak perlu menulis ulang kode yang sama, yaitu :

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

Catatan penting Hasil audit ini bukan keputusan pembersihan data. Audit hanya menunjukkan kolom mana saja yang perlu diperhatikan lebih lanjut, keputusan tetap diambil setelah memahami konteksnya.


5 Bagian II — Membersihkan Data

5.1 Membuat Salinan Kerja

Perlu diingat bahwa : jangan pernah mengubah data mentah secara langsung. Salinan data dibuat terlebih dahulu, sehingga apabila terjadi kesalahan, data asli tetap tersedia sebagai pembanding.

pelanggan <- pelanggan_raw

5.2 Merapikan Spasi dan Huruf Kapital

Langkah pertama yang dilakukan adalah hal paling sederhana yaitu : menghapus spasi berlebih di awal/akhir teks, kemudian menyeragamkan semuanya menjadi huruf kecil agar 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"

Variasi kategorinya sudah jauh berkurang setelah langkah ini.

5.3 Menyamakan Penulisan Kategori

Langkah berikutnya adalah menyatukan kategori-kategori yang sebenarnya merujuk pada hal yang sama. Aturan yang digunakan di sini ditentukan berdasarkan pemahaman terhadap data, dalam pekerjaan nyata, aturan semacam ini sebaiknya dikonfirmasikan terlebih dahulu kepada pemilik data atau tim pengelola data resmi.

# 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"

Kolom kota kini hanya memiliki 3 kategori dan status hanya memiliki 2 kategori, jauh lebih rapi dibandingkan sebelumnya.

5.4 Mencari dan Menghapus Data yang Terduplikasi

Sebelumnya sempat teridentifikasi ada customer_id yang muncul dua kali. Baris-baris yang terlibat diperiksa terlebih dahulu sebelum dihapus:

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

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

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

Perlu diperhatikan Penghapusan duplikasi hanya aman dilakukan apabila setiap customer_id memang seharusnya mewakili satu pelanggan. Apabila satu pelanggan boleh memiliki banyak baris (misalnya untuk mencatat banyak transaksi), penghapusan seperti ini justru menjadi langkah yang keliru.

5.5 Memeriksa Aturan Bisnis (Domain Rules)

Selanjutnya, diperiksa apakah terdapat nilai yang melanggar aturan bisnis yang wajar. Sebagai contoh, rentang usia pelanggan yang dianggap wajar ditetapkan antara 15–100 tahun, dan pendapatan tidak boleh bernilai 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 terdapat satu pelanggan dengan usia 150 tahun, jelas melanggar aturan tersebut. Pada kondisi nyata, hal ini akan dikonfirmasi ulang ke formulir aslinya. Untuk latihan ini, diasumsikan setelah dikonfirmasi, nilai yang benar adalah 50 tahun, sehingga dilakukan koreksi:

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

5.6 Mencatat Seluruh Perubahan yang Dilakukan

Kebiasaan yang tergolong penting namun sering terlewat adalah mencatat log perubahan. Dengan catatan ini, siapa pun yang membaca laporan ini dapat mengetahui secara pasti apa saja yang telah diubah, dan alasannya.

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 Lokasi 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), ]

Terdapat tiga pilihan strategi untuk menangani nilai hilang ini, dan ketiganya akan dicoba satu per satu agar dapat dilihat perbandingannya.

6.2 Strategi 1: Menghapus Baris yang Tidak Lengkap

Strategi ini paling sederhana, namun hanya didemonstrasikan di sini, tidak langsung diterapkan pada data utama.

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 dengan strategi ini, lebih dari seperempat data akan hilang. Jumlah tersebut terlalu besar untuk dataset sekecil ini, sehingga strategi lain dipilih.

6.3 Strategi 2: Mengisi dengan Mean atau Median

Untuk kolom numerik seperti pendapatan, nilai mean dan median dibandingkan terlebih dahulu:

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

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

Perbedaannya cukup jauh. Hal ini terjadi karena terdapat satu nilai pendapatan yang sangat ekstrem (500 juta) yang menarik rata-rata ke atas. Oleh karena itu, median dipilih untuk mengisi nilai yang hilang pada kolom pendapatan, karena median lebih tahan terhadap nilai ekstrem semacam ini.

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

Untuk kolom usia, pendekatan yang sama digunakan:

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, nilai yang hilang sengaja tidak diisi dengan modus (kategori yang paling sering muncul), karena hal itu dapat menyembunyikan fakta bahwa datanya memang tidak diketahui. Label eksplisit "Tidak diketahui" digunakan agar lebih jujur menggambarkan kondisi data:

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 Sebelumnya Hilang

Satu trik yang digunakan adalah menambahkan kolom penanda (indikator) agar tetap dapat diketahui baris mana saja yang nilai aslinya hilang, meskipun 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

Perlu dipastikan pula bahwa proses imputasi tadi tidak mengubah bentuk sebaran data secara drastis. Perbandingan dilakukan melalui 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))

Insight Imputasi dapat mengubah sebaran data dan mengecilkan variasinya. Proses pembersihan data belum tentu selesai hanya karena seluruh NA telah hilang, dampaknya tetap perlu dievaluasi.


7 Bagian IV — Menangani Nilai Ekstrem (Outlier)

7.1 Melihat Outlier Melalui Boxplot

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

Terlihat jelas ada satu titik yang jauh terpisah dari titik-titik lainnya.

7.2 Menghitung Batas Wajar dengan Metode IQR

Metode Interquartile Range (IQR) digunakan 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 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 tersebut terkonfirmasi sebagai outlier.

7.4 Memutuskan Perlakuan terhadap Outlier

Pada tahap ini, keputusan perlu dipertimbangkan dengan cermat, outlier tidak boleh langsung dihapus begitu saja. Terdapat tiga kemungkinan yang perlu dipertimbangkan:

  • Kesalahan input : apabila memang terjadi salah ketik, nilai dikoreksi menjadi nilai yang benar.
  • Observasi valid namun memang ekstrem : apabila datanya benar, nilai dipertahankan atau digunakan metode yang tahan terhadap outlier.
  • Berasal dari populasi berbeda : misalnya apabila ternyata merupakan pelanggan korporasi yang tercampur dengan data pelanggan ritel, segmen tersebut sebaiknya dipisahkan.

Untuk latihan ini, diputuskan untuk mempertahankan nilai aslinya, namun versi winsorized (nilai ekstrem “dipangkas” ke batas wajar) juga dibuat 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")]

Catatan Winsorizing di sini hanya digunakan sebagai demonstrasi tambahan. Apabila hendak diterapkan pada data asli, pastikan pendekatan ini memang sesuai dengan tujuan analisis, dan jangan lupa mendokumentasikan bahwa nilai ekstremnya telah dibatasi.


8 Bagian V — Mentransformasikan Data Numerik

8.1 Normalisasi Min–Maks

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

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

Fungsi berikut dibuat agar dapat digunakan secara 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 digunakan:

\[ 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 Pengaruh Outlier terhadap 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)

Insight Inilah alasan pentingnya menangani outlier sebelum melakukan normalisasi: satu nilai ekstrem saja dapat menekan hampir seluruh nilai lain ke rentang yang sangat sempit setelah dinormalisasi dengan metode min–maks.


9 Bagian VI — Menggabungkan Data Pelanggan dan Data Transaksi

9.1 Memeriksa Kunci Penghubung di Kedua Tabel

Sebelum menggabungkan dua tabel, kolom kunci (customer_id dan cust_id) dipastikan terlebih dahulu keunikannya, sekaligus diperiksa siapa saja yang “tidak memiliki 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 memiliki data pelanggan yang cocok.

9.2 Menyamakan Nama Kolom Identifier

Karena nama kolomnya berbeda (customer_id vs cust_id), penyamaan nama dilakukan terlebih dahulu agar kedua tabel dapat digabungkan:

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

9.3 Menggabungkan dengan Left Join

Fungsi merge() dengan all.x = TRUE digunakan agar seluruh pelanggan tetap ada di hasil akhir, meskipun sebagian di antaranya tidak memiliki 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, pemeriksaan ulang selalu dilakukan untuk memastikan proses berjalan sesuai yang diharapkan:

# 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?

Pertanyaan penting yang perlu diperhatikan di sini adalah: nilai NA pada kolom transaksi dapat berarti dua hal yang berbeda:

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

NA hanya boleh diubah menjadi 0 apabila telah dipastikan bahwa yang terjadi adalah kemungkinan pertama. Untuk latihan ini, diasumsikan hal tersebut sudah dikonfirmasi:

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 Relevan

Setelah seluruh tahap di atas, data dirapikan kembali dan hanya kolom-kolom yang relevan untuk analisis lanjutan yang disimpan:

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

Seluruh nilai hilang telah tertangani, dan tidak ada lagi customer_id yang terduplikasi.

10.3 Membandingkan Kondisi “Sebelum” vs “Sesudah”

Agar terlihat jelas seberapa besar perubahan yang telah dilakukan, perbandingannya diringkas sebagai berikut:

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

Perbedaannya cukup signifikan, jumlah kategori kota turun dari 10 menjadi 4, kategori status turun dari 8 menjadi 2, dan seluruh nilai hilang telah tertangani.

10.4 Menyimpan Hasil Akhir

Sebagai langkah terakhir, dataset yang sudah bersih beserta log perubahannya disimpan ke dalam file CSV, agar dapat digunakan kembali pada 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 untuk Bahan Refleksi

Sebagai bahan renungan, berikut beberapa pertanyaan yang relevan untuk diajukan setiap kali proses preprocessing selesai dilakukan:

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

12 Kesimpulan

Secara ringkas, berikut alur yang telah dilalui dari awal hingga 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.

Poin utama Preprocessing pada dasarnya merupakan proses pengambilan keputusan. Kode R hanya menjalankan keputusan yang telah dibuat, kualitas hasil akhirnya tetap bergantung pada seberapa dalam pemahaman terhadap data itu sendiri, tujuan analisis yang hendak dicapai, dan seberapa rapi setiap perubahan didokumentasikan.

Semoga langkah-langkah ini dapat dengan mudah dipahami bagi sedang mempelajari data preprocessing di RStudio.

13 Referensi

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