1 Pendahuluan

Sebelum data dapat digunakan untuk analisis maupun pemodelan, sebagian besar dataset di dunia nyata memerlukan tahap pembersihan dan persiapan terlebih dahulu. Panduan ini membahas penerapan tahapan data preprocessing secara sistematis pada studi kasus data pelanggan sebuah perusahaan e-commerce yang akan digabungkan dengan data transaksinya. Data mentah yang tersedia masih memiliki beberapa masalah kualitas, di antaranya:

  • nilai yang hilang (missing values) pada beberapa atribut,
  • penulisan kategori yang tidak konsisten (huruf besar/kecil, spasi berlebih, singkatan),
  • baris data yang terduplikasi,
  • nilai numerik yang secara logis tidak masuk akal (outlier akibat kesalahan input), dan
  • perbedaan nama kolom kunci antar dua sumber data yang perlu digabungkan.

Tujuan panduan ini adalah menguraikan setiap permasalahan kualitas data tersebut secara bertahap, menjelaskan dasar pertimbangan dalam memilih strategi penanganannya, dan mendokumentasikan setiap perubahan yang dilakukan, sebelum akhirnya data digabungkan menjadi satu dataset analisis yang siap dipakai. Pendekatan yang digunakan dapat diterapkan pada kasus data pelanggan lain dengan karakteristik serupa.

2 Menyiapkan Data

Dataset yang dipakai terdiri dari dua bagian: data pelanggan dan data transaksi. Kondisi kedua data ini sengaja dibuat kotor untuk mengilustrasikan berbagai jenis masalah kualitas data yang umum dijumpai pada praktiknya.

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

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

pelanggan_raw
##    customer_id   nama usia pendapatan       kota      status
## 1         C001    Ani   21    4.5e+06  Pekanbaru       Aktif
## 2         C002   Budi   25         NA        PKU       aktif
## 3         C003  Citra   23    5.2e+06  PEKANBARU      ACTIVE
## 4         C004   Dodi  150    4.8e+06      Dumai           A
## 5         C005    Eka   27    4.9e+06  pekanbaru Tidak Aktif
## 6         C006   Fani   NA    5.1e+06      DUMAI    nonaktif
## 7         C007 Gilang   31    5.0e+08 Pekanbaru        Aktif
## 8         C008   Hana   29    4.7e+06       Siak       AKTIF
## 9         C009  Indra   22    4.6e+06        PKU           A
## 10        C010   Joko   35    5.3e+06      Dumai Tidak aktif
## 11        C010   Joko   35    5.3e+06      Dumai Tidak aktif
## 12        C011   Kiki   28         NA       <NA>       Aktif
transaksi_raw
##    cust_id jumlah_transaksi total_purchase
## 1     C001                5        1.5e+06
## 2     C002                3        9.0e+05
## 3     C003                7        2.7e+06
## 4     C004                2        6.0e+05
## 5     C005                6        2.1e+06
## 6     C006                4        1.3e+06
## 7     C007               20        2.5e+07
## 8     C008                5        1.7e+06
## 9     C009                3        8.0e+05
## 10    C010                8        3.2e+06
## 11    C012                1        2.5e+05

3 Mengenali Masalah pada Data

Sebelum data dibersihkan, penting untuk memetakan dulu bentuk dan ukuran masalahnya. Pengecekan berikut dilakukan pada pelanggan_raw.

3.1 Struktur data

dim(pelanggan_raw)
## [1] 12  6
str(pelanggan_raw)
## 'data.frame':    12 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" ...
summary(pelanggan_raw)
##  customer_id            nama                usia          pendapatan       
##  Length:12          Length:12          Min.   : 21.00   Min.   :  4500000  
##  Class :character   Class :character   1st Qu.: 24.00   1st Qu.:  4725000  
##  Mode  :character   Mode  :character   Median : 28.00   Median :  5000000  
##                                        Mean   : 38.73   Mean   : 54440000  
##                                        3rd Qu.: 33.00   3rd Qu.:  5275000  
##                                        Max.   :150.00   Max.   :500000000  
##                                        NA's   :1        NA's   :2          
##      kota              status         
##  Length:12          Length:12         
##  Class :character   Class :character  
##  Mode  :character   Mode  :character  
##                                       
##                                       
##                                       
## 

