Skenario Kasus

Sistem basis data sebuah perusahaan e-commerce baru saja menarik dua berkas data mentah untuk kebutuhan analitik: Data Pelanggan (demografi dan finansial) serta Data Transaksi (aktivitas belanja).


  • data pelanggan, yang memuat identitas, usia, kota, pendapatan, dan status pelanggan;
  • data transaksi, yang memuat jumlah transaksi dan total pembelian.

Namun, data tersebut dalam kondisi sangat kotor. Terlihat penulisan ID yang tidak beraturan, entri duplikat, variasi ejaan nama kota, hingga angka usia dan pendapatan yang tidak masuk akal. Sebelum analisis segmentasi dapat dijalankan, kedua sumber data ini harus dibersihkan, diimputasi, dan diintegrasikan terlebih dahulu hingga menjadi satu dataset siap pakai.

Membangun Dataset Kasus

# ---------------------------------------------------------
# 1. Dataset Pelanggan
# ---------------------------------------------------------
pelanggan_raw <- data.frame(
  customer_id = c("C001", "C002", "C003", "C004", "C005", "C006", "C007", "C008", "C009", "C010", 
                  "C010", "C011", "c013", "C-014", "C015 ", "C016", "C017", "C018", "C019", "C020",
                  "C021", "C022", "C023", "C024", "C025"),
  nama = c("Ani", "Budi", "Citra", "Dodi", "Eka", "Fani", "Gilang", "Hana", "Indra", "Joko", 
           "Joko", "Kiki", "Lani", "Maulana", "Novi", "Omar", "Putri", "Qori", "Rian", "Sari",
           "Tono", "Umar", "Vina", "Wawan", "Yulia"),
  usia = c(21, 25, 23, 150, 27, NA, 31, 29, 22, 35, 
           35, 28, -5, 30, 999, 24, NA, 40, 19, 33, 
           26, 45, 22, NA, 30),
  pendapatan = c(4500000, NA, 5200000, 4800000, 4900000, 5100000, 500000000, 4700000, 4600000, 5300000, 
                 5300000, NA, 3800000, 6000000, NA, 4200000, 5500000, 1200000000, 3900000, 5100000, 
                 4800000, NA, 4500000, 5000000, 0),
  kota = c("Pekanbaru", " PKU", "PEKANBARU", "Dumai", "pekanbaru", "DUMAI", "Pekanbaru ", "Siak", "PKU", "Dumai", 
           "Dumai", NA, "Bangkinang", " BANGKINANG", "Bkn", "SIAK ", "Siak", "Pekanbaru", "dumai", "PKU", 
           "Bangkinang ", "Dumai", NA, "Pekanbaru", "PKU"),
  status = c("Aktif", "aktif", "ACTIVE", "A", "Tidak Aktif", "nonaktif", "Aktif", "AKTIF", "A", "Tidak aktif", 
            "Tidak aktif", "Aktif", "Aktif", "N", "NON-AKTIF", "Aktif", " Pasif", "Aktif", "aktif", "Aktif", 
            "Non Aktif", "A", "Tidak Aktif", "AKTIF", NA),
  stringsAsFactors = FALSE
)

# --------------------
# 2. Dataset Transaksi
# --------------------
transaksi_raw <- data.frame(
  cust_id = c("C001", "C002", "C003", "C004", "C005", "C006", "C007", "C008", "C009", "C010", 
              "C012", "C013", "C014", "C015", "C016", "C099", "C100", "C019", "C020", "C020"),
  jumlah_transaksi = c(5, 3, 7, 2, 6, 4, 20, 5, 3, 8, 
                       1, 10, 2, 4, 99, 3, 1, 6, 2, 2),
  total_purchase = c(1500000, 900000, 2700000, 600000, 2100000, 1300000, 25000000, 1700000, 800000, 3200000, 
                     250000, 4500000, 850000, 1200000, 999999999, 1100000, 300000, 2200000, 750000, 750000),
  stringsAsFactors = FALSE
)
pelanggan_raw

Bagian I — Konsep Data Preprocessing dan Urgensi Data Preprocessing

1.1 Memahami struktur data

Sebelum melakukan perubahan, periksa struktur, dimensi, tipe atribut, dan beberapa observasi awal.

dim(pelanggan_raw)
## [1] 25  6
names(pelanggan_raw)
## [1] "customer_id" "nama"        "usia"        "pendapatan"  "kota"       
## [6] "status"
str(pelanggan_raw)
## 'data.frame':    25 obs. of  6 variables:
##  $ customer_id: chr  "C001" "C002" "C003" "C004" ...
##  $ nama       : chr  "Ani" "Budi" "Citra" "Dodi" ...
##  $ usia       : num  21 25 23 150 27 NA 31 29 22 35 ...
##  $ pendapatan : num  4.5e+06 NA 5.2e+06 4.8e+06 4.9e+06 5.1e+06 5.0e+08 4.7e+06 4.6e+06 5.3e+06 ...
##  $ kota       : chr  "Pekanbaru" " PKU" "PEKANBARU" "Dumai" ...
##  $ status     : chr  "Aktif" "aktif" "ACTIVE" "A" ...
head(pelanggan_raw)
summary(pelanggan_raw)
##  customer_id            nama                usia          pendapatan      
##  Length:25          Length:25          Min.   : -5.00   Min.   :0.00e+00  
##  Class :character   Class :character   1st Qu.: 23.25   1st Qu.:4.50e+06  
##  Mode  :character   Mode  :character   Median : 28.50   Median :4.90e+06  
##                                        Mean   : 76.77   Mean   :8.51e+07  
##                                        3rd Qu.: 34.50   3rd Qu.:5.30e+06  
##                                        Max.   :999.00   Max.   :1.20e+09  
##                                        NA's   :3        NA's   :4         
##      kota              status         
##  Length:25          Length:25         
##  Class :character   Class :character  
##  Mode  :character   Mode  :character  
##                                       
##                                       
##                                       
## 

1.2 Mengukur kualitas awal

Enam dimensi kualitas data yang dibahas Han, Kamber, dan Pei meliputi accuracy, completeness, consistency, timeliness, believability, dan interpretability. Tidak semua dimensi dapat dihitung hanya dari isi tabel. Misalnya, accuracy memerlukan pembanding terhadap kondisi sebenarnya dan timeliness membutuhkan informasi waktu pemutakhiran.

Pemeriksaan awal berikut berfokus pada masalah yang dapat dideteksi dari dataset.

