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. Materi aslinya juga menekankan bahwa perubahan pada data sebaiknya memiliki alasan dan catatan.
Contoh ini cukup dijalankan dengan R dasar, jadi tidak perlu memasang paket tambahan.
knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE)
options(stringsAsFactors = FALSE)Saya mulai dari data pelanggan yang masih mentah. Beberapa nilai memang sengaja dibuat tidak konsisten supaya proses preprocessing bisa terlihat dengan jelas.
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")
)
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
Sumber kedua berisi ringkasan transaksi. Perhatikan bahwa kolom
ID-nya masih bernama cust_id, sedangkan pada data pelanggan
namanya customer_id.
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
)
transaksi_raw## 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 mengubah apa pun, saya ingin tahu dulu ukuran data, nama kolom, tipe data, dan contoh isinya.
## [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
Dari data awal terlihat beberapa hal yang perlu diperhatikan:
150 yang tidak masuk akal untuk konteks
pelanggan pada contoh ini;NA;PKU,
PEKANBARU, dan Pekanbaru;C010 muncul dua kali;500000000 jauh lebih besar dibanding nilai
lainnya.Supaya pengecekan tidak dilakukan satu per satu, saya membuat fungsi kecil untuk melihat jumlah nilai kosong dan jumlah nilai unik setiap 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
)
}
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
Saya membuat salinan kerja supaya data mentah tetap tersimpan.
Pertama, spasi yang tidak diperlukan dibuang. Setelah itu huruf dibuat kecil sementara agar variasi penulisan lebih mudah dicocokkan.
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"
Sekarang kategori bisa disatukan ke bentuk yang lebih konsisten.
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"
Dengan cara ini, kategori yang sebenarnya sama tidak lagi dianggap sebagai kategori berbeda hanya karena penulisannya berbeda.
customer_id seharusnya menjadi penanda unik untuk satu
pelanggan. Karena C010 muncul dua kali dengan isi yang
sama, satu baris cukup dipertahankan.
pelanggan[
duplicated(pelanggan$customer_id) |
duplicated(pelanggan$customer_id, fromLast = TRUE),
]## customer_id nama usia pendapatan kota status
## 10 C010 Joko 35 5300000 Dumai Tidak Aktif
## 11 C010 Joko 35 5300000 Dumai Tidak Aktif
pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL
dim(pelanggan)## [1] 11 6
Usia 150 pada C004 dianggap sebagai salah
input. Pada materi sumber, nilai tersebut dikoreksi menjadi
50 berdasarkan sumber asli.
Koreksi seperti ini sebaiknya dilakukan hanya ketika ada dasar yang cukup, bukan sekadar karena angka terlihat aneh.
Supaya perubahan mudah dilacak, saya simpan ringkasannya.
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"
)
)
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
Sekarang kita lihat posisi NA setelah data
dirapikan.
## 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 beberapa pilihan ketika menemukan nilai kosong. Tidak semuanya harus dihapus.
Sebagai pembanding, berikut yang terjadi jika hanya baris lengkap yang dipertahankan.
## [1] 11
## [1] 8
## [1] 27.27
Pada data ini, penghapusan langsung membuang sekitar 27,27% baris. Karena jumlah datanya kecil, saya memilih mempertahankan baris dan mengisi nilai numerik yang kosong.
Mean pendapatan sangat dipengaruhi oleh nilai ekstrem, sedangkan median berada di sekitar pola mayoritas data. Karena itu, median lebih masuk akal untuk contoh ini.
mean_pendapatan <- mean(pelanggan$pendapatan, na.rm = TRUE)
median_pendapatan <- median(pelanggan$pendapatan, na.rm = TRUE)
mean_pendapatan## [1] 59900000
## [1] 4900000
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 kota, saya tidak ingin menebak. Nilai kosong diberi label
Tidak diketahui supaya ketidakpastian tetap terlihat.
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
Kolom indikator bisa membantu mengetahui mana data pendapatan yang awalnya kosong.
pelanggan$pendapatan_missing <- as.integer(
is.na(pelanggan$pendapatan)
)
table(pelanggan$pendapatan_missing)##
## 0 1
## 9 2
Saya juga bisa membandingkan bentuk distribusi pendapatan sebelum dan setelah imputasi.
par(mfrow = c(1, 2))
hist(
pelanggan$pendapatan,
main = "Sebelum imputasi",
xlab = "Pendapatan",
breaks = 8
)
hist(
pelanggan$pendapatan_imputasi,
main = "Setelah imputasi median",
xlab = "Pendapatan",
breaks = 8
)Nilai ekstrem tidak otomatis berarti salah. Karena itu saya tandai dulu, baru memutuskan apa yang harus dilakukan.
boxplot(
pelanggan$pendapatan_imputasi,
horizontal = TRUE,
main = "Boxplot Pendapatan",
xlab = "Pendapatan"
)Saya menggunakan aturan IQR untuk menentukan kandidat outlier.
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
Batas yang diperoleh adalah sekitar 4,15 juta sampai 5,75 juta.
Dengan batas tersebut, pendapatan C007 menjadi 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")
]## customer_id nama pendapatan_imputasi
## 7 C007 Gilang 5e+08
Pendapatan 500 juta memang sangat jauh dari nilai pelanggan lainnya, tetapi belum tentu salah. Bisa saja itu pelanggan dengan karakteristik berbeda.
Untuk melihat pengaruhnya, saya membuat versi winsorized tanpa mengubah nilai asli.
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
Nilai usia dan pendapatan memiliki skala yang berbeda. Beberapa metode analisis akan lebih nyaman jika angka-angka tersebut ditransformasikan.
Rumus sederhananya adalah:
\[ x' = \frac{x-\min(x)}{\max(x)-\min(x)} \]
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
Karena C007 sangat besar, nilai tersebut menjadi 1 dan
sebagian besar pendapatan lainnya berada sangat dekat dengan 0.
Z-score menunjukkan posisi suatu nilai terhadap rata-rata dalam satuan simpangan baku.
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
Cara lain adalah mengecilkan angka 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
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
Perbandingan ini memperlihatkan bahwa satu data dapat direpresentasikan dengan skala yang berbeda. Pemilihan metode sebaiknya mengikuti kebutuhan analisis, bukan sekadar memilih metode yang paling mudah.
Setelah data pelanggan cukup rapi, data transaksi bisa disatukan.
Pertama saya cek apakah ada ID yang hanya muncul di salah satu sumber.
## [1] 0
## [1] 0
## [1] "C011"
## [1] "C012"
Hasilnya menunjukkan C011 tidak memiliki pasangan
transaksi, sedangkan C012 ada di data transaksi tetapi
tidak ada di data pelanggan.
Saya mempertahankan seluruh pelanggan, sehingga informasi pelanggan yang tidak punya transaksi tetap muncul.
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
## sebelum sesudah
## 11 11
## [1] 0
## jumlah_transaksi total_purchase
## 1 1
## customer_id nama
## 11 C011 Kiki
Hasilnya tetap memiliki 11 pelanggan. C011 menjadi
NA pada bagian transaksi karena memang tidak mempunyai
pasangan pada sumber transaksi.
Kalau sudah dipastikan bahwa NA memang berarti pelanggan
belum pernah bertransaksi, nilai tersebut bisa diubah menjadi 0. Kalau
belum ada kepastian, lebih aman mempertahankan NA.
Pada contoh ini saya anggap NA berarti belum ada
transaksi.
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)
] <- 0Saya pilih kolom yang memang masih berguna untuk analisis dan menyederhanakan nama kolom imputasi.
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
Sebelum data digunakan lebih lanjut, saya lakukan audit sekali lagi.
## 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
Hasil akhirnya tidak lagi memiliki nilai kosong pada kolom yang
dipakai, dan customer_id tetap unik.
Supaya perubahan lebih mudah dibaca, berikut perbandingannya.
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
Secara umum, jumlah baris berkurang dari 12 menjadi 11 karena satu duplikasi dihapus. Nilai kosong berhasil ditangani, kategori kota dan status menjadi lebih konsisten, dan ID pelanggan tidak lagi berulang.
Data akhir dan catatan perubahan bisa disimpan supaya prosesnya mudah digunakan kembali.
Dari proses ini terlihat bahwa preprocessing bukan hanya soal
menghapus NA atau membuang angka yang besar. Bagian yang
lebih penting justru memahami kenapa suatu nilai dianggap bermasalah,
memilih perlakuan yang masuk akal, lalu menyimpan jejak perubahan.
Pada contoh ini, data akhirnya sudah lebih konsisten dan siap dipakai
sebagai dasar analisis. Namun, nilai ekstrem seperti pendapatan
C007 tetap dipertahankan pada data utama karena belum ada
bukti bahwa angka tersebut salah. Versi winsorized hanya dibuat
untuk melihat dampak metode.
Catatan sumber: struktur latihan, dataset contoh, alur preprocessing, serta nilai hasil yang digunakan dalam dokumen ini mengikuti materi yang diberikan, tetapi penjelasan dan susunan narasinya ditulis ulang dengan bahasa yang lebih natural.