Dari ringkasan di atas terlihat kejanggalan pada atribut numerik: usia maksimum tercatat 150 tahun, dan pendapatan maksimum mencapai Rp500.000.000 — dua angka yang tidak wajar untuk konteks data pelanggan individual, dan patut dicurigai sebagai kesalahan input.

3.2 Menghitung nilai hilang, duplikasi, dan variasi kategori

# Jumlah dan persentase missing value per kolom
colSums(is.na(pelanggan_raw))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           1           2           1           0
round(colMeans(is.na(pelanggan_raw)) * 100, 2)
## customer_id        nama        usia  pendapatan        kota      status 
##        0.00        0.00        8.33       16.67        8.33        0.00
# Baris yang identik & customer_id yang berulang
sum(duplicated(pelanggan_raw))
## [1] 1
sum(duplicated(pelanggan_raw$customer_id))
## [1] 1
# Variasi penulisan kategori yang muncul
sort(unique(pelanggan_raw$kota))
## [1] " PKU"       "Dumai"      "DUMAI"      "pekanbaru"  "Pekanbaru" 
## [6] "PEKANBARU"  "Pekanbaru " "PKU"        "Siak"
sort(unique(pelanggan_raw$status))
## [1] "A"           "ACTIVE"      "aktif"       "Aktif"       "AKTIF"      
## [6] "nonaktif"    "Tidak aktif" "Tidak Aktif"

Kota “Pekanbaru” saja tercatat dalam lima variasi penulisan berbeda ("Pekanbaru", " PKU", "PEKANBARU", "pekanbaru", "Pekanbaru "), begitu pula status pelanggan yang punya enam variasi untuk hanya dua makna (aktif / tidak aktif). customer_id C010 juga muncul dua kali dengan data yang identik, menandakan input ganda.

3.3 Fungsi bantu untuk audit kualitas data

Supaya pengecekan kualitas bisa dipakai ulang di beberapa titik proses (sebelum dan sesudah dibersihkan), dibuat satu fungsi ringkasan:

audit_kualitas <- function(df) {
  data.frame(
    kolom       = names(df),
    tipe_data   = sapply(df, function(x) class(x)[1]),
    n_missing   = sapply(df, function(x) sum(is.na(x))),
    pct_missing = round(sapply(df, function(x) mean(is.na(x)) * 100), 2),
    n_unik      = sapply(df, function(x) length(unique(x))),
    row.names   = NULL
  )
}

audit_sebelum <- audit_kualitas(pelanggan_raw)
audit_sebelum
##         kolom tipe_data n_missing pct_missing n_unik
## 1 customer_id character         0        0.00     11
## 2        nama character         0        0.00     11
## 3        usia   numeric         1        8.33     11
## 4  pendapatan   numeric         2       16.67     10
## 5        kota character         1        8.33     10
## 6      status character         0        0.00      8

4 Data Cleaning

Seluruh proses pembersihan dilakukan pada salinan data, bukan pada data mentah, agar pelanggan_raw tetap tersimpan sebagai pembanding.

pelanggan <- pelanggan_raw

4.1 Merapikan spasi dan kapitalisasi

pelanggan$kota   <- trimws(pelanggan$kota)
pelanggan$status <- trimws(pelanggan$status)

pelanggan$kota   <- tolower(pelanggan$kota)
pelanggan$status <- tolower(pelanggan$status)

sort(unique(pelanggan$kota))
## [1] "dumai"     "pekanbaru" "pku"       "siak"
sort(unique(pelanggan$status))
## [1] "a"           "active"      "aktif"       "nonaktif"    "tidak aktif"

4.2 Menyamakan kategori ke satu bentuk baku

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"

Kategori kota dan status sekarang sudah seragam — tidak ada lagi variasi penulisan yang tercecer.

4.3 Menangani duplikasi

pelanggan[duplicated(pelanggan$customer_id) |
          duplicated(pelanggan$customer_id, fromLast = TRUE), ]
##    customer_id nama usia pendapatan  kota      status
## 10        C010 Joko   35    5300000 Dumai Tidak Aktif
## 11        C010 Joko   35    5300000 Dumai Tidak Aktif