# Banyak missing value per atribut
colSums(is.na(pelanggan_raw))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           3           4           2           1
# Persentase missing value per atribut
round(colMeans(is.na(pelanggan_raw)) * 100, 2)
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0          12          16           8           4
# Banyak baris duplikat penuh
sum(duplicated(pelanggan_raw))
## [1] 1
# Banyak customer_id yang berulang
sum(duplicated(pelanggan_raw$customer_id))
## [1] 1
# Kategori yang tercatat pada atribut kota dan status
sort(unique(pelanggan_raw$kota))
##  [1] " BANGKINANG" " PKU"        "Bangkinang"  "Bangkinang " "Bkn"        
##  [6] "dumai"       "Dumai"       "DUMAI"       "pekanbaru"   "Pekanbaru"  
## [11] "PEKANBARU"   "Pekanbaru "  "PKU"         "Siak"        "SIAK "
sort(unique(pelanggan_raw$status))
##  [1] " Pasif"      "A"           "ACTIVE"      "aktif"       "Aktif"      
##  [6] "AKTIF"       "N"           "NON-AKTIF"   "Non Aktif"   "nonaktif"   
## [11] "Tidak aktif" "Tidak Aktif"
# Rentang atribut numerik
range(pelanggan_raw$usia, na.rm = TRUE)
## [1]  -5 999
range(pelanggan_raw$pendapatan, na.rm = TRUE)
## [1] 0.0e+00 1.2e+09
# Cek string yang memiliki spasi di awal atau di akhir
sapply(pelanggan_raw[c("customer_id", "kota", "status")], function(x) {
  sum(grepl("^\\s+|\\s+$", x))
})
## customer_id        kota      status 
##           1           5           1
# Deteksi angka 0 pada pendapatan (kemungkinan missing value terselubung)
sum(pelanggan_raw$pendapatan == 0, na.rm = TRUE)
## [1] 1
# Deteksi string kosong ("")
sapply(pelanggan_raw, function(x) sum(x == "" | x == " ", na.rm = TRUE))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           0           0           0           0
# Cek customer_id yang tidak sesuai format standar "C000"
invalid_ids <- pelanggan_raw$customer_id[!grepl("^C[0-9]{3}$", pelanggan_raw$customer_id)]
invalid_ids
## [1] "c013"  "C-014" "C015 "
# Fungsi untuk menghitung jumlah outlier statistik per kolom numerik
sapply(pelanggan_raw[c("usia", "pendapatan")], function(x) {
  q1 <- quantile(x, 0.25, na.rm = TRUE)
  q3 <- quantile(x, 0.75, na.rm = TRUE)
  iqr <- q3 - q1
  sum(x < (q1 - 1.5 * iqr) | x > (q3 + 1.5 * iqr), na.rm = TRUE)
})
##       usia pendapatan 
##          3          3
# Jumlah transaksi yang ID-nya TIDAK ADA di data pelanggan
sum(!transaksi_raw$cust_id %in% pelanggan_raw$customer_id)
## [1] 6
# Menampilkan ID mana saja yang bermasalah (mismatch)
setdiff(transaksi_raw$cust_id, pelanggan_raw$customer_id)
## [1] "C012" "C013" "C014" "C015" "C099" "C100"
# Ringkasan profil data
data.frame(
  Tipe_Data   = sapply(pelanggan_raw, class),
  Jumlah_NA   = colSums(is.na(pelanggan_raw)),
  Jumlah_Unik = sapply(pelanggan_raw, function(x) length(unique(x)))
)

1.3 Membuat fungsi ringkasan kualitas

# =========================================================
# 1. FUNGSI AUDIT DATASET KOMPREHENSIF (Pengembangan Fungsi Kamu)
# =========================================================
audit_data <- function(data) {
  data.frame(
    atribut         = names(data),
    tipe            = sapply(data, function(x) class(x)[1]),
    jumlah_missing  = sapply(data, function(x) sum(is.na(x))),
    persen_missing  = round(sapply(data, function(x) mean(is.na(x)) * 100), 2),
    jumlah_unik     = sapply(data, function(x) length(unique(x))),
    
    # Deteksi Spasi Liar di awal/akhir string (khusus tipe karakter)
    ada_spasi_liar  = sapply(data, function(x) {
      if(is.character(x)) sum(grepl("^\\s+|\\s+$", x), na.rm = TRUE) else 0
    }),
    
    # Deteksi Angka 0 (Missing Value terselubung pada kolom numerik)
    jumlah_nol      = sapply(data, function(x) {
      if(is.numeric(x)) sum(x == 0, na.rm = TRUE) else 0
    }),
    
    # Deteksi Outlier Berdasarkan Metode IQR (khusus numerik)
    jumlah_outlier  = sapply(data, function(x) {
      if(is.numeric(x)) {
        q1 <- quantile(x, 0.25, na.rm = TRUE)
        q3 <- quantile(x, 0.75, na.rm = TRUE)
        iqr <- q3 - q1
        sum(x < (q1 - 1.5 * iqr) | x > (q3 + 1.5 * iqr), na.rm = TRUE)
      } else {
        NA
      }
    }),
    row.names = NULL
  )
}

# Eksekusi audit awal pada dataset pelanggan
audit_pelanggan <- audit_data(pelanggan_raw)
audit_pelanggan
# =========================================================
# 2. AUDIT KHUSUS: VALIDASI FORMAT ID & INTEGRITAS JOIN
# =========================================================

# A. Cek ID yang tidak memenuhi format standar "C000" (Regex Audit)
invalid_ids <- pelanggan_raw$customer_id[!grepl("^C[0-9]{3}$", pelanggan_raw$customer_id)]
cat("Format ID Tidak Standar:\n")
## Format ID Tidak Standar:
print(invalid_ids)
## [1] "c013"  "C-014" "C015 "
# B. Cek Duplikasi Baris dan Duplikasi ID
cat("\nJumlah Baris Duplikat Penuh :", sum(duplicated(pelanggan_raw)))
## 
## Jumlah Baris Duplikat Penuh : 1
cat("\nJumlah ID yang Berulang     :", sum(duplicated(pelanggan_raw$customer_id)), "\n")
## 
## Jumlah ID yang Berulang     : 1
# C. Audit Integrasi Antar Tabel (Cross-Table Integrity Test)
mismatch_ids <- setdiff(transaksi_raw$cust_id, pelanggan_raw$customer_id)
cat("\nID Transaksi yang Tidak Ada di Tabel Pelanggan (Orphan Records):\n")
## 
## ID Transaksi yang Tidak Ada di Tabel Pelanggan (Orphan Records):
print(mismatch_ids)
## [1] "C012" "C013" "C014" "C015" "C099" "C100"

Interpretasi: Hasil audit data itu seperti sedang mengecek kesehatan data sebelum diobati. Jadi, audit hanya bertugas membuat dan memberi tahu bagian mana yang error, tapi tidak langsung otomatis ngubah atau ngehapus datanya. Kenapa? karena setiap masalah punya cara penanganan yang beda tergantung konteksnya.

Dari hasil audit, kita jadi tahu tindakan apa yang pas untuk diambil. Jika kelihatan ada nilai yang hilang di kolom usia atau pendapatan, kita bisa berpikir apakah datanya mau dibuang atau diisi menggunakan nilai tengah (median). jika bertemu ID yang serupa, kita tahu data mana yang harus dihapus atau dijumlahkan. Selanjutnya, jika ada angka aneh seperti usia minus atau ratusan tahun, audit ngebuat kita sadar buat ngecek ulang berkas aslinya atau ngubah angka itu jadi data kosong dulu.

Bagian II — Prosedur Pembersihan Data (Data Cleaning)

2.1 Membuat salinan kerja

Pertahankan data mentah agar setiap perubahan dapat dilacak.

pelanggan <- pelanggan_raw

2.2 Membersihkan spasi dan kapitalisasi

