Data Preprocessing Menggunakan RStudio

Identitas Praktikum

Komponen Keterangan
Mata kuliah Data Mining
Pertemuan 03
Topik Data Preprocessing
Perangkat lunak R dan RStudio
Alokasi waktu 2 SKS / 100 menit
Acuan utama Han, Kamber, dan Pei (2012), Bab 3

Capaian Praktikum

Setelah mengikuti praktikum ini, mahasiswa diharapkan mampu:

  1. menjelaskan kondisi-kondisi data yang membuat preprocessing menjadi diperlukan;
  2. mengidentifikasi masalah-masalah kualitas data dengan menggunakan R;
  3. menerapkan data cleaning secara sistematis dan terarah;
  4. memilih serta menerapkan strategi yang tepat untuk menangani missing values;
  5. mendeteksi dan mengevaluasi keberadaan outlier;
  6. melakukan transformasi terhadap atribut numerik; dan
  7. mengintegrasikan dua sumber data yang berbeda serta memvalidasi hasil integrasinya.

Skenario Praktikum

Ada sebuah perusahaan e-commerce yang ingin menganalisis pelanggannya. Datanya berasal dari dua sumber:

  • data pelanggan, berisi identitas, usia, kota, pendapatan, dan status dari tiap pelanggan;
  • data transaksi, berisi jumlah transaksi dan total pembelian yang dilakukan.

Data ini belum bisa langsung dipakai karena masih mengandung banyak masalah, mulai dari nilai yang hilang, penulisan kategori yang tidak konsisten, data duplikat, nilai yang terlalu ekstrem, sampai perbedaan nama identifier antar sumber. Karena itu, mahasiswa perlu menyiapkan data tersebut lebih dulu hingga menjadi satu dataset analisis yang sudah bersih.

Prinsip utama: jangan asal membersihkan data hanya karena sebuah nilai terlihat aneh. Setiap perubahan yang dilakukan harus memiliki alasan yang jelas, mengikuti aturan tertentu, dan tetap dicatat.

Persiapan RStudio

Membuat proyek

  1. Buka aplikasi RStudio.
  2. Pilih menu File → New Project → New Directory → New Project.
  3. Beri nama proyeknya, misalnya praktikum_preprocessing.
  4. Simpan file ini ke dalam folder proyek yang sudah dibuat.
  5. Klik tombol Knit untuk menghasilkan file HTML-nya.

Memeriksa lingkungan kerja

getwd()
## [1] "C:/Users/User/Downloads"
R.version.string
## [1] "R version 4.5.1 (2025-06-13 ucrt)"

Modul ini hanya memakai fungsi bawaan R, sehingga bisa langsung dijalankan tanpa perlu memasang paket tambahan.

Membangun Dataset Praktikum

Jalankan kode berikut ini. Dataset yang digunakan memang sengaja dibuat “kotor” agar seluruh tahap preprocessing bisa benar-benar dipraktikkan.

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    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
## 7         C007 Gilang   31  500000000 Pekanbaru        Aktif
## 8         C008   Hana   29    4700000       Siak       AKTIF
## 9         C009  Indra   22    4600000        PKU           A
## 10        C010   Joko   35    5300000      Dumai Tidak aktif
## 11        C010   Joko   35    5300000      Dumai Tidak aktif
## 12        C011   Kiki   28         NA       <NA>       Aktif
transaksi_raw
##    cust_id jumlah_transaksi total_purchase
## 1     C001                5        1500000
## 2     C002                3         900000
## 3     C003                7        2700000
## 4     C004                2         600000
## 5     C005                6        2100000
## 6     C006                4        1300000
## 7     C007               20       25000000
## 8     C008                5        1700000
## 9     C009                3         800000
## 10    C010                8        3200000
## 11    C012                1         250000

Bagian I — Konsep Data Preprocessing

Memahami struktur data

