Pendahuluan

Latar Belakang

Data preprocessing merupakan salah satu tahap penting dalam proses data mining karena data yang diperoleh dari suatu sumber umumnya belum berada dalam kondisi yang sepenuhnya siap untuk digunakan dalam analisis. Data mentah dapat mengandung berbagai permasalahan, seperti missing value, data duplikat, kategori yang tidak konsisten, kesalahan dalam batas atau aturan nilai (domain error), serta nilai yang terlalu ekstrem atau outlier. Apabila permasalahan tersebut tidak diperiksa dan ditangani dengan tepat, kualitas data dapat menurun dan pada akhirnya dapat memengaruhi hasil analisis maupun proses pemodelan yang dilakukan.

Oleh karena itu, diperlukan proses preprocessing untuk memahami kondisi data sejak awal, mengidentifikasi berbagai permasalahan yang terdapat di dalamnya, serta menentukan tindakan yang sesuai untuk memperbaiki atau mengelola permasalahan tersebut. Proses ini tidak hanya berfokus pada menghilangkan data yang dianggap bermasalah, tetapi juga mempertimbangkan apakah suatu nilai benar-benar merupakan kesalahan atau justru memiliki informasi yang penting. Dengan demikian, preprocessing merupakan bagian yang membutuhkan pemeriksaan dan pengambilan keputusan secara sistematis sebelum data digunakan pada tahap analisis selanjutnya.

Pada tugas ini, proses preprocessing diterapkan secara bertahap menggunakan RStudio dengan mengacu pada materi Data Preprocessing Menggunakan RStudio. Tahapan dimulai dengan memahami struktur dan kondisi awal dataset, kemudian dilanjutkan dengan proses data cleaning untuk memperbaiki ketidakkonsistenan, memeriksa duplikasi, serta mengevaluasi kesesuaian nilai terhadap aturan domain. Setelah itu, dilakukan identifikasi dan penanganan missing values, kemudian dilanjutkan dengan pendeteksian dan evaluasi outlier menggunakan pendekatan yang sesuai.

Setelah kualitas data diperbaiki, tahap berikutnya adalah melakukan transformasi atribut numerik, seperti normalisasi Min-Max, normalisasi Z-Score, dan decimal scaling. Transformasi ini dilakukan untuk melihat bagaimana perubahan skala data dapat memengaruhi karakteristik variabel. Selanjutnya, dua sumber data diintegrasikan dengan memperhatikan kesesuaian identifier atau kunci penghubung antar-data. Hasil integrasi tersebut kemudian diperiksa kembali untuk memastikan bahwa proses penggabungan tidak menimbulkan masalah baru.

Melalui rangkaian tahapan tersebut, tugas ini bertujuan untuk menunjukkan bagaimana data yang awalnya masih memiliki berbagai permasalahan dapat diproses secara sistematis hingga menghasilkan dataset yang lebih konsisten dan siap digunakan untuk tahap analisis berikutnya. Seluruh proses dilakukan menggunakan fungsi dasar R, sehingga implementasinya tidak memerlukan pemasangan paket tambahan. Alur ini sejalan dengan materi praktikum yang menekankan pemahaman kondisi awal data, data cleaning, penanganan missing values, evaluasi outlier, transformasi data, integrasi sumber data, serta validasi kondisi data setelah preprocessing.

Tujuan Praktikum

Setelah menyelesaikan praktikum, diharapkan dapat:

  1. Mengidentifikasi masalah kualitas data menggunakan R.
  2. Melakukan data cleaning secara sistematis.
  3. Menangani missing values.
  4. Mendeteksi dan mengevaluasi outlier.
  5. Melakukan transformasi atribut numerik.
  6. Mengintegrasikan dua sumber data.
  7. Memvalidasi kondisi data setelah preprocessing.

Persiapan RStudio

Memeriksa lingkungan kerja

Kode berikut digunakan untuk mengetahui direktori kerja dan versi R yang digunakan.

getwd()
## [1] "C:/Users/USER/Downloads"
R.version.string
## [1] "R version 4.4.1 (2024-06-14 ucrt)"

Membangun Dataset Praktikum