# 1. Mencegah error duplikasi variabel dengan menyalin dataset asli
pelanggan_clean <- pelanggan_raw

# 2. Menghapus spasi liar di awal dan akhir pada SEMUA kolom berbasis karakter
pelanggan_clean[] <- lapply(pelanggan_clean, function(x) {
  if (is.character(x)) trimws(x) else x
})

# 3. Standardisasi Format ID (Kapitalisasi "c" -> "C" dan hapus tanda strip "-")
pelanggan_clean$customer_id <- toupper(pelanggan_clean$customer_id)
pelanggan_clean$customer_id <- gsub("-", "", pelanggan_clean$customer_id)

# 4. Standardisasi Nama Kota (Format Title Case & Pemetaan Singkatan)
pelanggan_clean$kota <- tolower(pelanggan_clean$kota)
pelanggan_clean$kota <- ifelse(pelanggan_clean$kota %in% c("pku", "bkn u"), "pekanbaru", pelanggan_clean$kota)
pelanggan_clean$kota <- ifelse(pelanggan_clean$kota %in% c("bkn"), "bangkinang", pelanggan_clean$kota)

# Mengubah huruf pertama menjadi Kapital (Title Case)
library(tools)
pelanggan_clean$kota <- toTitleCase(pelanggan_clean$kota)

# 5. Standardisasi Status (Konversi Berbagai Sinonim menjadi "Aktif" / "Tidak Aktif")
pelanggan_clean$status <- tolower(pelanggan_clean$status)
pelanggan_clean$status <- ifelse(pelanggan_clean$status %in% c("active", "a", "aktif"), "Aktif", pelanggan_clean$status)
pelanggan_clean$status <- ifelse(pelanggan_clean$status %in% c("nonaktif", "non-aktif", "non aktif", "pasif", "n", "tidak aktif"), "Tidak Aktif", pelanggan_clean$status)

# 6. Pengecekan Hasil Kategori yang Sudah Bersih
cat("=== Kategori Kota Setelah Dibersihkan ===\n")
## === Kategori Kota Setelah Dibersihkan ===
print(sort(unique(pelanggan_clean$kota)))
## [1] "Bangkinang" "Dumai"      "Pekanbaru"  "Siak"
cat("\n=== Kategori Status Setelah Dibersihkan ===\n")
## 
## === Kategori Status Setelah Dibersihkan ===
print(sort(unique(pelanggan_clean$status)))
## [1] "Aktif"       "Tidak Aktif"
cat("\n=== Format ID Setelah Dibersihkan ===\n")
## 
## === Format ID Setelah Dibersihkan ===
print(unique(pelanggan_clean$customer_id))
##  [1] "C001" "C002" "C003" "C004" "C005" "C006" "C007" "C008" "C009" "C010"
## [11] "C011" "C013" "C014" "C015" "C016" "C017" "C018" "C019" "C020" "C021"
## [21] "C022" "C023" "C024" "C025"
sort(unique(pelanggan$status))
##  [1] " Pasif"      "A"           "ACTIVE"      "aktif"       "Aktif"      
##  [6] "AKTIF"       "N"           "NON-AKTIF"   "Non Aktif"   "nonaktif"   
## [11] "Tidak aktif" "Tidak Aktif"

2.3 Menyeragamkan kategori

Setiap data yang ditarik dari sistem yang berbeda pasti punya masalah inkonsistensi penulisan, beda format, atau beda standar input. Sebelum data bisa dianalisis, kita wajib melakukan standardisasi kategori supaya nilai-nilai yang maksudnya sama tidak terbaca sebagai kategori yang berbeda oleh komputer. Penyeragaman ini kita lakukan berdasarkan aturan domain (business rules).

# ---------------------------------------------------------
# 1. Menghapus Spasi Liar Terlebih Dahulu
# ---------------------------------------------------------
pelanggan$kota <- trimws(pelanggan$kota)
pelanggan$status <- trimws(pelanggan$status)

# Menyeragamkan menjadi huruf kecil semua untuk memudahkan pengelompokan
pelanggan$kota <- tolower(pelanggan$kota)
pelanggan$status <- tolower(pelanggan$status)


# ---------------------------------------------------------
# 2. Standardisasi Kota (Pekanbaru, Dumai, Siak, Bangkinang)
# ---------------------------------------------------------
pelanggan$kota[pelanggan$kota %in% c("pku", "pekanbaru")] <- "Pekanbaru"
pelanggan$kota[pelanggan$kota %in% c("dumai")] <- "Dumai"
pelanggan$kota[pelanggan$kota %in% c("siak")] <- "Siak"
pelanggan$kota[pelanggan$kota %in% c("bangkinang", "bkn")] <- "Bangkinang"


# ---------------------------------------------------------
# 3. Standardisasi Status (Menutupi Seluruh Variasi Status)
# ---------------------------------------------------------
pelanggan$status[pelanggan$status %in% c("aktif", "active", "a")] <- "Aktif"
pelanggan$status[pelanggan$status %in% c("tidak aktif", "nonaktif", "non-aktif", "non aktif", "pasif", "n")] <- "Tidak Aktif"


# ---------------------------------------------------------
# 4. Pengecekan Hasil Kategori yang Sudah Seragam
# ---------------------------------------------------------
sort(unique(pelanggan$kota))
## [1] "Bangkinang" "Dumai"      "Pekanbaru"  "Siak"
sort(unique(pelanggan$status))
## [1] "Aktif"       "Tidak Aktif"

2.4 Mendeteksi dan menghapus duplikasi

# Menampilkan seluruh baris dengan customer_id yang berulang
pelanggan[duplicated(pelanggan$customer_id) |
          duplicated(pelanggan$customer_id, fromLast = TRUE), ]
# ---------------------------------------------------------
# 1. Preprocessing Format ID (Wajib dilakukan sebelum cek duplikat)
# ---------------------------------------------------------
pelanggan$customer_id <- trimws(pelanggan$customer_id)
pelanggan$customer_id <- toupper(pelanggan$customer_id)
pelanggan$customer_id <- gsub("-", "", pelanggan$customer_id)

# ---------------------------------------------------------
# 2. Menampilkan Seluruh Baris dengan customer_id yang Berulang
# ---------------------------------------------------------
# Melihat data mana saja yang memiliki duplikasi ID (termasuk C010)
duplikat_id <- pelanggan[duplicated(pelanggan$customer_id) | 
                         duplicated(pelanggan$customer_id, fromLast = TRUE), ]
duplikat_id
# ---------------------------------------------------------
# 3. Mempertahankan Kemunculan Pertama dan Menghapus Duplikat
# ---------------------------------------------------------
pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]

# Reset indeks baris agar rapi kembali
rownames(pelanggan) <- NULL

# ---------------------------------------------------------
# 4. Pengecekan Dimensi Data Setelah Duplikat Dibuang
# ---------------------------------------------------------
# Dimensi awal 25 baris akan berkurang menjadi 24 baris (karena C010 ganda)
dim(pelanggan)
## [1] 24  6

Menghapus duplikasi hanya aman dilakukan jika satu customer_id memang mewakili satu profil pelanggan. Jika satu pelanggan diperbolehkan memiliki lebih dari satu baris entri (seperti pada riwayat transaksi berulang), tindakan penghapusan justru akan menghilangkan informasi penting dan merusak integritas data.

