Dalam dunia analisis data real-world, dataset mentah jarang sekali langsung siap dianalisis. Dataset sering kali mengandung “sampah” seperti format tidak konsisten, missing values, data ganda, hingga angka tidak masuk akal (outliers).
Proyek ini bertujuan untuk mensimulasikan alur kerja Data Preprocessing secara end-to-end pada dataset transaksi pelanggan yang kotor (dirty data). Proses ini mencakup audit kualitas data awal, pembersihan (data cleaning), penanganan missing values, identifikasi outlier, transformasi data, hingga integrasi data dari multiple sumber.
Dataset pelanggan_raw dan transaksi_raw
sengaja dibuat dengan berbagai isu kualitas data (seperti spasi liar,
inkonsistensi kapitalisasi, nilai ekstrem, duplikasi, dan nilai hilang)
untuk kebutuhan pengujian.
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
)
head(pelanggan_raw)
## 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
Catatan Singkat: Dari tampilan awal di atas, terlihat keanehan fisik data seperti penulisan nama kota yang acak-acakan (” PKU”, “PEKANBARU”), usia tidak logis (150 tahun), dan beberapa nilai kosong (NA).
Sebelum melakukan intervensi, penting untuk mengevaluasi 3 dimensi utama kualitas data: Accuracy, Completeness, dan Consistency.
dim(pelanggan_raw)
## [1] 12 6
names(pelanggan_raw)
## [1] "customer_id" "nama" "usia" "pendapatan" "kota"
## [6] "status"
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" ...
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
##
##
##
##
# 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
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
# Deteksi Duplikasi
sum(duplicated(pelanggan_raw))
## [1] 1
sum(duplicated(pelanggan_raw$customer_id))
## [1] 1
# Keberagaman Nilai Kategori
sort(unique(pelanggan_raw$kota))
## [1] " PKU" "Dumai" "DUMAI" "pekanbaru" "Pekanbaru"
## [6] "PEKANBARU" "Pekanbaru " "PKU" "Siak"
sort(unique(pelanggan_raw$status))
## [1] "A" "ACTIVE" "aktif" "Aktif" "AKTIF"
## [6] "nonaktif" "Tidak aktif" "Tidak Aktif"
# Rentang Numerik
range(pelanggan_raw$usia, na.rm = TRUE)
## [1] 21 150
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
## 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
Temuan Audit Data:
Inkonsistensi Teks: Kota Pekanbaru punya 5 variasi penulisan. Status pelanggan juga beragam.
Nilai Hilang: Variabel usia, pendapatan, dan kota memiliki data kosong sebesar ~16.67%.
Duplikasi: ID pelanggan C010 muncul dua kali.
Outlier: Terdeteksi usia 150 tahun dan pendapatan Rp 500 Juta yang jauh melenceng dari data lain.
Pembersihan data dilakukan untuk memperbaiki format string, menghapus duplikasi, dan mengoreksi data yang melanggar aturan bisnis.
# Membuat salinan kerja
pelanggan = pelanggan_raw
# 1. Menghapus spasi liar (trimming) dan menyeragamkan huruf kecil
pelanggan$kota = tolower(trimws(pelanggan$kota))
pelanggan$status = tolower(trimws(pelanggan$status))
# 2. Standardisasi Kategori Kota
pelanggan$kota[pelanggan$kota %in% c("pku", "pekanbaru")] = "Pekanbaru"
pelanggan$kota[pelanggan$kota == "dumai"] = "Dumai"
pelanggan$kota[pelanggan$kota == "siak"] = "Siak"
# Standardisasi Kategori Status
pelanggan$status[pelanggan$status %in% c("aktif", "active", "a")] = "Aktif"
pelanggan$status[pelanggan$status %in% c("tidak aktif", "nonaktif")] = "Tidak Aktif"
# 3. Deduplikasi berdasarkan Customer ID (mempertahankan rekaman pertama)
pelanggan = pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) = NULL
# 4. Koreksi Aturan Bisnis/Domain (Koreksi Usia 150 menjadi 50)
pelanggan$usia[pelanggan$customer_id == "C004"] = 50
# Log Perubahan Data
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
Hasil Cleaning: Kategori kota disederhanakan dari 7 variasi menjadi 3 kota resmi (Pekanbaru, Dumai, Siak). Status pelanggan disatukan menjadi 2 pilihan baku (Aktif dan Tidak Aktif), serta duplikasi ID C010 telah dibuang.
Menghapus baris kosong bisa menghilangkan informasi berharga. Sebagai
solusinya, diterapkan teknik Imputasi mengisi data
kosong dengan nilai Median untuk variabel angka, dan
penamaan "Tidak diketahui" untuk variabel kategori.
# Identifikasi lokasi missing values
colSums(is.na(pelanggan))
## customer_id nama usia pendapatan kota status
## 0 0 1 2 1 0
pelanggan[!complete.cases(pelanggan), ]
## 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
# Strategi Imputasi Numerik (Median)
median_pendapatan = median(pelanggan$pendapatan, na.rm = TRUE)
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
# Imputasi Kategorik dengan Explicit Category
pelanggan$kota_imputasi = pelanggan$kota
pelanggan$kota_imputasi[is.na(pelanggan$kota_imputasi)] = "Tidak diketahui"
# Menambahkan Missing Indicator
pelanggan$pendapatan_missing = as.integer(is.na(pelanggan$pendapatan))
par(mfrow = c(1, 2))
# Visualisasi 1: Sebelum Imputasi (Biru Tua)
hist(pelanggan$pendapatan,
main = "Sebelum Imputasi",
xlab = "Pendapatan", col = "#1f77b4", border = "white", breaks = 8)
# Visualisasi 2: Sesudah Imputasi Median (Biru Muda/Skyblue)
hist(pelanggan$pendapatan_imputasi,
main = "Sesudah Imputasi Median",
xlab = "Pendapatan", col = "#6baed6", border = "white", breaks = 8)
par(mfrow = c(1, 1))
Penjelasan Teknis: Median dipilih dibanding rata-rata (mean) karena ada angka pendapatan ekstrem (Rp 500 Juta). Jika menggunakan rata-rata, pengganti data kosong akan ikut melambung tinggi. Median lebih stabil dan tahan terhadap outlier.
Metode Interquartile Range (IQR) digunakan untuk mencari batas wajar data numerik.
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
# Menandai 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 500000000
boxplot(pelanggan$pendapatan_imputasi,
horizontal = TRUE,
col = "#9ecae1",
border = "#08519c",
outcol = "#08306b",
outpch = 19,
main = "Boxplot Distribusi Pendapatan (Identifikasi Outlier)",
xlab = "Pendapatan (Rupiah)")
Cara Membaca Grafik: Mayoritas data berada di dalam kotak biru (Rp 4.5 - 5.3 Juta). Namun, terdapat satu titik terpisah di ujung kanan atas nama pelanggan Gilang (Rp 500 Juta).
Data ekstrem tidak dibuang, melainkan dibatasi (capping) agar nilainya tidak melebihi batas atas wajar.
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 500000000 5750000
Hasil Winsorization: Pendapatan Gilang yang tadinya Rp 500 Juta “ditekan” menjadi Rp 6,1 Juta (batas atas IQR), sehingga tidak akan mengacaukan perhitungan statistik nantinya.
Transformasi skala numerik dilakukan agar atribut berangka besar (pendapatan) tidak mendominasi atribut berangka kecil (usia).
# 1. Min-Max Scaling [0, 1]
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)
# 2. Z-Score Normalization
pelanggan$usia_z = as.numeric(scale(pelanggan$usia_imputasi))
pelanggan$pendapatan_z = as.numeric(scale(pelanggan$pendapatan_imputasi))
# 3. 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)
}
pelanggan$pendapatan_decimal = decimal_scale(pelanggan$pendapatan_imputasi)
pelanggan$pendapatan_winsor_minmax = minmax(pelanggan$pendapatan_winsor)
plot(pelanggan$pendapatan_minmax,
pelanggan$pendapatan_winsor_minmax,
pch = 19, col = "#08519c",
xlab = "Min-Maks Data Asli (Terpengaruh Outlier)",
ylab = "Min-Maks Data Winsorized",
main = "Dampak Penanganan Outlier terhadap Normalisasi")
abline(0, 1, col = "#6baed6", lty = 2, lwd = 2)
Analisis Grafik: Tanpa penanganan outlier (Sumbu X), sebagian besar data pelanggan menumpuk di angka mendekati 0 karena ditarik oleh Rp 500 Juta. Setelah penanganan outlier (Sumbu Y), rentang data terbagi jauh lebih merata.
Dataset master profil pelanggan digabungkan dengan tabel riwayat
transaksi (transaksi_raw) menggunakan teknik Left
Join.
# Penyelarasan Nama Identifier Key
transaksi = transaksi_raw
names(transaksi)[names(transaksi) == "cust_id"] = "customer_id"
# Left Join dengan Merge
data_terintegrasi = merge(
pelanggan,
transaksi,
by = "customer_id",
all.x = TRUE
)
# Menangani NA Hasil Join
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
Hasil Integrasi: Penggunaan Left Join menjaga pelanggan yang belum pernah bertransaksi (seperti Kiki/C011) agar tidak terhapus. Nilai belanjanya otomatis diisi dengan angka 0.
Atribut pilihan diambil untuk membentuk dataset konsolidasi akhir yang bersih dan siap dianalisis (analytics-ready).
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"
# Format Tampilan untuk Tabel
data_display = data_final
data_display$pendapatan_imputasi = format(data_display$pendapatan_imputasi, big.mark = ".", decimal.mark = ",", scientific = FALSE)
data_display$total_purchase_final = format(data_display$total_purchase_final, big.mark = ".", decimal.mark = ",", scientific = FALSE)
# Menampilkan Dataset Final
knitr::kable(data_display, caption = "Tabel Dataset Pelanggan Setelah Preprocessing")
| customer_id | nama | usia | kota | status | pendapatan_imputasi | pendapatan_missing | outlier_pendapatan | usia_minmax | pendapatan_minmax | jumlah_transaksi_final | total_purchase_final |
|---|---|---|---|---|---|---|---|---|---|---|---|
| C001 | Ani | 21.0 | Pekanbaru | Aktif | 4.500.000 | 0 | FALSE | 0.0000000 | 0.0000000 | 5 | 1.500.000 |
| C002 | Budi | 25.0 | Pekanbaru | Aktif | 4.900.000 | 1 | FALSE | 0.1379310 | 0.0008073 | 3 | 900.000 |
| C003 | Citra | 23.0 | Pekanbaru | Aktif | 5.200.000 | 0 | FALSE | 0.0689655 | 0.0014127 | 7 | 2.700.000 |
| C004 | Dodi | 50.0 | Dumai | Aktif | 4.800.000 | 0 | FALSE | 1.0000000 | 0.0006054 | 2 | 600.000 |
| C005 | Eka | 27.0 | Pekanbaru | Tidak Aktif | 4.900.000 | 0 | FALSE | 0.2068966 | 0.0008073 | 6 | 2.100.000 |
| C006 | Fani | 27.5 | Dumai | Tidak Aktif | 5.100.000 | 0 | FALSE | 0.2241379 | 0.0012109 | 4 | 1.300.000 |
| C007 | Gilang | 31.0 | Pekanbaru | Aktif | 500.000.000 | 0 | TRUE | 0.3448276 | 1.0000000 | 20 | 25.000.000 |
| C008 | Hana | 29.0 | Siak | Aktif | 4.700.000 | 0 | FALSE | 0.2758621 | 0.0004036 | 5 | 1.700.000 |
| C009 | Indra | 22.0 | Pekanbaru | Aktif | 4.600.000 | 0 | FALSE | 0.0344828 | 0.0002018 | 3 | 800.000 |
| C010 | Joko | 35.0 | Dumai | Tidak Aktif | 5.300.000 | 0 | FALSE | 0.4827586 | 0.0016145 | 8 | 3.200.000 |
| C011 | Kiki | 28.0 | Tidak diketahui | Aktif | 4.900.000 | 1 | FALSE | 0.2413793 | 0.0008073 | 0 | 0 |
perbandingan = data.frame(
Indikator = c(
"Jumlah Baris Observasi",
"Duplikasi Customer ID",
"Total Missing Values",
"Jumlah Kategori Unik (Kota)",
"Jumlah Kategori Unik (Status)"
),
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))
)
)
knitr::kable(perbandingan, caption = "Perbandingan Metrik Kualitas Data")
| Indikator | Sebelum | Sesudah |
|---|---|---|
| Jumlah Baris Observasi | 12 | 11 |
| Duplikasi Customer ID | 1 | 0 |
| Total Missing Values | 4 | 0 |
| Jumlah Kategori Unik (Kota) | 10 | 4 |
| Jumlah Kategori Unik (Status) | 8 | 2 |
Melalui alur kerja data preprocessing yang sistematis:
Mau belajar lebih lanjut? Langsung saja DM Instagram @tarayadivani ya!