Persiapan RStudio

Membuat proyek

Sebelum masuk ke tahap teknis, proyek RStudio perlu disiapkan lebih dulu agar seluruh file praktikum tersimpan rapi dalam satu folder kerja.

  • Buka RStudio.
  • Pilih File → New Project.
  • Lalu New Directory → New Project.
  • Tentukan nama proyek, misalnya praktikum_preprocessing.
  • Simpan file .Rmd dalam folder proyek tersebut.
  • Klik tombol Knit untuk menghasilkan output HTML.

Memeriksa lingkungan kerja

Dua hal paling dasar yang perlu dipastikan di awal adalah lokasi direktori kerja dan versi R yang sedang digunakan.

getwd()
## [1] "D:/SEMESTER 5/2. DATA MINING"
R.version.string
## [1] "R version 4.4.2 (2024-10-31 ucrt)"

Karena seluruh tahapan pada modul ini hanya memanfaatkan fungsi bawaan R (base R), praktikum dapat langsung dijalankan tanpa perlu memasang paket tambahan apa pun.

Dataset Praktikum

Dataset yang dipakai, yaitu dataset pelanggan dan dataset transaksi.Kondisi awal dataset masih mengandung beberapa ketidakteraturan sehingga dapat digunakan untuk memperlihatkan berbagai teknik preprocessing.

Data pelanggan memuat informasi seperti customer_id, nama, usia, pendapatan, kota, dan status. Sementara itu, data transaksi berisi informasi yang berkaitan dengan aktivitas transaksi pelanggan.

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
)

Isi dari kedua tabel tersebut dapat dilihat pada tampilan berikut.

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

Bagian I — Konsep Data Preprocessing

Memahami struktur data

Sebelum data “dibersihkan”, struktur dasarnya perlu dipahami lebih dulu — berapa ukurannya, kolom apa saja yang ada, dan bagaimana tipe datanya.

Dimensi data. Fungsi dim() memberi tahu berapa jumlah baris dan kolom sekaligus dalam satu perintah.

dim(pelanggan_raw)
## [1] 12  6

Nama Variabel. Untuk mengetahui atribut apa saja yang tersedia, cukup panggil names().

names(pelanggan_raw)
## [1] "customer_id" "nama"        "usia"        "pendapatan"  "kota"       
## [6] "status"

Struktur Data. Bila ingin melihat gambaran menyeluruh — tipe data tiap kolom sekaligus beberapa contoh nilainya — str() adalah pilihan yang paling praktis.

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

Cuplikan data. head() menampilkan beberapa baris pertama saja, berguna untuk sekadar mengintip bentuk data tanpa harus memuat seluruh tabel.

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

Statistik Deskriptif. Terakhir, summary() merangkum setiap kolom secara numerik maupun kategorikal — mulai dari nilai minimum, maksimum, hingga rata-rata untuk kolom angka.

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

Pemeriksaan Kualitas Awal Data

Setelah struktur data dipahami, langkah berikutnya adalah menilai kualitasnya: apakah ada nilai hilang, data yang terduplikasi, penulisan kategori yang tidak seragam, atau nilai yang berada di luar rentang wajar.

Missing Value

colSums(is.na(pelanggan_raw))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           1           2           1           0

Jika dilihat dalam bentuk persentase, gambarannya menjadi seperti ini.

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

DUplikasi Data

sum(duplicated(pelanggan_raw))
## [1] 1

Khusus untuk kolom customer_id, apakah ada ID pelanggan yang muncul lebih dari satu kali?

sum(duplicated(pelanggan_raw$customer_id))
## [1] 1

Konsistensi Kategori

sort(unique(pelanggan_raw$kota))
## [1] " PKU"       "Dumai"      "DUMAI"      "pekanbaru"  "Pekanbaru" 
## [6] "PEKANBARU"  "Pekanbaru " "PKU"        "Siak"

Pola serupa juga dicek pada kolom status.