2.5 Memeriksa aturan domain

# ---------------------------------------------------------
# 1. MENDETEKSI DATA TIDAK VALID (Berdasarkan Aturan Domain)
# ---------------------------------------------------------

# Memeriksa usia yang tidak valid (di luar rentang 15–100 tahun)
pelanggan[!is.na(pelanggan$usia) & (pelanggan$usia < 15 | pelanggan$usia > 100), ]
# Memeriksa pendapatan yang bernilai negatif (jika ada)
pelanggan[!is.na(pelanggan$pendapatan) & pelanggan$pendapatan < 0, ]
# ---------------------------------------------------------
# 2. KOREKSI DATA TIDAK VALID (Aturan Bisnis & Konversi NA)
# ---------------------------------------------------------

# Kasus 1 (C004): Usia 150 adalah salah ketik dari 50 (berdasarkan verifikasi formulir)
pelanggan$usia[pelanggan$customer_id == "C004"] <- 50

# Kasus 2 (C013): Usia -5 adalah error input, diubah menjadi NA agar diimputasi nanti
pelanggan$usia[pelanggan$customer_id == "C013"] <- NA

# Kasus 3 (C015): Usia 999 adalah kode dummy input, diubah menjadi NA
pelanggan$usia[pelanggan$customer_id == "C015"] <- NA


# ---------------------------------------------------------
# 3. VERIFIKASI HASIL KOREKSI
# ---------------------------------------------------------

# Memastikan tidak ada lagi usia di luar rentang wajar (15-100 tahun)
pelanggan[!is.na(pelanggan$usia) & (pelanggan$usia < 15 | pelanggan$usia > 100), ]
# Cek rentang nilai usia terbaru
range(pelanggan$usia, na.rm = TRUE)
## [1] 19 50

2.6 Membuat log perubahan

log_perubahan <- data.frame(
  tahu_proses = c(
    "Standardisasi", 
    "Standardisasi", 
    "Standardisasi", 
    "Deduplikasi", 
    "Koreksi Domain", 
    "Koreksi Domain"
  ),
  atribut = c(
    "customer_id", 
    "kota", 
    "status", 
    "customer_id", 
    "usia", 
    "usia"
  ),
  tindakan = c(
    "Menghapus spasi/strip, konversi ke kapital (misal: c013, C-014 -> C013, C014)",
    "Penanganan spasi liar, konversi ke Title Case, pemetaan singkatan (PKU, Bkn -> Pekanbaru, Bangkinang)",
    "Penanganan spasi liar, penyelarasan sinonim (ACTIVE/A -> Aktif; nonaktif/N/Pasif -> Tidak Aktif)",
    "Menghapus baris duplikat penuh pada ID C010 (kemunculan kedua)",
    "Usia C004 dikoreksi dari 150 menjadi 50 berdasarkan sumber asli",
    "Usia C013 (-5) dan C015 (999) dikonversi menjadi NA karena nilai tidak valid"
  ),
  stringsAsFactors = FALSE
)

# Menampilkan log perubahan
log_perubahan

Bagian III — Strategi Penanganan Nilai Hilang (Missing Values)

3.1 Mengidentifikasi lokasi nilai hilang

# ---------------------------------------------------------
# 1. Pengecekan Ringkas Jumlah Missing Value per Atribut
# ---------------------------------------------------------
colSums(is.na(pelanggan))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           5           4           2           1
# Persentase missing value per atribut
round(colMeans(is.na(pelanggan)) * 100, 2)
## customer_id        nama        usia  pendapatan        kota      status 
##        0.00        0.00       20.83       16.67        8.33        4.17
# ---------------------------------------------------------
# 2. Menampilkan Seluruh Baris yang Memiliki Minimal Satu Missing Value
# ---------------------------------------------------------
# Menampilkan baris yang tidak lengkap (incomplete cases)
pelanggan[!complete.cases(pelanggan), ]
# ---------------------------------------------------------
# 3. Analisis Spesifik Missing Value per Kolom Utama
# ---------------------------------------------------------

# A. Pelanggan dengan Usia yang Hilang (NA)
# Termasuk C006, C013, C015, C017, C024
pelanggan[is.na(pelanggan$usia), c("customer_id", "nama", "usia")]
# B. Pelanggan dengan Pendapatan yang Hilang (NA)
# Termasuk C002, C011, C015, C022
pelanggan[is.na(pelanggan$pendapatan), c("customer_id", "nama", "pendapatan")]
# C. Pelanggan dengan Kota atau Status yang Hilang (NA)
pelanggan[is.na(pelanggan$kota) | is.na(pelanggan$status), ]

A. Evaluasi Listwise Deletion (Pembersihan Baris Tidak Lengkap)

Sebelum melakukan imputasi, ukur seberapa banyak informasi yang hilang jika menggunakan opsi ekstrem (listwise deletion).

# Membuat subset data lengkap
pelanggan_complete <- pelanggan[complete.cases(pelanggan), ]

# Menampilkan statistik pengurangan baris
cat("Jumlah baris awal  :", nrow(pelanggan), "\n")
## Jumlah baris awal  : 24
cat("Jumlah baris lengkap:", nrow(pelanggan_complete), "\n")
## Jumlah baris lengkap: 14
# Persentase data yang hilang jika baris yang tidak lengkap dihapus
persen_hilang <- round((1 - nrow(pelanggan_complete) / nrow(pelanggan)) * 100, 2)
cat("Persentase data terbuang:", persen_hilang, "%\n")
## Persentase data terbuang: 41.67 %
round((1 - nrow(pelanggan_complete) / nrow(pelanggan)) * 100, 2)
## [1] 41.67

B. Imputasi Statistik Numerik (Mean vs. Median vs. Imputasi Berkelompok)

- Analisis Pemilihan Central Tendency (Mean vs Median)

Karena pendapatan memiliki outlier ekstrem (seperti 500 juta & 1.2 miliar), median lebih resisten dibandingkan mean.

mean_pendapatan   <- mean(pelanggan$pendapatan, na.rm = TRUE)
median_pendapatan <- median(pelanggan$pendapatan, na.rm = TRUE)

mean_usia   <- mean(pelanggan$usia, na.rm = TRUE)
median_usia <- median(pelanggan$usia, na.rm = TRUE)

cat("Pendapatan - Mean:", mean_pendapatan, "| Median:", median_pendapatan, "\n")
## Pendapatan - Mean: 89095000 | Median: 4850000
cat("Usia       - Mean:", mean_usia, "| Median:", median_usia, "\n")
## Usia       - Mean: 29.47368 | Median: 28

- Imputasi Standar (Median Imputation)

# Imputasi Pendapatan dengan Median
pelanggan$pendapatan_imputasi <- pelanggan$pendapatan
pelanggan$pendapatan_imputasi[is.na(pelanggan$pendapatan_imputasi)] <- median_pendapatan

# Imputasi Usia dengan Median
pelanggan$usia_imputasi <- pelanggan$usia
pelanggan$usia_imputasi[is.na(pelanggan$usia_imputasi)] <- median_usia

3.2 Imputasi Data Kategorik (Eksplisit vs Modus)

