1 Gambaran Singkat

Data yang terlihat rapi belum tentu siap dipakai untuk analisis. Pada contoh ini saya memakai data pelanggan dan data transaksi yang sengaja memiliki beberapa masalah kecil: ada nilai kosong, penulisan kategori yang berbeda-beda, data ganda, nilai yang terlalu ekstrem, serta nama ID yang belum sama.

Tujuannya bukan sekadar membuat data terlihat bersih. Setiap perubahan dibuat supaya alasan di baliknya jelas dan hasil akhirnya tetap mudah diperiksa.

Data ini mengikuti alur pada materi Data Preprocessing Menggunakan RStudio: mulai dari melihat kondisi awal, membersihkan data, menangani nilai kosong, mengecek outlier, melakukan transformasi, menggabungkan data, sampai melakukan pemeriksaan terakhir.

2 Menyiapkan Data

Contoh ini cukup dijalankan dengan R dasar, ditambah DT dan plotly khusus supaya tabel dan grafiknya bisa dipakai secara interaktif saat dibuka di RPubs.

2.1 Data pelanggan

pelanggan_raw <- data.frame(
  customer_id = c("C001", "C002", "C003", "C004", "C005", "C005",
                   "C006", "C007", "C008", "C009", "C010", "C011"),
  nama = c("Nadia", "Budi", "Citra", "Doni", "Eka", "Eka",
           "Farhan", "Gita", "Hendra", "Indah", "Joko", "Kirana"),
  usia = c(24, 31, NA, 200, 27, 27, 22, 35, 29, NA, 41, 26),
  pendapatan = c(5200000, 6800000, 4700000, 5100000, NA,
                 NA, 5900000, 800000000, 5400000, 6100000,
                 5300000, 4900000),
  kota = c("Jakarta", "BANDUNG", "surabaya ", "Jakarta", " bandung",
           " bandung", "Surabaya", "JAKARTA", NA, "Bandung",
           "surabaya", "jakarta"),
  status = c("Aktif", "aktif", "AKTIF", "A", "Nonaktif",
             "Nonaktif", "Tidak Aktif", "aktif", "Aktif",
             "tidak aktif", "AKTIF", "Aktif"),
  stringsAsFactors = FALSE
)

datatable(pelanggan_raw, options = list(pageLength = 6), caption = "Data pelanggan mentah")

2.2 Data transaksi

transaksi_raw <- data.frame(
  cust_id = c("C001", "C002", "C003", "C004", "C005", "C006",
              "C007", "C008", "C009", "C010", "C012", "C013"),
  jumlah_transaksi = c(4, 2, 6, 3, 5, 1, 7, 2, 4, 3, 2, 5),
  total_belanja = c(1200000, 850000, 2100000, 900000, 1750000,
                     400000, 2600000, 700000, 1150000, 980000,
                     1100000, 1600000),
  stringsAsFactors = FALSE
)

datatable(transaksi_raw, options = list(pageLength = 6), caption = "Data transaksi mentah")

3 Melihat Kondisi Awal

Sebelum diubah, data perlu dilihat dulu strukturnya: berapa baris, berapa kolom, tipe datanya apa, dan seberapa banyak nilai yang bermasalah.

dim(pelanggan_raw)
## [1] 12  6
str(pelanggan_raw)
## 'data.frame':    12 obs. of  6 variables:
##  $ customer_id: chr  "C001" "C002" "C003" "C004" ...
##  $ nama       : chr  "Nadia" "Budi" "Citra" "Doni" ...
##  $ usia       : num  24 31 NA 200 27 27 22 35 29 NA ...
##  $ pendapatan : num  5.2e+06 6.8e+06 4.7e+06 5.1e+06 NA NA 5.9e+06 8.0e+08 5.4e+06 6.1e+06 ...
##  $ kota       : chr  "Jakarta" "BANDUNG" "surabaya " "Jakarta" ...
##  $ status     : chr  "Aktif" "aktif" "AKTIF" "A" ...

3.1 Ringkasan kualitas data

Fungsi kecil ini dipakai berulang kali untuk mengecek kondisi data: berapa nilai kosong, berapa persen, dan berapa nilai unik di tiap kolom.

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

