Abstract & Ringkasan Proyek

Dalam dunia analisis data real-world, dataset mentah jarang sekali langsung siap dianalisis. Dataset sering kali mengandung “sampah” seperti format tidak konsisten, missing values, data ganda, hingga angka tidak masuk akal (outliers).

Proyek ini bertujuan untuk mensimulasikan alur kerja Data Preprocessing secara end-to-end pada dataset transaksi pelanggan yang kotor (dirty data). Proses ini mencakup audit kualitas data awal, pembersihan (data cleaning), penanganan missing values, identifikasi outlier, transformasi data, hingga integrasi data dari multiple sumber.


Membangun Dataset Praktikum

Dataset pelanggan_raw dan transaksi_raw sengaja dibuat dengan berbagai isu kualitas data (seperti spasi liar, inkonsistensi kapitalisasi, nilai ekstrem, duplikasi, dan nilai hilang) untuk kebutuhan pengujian.

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
)

head(pelanggan_raw)
##   customer_id  nama usia pendapatan      kota      status
## 1        C001   Ani   21    4500000 Pekanbaru       Aktif
## 2        C002  Budi   25         NA       PKU       aktif
## 3        C003 Citra   23    5200000 PEKANBARU      ACTIVE
## 4        C004  Dodi  150    4800000     Dumai           A
## 5        C005   Eka   27    4900000 pekanbaru Tidak Aktif
## 6        C006  Fani   NA    5100000     DUMAI    nonaktif

Catatan Singkat: Dari tampilan awal di atas, terlihat keanehan fisik data seperti penulisan nama kota yang acak-acakan (” PKU”, “PEKANBARU”), usia tidak logis (150 tahun), dan beberapa nilai kosong (NA).


Bagian I — Konsep Data Preprocessing & Audit Kualitas Data

Sebelum melakukan intervensi, penting untuk mengevaluasi 3 dimensi utama kualitas data: Accuracy, Completeness, dan Consistency.

Memahami Struktur Data Awal

dim(pelanggan_raw)
## [1] 12  6
names(pelanggan_raw)
## [1] "customer_id" "nama"        "usia"        "pendapatan"  "kota"       
## [6] "status"
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  4500000 NA 5200000 4800000 4900000 5100000 500000000 4700000 4600000 5300000 ...
##  $ 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  
##                                       
##                                       
##                                       
## 

Mengukur Kualitas Awal Data

# Missing value per atribut
colSums(is.na(pelanggan_raw))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           1           2           1           0
# Persentase missing value
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
# Deteksi Duplikasi
sum(duplicated(pelanggan_raw))
## [1] 1
sum(duplicated(pelanggan_raw$customer_id))
## [1] 1
# Keberagaman Nilai Kategori
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"
# Rentang Numerik
range(pelanggan_raw$usia, na.rm = TRUE)
## [1]  21 150
range(pelanggan_raw$pendapatan, na.rm = TRUE)
## [1]   4500000 500000000

Membuat Fungsi Audit Data Kustom

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))),
    row.names = NULL
  )
}

audit_awal = audit_data(pelanggan_raw)
audit_awal
##       atribut      tipe jumlah_missing persen_missing jumlah_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

Temuan Audit Data:

  • Inkonsistensi Teks: Kota Pekanbaru punya 5 variasi penulisan. Status pelanggan juga beragam.

  • Nilai Hilang: Variabel usia, pendapatan, dan kota memiliki data kosong sebesar ~16.67%.

  • Duplikasi: ID pelanggan C010 muncul dua kali.

  • Outlier: Terdeteksi usia 150 tahun dan pendapatan Rp 500 Juta yang jauh melenceng dari data lain.


Bagian II — Implementasi Data Cleaning

Pembersihan data dilakukan untuk memperbaiki format string, menghapus duplikasi, dan mengoreksi data yang melanggar aturan bisnis.

# Membuat salinan kerja
pelanggan = pelanggan_raw

# 1. Menghapus spasi liar (trimming) dan menyeragamkan huruf kecil
pelanggan$kota = tolower(trimws(pelanggan$kota))
pelanggan$status = tolower(trimws(pelanggan$status))