Karena kedua baris C010 identik, kemunculan kedua dianggap input ganda dan dihapus, dengan mempertahankan kemunculan pertama.

pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL
dim(pelanggan)
## [1] 11  6

4.4 Memeriksa nilai yang melanggar aturan domain

Untuk atribut usia, digunakan aturan bisnis sederhana: usia pelanggan individual yang wajar berada pada rentang 15–100 tahun.

pelanggan[pelanggan$usia < 15 | pelanggan$usia > 100, ]
##    customer_id nama usia pendapatan  kota status
## 4         C004 Dodi  150    4800000 Dumai  Aktif
## NA        <NA> <NA>   NA         NA  <NA>   <NA>
pelanggan[pelanggan$pendapatan < 0, ]
##      customer_id nama usia pendapatan kota status
## NA          <NA> <NA>   NA         NA <NA>   <NA>
## NA.1        <NA> <NA>   NA         NA <NA>   <NA>

Usia 150 tahun pada C004 jelas melanggar aturan tersebut. Setelah ditelusuri ke sumber data asli, nilai yang benar adalah 50 tahun — kemungkinan angka “1” pada awal digit tertulis tidak sengaja.

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

4.5 Log perubahan

Setiap perubahan pada tahap cleaning dicatat agar proses tetap bisa ditelusuri ulang:

log_perubahan <- data.frame(
  tahap    = c("Standardisasi", "Standardisasi", "Deduplikasi", "Koreksi domain"),
  kolom    = c("kota", "status", "customer_id", "usia"),
  keterangan = c(
    "Variasi 'PKU' dan variasi kapitalisasi disatukan menjadi 'Pekanbaru'",
    "'ACTIVE'/'A' disatukan jadi 'Aktif'; 'nonaktif' disatukan jadi 'Tidak Aktif'",
    "Baris kedua dengan customer_id C010 (identik) dihapus",
    "Usia C004 dikoreksi dari 150 menjadi 50 tahun berdasarkan data sumber"
  ),
  stringsAsFactors = FALSE
)

log_perubahan
##            tahap       kolom
## 1  Standardisasi        kota
## 2  Standardisasi      status
## 3    Deduplikasi customer_id
## 4 Koreksi domain        usia
##                                                                     keterangan
## 1         Variasi 'PKU' dan variasi kapitalisasi disatukan menjadi 'Pekanbaru'
## 2 'ACTIVE'/'A' disatukan jadi 'Aktif'; 'nonaktif' disatukan jadi 'Tidak Aktif'
## 3                        Baris kedua dengan customer_id C010 (identik) dihapus
## 4        Usia C004 dikoreksi dari 150 menjadi 50 tahun berdasarkan data sumber

5 Penanganan Missing Values

5.1 Memetakan lokasi nilai hilang

colSums(is.na(pelanggan))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           1           2           1           0
pelanggan[!complete.cases(pelanggan), ]
##    customer_id nama usia pendapatan      kota      status
## 2         C002 Budi   25         NA Pekanbaru       Aktif
## 6         C006 Fani   NA    5100000     Dumai Tidak Aktif
## 11        C011 Kiki   28         NA      <NA>       Aktif

5.2 Strategi 1 — Penghapusan baris (listwise deletion)

Sebagai pembanding, dihitung dulu berapa banyak data yang akan hilang jika strategi paling sederhana (menghapus semua baris bermasalah) dipakai:

pelanggan_lengkap <- pelanggan[complete.cases(pelanggan), ]
nrow(pelanggan)
## [1] 11
nrow(pelanggan_lengkap)
## [1] 8
round((1 - nrow(pelanggan_lengkap) / nrow(pelanggan)) * 100, 2)
## [1] 27.27

Menghapus baris ternyata membuang porsi data yang cukup besar untuk dataset sekecil ini, sehingga strategi ini kurang ideal dipakai sendirian.

5.3 Strategi 2 — Imputasi numerik dengan median

Median dipilih dibanding mean karena data pendapatan masih mengandung nilai ekstrem (Rp500 juta) yang dapat menarik rata-rata menjadi tidak representatif.

median_pendapatan <- median(pelanggan$pendapatan, na.rm = TRUE)
median_usia        <- median(pelanggan$usia, na.rm = TRUE)