Sebelum melakukan perubahan apa pun, ada baiknya kita periksa dulu strukturnya, dimensi datanya, tipe tiap atribut, serta beberapa observasi di 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" ...
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
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

Han, Kamber, dan Pei menyebutkan ada enam dimensi kualitas data, yaitu accuracy, completeness, consistency, timeliness, believability, dan interpretability. Namun tidak semua dimensi ini bisa langsung dihitung hanya dari isi tabel saja. Sebagai contoh, accuracy membutuhkan pembanding terhadap kondisi yang sebenarnya, sedangkan timeliness membutuhkan informasi kapan data terakhir diperbarui.

Pemeriksaan awal berikut ini difokuskan pada masalah-masalah yang memang bisa dideteksi langsung dari dataset.

# Banyak 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 per atribut
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
# 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] " 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 atribut numerik
range(pelanggan_raw$usia, na.rm = TRUE)
## [1]  21 150
range(pelanggan_raw$pendapatan, na.rm = TRUE)
## [1]   4500000 500000000

Membuat fungsi ringkasan kualitas

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

Interpretasi: hasil audit ini bukan berarti langsung menjadi keputusan cleaning. Audit hanya memberi sinyal atribut mana saja yang perlu diperiksa lebih lanjut.

Bagian II — Implementasi Data Cleaning

Membuat salinan kerja

Data mentah tetap disimpan supaya setiap perubahan yang dilakukan nanti masih bisa dilacak kembali.

pelanggan <- pelanggan_raw

Membersihkan spasi dan kapitalisasi

# Menghapus spasi di awal dan akhir
pelanggan$kota <- trimws(pelanggan$kota)
pelanggan$status <- trimws(pelanggan$status)

# Menyeragamkan huruf menjadi kecil untuk proses pencocokan
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"

Menyeragamkan kategori

Standardisasi ini dilakukan dengan mengikuti aturan domain. Dalam praktik yang sesungguhnya, aturan semacam ini sebaiknya dikonfirmasi lebih dulu melalui kamus data atau langsung ke pemilik data.

# Standardisasi kota
pelanggan$kota[pelanggan$kota %in% c("pku", "pekanbaru")] <- "Pekanbaru"
pelanggan$kota[pelanggan$kota == "dumai"] <- "Dumai"
pelanggan$kota[pelanggan$kota == "siak"] <- "Siak"

# Standardisasi status
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"

Mendeteksi dan menghapus duplikasi

# Menampilkan seluruh baris dengan customer_id yang berulang
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
# Mempertahankan kemunculan pertama untuk customer_id yang identik
pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL

dim(pelanggan)
## [1] 11  6

Catatan: menghapus data duplikat hanya aman dilakukan apabila setiap customer_id memang seharusnya mewakili satu pelanggan saja. Jika ternyata satu pelanggan boleh memiliki lebih dari satu baris data, maka menghapusnya justru menjadi kesalahan.

Memeriksa aturan domain

# Kandidat usia tidak valid berdasarkan aturan bisnis 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>
# Kandidat pendapatan tidak valid jika bernilai negatif
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>

Ada nilai usia 150 yang jelas melanggar aturan domain. Setelah diperiksa ke formulir aslinya, ternyata nilai yang benar seharusnya adalah 50.

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

Membuat log perubahan

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

Bagian III — Penanganan Missing Values

Mengidentifikasi lokasi nilai hilang

colSums(is.na(pelanggan))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           1           2           1           0
# Baris yang memiliki sedikitnya satu missing value
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 1: menghapus baris

Kode berikut ini hanya untuk mendemonstrasikan strateginya saja, objek pelanggan yang asli belum langsung diubah.

pelanggan_complete <- pelanggan[complete.cases(pelanggan), ]
nrow(pelanggan)
## [1] 11
nrow(pelanggan_complete)
## [1] 8

Pertanyaan: kira-kira berapa persen data yang akan hilang apabila seluruh baris yang tidak lengkap langsung dihapus?