# 2. Standardisasi Kategori Kota
pelanggan$kota[pelanggan$kota %in% c("pku", "pekanbaru")] = "Pekanbaru"
pelanggan$kota[pelanggan$kota == "dumai"] = "Dumai"
pelanggan$kota[pelanggan$kota == "siak"] = "Siak"

# Standardisasi Kategori Status
pelanggan$status[pelanggan$status %in% c("aktif", "active", "a")] = "Aktif"
pelanggan$status[pelanggan$status %in% c("tidak aktif", "nonaktif")] = "Tidak Aktif"

# 3. Deduplikasi berdasarkan Customer ID (mempertahankan rekaman pertama)
pelanggan = pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) = NULL

# 4. Koreksi Aturan Bisnis/Domain (Koreksi Usia 150 menjadi 50)
pelanggan$usia[pelanggan$customer_id == "C004"] = 50

# Log Perubahan Data
log_perubahan = data.frame(
  tahap = c("Standardisasi", "Standardisasi", "Deduplikasi", "Koreksi domain"),
  atribut = c("kota", "status", "customer_id", "usia"),
  tindakan = c(
    "PKU dan variasi kapital menjadi Pekanbaru",
    "ACTIVE/A menjadi Aktif; nonaktif menjadi Tidak Aktif",
    "Menghapus kemunculan kedua C010",
    "Usia C004 dikoreksi dari 150 menjadi 50 berdasarkan sumber asli"
  ),
  stringsAsFactors = FALSE
)

log_perubahan
##            tahap     atribut
## 1  Standardisasi        kota
## 2  Standardisasi      status
## 3    Deduplikasi customer_id
## 4 Koreksi domain        usia
##                                                          tindakan
## 1                       PKU dan variasi kapital menjadi Pekanbaru
## 2            ACTIVE/A menjadi Aktif; nonaktif menjadi Tidak Aktif
## 3                                 Menghapus kemunculan kedua C010
## 4 Usia C004 dikoreksi dari 150 menjadi 50 berdasarkan sumber asli

Hasil Cleaning: Kategori kota disederhanakan dari 7 variasi menjadi 3 kota resmi (Pekanbaru, Dumai, Siak). Status pelanggan disatukan menjadi 2 pilihan baku (Aktif dan Tidak Aktif), serta duplikasi ID C010 telah dibuang.


Bagian III — Penanganan Missing Values

Menghapus baris kosong bisa menghilangkan informasi berharga. Sebagai solusinya, diterapkan teknik Imputasi mengisi data kosong dengan nilai Median untuk variabel angka, dan penamaan "Tidak diketahui" untuk variabel kategori.

# Identifikasi lokasi missing values
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
# Strategi Imputasi Numerik (Median)
median_pendapatan = median(pelanggan$pendapatan, na.rm = TRUE)
pelanggan$pendapatan_imputasi = pelanggan$pendapatan
pelanggan$pendapatan_imputasi[is.na(pelanggan$pendapatan_imputasi)] = median_pendapatan

median_usia = median(pelanggan$usia, na.rm = TRUE)
pelanggan$usia_imputasi = pelanggan$usia
pelanggan$usia_imputasi[is.na(pelanggan$usia_imputasi)] = median_usia

# Imputasi Kategorik dengan Explicit Category
pelanggan$kota_imputasi = pelanggan$kota
pelanggan$kota_imputasi[is.na(pelanggan$kota_imputasi)] = "Tidak diketahui"

# Menambahkan Missing Indicator
pelanggan$pendapatan_missing = as.integer(is.na(pelanggan$pendapatan))

Visualisasi Perbandingan Sebelum dan Sesudah Imputasi

par(mfrow = c(1, 2))

# Visualisasi 1: Sebelum Imputasi (Biru Tua)
hist(pelanggan$pendapatan,
     main = "Sebelum Imputasi",
     xlab = "Pendapatan", col = "#1f77b4", border = "white", breaks = 8)

