Identitas Praktikum

Komponen Keterangan
Mata kuliah Data Mining
Pertemuan 03
Topik Data Preprocessing
Perangkat lunak R dan RStudio
Mahasiswa Nadya Destriani
NIM 2403113644

Skenario Praktikum

Sebuah perusahaan e-commerce ingin melakukan analisis terhadap pelanggan. Data berasal dari dua sumber:

  • data pelanggan, yang berisi identitas, usia, pendapatan, dan status pelanggan.
  • data transaksi, yang berisi jumlah transaksi dan total pembelian.

Data belum dapat langsung digunakan karena mengandung nilai hilang, kategori tidak konsisten, duplikasi, nilai ekstrem, dan perbedaan nama identifier. Mahasiswa diminta menyiapkan data tersebut sampai menjadi satu dataset analisis yang bersih.

Persiapan RStudio

Paket yang digunakan

  1. Buka Rstudio.
  2. Pilih File -> New Project -> New Directory -> New Project.
  3. Beri nama proyek, misalnya praktikum_preprocessing.
  4. Simpan file ini di dalam folder proyek.
  5. Klik Knit untuk menghasilkan HTML.

Memeriksa lingkungan kerja

getwd()
## [1] "D:/TUGAS KULIAH/SEM 5/Data mining"
R.version.string
## [1] "R version 4.5.3 (2026-03-11 ucrt)"

Modul ini menggunakan fungsi dasar R sehingga dapat dijalankan tanpa memasang paket tambahan.

Membangun Dataset Praktikum

Jalankan kode berikut. Dataset sengaja dibuat “kotor” agar seluruh tahap preprocessing dapat 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    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 melakukan perubahan, periksa struktur, dimensi, tipe atribut, dan beberapa observasi 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  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" ...
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  
##                                       
##                                       
##                                       
## 

Data pelanggan pada kondisi awal terdiri dari 12 observasi dan 6 variabel. Beberapa masalah sudah terlihat dari struktur tersebut, yaitu adanya NA pada usia dan pendapatan, usia 150 tahun, pendapatan 500 juta, serta variasi penulisan kota dan status. :contentReferenceoaicite:5

Mengukur kualitas awal

Enam dimensi kualitas data yang dibahas Han, Kamber, dan Pei meliputi accuracy, completeness, consistency, timeliness, believability, dan interpretability. Tidak semua dimensi dapat dihitung hanya dari isi tabel. Misalnya, accuracy memerlukan pembanding terhadap kondisi sebenarnya dan timeliness membutuhkan informasi waktu pemutakhiran.

Pemeriksaan awal berikut berfokus pada masalah yang dapat dideteksi 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"
# Rentang atribut numerik
range(pelanggan_raw$usia, na.rm = TRUE)
## [1]  21 150
range(pelanggan_raw$pendapatan, na.rm = TRUE)
## [1] 4.5e+06 5.0e+08

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 bukan keputusan cleaning. Audit hanya memberi sinyal atribut mana yang perlu diperiksa lebih lanjut.

Bagian II — Implementasi Data Cleaning

Membuat salinan kerja

Data mentah dipertahankan sebagai arsip sehingga perubahan dilakukan pada objek baru.

pelanggan <- pelanggan_raw

Pendekatan ini memungkinkan kondisi sebelum preprocessing tetap dapat dibandingkan dengan kondisi setelah preprocessing.

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 dilakukan menggunakan aturan domain. Dalam praktik nyata, aturan perlu dikonfirmasi melalui kamus data atau 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"

Dengan demikian, variasi aktif, ACTIVE, dan A diperlakukan sebagai kategori yang sama. Demikian pula Tidak aktif dan nonaktif diseragamkan menjadi 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

Ditemukan dua baris dengan customer_id C010. Karena customer_id diasumsikan sebagai identifier unik untuk satu pelanggan, kemunculan kedua dianggap sebagai duplikasi.

# Mempertahankan kemunculan pertama untuk customer_id yang identik
pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL

dim(pelanggan)
## [1] 11  6