round((1 - nrow(pelanggan_complete) / nrow(pelanggan)) * 100, 2)
## [1] 27.27

Strategi 2: imputasi mean dan median

Atribut pendapatan ternyata memiliki nilai yang ekstrem. Sebelum menentukan cara imputasinya, kita bandingkan dulu nilai mean dan median-nya.

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

mean_pendapatan
## [1] 59900000
median_pendapatan
## [1] 4900000

Karena distribusi pendapatan menjadi sangat miring akibat adanya nilai 500 juta, median dipilih karena lebih stabil untuk kasus ini.

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

# Imputasi usia dengan median
median_usia <- median(pelanggan$usia, na.rm = TRUE)
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    4500000             4500000
## 2         C002   25          25.0         NA             4900000
## 3         C003   23          23.0    5200000             5200000
## 4         C004   50          50.0    4800000             4800000
## 5         C005   27          27.0    4900000             4900000
## 6         C006   NA          27.5    5100000             5100000
## 7         C007   31          31.0  500000000           500000000
## 8         C008   29          29.0    4700000             4700000
## 9         C009   22          22.0    4600000             4600000
## 10        C010   35          35.0    5300000             5300000
## 11        C011   28          28.0         NA             4900000

Strategi 3: imputasi nilai kategorik

Nilai kota yang hilang tidak selalu tepat jika diisi dengan modus. Pada latihan ini kita gunakan kategori eksplisit Tidak diketahui agar ketidakpastian datanya tidak disembunyikan.

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

Menambahkan indikator missing

Indikator ini berguna untuk tetap menyimpan informasi bahwa suatu nilai sebenarnya awalnya hilang.

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

Membandingkan distribusi sebelum dan sesudah imputasi

par(mfrow = c(1, 2))
hist(pelanggan$pendapatan,
     main = "Sebelum Imputasi",
     xlab = "Pendapatan", col = "skyblue", breaks = 8)
hist(pelanggan$pendapatan_imputasi,
     main = "Sesudah Imputasi Median",
     xlab = "Pendapatan", col = "lightgreen", breaks = 8)

par(mfrow = c(1, 1))

Proses imputasi bisa mengubah distribusi data dan membuat variasinya menjadi lebih kecil. Karena itu, evaluasi tidak boleh berhenti begitu saja setelah semua nilai NA hilang.

Bagian IV — Penanganan Outlier

Visualisasi dengan boxplot

boxplot(pelanggan$pendapatan_imputasi,
        horizontal = TRUE,
        col = "lightblue",
        main = "Boxplot Pendapatan",
        xlab = "Pendapatan")

Menghitung batas 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

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           500000000

Mengevaluasi tindakan

Nilai 500 juta ini tidak langsung dihapus begitu saja. Ada tiga kemungkinan yang perlu dipertimbangkan:

  1. Kesalahan input: nilainya bisa dikoreksi apabila nilai yang benar memang dapat diverifikasi.
  2. Observasi valid tetapi ekstrem: nilai tetap dipertahankan, atau bisa ditangani dengan metode yang lebih robust.
  3. Populasi berbeda: segmen datanya perlu dipisah apabila pelanggan korporasi ternyata tercampur dengan pelanggan ritel.

Pada modul ini, nilai tersebut tetap dipertahankan, dan hanya dibuatkan variabel versi winsorized untuk membandingkan dampak dari metode yang digunakan.

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

Winsorizing di sini hanya sebagai demonstrasi tambahan. Metode ini sebaiknya diterapkan hanya jika memang sesuai dengan tujuan analisis, dan jangan lupa didokumentasikan bahwa nilai ekstremnya sudah dibatasi.

Bagian V — Transformasi Data

Normalisasi min–maks

Rumus untuk mengubah nilai ke rentang \([0,1]\) adalah sebagai berikut.