sort(unique(pelanggan_raw$status))
## [1] "A"           "ACTIVE"      "aktif"       "Aktif"       "AKTIF"      
## [6] "nonaktif"    "Tidak aktif" "Tidak Aktif"

Pemeriksaan Nilai Numerik

range(pelanggan_raw$usia, na.rm = TRUE)
## [1]  21 150

Begitu pula pada kolom pendapatan.

range(pelanggan_raw$pendapatan, na.rm = TRUE)
## [1] 4.5e+06 5.0e+08

Membuat Fungsi Ringkasan Kualitas

Mengulang kode pemeriksaan satu per satu seperti di atas cukup memakan waktu bila dilakukan berkali-kali. Karena itu, dibuat satu fungsi audit_data() yang merangkum semua indikator kualitas — tipe data, jumlah dan persentase missing value, serta banyaknya nilai unik — dalam satu tabel ringkas.

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

Dari tabel audit ini, atribut dengan persentase missing tinggi atau jumlah nilai unik yang mencurigakan langsung terlihat, sehingga bisa jadi prioritas untuk diperiksa lebih lanjut.

Bagian II — Data Cleaning

Agar data mentah (pelanggan_raw) tetap bisa dijadikan pembanding di akhir, seluruh proses pembersihan dilakukan pada sebuah salinan bernama pelanggan, bukan pada data aslinya.

pelanggan <- pelanggan_raw

Membersihkan Spasi

Dua kolom teks kota dan status masih memiliki variasi penulisan yang murni disebabkan oleh spasi berlebih dan perbedaan huruf besar/kecil. Ini dirapikan lebih dulu sebelum masuk ke standarisasi kategori yang lebih kompleks.

Menghilangkan spasi di awal dan akhir teks dengan trimws():

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

Lalu seluruh huruf diseragamkan menjadi huruf kecil supaya proses pencocokan kategori pada tahap berikutnya tidak sensitif terhadap kapitalisasi.

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

Efeknya cukup terlihat — jumlah variasi unik pada kedua kolom sudah mulai berkurang meski belum sepenuhnya seragam.

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

Menyeragamkan Kategori

Spasi dan kapitalisasi sudah rapi, namun beberapa kategori masih merupakan singkatan atau istilah berbeda untuk makna yang sama — misalnya “pku” dan “pekanbaru”, atau “active” dan “a”. Pemetaan manual diperlukan di sini.

Kota “pku” dan “pekanbaru” digabung menjadi satu label baku “Pekanbaru”, begitu juga kota lainnya:

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

Pola yang sama diterapkan pada kolom status, dengan mengelompokkan seluruh variasi “aktif” di satu sisi dan “tidak aktif” di sisi lain:

pelanggan$status[pelanggan$status %in% c("aktif", "active", "a")] <- "Aktif"
pelanggan$status[pelanggan$status %in% c("tidak aktif", "nonaktif")] <- "Tidak Aktif"

Sekarang kedua kolom sudah memiliki kategori yang benar-benar seragam, tanpa duplikasi makna.

sort(unique(pelanggan$kota))
## [1] "Dumai"     "Pekanbaru" "Siak"
sort(unique(pelanggan$status))
## [1] "Aktif"       "Tidak Aktif"

Menangani Duplikasi

Ingat bahwa C010 sempat muncul dua kali pada customer_id. Sebelum dihapus, baris-baris yang terlibat perlu ditampilkan dulu agar terlihat jelas datanya identik atau tidak.

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 tersebut memang identik, kemunculan kedua dapat dibuang dengan aman menggunakan duplicated(), lalu nomor baris dirapikan ulang.

pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL

dim(pelanggan)
## [1] 11  6

Koreksi Nilai Berdasarkan Domain

Selain duplikasi, nilai yang secara logika tidak masuk akal (domain violation) juga perlu diperiksa. Contohnya, usia manusia yang wajar biasanya berada di 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>

Aturan yang sama berlaku untuk pendapatan — nilai negatif jelas tidak masuk akal secara bisnis.

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>