datatable(audit_data(pelanggan_raw), options = list(pageLength = 6),
          caption = "Ringkasan kualitas data pelanggan (kondisi awal)")

Dari audit ini terlihat beberapa hal yang perlu ditindaklanjuti: kolom usia, pendapatan, dan kota punya nilai kosong, sementara kolom kota dan status punya lebih banyak kategori unik daripada yang seharusnya karena penulisan yang belum seragam.

4 Merapikan Data Pelanggan

Salinan kerja dibuat terlebih dahulu supaya data mentah tetap tersimpan dan setiap perubahan bisa dilacak balik.

pelanggan <- pelanggan_raw

4.1 Menyamakan penulisan kota dan status

# Menghapus spasi berlebih dan menyeragamkan huruf jadi kapital di awal kata
pelanggan$kota <- trimws(pelanggan$kota)
pelanggan$kota <- tolower(pelanggan$kota)
pelanggan$kota <- tools::toTitleCase(pelanggan$kota)

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

# Standardisasi status berdasarkan aturan domain
pelanggan$status[pelanggan$status %in% c("a", "aktif", "active")] <- "Aktif"
pelanggan$status[pelanggan$status %in% c("nonaktif", "tidak aktif", "n")] <- "Tidak Aktif"

sort(unique(pelanggan$kota))
## [1] "Bandung"  "Jakarta"  "Surabaya"
sort(unique(pelanggan$status))
## [1] "Aktif"       "Tidak Aktif"

4.2 Menangani data ganda

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

customer_id “C005” tercatat dua kali dengan isi yang identik, jadi baris keduanya bisa dibuang dan hanya kemunculan pertama yang dipertahankan.

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

4.3 Memperbaiki nilai domain

# Usia yang tidak masuk akal (aturan bisnis: 15-90 tahun)
pelanggan[pelanggan$usia < 15 | pelanggan$usia > 90, ]

Usia 200 tahun pada customer_id “C004” jelas tidak valid. Setelah dicek ke catatan aslinya, nilai yang benar adalah 20 tahun.

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

4.4 Catatan perubahan

Setiap perubahan sebaiknya dicatat, bukan hanya dilakukan diam-diam, supaya bisa diperiksa ulang kapan pun.

log_perubahan <- data.frame(
  tahap = c("Standardisasi", "Standardisasi", "Deduplikasi", "Koreksi domain"),
  atribut = c("kota", "status", "customer_id", "usia"),
  keterangan = c(
    "Menyeragamkan spasi dan kapitalisasi nama kota",
    "Menyeragamkan kategori status menjadi Aktif / Tidak Aktif",
    "Menghapus baris duplikat pada C005",
    "Mengoreksi usia C004 dari 200 menjadi 20 tahun"
  )
)

datatable(log_perubahan, options = list(pageLength = 6, dom = "t"))

5 Mengatasi Nilai Kosong

5.1 Kalau semua baris tidak lengkap dihapus

pelanggan[!complete.cases(pelanggan), ]
pelanggan_complete <- pelanggan[complete.cases(pelanggan), ]
c(sebelum = nrow(pelanggan), sesudah = nrow(pelanggan_complete))
## sebelum sesudah 
##      11       7
round((1 - nrow(pelanggan_complete) / nrow(pelanggan)) * 100, 2)
## [1] 36.36

Menghapus semua baris tidak lengkap membuang cukup banyak data, jadi imputasi lebih dipilih untuk contoh ini.

5.2 Imputasi usia dan pendapatan

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

Nilai pendapatan sangat miring karena ada satu angka yang jauh lebih besar dari yang lain, sehingga median dipilih sebagai nilai imputasi karena lebih tahan terhadap nilai ekstrem.

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

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

5.3 Mengisi kota yang kosong

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

table(pelanggan$kota_imputasi)
## 
##         Bandung         Jakarta        Surabaya Tidak diketahui 
##               3               4               3               1

5.4 Menyimpan jejak nilai yang diimputasi

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

Kolom indikator ini menjaga informasi bahwa nilai tersebut sebenarnya hasil isian, bukan data asli, supaya bisa dipertimbangkan lagi saat analisis lanjutan.