\[ x' = \frac{x-\min(x)}{\max(x)-\min(x)}. \]

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)

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             4500000      0.0000000000
## 2         C002   25  0.13793103             4900000      0.0008072654
## 3         C003   23  0.06896552             5200000      0.0014127144
## 4         C004   50  1.00000000             4800000      0.0006054490
## 5         C005   27  0.20689655             4900000      0.0008072654
## 6         C006   NA  0.22413793             5100000      0.0012108981
## 7         C007   31  0.34482759           500000000      1.0000000000
## 8         C008   29  0.27586207             4700000      0.0004036327
## 9         C009   22  0.03448276             4600000      0.0002018163
## 10        C010   35  0.48275862             5300000      0.0016145308
## 11        C011   28  0.24137931             4900000      0.0008072654

Normalisasi z-score

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

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

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)

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

Membandingkan metode transformasi

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

transformasi
##    customer_id pendapatan_imputasi pendapatan_minmax pendapatan_z
## 1         C001             4500000      0.0000000000   -0.3041235
## 2         C002             4900000      0.0008072654   -0.3014440
## 3         C003             5200000      0.0014127144   -0.2994343
## 4         C004             4800000      0.0006054490   -0.3021138
## 5         C005             4900000      0.0008072654   -0.3014440
## 6         C006             5100000      0.0012108981   -0.3001042
## 7         C007           500000000      1.0000000000    3.0151095
## 8         C008             4700000      0.0004036327   -0.3027837
## 9         C009             4600000      0.0002018163   -0.3034536
## 10        C010             5300000      0.0016145308   -0.2987645
## 11        C011             4900000      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

Dampak outlier terhadap normalisasi

pelanggan$pendapatan_winsor_minmax <- minmax(pelanggan$pendapatan_winsor)

plot(pelanggan$pendapatan_minmax,
     pelanggan$pendapatan_winsor_minmax,
     pch = 19, col = "navy",
     xlab = "Min–maks Data Asli",
     ylab = "Min–maks Data Winsorized",
     main = "Dampak Penanganan Outlier terhadap Normalisasi")
abline(0, 1, col = "red", lty = 2)

Interpretasi: adanya outlier bisa membuat sebagian besar nilai hasil min–maks scaling malah terjepit ke rentang yang sangat sempit. Inilah salah satu alasan kenapa deteksi outlier perlu dilakukan lebih dulu sebelum menentukan jenis transformasi yang akan dipakai.

Bagian VI — Integrasi Data

Memeriksa kunci pada kedua sumber

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"

Hasil dari setdiff() ini memperlihatkan pelanggan yang ternyata tidak memiliki data transaksi, sekaligus transaksi yang tidak memiliki pasangan data pelanggan.

Menyelaraskan nama identifier

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

Melakukan left join dengan merge()

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

data_terintegrasi[, c(
  "customer_id", "nama", "jumlah_transaksi", "total_purchase"
)]
##    customer_id   nama jumlah_transaksi total_purchase
## 1         C001    Ani                5        1500000
## 2         C002   Budi                3         900000
## 3         C003  Citra                7        2700000
## 4         C004   Dodi                2         600000
## 5         C005    Eka                6        2100000
## 6         C006   Fani                4        1300000
## 7         C007 Gilang               20       25000000
## 8         C008   Hana                5        1700000
## 9         C009  Indra                3         800000
## 10        C010   Joko                8        3200000
## 11        C011   Kiki               NA             NA

Penggunaan all.x = TRUE membuat seluruh data pelanggan tetap dipertahankan, meskipun ada yang tidak memiliki pasangan data transaksi.

Memvalidasi hasil integrasi

# Apakah jumlah baris berubah secara tidak wajar?
c(sebelum = nrow(pelanggan), sesudah = nrow(data_terintegrasi))
## sebelum sesudah 
##      11      11
# Apakah customer_id tetap unik?
sum(duplicated(data_terintegrasi$customer_id))
## [1] 0
# Missing value baru akibat tidak ditemukan pasangan transaksi
colSums(is.na(data_terintegrasi[, c("jumlah_transaksi", "total_purchase")]))
## jumlah_transaksi   total_purchase 
##                1                1
# Pelanggan tanpa pasangan transaksi
data_terintegrasi[is.na(data_terintegrasi$jumlah_transaksi),
                  c("customer_id", "nama")]