Catatan: menghapus duplikasi hanya aman jika setiap customer_id memang harus mewakili satu pelanggan. Jika satu pelanggan boleh memiliki banyak baris, tindakan ini justru salah.

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>
pelanggan$usia[pelanggan$customer_id == "C004"] <- 50

Nilai usia 150 melanggar aturan domain. Misalkan pemeriksaan terhadap formulir asli menunjukkan bahwa nilai yang benar adalah 50.

Log perubahan preprocessing

Agar proses dapat ditelusuri, perubahan utama didokumentasikan.

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

Untuk melihat baris yang memiliki minimal satu nilai hilang:

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

Pada kondisi setelah cleaning, missing value terdapat pada usia C006, pendapatan C002 dan C011, serta kota C011. Kondisi ini perlu ditangani berdasarkan jenis atribut dan karakteristik datanya.

Strategi 1 — Menghapus baris

Strategi complete case analysis ditunjukkan sebagai pembanding.

pelanggan_complete <- pelanggan[complete.cases(pelanggan), ]

cat("Jumlah data sebelum penghapusan :", nrow(pelanggan), "\n")
## Jumlah data sebelum penghapusan : 11
cat("Jumlah data setelah penghapusan  :", nrow(pelanggan_complete), "\n")
## Jumlah data setelah penghapusan  : 8
persen_hilang <- round(
  (1 - nrow(pelanggan_complete) / nrow(pelanggan)) * 100,
  2
)

cat("Persentase data yang hilang :", persen_hilang, "%\n")
## Persentase data yang hilang : 27.27 %

Pada data ini, menghapus seluruh baris yang tidak lengkap akan menghilangkan sebagian observasi. Karena ukuran dataset hanya kecil, penghapusan baris berpotensi menyebabkan kehilangan informasi yang cukup besar. Oleh karena itu, strategi ini tidak dipilih sebagai metode utama.

Strategi 2 — Membandingkan mean dan median

Pendapatan memiliki nilai ekstrem sebesar 500 juta sehingga mean dan median perlu dibandingkan.

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

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

cat("Mean pendapatan   :", mean_pendapatan, "\n")
## Mean pendapatan   : 59900000
cat("Median pendapatan :", median_pendapatan, "\n")
## Median pendapatan : 4900000

Mean sangat dipengaruhi oleh nilai 500 juta, sedangkan median relatif stabil. Oleh karena itu, median dipilih sebagai nilai imputasi pendapatan.

pelanggan$pendapatan_imputasi <- pelanggan$pendapatan

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

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

pelanggan$usia_imputasi <- pelanggan$usia

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

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

Median usia pada data ini adalah 27,5 sehingga usia C006 diisi dengan 27,5.

Strategi 3 — Imputasi nilai kategorik

Untuk kota, nilai hilang tidak langsung diganti dengan modus. Sebagai alternatif, digunakan kategori eksplisit Tidak diketahui.

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

Pendekatan ini menjaga informasi bahwa nilai awal memang tidak tersedia. Dengan demikian, ketidakpastian tidak disamarkan sebagai seolah-olah merupakan kategori yang benar-benar diketahui.

Menambahkan indikator missing

Untuk pendapatan, dibuat variabel indikator yang menunjukkan apakah nilai awalnya hilang.

pelanggan$pendapatan_missing <-
  as.integer(is.na(pelanggan$pendapatan))

table(pelanggan$pendapatan_missing)
## 
## 0 1 
## 9 2

Nilai:

  • 0 = pendapatan tersedia pada data awal;
  • 1 = pendapatan pada data awal merupakan missing value.

Membandingkan distribusi sebelum dan sesudah imputasi

par(mfrow = c(1, 2))

hist(
  pelanggan$pendapatan,
  main = "Sebelum Imputasi",
  xlab = "Pendapatan",
  breaks = 8
)

hist(
  pelanggan$pendapatan_imputasi,
  main = "Sesudah Imputasi Median",
  xlab = "Pendapatan",
  breaks = 8
)

par(mfrow = c(1, 1))

