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

  1. menguraikan kondisi-kondisi pada data yang membuat preprocessing menjadi perlu dilakukan;
  2. mengenali persoalan kualitas data dengan memanfaatkan R;
  3. melakukan proses pembersihan data (data cleaning) secara terstruktur;
  4. menentukan serta menerapkan cara penanganan nilai yang hilang (missing values);
  5. mengenali dan menilai keberadaan outlier;
  6. mentransformasikan atribut numerik ke bentuk yang sesuai; dan
  7. menggabungkan dua sumber data yang berbeda sekaligus memverifikasi hasil penggabungannya.

Skenario Praktikum

Sebuah perusahaan e-commerce hendak melakukan kajian terhadap pelanggannya. Data yang tersedia berasal dari dua sumber berbeda, yaitu:

  • data pelanggan, yang berisi identitas, usia, kota domisili, pendapatan, serta status keanggotaan; dan
  • data transaksi, yang berisi jumlah transaksi dan total nilai pembelian.

Kedua sumber data tersebut belum layak dipakai secara langsung sebab masih memuat nilai yang hilang, penulisan kategori yang tidak seragam, data ganda (duplikat), nilai-nilai ekstrem, serta perbedaan penamaan pada kolom identifier. Oleh sebab itu, mahasiswa diminta mengolah kedua data tersebut hingga terbentuk satu dataset analisis yang sudah bersih dan siap digunakan.

Prinsip utama: sebuah nilai tidak boleh langsung dibersihkan hanya karena tampak janggal. Setiap tindakan perubahan pada data wajib disertai alasan yang jelas, mengacu pada aturan tertentu, dan dicatat dalam sebuah log.

Persiapan RStudio

Membuat proyek

  1. Jalankan aplikasi RStudio.
  2. Pilih menu File → New Project → New Directory → New Project.
  3. Tentukan nama proyek, contohnya praktikum_preprocessing.
  4. Simpan berkas ini pada folder proyek yang baru dibuat.
  5. Tekan tombol Knit untuk mengubahnya menjadi keluaran HTML.

Memeriksa lingkungan kerja

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

Seluruh materi pada modul ini hanya memanfaatkan fungsi bawaan R, sehingga tidak diperlukan pemasangan paket tambahan apa pun.

Membangun Dataset Praktikum

Jalankan blok kode di bawah ini. Dataset yang dibuat sengaja dikondisikan “tidak rapi” supaya setiap tahapan preprocessing dapat 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 mengubah apa pun, lihat dahulu struktur, ukuran, jenis tiap atribut, serta contoh beberapa observasi paling 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 mengemukakan enam dimensi kualitas data, yakni accuracy, completeness, consistency, timeliness, believability, dan interpretability. Perlu dipahami bahwa tidak semua dimensi tersebut bisa langsung diukur hanya dari isi tabel yang ada. Sebagai contoh, dimensi accuracy baru dapat diketahui apabila ada pembanding terhadap kondisi yang sesungguhnya, sedangkan timeliness memerlukan keterangan mengenai kapan data terakhir diperbarui.

Pada bagian ini, pemeriksaan yang dilakukan hanya dibatasi pada persoalan yang memang bisa dikenali langsung dari dataset yang tersedia.

# 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

Catatan pribadi: angka-angka hasil audit di atas bukanlah keputusan final mengenai langkah cleaning yang harus diambil. Audit ini sekadar berfungsi sebagai sinyal awal, menunjukkan atribut mana saja yang layak ditelusuri lebih dalam.

Bagian II — Implementasi Data Cleaning

Membuat salinan kerja

Data mentah tetap disimpan apa adanya agar setiap perubahan yang dilakukan selanjutnya masih bisa ditelusuri kembali ke sumber aslinya.

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

Penyeragaman kategori pada tahap ini didasarkan pada aturan domain yang telah ditetapkan. Perlu diingat, pada kondisi nyata di lapangan, aturan semacam ini sebaiknya dikonfirmasi terlebih dahulu lewat kamus data resmi atau langsung kepada pihak yang memiliki data tersebut.

# 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: langkah menghapus baris duplikat semacam ini hanya tepat dilakukan apabila memang setiap customer_id diasumsikan hanya mewakili satu pelanggan tunggal. Apabila pada kenyataannya satu pelanggan dapat memiliki lebih dari satu baris data, maka tindakan penghapusan ini justru keliru dan perlu ditinjau ulang.

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>

Terlihat bahwa nilai usia 150 tidak memenuhi aturan domain yang berlaku. Anggaplah setelah dilakukan pengecekan ulang terhadap formulir aslinya, diketahui bahwa nilai yang seharusnya tercatat 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 sekadar memperagakan salah satu strategi yang tersedia. Objek pelanggan sendiri belum diganti secara permanen pada tahap ini.

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

Pertanyaan renungan: seandainya seluruh baris yang tidak lengkap dihapus, berapa persen data yang akan hilang dari keseluruhan?

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

