Ada sebuah perusahaan e-commerce yang punya dua sumber data soal pelanggannya: data pelanggan (identitas, usia, kota, pendapatan, status keaktifan) dan data transaksi (jumlah transaksi dan total pembelian). Masalahnya, dua data ini belum bisa langsung dipakai untuk analisis. Begitu dibuka, terlihat banyak hal janggal, seperti ada nilai yang hilang, penulisan kategori yang beda-beda padahal maksudnya sama, baris yang ternyata double, angka yang bernilai ekstrem, sampai nama kolom identifier yang tidak sama antara dua sumbernya.
Dokumen ini adalah catatan proses menyelesaikan kasus tersebut dari awal sampai akhir: mulai dari mendiagnosis apa saja yang salah dengan datanya, membersihkannya, menangani nilai hilang, menangani outlier, mentransformasi atribut numerik, sampai akhirnya menggabungkan dua sumber data itu jadi satu dataset yang siap dipakai untuk analisis lanjutan. Yang ditekankan di sini bukan cuma “apa yang dilakukan”, tapi juga “kenapa dilakukan seperti itu”. Setiap kali ada pilihan (misalnya pakai median dibanding mean, atau menghapus dibanding mengimputasi), alasannya dijelaskan, karena di dunia nyata data preprocessing memang bukan soal ikut resep, melainkan soal mengambil keputusan yang bisa dipertanggungjawabkan.
Dataset di bawah ini sengaja dibuat “kotor” seperti kondisi data mentah pada umumnya, supaya seluruh proses preprocessing bisa didemonstrasikan secara nyata.
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
Sekilas sudah terlihat beberapa kejanggalan: baris C010
muncul dua kali, usia 150 tahun pada C004 jelas mustahil,
dan pendapatan Rp500.000.000 pada C007 melompat jauh dari
pelanggan lain. Semua kejanggalan ini akan ditelusuri satu per satu di
bagian berikutnya, bukan langsung dihapus begitu saja.
Sebelum mengubah apa pun, langkah pertama yang perlu dilakukan adalah mengenali dulu bentuk dan tipe data yang sedang dihadapi.
## [1] 12 6
## [1] "customer_id" "nama" "usia" "pendapatan" "kota"
## [6] "status"
## '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 kota status
## 1 C001 Ani 21 4500000 Pekanbaru Aktif
## 2 C002 Budi 25 NA PKU aktif
## 3 C003 Citra 23 5200000 PEKANBARU ACTIVE
## 4 C004 Dodi 150 4800000 Dumai A
## 5 C005 Eka 27 4900000 pekanbaru Tidak Aktif
## 6 C006 Fani NA 5100000 DUMAI nonaktif
## 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
##
##
##
##
Ringkasan statistik dari summary() sudah kasih petunjuk
awal yang cukup jelas: usia maksimum tercatat 150, pendapatan maksimum
tercatat Rp500.000.000, padahal median keduanya jauh lebih rendah. Ini
pertanda ada nilai ekstrem yang perlu diperiksa lebih dalam, bukan
sesuatu yang bisa langsung disimpulkan sebagai outlier tanpa
verifikasi.
Pemeriksaan kualitas data dilakukan dari beberapa sisi sekaligus: seberapa banyak nilai hilang, apakah ada duplikasi, seberapa konsisten penulisan kategori, dan bagaimana rentang nilai numeriknya.
## 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"
## [1] 21 150
## [1] 4.5e+06 5.0e+08
Dari sini terlihat empat masalah utama pada data pelanggan:
usia,
pendapatan, dan kota.customer_id
C010.kota dan status ditulis dengan
variasi kapitalisasi, spasi, dan singkatan yang berbeda-beda padahal
maknanya sama.Karena kondisi kualitas data ini perlu dicek berulang kali, baik di awal maupun nanti di akhir proses, ada baiknya dibuat satu fungsi bantu supaya pengecekannya konsisten dan 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## atribut tipe jumlah_missing persen_missing jumlah_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
Tabel audit ini nantinya jadi pembanding: kondisi data di titik ini akan disandingkan dengan kondisi data setelah seluruh proses preprocessing selesai.
Data mentah (pelanggan_raw) sengaja dibiarkan apa adanya
sebagai arsip. Semua proses pembersihan dilakukan pada salinannya,
supaya nanti masih bisa dibandingkan kondisi sebelum dan sesudahnya.
Tulisan seperti " PKU", "Pekanbaru ", dan
"PEKANBARU" sebenarnya merujuk ke hal yang sama, hanya beda
cara penulisan. Sebelum kategori-kategori ini diseragamkan, spasi
berlebih dan perbedaan huruf besar/kecil dirapikan dulu, supaya proses
pencocokan berikutnya tidak meleset gara-gara hal sepele seperti spasi
tersembunyi.
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"
Setelah format penulisannya rapi, langkah berikutnya adalah memetakan tiap varian ke satu label baku. Pemetaannya didasarkan pada pemahaman terhadap arti tiap singkatan atau varian, misalnya “PKU” memang merujuk ke Pekanbaru dan “A” memang merujuk ke Aktif, bukan tebak-tebakan asal cocok.
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"
Setelah tahap ini, atribut kota cuma tersisa 3 kategori
baku dan status cuma 2, turun jauh dari 10 dan 8 varian
penulisan yang tadinya berserakan.
Sebelum baris duplikat dihapus, cek apakah baris tersebut memang
benar-benar identik di semua atributnya, bukan cuma kebetulan
customer_id-nya sama padahal isinya beda.
## customer_id nama usia pendapatan kota status
## 10 C010 Joko 35 5300000 Dumai Tidak Aktif
## 11 C010 Joko 35 5300000 Dumai Tidak Aktif
Ternyata kedua baris C010 memang identik persis di
seluruh kolomnya, jadi aman untuk menyimpan kemunculan pertama saja dan
membuang duplikatnya.
pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL
dim(pelanggan)## [1] 11 6
Selain duplikasi, nilai yang secara logika bisnis tidak masuk akal juga perlu diperiksa. Untuk kasus ini, ditetapkan aturan: usia pelanggan yang wajar ada di rentang 15 sampai 100 tahun, dan pendapatan tidak boleh bernilai negatif.
## 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>
Ketemu satu pelanggan (C004) dengan usia tercatat 150
tahun, kemungkinan besar ini salah ketik dari 50. Setelah dicek ulang ke
sumber data aslinya, nilai ini dikoreksi jadi 50, bukan dihapus, karena
baris tersebut masih menyimpan informasi yang valid di kolom
lainnya.
Sementara itu, pendapatan Rp500.000.000 pada C007 tidak
melanggar aturan domain (tidak negatif), jadi belum dikoreksi di tahap
ini. Nilai ini akan dievaluasi lagi sebagai kandidat outlier di Bagian
IV.
Setiap perubahan yang sifatnya mengoreksi nilai perlu dicatat, supaya proses ini bisa ditelusuri ulang kapan saja dan tidak cuma mengandalkan ingatan soal “tadi kenapa angka ini diubah”.
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## tahap atribut
## 1 Standardisasi kota
## 2 Standardisasi status
## 3 Deduplikasi customer_id
## 4 Koreksi domain usia
## tindakan
## 1 PKU dan variasi kapital menjadi Pekanbaru
## 2 ACTIVE/A menjadi Aktif; nonaktif menjadi Tidak Aktif
## 3 Menghapus kemunculan kedua C010
## 4 Usia C004 dikoreksi dari 150 menjadi 50 berdasarkan sumber asli
## 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
Ada tiga baris yang punya setidaknya satu nilai hilang: pendapatan
pada C002, usia pada C006, serta pendapatan
dan kota sekaligus pada C011.
Cara paling gampang tentu saja langsung buang semua baris yang datanya tidak lengkap.
## [1] 11
## [1] 8
## [1] 27.27
Tapi ternyata cara ini membuang lebih dari seperempat data (sekitar 27%), padahal cuma gara-gara satu atau dua nilai hilang per baris. Untuk dataset sekecil ini, kehilangan sebanyak itu jelas terlalu mahal. Karena itu, strategi menghapus baris tidak dipakai sebagai solusi utama. Imputasi jadi pilihan yang lebih masuk akal, supaya informasi lain yang masih valid di baris-baris tersebut tidak ikut terbuang.
mean_pendapatan <- mean(pelanggan$pendapatan, na.rm = TRUE)
median_pendapatan <- median(pelanggan$pendapatan, na.rm = TRUE)
mean_pendapatan## [1] 59900000
## [1] 4900000
Rata-rata pendapatan (sekitar Rp59,9 juta) ternyata jauh lebih tinggi
dari mediannya (Rp4,9 juta). Selisih sebesar ini disebabkan oleh nilai
ekstrem milik C007. Karena mean gampang tertarik oleh
outlier, median dipilih sebagai nilai imputasi untuk atribut
pendapatan maupun usia, supaya hasil
imputasinya tidak ikut terdistorsi oleh nilai ekstrem tadi.
pelanggan$pendapatan_imputasi <- pelanggan$pendapatan
pelanggan$pendapatan_imputasi[is.na(pelanggan$pendapatan_imputasi)] <-
median_pendapatan
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")]## 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 atribut kategorik seperti kota, mengisinya dengan
modus bisa menyesatkan karena seolah-olah nilai itu memang diketahui
pasti. Lebih jujur kalau nilai yang hilang diberi label eksplisit yang
menandakan bahwa datanya memang 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
Imputasi memang menutup nilai yang hilang, tapi di sisi lain juga menghapus jejak bahwa nilai itu sebelumnya tidak ada. Supaya jejaknya tetap terlihat, apalagi karena ada kemungkinan nilai yang hilang itu sendiri punya arti, ditambahkan satu kolom indikator biner untuk menandainya.
pelanggan$pendapatan_missing <- as.integer(is.na(pelanggan$pendapatan))
table(pelanggan$pendapatan_missing)##
## 0 1
## 9 2
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)Bentuk distribusinya secara umum masih terjaga setelah diimputasi, dengan nilai tambahan yang mengumpul di sekitar median. Ini memang sesuai perkiraan, karena begitu cara kerja imputasi median.
boxplot(pelanggan$pendapatan_imputasi,
horizontal = TRUE,
col = "lightblue",
main = "Boxplot Pendapatan",
xlab = "Pendapatan")Dari boxplot-nya kelihatan jelas ada satu titik yang jauh sendirian dari sebaran nilai lainnya. Ini kandidat outlier yang masih perlu diverifikasi lebih lanjut secara angka, bukan cuma dari kesan visual.
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
Kriteria IQR mengonfirmasi dugaan tadi: pendapatan C007
(Rp500.000.000) memang berada jauh di luar batas atas (Rp5.750.000).
Jadi bukan cuma kesan visual dari boxplot, tapi sudah diverifikasi
dengan angka.
Baris ini tidak dihapus, karena tidak ada tanda-tanda kesalahan input seperti kasus usia 150 tahun sebelumnya. Bisa saja nilai ini memang benar, ada satu pelanggan dengan pendapatan yang jauh lebih tinggi dari yang lain. Menghapusnya berisiko membuang informasi yang sebenarnya valid. Sebagai jalan tengah, nilai ini ditangani dengan winsorizing: dibatasi (di-cap) pada batas atas IQR, supaya pengaruhnya terhadap analisis statistik berkurang tanpa harus kehilangan baris data itu sepenuhnya.
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
Setelah nilai hilang dan outlier sudah ditangani, atribut numeriknya ditransformasi ke skala yang sebanding. Ini penting kalau nanti datanya mau dipakai untuk metode analisis yang sensitif terhadap skala, misalnya perhitungan jarak atau model berbasis gradien.
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")]## 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
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
transformasi <- pelanggan[, c(
"customer_id", "pendapatan_imputasi", "pendapatan_minmax",
"pendapatan_z", "pendapatan_decimal"
)]
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 ini menghasilkan karakter skala yang beda-beda.
Min-maks memampatkan semua nilai ke rentang 0 sampai 1, tapi sangat
terpengaruh oleh nilai ekstrem C007, terlihat dari
nilai-nilai lain yang jadi terdesak semua ke dekat angka 0. Z-score
menunjukkan pola serupa, dengan C007 bernilai standar
deviasi jauh di atas titik tengah (sekitar 3,0). Decimal scaling juga
menunjukkan pola sebanding karena pembaginya ikut ditentukan oleh nilai
maksimum. Dari sini bisa disimpulkan bahwa penanganan outlier memang
perlu dilakukan sebelum normalisasi, bukan sesudahnya, karena nilai
ekstrem yang belum ditangani akan mendominasi hasil transformasi apa pun
metode yang dipakai.
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)Titik-titik yang menyimpang dari garis diagonal menggambarkan seberapa besar hasil normalisasi berubah setelah outlier-nya ditangani lebih dulu, jadi ini semacam konfirmasi visual atas kesimpulan di bagian sebelumnya.
Sebelum digabungkan, kedua sumber data ini dicek dulu supaya tidak
ada kejutan saat proses penggabungan berlangsung, misalnya ada
customer_id yang cuma muncul di salah satu sisi saja.
## [1] 0
## [1] 0
## [1] "C011"
## [1] "C012"
Ternyata C011 ada di data pelanggan tapi tidak punya
transaksi sama sekali, sementara C012 ada di data transaksi
tapi tidak terdaftar sebagai pelanggan. Dua kondisi ini perlu ditangani
dengan sadar, bukan diabaikan begitu saja.
Kedua sumber data ini pakai nama kolom identifier yang berbeda
(customer_id di satu sisi, cust_id di sisi
lain), jadi perlu diselaraskan dulu supaya bisa dijadikan kunci
penggabungan.
Left join dipilih (bukan inner join) supaya semua pelanggan tetap
dipertahankan, walaupun belum tentu punya transaksi, termasuk
C011. Sebaliknya, C012 yang tidak terdaftar
sebagai pelanggan otomatis tidak ikut masuk ke hasil akhirnya, karena
data transaksi tanpa identitas pelanggan yang jelas memang tidak bisa
dianalisis lebih jauh.
data_terintegrasi <- merge(
pelanggan,
transaksi,
by = "customer_id",
all.x = TRUE
)
data_terintegrasi[, 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
Validasi perlu dilakukan supaya proses penggabungan ini tidak diam-diam menimbulkan efek samping yang tidak diinginkan, misalnya baris yang jadi dobel atau jumlah baris yang berubah tanpa alasan jelas.
## sebelum sesudah
## 11 11
## [1] 0
## jumlah_transaksi total_purchase
## 1 1
## customer_id nama
## 11 C011 Kiki
Jumlah barisnya tetap 11, sesuai jumlah pelanggan setelah
deduplikasi. customer_id juga tetap unik, dan satu-satunya
nilai hilang baru yang muncul memang cuma pada C011. Ini
sesuai perkiraan, karena pelanggan tersebut memang belum pernah
bertransaksi.
Nilai hilang pada jumlah_transaksi dan
total_purchase di sini punya makna yang beda dari nilai
hilang di Bagian III. Bukan karena datanya tidak tercatat, tapi karena
pelanggan tersebut memang belum pernah bertransaksi sama sekali. Setelah
kondisi ini dipastikan, nilai NA-nya diisi dengan 0, bukan diimputasi
pakai mean atau median seperti sebelumnya, karena 0 memang nilai yang
benar secara makna untuk “belum pernah bertransaksi”, bukan sekadar
perkiraan statistik.
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)
] <- 0Dari semua kolom yang terbentuk sepanjang proses (termasuk kolom
perantara seperti pendapatan_z atau
pendapatan_decimal yang tadinya dibuat cuma untuk
membandingkan metode), dipilih kombinasi atribut yang memang relevan
untuk dataset analisis akhir.
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## 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
## atribut tipe jumlah_missing persen_missing jumlah_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
Dataset akhir ini sudah tidak lagi punya nilai hilang atau duplikasi
customer_id, jauh berbeda dari kondisi data mentah di awal
tadi.
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
Tabel ini merangkum dampak keseluruhan dari proses preprocessing
tadi: jumlah baris berkurang satu gara-gara deduplikasi, duplikasi dan
nilai hilang sudah tuntas ditangani, dan jumlah kategori pada
kota maupun status menyusut jauh karena sudah
diseragamkan.
Kasus ini menunjukkan kalau data preprocessing itu bukan sekadar menjalankan rangkaian perintah teknis, tapi lebih ke serangkaian keputusan yang masing-masing punya alasan dan bisa dipertanggungjawabkan. Beberapa keputusan penting yang diambil sepanjang proses ini:
customer_id-nya
mirip.Dataset akhir (data_final) beserta jejak seluruh
perubahannya (log_perubahan) sekarang sudah siap dipakai
untuk tahap analisis lanjutan, seperti segmentasi pelanggan atau
pemodelan prediktif.