🎯 Misi Praktikum
Mengubah data pelanggan yang masih
“berantakan” menjadi data yang konsisten, dapat ditelusuri, dan siap
digunakan untuk menemukan insight.
Praktikum ini memakai skenario perusahaan e-commerce yang ingin memahami perilaku pelanggannya. Fokus utamanya bukan sekadar menghasilkan tabel bersih, tetapi menunjukkan bagaimana kualitas data memengaruhi kesimpulan.
Materi acuan menggunakan dua sumber data: data pelanggan yang memuat identitas, usia, kota, pendapatan, dan status; serta data transaksi yang memuat jumlah transaksi dan total pembelian. Dataset ini memang dirancang mengandung missing value, kategori tidak konsisten, duplikasi, nilai ekstrem, dan perbedaan identifier.
Pada versi baru ini, skala latihan diperbesar menjadi 111 ID pelanggan unik agar analisis lebih realistis. Masalah data tetap sengaja ditanam sehingga proses cleaning dapat terlihat secara nyata.
n <- 100
nama_depan <- c(
"Ani","Budi","Citra","Dodi","Eka","Fani","Gilang","Hana","Indra","Joko",
"Kiki","Lala","Maya","Nanda","Oki","Putri","Raka","Sari","Tio","Vina"
)
id_lanjutan <- sprintf("C%03d", 12:(n + 11))
nama_lanjutan <- paste0(
sample(nama_depan, n, replace = TRUE),
"_", sprintf("%03d", 12:(n + 11))
)
kota_lanjutan <- sample(
c("Pekanbaru","Dumai","Siak"),
n, replace = TRUE,
prob = c(.55,.30,.15)
)
usia_lanjutan <- round(
pmin(pmax(rnorm(n, 29, 6), 18), 60), 0
)
pendapatan_lanjutan <- round(
rlnorm(n, log(5e6), .13), -4
)
status_lanjutan <- sample(
c("Aktif","Tidak Aktif"),
n, replace = TRUE,
prob = c(.68,.32)
)
pelanggan_raw <- rbind(
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
),
data.frame(
customer_id = id_lanjutan,
nama = nama_lanjutan,
usia = usia_lanjutan,
pendapatan = pendapatan_lanjutan,
kota = kota_lanjutan,
status = status_lanjutan,
stringsAsFactors = FALSE
)
)
# Menanam masalah kualitas data secara sengaja pada data yang dibangkitkan
pelanggan_raw$usia[c(20, 59)] <- c(150, NA)
pelanggan_raw$pendapatan[c(25, 88, 103)] <- c(500000000, NA, NA)
pelanggan_raw$kota[c(33, 100)] <- c(" PKU", NA)
pelanggan_raw$kota[c(15, 23, 39, 56)] <- c(
"PEKANBARU","pekanbaru","PKU","DUMAI"
)
pelanggan_raw$status[c(16, 30, 47, 64, 79, 96)] <- c(
"aktif","ACTIVE","A","nonaktif","Tidak aktif","AKTIF"
)
# Data transaksi
transaksi_raw <- rbind(
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
),
data.frame(
cust_id = sprintf("C%03d", 13:112),
jumlah_transaksi = sample(1:18, n, replace = TRUE),
total_purchase = 0,
stringsAsFactors = FALSE
)
)
transaksi_raw$total_purchase[12:111] <-
transaksi_raw$jumlah_transaksi[12:111] *
round(runif(n, 250000, 550000), -4)
# Nilai ekstrem transaksi pada observasi yang dibangkitkan
idx_trx <- 12:111
idx_jumlah <- idx_trx[which.max(transaksi_raw$jumlah_transaksi[idx_trx])]
idx_purchase <- idx_trx[which.max(transaksi_raw$total_purchase[idx_trx])]
transaksi_raw$jumlah_transaksi[idx_jumlah] <- 40
transaksi_raw$total_purchase[idx_purchase] <- 30000000
# Satu pelanggan tidak memiliki pasangan transaksi
transaksi_raw <- transaksi_raw[transaksi_raw$cust_id != "C011", ]Dataset mentah kini terdiri dari 112 baris, dengan 111 ID pelanggan
unik karena C010 muncul pada dua observasi. Tujuannya sama
seperti skenario latihan: data “kotor” menjadi media untuk mempraktikkan
seluruh alur preprocessing.
## [1] 112 6
audit_data <- function(data) {
data.frame(
atribut = names(data),
tipe = sapply(data, function(x) class(x)[1]),
missing = sapply(data, function(x) sum(is.na(x))),
persen_missing = round(
sapply(data, function(x) mean(is.na(x)) * 100), 2
),
unik = sapply(data, function(x) length(unique(x))),
row.names = NULL
)
}
audit_awal <- audit_data(pelanggan_raw)
kable(audit_awal, caption = "Audit kualitas data pelanggan")| atribut | tipe | missing | persen_missing | unik |
|---|---|---|---|---|
| customer_id | character | 0 | 0.00 | 111 |
| nama | character | 0 | 0.00 | 111 |
| usia | numeric | 2 | 1.79 | 27 |
| pendapatan | numeric | 4 | 3.57 | 85 |
| kota | character | 2 | 1.79 | 9 |
| status | character | 0 | 0.00 | 8 |
## Jumlah baris mentah : 112
## Jumlah ID unik : 111
## Duplikasi ID : 1
## Missing usia : 2
## Missing pendapatan : 4
## Missing kota : 2
Audit berfungsi sebagai alarm, bukan keputusan otomatis. Modul acuan juga menekankan bahwa hasil audit menunjukkan atribut yang perlu diperiksa lebih lanjut, bukan langsung menentukan tindakan cleaning.
Kota dan status mempunyai beberapa penulisan berbeda. Pada data,
misalnya, PKU, Pekanbaru, variasi huruf
besar-kecil, dan spasi diperlakukan sebagai kategori berbeda sebelum
distandardisasi.
pelanggan <- pelanggan_raw
pelanggan$kota <- trimws(tolower(pelanggan$kota))
pelanggan$status <- trimws(tolower(pelanggan$status))
pelanggan$kota[pelanggan$kota %in% c("pku","pekanbaru")] <- "Pekanbaru"
pelanggan$kota[pelanggan$kota == "dumai"] <- "Dumai"
pelanggan$kota[pelanggan$kota == "siak"] <- "Siak"
pelanggan$status[pelanggan$status %in% c("aktif","active","a")] <- "Aktif"
pelanggan$status[pelanggan$status %in% c("tidak aktif","nonaktif")] <- "Tidak Aktif"
sort(unique(pelanggan$kota))## [1] "Dumai" "Pekanbaru" "Siak"
## [1] "Aktif" "Tidak Aktif"
duplikat_id <- pelanggan[
duplicated(pelanggan$customer_id) |
duplicated(pelanggan$customer_id, fromLast = TRUE),
]
kable(duplikat_id)| customer_id | nama | usia | pendapatan | kota | status | |
|---|---|---|---|---|---|---|
| 10 | C010 | Joko | 35 | 5300000 | Dumai | Tidak Aktif |
| 11 | C010 | Joko | 35 | 5300000 | Dumai | Tidak Aktif |
pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL
cat("Baris setelah deduplikasi:", nrow(pelanggan))## Baris setelah deduplikasi: 111
Deduplikasi berdasarkan customer_id layak dilakukan
jika satu ID memang merepresentasikan satu pelanggan.
Prinsip ini juga ditekankan pada materi acuan karena menghapus duplikasi
tanpa memahami struktur data dapat menghilangkan observasi yang
sebenarnya sah.
Nilai usia 150 diperlakukan sebagai kesalahan input. Nilai usia 150 diperlakukan sebagai kesalahan input dan dikoreksi berdasarkan konteks data.
missing_ringkas <- data.frame(
variabel = names(pelanggan),
jumlah = colSums(is.na(pelanggan)),
persen = round(colMeans(is.na(pelanggan)) * 100, 2)
)
kable(missing_ringkas, caption = "Peta nilai hilang setelah cleaning awal")| variabel | jumlah | persen | |
|---|---|---|---|
| customer_id | customer_id | 0 | 0.0 |
| nama | nama | 0 | 0.0 |
| usia | usia | 2 | 1.8 |
| pendapatan | pendapatan | 4 | 3.6 |
| kota | kota | 2 | 1.8 |
| status | status | 0 | 0.0 |
Menghapus seluruh baris yang tidak lengkap dapat mengurangi ukuran sampel secara cukup besar. Menghapus seluruh baris yang tidak lengkap dapat mengurangi ukuran sampel secara cukup besar.
Untuk dataset 111 pelanggan, pendekatan yang dipilih adalah imputasi median pada variabel numerik dan kategori eksplisit untuk kota yang tidak diketahui.
median_usia <- median(pelanggan$usia, na.rm = TRUE)
median_pendapatan <- median(pelanggan$pendapatan, na.rm = TRUE)
pelanggan$usia_imputasi <- pelanggan$usia
pelanggan$usia_imputasi[is.na(pelanggan$usia_imputasi)] <- median_usia
pelanggan$pendapatan_imputasi <- pelanggan$pendapatan
pelanggan$pendapatan_imputasi[
is.na(pelanggan$pendapatan_imputasi)
] <- median_pendapatan
pelanggan$kota_imputasi <- pelanggan$kota
pelanggan$kota_imputasi[
is.na(pelanggan$kota_imputasi)
] <- "Tidak diketahui"
pelanggan$pendapatan_missing <- as.integer(is.na(pelanggan$pendapatan))
cat("Median usia :", median_usia, "\n")## Median usia : 30
## Median pendapatan : 4950000
Pemilihan median penting karena pendapatan memiliki nilai ekstrem. Nilai ekstrem dapat mendorong mean jauh dari pusat mayoritas data, sedangkan median tetap berada di sekitar pusat mayoritas data.
ggplot(pelanggan, aes(y = pendapatan_imputasi)) +
geom_boxplot(fill = "#6C8EF5", alpha = .75) +
scale_y_continuous(labels = label_number(big.mark = ".")) +
labs(
title = "Pendapatan pelanggan sebelum penanganan outlier",
y = "Pendapatan (Rp)",
x = NULL
) +
theme_minimal(base_size = 13)Q1 <- quantile(pelanggan$pendapatan_imputasi, .25, na.rm = TRUE)
Q3 <- quantile(pelanggan$pendapatan_imputasi, .75, na.rm = TRUE)
IQR_pendapatan <- Q3 - Q1
batas_bawah <- Q1 - 1.5 * IQR_pendapatan
batas_atas <- Q3 + 1.5 * IQR_pendapatan
pelanggan$outlier_pendapatan <- pelanggan$pendapatan_imputasi < batas_bawah |
pelanggan$pendapatan_imputasi > batas_atas
outlier_data <- pelanggan[pelanggan$outlier_pendapatan, ]
kable(
outlier_data[, c("customer_id","nama","pendapatan_imputasi")],
caption = "Kandidat outlier berdasarkan aturan IQR"
)| customer_id | nama | pendapatan_imputasi | |
|---|---|---|---|
| 7 | C007 | Gilang | 5.00e+08 |
| 24 | C024 | Sari_024 | 5.00e+08 |
| 72 | C072 | Maya_072 | 6.84e+06 |
Outlier tidak otomatis berarti salah. Prinsipnya adalah membedakan kesalahan input, observasi valid tetapi ekstrem, dan kelompok populasi berbeda. Materi acuan juga menyarankan mempertahankan observasi ekstrem bila memang valid atau memakai metode robust sesuai tujuan analisis.
Untuk memperlihatkan dampaknya, dibuat versi winsorized tanpa menghapus nilai asli.
pelanggan$pendapatan_winsor <- pmin(
pmax(pelanggan$pendapatan_imputasi, batas_bawah),
batas_atas
)
kable(
pelanggan[pelanggan$outlier_pendapatan,
c("customer_id","pendapatan_imputasi","pendapatan_winsor")]
)| customer_id | pendapatan_imputasi | pendapatan_winsor | |
|---|---|---|---|
| 7 | C007 | 5.00e+08 | 6597500 |
| 24 | C024 | 5.00e+08 | 6597500 |
| 72 | C072 | 6.84e+06 | 6597500 |
Transformasi bukan berarti “mengubah data sesuka hati”. Tujuannya adalah membuat representasi data lebih mudah dibaca tanpa kehilangan makna data.
Mengurangi variasi lokal untuk membantu melihat kecenderungan umum.
Membawa nilai ke skala 0–1 agar perbedaan skala lebih mudah dibandingkan.
Menunjukkan posisi nilai terhadap rata-rata dalam satuan simpangan baku.
⚠️ Catatan: dataset pelanggan ini bersifat cross-sectional, bukan time series. Karena itu, smoothing digunakan sebagai contoh transformasi untuk eksplorasi, bukan sebagai pengganti nilai pendapatan.
Smoothing membantu mengurangi variasi lokal sehingga kecenderungan umum lebih mudah terlihat. Karena dataset pelanggan bersifat cross-sectional dan bukan data time series, smoothing di sini digunakan sebagai contoh transformasi untuk eksplorasi, bukan untuk menggantikan nilai pendapatan.
pelanggan$pendapatan_smoothing <- as.numeric(stats::filter(
pelanggan$pendapatan_imputasi,
rep(1/3, 3), sides = 2
))
kable(
head(pelanggan[, c("customer_id", "pendapatan_imputasi", "pendapatan_smoothing")], 12),
digits = 2,
caption = "Contoh smoothing dengan moving average 3 observasi"
)| customer_id | pendapatan_imputasi | pendapatan_smoothing |
|---|---|---|
| C001 | 4.50e+06 | NA |
| C002 | 4.95e+06 | 4883333 |
| C003 | 5.20e+06 | 4983333 |
| C004 | 4.80e+06 | 4966667 |
| C005 | 4.90e+06 | 4933333 |
| C006 | 5.10e+06 | 170000000 |
| C007 | 5.00e+08 | 169933333 |
| C008 | 4.70e+06 | 169766667 |
| C009 | 4.60e+06 | 4866667 |
| C010 | 5.30e+06 | 4950000 |
| C011 | 4.95e+06 | 5323333 |
| C012 | 5.72e+06 | 5050000 |
Nilai pendapatan_imputasi tetap dipertahankan. Kolom
pendapatan_smoothing hanya digunakan untuk melihat pola
yang lebih halus.
Normalisasi Min–Max mengubah nilai ke rentang 0–1 sehingga variabel dengan skala berbeda dapat lebih mudah dibandingkan.
minmax <- function(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)
kable(
head(pelanggan[, c("customer_id", "usia_imputasi", "usia_minmax", "pendapatan_imputasi", "pendapatan_minmax")], 12),
digits = 3,
caption = "Hasil normalisasi Min–Max"
)| customer_id | usia_imputasi | usia_minmax | pendapatan_imputasi | pendapatan_minmax |
|---|---|---|---|---|
| C001 | 21 | 0.023 | 4.50e+06 | 0.002 |
| C002 | 25 | 0.053 | 4.95e+06 | 0.003 |
| C003 | 23 | 0.038 | 5.20e+06 | 0.004 |
| C004 | 50 | 0.242 | 4.80e+06 | 0.003 |
| C005 | 27 | 0.068 | 4.90e+06 | 0.003 |
| C006 | 30 | 0.091 | 5.10e+06 | 0.003 |
| C007 | 31 | 0.098 | 5.00e+08 | 1.000 |
| C008 | 29 | 0.083 | 4.70e+06 | 0.003 |
| C009 | 22 | 0.030 | 4.60e+06 | 0.002 |
| C010 | 35 | 0.129 | 5.30e+06 | 0.004 |
| C011 | 28 | 0.076 | 4.95e+06 | 0.003 |
| C012 | 39 | 0.159 | 5.72e+06 | 0.005 |
Standardisasi mengubah data berdasarkan rata-rata dan simpangan baku sehingga nilai dinyatakan dalam satuan deviasi standar.
pelanggan$usia_z <- as.numeric(scale(pelanggan$usia_imputasi))
pelanggan$pendapatan_z <- as.numeric(scale(pelanggan$pendapatan_imputasi))
kable(
head(pelanggan[, c("customer_id", "usia_imputasi", "usia_z", "pendapatan_imputasi", "pendapatan_z")], 12),
digits = 3,
caption = "Hasil standardisasi Z-score"
)| customer_id | usia_imputasi | usia_z | pendapatan_imputasi | pendapatan_z |
|---|---|---|---|---|
| C001 | 21 | -0.767 | 4.50e+06 | -0.143 |
| C002 | 25 | -0.459 | 4.95e+06 | -0.136 |
| C003 | 23 | -0.613 | 5.20e+06 | -0.132 |
| C004 | 50 | 1.470 | 4.80e+06 | -0.138 |
| C005 | 27 | -0.304 | 4.90e+06 | -0.137 |
| C006 | 30 | -0.073 | 5.10e+06 | -0.134 |
| C007 | 31 | 0.004 | 5.00e+08 | 7.349 |
| C008 | 29 | -0.150 | 4.70e+06 | -0.140 |
| C009 | 22 | -0.690 | 4.60e+06 | -0.141 |
| C010 | 35 | 0.313 | 5.30e+06 | -0.131 |
| C011 | 28 | -0.227 | 4.95e+06 | -0.136 |
| C012 | 39 | 0.621 | 5.72e+06 | -0.124 |
Parameter transformasi sebaiknya dihitung dari data pelatihan ketika digunakan untuk pemodelan agar tidak terjadi data leakage.
💡 Cara membaca hasilnya:
• Smoothing membantu
melihat pola yang lebih halus.
• Min–Max menghasilkan nilai
antara 0 dan 1.
• Z-score bernilai sekitar 0 untuk data dekat
rata-rata; nilai positif menunjukkan posisi di atas rata-rata dan nilai
negatif di bawah rata-rata.
Ketiga hasil tersebut disimpan pada
kolom baru sehingga nilai sebelum transformasi tetap dapat
ditelusuri.
minmax <- function(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$usia_z <- as.numeric(scale(pelanggan$usia_imputasi))
pelanggan$pendapatan_z <- as.numeric(scale(pelanggan$pendapatan_imputasi))
transformasi <- pelanggan[, c(
"customer_id",
"pendapatan_imputasi",
"pendapatan_minmax",
"pendapatan_z"
)]
kable(head(transformasi, 12))| customer_id | pendapatan_imputasi | pendapatan_minmax | pendapatan_z |
|---|---|---|---|
| C001 | 4.50e+06 | 0.0022552 | -0.1426768 |
| C002 | 4.95e+06 | 0.0031614 | -0.1358733 |
| C003 | 5.20e+06 | 0.0036648 | -0.1320935 |
| C004 | 4.80e+06 | 0.0028593 | -0.1381411 |
| C005 | 4.90e+06 | 0.0030607 | -0.1366292 |
| C006 | 5.10e+06 | 0.0034634 | -0.1336054 |
| C007 | 5.00e+08 | 1.0000000 | 7.3487747 |
| C008 | 4.70e+06 | 0.0026580 | -0.1396530 |
| C009 | 4.60e+06 | 0.0024566 | -0.1411649 |
| C010 | 5.30e+06 | 0.0038661 | -0.1305816 |
| C011 | 4.95e+06 | 0.0031614 | -0.1358733 |
| C012 | 5.72e+06 | 0.0047119 | -0.1242317 |
Min–maks membawa data ke rentang 0–1, sedangkan z-score memusatkan data berdasarkan rata-rata dan simpangan baku. Materi acuan juga memperlihatkan bahwa min–maks sangat sensitif terhadap outlier.
pelanggan$pendapatan_winsor_minmax <- minmax(pelanggan$pendapatan_winsor)
ggplot(
pelanggan,
aes(x = pendapatan_minmax, y = pendapatan_winsor_minmax)
) +
geom_point(size = 2.7, alpha = .75) +
geom_abline(linetype = 2) +
labs(
title = "Dampak outlier terhadap normalisasi min–maks",
x = "Min–maks dengan nilai ekstrem",
y = "Min–maks setelah winsorizing"
) +
theme_minimal(base_size = 13)Insight visual: ketika satu nilai ekstrem terlalu jauh dari mayoritas, sebagian besar observasi terdorong berkumpul sangat dekat dengan nol. Karena itu, deteksi outlier sebaiknya dilakukan sebelum memilih transformasi.
transaksi <- transaksi_raw
names(transaksi)[names(transaksi) == "cust_id"] <- "customer_id"
id_pelanggan_tanpa_transaksi <- setdiff(
pelanggan$customer_id,
transaksi$customer_id
)
id_transaksi_tanpa_pelanggan <- setdiff(
transaksi$customer_id,
pelanggan$customer_id
)
cat("Pelanggan tanpa transaksi :", paste(id_pelanggan_tanpa_transaksi, collapse = ", "), "\n")## Pelanggan tanpa transaksi : C011
## Transaksi tanpa pelanggan : C112
Pemeriksaan setdiff() diperlukan untuk mengetahui apakah
ada ID yang hanya muncul di salah satu sumber. Pada contoh awal,
terdapat pelanggan yang tidak memiliki transaksi dan transaksi yang
tidak memiliki pasangan pelanggan.
data_terintegrasi <- merge(
pelanggan,
transaksi,
by = "customer_id",
all.x = TRUE
)
data_terintegrasi$jumlah_transaksi_final <-
ifelse(is.na(data_terintegrasi$jumlah_transaksi), 0,
data_terintegrasi$jumlah_transaksi)
data_terintegrasi$total_purchase_final <-
ifelse(is.na(data_terintegrasi$total_purchase), 0,
data_terintegrasi$total_purchase)
cat("Baris sebelum integrasi :", nrow(pelanggan), "\n")## Baris sebelum integrasi : 111
## Baris sesudah integrasi : 111
## ID duplikat : 0
Catatan metodologis: nilai transaksi yang kosong
hanya boleh diubah menjadi 0 jika definisi bisnis memastikan bahwa
NA berarti pelanggan memang belum pernah bertransaksi. Jika
NA berarti data gagal dipadankan, sebaiknya tetap
dipertahankan sebagai NA.
status_ringkas <- data_terintegrasi %>%
count(status) %>%
mutate(persen = n / sum(n) * 100)
kota_ringkas <- data_terintegrasi %>%
count(kota_imputasi) %>%
mutate(persen = n / sum(n) * 100)
kable(status_ringkas, digits = 2)| status | n | persen |
|---|---|---|
| Aktif | 70 | 63.06 |
| Tidak Aktif | 41 | 36.94 |
| kota_imputasi | n | persen |
|---|---|---|
| Dumai | 30 | 27.03 |
| Pekanbaru | 68 | 61.26 |
| Siak | 11 | 9.91 |
| Tidak diketahui | 2 | 1.80 |
ggplot(kota_ringkas,
aes(x = reorder(kota_imputasi, n), y = n)) +
geom_col(fill = "#5B7CFA", width = .7) +
geom_text(aes(label = paste0(n, " (", round(persen,1), "%)")),
hjust = -0.1, size = 4) +
coord_flip() +
labs(
title = "Sebaran pelanggan menurut kota",
x = NULL, y = "Jumlah pelanggan"
) +
theme_minimal(base_size = 13) +
ylim(0, max(kota_ringkas$n) * 1.18)transaksi_ringkas <- data_terintegrasi %>%
summarise(
pelanggan = n(),
total_transaksi = sum(jumlah_transaksi_final),
total_pembelian = sum(total_purchase_final),
median_transaksi = median(jumlah_transaksi_final),
median_pembelian = median(total_purchase_final)
)
kable(transaksi_ringkas, digits = 2)| pelanggan | total_transaksi | total_pembelian | median_transaksi | median_pembelian |
|---|---|---|---|---|
| 111 | 1124 | 462490000 | 10 | 3710000 |
ggplot(
data_terintegrasi,
aes(
x = jumlah_transaksi_final,
y = total_purchase_final,
size = pendapatan_winsor,
shape = status
)
) +
geom_point(alpha = .7) +
scale_y_continuous(labels = label_number(big.mark = ".")) +
scale_size_continuous(labels = label_number(big.mark = ".")) +
labs(
title = "Frekuensi transaksi vs nilai pembelian",
x = "Jumlah transaksi",
y = "Total pembelian (Rp)",
size = "Pendapatan (Rp)",
shape = "Status"
) +
theme_minimal(base_size = 13)cor_data <- data_terintegrasi %>%
select(pendapatan_winsor, jumlah_transaksi_final, total_purchase_final) %>%
cor(use = "complete.obs")
kable(round(cor_data, 3),
caption = "Korelasi antarvariabel numerik utama")| pendapatan_winsor | jumlah_transaksi_final | total_purchase_final | |
|---|---|---|---|
| pendapatan_winsor | 1.000 | 0.188 | 0.096 |
| jumlah_transaksi_final | 0.188 | 1.000 | 0.622 |
| total_purchase_final | 0.096 | 0.622 | 1.000 |
model <- lm(
total_purchase_final ~ jumlah_transaksi_final + pendapatan_winsor,
data = data_terintegrasi
)
summary(model)##
## Call:
## lm(formula = total_purchase_final ~ jumlah_transaksi_final +
## pendapatan_winsor, data = data_terintegrasi)
##
## Residuals:
## Min 1Q Median 3Q Max
## -7889243 -830961 -188719 417160 22541476
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 7.374e+05 2.284e+06 0.323 0.747
## jumlah_transaksi_final 4.024e+05 4.928e+04 8.165 6.5e-13 ***
## pendapatan_winsor -1.280e-01 4.571e-01 -0.280 0.780
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 2990000 on 108 degrees of freedom
## Multiple R-squared: 0.3874, Adjusted R-squared: 0.3761
## F-statistic: 34.15 on 2 and 108 DF, p-value: 3.217e-12
Korelasi dan regresi di sini digunakan sebagai eksplorasi, bukan bukti kausalitas. Fokusnya adalah melihat pola yang dapat menjadi petunjuk untuk analisis lanjutan.
aktif <- mean(data_terintegrasi$status == "Aktif") * 100
median_beli <- median(data_terintegrasi$total_purchase_final)
median_trx <- median(data_terintegrasi$jumlah_transaksi_final)
n_outlier <- sum(data_terintegrasi$outlier_pendapatan)
cat(
'<div class="kpi"><b>', round(aktif,1), '%</b><br>pelanggan aktif</div>',
'<div class="kpi"><b>', round(median_trx,1), '</b><br>median transaksi</div>',
'<div class="kpi"><b>Rp ', format(round(median_beli), big.mark="."), '</b><br>median pembelian</div>',
'<div class="kpi"><b>', n_outlier, '</b><br>kandidat outlier pendapatan</div>'
)🔎 Temuan 1 — Kualitas data bukan masalah
kosmetik.
Kesalahan kategori, duplikasi, missing value, dan nilai ekstrem
dapat mengubah statistik dasar. Jika data langsung dianalisis tanpa
preprocessing, angka rata-rata dan hasil visual dapat memberi gambaran
yang menyesatkan.
🔎 Temuan 2 — Pendapatan memiliki ekor
ekstrem.
Satu atau beberapa nilai sangat besar membuat skala pendapatan menjadi
tidak seimbang. Median dan pendekatan robust lebih informatif untuk
menggambarkan pelanggan “tipikal”.
🔎 Temuan 3 — Aktivitas transaksi perlu dibaca bersama nilai
pembelian.
Pelanggan dengan transaksi banyak belum tentu menghasilkan nilai
pembelian terbesar. Karena itu, segmentasi pelanggan sebaiknya tidak
hanya memakai satu indikator.
🔎 Temuan 4 — NA pada transaksi memiliki makna
bisnis.
Mengubah NA menjadi nol bukan sekadar operasi teknis.
Keputusan tersebut harus didukung definisi bahwa pelanggan benar-benar
belum bertransaksi.
🔎 Temuan 5 — Preprocessing menghasilkan data yang lebih siap
dianalisis.
Setelah standardisasi, deduplikasi, koreksi domain, imputasi, deteksi
outlier, transformasi, dan integrasi, data menjadi lebih konsisten untuk
eksplorasi pola pelanggan.
ringkasan_perubahan <- data.frame(
indikator = c(
"Baris mentah",
"Baris setelah deduplikasi",
"ID duplikat awal",
"Missing usia awal",
"Missing pendapatan awal",
"Missing kota awal",
"Kandidat outlier pendapatan"
),
nilai = c(
nrow(pelanggan_raw),
nrow(pelanggan),
sum(duplicated(pelanggan_raw$customer_id)),
sum(is.na(pelanggan_raw$usia)),
sum(is.na(pelanggan_raw$pendapatan)),
sum(is.na(pelanggan_raw$kota)),
sum(data_terintegrasi$outlier_pendapatan)
)
)
kable(ringkasan_perubahan)| indikator | nilai |
|---|---|
| Baris mentah | 112 |
| Baris setelah deduplikasi | 111 |
| ID duplikat awal | 1 |
| Missing usia awal | 2 |
| Missing pendapatan awal | 4 |
| Missing kota awal | 2 |
| Kandidat outlier pendapatan | 3 |
data_final <- data_terintegrasi %>%
select(
customer_id,
nama,
usia = usia_imputasi,
kota = kota_imputasi,
status,
pendapatan = pendapatan_imputasi,
pendapatan_missing,
outlier_pendapatan,
jumlah_transaksi = jumlah_transaksi_final,
total_purchase = total_purchase_final,
pendapatan_winsor
)
kable(head(data_final, 20), digits = 2,
caption = "20 observasi pertama dataset final")| customer_id | nama | usia | kota | status | pendapatan | pendapatan_missing | outlier_pendapatan | jumlah_transaksi | total_purchase | pendapatan_winsor |
|---|---|---|---|---|---|---|---|---|---|---|
| C001 | Ani | 21 | Pekanbaru | Aktif | 4.50e+06 | 0 | FALSE | 5 | 1500000 | 4500000 |
| C002 | Budi | 25 | Pekanbaru | Aktif | 4.95e+06 | 1 | FALSE | 3 | 900000 | 4950000 |
| C003 | Citra | 23 | Pekanbaru | Aktif | 5.20e+06 | 0 | FALSE | 7 | 2700000 | 5200000 |
| C004 | Dodi | 50 | Dumai | Aktif | 4.80e+06 | 0 | FALSE | 2 | 600000 | 4800000 |
| C005 | Eka | 27 | Pekanbaru | Tidak Aktif | 4.90e+06 | 0 | FALSE | 6 | 2100000 | 4900000 |
| C006 | Fani | 30 | Dumai | Tidak Aktif | 5.10e+06 | 0 | FALSE | 4 | 1300000 | 5100000 |
| C007 | Gilang | 31 | Pekanbaru | Aktif | 5.00e+08 | 0 | TRUE | 20 | 25000000 | 6597500 |
| C008 | Hana | 29 | Siak | Aktif | 4.70e+06 | 0 | FALSE | 5 | 1700000 | 4700000 |
| C009 | Indra | 22 | Pekanbaru | Aktif | 4.60e+06 | 0 | FALSE | 3 | 800000 | 4600000 |
| C010 | Joko | 35 | Dumai | Tidak Aktif | 5.30e+06 | 0 | FALSE | 8 | 3200000 | 5300000 |
| C011 | Kiki | 28 | Tidak diketahui | Aktif | 4.95e+06 | 1 | FALSE | 0 | 0 | 4950000 |
| C012 | Lala_012 | 39 | Siak | Aktif | 5.72e+06 | 0 | FALSE | 1 | 250000 | 5720000 |
| C013 | Vina_013 | 26 | Dumai | Tidak Aktif | 4.48e+06 | 0 | FALSE | 11 | 2860000 | 4480000 |
| C014 | Citra_014 | 30 | Pekanbaru | Tidak Aktif | 4.70e+06 | 0 | FALSE | 5 | 2700000 | 4700000 |
| C015 | Fani_015 | 31 | Pekanbaru | Aktif | 6.01e+06 | 0 | FALSE | 14 | 6160000 | 6010000 |
| C016 | Budi_016 | 23 | Siak | Tidak Aktif | 4.56e+06 | 0 | FALSE | 14 | 5460000 | 4560000 |
| C017 | Nanda_017 | 33 | Dumai | Tidak Aktif | 5.90e+06 | 0 | FALSE | 13 | 6500000 | 5900000 |
| C018 | Budi_018 | 26 | Pekanbaru | Tidak Aktif | 5.01e+06 | 0 | FALSE | 17 | 7650000 | 5010000 |
| C019 | Joko_019 | 150 | Pekanbaru | Aktif | 5.89e+06 | 0 | FALSE | 10 | 3400000 | 5890000 |
| C020 | Maya_020 | 35 | Dumai | Aktif | 5.43e+06 | 0 | FALSE | 12 | 3360000 | 5430000 |
## Jumlah observasi final : 111
## Jumlah variabel final : 11
## ID unik : 111
## Missing final : 0
1. Apakah dataset tanpa missing value selalu lebih
berkualitas?
Tidak selalu. Dataset tanpa missing value belum tentu lebih
berkualitas karena nilai yang hilang dapat memiliki makna penting.
Menghapus semua baris yang memiliki missing value juga dapat
mengurangi ukuran sampel dan menghilangkan informasi. Pada praktikum
ini, missing value ditangani dengan imputasi median untuk
variabel numerik dan kategori “Tidak diketahui” untuk
kota. Dengan demikian, data tetap dapat digunakan tanpa langsung
membuang seluruh observasi.
2. Mengapa outlier tidak boleh otomatis
dihapus?
Karena outlier tidak selalu merupakan kesalahan. Nilai ekstrem dapat
berasal dari kesalahan input, tetapi dapat juga merupakan observasi yang
valid atau menunjukkan pelanggan dengan karakteristik berbeda. Oleh
karena itu, outlier harus diperiksa berdasarkan konteks dan tujuan
analisis. Dalam praktikum ini, kandidat outlier pendapatan tidak
langsung dihapus, tetapi digunakan untuk mempertimbangkan pendekatan
winsorizing.
3. Bagaimana preprocessing dapat menimbulkan
bias?
Preprocessing dapat menimbulkan bias jika keputusan cleaning
mengubah representasi data secara tidak proporsional. Contohnya,
menghapus semua baris yang memiliki missing value dapat
mengurangi sampel dan berpotensi menghilangkan kelompok tertentu.
Imputasi atau transformasi yang tidak sesuai juga dapat mengubah
distribusi data. Karena itu, setiap keputusan preprocessing harus
didasarkan pada konteks data dan tujuan analisis.
4. Mengapa parameter imputasi dan transformasi seharusnya
dihitung dari data pelatihan?
Agar informasi dari data pengujian tidak ikut memengaruhi proses
pelatihan model. Jika median, rata-rata, simpangan baku, nilai minimum,
atau maksimum dihitung dari seluruh data sebelum pembagian
training dan testing, dapat terjadi data
leakage. Akibatnya, performa model terlihat lebih baik daripada
kondisi sebenarnya. Karena itu, parameter preprocessing sebaiknya
dipelajari dari data pelatihan, kemudian diterapkan pada data
pengujian.
5. Apa risiko integrasi data jika identifier tidak
unik?
Identifier yang tidak unik dapat menyebabkan satu pelanggan dipasangkan
dengan beberapa baris ketika proses merge dilakukan. Akibatnya,
jumlah baris dapat bertambah secara tidak semestinya, transaksi dapat
terhitung berulang, dan statistik pelanggan menjadi bias. Oleh karena
itu, keunikan customer_id perlu diperiksa sebelum
integrasi. Pada praktikum ini, duplikasi customer_id
diperiksa dan kemudian dilakukan deduplikasi dengan asumsi bahwa satu ID
merepresentasikan satu pelanggan.
Preprocessing bukan sekadar serangkaian fungsi R. Setiap perubahan memiliki konsekuensi terhadap makna data. Dataset yang baik adalah dataset yang konsisten, dapat ditelusuri perubahanannya, dan sesuai dengan definisi bisnis.
Dalam versi 112 observasi ini, proses dibuat lebih dekat dengan situasi nyata. Ada data yang hilang, kategori yang tidak seragam, duplikasi, kesalahan domain, outlier, dua sumber data, serta kebutuhan memilih transformasi. Alur tersebut mengikuti cakupan materi asli yang menempatkan data cleaning, missing values, outlier, transformasi, dan integrasi data sebagai kompetensi inti preprocessing.
Pesan akhirnya: data yang terlihat rapi belum tentu benar. Data yang dipahami, diperiksa, dan didokumentasikan jauh lebih berharga.