✨ CUSTOMER DATA RESCUE LAB
Praktikum ini mensimulasikan
proses menyiapkan data pelanggan e-commerce yang masih “kotor” hingga
menjadi dataset siap analisis.
| Komponen | Keterangan |
|---|---|
| Nama | Syarifah Al Fa’izah |
| NIM | 2403110912 |
| Mata Kuliah | Data Mining |
| Pertemuan | 03 |
| Topik | Data Preprocessing |
| Dosen Pengampu | Anisa Nurizki, S.Si., M.Si. |
| Perangkat Lunak | R & RStudio |
| Alokasi Waktu | 2 SKS / 100 menit |
| Acuan Utama | Han, Kamber, & Pei — Chapter 3 |
🎯 Tujuan: menghasilkan dataset pelanggan yang lebih konsisten, lengkap, terdokumentasi, dan siap digunakan untuk tahap analisis atau data mining berikutnya.
Setelah menyelesaikan praktikum, mahasiswa mampu:
Sebuah perusahaan e-commerce ingin melakukan analisis pelanggan. Data berasal dari dua sumber:
Dataset sengaja dibuat memiliki masalah nyata seperti missing value, kategori tidak konsisten, duplikasi, nilai ekstrem, dan perbedaan nama identifier. Tugas praktikum adalah memperbaiki masalah tersebut secara bertahap.
⚠️ Prinsip penting: data tidak boleh diubah hanya karena terlihat aneh. Setiap perubahan harus memiliki alasan, aturan, dan dokumentasi.
customer_data_rescue..Rmd ini di folder project.getwd()
## [1] "C:/Users/infinix/Documents/LEARN/SEMS 5/DATA MINING"
R.version.string
## [1] "R version 4.4.2 (2024-10-31 ucrt)"
Modul utama menggunakan fungsi dasar R sehingga proses inti dapat dijalankan tanpa paket tambahan.
Dataset dibuat dengan struktur yang sama seperti contoh praktikum: data pelanggan dan data transaksi.
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
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 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" ...
head(pelanggan_raw)
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
##
##
##
##
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
cat("Missing value per atribut:\n")
## Missing value per atribut:
colSums(is.na(pelanggan_raw))
## customer_id nama usia pendapatan kota status
## 0 0 1 2 1 0
cat("\nPersentase missing value:\n")
##
## 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
cat("\nDuplikasi baris penuh:", sum(duplicated(pelanggan_raw)), "\n")
##
## Duplikasi baris penuh: 1
cat("Duplikasi customer_id:", sum(duplicated(pelanggan_raw$customer_id)), "\n")
## Duplikasi customer_id: 1
cat("\nKategori kota:\n")
##
## Kategori kota:
sort(unique(pelanggan_raw$kota))
## [1] " PKU" "Dumai" "DUMAI" "pekanbaru" "Pekanbaru"
## [6] "PEKANBARU" "Pekanbaru " "PKU" "Siak"
cat("\nKategori status:\n")
##
## Kategori status:
sort(unique(pelanggan_raw$status))
## [1] "A" "ACTIVE" "aktif" "Aktif" "AKTIF"
## [6] "nonaktif" "Tidak aktif" "Tidak Aktif"
cat("\nRentang usia:\n")
##
## Rentang usia:
range(pelanggan_raw$usia, na.rm = TRUE)
## [1] 21 150
cat("\nRentang pendapatan:\n")
##
## Rentang pendapatan:
range(pelanggan_raw$pendapatan, na.rm = TRUE)
## [1] 4.5e+06 5.0e+08
🔎 Temuan awal: terdapat missing value pada usia, pendapatan, dan kota; satu customer_id berulang; kategori kota/status tidak konsisten; usia 150 merupakan kandidat nilai tidak valid; dan pendapatan Rp500 juta merupakan kandidat outlier.
pelanggan <- pelanggan_raw
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"
sort(unique(pelanggan$status))
## [1] "a" "active" "aktif" "nonaktif" "tidak aktif"
# Kota
pelanggan$kota[pelanggan$kota %in% c("pku", "pekanbaru")] <- "Pekanbaru"
pelanggan$kota[pelanggan$kota == "dumai"] <- "Dumai"
pelanggan$kota[pelanggan$kota == "siak"] <- "Siak"
# 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"
pelanggan[
duplicated(pelanggan$customer_id) |
duplicated(pelanggan$customer_id, fromLast = TRUE),
]
pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL
dim(pelanggan)
## [1] 11 6
Catatan: deduplikasi berdasarkan customer_id aman
jika satu customer memang hanya boleh memiliki satu baris pada tabel
pelanggan.
pelanggan[pelanggan$usia < 15 | pelanggan$usia > 100, ]
pelanggan[pelanggan$pendapatan < 0, ]
Nilai usia 150 melanggar aturan domain usia 15–100 tahun. Pada skenario praktikum, pemeriksaan sumber asli menunjukkan bahwa nilai tersebut seharusnya 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
pelanggan[!complete.cases(pelanggan), ]
pelanggan_complete <- pelanggan[complete.cases(pelanggan), ]
nrow(pelanggan)
## [1] 11
nrow(pelanggan_complete)
## [1] 8
round(
(1 - nrow(pelanggan_complete) / nrow(pelanggan)) * 100,
2
)
## [1] 27.27
Hasil menunjukkan bahwa penghapusan seluruh baris tidak lengkap akan membuang 27,27% baris data. Karena ukuran data kecil dan informasi pelanggan masih berguna, strategi ini tidak dipilih sebagai hasil akhir.
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 pendapatan sangat dipengaruhi nilai ekstrem Rp500 juta, median lebih stabil untuk contoh ini.
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"
)]
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
pelanggan$pendapatan_missing <- as.integer(
is.na(pelanggan$pendapatan)
)
table(pelanggan$pendapatan_missing)
##
## 0 1
## 9 2
💡 Keputusan: median digunakan untuk usia dan pendapatan, sedangkan kota yang hilang diberi kategori Tidak diketahui agar ketidakpastian tidak disembunyikan.
par(mfrow = c(1, 2))
hist(
pelanggan$pendapatan,
main = "Sebelum Imputasi",
xlab = "Pendapatan",
col = "#A78BFA",
breaks = 8
)
hist(
pelanggan$pendapatan_imputasi,
main = "Sesudah Imputasi Median",
xlab = "Pendapatan",
col = "#5EEAD4",
breaks = 8
)
par(mfrow = c(1, 1))
boxplot(
pelanggan$pendapatan_imputasi,
horizontal = TRUE,
col = "#93C5FD",
main = "Boxplot Pendapatan Setelah Imputasi",
xlab = "Pendapatan (Rp)"
)
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")
]
Hasil menunjukkan C007 (Gilang) dengan pendapatan Rp500.000.000 sebagai kandidat outlier.
pelanggan$pendapatan_winsor <- pmin(
pmax(pelanggan$pendapatan_imputasi, batas_bawah),
batas_atas
)
pelanggan[
pelanggan$outlier_pendapatan,
c("customer_id", "pendapatan_imputasi", "pendapatan_winsor")
]
⚠️ Interpretasi: nilai ekstrem tidak otomatis dihapus. Nilai Rp500 juta dipertahankan sebagai data asli dan versi winsorized dibuat hanya untuk melihat dampak metode.
Rumus:
\[ 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"
)]
\[ z = \frac{x-\bar{x}}{s} \]
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) {
if (all(is.na(x))) return(rep(NA_real_, length(x)))
j <- ifelse(max(abs(x), na.rm = TRUE) == 0, 0,
ceiling(log10(max(abs(x), na.rm = TRUE)) + 1))
x / (10^j)
}
pelanggan$pendapatan_decimal <-
decimal_scale(pelanggan$pendapatan_imputasi)
pelanggan[, c(
"customer_id", "pendapatan_imputasi",
"pendapatan_decimal"
)]
pelanggan$pendapatan_winsor_minmax <-
minmax(pelanggan$pendapatan_winsor)
plot(
pelanggan$pendapatan_minmax,
pelanggan$pendapatan_winsor_minmax,
pch = 19,
col = "#7C3AED",
xlab = "Min–maks Data Asli",
ylab = "Min–maks Data Winsorized",
main = "Dampak Outlier terhadap Normalisasi"
)
abline(0, 1, col = "#E11D48", lty = 2)
📌 Insight: outlier dapat membuat sebagian besar nilai lain terkonsentrasi pada rentang yang sangat sempit setelah normalisasi min–maks.
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:
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")
]
🔗 Mengapa left join? Karena seluruh pelanggan pada tabel utama harus tetap dipertahankan meskipun belum memiliki pasangan transaksi.
c(
sebelum = nrow(pelanggan),
sesudah = nrow(data_terintegrasi)
)
## sebelum sesudah
## 11 11
sum(duplicated(data_terintegrasi$customer_id))
## [1] 0
colSums(
is.na(
data_terintegrasi[, c(
"jumlah_transaksi",
"total_purchase"
)]
)
)
## jumlah_transaksi total_purchase
## 1 1
data_terintegrasi[
is.na(data_terintegrasi$jumlah_transaksi),
c("customer_id", "nama")
]
NA transaksi dapat berarti:
Nilai NA hanya boleh diubah menjadi 0 jika definisi tersebut sudah dikonfirmasi. Untuk skenario praktikum, diasumsikan bahwa C011 memang belum memiliki transaksi, sehingga NA dikonversi menjadi 0.
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
✅ Target akhir: tidak ada duplikasi customer_id dan tidak ada missing value pada dataset akhir setelah strategi preprocessing diterapkan.
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
)
Tidak selalu. Menghilangkan missing value memang membuat data terlihat lengkap, tetapi dapat mengurangi ukuran sampel atau menghilangkan informasi penting. Kualitas data harus dinilai berdasarkan tujuan analisis.
Karena outlier dapat merupakan kesalahan input, observasi valid yang ekstrem, atau anggota populasi yang berbeda. Keputusan harus didasarkan pada konteks dan verifikasi data.
Imputasi, penghapusan data, standardisasi kategori, dan pembatasan outlier dapat mengubah distribusi data. Jika aturan diterapkan secara tidak tepat, karakteristik asli data dapat berubah.
Dalam analisis prediktif, parameter seperti median, mean, atau batas transformasi sebaiknya dihitung dari data pelatihan agar informasi dari data pengujian tidak “bocor” ke proses pembentukan model.
Identifier yang tidak unik dapat menyebabkan satu pelanggan dipasangkan dengan beberapa baris secara tidak semestinya sehingga jumlah observasi membengkak dan hasil analisis menjadi bias.
Alur preprocessing yang telah dilakukan:
Audit → Cleaning → Missing Values → Outlier → Transformasi → Integrasi → Validasi
🌈 Kesimpulan: preprocessing bukan sekadar menjalankan kode R. Setiap perubahan pada data harus mempunyai alasan, aturan, dan dokumentasi. Dataset akhir yang baik bukan hanya bebas dari error teknis, tetapi juga sesuai dengan tujuan analisis dan konteks data.
Han, J., Kamber, M., & Pei, J. (2012). Data Mining: Concepts and Techniques (3rd ed.). Morgan Kaufmann. Chapter 3: Data Preprocessing. ISBN 978-0-12-381479-1.
Wickham, H., & Grolemund, G. (2017). R for Data Science: Import, Tidy, Transform, Visualize, and Model Data. O’Reilly Media.
📚 Catatan sumber: Referensi utama Han, Kamber, & Pei memang memuat Chapter 3 tentang Data Preprocessing, termasuk data cleaning, data integration, data reduction, serta data transformation. Referensi R for Data Science digunakan sebagai pendukung penggunaan R dalam pengolahan dan eksplorasi data.