Menjaga transparansi data tanpa mengasumsi kota atau status pelanggan. Nilai kota yang hilang tidak selalu tepat diisi dengan modus. Dalam hal ini digunakan kategori eksplisit Tidak diketahui agar ketidakpastian tidak disembunyikan.

pelanggan$kota_imputasi <- pelanggan$kota
pelanggan$kota_imputasi[is.na(pelanggan$kota_imputasi)] <- "Tidak diketahui"

pelanggan$status_imputasi <- pelanggan$status
pelanggan$status_imputasi[is.na(pelanggan$status_imputasi)] <- "Tidak diketahui"

3.3 Membuat Indikator Missing Value (Missing Value)

Praktik ini menjaga informasi asli untuk algoritma machine learning bahwa nilai tersebut sebenarnya hasil rekaan/imputasi.

pelanggan$pendapatan_is_na <- as.integer(is.na(pelanggan$pendapatan))
pelanggan$usia_is_na       <- as.integer(is.na(pelanggan$usia))
pelanggan$kota_is_na       <- as.integer(is.na(pelanggan$kota))

# Menampilkan distribusi indikator
table(pelanggan$pendapatan_is_na)
## 
##  0  1 
## 20  4

3.4 Visualisasi & Perbandingan Distribusi

Visualisasi dua panel untuk mengevaluasi pergeseran distribusi nilai sebelum dan sesudah dilakukan imputasi.

# Visualisasi Distribusi Pendapatan (Log Scale agar Outlier Terlihat Rapi)
par(mfrow = c(1, 2))

hist(log10(pelanggan$pendapatan),
     main = "Sebelum Imputasi (Log10)",
     xlab = "Log10(Pendapatan)", col = "brown", breaks = 8)

hist(log10(pelanggan$pendapatan_imputasi),
     main = "Sesudah Imputasi Median",
     xlab = "Log10(Pendapatan)", col = "navy", breaks = 8)

# Reset tata letak grafik
par(mfrow = c(1, 1))

Imputasi dapat mengubah distribusi dan mengecilkan variasi. Karena itu, evaluasi tidak berhenti setelah semua NA hilang.

Bagian IV — Identifikasi dan Penanganan Nilai Ekstrem (Outlier)

4.1 Deteksi Visual & Perhitungan Statistik (Metode IQR)

A. Visualisasi Boxplot

# Visualisasi awal untuk mendeteksi outlier pada variabel pendapatan
boxplot(pelanggan$pendapatan_imputasi,
        horizontal = TRUE,
        col = "brown",
        main = "Boxplot Pendapatan Pelanggan",
        xlab = "Pendapatan (Rupiah)")

B. Menghitung Batas Batas IQR (Interquartile Range)

# Menghitung Kuartil 1, Kuartil 3, dan IQR
q1  <- quantile(pelanggan$pendapatan_imputasi, 0.25, na.rm = TRUE)
q3  <- quantile(pelanggan$pendapatan_imputasi, 0.75, na.rm = TRUE)
iqr <- IQR(pelanggan$pendapatan_imputasi, na.rm = TRUE)

# Menentukan batas bawah dan batas atas toleransi
batas_bawah <- q1 - 1.5 * iqr
batas_atas  <- q3 + 1.5 * iqr

# Menampilkan hasil perhitungan batas
c(Q1 = q1, Q3 = q3, IQR = iqr, Batas_Bawah = batas_bawah, Batas_Atas = batas_atas)
##          Q1.25%          Q3.75%             IQR Batas_Bawah.25%  Batas_Atas.75% 
##         4575000         5125000          550000         3750000         5950000

C. Menandai kandidat outlier

# Menandai baris yang tergolong outlier
pelanggan$outlier_pendapatan <- pelanggan$pendapatan_imputasi < batas_bawah | 
                                pelanggan$pendapatan_imputasi > batas_atas

# Menampilkan daftar pelanggan yang terdeteksi memiliki pendapatan outlier
pelanggan[pelanggan$outlier_pendapatan, 
          c("customer_id", "nama", "pendapatan_imputasi")]

D. Mengevaluasi tindakan

Berdasarkan perhitungan statistik, terdeteksi dua nilai pendapatan ekstrem pada dataset 25 pelanggan tersebut, yaitu 500 juta rupiah pada pelanggan C007 (baris ke-7) dan 1,2 miliar rupiah pada pelanggan C018 (baris ke-18). Kedua angka ini berada jauh di atas batas toleransi IQR. Namun, nilai tersebut tidak serta-merta langsung dihapus. Terdapat tiga kemungkinan yang perlu dipertimbangkan:

  1. Kesalahan Input (Data Entry Error): Angka tersebut dikoreksi apabila hasil verifikasi dokumen menunjukkan adanya kesalahan pengetikan (misalnya kelebihan memasukkan angka nol).

  2. Observasi Valid tetapi Ekstrem: Jika angka tersebut memang riil (misalnya pelanggan segmen High Net-Worth Individual), nilai tetap dipertahankan atau ditangani dengan pendekatan robust / winsorizing.

  3. Populasi Berbeda: Apabila pelanggan tersebut berasal dari segmen korporasi yang tidak sengaja tercampur dengan pelanggan ritel, data dipisahkan ke dalam kelompok analisis tersendiri.

Pada modul ini, nilai ekstrem tersebut tetap dipertahankan pada variabel asli dan dibuatkan variabel versi winsorized sebagai bahan komparasi dampak metode terhadap distribusi data.

4.2 Penanganan Capping / Censoring (Metode Winsorizing)

Membatasi nilai ekstrem agar tidak melebihi batas statistik atas/bawah tanpa membuang baris data (mencegah kehilangan informasi).

# Membatasi nilai pendapatan maksimum setara nilai batas atas IQR
pelanggan$pendapatan_winsor <- pmin(
  pmax(pelanggan$pendapatan_imputasi, batas_bawah),
  batas_atas
)

# Menampilkan perbandingan sebelum dan sesudah Winsorizing
pelanggan[pelanggan$outlier_pendapatan, 
          c("customer_id", "pendapatan_imputasi", "pendapatan_winsor")]

4.3 Penanganan Transformasi Skala (Log Transformation)

Mengubah skala data menggunakan fungsi logaritma Sangat disarankan untuk meredam rentang nilai outlier secara alami tanpa harus mengubah nilai riil data.

# Transformasi logaritma berbasis 10 (ditambah 1 untuk mencegah error log(0))
pelanggan$pendapatan_log <- log10(pelanggan$pendapatan_imputasi + 1)

# Menampilkan hasil transformasi logaritma pada data outlier
pelanggan[pelanggan$outlier_pendapatan, 
          c("customer_id", "pendapatan_imputasi", "pendapatan_log")]

4.4 Evaluasi & Perbandingan Hasil Penanganan Outlier

Membandingkan skenario data asli vs winsorized vs log transformation secara visual untuk melihat perubahan sebaran data.

# Pengaturan grid 1 baris 3 kolom
par(mfrow = c(1, 3))

# 1. Boxplot Data Imputasi Asli
boxplot(pelanggan$pendapatan_imputasi, 
        main = "1. Imputasi Asli", 
        col = "skyblue", ylab = "Rupiah")