pelanggan$pendapatan_imputasi <- pelanggan$pendapatan
pelanggan$pendapatan_imputasi[is.na(pelanggan$pendapatan_imputasi)] <- median_pendapatan

pelanggan$usia_imputasi <- pelanggan$usia
pelanggan$usia_imputasi[is.na(pelanggan$usia_imputasi)] <- median_usia

pelanggan[, c("customer_id", "usia", "usia_imputasi",
              "pendapatan", "pendapatan_imputasi")]
##    customer_id usia usia_imputasi pendapatan pendapatan_imputasi
## 1         C001   21          21.0    4.5e+06             4.5e+06
## 2         C002   25          25.0         NA             4.9e+06
## 3         C003   23          23.0    5.2e+06             5.2e+06
## 4         C004   50          50.0    4.8e+06             4.8e+06
## 5         C005   27          27.0    4.9e+06             4.9e+06
## 6         C006   NA          27.5    5.1e+06             5.1e+06
## 7         C007   31          31.0    5.0e+08             5.0e+08
## 8         C008   29          29.0    4.7e+06             4.7e+06
## 9         C009   22          22.0    4.6e+06             4.6e+06
## 10        C010   35          35.0    5.3e+06             5.3e+06
## 11        C011   28          28.0         NA             4.9e+06

5.4 Strategi 3 — Imputasi kategorik

Untuk kota yang hilang, dipakai label eksplisit alih-alih diisi sembarang kota, supaya ketidaktahuan tetap terlihat jelas di data:

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

table(pelanggan$kota_imputasi, useNA = "ifany")
## 
##           Dumai       Pekanbaru            Siak Tidak diketahui 
##               3               6               1               1

5.5 Menyimpan jejak nilai yang diimputasi

pelanggan$pendapatan_missing <- as.integer(is.na(pelanggan$pendapatan))
table(pelanggan$pendapatan_missing)
## 
## 0 1 
## 9 2

Kolom indikator ini penting supaya nilai hasil imputasi tidak diperlakukan seolah-olah data asli pada analisis selanjutnya.

5.6 Membandingkan distribusi sebelum dan sesudah imputasi

par(mfrow = c(1, 2))
hist(pelanggan$pendapatan,
     main = "Sebelum Imputasi", xlab = "Pendapatan",
     col = "#e8a1a5", border = "white", breaks = 8)
hist(pelanggan$pendapatan_imputasi,
     main = "Sesudah Imputasi Median", xlab = "Pendapatan",
     col = "#c1666b", border = "white", breaks = 8)

par(mfrow = c(1, 1))

6 Deteksi dan Penanganan Outlier

6.1 Visualisasi awal dengan boxplot

boxplot(pelanggan$pendapatan_imputasi,
        horizontal = TRUE, col = "#e8a1a5",
        border = "#8f3a3f", lwd = 1.5,
        main = "Sebaran Pendapatan Pelanggan",
        xlab = "Pendapatan")

6.2 Menentukan batas wajar dengan metode IQR

q1  <- quantile(pelanggan$pendapatan_imputasi, 0.25)
q3  <- quantile(pelanggan$pendapatan_imputasi, 0.75)
iqr <- IQR(pelanggan$pendapatan_imputasi)

batas_bawah <- q1 - 1.5 * iqr
batas_atas  <- q3 + 1.5 * iqr

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% 
##         4750000         5150000          400000         4150000         5750000

6.3 Menandai kandidat outlier

pelanggan$outlier_pendapatan <-
  pelanggan$pendapatan_imputasi < batas_bawah |
  pelanggan$pendapatan_imputasi > batas_atas

pelanggan[pelanggan$outlier_pendapatan,
          c("customer_id", "nama", "pendapatan_imputasi")]
##   customer_id   nama pendapatan_imputasi
## 7        C007 Gilang               5e+08

6.4 Menangani outlier dengan winsorizing

Alih-alih membuang baris outlier begitu saja, nilai ekstremnya dipangkas (winsorized) ke batas IQR terdekat, sehingga pelanggan tersebut tetap ada dalam dataset tapi nilainya tidak lagi mendominasi skala:

pelanggan$pendapatan_winsor <- pmin(
  pmax(pelanggan$pendapatan_imputasi, batas_bawah),
  batas_atas
)