Perbandingan distribusi digunakan untuk memastikan bahwa imputasi tidak dilakukan secara buta. Menghilangkan seluruh NA belum tentu berarti kualitas data otomatis meningkat karena imputasi dapat mengubah distribusi dan variasi data.

Bagian IV — Penanganan Outlier

Visualisasi dengan boxplot

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

Boxplot menunjukkan adanya nilai yang sangat jauh dari sebagian besar observasi.

Menghitung batas IQR

q1 <- quantile(
  pelanggan$pendapatan_imputasi,
  0.25,
  na.rm = TRUE
)

q3 <- quantile(
  pelanggan$pendapatan_imputasi,
  0.75,
  na.rm = TRUE
)

iqr <- IQR(
  pelanggan$pendapatan_imputasi,
  na.rm = TRUE
)

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

Berdasarkan aturan IQR, nilai di luar interval:

\[ [Q_1 - 1.5(IQR),\ Q_3 + 1.5(IQR)] \]

ditandai sebagai kandidat outlier.

Menandai kandidat outlier

pelanggan$outlier_pendapatan <-
  !is.na(pelanggan$pendapatan_imputasi) &
  (
    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

Hasilnya menunjukkan C007 sebagai kandidat outlier dengan pendapatan 500 juta. :contentReferenceoaicite:6

Mengevaluasi tindakan terhadap outlier

Outlier tidak langsung dihapus. Ada beberapa kemungkinan interpretasi:

  1. kesalahan input;
  2. observasi valid tetapi ekstrem;
  3. observasi berasal dari populasi yang berbeda.

Dalam kasus ini, tidak terdapat bukti bahwa pendapatan C007 merupakan kesalahan input. Oleh karena itu, nilai tersebut dipertahankan pada data utama dan hanya dibuat versi winsorized sebagai pembanding.

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

Winsorization di sini digunakan sebagai demonstrasi analitis, bukan sebagai pengganti otomatis terhadap data asli.

Bagian V — Transformasi Data

Transformasi dilakukan agar atribut numerik dapat dibandingkan dalam skala yang lebih sesuai untuk metode data mining tertentu.

Normalisasi min–maks

Normalisasi min–maks mengubah nilai menjadi rentang 0 sampai 1.

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

minmax <- function(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

Normalisasi min–maks membuat kedua atribut berada pada skala yang sama. Namun, metode ini sensitif terhadap nilai ekstrem karena nilai minimum dan maksimum menjadi penentu seluruh rentang.

Normalisasi z-score

Z-score menggunakan rumus:

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

Z-score menunjukkan posisi suatu observasi relatif terhadap rata-rata dalam satuan simpangan baku.

Decimal scaling

Decimal scaling dilakukan dengan membagi nilai dengan \(10^j\), dengan \(j\) dipilih sehingga nilai absolut maksimum menjadi kurang dari 1.

decimal_scaling <- function(x) {

  max_abs <- max(
    abs(x),
    na.rm = TRUE
  )

  if (max_abs == 0) {
    return(x)
  }

  j <- ceiling(log10(max_abs + 1))

  x / (10^j)
}

pelanggan$usia_decimal <-
  decimal_scaling(pelanggan$usia_imputasi)

pelanggan$pendapatan_decimal <-
  decimal_scaling(pelanggan$pendapatan_imputasi)

pelanggan[
  ,
  c(
    "customer_id",
    "pendapatan_imputasi",
    "pendapatan_decimal"
  )
]
##    customer_id pendapatan_imputasi pendapatan_decimal
## 1         C001             4.5e+06             0.0045
## 2         C002             4.9e+06             0.0049
## 3         C003             5.2e+06             0.0052
## 4         C004             4.8e+06             0.0048
## 5         C005             4.9e+06             0.0049
## 6         C006             5.1e+06             0.0051
## 7         C007             5.0e+08             0.5000
## 8         C008             4.7e+06             0.0047
## 9         C009             4.6e+06             0.0046
## 10        C010             5.3e+06             0.0053
## 11        C011             4.9e+06             0.0049

Membandingkan metode transformasi

# Membandingkan metode transformasi

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

# Membulatkan hanya kolom numerik
transformasi[] <- lapply(
  transformasi,
  function(x) {
    if (is.numeric(x)) {
      round(x, 4)
    } else {
      x
    }
  }
)

transformasi
##    customer_id pendapatan_imputasi pendapatan_minmax pendapatan_z
## 1         C001             4.5e+06            0.0000      -0.3041
## 2         C002             4.9e+06            0.0008      -0.3014
## 3         C003             5.2e+06            0.0014      -0.2994
## 4         C004             4.8e+06            0.0006      -0.3021
## 5         C005             4.9e+06            0.0008      -0.3014
## 6         C006             5.1e+06            0.0012      -0.3001
## 7         C007             5.0e+08            1.0000       3.0151
## 8         C008             4.7e+06            0.0004      -0.3028
## 9         C009             4.6e+06            0.0002      -0.3035
## 10        C010             5.3e+06            0.0016      -0.2988
## 11        C011             4.9e+06            0.0008      -0.3014
##    pendapatan_decimal
## 1              0.0045
## 2              0.0049
## 3              0.0052
## 4              0.0048
## 5              0.0049
## 6              0.0051
## 7              0.5000
## 8              0.0047
## 9              0.0046
## 10             0.0053
## 11             0.0049

Ketiga metode menghasilkan skala yang berbeda walaupun berasal dari atribut yang sama. Min–maks menghasilkan rentang 0–1, z-score berpusat pada rata-rata dengan simpangan baku sebagai skala, sedangkan decimal scaling hanya menggeser posisi desimal.

Dampak outlier terhadap normalisasi

Untuk memperlihatkan dampak nilai ekstrem, dibandingkan hasil min–maks data asli dengan data yang telah di-winsorize.

pelanggan$pendapatan_winsor_minmax <-
  minmax(pelanggan$pendapatan_winsor)

plot(
  pelanggan$pendapatan_minmax,
  pelanggan$pendapatan_winsor_minmax,
  pch = 19,
  xlab = "Min-maks Data Asli",
  ylab = "Min-maks Data Winsorized",
  main = "Dampak Outlier terhadap Normalisasi"
)

abline(
  a = 0,
  b = 1,
  lty = 2
)

Nilai 500 juta menyebabkan sebagian besar pendapatan yang berada pada kisaran 4–5 juta menjadi sangat dekat dengan nol setelah min–max normalization. Ini menunjukkan bahwa deteksi outlier sebaiknya dipertimbangkan sebelum menentukan metode transformasi. :contentReferenceoaicite:7

Bagian VI — Integrasi Data

Memeriksa kunci pada kedua sumber

Sebelum menggabungkan data, struktur identifier diperiksa.

cat(
  "Jumlah customer_id unik pada pelanggan :",
  length(unique(pelanggan$customer_id)),
  "\n"
)
## Jumlah customer_id unik pada pelanggan : 11
cat(
  "Jumlah cust_id unik pada transaksi :",
  length(unique(transaksi_raw$cust_id)),
  "\n"
)
## Jumlah cust_id unik pada transaksi : 11
cat(
  "Customer tanpa transaksi :",
  sum(
    !pelanggan$customer_id %in%
      transaksi_raw$cust_id
  ),
  "\n"
)
## Customer tanpa transaksi : 1
cat(
  "Transaksi tanpa data pelanggan :",
  sum(
    !transaksi_raw$cust_id %in%
      pelanggan$customer_id
  ),
  "\n"
)
## Transaksi tanpa data pelanggan : 1

Hasil pemeriksaan menunjukkan terdapat identifier C012 pada sumber transaksi yang tidak memiliki pasangan pada sumber pelanggan.

Menyelaraskan nama identifier

Nama identifier pada kedua sumber berbeda:

  • data pelanggan menggunakan customer_id;
  • data transaksi menggunakan cust_id.

Agar dapat digabungkan, nama identifier pada transaksi diselaraskan.

transaksi <- transaksi_raw

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

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

Melakukan left join

Karena data pelanggan merupakan sumber utama, digunakan left join melalui fungsi 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

Penggunaan all.x = TRUE memastikan seluruh pelanggan tetap dipertahankan walaupun tidak memiliki pasangan transaksi. Hal ini menghasilkan NA untuk transaksi C011. :contentReferenceoaicite:8

Memvalidasi hasil integrasi

Memeriksa jumlah baris

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

Jumlah baris tidak seharusnya bertambah secara tidak wajar karena identifier pelanggan telah dibuat unik sebelum proses integrasi.

Memeriksa duplikasi identifier

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

Memeriksa missing value akibat integrasi

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

C011 tidak memiliki pasangan transaksi pada sumber kedua sehingga menghasilkan NA. Kondisi tersebut berbeda dengan missing value pada pendapatan: missing akibat integrasi memiliki makna bahwa tidak ditemukan pasangan transaksi pada sumber transaksi.

Mengisi nol atau mempertahankan NA?

Untuk latihan ini, diasumsikan bahwa tidak ditemukannya transaksi berarti pelanggan belum memiliki transaksi yang tercatat dalam sumber transaksi.

Oleh karena itu, NA pada jumlah transaksi dan total pembelian diubah menjadi nol.

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

data_terintegrasi[
  ,
  c(
    "customer_id",
    "jumlah_transaksi_final",
    "total_purchase_final"
  )
]
##    customer_id jumlah_transaksi_final total_purchase_final
## 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        C011                      0              0.0e+00

Keputusan mengisi nol harus dipahami sebagai keputusan berbasis makna bisnis. Jika NA sebenarnya berarti “data transaksi belum tersedia” dan bukan “tidak ada transaksi”, maka menggantinya dengan nol akan menimbulkan bias.

Dataset Akhir dan Evaluasi

Memilih atribut akhir

Dataset akhir menyimpan atribut identitas, atribut yang telah dibersihkan, hasil imputasi, indikator missing, indikator outlier, hasil transformasi, dan atribut transaksi.

data_final <- data_terintegrasi[
  ,
  c(
    "customer_id",
    "nama",
    "kota_imputasi",
    "status",
    "usia_imputasi",
    "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            kota      status usia pendapatan_imputasi
## 1         C001    Ani       Pekanbaru       Aktif 21.0             4.5e+06
## 2         C002   Budi       Pekanbaru       Aktif 25.0             4.9e+06
## 3         C003  Citra       Pekanbaru       Aktif 23.0             5.2e+06
## 4         C004   Dodi           Dumai       Aktif 50.0             4.8e+06
## 5         C005    Eka       Pekanbaru Tidak Aktif 27.0             4.9e+06
## 6         C006   Fani           Dumai Tidak Aktif 27.5             5.1e+06
## 7         C007 Gilang       Pekanbaru       Aktif 31.0             5.0e+08
## 8         C008   Hana            Siak       Aktif 29.0             4.7e+06
## 9         C009  Indra       Pekanbaru       Aktif 22.0             4.6e+06
## 10        C010   Joko           Dumai Tidak Aktif 35.0             5.3e+06
## 11        C011   Kiki Tidak diketahui       Aktif 28.0             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

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                    kota character              0              0           4
## 4                  status character              0              0           2
## 5                    usia   numeric              0              0          11
## 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

Pemeriksaan khusus:

cat(
  "Duplikasi customer_id :",
  sum(duplicated(data_final$customer_id)),
  "\n"
)
## Duplikasi customer_id : 0
cat(
  "Total missing value :",
  sum(is.na(data_final)),
  "\n"
)
## Total missing value : 0

Dataset akhir diharapkan tidak memiliki missing value pada atribut yang dipilih untuk analisis dan tidak memiliki identifier pelanggan yang duplikat. Pada hasil yang menjadi acuan, audit akhir menunjukkan seluruh atribut akhir memiliki nol missing value dan customer_id tetap unik. :contentReferenceoaicite:9

Membandingkan kondisi sebelum dan sesudah

perbandingan <- data.frame(
  indikator = c(
    "Jumlah baris",
    "Jumlah kolom",
    "Duplikasi customer_id",
    "Total missing value",
    "Kategori kota unik",
    "Kategori status unik"
  ),
  sebelum = c(
    nrow(pelanggan_raw),
    ncol(pelanggan_raw),
    sum(duplicated(pelanggan_raw$customer_id)),
    sum(is.na(pelanggan_raw)),
    length(unique(na.omit(pelanggan_raw$kota))),
    length(unique(na.omit(pelanggan_raw$status)))
  ),
  sesudah = c(
    nrow(data_final),
    ncol(data_final),
    sum(duplicated(data_final$customer_id)),
    sum(is.na(data_final)),
    length(unique(na.omit(data_final$kota))),
    length(unique(na.omit(data_final$status)))
  )
)

library(knitr)
kable(
  perbandingan,
  caption = "Perbandingan kondisi data sebelum dan sesudah preprocessing"
)
Perbandingan kondisi data sebelum dan sesudah preprocessing
indikator sebelum sesudah
Jumlah baris 12 11
Jumlah kolom 6 12
Duplikasi customer_id 1 0
Total missing value 4 0
Kategori kota unik 9 4
Kategori status unik 8 2

Perbandingan tersebut menunjukkan bahwa tujuan preprocessing bukan sekadar mengurangi jumlah masalah secara mekanis. Yang lebih penting adalah memastikan bahwa perubahan tersebut menghasilkan data yang lebih konsisten dan sesuai dengan tujuan analisis.

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 tanpa missing value selalu lebih berkualitas?

Tidak. Dataset tanpa missing value belum tentu lebih berkualitas karena nilai yang hilang dapat diganti dengan nilai yang tidak tepat. Misalnya, mengganti seluruh missing value menggunakan mean dapat mengurangi variasi data dan mengubah distribusi. Selain itu, missing value sendiri dapat mengandung informasi mengenai proses pengumpulan data. Oleh karena itu, kualitas dataset harus dinilai berdasarkan ketepatan, konsistensi, relevansi, dan validitas data, bukan hanya berdasarkan jumlah NA.

Pada preprocessing ini, misalnya, missing pada pendapatan ditangani menggunakan median karena terdapat nilai ekstrem yang membuat mean kurang representatif. Sementara itu, missing pada kota diberi kategori Tidak diketahui agar ketidakpastian tetap terlihat.

2. Mengapa outlier tidak boleh otomatis dihapus?

Outlier tidak selalu merupakan kesalahan. Nilai ekstrem dapat merupakan observasi yang benar-benar terjadi pada populasi. Pada data ini, pendapatan C007 sebesar 500 juta memang sangat jauh dibandingkan pelanggan lain, tetapi tidak terdapat bukti bahwa nilai tersebut merupakan kesalahan input.

Jika nilai tersebut langsung dihapus, informasi mengenai pelanggan dengan karakteristik ekstrem akan hilang. Oleh karena itu, outlier sebaiknya terlebih dahulu diperiksa menggunakan konteks domain, sumber data, dan tujuan analisis.

Dalam preprocessing ini, C007 dipertahankan dalam data utama dan Winsorization hanya digunakan sebagai pembanding. Pendekatan tersebut mengikuti prinsip bahwa outlier harus dievaluasi sebelum ditentukan tindakannya. :contentReferenceoaicite:10

3. Bagaimana preprocessing dapat menimbulkan bias?

Preprocessing dapat menimbulkan bias ketika keputusan yang dibuat selama cleaning mengubah representasi data secara sistematis.

Contohnya adalah imputasi mean. Jika distribusi pendapatan sangat menceng, mean dapat dipengaruhi oleh beberapa nilai ekstrem sehingga mengganti missing value menggunakan mean dapat memberikan nilai yang tidak representatif.

Bias juga dapat muncul ketika baris dengan missing value langsung dihapus. Jika missing value lebih banyak terjadi pada kelompok tertentu, penghapusan tersebut dapat membuat kelompok tersebut menjadi kurang terwakili dalam dataset.

Dengan demikian, preprocessing harus mempertimbangkan karakteristik data dan tujuan analisis, bukan hanya menghasilkan dataset yang terlihat lebih rapi.

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

Jika preprocessing digunakan dalam machine learning, parameter seperti mean, median, standar deviasi, nilai minimum, dan maksimum seharusnya dihitung hanya dari data training.

Alasannya adalah untuk mencegah data leakage. Jika informasi dari data testing digunakan ketika menghitung parameter preprocessing, maka informasi mengenai distribusi data testing secara tidak langsung masuk ke dalam proses pelatihan.

Akibatnya, performa model pada data testing dapat terlihat lebih baik daripada kondisi sebenarnya.

Karena itu, alur yang benar adalah:

Data training
      ↓
Hitung parameter preprocessing
      ↓
Transform training
      ↓
Gunakan parameter yang sama
      ↓
Transform testing
      ↓
Evaluasi model

5. Apa risiko integrasi data jika identifier tidak unik?

Identifier yang tidak unik dapat menyebabkan hasil join menggandakan baris. Misalnya, apabila satu customer_id muncul dua kali pada data pelanggan dan memiliki satu pasangan transaksi, proses join dapat menghasilkan lebih dari satu baris untuk pelanggan tersebut.

Akibatnya jumlah transaksi, total pembelian, atau statistik lainnya dapat terhitung lebih dari satu kali.

Pada preprocessing ini, duplikasi C010 harus ditangani sebelum integrasi. Setelah proses tersebut, customer_id diperiksa kembali untuk memastikan identifier tetap unik. Pendekatan ini penting karena integrasi yang secara sintaksis berhasil belum tentu benar secara substantif.

Perbedaan dan Pengembangan pada Versi Ini

Selain mengikuti seluruh alur utama praktikum, versi ini menambahkan beberapa sudut pandang.

Pertama, dilakukan audit kualitas data secara eksplisit sebelum cleaning sehingga perubahan dapat dibandingkan secara kuantitatif.

Kedua, missing value tidak diperlakukan sama. Missing pada variabel numerik dipertimbangkan menggunakan median, sedangkan missing pada variabel kategorik diberi kategori Tidak diketahui.

Ketiga, outlier tidak langsung dihapus. Pendapatan ekstrem C007 dipertahankan pada dataset utama dan dibuat versi Winsorization sebagai pembanding.

Keempat, proses integrasi divalidasi dari sisi jumlah baris, keunikan identifier, dan missing value yang muncul setelah join.

Kelima, dibuat log perubahan preprocessing sehingga setiap keputusan cleaning dapat ditelusuri.

Ringkasan

Alur preprocessing yang dilakukan dalam praktik ini adalah:

  1. membangun dan memeriksa dataset awal;
  2. melakukan audit kualitas data;
  3. menstandardisasi kategori;
  4. menangani duplikasi;
  5. mengevaluasi dan mengoreksi pelanggaran domain;
  6. mengidentifikasi missing values;
  7. membandingkan strategi penghapusan dan imputasi;
  8. melakukan imputasi numerik dan kategorik;
  9. mempertahankan indikator missing;
  10. mendeteksi outlier menggunakan IQR;
  11. mengevaluasi outlier tanpa langsung menghapusnya;
  12. melakukan Winsorization sebagai analisis pembanding;
  13. melakukan normalisasi min–maks;
  14. melakukan normalisasi z-score;
  15. melakukan decimal scaling;
  16. membandingkan dampak transformasi;
  17. mengintegrasikan data pelanggan dan transaksi;
  18. memvalidasi hasil integrasi;
  19. membentuk dataset akhir;
  20. melakukan audit akhir dan menyimpan hasil.

Preprocessing pada akhirnya bukan sekadar proses teknis untuk membuat data menjadi “bersih”. Preprocessing merupakan rangkaian keputusan yang menentukan informasi apa yang dipertahankan, diubah, atau dihilangkan dari dataset. Karena itu, alasan setiap keputusan perlu dapat dijelaskan dan didokumentasikan.

Daftar Pustaka

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