Hasil investigasi lapangan menunjukkan bahwa usia 150 tahun tadi sebenarnya adalah kesalahan input, dan nilai yang benar adalah 50 tahun. Koreksi pun dilakukan langsung pada baris dengan customer_id “C004”.

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

Membuat Log Perubahan Data

Setiap keputusan yang diambil selama proses cleaning sebaiknya didokumentasikan, bukan hanya diketahui dari kode. Log ini penting sebagai bentuk transparansi dan jejak audit.

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 Missing Value

Setelah proses cleaning kategori dan duplikasi selesai, giliran nilai hilang yang ditangani. Langkah pertama selalu sama: cari tahu di kolom mana saja NA itu berada.

colSums(is.na(pelanggan))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           1           2           1           0

Mengetahui kolomnya saja belum cukup — baris spesifik yang mengandung nilai hilang juga perlu dilihat langsung, dengan bantuan complete.cases().

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

Penghapusan Baris

Cara paling sederhana adalah membuang seluruh baris yang mengandung nilai hilang (listwise deletion). Perlu ditekankan bahwa ini hanya simulasi — objek pelanggan yang asli tidak diubah, hasilnya disimpan terpisah di pelanggan_complete.

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

Berapa persen data yang akan hilang jika strategi ini benar-benar diterapkan?

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

Angka ini memberi gambaran seberapa “mahal” strategi penghapusan baris — semakin tinggi persentasenya, semakin besar risiko kehilangan informasi yang sebenarnya masih berguna.

Imputasi Mean dan Median

Alternatif yang lebih hemat data adalah imputasi, yaitu mengisi nilai yang hilang dengan estimasi tertentu. Dua pilihan paling umum adalah rata-rata (mean) dan nilai tengah (median).

Karena kolom pendapatan diketahui memiliki nilai ekstrem (outlier), mean dan median perlu dibandingkan dulu sebelum menentukan mana yang lebih representatif.

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

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

Selisih keduanya cukup jauh — pertanda mean sudah terpengaruh outlier. Karena itu, median dipilih sebagai nilai pengganti karena lebih tahan terhadap nilai ekstrem.

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

Perlakuan yang sama, dengan alasan konsistensi metode, diterapkan pula pada kolom usia.

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

Perbandingan nilai sebelum dan sesudah imputasi dapat dilihat berdampingan berikut ini.

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

Imputasi Nilai Kategorik

Kolom kategorik seperti kota tidak bisa langsung diisi dengan modus, sebab hal itu berisiko menciptakan asumsi lokasi yang sebenarnya tidak diketahui kebenarannya. Pendekatan yang lebih jujur adalah memberi label eksplisit “Tidak diketahui”, sehingga nilai hilang tetap dapat ditelusuri, bukan disamarkan seolah-olah datanya lengkap.

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

Indikator Missing

Salah satu kelemahan imputasi adalah informasi “nilai ini awalnya hilang” bisa hilang begitu saja setelah diisi. Kolom indikator biner dibuat untuk menjaga jejak tersebut tetap ada.

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

Membandingkan Distribusi Sebelum dan Sesudah Imputasi

Angka saja terkadang kurang menggambarkan dampak imputasi secara utuh. Visualisasi histogram berdampingan membantu melihat apakah bentuk distribusi berubah signifikan setelah nilai hilang diisi.

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

Terlihat bahwa imputasi cenderung memadatkan sebagian nilai di sekitar median, sehingga variasi data sedikit mengecil. Ini mengingatkan bahwa proses evaluasi tidak boleh berhenti hanya karena seluruh NA sudah terisi.

Bagian IV — Penanganan Outlier

Visualisasi dengan Boxplot

Sebelum menghitung batas outlier secara matematis, boxplot memberi gambaran visual cepat mengenai keberadaan nilai-nilai ekstrem pada pendapatan.

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

Metode IQR