pelanggan[pelanggan$outlier_pendapatan,
          c("customer_id", "pendapatan_imputasi", "pendapatan_winsor")]
##   customer_id pendapatan_imputasi pendapatan_winsor
## 7        C007               5e+08           5750000

7 Transformasi Atribut Numerik

7.1 Normalisasi min–maks

normalisasi_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
}

pelanggan$usia_minmax       <- normalisasi_minmax(pelanggan$usia_imputasi)
pelanggan$pendapatan_minmax <- normalisasi_minmax(pelanggan$pendapatan_imputasi)

pelanggan[, c("customer_id", "usia", "usia_minmax",
              "pendapatan_imputasi", "pendapatan_minmax")]
##    customer_id usia usia_minmax pendapatan_imputasi pendapatan_minmax
## 1         C001   21  0.00000000             4.5e+06      0.0000000000
## 2         C002   25  0.13793103             4.9e+06      0.0008072654
## 3         C003   23  0.06896552             5.2e+06      0.0014127144
## 4         C004   50  1.00000000             4.8e+06      0.0006054490
## 5         C005   27  0.20689655             4.9e+06      0.0008072654
## 6         C006   NA  0.22413793             5.1e+06      0.0012108981
## 7         C007   31  0.34482759             5.0e+08      1.0000000000
## 8         C008   29  0.27586207             4.7e+06      0.0004036327
## 9         C009   22  0.03448276             4.6e+06      0.0002018163
## 10        C010   35  0.48275862             5.3e+06      0.0016145308
## 11        C011   28  0.24137931             4.9e+06      0.0008072654

7.2 Normalisasi z-score

pelanggan$usia_z       <- as.numeric(scale(pelanggan$usia_imputasi))
pelanggan$pendapatan_z <- as.numeric(scale(pelanggan$pendapatan_imputasi))

round(pelanggan[, c("usia_z", "pendapatan_z")], 3)
##    usia_z pendapatan_z
## 1  -0.984       -0.304
## 2  -0.489       -0.301
## 3  -0.737       -0.299
## 4   2.604       -0.302
## 5  -0.242       -0.301
## 6  -0.180       -0.300
## 7   0.253        3.015
## 8   0.006       -0.303
## 9  -0.860       -0.303
## 10  0.748       -0.299
## 11 -0.118       -0.301

7.3 Decimal scaling

skala_desimal <- function(x) {
  nilai_maks <- max(abs(x), na.rm = TRUE)
  if (nilai_maks == 0) return(x)
  j <- ceiling(log10(nilai_maks + 1))
  x / (10 ^ j)
}

pelanggan$pendapatan_decimal <- skala_desimal(pelanggan$pendapatan_imputasi)
range(pelanggan$pendapatan_decimal, na.rm = TRUE)
## [1] 0.0045 0.5000

7.4 Perbandingan ketiga metode transformasi

tabel_transformasi <- pelanggan[, c(
  "customer_id", "pendapatan_imputasi", "pendapatan_minmax",
  "pendapatan_z", "pendapatan_decimal"
)]

tabel_transformasi
##    customer_id pendapatan_imputasi pendapatan_minmax pendapatan_z
## 1         C001             4.5e+06      0.0000000000   -0.3041235
## 2         C002             4.9e+06      0.0008072654   -0.3014440
## 3         C003             5.2e+06      0.0014127144   -0.2994343
## 4         C004             4.8e+06      0.0006054490   -0.3021138
## 5         C005             4.9e+06      0.0008072654   -0.3014440
## 6         C006             5.1e+06      0.0012108981   -0.3001042
## 7         C007             5.0e+08      1.0000000000    3.0151095
## 8         C008             4.7e+06      0.0004036327   -0.3027837
## 9         C009             4.6e+06      0.0002018163   -0.3034536
## 10        C010             5.3e+06      0.0016145308   -0.2987645
## 11        C011             4.9e+06      0.0008072654   -0.3014440
##    pendapatan_decimal
## 1              0.0045
## 2              0.0049
## 3              0.0052
## 4              0.0048
## 5              0.0049
## 6              0.0051
## 7              0.5000
## 8              0.0047
## 9              0.0046
## 10             0.0053
## 11             0.0049