# 2. Boxplot Hasil Winsorizing
boxplot(pelanggan$pendapatan_winsor, 
        main = "2. Hasil Winsorizing", 
        col = "yellow", ylab = "Rupiah")

# 3. Boxplot Hasil Transformasi Log
boxplot(pelanggan$pendapatan_log, 
        main = "3. Transformasi Log10", 
        col = "orange", ylab = "Log10 Scale")

# Reset tata letak grafik ke normal
par(mfrow = c(1, 1))

Bagian V — Teknik Transformasi dan Normalisasi Data

5.1 Normalisasi Min–Max (Rentang [0,1])

Mengubah skala numerik ke dalam rentang terikat antara 0 dan 1

# Fungsi kustom untuk Min-Max Scaling
minmax <- function(x) {
  if (all(is.na(x))) return(rep(NA_real_, length(x)))
  rentang <- max(x, na.rm = TRUE) - min(x, na.rm = TRUE)
  if (rentang == 0) return(rep(0, length(x)))
  (x - min(x, na.rm = TRUE)) / rentang
}

# Penerapan pada atribut usia dan pendapatan
pelanggan$usia_minmax <- minmax(pelanggan$usia_imputasi)
pelanggan$pendapatan_minmax <- minmax(pelanggan$pendapatan_imputasi)

# Menampilkan hasil transformasi Min-Max
pelanggan[, c("customer_id", "usia_imputasi", "usia_minmax",
              "pendapatan_imputasi", "pendapatan_minmax")]

5.2 Standarisasi Z-Score (Mean = 0, Std Dev = 1)

Mengubah skala data berdistribusi normal dengan mengukur seberapa jauh jarak suatu nilai dari rata-ratanya.

\[z = \frac{x - \bar{x}}{s}\]

# Menggunakan fungsi bawaan R: scale()
pelanggan$usia_z <- as.numeric(scale(pelanggan$usia_imputasi))
pelanggan$pendapatan_z <- as.numeric(scale(pelanggan$pendapatan_imputasi))

# Menampilkan hasil transformasi Z-Score (dibulatkan 3 desimal)
round(pelanggan[, c("usia_z", "pendapatan_z")], 3)

5.3 Decimal Scaling

Menggeser koma desimal berdasarkan nilai absolut maksimum.

# Fungsi kustom untuk Decimal Scaling
decimal_scale <- function(x) {
  maks <- max(abs(x), na.rm = TRUE)
  if (maks == 0) return(x)
  j <- ceiling(log10(maks + 1))
  x / (10 ^ j)
}

# Penerapan pada atribut pendapatan
pelanggan$pendapatan_decimal <- decimal_scale(pelanggan$pendapatan_imputasi)

# Menampilkan rentang hasil transformasi
range(pelanggan$pendapatan_decimal, na.rm = TRUE)
## [1] 0.00 0.12

5.4 Robust Scaling

Menggunakan Median dan IQR menggantikan Mean dan Standar Deviasi (\(\frac{x - \text{median}}{\text{IQR}}\)). Sangat disarankan jika data mengandung outlier tinggi karena tidak mudah terdistorsi.

# Fungsi kustom untuk Robust Scaler
robust_scale <- function(x) {
  med <- median(x, na.rm = TRUE)
  iqr_val <- IQR(x, na.rm = TRUE)
  if (iqr_val == 0) return(x - med)
  (x - med) / iqr_val
}

# Penerapan Robust Scaling pada pendapatan
pelanggan$pendapatan_robust <- robust_scale(pelanggan$pendapatan_imputasi)

# Menampilkan perbandingan Z-score vs Robust Scaling pada data outlier
pelanggan[pelanggan$outlier_pendapatan, c("customer_id", "pendapatan_z", "pendapatan_robust")]

5.5 Komparasi Keseluruhan Metode Transformasi

Menyajikan perbandingan nilai hasil dari seluruh metode scaling yang telah diterapkan pada variabel pendapatan.

# Ringkasan seluruh metode skalar dalam satu data frame
transformasi_ringkasan <- pelanggan[, c(
  "customer_id", "pendapatan_imputasi", "pendapatan_minmax",
  "pendapatan_z", "pendapatan_decimal", "pendapatan_robust"
)]

# Menampilkan 10 baris pertama
head(transformasi_ringkasan, 10)

5.6 Analisis Sensitivitas Outlier Terhadap Hasil Normalisasi

Menguji sejauh mana penanganan outlier (Metode Winsorized) mengubah hasil distribusi skala Min–Max.

# Melakukan Min-Max scaling pada data yang sudah di-Winsorized
pelanggan$pendapatan_winsor_minmax <- minmax(pelanggan$pendapatan_winsor)

# Plot perbandingan Min-Max data asli vs data Winsorized
plot(pelanggan$pendapatan_minmax,
     pelanggan$pendapatan_winsor_minmax,
     pch = 19, col = "navy",
     xlab = "Min–Max Data Asli (Dengan Outlier)",
     ylab = "Min–Max Data Winsorized (Outlier Ditangani)",
     main = "Dampak Penanganan Outlier terhadap Normalisasi")

# Garis acuan diagonal
abline(0, 1, col = "red", lty = 2)

Interpretasi Analisis Nilai ekstrem outlier yang belum ditangani akan membuat sebagian besar data lain menumpuk di rentang angka yang sangat sempit saat diubah ke skala min-maks. Oleh karena itu, outlier perlu diidentifikasi dan ditangani terlebih dahulu sebelum menentukan metode transformasi yang tepat.

Bagian VI — Integrasi Data

6.1 Audit Integritas & Pembersihan Data Transaksi

Sebelum melakukan merge, data transaksi harus diselaraskan format ID-nya dan diperiksa duplikasi ID-nya agar tidak memicu perkalian baris (cartesian join) yang tidak diinginkan.

# 1. Menyelaraskan nama & format ID pada data transaksi
transaksi <- transaksi_raw
names(transaksi)[names(transaksi) == "cust_id"] <- "customer_id"

# Standardisasi format string ID
transaksi$customer_id <- trimws(transaksi$customer_id)
transaksi$customer_id <- toupper(transaksi$customer_id)
transaksi$customer_id <- gsub("-", "", transaksi$customer_id)

# 2. Menangani Duplikasi ID pada Data Transaksi (Agregasi)
# Karena C020 muncul 2x di data transaksi, kita aggregasikan agar 1 ID = 1 Baris
transaksi_clean <- aggregate(
  cbind(jumlah_transaksi, total_purchase) ~ customer_id,
  data = transaksi,
  FUN = sum
)

# 3. Pemeriksaan Kunci Antar Tabel
cat("Duplikat ID Pelanggan :", sum(duplicated(pelanggan$customer_id)), "\n")
## Duplikat ID Pelanggan : 0
cat("Duplikat ID Transaksi :", sum(duplicated(transaksi_clean$customer_id)), "\n")
## Duplikat ID Transaksi : 0
# Cek selisih ID antar dua dataset
cat("Pelanggan tanpa transaksi :", length(setdiff(pelanggan$customer_id, transaksi_clean$customer_id)), "\n")
## Pelanggan tanpa transaksi : 8
cat("Transaksi tanpa pelanggan :", length(setdiff(transaksi_clean$customer_id, pelanggan$customer_id)), "\n")
## Transaksi tanpa pelanggan : 3

