Sistem basis data sebuah perusahaan e-commerce baru saja menarik dua berkas data mentah untuk kebutuhan analitik: Data Pelanggan (demografi dan finansial) serta Data Transaksi (aktivitas belanja).
Namun, data tersebut dalam kondisi sangat kotor. Terlihat penulisan ID yang tidak beraturan, entri duplikat, variasi ejaan nama kota, hingga angka usia dan pendapatan yang tidak masuk akal. Sebelum analisis segmentasi dapat dijalankan, kedua sumber data ini harus dibersihkan, diimputasi, dan diintegrasikan terlebih dahulu hingga menjadi satu dataset siap pakai.
# ---------------------------------------------------------
# 1. Dataset Pelanggan
# ---------------------------------------------------------
pelanggan_raw <- data.frame(
customer_id = c("C001", "C002", "C003", "C004", "C005", "C006", "C007", "C008", "C009", "C010",
"C010", "C011", "c013", "C-014", "C015 ", "C016", "C017", "C018", "C019", "C020",
"C021", "C022", "C023", "C024", "C025"),
nama = c("Ani", "Budi", "Citra", "Dodi", "Eka", "Fani", "Gilang", "Hana", "Indra", "Joko",
"Joko", "Kiki", "Lani", "Maulana", "Novi", "Omar", "Putri", "Qori", "Rian", "Sari",
"Tono", "Umar", "Vina", "Wawan", "Yulia"),
usia = c(21, 25, 23, 150, 27, NA, 31, 29, 22, 35,
35, 28, -5, 30, 999, 24, NA, 40, 19, 33,
26, 45, 22, NA, 30),
pendapatan = c(4500000, NA, 5200000, 4800000, 4900000, 5100000, 500000000, 4700000, 4600000, 5300000,
5300000, NA, 3800000, 6000000, NA, 4200000, 5500000, 1200000000, 3900000, 5100000,
4800000, NA, 4500000, 5000000, 0),
kota = c("Pekanbaru", " PKU", "PEKANBARU", "Dumai", "pekanbaru", "DUMAI", "Pekanbaru ", "Siak", "PKU", "Dumai",
"Dumai", NA, "Bangkinang", " BANGKINANG", "Bkn", "SIAK ", "Siak", "Pekanbaru", "dumai", "PKU",
"Bangkinang ", "Dumai", NA, "Pekanbaru", "PKU"),
status = c("Aktif", "aktif", "ACTIVE", "A", "Tidak Aktif", "nonaktif", "Aktif", "AKTIF", "A", "Tidak aktif",
"Tidak aktif", "Aktif", "Aktif", "N", "NON-AKTIF", "Aktif", " Pasif", "Aktif", "aktif", "Aktif",
"Non Aktif", "A", "Tidak Aktif", "AKTIF", NA),
stringsAsFactors = FALSE
)
# --------------------
# 2. Dataset Transaksi
# --------------------
transaksi_raw <- data.frame(
cust_id = c("C001", "C002", "C003", "C004", "C005", "C006", "C007", "C008", "C009", "C010",
"C012", "C013", "C014", "C015", "C016", "C099", "C100", "C019", "C020", "C020"),
jumlah_transaksi = c(5, 3, 7, 2, 6, 4, 20, 5, 3, 8,
1, 10, 2, 4, 99, 3, 1, 6, 2, 2),
total_purchase = c(1500000, 900000, 2700000, 600000, 2100000, 1300000, 25000000, 1700000, 800000, 3200000,
250000, 4500000, 850000, 1200000, 999999999, 1100000, 300000, 2200000, 750000, 750000),
stringsAsFactors = FALSE
)
pelanggan_rawSebelum melakukan perubahan, periksa struktur, dimensi, tipe atribut, dan beberapa observasi awal.
## [1] 25 6
## [1] "customer_id" "nama" "usia" "pendapatan" "kota"
## [6] "status"
## 'data.frame': 25 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:25 Length:25 Min. : -5.00 Min. :0.00e+00
## Class :character Class :character 1st Qu.: 23.25 1st Qu.:4.50e+06
## Mode :character Mode :character Median : 28.50 Median :4.90e+06
## Mean : 76.77 Mean :8.51e+07
## 3rd Qu.: 34.50 3rd Qu.:5.30e+06
## Max. :999.00 Max. :1.20e+09
## NA's :3 NA's :4
## kota status
## Length:25 Length:25
## Class :character Class :character
## Mode :character Mode :character
##
##
##
##
Enam dimensi kualitas data yang dibahas Han, Kamber, dan Pei meliputi accuracy, completeness, consistency, timeliness, believability, dan interpretability. Tidak semua dimensi dapat dihitung hanya dari isi tabel. Misalnya, accuracy memerlukan pembanding terhadap kondisi sebenarnya dan timeliness membutuhkan informasi waktu pemutakhiran.
Pemeriksaan awal berikut berfokus pada masalah yang dapat dideteksi dari dataset.
## customer_id nama usia pendapatan kota status
## 0 0 3 4 2 1
## customer_id nama usia pendapatan kota status
## 0 0 12 16 8 4
## [1] 1
## [1] 1
## [1] " BANGKINANG" " PKU" "Bangkinang" "Bangkinang " "Bkn"
## [6] "dumai" "Dumai" "DUMAI" "pekanbaru" "Pekanbaru"
## [11] "PEKANBARU" "Pekanbaru " "PKU" "Siak" "SIAK "
## [1] " Pasif" "A" "ACTIVE" "aktif" "Aktif"
## [6] "AKTIF" "N" "NON-AKTIF" "Non Aktif" "nonaktif"
## [11] "Tidak aktif" "Tidak Aktif"
## [1] -5 999
## [1] 0.0e+00 1.2e+09
# Cek string yang memiliki spasi di awal atau di akhir
sapply(pelanggan_raw[c("customer_id", "kota", "status")], function(x) {
sum(grepl("^\\s+|\\s+$", x))
})## customer_id kota status
## 1 5 1
# Deteksi angka 0 pada pendapatan (kemungkinan missing value terselubung)
sum(pelanggan_raw$pendapatan == 0, na.rm = TRUE)## [1] 1
# Deteksi string kosong ("")
sapply(pelanggan_raw, function(x) sum(x == "" | x == " ", na.rm = TRUE))## customer_id nama usia pendapatan kota status
## 0 0 0 0 0 0
# Cek customer_id yang tidak sesuai format standar "C000"
invalid_ids <- pelanggan_raw$customer_id[!grepl("^C[0-9]{3}$", pelanggan_raw$customer_id)]
invalid_ids## [1] "c013" "C-014" "C015 "
# Fungsi untuk menghitung jumlah outlier statistik per kolom numerik
sapply(pelanggan_raw[c("usia", "pendapatan")], function(x) {
q1 <- quantile(x, 0.25, na.rm = TRUE)
q3 <- quantile(x, 0.75, na.rm = TRUE)
iqr <- q3 - q1
sum(x < (q1 - 1.5 * iqr) | x > (q3 + 1.5 * iqr), na.rm = TRUE)
})## usia pendapatan
## 3 3
# Jumlah transaksi yang ID-nya TIDAK ADA di data pelanggan
sum(!transaksi_raw$cust_id %in% pelanggan_raw$customer_id)## [1] 6
# Menampilkan ID mana saja yang bermasalah (mismatch)
setdiff(transaksi_raw$cust_id, pelanggan_raw$customer_id)## [1] "C012" "C013" "C014" "C015" "C099" "C100"
# Ringkasan profil data
data.frame(
Tipe_Data = sapply(pelanggan_raw, class),
Jumlah_NA = colSums(is.na(pelanggan_raw)),
Jumlah_Unik = sapply(pelanggan_raw, function(x) length(unique(x)))
)# =========================================================
# 1. FUNGSI AUDIT DATASET KOMPREHENSIF (Pengembangan Fungsi Kamu)
# =========================================================
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))),
# Deteksi Spasi Liar di awal/akhir string (khusus tipe karakter)
ada_spasi_liar = sapply(data, function(x) {
if(is.character(x)) sum(grepl("^\\s+|\\s+$", x), na.rm = TRUE) else 0
}),
# Deteksi Angka 0 (Missing Value terselubung pada kolom numerik)
jumlah_nol = sapply(data, function(x) {
if(is.numeric(x)) sum(x == 0, na.rm = TRUE) else 0
}),
# Deteksi Outlier Berdasarkan Metode IQR (khusus numerik)
jumlah_outlier = sapply(data, function(x) {
if(is.numeric(x)) {
q1 <- quantile(x, 0.25, na.rm = TRUE)
q3 <- quantile(x, 0.75, na.rm = TRUE)
iqr <- q3 - q1
sum(x < (q1 - 1.5 * iqr) | x > (q3 + 1.5 * iqr), na.rm = TRUE)
} else {
NA
}
}),
row.names = NULL
)
}
# Eksekusi audit awal pada dataset pelanggan
audit_pelanggan <- audit_data(pelanggan_raw)
audit_pelanggan# =========================================================
# 2. AUDIT KHUSUS: VALIDASI FORMAT ID & INTEGRITAS JOIN
# =========================================================
# A. Cek ID yang tidak memenuhi format standar "C000" (Regex Audit)
invalid_ids <- pelanggan_raw$customer_id[!grepl("^C[0-9]{3}$", pelanggan_raw$customer_id)]
cat("Format ID Tidak Standar:\n")## Format ID Tidak Standar:
## [1] "c013" "C-014" "C015 "
# B. Cek Duplikasi Baris dan Duplikasi ID
cat("\nJumlah Baris Duplikat Penuh :", sum(duplicated(pelanggan_raw)))##
## Jumlah Baris Duplikat Penuh : 1
##
## Jumlah ID yang Berulang : 1
# C. Audit Integrasi Antar Tabel (Cross-Table Integrity Test)
mismatch_ids <- setdiff(transaksi_raw$cust_id, pelanggan_raw$customer_id)
cat("\nID Transaksi yang Tidak Ada di Tabel Pelanggan (Orphan Records):\n")##
## ID Transaksi yang Tidak Ada di Tabel Pelanggan (Orphan Records):
## [1] "C012" "C013" "C014" "C015" "C099" "C100"
Interpretasi: Hasil audit data itu seperti sedang mengecek kesehatan data sebelum diobati. Jadi, audit hanya bertugas membuat dan memberi tahu bagian mana yang error, tapi tidak langsung otomatis ngubah atau ngehapus datanya. Kenapa? karena setiap masalah punya cara penanganan yang beda tergantung konteksnya.
Dari hasil audit, kita jadi tahu tindakan apa yang pas untuk diambil. Jika kelihatan ada nilai yang hilang di kolom usia atau pendapatan, kita bisa berpikir apakah datanya mau dibuang atau diisi menggunakan nilai tengah (median). jika bertemu ID yang serupa, kita tahu data mana yang harus dihapus atau dijumlahkan. Selanjutnya, jika ada angka aneh seperti usia minus atau ratusan tahun, audit ngebuat kita sadar buat ngecek ulang berkas aslinya atau ngubah angka itu jadi data kosong dulu.
Pertahankan data mentah agar setiap perubahan dapat dilacak.
# 1. Mencegah error duplikasi variabel dengan menyalin dataset asli
pelanggan_clean <- pelanggan_raw
# 2. Menghapus spasi liar di awal dan akhir pada SEMUA kolom berbasis karakter
pelanggan_clean[] <- lapply(pelanggan_clean, function(x) {
if (is.character(x)) trimws(x) else x
})
# 3. Standardisasi Format ID (Kapitalisasi "c" -> "C" dan hapus tanda strip "-")
pelanggan_clean$customer_id <- toupper(pelanggan_clean$customer_id)
pelanggan_clean$customer_id <- gsub("-", "", pelanggan_clean$customer_id)
# 4. Standardisasi Nama Kota (Format Title Case & Pemetaan Singkatan)
pelanggan_clean$kota <- tolower(pelanggan_clean$kota)
pelanggan_clean$kota <- ifelse(pelanggan_clean$kota %in% c("pku", "bkn u"), "pekanbaru", pelanggan_clean$kota)
pelanggan_clean$kota <- ifelse(pelanggan_clean$kota %in% c("bkn"), "bangkinang", pelanggan_clean$kota)
# Mengubah huruf pertama menjadi Kapital (Title Case)
library(tools)
pelanggan_clean$kota <- toTitleCase(pelanggan_clean$kota)
# 5. Standardisasi Status (Konversi Berbagai Sinonim menjadi "Aktif" / "Tidak Aktif")
pelanggan_clean$status <- tolower(pelanggan_clean$status)
pelanggan_clean$status <- ifelse(pelanggan_clean$status %in% c("active", "a", "aktif"), "Aktif", pelanggan_clean$status)
pelanggan_clean$status <- ifelse(pelanggan_clean$status %in% c("nonaktif", "non-aktif", "non aktif", "pasif", "n", "tidak aktif"), "Tidak Aktif", pelanggan_clean$status)
# 6. Pengecekan Hasil Kategori yang Sudah Bersih
cat("=== Kategori Kota Setelah Dibersihkan ===\n")## === Kategori Kota Setelah Dibersihkan ===
## [1] "Bangkinang" "Dumai" "Pekanbaru" "Siak"
##
## === Kategori Status Setelah Dibersihkan ===
## [1] "Aktif" "Tidak Aktif"
##
## === Format ID Setelah Dibersihkan ===
## [1] "C001" "C002" "C003" "C004" "C005" "C006" "C007" "C008" "C009" "C010"
## [11] "C011" "C013" "C014" "C015" "C016" "C017" "C018" "C019" "C020" "C021"
## [21] "C022" "C023" "C024" "C025"
## [1] " Pasif" "A" "ACTIVE" "aktif" "Aktif"
## [6] "AKTIF" "N" "NON-AKTIF" "Non Aktif" "nonaktif"
## [11] "Tidak aktif" "Tidak Aktif"
Setiap data yang ditarik dari sistem yang berbeda pasti punya masalah inkonsistensi penulisan, beda format, atau beda standar input. Sebelum data bisa dianalisis, kita wajib melakukan standardisasi kategori supaya nilai-nilai yang maksudnya sama tidak terbaca sebagai kategori yang berbeda oleh komputer. Penyeragaman ini kita lakukan berdasarkan aturan domain (business rules).
# ---------------------------------------------------------
# 1. Menghapus Spasi Liar Terlebih Dahulu
# ---------------------------------------------------------
pelanggan$kota <- trimws(pelanggan$kota)
pelanggan$status <- trimws(pelanggan$status)
# Menyeragamkan menjadi huruf kecil semua untuk memudahkan pengelompokan
pelanggan$kota <- tolower(pelanggan$kota)
pelanggan$status <- tolower(pelanggan$status)
# ---------------------------------------------------------
# 2. Standardisasi Kota (Pekanbaru, Dumai, Siak, Bangkinang)
# ---------------------------------------------------------
pelanggan$kota[pelanggan$kota %in% c("pku", "pekanbaru")] <- "Pekanbaru"
pelanggan$kota[pelanggan$kota %in% c("dumai")] <- "Dumai"
pelanggan$kota[pelanggan$kota %in% c("siak")] <- "Siak"
pelanggan$kota[pelanggan$kota %in% c("bangkinang", "bkn")] <- "Bangkinang"
# ---------------------------------------------------------
# 3. Standardisasi Status (Menutupi Seluruh Variasi Status)
# ---------------------------------------------------------
pelanggan$status[pelanggan$status %in% c("aktif", "active", "a")] <- "Aktif"
pelanggan$status[pelanggan$status %in% c("tidak aktif", "nonaktif", "non-aktif", "non aktif", "pasif", "n")] <- "Tidak Aktif"
# ---------------------------------------------------------
# 4. Pengecekan Hasil Kategori yang Sudah Seragam
# ---------------------------------------------------------
sort(unique(pelanggan$kota))## [1] "Bangkinang" "Dumai" "Pekanbaru" "Siak"
## [1] "Aktif" "Tidak Aktif"
# Menampilkan seluruh baris dengan customer_id yang berulang
pelanggan[duplicated(pelanggan$customer_id) |
duplicated(pelanggan$customer_id, fromLast = TRUE), ]# ---------------------------------------------------------
# 1. Preprocessing Format ID (Wajib dilakukan sebelum cek duplikat)
# ---------------------------------------------------------
pelanggan$customer_id <- trimws(pelanggan$customer_id)
pelanggan$customer_id <- toupper(pelanggan$customer_id)
pelanggan$customer_id <- gsub("-", "", pelanggan$customer_id)
# ---------------------------------------------------------
# 2. Menampilkan Seluruh Baris dengan customer_id yang Berulang
# ---------------------------------------------------------
# Melihat data mana saja yang memiliki duplikasi ID (termasuk C010)
duplikat_id <- pelanggan[duplicated(pelanggan$customer_id) |
duplicated(pelanggan$customer_id, fromLast = TRUE), ]
duplikat_id# ---------------------------------------------------------
# 3. Mempertahankan Kemunculan Pertama dan Menghapus Duplikat
# ---------------------------------------------------------
pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
# Reset indeks baris agar rapi kembali
rownames(pelanggan) <- NULL
# ---------------------------------------------------------
# 4. Pengecekan Dimensi Data Setelah Duplikat Dibuang
# ---------------------------------------------------------
# Dimensi awal 25 baris akan berkurang menjadi 24 baris (karena C010 ganda)
dim(pelanggan)## [1] 24 6
Menghapus duplikasi hanya aman dilakukan jika satu customer_id memang mewakili satu profil pelanggan. Jika satu pelanggan diperbolehkan memiliki lebih dari satu baris entri (seperti pada riwayat transaksi berulang), tindakan penghapusan justru akan menghilangkan informasi penting dan merusak integritas data.
# ---------------------------------------------------------
# 1. MENDETEKSI DATA TIDAK VALID (Berdasarkan Aturan Domain)
# ---------------------------------------------------------
# Memeriksa usia yang tidak valid (di luar rentang 15–100 tahun)
pelanggan[!is.na(pelanggan$usia) & (pelanggan$usia < 15 | pelanggan$usia > 100), ]# Memeriksa pendapatan yang bernilai negatif (jika ada)
pelanggan[!is.na(pelanggan$pendapatan) & pelanggan$pendapatan < 0, ]# ---------------------------------------------------------
# 2. KOREKSI DATA TIDAK VALID (Aturan Bisnis & Konversi NA)
# ---------------------------------------------------------
# Kasus 1 (C004): Usia 150 adalah salah ketik dari 50 (berdasarkan verifikasi formulir)
pelanggan$usia[pelanggan$customer_id == "C004"] <- 50
# Kasus 2 (C013): Usia -5 adalah error input, diubah menjadi NA agar diimputasi nanti
pelanggan$usia[pelanggan$customer_id == "C013"] <- NA
# Kasus 3 (C015): Usia 999 adalah kode dummy input, diubah menjadi NA
pelanggan$usia[pelanggan$customer_id == "C015"] <- NA
# ---------------------------------------------------------
# 3. VERIFIKASI HASIL KOREKSI
# ---------------------------------------------------------
# Memastikan tidak ada lagi usia di luar rentang wajar (15-100 tahun)
pelanggan[!is.na(pelanggan$usia) & (pelanggan$usia < 15 | pelanggan$usia > 100), ]## [1] 19 50
log_perubahan <- data.frame(
tahu_proses = c(
"Standardisasi",
"Standardisasi",
"Standardisasi",
"Deduplikasi",
"Koreksi Domain",
"Koreksi Domain"
),
atribut = c(
"customer_id",
"kota",
"status",
"customer_id",
"usia",
"usia"
),
tindakan = c(
"Menghapus spasi/strip, konversi ke kapital (misal: c013, C-014 -> C013, C014)",
"Penanganan spasi liar, konversi ke Title Case, pemetaan singkatan (PKU, Bkn -> Pekanbaru, Bangkinang)",
"Penanganan spasi liar, penyelarasan sinonim (ACTIVE/A -> Aktif; nonaktif/N/Pasif -> Tidak Aktif)",
"Menghapus baris duplikat penuh pada ID C010 (kemunculan kedua)",
"Usia C004 dikoreksi dari 150 menjadi 50 berdasarkan sumber asli",
"Usia C013 (-5) dan C015 (999) dikonversi menjadi NA karena nilai tidak valid"
),
stringsAsFactors = FALSE
)
# Menampilkan log perubahan
log_perubahan# ---------------------------------------------------------
# 1. Pengecekan Ringkas Jumlah Missing Value per Atribut
# ---------------------------------------------------------
colSums(is.na(pelanggan))## customer_id nama usia pendapatan kota status
## 0 0 5 4 2 1
## customer_id nama usia pendapatan kota status
## 0.00 0.00 20.83 16.67 8.33 4.17
# ---------------------------------------------------------
# 2. Menampilkan Seluruh Baris yang Memiliki Minimal Satu Missing Value
# ---------------------------------------------------------
# Menampilkan baris yang tidak lengkap (incomplete cases)
pelanggan[!complete.cases(pelanggan), ]# ---------------------------------------------------------
# 3. Analisis Spesifik Missing Value per Kolom Utama
# ---------------------------------------------------------
# A. Pelanggan dengan Usia yang Hilang (NA)
# Termasuk C006, C013, C015, C017, C024
pelanggan[is.na(pelanggan$usia), c("customer_id", "nama", "usia")]# B. Pelanggan dengan Pendapatan yang Hilang (NA)
# Termasuk C002, C011, C015, C022
pelanggan[is.na(pelanggan$pendapatan), c("customer_id", "nama", "pendapatan")]# C. Pelanggan dengan Kota atau Status yang Hilang (NA)
pelanggan[is.na(pelanggan$kota) | is.na(pelanggan$status), ]Sebelum melakukan imputasi, ukur seberapa banyak informasi yang hilang jika menggunakan opsi ekstrem (listwise deletion).
# Membuat subset data lengkap
pelanggan_complete <- pelanggan[complete.cases(pelanggan), ]
# Menampilkan statistik pengurangan baris
cat("Jumlah baris awal :", nrow(pelanggan), "\n")## Jumlah baris awal : 24
## Jumlah baris lengkap: 14
# Persentase data yang hilang jika baris yang tidak lengkap dihapus
persen_hilang <- round((1 - nrow(pelanggan_complete) / nrow(pelanggan)) * 100, 2)
cat("Persentase data terbuang:", persen_hilang, "%\n")## Persentase data terbuang: 41.67 %
## [1] 41.67
Karena pendapatan memiliki outlier ekstrem (seperti 500 juta & 1.2 miliar), median lebih resisten dibandingkan mean.
mean_pendapatan <- mean(pelanggan$pendapatan, na.rm = TRUE)
median_pendapatan <- median(pelanggan$pendapatan, na.rm = TRUE)
mean_usia <- mean(pelanggan$usia, na.rm = TRUE)
median_usia <- median(pelanggan$usia, na.rm = TRUE)
cat("Pendapatan - Mean:", mean_pendapatan, "| Median:", median_pendapatan, "\n")## Pendapatan - Mean: 89095000 | Median: 4850000
## Usia - Mean: 29.47368 | Median: 28
# Imputasi Pendapatan dengan Median
pelanggan$pendapatan_imputasi <- pelanggan$pendapatan
pelanggan$pendapatan_imputasi[is.na(pelanggan$pendapatan_imputasi)] <- median_pendapatan
# Imputasi Usia dengan Median
pelanggan$usia_imputasi <- pelanggan$usia
pelanggan$usia_imputasi[is.na(pelanggan$usia_imputasi)] <- median_usiaMenjaga transparansi data tanpa mengasumsi kota atau status
pelanggan. Nilai kota yang hilang tidak selalu tepat diisi dengan modus.
Dalam hal ini digunakan kategori
eksplisit Tidak diketahui agar ketidakpastian tidak
disembunyikan.
Praktik ini menjaga informasi asli untuk algoritma machine learning bahwa nilai tersebut sebenarnya hasil rekaan/imputasi.
pelanggan$pendapatan_is_na <- as.integer(is.na(pelanggan$pendapatan))
pelanggan$usia_is_na <- as.integer(is.na(pelanggan$usia))
pelanggan$kota_is_na <- as.integer(is.na(pelanggan$kota))
# Menampilkan distribusi indikator
table(pelanggan$pendapatan_is_na)##
## 0 1
## 20 4
Visualisasi dua panel untuk mengevaluasi pergeseran distribusi nilai sebelum dan sesudah dilakukan imputasi.
# Visualisasi Distribusi Pendapatan (Log Scale agar Outlier Terlihat Rapi)
par(mfrow = c(1, 2))
hist(log10(pelanggan$pendapatan),
main = "Sebelum Imputasi (Log10)",
xlab = "Log10(Pendapatan)", col = "brown", breaks = 8)
hist(log10(pelanggan$pendapatan_imputasi),
main = "Sesudah Imputasi Median",
xlab = "Log10(Pendapatan)", col = "navy", breaks = 8)Imputasi dapat mengubah distribusi dan mengecilkan variasi. Karena itu, evaluasi tidak berhenti setelah semua
NAhilang.
# Visualisasi awal untuk mendeteksi outlier pada variabel pendapatan
boxplot(pelanggan$pendapatan_imputasi,
horizontal = TRUE,
col = "brown",
main = "Boxplot Pendapatan Pelanggan",
xlab = "Pendapatan (Rupiah)")# Menghitung Kuartil 1, Kuartil 3, dan IQR
q1 <- quantile(pelanggan$pendapatan_imputasi, 0.25, na.rm = TRUE)
q3 <- quantile(pelanggan$pendapatan_imputasi, 0.75, na.rm = TRUE)
iqr <- IQR(pelanggan$pendapatan_imputasi, na.rm = TRUE)
# Menentukan batas bawah dan batas atas toleransi
batas_bawah <- q1 - 1.5 * iqr
batas_atas <- q3 + 1.5 * iqr
# Menampilkan hasil perhitungan batas
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%
## 4575000 5125000 550000 3750000 5950000
# Menandai baris yang tergolong outlier
pelanggan$outlier_pendapatan <- pelanggan$pendapatan_imputasi < batas_bawah |
pelanggan$pendapatan_imputasi > batas_atas
# Menampilkan daftar pelanggan yang terdeteksi memiliki pendapatan outlier
pelanggan[pelanggan$outlier_pendapatan,
c("customer_id", "nama", "pendapatan_imputasi")]Berdasarkan perhitungan statistik, terdeteksi dua nilai pendapatan ekstrem pada dataset 25 pelanggan tersebut, yaitu 500 juta rupiah pada pelanggan C007 (baris ke-7) dan 1,2 miliar rupiah pada pelanggan C018 (baris ke-18). Kedua angka ini berada jauh di atas batas toleransi IQR. Namun, nilai tersebut tidak serta-merta langsung dihapus. Terdapat tiga kemungkinan yang perlu dipertimbangkan:
Kesalahan Input (Data Entry Error): Angka tersebut dikoreksi apabila hasil verifikasi dokumen menunjukkan adanya kesalahan pengetikan (misalnya kelebihan memasukkan angka nol).
Observasi Valid tetapi Ekstrem: Jika angka tersebut memang riil (misalnya pelanggan segmen High Net-Worth Individual), nilai tetap dipertahankan atau ditangani dengan pendekatan robust / winsorizing.
Populasi Berbeda: Apabila pelanggan tersebut berasal dari segmen korporasi yang tidak sengaja tercampur dengan pelanggan ritel, data dipisahkan ke dalam kelompok analisis tersendiri.
Pada modul ini, nilai ekstrem tersebut tetap dipertahankan pada variabel asli dan dibuatkan variabel versi winsorized sebagai bahan komparasi dampak metode terhadap distribusi data.
Membatasi nilai ekstrem agar tidak melebihi batas statistik atas/bawah tanpa membuang baris data (mencegah kehilangan informasi).
# Membatasi nilai pendapatan maksimum setara nilai batas atas IQR
pelanggan$pendapatan_winsor <- pmin(
pmax(pelanggan$pendapatan_imputasi, batas_bawah),
batas_atas
)
# Menampilkan perbandingan sebelum dan sesudah Winsorizing
pelanggan[pelanggan$outlier_pendapatan,
c("customer_id", "pendapatan_imputasi", "pendapatan_winsor")]Mengubah skala data menggunakan fungsi logaritma Sangat disarankan untuk meredam rentang nilai outlier secara alami tanpa harus mengubah nilai riil data.
# Transformasi logaritma berbasis 10 (ditambah 1 untuk mencegah error log(0))
pelanggan$pendapatan_log <- log10(pelanggan$pendapatan_imputasi + 1)
# Menampilkan hasil transformasi logaritma pada data outlier
pelanggan[pelanggan$outlier_pendapatan,
c("customer_id", "pendapatan_imputasi", "pendapatan_log")]Membandingkan skenario data asli vs winsorized vs log transformation secara visual untuk melihat perubahan sebaran data.
# Pengaturan grid 1 baris 3 kolom
par(mfrow = c(1, 3))
# 1. Boxplot Data Imputasi Asli
boxplot(pelanggan$pendapatan_imputasi,
main = "1. Imputasi Asli",
col = "skyblue", ylab = "Rupiah")
# 2. Boxplot Hasil Winsorizing
boxplot(pelanggan$pendapatan_winsor,
main = "2. Hasil Winsorizing",
col = "yellow", ylab = "Rupiah")
# 3. Boxplot Hasil Transformasi Log
boxplot(pelanggan$pendapatan_log,
main = "3. Transformasi Log10",
col = "orange", ylab = "Log10 Scale")Mengubah skala numerik ke dalam rentang terikat antara 0 dan 1
# Fungsi kustom untuk Min-Max Scaling
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
}
# Penerapan pada atribut usia dan pendapatan
pelanggan$usia_minmax <- minmax(pelanggan$usia_imputasi)
pelanggan$pendapatan_minmax <- minmax(pelanggan$pendapatan_imputasi)
# Menampilkan hasil transformasi Min-Max
pelanggan[, c("customer_id", "usia_imputasi", "usia_minmax",
"pendapatan_imputasi", "pendapatan_minmax")]Mengubah skala data berdistribusi normal dengan mengukur seberapa jauh jarak suatu nilai dari rata-ratanya.
\[z = \frac{x - \bar{x}}{s}\]
# Menggunakan fungsi bawaan R: scale()
pelanggan$usia_z <- as.numeric(scale(pelanggan$usia_imputasi))
pelanggan$pendapatan_z <- as.numeric(scale(pelanggan$pendapatan_imputasi))
# Menampilkan hasil transformasi Z-Score (dibulatkan 3 desimal)
round(pelanggan[, c("usia_z", "pendapatan_z")], 3)Menggeser koma desimal berdasarkan nilai absolut maksimum.
# Fungsi kustom untuk Decimal Scaling
decimal_scale <- function(x) {
maks <- max(abs(x), na.rm = TRUE)
if (maks == 0) return(x)
j <- ceiling(log10(maks + 1))
x / (10 ^ j)
}
# Penerapan pada atribut pendapatan
pelanggan$pendapatan_decimal <- decimal_scale(pelanggan$pendapatan_imputasi)
# Menampilkan rentang hasil transformasi
range(pelanggan$pendapatan_decimal, na.rm = TRUE)## [1] 0.00 0.12
Menggunakan Median dan IQR menggantikan Mean dan Standar Deviasi (\(\frac{x - \text{median}}{\text{IQR}}\)). Sangat disarankan jika data mengandung outlier tinggi karena tidak mudah terdistorsi.
# Fungsi kustom untuk Robust Scaler
robust_scale <- function(x) {
med <- median(x, na.rm = TRUE)
iqr_val <- IQR(x, na.rm = TRUE)
if (iqr_val == 0) return(x - med)
(x - med) / iqr_val
}
# Penerapan Robust Scaling pada pendapatan
pelanggan$pendapatan_robust <- robust_scale(pelanggan$pendapatan_imputasi)
# Menampilkan perbandingan Z-score vs Robust Scaling pada data outlier
pelanggan[pelanggan$outlier_pendapatan, c("customer_id", "pendapatan_z", "pendapatan_robust")]Menyajikan perbandingan nilai hasil dari seluruh metode scaling yang telah diterapkan pada variabel pendapatan.
# Ringkasan seluruh metode skalar dalam satu data frame
transformasi_ringkasan <- pelanggan[, c(
"customer_id", "pendapatan_imputasi", "pendapatan_minmax",
"pendapatan_z", "pendapatan_decimal", "pendapatan_robust"
)]
# Menampilkan 10 baris pertama
head(transformasi_ringkasan, 10)Menguji sejauh mana penanganan outlier (Metode Winsorized) mengubah hasil distribusi skala Min–Max.
# Melakukan Min-Max scaling pada data yang sudah di-Winsorized
pelanggan$pendapatan_winsor_minmax <- minmax(pelanggan$pendapatan_winsor)
# Plot perbandingan Min-Max data asli vs data Winsorized
plot(pelanggan$pendapatan_minmax,
pelanggan$pendapatan_winsor_minmax,
pch = 19, col = "navy",
xlab = "Min–Max Data Asli (Dengan Outlier)",
ylab = "Min–Max Data Winsorized (Outlier Ditangani)",
main = "Dampak Penanganan Outlier terhadap Normalisasi")
# Garis acuan diagonal
abline(0, 1, col = "red", lty = 2)Interpretasi Analisis Nilai ekstrem outlier yang belum ditangani akan membuat sebagian besar data lain menumpuk di rentang angka yang sangat sempit saat diubah ke skala min-maks. Oleh karena itu, outlier perlu diidentifikasi dan ditangani terlebih dahulu sebelum menentukan metode transformasi yang tepat.
Sebelum melakukan merge, data transaksi harus diselaraskan format ID-nya dan diperiksa duplikasi ID-nya agar tidak memicu perkalian baris (cartesian join) yang tidak diinginkan.
# 1. Menyelaraskan nama & format ID pada data transaksi
transaksi <- transaksi_raw
names(transaksi)[names(transaksi) == "cust_id"] <- "customer_id"
# Standardisasi format string ID
transaksi$customer_id <- trimws(transaksi$customer_id)
transaksi$customer_id <- toupper(transaksi$customer_id)
transaksi$customer_id <- gsub("-", "", transaksi$customer_id)
# 2. Menangani Duplikasi ID pada Data Transaksi (Agregasi)
# Karena C020 muncul 2x di data transaksi, kita aggregasikan agar 1 ID = 1 Baris
transaksi_clean <- aggregate(
cbind(jumlah_transaksi, total_purchase) ~ customer_id,
data = transaksi,
FUN = sum
)
# 3. Pemeriksaan Kunci Antar Tabel
cat("Duplikat ID Pelanggan :", sum(duplicated(pelanggan$customer_id)), "\n")## Duplikat ID Pelanggan : 0
## Duplikat ID Transaksi : 0
# Cek selisih ID antar dua dataset
cat("Pelanggan tanpa transaksi :", length(setdiff(pelanggan$customer_id, transaksi_clean$customer_id)), "\n")## Pelanggan tanpa transaksi : 8
cat("Transaksi tanpa pelanggan :", length(setdiff(transaksi_clean$customer_id, pelanggan$customer_id)), "\n")## Transaksi tanpa pelanggan : 3
Menggabungkan data utama pelanggan dengan data agregasi transaksi.
# A. Left Join (Mempertahankan seluruh pelanggan meskipun belum pernah bertransaksi)
data_terintegrasi <- merge(
pelanggan,
transaksi_clean,
by = "customer_id",
all.x = TRUE
)
# Menampilkan sampel data terintegrasi
head(data_terintegrasi[, c("customer_id", "nama", "jumlah_transaksi", "total_purchase")], 10)Nilai NA pada kolom transaksi setelah left join dapat diimputasi dengan nilai 0 apabila diasumsikan pelanggan tersebut memang belum melakukan transaksi.
# Duplikasi kolom untuk memelihara trace data asli
data_terintegrasi$jumlah_transaksi_final <- data_terintegrasi$jumlah_transaksi
data_terintegrasi$total_purchase_final <- data_terintegrasi$total_purchase
# Mengisi NA hasil join dengan nilai 0
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
# Verifikasi penanganan NA
colSums(is.na(data_terintegrasi[, c("jumlah_transaksi_final", "total_purchase_final")]))## jumlah_transaksi_final total_purchase_final
## 0 0
Keberadaan nilai NA pada variabel transaksi tidak bisa langsung disimpulkan sebagai angka nol tanpa adanya konfirmasi aturan bisnis. Secara teoretis, Little & Rubin (2019) menegaskan pentingnya memahami mekanisme di balik nilai hilang—apakah bersifat Missing at Random (MAR) atau Missing Not at Random (MNAR)—sebelum menentukan metode penanganannya. Secara teknis dan operasional, kondisi NA pada data transaksi ini mengandung dua kemungkinan interpretasi yang bertolak belakang:
Aktivitas Riil Nol: Pelanggan memang terdaftar di basis data profil, namun secara faktual belum pernah melakukan transaksi sama sekali sejak akun dibuat.
Kegagalan Integrasi Data (Data Unmatched / Missing Information): Pelanggan sebenarnya sudah pernah bertransaksi, tetapi catatannya tidak ditemukan akibat perbedaan format ID saat proses penggabungan (join), atau data tersebut hilang dari sistem pencatatan transaksi.
Pengubahan nilai NA menjadi angka 0 hanya sah dilakukan apabila sudah dikonfirmasi melalui kamus data atau aturan bisnis bahwa tidak ditemukannya riwayat belanja secara eksplisit mendefinisikan ketiadaan aktivitas transaksi. Mengisi NA secara sembarangan tanpa validasi berisiko memalsukan profil pelanggan dan menghasilkan bias pada analisis statistik selanjutnya.
Memilih dan merapikan kolom-kolom hasil pembersihan yang akan digunakan untuk pemodelan/analisis tingkat lanjut.
# Memilih atribut akhir yang bersih
data_final <- data_terintegrasi[, c(
"customer_id", "nama", "usia_imputasi", "kota_imputasi", "status",
"pendapatan_imputasi", "pendapatan_is_na", "outlier_pendapatan",
"usia_minmax", "pendapatan_minmax", "pendapatan_z", "pendapatan_robust",
"jumlah_transaksi_final", "total_purchase_final"
)]
# Merapikan nama kolom
names(data_final)[names(data_final) == "kota_imputasi"] <- "kota"
names(data_final)[names(data_final) == "usia_imputasi"] <- "usia"
names(data_final)[names(data_final) == "pendapatan_imputasi"] <- "pendapatan"
# Menampilkan struktur dataset akhir
str(data_final)## 'data.frame': 24 obs. of 14 variables:
## $ customer_id : chr "C001" "C002" "C003" "C004" ...
## $ nama : chr "Ani" "Budi" "Citra" "Dodi" ...
## $ usia : num 21 25 23 50 27 28 31 29 22 35 ...
## $ kota : chr "Pekanbaru" "Pekanbaru" "Pekanbaru" "Dumai" ...
## $ status : chr "Aktif" "Aktif" "Aktif" "Aktif" ...
## $ pendapatan : num 4.50e+06 4.85e+06 5.20e+06 4.80e+06 4.90e+06 5.10e+06 5.00e+08 4.70e+06 4.60e+06 5.30e+06 ...
## $ pendapatan_is_na : int 0 1 0 0 0 0 0 0 0 0 ...
## $ outlier_pendapatan : logi FALSE FALSE FALSE FALSE FALSE FALSE ...
## $ usia_minmax : num 0.0645 0.1935 0.129 1 0.2581 ...
## $ pendapatan_minmax : num 0.00375 0.00404 0.00433 0.004 0.00408 ...
## $ pendapatan_z : num -0.271 -0.27 -0.269 -0.27 -0.27 ...
## $ pendapatan_robust : num -0.6364 0 0.6364 -0.0909 0.0909 ...
## $ jumlah_transaksi_final: num 5 3 7 2 6 4 20 5 3 8 ...
## $ total_purchase_final : num 1.5e+06 9.0e+05 2.7e+06 6.0e+05 2.1e+06 1.3e+06 2.5e+07 1.7e+06 8.0e+05 3.2e+06 ...
# 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)
] <- 0Mengukur peningkatan kualitas data sebelum dan sesudah seluruh rantai data preprocessing dijalankan.
# 1. Audit akhir menggunakan fungsi audit_data
audit_akhir <- audit_data(data_final)
print(audit_akhir)## atribut tipe jumlah_missing persen_missing jumlah_unik
## 1 customer_id character 0 0.00 24
## 2 nama character 0 0.00 24
## 3 usia numeric 0 0.00 17
## 4 kota character 0 0.00 5
## 5 status character 1 4.17 3
## 6 pendapatan numeric 0 0.00 18
## 7 pendapatan_is_na integer 0 0.00 2
## 8 outlier_pendapatan logical 0 0.00 2
## 9 usia_minmax numeric 0 0.00 17
## 10 pendapatan_minmax numeric 0 0.00 18
## 11 pendapatan_z numeric 0 0.00 18
## 12 pendapatan_robust numeric 0 0.00 18
## 13 jumlah_transaksi_final numeric 0 0.00 11
## 14 total_purchase_final numeric 0 0.00 16
## ada_spasi_liar jumlah_nol jumlah_outlier
## 1 0 0 NA
## 2 0 0 NA
## 3 0 0 3
## 4 0 0 NA
## 5 0 0 NA
## 6 0 1 4
## 7 0 20 4
## 8 0 0 NA
## 9 0 1 3
## 10 0 1 4
## 11 0 0 4
## 12 0 4 4
## 13 0 8 2
## 14 0 8 2
# 2. Tabel Ringkasan Komparasi Sebelum vs Sesudah
perbandingan <- data.frame(
Indikator = c(
"Jumlah Baris Data",
"Duplikasi customer_id",
"Total Missing Value (NA)",
"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))
)
)
print(perbandingan)## Indikator Sebelum Sesudah
## 1 Jumlah Baris Data 25 24
## 2 Duplikasi customer_id 1 0
## 3 Total Missing Value (NA) 10 1
## 4 Kategori Kota Unik 16 5
## 5 Kategori Status Unik 13 3
Dataset tanpa missing value tidak otomatis lebih berkualitas apabila proses pengisiannya dilakukan secara sembarangan. Mengisi nilai yang hilang dengan teknik yang salah—seperti langsung mengganti NA menjadi nol atau nilai rata-rata tanpa memahami konteks bisnis—justru dapat merusak distribusi asli, menutupi pola informasi penting, dan menyuntikkan informasi palsu yang tidak mencerminkan fakta lapangan.
Outlier tidak boleh otomatis dihapus karena nilai ekstrem tersebut belum tentu merupakan kesalahan input data. Dalam banyak kasus, outlier merupakan observasi yang sepenuhnya valid dan merepresentasikan fakta nyata di dunia bisnis, seperti transaksi bernilai sangat tinggi dari pelanggan segmen High Net-Worth Individual atau transaksi korporasi. Menghapus nilai ekstrem berisiko menghilangkan informasi penting mengenai segmen pasar dan dapat mengurangi daya generalisasi model terhadap kondisi nyata.
Preprocessing dapat menimbulkan bias apabila tindakan pembersihan data justru mengubah atau membelokkan statistik dasar dari populasi aslinya. Contohnya terjadi ketika data hilang (missing value) dihapus secara listwise padahal pola hilangnya data bersifat sistematis (Missing Not at Random), yang mengakibatkan kelompok sampel tertentu terbuang dan sisa data tidak lagi representatif. Begitu pula saat imputasi data dilakukan menggunakan asumsi yang salah, hal ini akan memperkecil varians data secara artifisial dan memicu kesimpulan analisis yang menyesatkan.
Parameter imputasi dan transformasi—seperti nilai median untuk pengisian data, serta nilai minimum, maksimum, rata-rata, atau standar deviasi untuk pemrosesan skala—seharusnya dihitung khusus dari data pelatihan (train set) saja untuk mencegah masalah data leakage. Jika parameter ini dihitung dari seluruh dataset (termasuk test set), informasi dari data pengujian secara tidak sengaja telah “bocor” ke dalam proses pelatihan. Akibatnya, evaluasi performa model menjadi terlalu optimis (overfitted) dan model gagal memberikan performa yang konsisten saat diuji dengan data baru yang benar-benar belum pernah dilihat.
Risiko utama integrasi data apabila identifier atau primary key tidak unik adalah terciptanya duplikasi fiktif (cartesian join) atau salah pemadanan entitas (misalignment) saat proses merge dijalankan. Ketika satu ID pelanggan muncul berkali-kali di tabel profil dengan data yang bertentangan, penggabungan data akan memicu lonjakan jumlah baris secara tidak wajar, menggelembungkan angka statistik transaksi, dan merusak integritas seluruh analisis turunan seperti pemetaan profil atau analisis perilaku konsumen.
Rangkaian alur data preprocessing yang telah dikerjakan—mulai dari eksplorasi awal, pembersihan, imputasi, penanganan outlier, transformasi, hingga integrasi akhir—menegaskan satu prinsip mendasar yaitu pemrosesan data bukanlah prosedur mekanis rutin, melainkan sebuah proses pengambilan keputusan yang terstruktur.
Setiap tahap pembersihan menuntut penilaian kritis yang berlandaskan logika bisnis. Ketika kita mengoreksi format ID, menetapkan strategi imputasi pada nilai hilang, atau menentukan perlakuan terhadap angka pendapatan bernilai ratusan juta, kita sedang mengambil keputusan metodologis yang secara langsung akan memengaruhi hasil analisis.
Kualitas akhir dari dataset siap pakai sepenuhnya bergantung pada kedalaman pemahaman analisis terhadap domain data, kejelasan tujuan analitik yang ingin dicapai, serta kedisiplinan dalam mendokumentasikan setiap perubahan. Tanpa dokumentasi dan pertimbangan yang matang, tindakan pembersihan data justru berisiko bias baru, merusak distribusi, atau menghilangkan informasi penting. Integrasi dan validasi akhir menjadi bukti bahwa seluruh keputusan yang diambil sepanjang alur pemrosesan mampu menghasilkan data yang utuh, konsisten, dan tepercaya untuk analisis tahap berikutnya.
Han, J., Kamber, M., & Pei, J. (2012). Data Mining: Concepts and Techniques (3rd ed.). Morgan Kaufmann. Chapter 3: Data Preprocessing.
Little, R. J. A., & Rubin, D. B. (2019). Statistical Analysis with Missing Data (3rd ed.). John Wiley & Sons