Ketiga metode menghasilkan skala akhir yang berbeda: min–maks memampatkan seluruh nilai ke rentang 0–1, z-score menyatakan posisi relatif terhadap rata-rata dalam satuan simpangan baku, sedangkan decimal scaling sekadar menggeser koma desimal tanpa mengubah bentuk distribusi aslinya.

7.5 Pengaruh outlier terhadap hasil normalisasi

pelanggan$pendapatan_winsor_minmax <- normalisasi_minmax(pelanggan$pendapatan_winsor)

plot(pelanggan$pendapatan_minmax,
     pelanggan$pendapatan_winsor_minmax,
     pch = 19, col = "#a4444a", cex = 1.4,
     xlab = "Min–maks dari Data Asli",
     ylab = "Min–maks dari Data Winsorized",
     main = "Efek Penanganan Outlier terhadap Normalisasi")
abline(0, 1, col = "#c1666b", lty = 2, lwd = 2)
grid(col = "#f3d4d4")

Titik yang menyimpang jauh dari garis diagonal adalah pelanggan dengan pendapatan outlier (C007) — tanpa winsorizing, nilai min–maksnya menekan seluruh pelanggan lain mendekati 0.

8 Integrasi Data Pelanggan dan Data Transaksi

8.1 Memeriksa kesiapan kunci penggabungan

sum(duplicated(pelanggan$customer_id))
## [1] 0
sum(duplicated(transaksi_raw$cust_id))
## [1] 0
setdiff(pelanggan$customer_id, transaksi_raw$cust_id)
## [1] "C011"
setdiff(transaksi_raw$cust_id, pelanggan$customer_id)
## [1] "C012"

C011 ada di data pelanggan tapi tidak punya catatan transaksi, sementara C012 punya catatan transaksi tapi tidak terdaftar di data pelanggan (kemungkinan tercatat dengan ID pelanggan yang berbeda, C010, pada sumber data yang tersedia).

8.2 Menyamakan nama kolom kunci

transaksi <- transaksi_raw
names(transaksi)[names(transaksi) == "cust_id"] <- "customer_id"

8.3 Left join

Digunakan left join agar seluruh pelanggan tetap tercatat, meski sebagian tidak memiliki transaksi:

data_gabungan <- merge(
  pelanggan, transaksi,
  by = "customer_id", all.x = TRUE
)

data_gabungan[, c("customer_id", "nama", "jumlah_transaksi", "total_purchase")]
##    customer_id   nama jumlah_transaksi total_purchase
## 1         C001    Ani                5        1.5e+06
## 2         C002   Budi                3        9.0e+05
## 3         C003  Citra                7        2.7e+06
## 4         C004   Dodi                2        6.0e+05
## 5         C005    Eka                6        2.1e+06
## 6         C006   Fani                4        1.3e+06
## 7         C007 Gilang               20        2.5e+07
## 8         C008   Hana                5        1.7e+06
## 9         C009  Indra                3        8.0e+05
## 10        C010   Joko                8        3.2e+06
## 11        C011   Kiki               NA             NA

8.4 Validasi hasil penggabungan

c(sebelum = nrow(pelanggan), sesudah = nrow(data_gabungan))
## sebelum sesudah 
##      11      11
sum(duplicated(data_gabungan$customer_id))
## [1] 0
colSums(is.na(data_gabungan[, c("jumlah_transaksi", "total_purchase")]))
## jumlah_transaksi   total_purchase 
##                1                1
data_gabungan[is.na(data_gabungan$jumlah_transaksi), c("customer_id", "nama")]
##    customer_id nama
## 11        C011 Kiki

Jumlah baris tidak berubah dan customer_id tetap unik, artinya proses join tidak menggandakan data. Nilai hilang baru pada jumlah_transaksi dan total_purchase murni berasal dari pelanggan yang belum pernah bertransaksi (C011), bukan kesalahan penggabungan.

8.5 Mengisi nol untuk pelanggan tanpa transaksi

Karena sudah dipastikan bahwa NA pada kolom transaksi berarti “belum pernah bertransaksi”, nilainya diisi 0 — bukan diisi dengan median seperti pada atribut pelanggan sebelumnya, karena maknanya berbeda (ketiadaan aktivitas, bukan data yang gagal tercatat):