6.2 Penggabungan Data (Left Join & Inner Join)

Menggabungkan data utama pelanggan dengan data agregasi transaksi.

# A. Left Join (Mempertahankan seluruh pelanggan meskipun belum pernah bertransaksi)
data_terintegrasi <- merge(
  pelanggan,
  transaksi_clean,
  by = "customer_id",
  all.x = TRUE
)

# Menampilkan sampel data terintegrasi
head(data_terintegrasi[, c("customer_id", "nama", "jumlah_transaksi", "total_purchase")], 10)

6.3 Penanganan Missing Value Akibat Mismatch Join

Nilai NA pada kolom transaksi setelah left join dapat diimputasi dengan nilai 0 apabila diasumsikan pelanggan tersebut memang belum melakukan transaksi.

# Duplikasi kolom untuk memelihara trace data asli
data_terintegrasi$jumlah_transaksi_final <- data_terintegrasi$jumlah_transaksi
data_terintegrasi$total_purchase_final   <- data_terintegrasi$total_purchase

# Mengisi NA hasil join dengan nilai 0
data_terintegrasi$jumlah_transaksi_final[is.na(data_terintegrasi$jumlah_transaksi_final)] <- 0
data_terintegrasi$total_purchase_final[is.na(data_terintegrasi$total_purchase_final)]     <- 0

# Verifikasi penanganan NA
colSums(is.na(data_terintegrasi[, c("jumlah_transaksi_final", "total_purchase_final")]))
## jumlah_transaksi_final   total_purchase_final 
##                      0                      0

Mengisi nol atau mempertahankan NA?

Keberadaan nilai NA pada variabel transaksi tidak bisa langsung disimpulkan sebagai angka nol tanpa adanya konfirmasi aturan bisnis. Secara teoretis, Little & Rubin (2019) menegaskan pentingnya memahami mekanisme di balik nilai hilang—apakah bersifat Missing at Random (MAR) atau Missing Not at Random (MNAR)—sebelum menentukan metode penanganannya. Secara teknis dan operasional, kondisi NA pada data transaksi ini mengandung dua kemungkinan interpretasi yang bertolak belakang:

  1. Aktivitas Riil Nol: Pelanggan memang terdaftar di basis data profil, namun secara faktual belum pernah melakukan transaksi sama sekali sejak akun dibuat.

  2. Kegagalan Integrasi Data (Data Unmatched / Missing Information): Pelanggan sebenarnya sudah pernah bertransaksi, tetapi catatannya tidak ditemukan akibat perbedaan format ID saat proses penggabungan (join), atau data tersebut hilang dari sistem pencatatan transaksi.

Pengubahan nilai NA menjadi angka 0 hanya sah dilakukan apabila sudah dikonfirmasi melalui kamus data atau aturan bisnis bahwa tidak ditemukannya riwayat belanja secara eksplisit mendefinisikan ketiadaan aktivitas transaksi. Mengisi NA secara sembarangan tanpa validasi berisiko memalsukan profil pelanggan dan menghasilkan bias pada analisis statistik selanjutnya.

6.4 Penyusunan Dataset Akhir (Final Feature Selection)

Memilih dan merapikan kolom-kolom hasil pembersihan yang akan digunakan untuk pemodelan/analisis tingkat lanjut.

# Memilih atribut akhir yang bersih
data_final <- data_terintegrasi[, c(
  "customer_id", "nama", "usia_imputasi", "kota_imputasi", "status",
  "pendapatan_imputasi", "pendapatan_is_na", "outlier_pendapatan",
  "usia_minmax", "pendapatan_minmax", "pendapatan_z", "pendapatan_robust",
  "jumlah_transaksi_final", "total_purchase_final"
)]

# Merapikan nama kolom
names(data_final)[names(data_final) == "kota_imputasi"]       <- "kota"
names(data_final)[names(data_final) == "usia_imputasi"]       <- "usia"
names(data_final)[names(data_final) == "pendapatan_imputasi"] <- "pendapatan"

# Menampilkan struktur dataset akhir
str(data_final)
## 'data.frame':    24 obs. of  14 variables:
##  $ customer_id           : chr  "C001" "C002" "C003" "C004" ...
##  $ nama                  : chr  "Ani" "Budi" "Citra" "Dodi" ...
##  $ usia                  : num  21 25 23 50 27 28 31 29 22 35 ...
##  $ kota                  : chr  "Pekanbaru" "Pekanbaru" "Pekanbaru" "Dumai" ...
##  $ status                : chr  "Aktif" "Aktif" "Aktif" "Aktif" ...
##  $ pendapatan            : num  4.50e+06 4.85e+06 5.20e+06 4.80e+06 4.90e+06 5.10e+06 5.00e+08 4.70e+06 4.60e+06 5.30e+06 ...
##  $ pendapatan_is_na      : int  0 1 0 0 0 0 0 0 0 0 ...
##  $ outlier_pendapatan    : logi  FALSE FALSE FALSE FALSE FALSE FALSE ...
##  $ usia_minmax           : num  0.0645 0.1935 0.129 1 0.2581 ...
##  $ pendapatan_minmax     : num  0.00375 0.00404 0.00433 0.004 0.00408 ...
##  $ pendapatan_z          : num  -0.271 -0.27 -0.269 -0.27 -0.27 ...
##  $ pendapatan_robust     : num  -0.6364 0 0.6364 -0.0909 0.0909 ...
##  $ jumlah_transaksi_final: num  5 3 7 2 6 4 20 5 3 8 ...
##  $ total_purchase_final  : num  1.5e+06 9.0e+05 2.7e+06 6.0e+05 2.1e+06 1.3e+06 2.5e+07 1.7e+06 8.0e+05 3.2e+06 ...
# Contoh jika telah dikonfirmasi bahwa NA berarti belum pernah bertransaksi
data_terintegrasi$jumlah_transaksi_final <- data_terintegrasi$jumlah_transaksi
data_terintegrasi$total_purchase_final <- data_terintegrasi$total_purchase

data_terintegrasi$jumlah_transaksi_final[
  is.na(data_terintegrasi$jumlah_transaksi_final)
] <- 0

data_terintegrasi$total_purchase_final[
  is.na(data_terintegrasi$total_purchase_final)
] <- 0

6.5 Audit Komparatif (Sebelum vs Sesudah Preprocessing)

Mengukur peningkatan kualitas data sebelum dan sesudah seluruh rantai data preprocessing dijalankan.

