🎯 Misi Praktikum
Mengubah data pelanggan yang masih “berantakan” menjadi data yang konsisten, dapat ditelusuri, dan siap digunakan untuk menemukan insight.

01) Mengapa Data Perlu “PREPROCESSING”?

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.

02) Membangun Dunia Data

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.

dim(pelanggan_raw)
## [1] 112   6
head(pelanggan_raw, 10)

03) Potret Pertama : Apa yang Terjadi dalam Data?

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")
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
cat("Jumlah baris mentah :", nrow(pelanggan_raw), "\n")
## Jumlah baris mentah : 112
cat("Jumlah ID unik      :", length(unique(pelanggan_raw$customer_id)), "\n")
## Jumlah ID unik      : 111
cat("Duplikasi ID        :", sum(duplicated(pelanggan_raw$customer_id)), "\n")
## Duplikasi ID        : 1
cat("Missing usia        :", sum(is.na(pelanggan_raw$usia)), "\n")
## Missing usia        : 2
cat("Missing pendapatan  :", sum(is.na(pelanggan_raw$pendapatan)), "\n")
## Missing pendapatan  : 4
cat("Missing kota        :", sum(is.na(pelanggan_raw$kota)), "\n")
## 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.

04) Rapikan Identitas Sebelum Membaca Perilaku

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"
sort(unique(pelanggan$status))
## [1] "Aktif"       "Tidak Aktif"

05) Deduplikasi : Satu Baris Terlalu Banyak

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.

06) Usia 150 tahun? Berhenti sebentar.

pelanggan[pelanggan$usia < 15 | pelanggan$usia > 100, ]

Nilai usia 150 diperlakukan sebagai kesalahan input. Nilai usia 150 diperlakukan sebagai kesalahan input dan dikoreksi berdasarkan konteks data.

pelanggan$usia[pelanggan$customer_id == "C004"] <- 50

07) Missing Value : Jangan Buru-Buru Membuang 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")
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
cat("Median pendapatan :", median_pendapatan, "\n")
## 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.

08) Outlier: Angka Aneh Atau Pelanggan yang Memang Berbeda?

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"
)
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

09) Transformasi Data : Smoothing, Normalisasi, dan Standardisasi

Transformasi bukan berarti “mengubah data sesuka hati”. Tujuannya adalah membuat representasi data lebih mudah dibaca tanpa kehilangan makna data.

〰️ Smoothing

Mengurangi variasi lokal untuk membantu melihat kecenderungan umum.

↔︎️ Min–Max

Membawa nilai ke skala 0–1 agar perbedaan skala lebih mudah dibandingkan.

σ Z-score

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 dengan moving average

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"
)
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

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"
)
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 Z-score

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"
)
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.

10) Ketika Satu Angka Menguasai Seluruh Skala

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.

11) Menggabungkan Dua Dunia : Pelanggan + Transaksi

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
cat("Transaksi tanpa pelanggan :", paste(id_transaksi_tanpa_pelanggan, collapse = ", "), "\n")
## 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
cat("Baris sesudah integrasi :", nrow(data_terintegrasi), "\n")
## Baris sesudah integrasi : 111
cat("ID duplikat             :",
    sum(duplicated(data_terintegrasi$customer_id)))
## 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.

12) “Membaca” Pelanggan

Siapa yang paling dominan?

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
kable(kota_ringkas, digits = 2)
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)

Aktivitas transaksi berbicara lebih keras

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)

13) Apakah Pelanggan Berpendapatan Tinggi Juga Paling Aktif?

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")
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.

14) Insight Utama

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>'
)
63.1 %
pelanggan aktif
10
median transaksi
Rp 3.710.000
median pembelian
3
kandidat outlier pendapatan

🔎 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.

15) Ringkasan Perubahan Data

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

16) Data Final yang Siap Digunakan

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")
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
cat("Jumlah observasi final :", nrow(data_final), "\n")
## Jumlah observasi final : 111
cat("Jumlah variabel final  :", ncol(data_final), "\n")
## Jumlah variabel final  : 11
cat("ID unik                :", length(unique(data_final$customer_id)), "\n")
## ID unik                : 111
cat("Missing final          :", sum(is.na(data_final)), "\n")
## Missing final          : 0

17) Refleksi

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.

18) Penutup : Dari “Data Berantakan” Menjadi Cerita

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.