data_gabungan$jumlah_transaksi_final <- data_gabungan$jumlah_transaksi
data_gabungan$total_purchase_final   <- data_gabungan$total_purchase

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

9 Dataset Akhir dan Evaluasi

9.1 Menyusun atribut akhir

data_final <- data_gabungan[, c(
  "customer_id", "nama", "usia_imputasi", "kota_imputasi", "status",
  "pendapatan_imputasi", "pendapatan_missing", "outlier_pendapatan",
  "usia_minmax", "pendapatan_minmax",
  "jumlah_transaksi_final", "total_purchase_final"
)]

names(data_final)[names(data_final) == "usia_imputasi"] <- "usia"
names(data_final)[names(data_final) == "kota_imputasi"] <- "kota"

data_final
##    customer_id   nama usia            kota      status pendapatan_imputasi
## 1         C001    Ani 21.0       Pekanbaru       Aktif             4.5e+06
## 2         C002   Budi 25.0       Pekanbaru       Aktif             4.9e+06
## 3         C003  Citra 23.0       Pekanbaru       Aktif             5.2e+06
## 4         C004   Dodi 50.0           Dumai       Aktif             4.8e+06
## 5         C005    Eka 27.0       Pekanbaru Tidak Aktif             4.9e+06
## 6         C006   Fani 27.5           Dumai Tidak Aktif             5.1e+06
## 7         C007 Gilang 31.0       Pekanbaru       Aktif             5.0e+08
## 8         C008   Hana 29.0            Siak       Aktif             4.7e+06
## 9         C009  Indra 22.0       Pekanbaru       Aktif             4.6e+06
## 10        C010   Joko 35.0           Dumai Tidak Aktif             5.3e+06
## 11        C011   Kiki 28.0 Tidak diketahui       Aktif             4.9e+06
##    pendapatan_missing outlier_pendapatan usia_minmax pendapatan_minmax
## 1                   0              FALSE  0.00000000      0.0000000000
## 2                   1              FALSE  0.13793103      0.0008072654
## 3                   0              FALSE  0.06896552      0.0014127144
## 4                   0              FALSE  1.00000000      0.0006054490
## 5                   0              FALSE  0.20689655      0.0008072654
## 6                   0              FALSE  0.22413793      0.0012108981
## 7                   0               TRUE  0.34482759      1.0000000000
## 8                   0              FALSE  0.27586207      0.0004036327
## 9                   0              FALSE  0.03448276      0.0002018163
## 10                  0              FALSE  0.48275862      0.0016145308
## 11                  1              FALSE  0.24137931      0.0008072654
##    jumlah_transaksi_final total_purchase_final
## 1                       5              1.5e+06
## 2                       3              9.0e+05
## 3                       7              2.7e+06
## 4                       2              6.0e+05
## 5                       6              2.1e+06
## 6                       4              1.3e+06
## 7                      20              2.5e+07
## 8                       5              1.7e+06
## 9                       3              8.0e+05
## 10                      8              3.2e+06
## 11                      0              0.0e+00

9.2 Audit akhir

audit_sesudah <- audit_kualitas(data_final)
audit_sesudah
##                     kolom tipe_data n_missing pct_missing n_unik
## 1             customer_id character         0           0     11
## 2                    nama character         0           0     11
## 3                    usia   numeric         0           0     11
## 4                    kota character         0           0      4
## 5                  status character         0           0      2
## 6     pendapatan_imputasi   numeric         0           0      9
## 7      pendapatan_missing   integer         0           0      2
## 8      outlier_pendapatan   logical         0           0      2
## 9             usia_minmax   numeric         0           0     11
## 10      pendapatan_minmax   numeric         0           0      9
## 11 jumlah_transaksi_final   numeric         0           0      9
## 12   total_purchase_final   numeric         0           0     11
sum(duplicated(data_final$customer_id))
## [1] 0
colSums(is.na(data_final))
##            customer_id                   nama                   usia 
##                      0                      0                      0 
##                   kota                 status    pendapatan_imputasi 
##                      0                      0                      0 
##     pendapatan_missing     outlier_pendapatan            usia_minmax 
##                      0                      0                      0 
##      pendapatan_minmax jumlah_transaksi_final   total_purchase_final 
##                      0                      0                      0

9.3 Ringkasan sebelum vs sesudah preprocessing