Dataset berikut sengaja dibuat memiliki beberapa masalah agar seluruh tahapan preprocessing dapat dipraktikkan. Masalah tersebut meliputi duplikasi customer_id, missing value, kategori tidak konsisten, kesalahan nilai usia, dan nilai pendapatan yang sangat besar.

Data Pelanggan

pelanggan_raw <- data.frame(
  customer_id = c(
    "C001", "C002", "C003", "C004", "C005",
    "C006", "C007", "C008", "C009", "C010",
    "C010", "C011", "C012", "C013", "C014",
    "C015", "C016", "C017", "C018", "C019"
  ),
  nama = c(
    "Ani", "Budi", "Citra", "Dodi", "Eka",
    "Fani", "Gilang", "Hana", "Indra", "Joko",
    "Joko", "Kiki", "Lala", "Maya", "Nanda",
    "Oki", "Putri", "Rian", "Sari", "Tono"
  ),
  usia = c(
    21, 25, 23, 150, 27,
    NA, 31, 29, 22, 35,
    35, 28, 24, 26, 30,
    19, NA, 33, 27, 41
  ),
  pendapatan = c(
    4500000, NA, 5200000, 4800000, 4900000,
    5100000, 500000000, 4700000, 4600000, 5300000,
    5300000, NA, 4200000, 5500000, 4800000,
    3900000, 5100000, 5700000, 5000000, 6000000
  ),
  kota = c(
    "Pekanbaru", " PKU", "PEKANBARU", "Dumai", "pekanbaru",
    "DUMAI", "Pekanbaru ", "Siak", "PKU", "Dumai",
    "Dumai", NA, "Pekanbaru", "pekanbaru", "DUMAI",
    "Siak", "PEKANBARU", "Pekanbaru", "PKU", "Dumai"
  ),
  status = c(
    "Aktif", "aktif", "ACTIVE", "A", "Tidak Aktif",
    "nonaktif", "Aktif", "AKTIF", "A", "Tidak aktif",
    "Tidak aktif", "Aktif", "active", "A", "Aktif",
    "tidak aktif", "AKTIF", "aktif", "nonaktif", "Aktif"
  ),
  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
## 13        C012   Lala   24    4.2e+06  Pekanbaru      active
## 14        C013   Maya   26    5.5e+06  pekanbaru           A
## 15        C014  Nanda   30    4.8e+06      DUMAI       Aktif
## 16        C015    Oki   19    3.9e+06       Siak tidak aktif
## 17        C016  Putri   NA    5.1e+06  PEKANBARU       AKTIF
## 18        C017   Rian   33    5.7e+06  Pekanbaru       aktif
## 19        C018   Sari   27    5.0e+06        PKU    nonaktif
## 20        C019   Tono   41    6.0e+06      Dumai       Aktif

Data Transaksi

transaksi_raw <- data.frame(
  cust_id = c(
    "C001", "C002", "C003", "C004", "C005",
    "C006", "C007", "C008", "C009", "C010",
    "C011", "C012", "C013", "C014", "C015",
    "C016", "C017", "C018", "C999", "C019"
  ),
  jumlah_transaksi = c(
    5, 3, 7, 2, 6,
    4, 25, 5, 3, 8,
    1, 4, 6, 9, 3,
    NA, 7, 2, 5, 10
  ),
  total_purchase = c(
    1500000, 900000, 2700000, 600000, 2100000,
    1300000, 35000000, 1700000, 800000, 3200000,
    250000, 1400000, 2200000, 4500000, 1000000,
    NA, 2800000, 700000, 1200000, 4200000
  ),
  stringsAsFactors = FALSE
)

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               25        3.5e+07
## 8     C008                5        1.7e+06
## 9     C009                3        8.0e+05
## 10    C010                8        3.2e+06
## 11    C011                1        2.5e+05
## 12    C012                4        1.4e+06
## 13    C013                6        2.2e+06
## 14    C014                9        4.5e+06
## 15    C015                3        1.0e+06
## 16    C016               NA             NA
## 17    C017                7        2.8e+06
## 18    C018                2        7.0e+05
## 19    C999                5        1.2e+06
## 20    C019               10        4.2e+06

Eksplorasi Awal Data

Melihat struktur dataset

Tahap ini bertujuan memahami ukuran, nama variabel, tipe data, beberapa baris awal, dan ringkasan statistik.

dim(pelanggan_raw)
## [1] 20  6
names(pelanggan_raw)
## [1] "customer_id" "nama"        "usia"        "pendapatan"  "kota"       
## [6] "status"
str(pelanggan_raw)
## 'data.frame':    20 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:20          Length:20          Min.   : 19.00   Min.   :  3900000  
##  Class :character   Class :character   1st Qu.: 24.25   1st Qu.:  4725000  
##  Mode  :character   Mode  :character   Median : 27.50   Median :  5050000  
##                                        Mean   : 34.78   Mean   : 32477778  
##                                        3rd Qu.: 32.50   3rd Qu.:  5300000  
##                                        Max.   :150.00   Max.   :500000000  
##                                        NA's   :2        NA's   :2          
##      kota              status         
##  Length:20          Length:20         
##  Class :character   Class :character  
##  Mode  :character   Mode  :character  
##                                       
##                                       
##                                       
## 

Audit awal kualitas data

Missing value

colSums(is.na(pelanggan_raw))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           2           2           1           0
round(colMeans(is.na(pelanggan_raw)) * 100, 2)
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0          10          10           5           0

Duplikasi

sum(duplicated(pelanggan_raw))
## [1] 1
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"
sort(unique(pelanggan_raw$status))
##  [1] "A"           "active"      "ACTIVE"      "aktif"       "Aktif"      
##  [6] "AKTIF"       "nonaktif"    "tidak aktif" "Tidak aktif" "Tidak Aktif"

Rentang atribut numerik

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

Fungsi audit data

Fungsi berikut membuat ringkasan kualitas setiap atribut sehingga pemeriksaan dapat dilakukan secara lebih sistematis.

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          19
## 2        nama character              0              0          19
## 3        usia   numeric              2             10          17
## 4  pendapatan   numeric              2             10          16
## 5        kota character              1              5          10
## 6      status character              0              0          10

Data Cleaning

Menyiapkan data untuk dibersihkan

pelanggan <- pelanggan_raw

Standardisasi kategori

Pada data mentah terdapat perbedaan spasi dan penggunaan huruf besar-kecil. Tahap pertama adalah membersihkan spasi, kemudian menyeragamkan huruf menjadi kecil untuk memudahkan pencocokan kategori.

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

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"

Selanjutnya kategori diseragamkan menjadi label yang konsisten.

pelanggan$kota[
  pelanggan$kota %in% c("pku", "pekanbaru")
] <- "Pekanbaru"

pelanggan$kota[
  pelanggan$kota == "dumai"
] <- "Dumai"

pelanggan$kota[
  pelanggan$kota == "siak"
] <- "Siak"

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"

Deduplikasi

customer_id digunakan sebagai identifier pelanggan. Baris dengan identifier yang berulang diperiksa terlebih dahulu.

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

Kemunculan kedua untuk C010 dihapus.

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

rownames(pelanggan) <- NULL

dim(pelanggan)
## [1] 19  6

Pemeriksaan aturan domain

Aturan domain digunakan untuk menemukan nilai yang tidak masuk akal berdasarkan konteks data. Pada contoh ini usia yang dianggap masuk akal adalah 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>
## NA.1        <NA> <NA>   NA         NA  <NA>   <NA>

Pendapatan negatif juga diperiksa sebagai kandidat pelanggaran domain.

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>

Nilai usia C004 sebesar 150 diperbaiki menjadi 50 berdasarkan sumber asli.

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

Log perubahan

Pencatatan perubahan penting agar proses preprocessing dapat ditelusuri.

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

Penanganan Missing Values

Mengidentifikasi lokasi nilai hilang

colSums(is.na(pelanggan))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           2           2           1           0
pelanggan[
  !complete.cases(pelanggan),
]
##    customer_id  nama usia pendapatan      kota      status
## 2         C002  Budi   25         NA Pekanbaru       Aktif
## 6         C006  Fani   NA    5100000     Dumai Tidak Aktif
## 11        C011  Kiki   28         NA      <NA>       Aktif
## 16        C016 Putri   NA    5100000 Pekanbaru       Aktif

Strategi 1 — Menghapus baris lengkap

Sebagai ilustrasi, data tanpa missing value dapat diperoleh dengan complete.cases(). Jumlah baris sebelum dan sesudah dibandingkan.

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

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

Strategi 2 — Perbandingan Mean dan Median serta Imputasi Median

Strategi kedua dalam menangani missing value adalah melakukan imputasi menggunakan nilai median. Median dipilih karena lebih tahan terhadap pengaruh nilai ekstrem dibandingkan mean.

Perbandingan Mean dan Median

Sebelum melakukan imputasi, nilai mean dan median pada atribut numerik dibandingkan terlebih dahulu.

# Menghitung mean dan median usia
mean_usia <- mean(
  pelanggan$usia,
  na.rm = TRUE
)

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

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

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

# Menampilkan hasil perbandingan
data.frame(
  Atribut = c(
    "Usia",
    "Pendapatan"
  ),
  Mean = c(
    mean_usia,
    mean_pendapatan
  ),
  Median = c(
    median_usia,
    median_pendapatan
  )
)
##      Atribut         Mean  Median
## 1       Usia 2.888235e+01      27
## 2 Pendapatan 3.407647e+07 5000000

Perbandingan mean dan median digunakan untuk melihat apakah terdapat perbedaan yang cukup besar antara keduanya. Perbedaan yang besar dapat menunjukkan adanya nilai ekstrem yang memengaruhi rata-rata.

Visualisasi Perbandingan Mean dan Median

Untuk memperjelas perbedaan antara mean dan median, digunakan visualisasi titik dengan nilai mean dan median ditampilkan secara langsung pada grafik. Visualisasi ini membantu pembaca melihat perbedaan kedua ukuran statistik tanpa terpengaruh oleh tinggi batang.

# Menyiapkan data
nilai <- c(
  mean_usia,
  median_usia,
  mean_pendapatan,
  median_pendapatan
)

nama <- c(
  "Mean Usia",
  "Median Usia",
  "Mean Pendapatan",
  "Median Pendapatan"
)

# Membuat grafik
plot(
  nilai,
  1:4,
  xlab = "Nilai",
  ylab = "",
  yaxt = "n",
  pch = 19,
  main = "Perbandingan Mean dan Median"
)

# Nama pada sumbu Y
axis(
  2,
  at = 1:4,
  labels = nama,
  las = 1
)

# Menampilkan nilai pada grafik
text(
  nilai,
  1:4,
  labels = format(
    round(nilai),
    big.mark = ".",
    scientific = FALSE
  ),
  pos = 3
)
## Warning in prettyNum(.Internal(format(x, trim, digits, nsmall, width, 3L, :
## 'big.mark' and 'decimal.mark' are both '.', which could be confusing

Berdasarkan visualisasi tersebut, nilai mean dan median dapat dibandingkan secara langsung melalui angka yang ditampilkan pada grafik. Perbedaan antara mean dan median, khususnya pada pendapatan, menunjukkan bahwa terdapat nilai yang relatif ekstrem yang memengaruhi nilai mean. Oleh karena itu, median dipilih sebagai nilai imputasi karena lebih tahan terhadap pengaruh nilai ekstrem.

Melakukan Imputasi Median

Selanjutnya, nilai NA pada atribut usia dan pendapatan digantikan dengan nilai median masing-masing.

# Membuat salinan atribut usia
pelanggan$usia_imputasi <- pelanggan$usia

# Mengganti NA pada usia dengan median usia
pelanggan$usia_imputasi[
  is.na(pelanggan$usia_imputasi)
] <- median_usia

# Membuat salinan atribut pendapatan
pelanggan$pendapatan_imputasi <- pelanggan$pendapatan

# Mengganti NA pada pendapatan dengan median pendapatan
pelanggan$pendapatan_imputasi[
  is.na(pelanggan$pendapatan_imputasi)
] <- median_pendapatan

# Menampilkan hasil imputasi
pelanggan[
  ,
  c(
    "customer_id",
    "usia",
    "usia_imputasi",
    "pendapatan",
    "pendapatan_imputasi"
  )
]
##    customer_id usia usia_imputasi pendapatan pendapatan_imputasi
## 1         C001   21            21    4.5e+06             4.5e+06
## 2         C002   25            25         NA             5.0e+06
## 3         C003   23            23    5.2e+06             5.2e+06
## 4         C004   50            50    4.8e+06             4.8e+06
## 5         C005   27            27    4.9e+06             4.9e+06
## 6         C006   NA            27    5.1e+06             5.1e+06
## 7         C007   31            31    5.0e+08             5.0e+08
## 8         C008   29            29    4.7e+06             4.7e+06
## 9         C009   22            22    4.6e+06             4.6e+06
## 10        C010   35            35    5.3e+06             5.3e+06
## 11        C011   28            28         NA             5.0e+06
## 12        C012   24            24    4.2e+06             4.2e+06
## 13        C013   26            26    5.5e+06             5.5e+06
## 14        C014   30            30    4.8e+06             4.8e+06
## 15        C015   19            19    3.9e+06             3.9e+06
## 16        C016   NA            27    5.1e+06             5.1e+06
## 17        C017   33            33    5.7e+06             5.7e+06
## 18        C018   27            27    5.0e+06             5.0e+06
## 19        C019   41            41    6.0e+06             6.0e+06

Pemeriksaan Hasil Imputasi

Setelah proses imputasi dilakukan, jumlah missing value diperiksa kembali untuk memastikan bahwa nilai NA telah ditangani.

c(
  missing_usia = sum(
    is.na(pelanggan$usia_imputasi)
  ),
  missing_pendapatan = sum(
    is.na(pelanggan$pendapatan_imputasi)
  )
)
##       missing_usia missing_pendapatan 
##                  0                  0

Jika hasil pemeriksaan menunjukkan nilai 0 pada kedua atribut, maka seluruh missing value pada atribut usia dan pendapatan telah berhasil ditangani menggunakan metode imputasi median.

Imputasi kategori

Untuk kota yang tidak diketahui, digunakan label Tidak diketahui agar ketidakpastian tidak disamarkan sebagai kategori tertentu.

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 
##               5              11               2               1

Menambahkan indikator missing

Indikator biner dapat digunakan untuk mempertahankan informasi bahwa nilai pendapatan pada awalnya merupakan missing value.

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

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

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

Penanganan Outlier

Visualisasi dengan boxplot

Boxplot digunakan untuk membantu melihat kandidat nilai ekstrem pada pendapatan.

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

Menghitung batas IQR

Metode IQR menggunakan Q1, Q3, dan IQR untuk menentukan batas bawah dan batas atas kandidat outlier.

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         5250000          500000         4000000         6000000

Menandai kandidat outlier

Nilai yang berada di luar batas IQR ditandai sebagai kandidat outlier. Penandaan ini bukan berarti nilai langsung dihapus.

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             5.0e+08
## 15        C015    Oki             3.9e+06

Winsorization

Sebagai contoh penanganan, nilai yang berada di luar batas digeser ke batas bawah atau batas atas menggunakan winsorization.

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             5.0e+08             6e+06
## 15        C015             3.9e+06             4e+06

Visualisasi Setelah Penanganan Outlier

Setelah dilakukan winsorization, boxplot kembali digunakan untuk melihat perubahan distribusi pendapatan. Visualisasi ini digunakan untuk membandingkan kondisi data sebelum dan sesudah penanganan outlier.

par(mfrow = c(1, 2))

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

boxplot(
  pelanggan$pendapatan_winsor,
  horizontal = TRUE,
  main = "Sesudah Winsorization",
  xlab = "Pendapatan"
)

par(mfrow = c(1, 1))

Berdasarkan perbandingan boxplot, sebelum dilakukan winsorization terdapat nilai pendapatan yang sangat ekstrem dan berada jauh dari kelompok data utama. Setelah dilakukan winsorization, nilai ekstrem tersebut dibatasi berdasarkan batas yang diperoleh dari metode IQR. Dengan demikian, pengaruh nilai ekstrem terhadap distribusi pendapatan dapat dikurangi tanpa menghapus observasi dari dataset.

Transformasi Data

Transformasi digunakan untuk mengubah skala atribut numerik. Tiga metode yang digunakan adalah min–maks, z-score, dan decimal scaling.

Normalisasi min–maks

Rumus min–maks untuk rentang [0,1] adalah:

\[ 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.06451613             4.5e+06      0.0012094336
## 2         C002   25  0.19354839             5.0e+06      0.0022172949
## 3         C003   23  0.12903226             5.2e+06      0.0026204394
## 4         C004   50  1.00000000             4.8e+06      0.0018141504
## 5         C005   27  0.25806452             4.9e+06      0.0020157226
## 6         C006   NA  0.25806452             5.1e+06      0.0024188672
## 7         C007   31  0.38709677             5.0e+08      1.0000000000
## 8         C008   29  0.32258065             4.7e+06      0.0016125781
## 9         C009   22  0.09677419             4.6e+06      0.0014110058
## 10        C010   35  0.51612903             5.3e+06      0.0028220117
## 11        C011   28  0.29032258             5.0e+06      0.0022172949
## 12        C012   24  0.16129032             4.2e+06      0.0006047168
## 13        C013   26  0.22580645             5.5e+06      0.0032251562
## 14        C014   30  0.35483871             4.8e+06      0.0018141504
## 15        C015   19  0.00000000             3.9e+06      0.0000000000
## 16        C016   NA  0.25806452             5.1e+06      0.0024188672
## 17        C017   33  0.45161290             5.7e+06      0.0036283007
## 18        C018   27  0.25806452             5.0e+06      0.0022172949
## 19        C019   41  0.70967742             6.0e+06      0.0042330175

Normalisasi z-score

Z-score mengubah nilai menjadi ukuran relatif terhadap mean dan simpangan baku.

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  -1.054       -0.233
## 2  -0.506       -0.229
## 3  -0.780       -0.227
## 4   2.925       -0.231
## 5  -0.231       -0.230
## 6  -0.231       -0.228
## 7   0.318        4.129
## 8   0.043       -0.232
## 9  -0.917       -0.233
## 10  0.867       -0.226
## 11 -0.094       -0.229
## 12 -0.643       -0.236
## 13 -0.368       -0.225
## 14  0.181       -0.231
## 15 -1.329       -0.239
## 16 -0.231       -0.228
## 17  0.592       -0.223
## 18 -0.231       -0.229
## 19  1.690       -0.220

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.0039 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             4.5e+06      0.0012094336   -0.2334737
## 2         C002             5.0e+06      0.0022172949   -0.2290712
## 3         C003             5.2e+06      0.0026204394   -0.2273102
## 4         C004             4.8e+06      0.0018141504   -0.2308322
## 5         C005             4.9e+06      0.0020157226   -0.2299517
## 6         C006             5.1e+06      0.0024188672   -0.2281907
## 7         C007             5.0e+08      1.0000000000    4.1294449
## 8         C008             4.7e+06      0.0016125781   -0.2317127
## 9         C009             4.6e+06      0.0014110058   -0.2325932
## 10        C010             5.3e+06      0.0028220117   -0.2264297
## 11        C011             5.0e+06      0.0022172949   -0.2290712
## 12        C012             4.2e+06      0.0006047168   -0.2361153
## 13        C013             5.5e+06      0.0032251562   -0.2246686
## 14        C014             4.8e+06      0.0018141504   -0.2308322
## 15        C015             3.9e+06      0.0000000000   -0.2387568
## 16        C016             5.1e+06      0.0024188672   -0.2281907
## 17        C017             5.7e+06      0.0036283007   -0.2229076
## 18        C018             5.0e+06      0.0022172949   -0.2290712
## 19        C019             6.0e+06      0.0042330175   -0.2202661
##    pendapatan_decimal
## 1              0.0045
## 2              0.0050
## 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.0050
## 12             0.0042
## 13             0.0055
## 14             0.0048
## 15             0.0039
## 16             0.0051
## 17             0.0057
## 18             0.0050
## 19             0.0060

Dampak outlier terhadap normalisasi

Perbandingan berikut menunjukkan bagaimana penanganan outlier dapat memengaruhi hasil normalisasi min–maks.

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 Penanganan Outlier terhadap Normalisasi"
)

abline(
  0,
  1,
  lty = 2
)

Integrasi Data

Memeriksa kunci pada kedua sumber

Sebelum melakukan integrasi, identifier harus diperiksa agar tidak menghasilkan penggandaan baris yang tidak diinginkan.

sum(
  duplicated(
    pelanggan$customer_id
  )
)
## [1] 0
sum(
  duplicated(
    transaksi_raw$cust_id
  )
)
## [1] 0
setdiff(
  pelanggan$customer_id,
  transaksi_raw$cust_id
)
## character(0)
setdiff(
  transaksi_raw$cust_id,
  pelanggan$customer_id
)
## [1] "C999"

Menyamakan nama kunci

transaksi <- transaksi_raw

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

Menggabungkan data

merge(..., all.x = TRUE) digunakan untuk mempertahankan seluruh pelanggan dan mengambil informasi transaksi yang cocok.

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               25        3.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                1        2.5e+05
## 12        C012   Lala                4        1.4e+06
## 13        C013   Maya                6        2.2e+06
## 14        C014  Nanda                9        4.5e+06
## 15        C015    Oki                3        1.0e+06
## 16        C016  Putri               NA             NA
## 17        C017   Rian                7        2.8e+06
## 18        C018   Sari                2        7.0e+05
## 19        C019   Tono               10        4.2e+06

Validasi hasil integrasi

c(
  sebelum = nrow(pelanggan),
  sesudah = nrow(data_terintegrasi)
)
## sebelum sesudah 
##      19      19
sum(
  duplicated(
    data_terintegrasi$customer_id
  )
)
## [1] 0
colSums(
  is.na(
    data_terintegrasi[
      ,
      c(
        "jumlah_transaksi",
        "total_purchase"
      )
    ]
  )
)
## jumlah_transaksi   total_purchase 
##                1                1

Pelanggan yang tidak memiliki pasangan transaksi ditampilkan untuk diperiksa lebih lanjut.

data_terintegrasi[
  is.na(
    data_terintegrasi$jumlah_transaksi
  ),
  c(
    "customer_id",
    "nama"
  )
]
##    customer_id  nama
## 16        C016 Putri

Penanganan pelanggan tanpa transaksi

Jika setelah pemeriksaan diketahui bahwa NA memang berarti pelanggan belum pernah bertransaksi, nilai dapat diubah menjadi 0.

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 Final dan Audit Akhir

Membentuk dataset final

Hanya variabel yang diperlukan untuk dataset akhir dipertahankan.

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       Pekanbaru       Aktif             4.5e+06
## 2         C002   Budi   25       Pekanbaru       Aktif             5.0e+06
## 3         C003  Citra   23       Pekanbaru       Aktif             5.2e+06
## 4         C004   Dodi   50           Dumai       Aktif             4.8e+06
## 5         C005    Eka   27       Pekanbaru Tidak Aktif             4.9e+06
## 6         C006   Fani   27           Dumai Tidak Aktif             5.1e+06
## 7         C007 Gilang   31       Pekanbaru       Aktif             5.0e+08
## 8         C008   Hana   29            Siak       Aktif             4.7e+06
## 9         C009  Indra   22       Pekanbaru       Aktif             4.6e+06
## 10        C010   Joko   35           Dumai Tidak Aktif             5.3e+06
## 11        C011   Kiki   28 Tidak diketahui       Aktif             5.0e+06
## 12        C012   Lala   24       Pekanbaru       Aktif             4.2e+06
## 13        C013   Maya   26       Pekanbaru       Aktif             5.5e+06
## 14        C014  Nanda   30           Dumai       Aktif             4.8e+06
## 15        C015    Oki   19            Siak Tidak Aktif             3.9e+06
## 16        C016  Putri   27       Pekanbaru       Aktif             5.1e+06
## 17        C017   Rian   33       Pekanbaru       Aktif             5.7e+06
## 18        C018   Sari   27       Pekanbaru Tidak Aktif             5.0e+06
## 19        C019   Tono   41           Dumai       Aktif             6.0e+06
##    pendapatan_missing outlier_pendapatan usia_minmax pendapatan_minmax
## 1                   0              FALSE  0.06451613      0.0012094336
## 2                   1              FALSE  0.19354839      0.0022172949
## 3                   0              FALSE  0.12903226      0.0026204394
## 4                   0              FALSE  1.00000000      0.0018141504
## 5                   0              FALSE  0.25806452      0.0020157226
## 6                   0              FALSE  0.25806452      0.0024188672
## 7                   0               TRUE  0.38709677      1.0000000000
## 8                   0              FALSE  0.32258065      0.0016125781
## 9                   0              FALSE  0.09677419      0.0014110058
## 10                  0              FALSE  0.51612903      0.0028220117
## 11                  1              FALSE  0.29032258      0.0022172949
## 12                  0              FALSE  0.16129032      0.0006047168
## 13                  0              FALSE  0.22580645      0.0032251562
## 14                  0              FALSE  0.35483871      0.0018141504
## 15                  0               TRUE  0.00000000      0.0000000000
## 16                  0              FALSE  0.25806452      0.0024188672
## 17                  0              FALSE  0.45161290      0.0036283007
## 18                  0              FALSE  0.25806452      0.0022172949
## 19                  0              FALSE  0.70967742      0.0042330175
##    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                      25              3.5e+07
## 8                       5              1.7e+06
## 9                       3              8.0e+05
## 10                      8              3.2e+06
## 11                      1              2.5e+05
## 12                      4              1.4e+06
## 13                      6              2.2e+06
## 14                      9              4.5e+06
## 15                      3              1.0e+06
## 16                      0              0.0e+00
## 17                      7              2.8e+06
## 18                      2              7.0e+05
## 19                     10              4.2e+06

Audit akhir

audit_akhir <- audit_data(
  data_final
)

audit_akhir
##                   atribut      tipe jumlah_missing persen_missing jumlah_unik
## 1             customer_id character              0              0          19
## 2                    nama character              0              0          19
## 3                    usia   numeric              0              0          16
## 4                    kota character              0              0           4
## 5                  status character              0              0           2
## 6     pendapatan_imputasi   numeric              0              0          15
## 7      pendapatan_missing   integer              0              0           2
## 8      outlier_pendapatan   logical              0              0           2
## 9             usia_minmax   numeric              0              0          16
## 10      pendapatan_minmax   numeric              0              0          15
## 11 jumlah_transaksi_final   numeric              0              0          12
## 12   total_purchase_final   numeric              0              0          19
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      20      19
## 2 Duplikasi customer_id       1       0
## 3   Total missing value       5       0
## 4    Kategori kota unik      10       4
## 5  Kategori status unik      10       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
)

