Bayangkan sebuah perusahaan e-commerce yang ingin menganalisis pelanggannya. Data yang dipakai berasal dari dua sumber:
Data tersebut belum siap dianalisis karena masih memiliki beberapa masalah: nilai hilang, kategori yang tidak seragam, data duplikat, nilai ekstrem, dan nama identifier yang berbeda. Untuk mengatasi masalah tersebut, perlu dilakukan preprocessing data, seperti menangani data hilang, data duplikat, kategori yang tidak seragam, dan nilai ekstrem. Dengan demikian, data menjadi lebih rapi dan konsisten sehingga analisis yang dilakukan dapat menghasilkan kesimpulan yang lebih baik.
getwd()
#> [1] "C:/Users/Dell/Downloads"
R.version.string
#> [1] "R version 4.4.2 (2024-10-31 ucrt)"
berikut merupakan data dummy yang sengaja dibuat untuk memudahkan langkah-langkah dalam pre-processing.
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
transaksi_raw
Sebelum melakukan langkah Preprocessing, sebaiknya kita harus mengenali struktur data yang ada Sebelum membersihkan apa pun, kita kenali dulu datanya: berapa baris dan kolomnya, apa nama atributnya, bagaimana tipenya, dan seperti apa isi beberapa baris awal.
Jumlah baris dan kolom
dim(pelanggan_raw)
#> [1] 12 6
Nama Atribut/Variabel
names(pelanggan_raw)
#> [1] "customer_id" "nama" "usia" "pendapatan" "kota"
#> [6] "status"
Tipe Data
str(pelanggan_raw)
#> '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 4500000 NA 5200000 4800000 4900000 5100000 500000000 4700000 4600000 5300000 ...
#> $ kota : chr "Pekanbaru" " PKU" "PEKANBARU" "Dumai" ...
#> $ status : chr "Aktif" "aktif" "ACTIVE" "A" ...
6 baris awal
head(pelanggan_raw)
Statistik Deskriptif
summary(pelanggan_raw)
#> 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
#>
#>
#>
#>
Kualitas data bisa dilihat dari beberapa sisi, yaitu accuracy, completeness, consistency, timeliness, believability, dan interpretability. Tidak semuanya bisa dinilai hanya dari tabel yang kita punya. Karena itu, di tahap awal kita fokus pada masalah yang memang bisa terdeteksi langsung dari dataset.
Beberapa pemeriksaan sederhana berikut membantu kita menemukan masalah yang terlihat sejak awal.
Missing Value per atribut
colSums(is.na(pelanggan_raw))
#> customer_id nama usia pendapatan kota status
#> 0 0 1 2 1 0
Persentase missing value per atribut
round(colMeans(is.na(pelanggan_raw)) * 100, 2)
#> customer_id nama usia pendapatan kota status
#> 0.00 0.00 8.33 16.67 8.33 0.00
banyaknya data duplikat
sum(duplicated(pelanggan_raw))
#> [1] 1
Banyaknya customer_id yang berulang
sum(duplicated(pelanggan_raw$customer_id))
#> [1] 1
Kategori untuk atribut kota
sort(unique(pelanggan_raw$kota))
#> [1] " PKU" "Dumai" "DUMAI" "pekanbaru" "Pekanbaru"
#> [6] "PEKANBARU" "Pekanbaru " "PKU" "Siak"
Kategori untuk atribut status
sort(unique(pelanggan_raw$status))
#> [1] "A" "ACTIVE" "aktif" "Aktif" "AKTIF"
#> [6] "nonaktif" "Tidak aktif" "Tidak Aktif"
Rentang atribut usia
range(pelanggan_raw$usia, na.rm = TRUE)
#> [1] 21 150
Rentang atribut Pendapatan
range(pelanggan_raw$pendapatan, na.rm = TRUE)
#> [1] 4500000 500000000
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
Interpretasi: berdasarkan tabel di atas, kita dapat mengetahui adanya missing value pada variabel usia, pendapatan dan kota yaitu sebanyak 4. Namun, kita harus melihat lebih detail untuk penanganannya.
salinan kerja dibuat agar kita tidak kehilangan data asli, sehingga mudah untuk menemukan perubahan ataupun perbedaan sebelum dan sesudah tahap preprocessing.
pelanggan <- pelanggan_raw
# Menghapus spasi di awal dan akhir
pelanggan$kota <- trimws(pelanggan$kota)
pelanggan$status <- trimws(pelanggan$status)
# Menyeragamkan huruf menjadi kecil untuk proses pencocokan
pelanggan$kota <- tolower(pelanggan$kota)
pelanggan$status <- tolower(pelanggan$status)
sort(unique(pelanggan$kota))
#> [1] "dumai" "pekanbaru" "pku" "siak"
sort(unique(pelanggan$status))
#> [1] "a" "active" "aktif" "nonaktif" "tidak aktif"
Standardisasi dilakukan berdasarkan aturan domain yang masuk akal. Dalam kondisi nyata, aturan seperti ini sebaiknya dikonfirmasi melalui data dictionary atau pemilik data.
# Standardisasi kota
pelanggan$kota[pelanggan$kota %in% c("pku", "pekanbaru")] <- "Pekanbaru"
pelanggan$kota[pelanggan$kota == "dumai"] <- "Dumai"
pelanggan$kota[pelanggan$kota == "siak"] <- "Siak"
# Standardisasi status
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"
sort(unique(pelanggan$status))
#> [1] "Aktif" "Tidak Aktif"
# Menampilkan seluruh baris dengan customer_id yang berulang
pelanggan[duplicated(pelanggan$customer_id) |
duplicated(pelanggan$customer_id, fromLast = TRUE), ]
# Mempertahankan kemunculan pertama untuk customer_id yang identik
pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL
dim(pelanggan)
#> [1] 11 6
Catatan: penghapusan duplikasi tidak boleh dilakukan otomatis. Langkah ini tepat hanya jika satu
customer_idmemang seharusnya mewakili satu pelanggan.
# Kandidat usia tidak valid berdasarkan aturan bisnis 15–100 tahun
pelanggan[pelanggan$usia < 15 | pelanggan$usia > 100, ]
# Kandidat pendapatan tidak valid jika bernilai negatif
pelanggan[pelanggan$pendapatan < 0, ]
Nilai usia 150 tidak masuk akal. Setelah dicek kembali pada sumber data asli, ternyata nilai yang benar adalah 50. Oleh karena itu, nilai tersebut diperbaiki menjadi 50.
pelanggan$usia[pelanggan$customer_id == "C004"] <- 50
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
colSums(is.na(pelanggan))
#> customer_id nama usia pendapatan kota status
#> 0 0 1 2 1 0
# Baris yang memiliki sedikitnya satu missing value
pelanggan[!complete.cases(pelanggan), ]
Kode berikut hanya digunakan sebagai contoh untuk melihat dampak
metode listwise deletion. Data asli pada objek
pelanggan tidak langsung diubah.
pelanggan_complete <- pelanggan[complete.cases(pelanggan), ]
nrow(pelanggan)
#> [1] 11
nrow(pelanggan_complete)
#> [1] 8
Pertanyaan: berapa persen data yang hilang jika seluruh baris tidak lengkap dihapus?
round((1 - nrow(pelanggan_complete) / nrow(pelanggan)) * 100, 2)
#> [1] 27.27
Pendapatan memiliki satu nilai yang sangat besar. Jadi, sebelum memilih cara imputasi, bandingkan dulu mean dan median.
mean_pendapatan <- mean(pelanggan$pendapatan, na.rm = TRUE)
median_pendapatan <- median(pelanggan$pendapatan, na.rm = TRUE)
mean_pendapatan
#> [1] 59900000
median_pendapatan
#> [1] 4900000
Karena nilai 500 juta sangat berbeda dengan data lainnya sehingga membuat distribusi pendapatan menjadi sangat menceng maka median lebih aman digunakan dari pada mean.
pelanggan$pendapatan_imputasi <- pelanggan$pendapatan
pelanggan$pendapatan_imputasi[is.na(pelanggan$pendapatan_imputasi)] <-
median_pendapatan
# Imputasi usia dengan median
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")]
Nilai kategorik yang hilang tidak selalu harus diisi dengan modus.
Pada contoh ini, kita menggunakan kategori Tidak diketahui
agar informasi bahwa data tersebut awalnya kosong tetap dapat
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
Kita juga bisa membuat indikator missing untuk menandai bahwa suatu nilai sebelumnya kosong.
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)
par(mfrow = c(1, 1))
Imputasi dapat mengubah distribusi dan mengecilkan variasi. Karena itu, evaluasi tidak berhenti setelah semua
NAhilang.
boxplot(pelanggan$pendapatan_imputasi,
horizontal = TRUE,
col = "lightblue",
main = "Boxplot Pendapatan",
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")]
Nilai 500 juta tidak boleh langsung dihapus. Ada beberapa kemungkinan yang perlu dipertimbangkan:
Karena mempertimbangkan alasan di atas maka nilai 500 juta tetap dipertahankan. Kita juga membuat variabel versi winsorized hanya untuk melihat perbedaan hasil dari kedua metode.
pelanggan$pendapatan_winsor <- pmin(
pmax(pelanggan$pendapatan_imputasi, batas_bawah),
batas_atas
)
pelanggan[pelanggan$outlier_pendapatan,
c("customer_id", "pendapatan_imputasi", "pendapatan_winsor")]
Winsorizing hanya digunakan sebagai contoh tambahan. Metode ini sebaiknya diterapkan jika memang sesuai dengan tujuan analisis, dan perubahan yang dilakukan perlu dicatat agar jelas bahwa nilai ekstrem telah dibatasi.
Rumus normalisasi min–maks untuk mengubah nilai ke rentang 0 sampai 1 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")]
Rumus z-score adalah:
\[ z = \frac{x-\bar{x}}{s} \]
Keterangan:
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)
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
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)
Interpretasi: outlier dapat menekan sebagian besar nilai min–maks ke rentang yang sangat sempit. Inilah alasan deteksi outlier perlu dilakukan sebelum memilih transformasi.
sum(duplicated(pelanggan$customer_id))
#> [1] 0
sum(duplicated(transaksi_raw$cust_id))
#> [1] 0
setdiff(pelanggan$customer_id, transaksi_raw$cust_id)
#> [1] "C011"
setdiff(transaksi_raw$cust_id, pelanggan$customer_id)
#> [1] "C012"
Hasil setdiff() membantu menemukan ID yang hanya muncul
di salah satu sumber, sehingga kita tahu siapa yang belum punya pasangan
saat data digabungkan.
transaksi <- transaksi_raw
names(transaksi)[names(transaksi) == "cust_id"] <- "customer_id"
merge()data_terintegrasi <- merge(
pelanggan,
transaksi,
by = "customer_id",
all.x = TRUE
)
data_terintegrasi[, c(
"customer_id", "nama", "jumlah_transaksi", "total_purchase"
)]
Dengan all.x = TRUE, semua pelanggan tetap
dipertahankan. Kalau seorang pelanggan tidak punya pasangan transaksi,
nilai transaksi akan menjadi NA.
# Apakah jumlah baris berubah secara tidak wajar?
c(sebelum = nrow(pelanggan), sesudah = nrow(data_terintegrasi))
#> sebelum sesudah
#> 11 11
# Apakah customer_id tetap unik?
sum(duplicated(data_terintegrasi$customer_id))
#> [1] 0
# Missing value baru akibat tidak ditemukan pasangan transaksi
colSums(is.na(data_terintegrasi[, c("jumlah_transaksi", "total_purchase")]))
#> jumlah_transaksi total_purchase
#> 1 1
# Pelanggan tanpa pasangan transaksi
data_terintegrasi[is.na(data_terintegrasi$jumlah_transaksi),
c("customer_id", "nama")]
NA pada data transaksi belum tentu berarti nol. Bisa
saja artinya:
Karena itu, ubah NA menjadi 0 hanya setelah definisinya
sudah dipastikan.
# Contoh jika telah dikonfirmasi bahwa NA berarti belum pernah bertransaksi
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)
] <- 0
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
audit_akhir <- audit_data(data_final)
audit_akhir
sum(duplicated(data_final$customer_id))
#> [1] 0
colSums(is.na(data_final))
#> 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
write.csv(data_final,
"data_pelanggan_setelah_preprocessing.csv",
row.names = FALSE)
write.csv(log_perubahan,
"log_perubahan_preprocessing.csv",
row.names = FALSE)
1. Apakah dataset tanpa missing value selalu lebih berkualitas?
Jawaban: Tidak, Dataset tanpa
missing value memang terlihat lebih lengkap, tetapi
kualitas data tidak hanya dilihat dari ada atau tidaknya nilai kosong.
Jika missing value diisi sembarangan, hasilnya justru bisa
membuat data kurang sesuai dengan kondisi sebenarnya. Jadi, yang penting
adalah menangani missing value dengan cara yang tepat tanpa
menghilangkan informasi pentingsehingga tetap menggambarkan data
sebenarnya.
2. Mengapa outlier tidak boleh otomatis dihapus?
Jawaban: Karena outlier belum tentu sebuah kesalahan tetapi bisa jadi merupakan kondisi nyata yang memang terjadi pada data. Sebelum menghapus outlier, kita perlu memeriksa apakah nilai tersebut merupakan kesalahan input, nilai yang memang valid tapi ekstrem, atau berasal dari kelompok/populasi yang berbeda.
3. Bagaimana preprocessing dapat menimbulkan bias?
Jawaban: Bias bisa terjadi jika cara kita membersihkan atau mengubah data membuat sebagian kelompok menjadi tidak terwakili dengan baik. Misalnya, terlalu banyak menghapus data yang memiliki missing value dapat menyebabkan jumlah data dari kelompok tertentu berkurang. Imputasi yang tidak tepat juga dapat mengubah pola atau distribusi data dari kondisi sebenarnya.
4. Mengapa parameter imputasi dan transformasi seharusnya dihitung dari data pelatihan?
Jawaban: Agar tidak terjadi data leakage, yaitu ketika informasi dari data pengujian ikut digunakan dalam proses pengolahan data. Misalnya, nilai mean atau median sebaiknya dihitung dari data pelatihan saja, kemudian digunakan untuk mengolah data pengujian. Dengan begitu, hasil evaluasi model menjadi lebih objektif.
5. Apa risiko integrasi data jika identifier tidak unik?
Jawaban: Identifier yang tidak unik dapat menyebabkan satu data dipasangkan dengan beberapa baris saat proses penggabungan. Akibatnya jumlah baris bisa bertambah secara tidak wajar, terjadi duplikasi informasi, dan hasil analisis dapat menjadi bias. Karena itu, keunikan identifier perlu diperiksa sebelum integrasi data.
Secara garis besar, alur preprocessing yang telah di lakukan meliputi:
Secara keseluruhan, preprocessing data bertujuan untuk membuat data menjadi lebih bersih, konsisten, dan siap digunakan untuk analisis. Proses ini mencakup penanganan berbagai masalah seperti data yang tidak sesuai, missing value, duplikasi, dan outlier. Setiap perubahan pada data perlu dilakukan dengan pertimbangan yang jelas agar informasi penting tetap terjaga dan hasil analisis dapat dipercaya.
Han, J., Kamber, M., & Pei, J. (2012). Data Mining: Concepts and Techniques (3rd ed.). Morgan Kaufmann. Chapter 3: Data Preprocessing.