Praktikum ini mengambil gambaran sebuah perusahaan e-commerce yang sedang mencoba mengenali karakter pelanggannya. Tujuannya bukan hanya membuat tabel terlihat rapi, melainkan memperlihatkan bahwa kondisi data dapat menentukan seberapa tepat kesimpulan yang dihasilkan.
Analisis memanfaatkan dua tabel utama. Tabel pelanggan berisi informasi identitas, usia, wilayah, pendapatan, dan status, sedangkan tabel transaksi mencatat frekuensi transaksi serta nilai pembelian. Sejak awal, dataset sengaja diberi beberapa persoalan seperti missing value, kategori yang tidak seragam, data ganda, nilai ekstrem, dan ketidaksesuaian identifier.
Pada pengembangan ini digunakan 110 observasi pelanggan supaya alur pengolahan terasa lebih mendekati data nyata. Berbagai gangguan kualitas tetap disisipkan agar setiap tahap cleaning dapat diamati dan dijelaskan.
# ==========================================================
# DATA PELANGGAN
# 12 observasi pertama dipertahankan persis seperti sumber awal
# ==========================================================
pelanggan_awal <- 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
)
# Menambahkan 99 observasi baru TANPA mengubah 12 observasi pertama
set.seed(2408)
tambahan <- data.frame(
customer_id = sprintf("C%03d", 12:110),
nama = paste0(
sample(c("Lina","Maya","Nanda","Oki","Putri","Raka","Sari",
"Tio","Vina","Wahyu","Yuni","Zaki"), 99, replace = TRUE),
"_", sprintf("%03d", 12:110)
),
usia = round(pmin(pmax(rnorm(99, 30, 6), 18), 60)),
pendapatan = round(rlnorm(99, log(5200000), .14), -4),
kota = sample(c("Pekanbaru", "Dumai", "Siak"), 99,
replace = TRUE, prob = c(.50, .32, .18)),
status = sample(c("Aktif", "Tidak Aktif"), 99,
replace = TRUE, prob = c(.70, .30)),
stringsAsFactors = FALSE
)
# Dataset pelanggan mentah: 12 data asli berada PALING ATAS
pelanggan_raw <- rbind(pelanggan_awal, tambahan)
# ==========================================================
# DATA TRANSAKSI
# 11 observasi transaksi dari sumber awal dipertahankan persis
# ==========================================================
transaksi_awal <- 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
)
# Tambahan transaksi untuk pelanggan lainnya
transaksi_tambahan <- data.frame(
cust_id = sprintf("C%03d", 13:111),
jumlah_transaksi = sample(1:18, 99, replace = TRUE),
total_purchase = 0,
stringsAsFactors = FALSE
)
transaksi_tambahan$total_purchase <-
transaksi_tambahan$jumlah_transaksi *
round(runif(99, 250000, 550000), -4)
# Gabungkan transaksi dengan 11 data awal tetap di posisi pertama
transaksi_raw <- rbind(transaksi_awal, transaksi_tambahan)
# Nilai ekstrem tambahan untuk memperlihatkan proses pemeriksaan outlier
transaksi_raw$jumlah_transaksi[
which.max(transaksi_raw$jumlah_transaksi)
] <- 40
transaksi_raw$total_purchase[
which.max(transaksi_raw$total_purchase)
] <- 30000000
pelanggan_rawaudit_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.0 | 110 |
| nama | character | 0 | 0.0 | 110 |
| usia | numeric | 1 | 0.9 | 26 |
| pendapatan | numeric | 2 | 1.8 | 96 |
| kota | character | 1 | 0.9 | 10 |
| status | character | 0 | 0.0 | 8 |
## Jumlah baris mentah : 111
## Jumlah ID unik : 110
## Duplikasi ID : 1
## Missing usia : 1
## Missing pendapatan : 2
## Missing kota : 1
Audit dipakai sebagai sinyal awal untuk menemukan bagian yang perlu ditinjau. Hasil audit tidak otomatis menentukan cara memperbaiki data; setiap temuan tetap perlu dilihat berdasarkan konteks dan karakteristik variabel.
Variabel kota dan status masih memiliki format penulisan yang
beragam. Contohnya, PKU, Pekanbaru, perbedaan
kapitalisasi, dan spasi dapat terbaca sebagai kategori yang berbeda
sebelum dilakukan penyeragaman.
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: 110
Penghapusan duplikasi berdasarkan customer_id masuk akal
apabila satu ID memang hanya mewakili satu pelanggan.
Tanpa memahami aturan identitas tersebut, deduplikasi justru berisiko
menghilangkan baris yang sebenarnya valid.
Angka usia 150 dianggap tidak sesuai dengan konteks variabel sehingga diperlakukan sebagai kesalahan pencatatan. Dalam latihan ini, nilai tersebut kemudian dikoreksi mengikuti nilai yang telah ditetapkan pada sumber latihan.
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.00 |
| nama | nama | 0 | 0.00 |
| usia | usia | 1 | 0.91 |
| pendapatan | pendapatan | 2 | 1.82 |
| kota | kota | 1 | 0.91 |
| status | status | 0 | 0.00 |
Membuang semua observasi yang memiliki nilai kosong dapat membuat jumlah data berkurang cukup banyak. Pada contoh sebelumnya, pendekatan tersebut bahkan mengurangi sekitar 27,27% observasi.
Pada 100 data pelanggan ini, nilai numerik yang hilang diisi menggunakan median, sedangkan kota yang belum diketahui diberi kategori khusus agar observasi tetap dapat dipertahankan.
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 : 29
## Median pendapatan : 5180000
Median dipilih karena variabel pendapatan mengandung nilai yang sangat tinggi. Berbeda dengan mean yang mudah tertarik oleh nilai ekstrem seperti 500 juta, median lebih stabil dalam merepresentasikan bagian tengah mayoritas pelanggan.
ggplot(pelanggan, aes(y = pendapatan_imputasi)) +
geom_boxplot(fill = "#ED78AD", 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 |
| 17 | C017 | Nanda_017 | 7.16e+06 |
| 109 | C109 | Nanda_109 | 7.41e+06 |
Kemunculan outlier belum cukup menjadi alasan untuk menghapusnya. Nilai tersebut bisa saja merupakan kesalahan pencatatan, data yang benar tetapi sangat ekstrem, atau tanda adanya kelompok pelanggan tertentu. Karena itu, keputusan penanganannya perlu mengikuti konteks analisis.
Agar pengaruh penanganan outlier dapat dibandingkan, dibuat kolom winsorized sambil tetap menyimpan nilai pendapatan sebelumnya.
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 | 7088750 |
| 17 | C017 | 7.16e+06 | 7088750 |
| 109 | C109 | 7.41e+06 | 7088750 |
Transformasi dilakukan bukan untuk mengubah makna data secara sembarangan. Fungsinya adalah menghasilkan bentuk representasi yang lebih sesuai untuk eksplorasi atau perbandingan dengan tetap menjaga informasi dasarnya.
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 asli.
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 asli.
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 | 5.18e+06 | 4960000 |
| C003 | 5.20e+06 | 5060000 |
| 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 | 5026667 |
| C011 | 5.18e+06 | 5213333 |
| C012 | 5.16e+06 | 5056667 |
Nilai pendapatan_imputasi tetap dipertahankan. Kolom
pendapatan_smoothing hanya digunakan untuk melihat pola
yang lebih halus.
Normalisasi dengan Metode 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 | 5.18e+06 | 0.003 |
| C003 | 23 | 0.038 | 5.20e+06 | 0.003 |
| C004 | 150 | 1.000 | 4.80e+06 | 0.002 |
| C005 | 27 | 0.068 | 4.90e+06 | 0.002 |
| C006 | 29 | 0.083 | 5.10e+06 | 0.003 |
| C007 | 31 | 0.098 | 5.00e+08 | 1.000 |
| C008 | 50 | 0.242 | 4.70e+06 | 0.002 |
| C009 | 22 | 0.030 | 4.60e+06 | 0.002 |
| C010 | 35 | 0.129 | 5.30e+06 | 0.003 |
| C011 | 28 | 0.076 | 5.18e+06 | 0.003 |
| C012 | 31 | 0.098 | 5.16e+06 | 0.003 |
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.732 | 4.50e+06 | -0.111 |
| C002 | 25 | -0.420 | 5.18e+06 | -0.097 |
| C003 | 23 | -0.576 | 5.20e+06 | -0.096 |
| C004 | 150 | 9.326 | 4.80e+06 | -0.105 |
| C005 | 27 | -0.264 | 4.90e+06 | -0.102 |
| C006 | 29 | -0.108 | 5.10e+06 | -0.098 |
| C007 | 31 | 0.047 | 5.00e+08 | 10.392 |
| C008 | 50 | 1.529 | 4.70e+06 | -0.107 |
| C009 | 22 | -0.654 | 4.60e+06 | -0.109 |
| C010 | 35 | 0.359 | 5.30e+06 | -0.094 |
| C011 | 28 | -0.186 | 5.18e+06 | -0.097 |
| C012 | 31 | 0.047 | 5.16e+06 | -0.097 |
Jika transformasi digunakan dalam pemodelan, parameter seperti mean, simpangan baku, minimum, dan maksimum sebaiknya dipelajari dari data pelatihan saja. Langkah ini membantu mencegah 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 data asli 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.0016723 | -0.1109705 |
| C002 | 5.18e+06 | 0.0030423 | -0.0965572 |
| C003 | 5.20e+06 | 0.0030826 | -0.0961333 |
| C004 | 4.80e+06 | 0.0022767 | -0.1046117 |
| C005 | 4.90e+06 | 0.0024782 | -0.1024921 |
| C006 | 5.10e+06 | 0.0028811 | -0.0982529 |
| C007 | 5.00e+08 | 1.0000000 | 10.3916271 |
| C008 | 4.70e+06 | 0.0020752 | -0.1067313 |
| C009 | 4.60e+06 | 0.0018738 | -0.1088509 |
| C010 | 5.30e+06 | 0.0032841 | -0.0940137 |
| C011 | 5.18e+06 | 0.0030423 | -0.0965572 |
| C012 | 5.16e+06 | 0.0030020 | -0.0969811 |
Metode Min–Max memetakan nilai ke interval 0–1, sementara z-score menyatakan jarak sebuah nilai dari rata-rata dalam satuan simpangan baku. Perlu diperhatikan bahwa Min–Max sangat mudah terpengaruh oleh nilai ekstrem.
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)Pembacaan grafik: nilai ekstrem yang jauh dari sebagian besar data dapat membuat observasi lain tampak menumpuk di dekat nol setelah normalisasi. Hal ini menunjukkan mengapa pemeriksaan outlier penting sebelum menentukan metode 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 : C111
Fungsi setdiff() membantu menemukan identifier yang
hanya terdapat pada salah satu tabel. Dengan cara ini, pelanggan tanpa
transaksi maupun transaksi tanpa pasangan data pelanggan dapat segera
dikenali.
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 : 110
## Baris sesudah integrasi : 110
## 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.
fileciteturn4file0L226-L245
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 | 74 | 67.27 |
| Tidak Aktif | 36 | 32.73 |
| kota_imputasi | n | persen |
|---|---|---|
| Dumai | 29 | 26.36 |
| Pekanbaru | 61 | 55.45 |
| Siak | 19 | 17.27 |
| Tidak diketahui | 1 | 0.91 |
ggplot(kota_ringkas,
aes(x = reorder(kota_imputasi, n), y = n)) +
geom_col(fill = "#D85B98", 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 |
|---|---|---|---|---|
| 110 | 1072 | 436090000 | 10 | 3690000 |
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.206 | 0.228 |
| jumlah_transaksi_final | 0.206 | 1.000 | 0.893 |
| total_purchase_final | 0.228 | 0.893 | 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
## -3853496 -690604 217364 788155 9794798
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) -2.285e+06 1.075e+06 -2.125 0.0359 *
## jumlah_transaksi_final 5.235e+05 2.625e+04 19.940 <2e-16 ***
## pendapatan_winsor 2.185e-01 2.074e-01 1.054 0.2943
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 1490000 on 107 degrees of freedom
## Multiple R-squared: 0.799, Adjusted R-squared: 0.7952
## F-statistic: 212.7 on 2 and 107 DF, p-value: < 2.2e-16
Korelasi dan regresi pada bagian ini bersifat eksploratif. Hasilnya dipakai untuk membaca kecenderungan hubungan antarvariabel, bukan untuk menyatakan bahwa satu variabel pasti menyebabkan variabel lainnya.
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>'
)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 | 111 |
| Baris setelah deduplikasi | 110 |
| ID duplikat awal | 1 |
| Missing usia awal | 1 |
| Missing pendapatan awal | 2 |
| Missing kota awal | 1 |
| 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 | 5.18e+06 | 1 | FALSE | 3 | 900000 | 5180000 |
| C003 | Citra | 23 | Pekanbaru | Aktif | 5.20e+06 | 0 | FALSE | 7 | 2700000 | 5200000 |
| C004 | Dodi | 150 | 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 | 29 | Dumai | Tidak Aktif | 5.10e+06 | 0 | FALSE | 4 | 1300000 | 5100000 |
| C007 | Gilang | 31 | Pekanbaru | Aktif | 5.00e+08 | 0 | TRUE | 40 | 30000000 | 7088750 |
| C008 | Hana | 50 | 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 | 5.18e+06 | 1 | FALSE | 0 | 0 | 5180000 |
| C012 | Vina_012 | 31 | Pekanbaru | Tidak Aktif | 5.16e+06 | 0 | FALSE | 1 | 250000 | 5160000 |
| C013 | Zaki_013 | 42 | Pekanbaru | Aktif | 4.83e+06 | 0 | FALSE | 3 | 1410000 | 4830000 |
| C014 | Zaki_014 | 35 | Siak | Aktif | 4.20e+06 | 0 | FALSE | 6 | 2400000 | 4200000 |
| C015 | Tio_015 | 30 | Dumai | Tidak Aktif | 6.23e+06 | 0 | FALSE | 3 | 1080000 | 6230000 |
| C016 | Oki_016 | 33 | Pekanbaru | Tidak Aktif | 5.13e+06 | 0 | FALSE | 10 | 3700000 | 5130000 |
| C017 | Nanda_017 | 29 | Siak | Tidak Aktif | 7.16e+06 | 0 | TRUE | 10 | 2600000 | 7088750 |
| C018 | Raka_018 | 27 | Dumai | Aktif | 5.34e+06 | 0 | FALSE | 2 | 900000 | 5340000 |
| C019 | Maya_019 | 26 | Pekanbaru | Aktif | 5.52e+06 | 0 | FALSE | 5 | 1600000 | 5520000 |
| C020 | Raka_020 | 36 | Pekanbaru | Aktif | 4.92e+06 | 0 | FALSE | 8 | 2960000 | 4920000 |
## Jumlah observasi final : 110
## Jumlah variabel final : 11
## ID unik : 110
## Missing final : 0
1. Apakah dataset tanpa missing value selalu lebih
berkualitas?
Belum tentu. Ketiadaan missing value tidak otomatis menandakan
kualitas data lebih baik karena nilai kosong dapat berkaitan dengan
proses pengumpulan data. Menghapus semua baris yang tidak lengkap juga
berisiko mengecilkan sampel dan menghilangkan informasi tertentu. Di
praktikum ini, nilai numerik diisi menggunakan median, sedangkan kota
yang kosong diberi label “Tidak diketahui” sehingga
observasi masih dapat dimanfaatkan.
2. Mengapa outlier tidak boleh otomatis
dihapus?
Outlier tidak selalu identik dengan kesalahan. Nilai ekstrem mungkin
muncul karena salah input, tetapi bisa pula benar-benar menggambarkan
pelanggan dengan karakteristik khusus. Pemeriksaannya perlu
mempertimbangkan konteks data dan tujuan analisis. Pada latihan ini,
kandidat outlier pendapatan tidak dihapus begitu saja, melainkan
digunakan untuk melihat kemungkinan penerapan winsorizing.
3. Bagaimana preprocessing dapat menimbulkan
bias?
Bias dapat muncul ketika aturan cleaning mengubah komposisi
data secara tidak seimbang. Misalnya, menghapus semua observasi yang
memiliki missing value dapat membuat kelompok tertentu lebih
banyak hilang. Imputasi dan transformasi yang keliru juga dapat
menggeser distribusi. Oleh sebab itu, metode preprocessing harus dipilih
dengan mempertimbangkan konteks serta tujuan analisis.
4. Mengapa parameter imputasi dan transformasi seharusnya
dihitung dari data pelatihan?
Tujuannya adalah mencegah informasi dari data pengujian ikut masuk ke
proses pembelajaran. Jika median, rata-rata, simpangan baku, minimum,
atau maksimum dihitung menggunakan seluruh data sebelum
training dan testing dipisahkan, dapat terjadi
data leakage. Model kemudian bisa tampak lebih baik daripada
performa sebenarnya. Karena itu, parameter preprocessing perlu
dipelajari dari data training dan baru diterapkan ke data
testing.
5. Apa risiko integrasi data jika identifier tidak
unik?
Identifier yang tidak unik berpotensi membuat satu pelanggan terhubung
dengan lebih dari satu baris ketika merge dilakukan. Dampaknya
dapat berupa penambahan baris yang tidak semestinya, transaksi yang
terhitung ganda, dan statistik yang bias. Karena itu, keunikan
customer_id harus diperiksa sebelum penggabungan. Dalam
latihan ini, ID yang berulang ditinjau lalu dideduplikasi dengan asumsi
satu ID mewakili satu pelanggan.
Preprocessing lebih dari sekadar menjalankan fungsi-fungsi di R. Setiap keputusan pengolahan dapat memengaruhi interpretasi data. Dataset yang layak dianalisis perlu konsisten, perubahan datanya dapat dilacak, dan definisinya selaras dengan kebutuhan bisnis.