# Visualisasi 2: Sesudah Imputasi Median (Biru Muda/Skyblue)
hist(pelanggan$pendapatan_imputasi,
     main = "Sesudah Imputasi Median",
     xlab = "Pendapatan", col = "#6baed6", border = "white", breaks = 8)

par(mfrow = c(1, 1))

Penjelasan Teknis: Median dipilih dibanding rata-rata (mean) karena ada angka pendapatan ekstrem (Rp 500 Juta). Jika menggunakan rata-rata, pengganti data kosong akan ikut melambung tinggi. Median lebih stabil dan tahan terhadap outlier.


Bagian IV — Penanganan Outlier

Identifikasi Outlier dengan Batas IQR

Metode Interquartile Range (IQR) digunakan untuk mencari batas wajar data numerik.

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

# Menandai 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           500000000

Visualisasi Outlier (Boxplot)

boxplot(pelanggan$pendapatan_imputasi,
        horizontal = TRUE,
        col = "#9ecae1",
        border = "#08519c",
        outcol = "#08306b",
        outpch = 19,
        main = "Boxplot Distribusi Pendapatan (Identifikasi Outlier)",
        xlab = "Pendapatan (Rupiah)")

Cara Membaca Grafik: Mayoritas data berada di dalam kotak biru (Rp 4.5 - 5.3 Juta). Namun, terdapat satu titik terpisah di ujung kanan atas nama pelanggan Gilang (Rp 500 Juta).

Treatment: Winsorization

Data ekstrem tidak dibuang, melainkan dibatasi (capping) agar nilainya tidak melebihi batas atas wajar.

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           500000000           5750000

Hasil Winsorization: Pendapatan Gilang yang tadinya Rp 500 Juta “ditekan” menjadi Rp 6,1 Juta (batas atas IQR), sehingga tidak akan mengacaukan perhitungan statistik nantinya.


Bagian V — Transformasi Data

Transformasi skala numerik dilakukan agar atribut berangka besar (pendapatan) tidak mendominasi atribut berangka kecil (usia).

# 1. Min-Max Scaling [0, 1]
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 = minmax(pelanggan$usia_imputasi)
pelanggan$pendapatan_minmax = minmax(pelanggan$pendapatan_imputasi)

# 2. Z-Score Normalization
pelanggan$usia_z = as.numeric(scale(pelanggan$usia_imputasi))
pelanggan$pendapatan_z = as.numeric(scale(pelanggan$pendapatan_imputasi))

# 3. 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)
}

pelanggan$pendapatan_decimal = decimal_scale(pelanggan$pendapatan_imputasi)

Dampak Outlier Terhadap Normalisasi Min-Max

pelanggan$pendapatan_winsor_minmax = minmax(pelanggan$pendapatan_winsor)

plot(pelanggan$pendapatan_minmax,
     pelanggan$pendapatan_winsor_minmax,
     pch = 19, col = "#08519c",
     xlab = "Min-Maks Data Asli (Terpengaruh Outlier)",
     ylab = "Min-Maks Data Winsorized",
     main = "Dampak Penanganan Outlier terhadap Normalisasi")
abline(0, 1, col = "#6baed6", lty = 2, lwd = 2)

Analisis Grafik: Tanpa penanganan outlier (Sumbu X), sebagian besar data pelanggan menumpuk di angka mendekati 0 karena ditarik oleh Rp 500 Juta. Setelah penanganan outlier (Sumbu Y), rentang data terbagi jauh lebih merata.


Bagian VI — Integrasi Data

Dataset master profil pelanggan digabungkan dengan tabel riwayat transaksi (transaksi_raw) menggunakan teknik Left Join.

# Penyelarasan Nama Identifier Key
transaksi = transaksi_raw
names(transaksi)[names(transaksi) == "cust_id"] = "customer_id"

# Left Join dengan Merge
data_terintegrasi = merge(
  pelanggan,
  transaksi,
  by = "customer_id",
  all.x = TRUE
)

# Menangani NA Hasil Join
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

Hasil Integrasi: Penggunaan Left Join menjaga pelanggan yang belum pernah bertransaksi (seperti Kiki/C011) agar tidak terhapus. Nilai belanjanya otomatis diisi dengan angka 0.