plot_ly(alpha = 0.7) |>
  add_histogram(x = pelanggan$pendapatan, name = "Sebelum imputasi") |>
  add_histogram(x = pelanggan$pendapatan_imputasi, name = "Sesudah imputasi") |>
  layout(barmode = "overlay",
         title = "Distribusi Pendapatan Sebelum vs Sesudah Imputasi",
         xaxis = list(title = "Pendapatan"),
         yaxis = list(title = "Frekuensi"))

6 Memeriksa Nilai Ekstrem

6.1 Boxplot pendapatan

plot_ly(y = ~pelanggan$pendapatan_imputasi, type = "box", name = "Pendapatan") |>
  layout(title = "Boxplot Pendapatan (Interaktif)", yaxis = list(title = "Pendapatan"))

6.2 Batas IQR

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% 
##         5150000         6000000          850000         3875000         7275000

6.3 Menandai dan mengevaluasi outlier

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

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

Nilai pendapatan yang sangat besar pada satu pelanggan tidak langsung dihapus. Nilai itu bisa jadi kesalahan input, tapi bisa juga benar-benar pelanggan bernilai tinggi, jadi dibuat versi winsorized untuk membandingkan dampaknya tanpa membuang datanya.

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

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

7 Mengubah Skala Data

7.1 Normalisasi min-maks

Rumus untuk rentang \([0,1]\) adalah

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

minmax <- function(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$pendapatan_minmax <- minmax(pelanggan$pendapatan_imputasi)
pelanggan$pendapatan_winsor_minmax <- minmax(pelanggan$pendapatan_winsor)

7.2 Normalisasi z-score

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

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

7.3 Membandingkan hasil transformasi

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

datatable(transformasi, options = list(pageLength = 6),
          caption = "Perbandingan metode transformasi") |>
  formatRound(columns = c("pendapatan_minmax", "pendapatan_z",
                           "pendapatan_winsor_minmax"), digits = 4)

Tanpa penanganan outlier, satu nilai ekstrem bisa menekan hampir semua hasil min-maks yang lain ke rentang yang sangat sempit. Itu sebabnya deteksi outlier dilakukan lebih dulu sebelum transformasi skala.

8 Menggabungkan Data Pelanggan dan Transaksi

8.1 Memeriksa kecocokan ID

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

Ada pelanggan tanpa transaksi dan transaksi tanpa data pelanggan yang cocok, jadi nama kolom ID perlu diseragamkan dulu sebelum digabung.

8.2 Menggabungkan dengan merge()

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

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

datatable(data_terintegrasi[, c("customer_id", "nama", "jumlah_transaksi", "total_belanja")],
          options = list(pageLength = 6), caption = "Hasil penggabungan data")

all.x = TRUE dipakai supaya seluruh pelanggan tetap ada di hasil akhir, meskipun ada yang belum pernah bertransaksi.

8.3 Memvalidasi hasil gabungan

c(sebelum = nrow(pelanggan), sesudah = nrow(data_terintegrasi))
## sebelum sesudah 
##      11      11
sum(duplicated(data_terintegrasi$customer_id))
## [1] 0
colSums(is.na(data_terintegrasi[, c("jumlah_transaksi", "total_belanja")]))
## jumlah_transaksi    total_belanja 
##                1                1

9 Dataset Akhir

9.1 Audit akhir

data_final <- data_terintegrasi[, c(
  "customer_id", "nama", "usia_imputasi", "kota_imputasi", "status",
  "pendapatan_imputasi", "pendapatan_missing", "jumlah_transaksi", "total_belanja"
)]

datatable(audit_data(data_final), options = list(pageLength = 6, dom = "t"),
          caption = "Audit akhir dataset")

9.2 Menyimpan hasil

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

10 Refleksi

  • Nilai kosong, kategori yang tidak seragam, data ganda, dan nilai ekstrem semuanya perlu ditangani dengan alasan yang jelas, bukan sekadar “dibuang karena terlihat aneh”.
  • Median lebih aman dipakai untuk imputasi ketika distribusinya miring akibat nilai ekstrem.
  • Deteksi outlier sebaiknya dilakukan sebelum normalisasi, karena satu nilai ekstrem bisa merusak hasil skala data yang lain.
  • Menyimpan log perubahan dan indikator missing membantu proses ini tetap bisa diperiksa ulang di kemudian hari.

11 Referensi