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.
Contoh ini cukup dijalankan dengan R dasar, ditambah DT
dan plotly khusus supaya tabel dan grafiknya bisa dipakai
secara interaktif saat dibuka di RPubs.
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")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")Sebelum diubah, data perlu dilihat dulu strukturnya: berapa baris, berapa kolom, tipe datanya apa, dan seberapa banyak nilai yang bermasalah.
## [1] 12 6
## '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" ...
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.
Salinan kerja dibuat terlebih dahulu supaya data mentah tetap tersimpan dan setiap perubahan bisa dilacak balik.
# 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"
## [1] "Aktif" "Tidak Aktif"
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
# 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.
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"))pelanggan_complete <- pelanggan[complete.cases(pelanggan), ]
c(sebelum = nrow(pelanggan), sesudah = nrow(pelanggan_complete))## sebelum sesudah
## 11 7
## [1] 36.36
Menghapus semua baris tidak lengkap membuang cukup banyak data, jadi imputasi lebih dipilih untuk contoh ini.
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$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
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"))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
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")]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)\[z = \frac{x-\bar{x}}{s}.\]
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.
## [1] "C011"
## [1] "C012" "C013"
Ada pelanggan tanpa transaksi dan transaksi tanpa data pelanggan yang cocok, jadi nama kolom ID perlu diseragamkan dulu sebelum digabung.
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.
DT: https://rstudio.github.io/DT/plotly untuk R: https://plotly.com/r/