##    customer_id nama
## 11        C011 Kiki

Mengisi nol atau mempertahankan NA?

Nilai NA pada data transaksi ini sebenarnya bisa memiliki dua arti yang berbeda:

  • pelanggan tersebut memang belum pernah melakukan transaksi sama sekali; atau
  • data transaksi pelanggan tersebut tidak tersedia, atau gagal dipadankan dengan data pelanggan.

Nilai ini baru boleh diubah menjadi 0 apabila definisinya memang sudah dipastikan lebih dulu.

# 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

Dataset Akhir dan Evaluasi

Memilih atribut akhir

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"

data_final
##    customer_id   nama usia            kota      status pendapatan_imputasi
## 1         C001    Ani 21.0       Pekanbaru       Aktif             4500000
## 2         C002   Budi 25.0       Pekanbaru       Aktif             4900000
## 3         C003  Citra 23.0       Pekanbaru       Aktif             5200000
## 4         C004   Dodi 50.0           Dumai       Aktif             4800000
## 5         C005    Eka 27.0       Pekanbaru Tidak Aktif             4900000
## 6         C006   Fani 27.5           Dumai Tidak Aktif             5100000
## 7         C007 Gilang 31.0       Pekanbaru       Aktif           500000000
## 8         C008   Hana 29.0            Siak       Aktif             4700000
## 9         C009  Indra 22.0       Pekanbaru       Aktif             4600000
## 10        C010   Joko 35.0           Dumai Tidak Aktif             5300000
## 11        C011   Kiki 28.0 Tidak diketahui       Aktif             4900000
##    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              1500000
## 2                       3               900000
## 3                       7              2700000
## 4                       2               600000
## 5                       6              2100000
## 6                       4              1300000
## 7                      20             25000000
## 8                       5              1700000
## 9                       3               800000
## 10                      8              3200000
## 11                      0                    0

Audit akhir

audit_akhir <- audit_data(data_final)
audit_akhir
##                   atribut      tipe jumlah_missing persen_missing jumlah_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

Membandingkan kondisi sebelum dan sesudah

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

Menyimpan hasil

write.csv(data_final,
          "data_pelanggan_setelah_preprocessing.csv",
          row.names = FALSE)

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

Pertanyaan Refleksi

  1. Apakah dataset yang sudah tidak memiliki missing value otomatis berarti lebih berkualitas?
  2. Mengapa outlier tidak boleh langsung dihapus begitu saja?
  3. Bagaimana proses preprocessing bisa menimbulkan bias pada data?
  4. Mengapa parameter imputasi dan transformasi sebaiknya dihitung dari data pelatihan saja?
  5. Apa risiko yang muncul saat integrasi data dilakukan jika identifier-nya ternyata tidak unik?

Ringkasan

Berikut rangkuman alur yang sudah dikerjakan:

  1. memahami struktur data serta kualitas awalnya;
  2. membersihkan kategori yang tidak konsisten, data duplikat, dan pelanggaran aturan domain;
  3. menangani missing values tanpa harus menyembunyikan ketidakpastian yang ada;
  4. mendeteksi dan mengevaluasi keberadaan outlier;
  5. mentransformasikan atribut-atribut numerik; dan
  6. mengintegrasikan seluruh data serta memvalidasi hasil akhirnya.

Pada dasarnya, preprocessing merupakan proses pengambilan keputusan. Kode R di sini hanya menjalankan keputusan-keputusan tersebut, sementara kualitas hasil akhirnya tetap bergantung pada seberapa paham kita terhadap data, tujuan analisis yang ingin dicapai, dan kelengkapan dokumentasi dari setiap perubahan yang dilakukan.

Daftar Pustaka

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