Agar batas outlier tidak sekadar bersifat visual dan subjektif, metode IQR (Interquartile Range) digunakan untuk menghitung batas bawah dan batas atas secara pasti.

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 Outlier

Nilai apa pun yang berada di luar rentang [batas_bawah, batas_atas] ditandai sebagai kandidat outlier melalui kolom logika baru.

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

Winsorizing

Menemukan outlier bukan berarti otomatis harus dihapus. Ada tiga kemungkinan penyebab yang perlu dipertimbangkan lebih dulu:

  • Kesalahan input — koreksi jika nilai yang benar dapat diverifikasi.
  • Observasi valid tetapi ekstrem — dipertahankan, atau ditangani dengan metode yang lebih robust.
  • Berasal dari populasi berbeda — misalnya pelanggan korporasi tercampur dengan pelanggan ritel, sehingga perlu dipisah sebagai segmen tersendiri.

Pada kasus ini, outlier tetap dipertahankan (tidak dihapus), namun sebagai perbandingan dampak metode, teknik winsorizing diterapkan untuk membatasi nilai ekstrem tanpa membuang datanya.

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

Penting dicatat: winsorizing di sini hanya demonstrasi tambahan. Terapkan hanya jika sesuai tujuan analisis, dan selalu dokumentasikan bahwa nilai ekstrem telah dibatasi — jangan sampai pembaca laporan mengira nilai tersebut asli.

Bagian V — Transformasi Data

Normalisasi Min–Max

Cara pertama untuk mentransformasi data numerik adalah normalisasi min–maks, yang memetakan seluruh nilai ke rentang \([0,1]\) menggunakan rumus:

\[ 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             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

Standarisasi Z-Score

Berbeda dari min–maks yang terikat rentang [0,1], normalisasi z-score justru mengukur seberapa jauh sebuah nilai menyimpang dari rata-rata dalam satuan standar deviasi:

\[ 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

Pendekatan ketiga, decimal scaling, sedikit lebih sederhana secara konsep: nilai cukup dibagi dengan pangkat 10 tertentu sampai seluruh nilai berada dalam rentang [-1, 1].

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

Ketiga metode di atas punya efek numerik yang berbeda meskipun berasal dari kolom yang sama. Menampilkannya bersisian memudahkan melihat perbedaan skala hasil masing-masing metode.

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

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

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)

Titik-titik yang menyimpang jauh dari garis diagonal merah menunjukkan betapa outlier dapat menekan sebagian besar nilai min–maks lainnya ke rentang yang sangat sempit. Inilah alasan mengapa deteksi outlier semestinya dilakukan sebelum memilih metode transformasi, bukan sesudahnya.

Bagian VI — Integrasi Data

Pemeriksaan Kunci

Sebelum dua tabel digabungkan, kolom kunci penghubungnya — customer_id di data pelanggan dan cust_id di data transaksi — perlu dipastikan bersih dari duplikasi dan diketahui kecocokannya.

sum(duplicated(pelanggan$customer_id))
## [1] 0
sum(duplicated(transaksi_raw$cust_id))
## [1] 0

Pelanggan mana saja yang tidak memiliki data transaksi?

setdiff(pelanggan$customer_id, transaksi_raw$cust_id)
## [1] "C011"

Sebaliknya, apakah ada transaksi milik pelanggan yang tidak terdaftar di tabel pelanggan?

setdiff(transaksi_raw$cust_id, pelanggan$customer_id)
## [1] "C012"

Kedua hasil setdiff() di atas menegaskan bahwa hubungan antar tabel ini tidak sepenuhnya 1-ke-1 — ada pelanggan tanpa transaksi, dan ada transaksi tanpa pasangan pelanggan yang valid.

Menyamakan Nama Identifier

Agar merge() dapat mengenali kolom kunci yang sama, nama kolom cust_id pada tabel transaksi disamakan terlebih dahulu menjadi customer_id, mengikuti penamaan pada tabel pelanggan.

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

Melakukan Penggabungan Dataset ()

Dengan nama kolom kunci yang sudah selaras, kedua tabel dapat digabung menggunakan 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        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

Validasi Hasil Integrasi

Proses join tidak boleh langsung dipercaya begitu saja; beberapa pengecekan berikut memastikan hasilnya benar.

Apakah jumlah baris berubah setelah digabung?

c(sebelum = nrow(pelanggan), sesudah = nrow(data_terintegrasi))
## sebelum sesudah 
##      11      11

Apakah customer_id tetap unik pasca-penggabungan, atau justru muncul duplikasi baru akibat relasi many-to-many?

sum(duplicated(data_terintegrasi$customer_id))
## [1] 0

Adakah nilai hilang baru yang muncul akibat pelanggan tanpa pasangan transaksi?

colSums(is.na(data_terintegrasi[, c("jumlah_transaksi", "total_purchase")]))
## jumlah_transaksi   total_purchase 
##                1                1

Pelanggan mana saja tepatnya yang tidak memiliki pasangan transaksi tersebut?

data_terintegrasi[is.na(data_terintegrasi$jumlah_transaksi),
                  c("customer_id", "nama")]
##    customer_id nama
## 11        C011 Kiki

Menangani Missing setelah Integrasi

Ini adalah keputusan yang mudah terlewat: NA pada kolom transaksi setelah join sebenarnya bisa punya dua makna yang sangat berbeda.

Nilai NA baru boleh diganti menjadi 0 apabila makna pertama sudah benar-benar dikonfirmasi kebenarannya — mengubahnya tanpa verifikasi berisiko menyembunyikan masalah pencocokan data.

# 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

Dari seluruh kolom yang telah dibuat sepanjang proses — termasuk kolom-kolom perantara seperti usia, kota, atau pendapatan versi mentah — hanya kolom hasil akhir yang dipertahankan untuk dataset final, lalu diberi nama yang lebih ringkas.

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

Audit Akhir

Sama seperti di awal, dataset yang sudah melalui seluruh tahapan preprocessing ini perlu diaudit ulang untuk memastikan tidak ada lagi nilai hilang atau duplikasi yang tersisa.

Fungsi audit_data() yang sama dari Bagian I dipakai kembali di sini:

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

Pengecekan duplikasi pada customer_id:

sum(duplicated(data_final$customer_id))
## [1] 0

Dan rekap total nilai hilang per kolom:

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

Perbandingan Kondisi Sebelum dan Sesudah

Untuk melihat sejauh mana dampak preprocessing secara keseluruhan, beberapa indikator kunci dibandingkan antara kondisi data mentah dan kondisi data final dalam satu tabel ringkas.

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

Sebagai langkah penutup, dataset final beserta log seluruh perubahan yang telah dilakukan disimpan ke dalam file CSV agar dapat dipakai kembali untuk analisis selanjutnya tanpa perlu mengulang proses preprocessing dari awal.

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

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

Ringkasan

Secara garis besar, alur preprocessing yang telah dilalui pada praktikum ini adalah:

  1. Memahami struktur data dan menilai kualitas awalnya;
  2. Membuat salinan kerja, merapikan spasi dan kapitalisasi, menyeragamkan kategori, mendeteksi serta menghapus duplikasi, dan memeriksa pelanggaran aturan domain;
  3. Menangani nilai hilang (missing values) dengan strategi yang sesuai untuk masing-masing tipe data;
  4. Mendeteksi dan menangani outlier;
  5. Melakukan transformasi data melalui normalisasi dan scaling; dan
  6. Menggabungkan (mengintegrasikan) data dari dua sumber serta memvalidasi hasil penggabungannya.

Baik-buruknya hasil preprocessing pada akhirnya bergantung pada keputusan analis dalam memahami konteks data dan tujuan analisis — R hanyalah alat untuk menjalankan keputusan tersebut, bukan penentu keputusan itu sendiri.

Daftar Pustaka

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