Strategi 2: imputasi mean dan median

Atribut pendapatan diketahui memiliki nilai yang sangat ekstrem. Sebelum menentukan metode imputasi yang dipakai, sebaiknya nilai mean dan median dibandingkan lebih dulu.

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

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

Oleh karena sebaran pendapatan menjadi sangat condong (skewed) akibat adanya angka 500 juta, median dipandang lebih stabil untuk dipakai sebagai nilai pengganti pada contoh 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 langsung diisi memakai modus. Pada latihan kali ini, dipilih kategori eksplisit berupa Tidak diketahui, dengan tujuan agar ketidakpastian yang ada tidak ditutup-tutupi begitu saja.

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

Kolom indikator berguna untuk tetap menyimpan jejak informasi bahwa suatu nilai sebenarnya pernah hilang, meskipun nilai tersebut sudah diisi lewat imputasi.

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 berpotensi mengubah bentuk sebaran data serta memperkecil variasinya. Karena alasan itulah, evaluasi terhadap data tidak boleh dianggap selesai hanya karena seluruh NA sudah tidak ada lagi.

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

Angka 500 juta tersebut tidak lantas dihapus begitu saja. Ada tiga kemungkinan penyebab yang perlu dipertimbangkan terlebih dahulu:

  1. Kesalahan saat input data — apabila memang demikian, nilai dapat dikoreksi asalkan nilai yang benar bisa dipastikan kebenarannya.
  2. Observasi yang sah namun bernilai ekstrem — jika demikian, nilai tersebut sebaiknya tetap dipertahankan, atau ditangani memakai metode yang bersifat robust.
  3. Berasal dari populasi yang berbeda — apabila ternyata pelanggan korporasi tercampur dengan pelanggan ritel biasa, maka kedua segmen tersebut sebaiknya dipisahkan.

Pada modul ini, nilai tersebut tetap dipertahankan apa adanya, dan sebagai pembanding dibuat pula satu variabel tambahan berupa versi winsorized, semata-mata untuk melihat perbedaan dampak dari kedua pendekatan tersebut.

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

Teknik winsorizing di atas hanya dijadikan sebagai contoh tambahan. Metode ini sebaiknya diterapkan hanya bila memang sesuai dengan tujuan analisis yang ingin dicapai, dan setiap pembatasan nilai ekstrem yang dilakukan wajib didokumentasikan dengan jelas.

Bagian V — Transformasi Data

Normalisasi min–maks

Rumus untuk memetakan nilai ke dalam 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 saya: keberadaan outlier bisa membuat sebagian besar nilai hasil normalisasi min–maks terjepit ke dalam rentang yang amat sempit. Itulah sebabnya deteksi outlier perlu lebih dahulu dilakukan sebelum akhirnya menentukan metode 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"

Keluaran dari fungsi setdiff() di atas memperlihatkan pelanggan-pelanggan yang belum memiliki catatan transaksi, sekaligus catatan transaksi yang tidak menemukan pasangan datanya pada tabel 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 argumen all.x = TRUE memastikan seluruh data pelanggan tetap dipertahankan, sekalipun ada di antaranya 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 yang muncul pada data transaksi sesungguhnya dapat menyimpan dua makna yang berbeda, yaitu:

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

Oleh karena itu, nilai NA baru boleh diganti menjadi 0 apabila maknanya sudah dipastikan dan dikonfirmasi terlebih dahulu.

# 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. Benarkah dataset yang sudah tidak memiliki missing value dapat dipastikan lebih berkualitas?
  2. Mengapa outlier tidak selayaknya dihapus secara otomatis begitu saja?
  3. Melalui mekanisme apa preprocessing yang dilakukan justru dapat memunculkan bias baru pada data?
  4. Apa alasan yang mendasari mengapa parameter untuk imputasi dan transformasi semestinya dihitung dari data pelatihan saja?
  5. Risiko apa yang mungkin muncul ketika integrasi data dilakukan padahal identifier yang dipakai tidak unik?

Ringkasan

Secara garis besar, tahapan yang telah dikerjakan pada praktikum ini meliputi:

  1. menelaah struktur data serta menilai kualitas awalnya;
  2. membersihkan kategori yang tidak seragam, data ganda, dan pelanggaran terhadap aturan domain;
  3. menangani nilai yang hilang tanpa menutupi ketidakpastian yang sebenarnya ada;
  4. mengenali dan menilai keberadaan outlier;
  5. mentransformasikan atribut numerik ke bentuk yang lebih sesuai; serta
  6. menggabungkan data dari dua sumber sekaligus memvalidasi hasil penggabungannya.

Pada dasarnya, preprocessing merupakan rangkaian proses pengambilan keputusan. Kode R yang ditulis hanyalah alat untuk menjalankan keputusan-keputusan tersebut; baik atau tidaknya hasil akhir tetap bergantung pada seberapa dalam pemahaman terhadap data, kejelasan tujuan analisis, serta kelengkapan dokumentasi atas 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.