Kesimpulan

Berdasarkan seluruh tahapan praktikum, preprocessing dilakukan secara bertahap mulai dari memahami struktur dan kualitas awal data, membersihkan kategori, menghapus duplikasi, memperbaiki pelanggaran domain, menangani missing values, mendeteksi dan mengevaluasi outlier, melakukan transformasi numerik, mengintegrasikan data, hingga melakukan audit akhir.

Hal penting yang perlu diperhatikan adalah bahwa preprocessing bukan sekadar menjalankan kode. Setiap perubahan terhadap data merupakan keputusan yang perlu memiliki alasan. Outlier tidak otomatis dihapus, missing value tidak selalu harus dibuang, dan hasil integrasi harus divalidasi agar tidak menimbulkan duplikasi atau kesalahan baru.

Pertanyaan Refleksi

  1. Apakah dataset tanpa missing value selalu lebih berkualitas?
  2. Mengapa outlier tidak boleh otomatis dihapus?
  3. Bagaimana preprocessing dapat menimbulkan bias?
  4. Mengapa parameter imputasi dan transformasi seharusnya dihitung dari data pelatihan?
  5. Apa risiko integrasi data jika identifier tidak unik?

Ringkasan Alur

Data mentah
    ↓
Eksplorasi dan audit awal
    ↓
Standardisasi kategori
    ↓
Deduplikasi dan pemeriksaan domain
    ↓
Penanganan missing value
    ↓
Deteksi dan evaluasi outlier
    ↓
Transformasi data
    ↓
Integrasi data
    ↓
Audit akhir
    ↓
Data siap digunakan untuk analisis/data mining