Dataset Akhir & Evaluasi Ringkas

Atribut pilihan diambil untuk membentuk dataset konsolidasi akhir yang bersih dan siap dianalisis (analytics-ready).

data_final = data_terintegrasi[, 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) == "kota_imputasi"] = "kota"
names(data_final)[names(data_final) == "usia_imputasi"] = "usia"

# Format Tampilan untuk Tabel
data_display = data_final
data_display$pendapatan_imputasi = format(data_display$pendapatan_imputasi, big.mark = ".", decimal.mark = ",", scientific = FALSE)
data_display$total_purchase_final = format(data_display$total_purchase_final, big.mark = ".", decimal.mark = ",", scientific = FALSE)

# Menampilkan Dataset Final
knitr::kable(data_display, caption = "Tabel Dataset Pelanggan Setelah Preprocessing")
Tabel Dataset Pelanggan Setelah Preprocessing
customer_id nama usia kota status pendapatan_imputasi pendapatan_missing outlier_pendapatan usia_minmax pendapatan_minmax jumlah_transaksi_final total_purchase_final
C001 Ani 21.0 Pekanbaru Aktif 4.500.000 0 FALSE 0.0000000 0.0000000 5 1.500.000
C002 Budi 25.0 Pekanbaru Aktif 4.900.000 1 FALSE 0.1379310 0.0008073 3 900.000
C003 Citra 23.0 Pekanbaru Aktif 5.200.000 0 FALSE 0.0689655 0.0014127 7 2.700.000
C004 Dodi 50.0 Dumai Aktif 4.800.000 0 FALSE 1.0000000 0.0006054 2 600.000
C005 Eka 27.0 Pekanbaru Tidak Aktif 4.900.000 0 FALSE 0.2068966 0.0008073 6 2.100.000
C006 Fani 27.5 Dumai Tidak Aktif 5.100.000 0 FALSE 0.2241379 0.0012109 4 1.300.000
C007 Gilang 31.0 Pekanbaru Aktif 500.000.000 0 TRUE 0.3448276 1.0000000 20 25.000.000
C008 Hana 29.0 Siak Aktif 4.700.000 0 FALSE 0.2758621 0.0004036 5 1.700.000
C009 Indra 22.0 Pekanbaru Aktif 4.600.000 0 FALSE 0.0344828 0.0002018 3 800.000
C010 Joko 35.0 Dumai Tidak Aktif 5.300.000 0 FALSE 0.4827586 0.0016145 8 3.200.000
C011 Kiki 28.0 Tidak diketahui Aktif 4.900.000 1 FALSE 0.2413793 0.0008073 0 0

Matriks Evaluasi Kualitas Data (Sebelum vs Sesudah)

perbandingan = data.frame(
  Indikator = c(
    "Jumlah Baris Observasi",
    "Duplikasi Customer ID",
    "Total Missing Values",
    "Jumlah Kategori Unik (Kota)",
    "Jumlah Kategori Unik (Status)"
  ),
  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))
  )
)

knitr::kable(perbandingan, caption = "Perbandingan Metrik Kualitas Data")
Perbandingan Metrik Kualitas Data
Indikator Sebelum Sesudah
Jumlah Baris Observasi 12 11
Duplikasi Customer ID 1 0
Total Missing Values 4 0
Jumlah Kategori Unik (Kota) 10 4
Jumlah Kategori Unik (Status) 8 2

Kesimpulan

Melalui alur kerja data preprocessing yang sistematis:

  1. Pembersihan & Konsistensi: Inkonsistensi teks pada kota dan status berhasil distandardisasi, serta duplikasi entitas berhasil dihilangkan tanpa kehilangan integritas data.
  2. Kebutuhan Data (Completeness): Missing values berhasil ditangani menggunakan median dan pengkodean eksplisit tanpa perlu membuang baris data.
  3. Kestabilan Model: Penanganan outliers via Winsorization dan pemetaan skala Min-Max menghasilkan variabel numerik yang stabil dan aman untuk digunakan dalam pemodelan Machine Learning.
Mau belajar lebih lanjut? Langsung saja DM Instagram @tarayadivani ya!