perbandingan <- data.frame(
  indikator = c(
    "Jumlah baris",
    "Duplikasi customer_id",
    "Total missing value",
    "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))
  )
)

perbandingan
##               indikator sebelum sesudah
## 1          Jumlah baris      12      11
## 2 Duplikasi customer_id       1       0
## 3   Total missing value       4       0
## 4    Kategori kota unik      10       4
## 5  Kategori status unik       8       2
barplot(
  t(as.matrix(perbandingan[, c("sebelum", "sesudah")])),
  beside = TRUE,
  names.arg = perbandingan$indikator,
  col = c("#e8a1a5", "#8f3a3f"),
  legend.text = c("Sebelum", "Sesudah"),
  args.legend = list(x = "topright", bty = "n"),
  las = 2, cex.names = 0.75,
  main = "Perbandingan Kondisi Data: Sebelum vs Sesudah Preprocessing",
  border = "white"
)

9.4 Menyimpan hasil akhir

write.csv(data_final, "data_pelanggan_bersih.csv", row.names = FALSE)
write.csv(log_perubahan, "log_perubahan_preprocessing.csv", row.names = FALSE)

10 Diskusi

Beberapa pertanyaan berikut sering muncul dalam praktik data preprocessing dan relevan untuk didiskusikan sebagai bahan pertimbangan lebih lanjut.

1. Apakah dataset tanpa missing value selalu lebih berkualitas?

Tidak selalu. Tidak adanya nilai hilang bisa jadi hanya berarti nilai tersebut sudah ditutupi lewat imputasi yang kurang tepat, bukan berarti datanya benar-benar lengkap dan akurat. Kualitas data lebih ditentukan oleh seberapa representatif dan konsisten nilainya, bukan semata dari ada-tidaknya sel yang kosong.

2. Mengapa outlier tidak boleh otomatis dihapus?

Karena outlier bisa jadi merupakan kesalahan input (seperti usia 150 tahun pada kasus ini), tapi bisa juga mencerminkan kondisi nyata yang justru penting untuk dianalisis (misalnya pelanggan dengan nilai transaksi sangat besar). Menghapusnya tanpa investigasi berisiko membuang informasi yang justru bermakna.

3. Bagaimana preprocessing dapat menimbulkan bias?

Setiap keputusan pembersihan — memilih median dibanding mean, memilih menghapus dibanding mengimputasi, menentukan ambang batas outlier — adalah keputusan yang mempengaruhi bentuk akhir data. Jika strategi tersebut diterapkan secara tidak konsisten atau tanpa dasar yang jelas, hasil analisis bisa condong ke arah tertentu tanpa disadari.

4. Mengapa parameter imputasi dan transformasi seharusnya dihitung dari data pelatihan?

Supaya proses transformasi pada data uji tidak “mengintip” informasi dari data uji itu sendiri (data leakage). Median, mean, atau parameter skala yang dipakai untuk data baru harus konsisten dengan apa yang dipelajari model dari data pelatihan.

5. Apa risiko integrasi data jika identifier tidak unik?

Penggabungan dapat menghasilkan duplikasi baris secara tidak sengaja (satu baris di satu tabel bisa cocok dengan lebih dari satu baris di tabel lain), sehingga jumlah baris hasil gabungan membengkak dan statistik ringkasan seperti total transaksi menjadi tidak akurat.

11 Kesimpulan

Tahapan yang diuraikan pada panduan ini mencakup pemeriksaan kualitas data awal, pembersihan kategori dan duplikasi, penanganan nilai hilang dengan beberapa strategi berbeda, deteksi dan penanganan outlier, transformasi skala atribut numerik, hingga integrasi dua sumber data yang divalidasi hasilnya. Data preprocessing pada dasarnya merupakan rangkaian pengambilan keputusan yang harus dapat dipertanggungjawabkan secara metodologis — kode program hanya berperan sebagai alat untuk menjalankan keputusan tersebut secara konsisten dan terdokumentasi. Pendekatan bertahap dan pendokumentasian setiap perubahan, sebagaimana dicontohkan pada log perubahan di atas, merupakan praktik yang disarankan agar proses preprocessing tetap transparan dan dapat direproduksi oleh pihak lain.

12 Referensi

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