# 1. Audit akhir menggunakan fungsi audit_data
audit_akhir <- audit_data(data_final)
print(audit_akhir)
##                   atribut      tipe jumlah_missing persen_missing jumlah_unik
## 1             customer_id character              0           0.00          24
## 2                    nama character              0           0.00          24
## 3                    usia   numeric              0           0.00          17
## 4                    kota character              0           0.00           5
## 5                  status character              1           4.17           3
## 6              pendapatan   numeric              0           0.00          18
## 7        pendapatan_is_na   integer              0           0.00           2
## 8      outlier_pendapatan   logical              0           0.00           2
## 9             usia_minmax   numeric              0           0.00          17
## 10      pendapatan_minmax   numeric              0           0.00          18
## 11           pendapatan_z   numeric              0           0.00          18
## 12      pendapatan_robust   numeric              0           0.00          18
## 13 jumlah_transaksi_final   numeric              0           0.00          11
## 14   total_purchase_final   numeric              0           0.00          16
##    ada_spasi_liar jumlah_nol jumlah_outlier
## 1               0          0             NA
## 2               0          0             NA
## 3               0          0              3
## 4               0          0             NA
## 5               0          0             NA
## 6               0          1              4
## 7               0         20              4
## 8               0          0             NA
## 9               0          1              3
## 10              0          1              4
## 11              0          0              4
## 12              0          4              4
## 13              0          8              2
## 14              0          8              2
# 2. Tabel Ringkasan Komparasi Sebelum vs Sesudah
perbandingan <- data.frame(
  Indikator = c(
    "Jumlah Baris Data",
    "Duplikasi customer_id",
    "Total Missing Value (NA)",
    "Kategori Kota Unik",
    "Kategori Status Unik"
  ),
  Sebelum = c(
    nrow(pelanggan_raw),
    sum(duplicated(pelanggan_raw$customer_id)),
    sum(is.na(pelanggan_raw)),
    length(unique(pelanggan_raw$kota)),
    length(unique(pelanggan_raw$status))
  ),
  Sesudah = c(
    nrow(data_final),
    sum(duplicated(data_final$customer_id)),
    sum(is.na(data_final)),
    length(unique(data_final$kota)),
    length(unique(data_final$status))
  )
)

print(perbandingan)
##                  Indikator Sebelum Sesudah
## 1        Jumlah Baris Data      25      24
## 2    Duplikasi customer_id       1       0
## 3 Total Missing Value (NA)      10       1
## 4       Kategori Kota Unik      16       5
## 5     Kategori Status Unik      13       3

6.6 Ekspor Data Akhir

# Menyimpan dataset akhir yang sudah bersih
write.csv(data_final,
          "data_pelanggan_setelah_preprocessing.csv",
          row.names = FALSE)

# Menyimpan log perubahan proyek
write.csv(log_perubahan,
          "log_perubahan_preprocessing.csv",
          row.names = FALSE)

Bagian VII - Pertanyaan Refleksi

7.1 Apakah dataset tanpa missing value selalu lebih berkualitas?

Dataset tanpa missing value tidak otomatis lebih berkualitas apabila proses pengisiannya dilakukan secara sembarangan. Mengisi nilai yang hilang dengan teknik yang salah—seperti langsung mengganti NA menjadi nol atau nilai rata-rata tanpa memahami konteks bisnis—justru dapat merusak distribusi asli, menutupi pola informasi penting, dan menyuntikkan informasi palsu yang tidak mencerminkan fakta lapangan.

7.2 Mengapa outlier tidak boleh otomatis dihapus?

Outlier tidak boleh otomatis dihapus karena nilai ekstrem tersebut belum tentu merupakan kesalahan input data. Dalam banyak kasus, outlier merupakan observasi yang sepenuhnya valid dan merepresentasikan fakta nyata di dunia bisnis, seperti transaksi bernilai sangat tinggi dari pelanggan segmen High Net-Worth Individual atau transaksi korporasi. Menghapus nilai ekstrem berisiko menghilangkan informasi penting mengenai segmen pasar dan dapat mengurangi daya generalisasi model terhadap kondisi nyata.

7.3 Bagaimana preprocessing dapat menimbulkan bias?

Preprocessing dapat menimbulkan bias apabila tindakan pembersihan data justru mengubah atau membelokkan statistik dasar dari populasi aslinya. Contohnya terjadi ketika data hilang (missing value) dihapus secara listwise padahal pola hilangnya data bersifat sistematis (Missing Not at Random), yang mengakibatkan kelompok sampel tertentu terbuang dan sisa data tidak lagi representatif. Begitu pula saat imputasi data dilakukan menggunakan asumsi yang salah, hal ini akan memperkecil varians data secara artifisial dan memicu kesimpulan analisis yang menyesatkan.

7.4 Mengapa parameter imputasi dan transformasi seharusnya dihitung dari data pelatihan?

Parameter imputasi dan transformasi—seperti nilai median untuk pengisian data, serta nilai minimum, maksimum, rata-rata, atau standar deviasi untuk pemrosesan skala—seharusnya dihitung khusus dari data pelatihan (train set) saja untuk mencegah masalah data leakage. Jika parameter ini dihitung dari seluruh dataset (termasuk test set), informasi dari data pengujian secara tidak sengaja telah “bocor” ke dalam proses pelatihan. Akibatnya, evaluasi performa model menjadi terlalu optimis (overfitted) dan model gagal memberikan performa yang konsisten saat diuji dengan data baru yang benar-benar belum pernah dilihat.

7.5 Apa risiko integrasi data jika identifier tidak unik?

Risiko utama integrasi data apabila identifier atau primary key tidak unik adalah terciptanya duplikasi fiktif (cartesian join) atau salah pemadanan entitas (misalignment) saat proses merge dijalankan. Ketika satu ID pelanggan muncul berkali-kali di tabel profil dengan data yang bertentangan, penggabungan data akan memicu lonjakan jumlah baris secara tidak wajar, menggelembungkan angka statistik transaksi, dan merusak integritas seluruh analisis turunan seperti pemetaan profil atau analisis perilaku konsumen.

Ringkasan

Rangkaian alur data preprocessing yang telah dikerjakan—mulai dari eksplorasi awal, pembersihan, imputasi, penanganan outlier, transformasi, hingga integrasi akhir—menegaskan satu prinsip mendasar yaitu pemrosesan data bukanlah prosedur mekanis rutin, melainkan sebuah proses pengambilan keputusan yang terstruktur.

Setiap tahap pembersihan menuntut penilaian kritis yang berlandaskan logika bisnis. Ketika kita mengoreksi format ID, menetapkan strategi imputasi pada nilai hilang, atau menentukan perlakuan terhadap angka pendapatan bernilai ratusan juta, kita sedang mengambil keputusan metodologis yang secara langsung akan memengaruhi hasil analisis.

Kualitas akhir dari dataset siap pakai sepenuhnya bergantung pada kedalaman pemahaman analisis terhadap domain data, kejelasan tujuan analitik yang ingin dicapai, serta kedisiplinan dalam mendokumentasikan setiap perubahan. Tanpa dokumentasi dan pertimbangan yang matang, tindakan pembersihan data justru berisiko bias baru, merusak distribusi, atau menghilangkan informasi penting. Integrasi dan validasi akhir menjadi bukti bahwa seluruh keputusan yang diambil sepanjang alur pemrosesan mampu menghasilkan data yang utuh, konsisten, dan tepercaya untuk analisis tahap berikutnya.

Daftar Pustaka

Han, J., Kamber, M., & Pei, J. (2012). Data Mining: Concepts and Techniques (3rd ed.). Morgan Kaufmann. Chapter 3: Data Preprocessing.

Little, R. J. A., & Rubin, D. B. (2019). Statistical Analysis with Missing Data (3rd ed.). John Wiley & Sons