A · Mengapa Data Perlu Dipersiapkan?

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.

B · Menyusun Dataset Awal

# ==========================================================
# 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_raw

C · Pemeriksaan Awal

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

D · Menyeragamkan Identitas Sebelum Analisis

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

E · Deduplikasi

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.

F · Nilai Usia Tidak Wajar

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

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.

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

G · Nilai Hilang

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.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
cat("Median pendapatan :", median_pendapatan, "\n")
## 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.

H · Outlier

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

I · Mengolah Skala Data

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.

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

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

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

J · Saat Nilai Ekstrem Mempengaruhi 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.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.

K · Menyatukan Data Pelanggan dan 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 : 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
cat("Baris sesudah integrasi :", nrow(data_terintegrasi), "\n")
## Baris sesudah integrasi : 110
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. fileciteturn4file0L226-L245

L · Menelusuri Karakteristik Pelanggan

Kelompok Mana yang Paling Menonjol?

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

Aktivitas Transaksi Memberi Gambaran Tambahan

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)

M · Apakah Pendapatan Tinggi Selaras dengan Aktivitas?

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

N · Temuan Penting

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

O · Rekap Perjalanan 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 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

P · Dataset Akhir

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

Q · Catatan Reflektif

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.

R · Penutup

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.