Sebelum data dapat digunakan untuk analisis maupun pemodelan, sebagian besar dataset di dunia nyata memerlukan tahap pembersihan dan persiapan terlebih dahulu. Panduan ini membahas penerapan tahapan data preprocessing secara sistematis pada studi kasus data pelanggan sebuah perusahaan e-commerce yang akan digabungkan dengan data transaksinya. Data mentah yang tersedia masih memiliki beberapa masalah kualitas, di antaranya:
Tujuan panduan ini adalah menguraikan setiap permasalahan kualitas data tersebut secara bertahap, menjelaskan dasar pertimbangan dalam memilih strategi penanganannya, dan mendokumentasikan setiap perubahan yang dilakukan, sebelum akhirnya data digabungkan menjadi satu dataset analisis yang siap dipakai. Pendekatan yang digunakan dapat diterapkan pada kasus data pelanggan lain dengan karakteristik serupa.
Dataset yang dipakai terdiri dari dua bagian: data pelanggan dan data transaksi. Kondisi kedua data ini sengaja dibuat kotor untuk mengilustrasikan berbagai jenis masalah kualitas data yang umum dijumpai pada praktiknya.
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## 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
## 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
Sebelum data dibersihkan, penting untuk memetakan dulu bentuk dan
ukuran masalahnya. Pengecekan berikut dilakukan pada
pelanggan_raw.
## [1] 12 6
## '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" ...
## 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
##
##
##
##
Dari ringkasan di atas terlihat kejanggalan pada atribut numerik: usia maksimum tercatat 150 tahun, dan pendapatan maksimum mencapai Rp500.000.000 — dua angka yang tidak wajar untuk konteks data pelanggan individual, dan patut dicurigai sebagai kesalahan input.
## customer_id nama usia pendapatan kota status
## 0 0 1 2 1 0
## customer_id nama usia pendapatan kota status
## 0.00 0.00 8.33 16.67 8.33 0.00
## [1] 1
## [1] 1
## [1] " PKU" "Dumai" "DUMAI" "pekanbaru" "Pekanbaru"
## [6] "PEKANBARU" "Pekanbaru " "PKU" "Siak"
## [1] "A" "ACTIVE" "aktif" "Aktif" "AKTIF"
## [6] "nonaktif" "Tidak aktif" "Tidak Aktif"
Kota “Pekanbaru” saja tercatat dalam lima variasi penulisan berbeda
("Pekanbaru", " PKU",
"PEKANBARU", "pekanbaru",
"Pekanbaru "), begitu pula status pelanggan yang punya enam
variasi untuk hanya dua makna (aktif / tidak aktif).
customer_id C010 juga muncul dua kali
dengan data yang identik, menandakan input ganda.
Supaya pengecekan kualitas bisa dipakai ulang di beberapa titik proses (sebelum dan sesudah dibersihkan), dibuat satu fungsi ringkasan:
audit_kualitas <- function(df) {
data.frame(
kolom = names(df),
tipe_data = sapply(df, function(x) class(x)[1]),
n_missing = sapply(df, function(x) sum(is.na(x))),
pct_missing = round(sapply(df, function(x) mean(is.na(x)) * 100), 2),
n_unik = sapply(df, function(x) length(unique(x))),
row.names = NULL
)
}
audit_sebelum <- audit_kualitas(pelanggan_raw)
audit_sebelum## kolom tipe_data n_missing pct_missing n_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
Seluruh proses pembersihan dilakukan pada salinan data, bukan pada
data mentah, agar pelanggan_raw tetap tersimpan sebagai
pembanding.
pelanggan$kota <- trimws(pelanggan$kota)
pelanggan$status <- trimws(pelanggan$status)
pelanggan$kota <- tolower(pelanggan$kota)
pelanggan$status <- tolower(pelanggan$status)
sort(unique(pelanggan$kota))## [1] "dumai" "pekanbaru" "pku" "siak"
## [1] "a" "active" "aktif" "nonaktif" "tidak aktif"
pelanggan$kota[pelanggan$kota %in% c("pku", "pekanbaru")] <- "Pekanbaru"
pelanggan$kota[pelanggan$kota == "dumai"] <- "Dumai"
pelanggan$kota[pelanggan$kota == "siak"] <- "Siak"
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"
## [1] "Aktif" "Tidak Aktif"
Kategori kota dan status sekarang sudah seragam — tidak ada lagi variasi penulisan yang tercecer.
## 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 C010 identik, kemunculan kedua dianggap input ganda dan dihapus, dengan mempertahankan kemunculan pertama.
pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL
dim(pelanggan)## [1] 11 6
Untuk atribut usia, digunakan aturan bisnis sederhana: usia pelanggan individual yang wajar berada pada rentang 15–100 tahun.
## customer_id nama usia pendapatan kota status
## 4 C004 Dodi 150 4800000 Dumai Aktif
## NA <NA> <NA> NA NA <NA> <NA>
## customer_id nama usia pendapatan kota status
## NA <NA> <NA> NA NA <NA> <NA>
## NA.1 <NA> <NA> NA NA <NA> <NA>
Usia 150 tahun pada C004 jelas melanggar aturan tersebut. Setelah ditelusuri ke sumber data asli, nilai yang benar adalah 50 tahun — kemungkinan angka “1” pada awal digit tertulis tidak sengaja.
Setiap perubahan pada tahap cleaning dicatat agar proses tetap bisa ditelusuri ulang:
log_perubahan <- data.frame(
tahap = c("Standardisasi", "Standardisasi", "Deduplikasi", "Koreksi domain"),
kolom = c("kota", "status", "customer_id", "usia"),
keterangan = c(
"Variasi 'PKU' dan variasi kapitalisasi disatukan menjadi 'Pekanbaru'",
"'ACTIVE'/'A' disatukan jadi 'Aktif'; 'nonaktif' disatukan jadi 'Tidak Aktif'",
"Baris kedua dengan customer_id C010 (identik) dihapus",
"Usia C004 dikoreksi dari 150 menjadi 50 tahun berdasarkan data sumber"
),
stringsAsFactors = FALSE
)
log_perubahan## tahap kolom
## 1 Standardisasi kota
## 2 Standardisasi status
## 3 Deduplikasi customer_id
## 4 Koreksi domain usia
## keterangan
## 1 Variasi 'PKU' dan variasi kapitalisasi disatukan menjadi 'Pekanbaru'
## 2 'ACTIVE'/'A' disatukan jadi 'Aktif'; 'nonaktif' disatukan jadi 'Tidak Aktif'
## 3 Baris kedua dengan customer_id C010 (identik) dihapus
## 4 Usia C004 dikoreksi dari 150 menjadi 50 tahun berdasarkan data sumber
## customer_id nama usia pendapatan kota status
## 0 0 1 2 1 0
## 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
Sebagai pembanding, dihitung dulu berapa banyak data yang akan hilang jika strategi paling sederhana (menghapus semua baris bermasalah) dipakai:
## [1] 11
## [1] 8
## [1] 27.27
Menghapus baris ternyata membuang porsi data yang cukup besar untuk dataset sekecil ini, sehingga strategi ini kurang ideal dipakai sendirian.
Median dipilih dibanding mean karena data pendapatan masih mengandung nilai ekstrem (Rp500 juta) yang dapat menarik rata-rata menjadi tidak representatif.
median_pendapatan <- median(pelanggan$pendapatan, na.rm = TRUE)
median_usia <- median(pelanggan$usia, na.rm = TRUE)
pelanggan$pendapatan_imputasi <- pelanggan$pendapatan
pelanggan$pendapatan_imputasi[is.na(pelanggan$pendapatan_imputasi)] <- median_pendapatan
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")]## 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
Untuk kota yang hilang, dipakai label eksplisit alih-alih diisi sembarang kota, supaya ketidaktahuan tetap terlihat jelas di 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
pelanggan$pendapatan_missing <- as.integer(is.na(pelanggan$pendapatan))
table(pelanggan$pendapatan_missing)##
## 0 1
## 9 2
Kolom indikator ini penting supaya nilai hasil imputasi tidak diperlakukan seolah-olah data asli pada analisis selanjutnya.
par(mfrow = c(1, 2))
hist(pelanggan$pendapatan,
main = "Sebelum Imputasi", xlab = "Pendapatan",
col = "#e8a1a5", border = "white", breaks = 8)
hist(pelanggan$pendapatan_imputasi,
main = "Sesudah Imputasi Median", xlab = "Pendapatan",
col = "#c1666b", border = "white", breaks = 8)boxplot(pelanggan$pendapatan_imputasi,
horizontal = TRUE, col = "#e8a1a5",
border = "#8f3a3f", lwd = 1.5,
main = "Sebaran Pendapatan Pelanggan",
xlab = "Pendapatan")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
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
Alih-alih membuang baris outlier begitu saja, nilai ekstremnya dipangkas (winsorized) ke batas IQR terdekat, sehingga pelanggan tersebut tetap ada dalam dataset tapi nilainya tidak lagi mendominasi skala:
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
normalisasi_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 <- normalisasi_minmax(pelanggan$usia_imputasi)
pelanggan$pendapatan_minmax <- normalisasi_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
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
skala_desimal <- function(x) {
nilai_maks <- max(abs(x), na.rm = TRUE)
if (nilai_maks == 0) return(x)
j <- ceiling(log10(nilai_maks + 1))
x / (10 ^ j)
}
pelanggan$pendapatan_decimal <- skala_desimal(pelanggan$pendapatan_imputasi)
range(pelanggan$pendapatan_decimal, na.rm = TRUE)## [1] 0.0045 0.5000
tabel_transformasi <- pelanggan[, c(
"customer_id", "pendapatan_imputasi", "pendapatan_minmax",
"pendapatan_z", "pendapatan_decimal"
)]
tabel_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
Ketiga metode menghasilkan skala akhir yang berbeda: min–maks memampatkan seluruh nilai ke rentang 0–1, z-score menyatakan posisi relatif terhadap rata-rata dalam satuan simpangan baku, sedangkan decimal scaling sekadar menggeser koma desimal tanpa mengubah bentuk distribusi aslinya.
pelanggan$pendapatan_winsor_minmax <- normalisasi_minmax(pelanggan$pendapatan_winsor)
plot(pelanggan$pendapatan_minmax,
pelanggan$pendapatan_winsor_minmax,
pch = 19, col = "#a4444a", cex = 1.4,
xlab = "Min–maks dari Data Asli",
ylab = "Min–maks dari Data Winsorized",
main = "Efek Penanganan Outlier terhadap Normalisasi")
abline(0, 1, col = "#c1666b", lty = 2, lwd = 2)
grid(col = "#f3d4d4")Titik yang menyimpang jauh dari garis diagonal adalah pelanggan dengan pendapatan outlier (C007) — tanpa winsorizing, nilai min–maksnya menekan seluruh pelanggan lain mendekati 0.
## [1] 0
## [1] 0
## [1] "C011"
## [1] "C012"
C011 ada di data pelanggan tapi tidak punya catatan transaksi, sementara C012 punya catatan transaksi tapi tidak terdaftar di data pelanggan (kemungkinan tercatat dengan ID pelanggan yang berbeda, C010, pada sumber data yang tersedia).
Digunakan left join agar seluruh pelanggan tetap tercatat, meski sebagian tidak memiliki transaksi:
data_gabungan <- merge(
pelanggan, transaksi,
by = "customer_id", all.x = TRUE
)
data_gabungan[, 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
## sebelum sesudah
## 11 11
## [1] 0
## jumlah_transaksi total_purchase
## 1 1
## customer_id nama
## 11 C011 Kiki
Jumlah baris tidak berubah dan customer_id tetap unik,
artinya proses join tidak menggandakan data. Nilai hilang baru pada
jumlah_transaksi dan total_purchase murni
berasal dari pelanggan yang belum pernah bertransaksi (C011), bukan
kesalahan penggabungan.
Karena sudah dipastikan bahwa NA pada kolom transaksi berarti “belum pernah bertransaksi”, nilainya diisi 0 — bukan diisi dengan median seperti pada atribut pelanggan sebelumnya, karena maknanya berbeda (ketiadaan aktivitas, bukan data yang gagal tercatat):
data_gabungan$jumlah_transaksi_final <- data_gabungan$jumlah_transaksi
data_gabungan$total_purchase_final <- data_gabungan$total_purchase
data_gabungan$jumlah_transaksi_final[is.na(data_gabungan$jumlah_transaksi_final)] <- 0
data_gabungan$total_purchase_final[is.na(data_gabungan$total_purchase_final)] <- 0data_final <- data_gabungan[, 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) == "usia_imputasi"] <- "usia"
names(data_final)[names(data_final) == "kota_imputasi"] <- "kota"
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
## kolom tipe_data n_missing pct_missing n_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
## [1] 0
## 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 <- 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
barplot(
t(as.matrix(perbandingan[, c("sebelum", "sesudah")])),
beside = TRUE,
names.arg = perbandingan$indikator,
col = c("#e8a1a5", "#8f3a3f"),
legend.text = c("Sebelum", "Sesudah"),
args.legend = list(x = "topright", bty = "n"),
las = 2, cex.names = 0.75,
main = "Perbandingan Kondisi Data: Sebelum vs Sesudah Preprocessing",
border = "white"
)Beberapa pertanyaan berikut sering muncul dalam praktik data preprocessing dan relevan untuk didiskusikan sebagai bahan pertimbangan lebih lanjut.
1. Apakah dataset tanpa missing value selalu lebih berkualitas?
Tidak selalu. Tidak adanya nilai hilang bisa jadi hanya berarti nilai tersebut sudah ditutupi lewat imputasi yang kurang tepat, bukan berarti datanya benar-benar lengkap dan akurat. Kualitas data lebih ditentukan oleh seberapa representatif dan konsisten nilainya, bukan semata dari ada-tidaknya sel yang kosong.
2. Mengapa outlier tidak boleh otomatis dihapus?
Karena outlier bisa jadi merupakan kesalahan input (seperti usia 150 tahun pada kasus ini), tapi bisa juga mencerminkan kondisi nyata yang justru penting untuk dianalisis (misalnya pelanggan dengan nilai transaksi sangat besar). Menghapusnya tanpa investigasi berisiko membuang informasi yang justru bermakna.
3. Bagaimana preprocessing dapat menimbulkan bias?
Setiap keputusan pembersihan — memilih median dibanding mean, memilih menghapus dibanding mengimputasi, menentukan ambang batas outlier — adalah keputusan yang mempengaruhi bentuk akhir data. Jika strategi tersebut diterapkan secara tidak konsisten atau tanpa dasar yang jelas, hasil analisis bisa condong ke arah tertentu tanpa disadari.
4. Mengapa parameter imputasi dan transformasi seharusnya dihitung dari data pelatihan?
Supaya proses transformasi pada data uji tidak “mengintip” informasi dari data uji itu sendiri (data leakage). Median, mean, atau parameter skala yang dipakai untuk data baru harus konsisten dengan apa yang dipelajari model dari data pelatihan.
5. Apa risiko integrasi data jika identifier tidak unik?
Penggabungan dapat menghasilkan duplikasi baris secara tidak sengaja (satu baris di satu tabel bisa cocok dengan lebih dari satu baris di tabel lain), sehingga jumlah baris hasil gabungan membengkak dan statistik ringkasan seperti total transaksi menjadi tidak akurat.
Tahapan yang diuraikan pada panduan ini mencakup pemeriksaan kualitas data awal, pembersihan kategori dan duplikasi, penanganan nilai hilang dengan beberapa strategi berbeda, deteksi dan penanganan outlier, transformasi skala atribut numerik, hingga integrasi dua sumber data yang divalidasi hasilnya. Data preprocessing pada dasarnya merupakan rangkaian pengambilan keputusan yang harus dapat dipertanggungjawabkan secara metodologis — kode program hanya berperan sebagai alat untuk menjalankan keputusan tersebut secara konsisten dan terdokumentasi. Pendekatan bertahap dan pendokumentasian setiap perubahan, sebagaimana dicontohkan pada log perubahan di atas, merupakan praktik yang disarankan agar proses preprocessing tetap transparan dan dapat direproduksi oleh pihak lain.
Han, J., Kamber, M., & Pei, J. (2012). Data Mining: Concepts and Techniques (3rd ed.). Morgan Kaufmann. Bab 3: Data Preprocessing.