1 Pendahuluan

Data preprocessing (praproses data) adalah tahapan penting dalam analisis data dan machine learning yang bertujuan untuk mengubah data mentah (raw data) menjadi data yang bersih, konsisten, dan siap dianalisis. Data mentah sering kali memiliki masalah seperti nilai hilang (missing values), duplikasi, outlier, skala yang tidak seragam, maupun format yang tidak konsisten.

Tahapan umum dalam data preprocessing meliputi:

  1. Import dan eksplorasi data
  2. Penanganan missing values
  3. Penanganan data duplikat
  4. Deteksi dan penanganan outlier
  5. Transformasi data (normalisasi/standardisasi)
  6. Encoding data kategorik
  7. Pembagian data (train-test split)

2 Persiapan RStudio

2.1 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.2 (2024-10-31 ucrt)"

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

# Dataset 1: Data Pelanggan Mentah
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", "C020", "C021", "C022", "C023",
                   "C024", "C025", "C026", "C027", "C028", "C029"),
  nama = c("Ani", "Budi", "Citra", "Dodi", "Eka", "Fani",
           "Gilang", "Hana", "Indra", "Joko", "Joko", "Kiki",
           "Lina", "Made", "Nita", "Oki", "Putri", "Qori",
           "Rian", "Sari", "Tono", "Umi", "Vino", "Wati",
           "Xaverius", "Yani", "Zaki", "Ayu", "Bayu", "Cinta"),
  usia = c(21, 25, 23, 150, 27, NA, 31, 29, 22, 35, 35, 28,
           24, 26, 30, 33, 19, 45, 38, 27, 29, 0, 34, 26,
           41, 23, 37, NA, 29, 25),
  pendapatan = c(4500000, NA, 5200000, 4800000, 4900000, 5100000,
                 500000000, 4700000, 4600000, 5300000, 5300000, NA,
                 4550000, 4650000, 4750000, NA, 4400000, 5400000,
                 4950000, -500000, 4850000, 4700000, 4900000, NA,
                 5250000, 4650000, 5050000, 4800000, 4700000, 5300000),
  kota = c("Pekanbaru", " PKU", "PEKANBARU", "Dumai", "pekanbaru",
           "DUMAI", "Pekanbaru ", "Siak", "PKU", "Dumai", "Dumai", NA,
           "Pekanbaru", "PKU", "Dumai", "pekanbaru", "Siak", "DUMAI",
           "Pekanbaru ", "PKU", "pekanbaru", "Dumai", "Siak", "PEKANBARU",
           "Dumai", "pku", "Pekanbaru", "DUMAI", "Siak", "Pekanbaru "),
  status = c("Aktif", "aktif", "ACTIVE", "A", "Tidak Aktif", "nonaktif",
             "Aktif", "AKTIF", "A", "Tidak aktif", "Tidak aktif", "Aktif",
             "aktif", "Aktif", "TIDAK AKTIF", "Aktif", "aktif", "A",
             "nonaktif", "Aktif", "Tidak Aktif", "aktif", "AKTIF", "A",
             "Tidak aktif", "aktif", "Aktif", "nonaktif", "A", "Tidak Aktif"),
  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
## 13        C012     Lina   24    4550000  Pekanbaru       aktif
## 14        C013     Made   26    4650000        PKU       Aktif
## 15        C014     Nita   30    4750000      Dumai TIDAK AKTIF
## 16        C015      Oki   33         NA  pekanbaru       Aktif
## 17        C016    Putri   19    4400000       Siak       aktif
## 18        C017     Qori   45    5400000      DUMAI           A
## 19        C018     Rian   38    4950000 Pekanbaru     nonaktif
## 20        C019     Sari   27    -500000        PKU       Aktif
## 21        C020     Tono   29    4850000  pekanbaru Tidak Aktif
## 22        C021      Umi    0    4700000      Dumai       aktif
## 23        C022     Vino   34    4900000       Siak       AKTIF
## 24        C023     Wati   26         NA  PEKANBARU           A
## 25        C024 Xaverius   41    5250000      Dumai Tidak aktif
## 26        C025     Yani   23    4650000        pku       aktif
## 27        C026     Zaki   37    5050000  Pekanbaru       Aktif
## 28        C027      Ayu   NA    4800000      DUMAI    nonaktif
## 29        C028     Bayu   29    4700000       Siak           A
## 30        C029    Cinta   25    5300000 Pekanbaru  Tidak Aktif
# Dataset 2: Data Transaksi Mentah
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",
              "C019", "C999"),
  jumlah_transaksi = c(5, 3, 7, 2, 6, 4, 20, 5, 3, 8,
                       1, 4, 6, 9, 3, NA, 7, 2, 10, 5),
  total_purchase = c(1500000, 900000, 2700000, 600000, 2100000, 1300000,
                     25000000, 1700000, 800000, 3200000, 250000, 1400000,
                     2200000, 4500000, 1000000, NA, 2800000, 700000,
                     4200000, 1200000),
  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               20        2.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    C019               10        4.2e+06
## 20    C999                5        1.2e+06

4 Bagian I — Eksplorasi Awal Data

4.1 Melihat struktur dataset

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

dim(pelanggan_raw)
## [1] 30  6
names(pelanggan_raw)
## [1] "customer_id" "nama"        "usia"        "pendapatan"  "kota"       
## [6] "status"
str(pelanggan_raw)
## 'data.frame':    30 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:30          Length:30          Min.   :  0.00   Min.   :  -500000  
##  Class :character   Class :character   1st Qu.: 24.75   1st Qu.:  4662500  
##  Mode  :character   Mode  :character   Median : 28.50   Median :  4825000  
##                                        Mean   : 32.57   Mean   : 23723077  
##                                        3rd Qu.: 34.25   3rd Qu.:  5175000  
##                                        Max.   :150.00   Max.   :500000000  
##                                        NA's   :2        NA's   :4          
##      kota              status         
##  Length:30          Length:30         
##  Class :character   Class :character  
##  Mode  :character   Mode  :character  
##                                       
##                                       
##                                       
## 

4.2 Audit awal kualitas data

4.2.1 Missing value

# Banyak missing value per atribut
colSums(is.na(pelanggan_raw))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           2           4           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        6.67       13.33        3.33        0.00

4.2.2 Duplikasi

# Banyak baris duplikat penuh
sum(duplicated(pelanggan_raw))
## [1] 1
# Banyak customer_id yang berulang
sum(duplicated(pelanggan_raw$customer_id))
## [1] 1

4.2.3 Konsistensi kategori

# Kategori yang tercatat pada atribut kota dan status
sort(unique(pelanggan_raw$kota))
##  [1] " PKU"       "Dumai"      "DUMAI"      "pekanbaru"  "Pekanbaru" 
##  [6] "PEKANBARU"  "Pekanbaru " "pku"        "PKU"        "Siak"
sort(unique(pelanggan_raw$status))
## [1] "A"           "ACTIVE"      "aktif"       "Aktif"       "AKTIF"      
## [6] "nonaktif"    "Tidak aktif" "Tidak Aktif" "TIDAK AKTIF"

4.2.4 Rentang atribut numerik

range(pelanggan_raw$usia, na.rm = TRUE)
## [1]   0 150
range(pelanggan_raw$pendapatan, na.rm = TRUE)
## [1] -5e+05  5e+08

4.3 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.00          29
## 2        nama character              0           0.00          29
## 3        usia   numeric              2           6.67          22
## 4  pendapatan   numeric              4          13.33          20
## 5        kota character              1           3.33          11
## 6      status character              0           0.00           9

5 Bagian II — Data Cleaning

5.1 Menyiapkan data untuk dibersihkan

pelanggan <- pelanggan_raw

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

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

Selanjutnya kategori diseragamkan menjadi label yang konsisten.

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

5.3 Deduplikasi

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

# 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

Kemunculan kedua untuk C010 dihapus.

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

dim(pelanggan)
## [1] 29  6

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

# 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>
## 21          C021  Umi    0    4700000 Dumai  Aktif
## NA.1        <NA> <NA>   NA         NA  <NA>   <NA>

Pendapatan negatif juga diperiksa sebagai kandidat pelanggaran domain.

# 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>
## NA.2        <NA> <NA>   NA         NA      <NA>   <NA>
## 19          C019 Sari   27     -5e+05 Pekanbaru  Aktif
## NA.3        <NA> <NA>   NA         NA      <NA>   <NA>

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

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

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

6 Bagian III — Penanganan Missing Values

6.1 Mengidentifikasi lokasi nilai hilang

colSums(is.na(pelanggan))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           2           4           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
## 15        C015  Oki   33         NA Pekanbaru       Aktif
## 23        C023 Wati   26         NA Pekanbaru       Aktif
## 27        C027  Ayu   NA    4800000     Dumai Tidak Aktif

6.2 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] 29
nrow(pelanggan_complete)
## [1] 23
# Persentase baris yang terhapus
round((1 - nrow(pelanggan_complete) / nrow(pelanggan)) * 100, 2)
## [1] 20.69

6.3 Strategi 2 — Imputasi median

Untuk atribut numerik, median digunakan sebagai salah satu strategi imputasi. Mean juga dihitung sebagai pembanding.

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

mean_pendapatan
## [1] 24460000
median_pendapatan
## [1] 4800000

Pendapatan dan usia kemudian dibuat versi imputasinya tanpa menghilangkan kolom asli.

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    4500000             4500000
## 2         C002   25            25         NA             4800000
## 3         C003   23            23    5200000             5200000
## 4         C004   50            50    4800000             4800000
## 5         C005   27            27    4900000             4900000
## 6         C006   NA            28    5100000             5100000
## 7         C007   31            31  500000000           500000000
## 8         C008   29            29    4700000             4700000
## 9         C009   22            22    4600000             4600000
## 10        C010   35            35    5300000             5300000
## 11        C011   28            28         NA             4800000
## 12        C012   24            24    4550000             4550000
## 13        C013   26            26    4650000             4650000
## 14        C014   30            30    4750000             4750000
## 15        C015   33            33         NA             4800000
## 16        C016   19            19    4400000             4400000
## 17        C017   45            45    5400000             5400000
## 18        C018   38            38    4950000             4950000
## 19        C019   27            27    -500000             -500000
## 20        C020   29            29    4850000             4850000
## 21        C021    0             0    4700000             4700000
## 22        C022   34            34    4900000             4900000
## 23        C023   26            26         NA             4800000
## 24        C024   41            41    5250000             5250000
## 25        C025   23            23    4650000             4650000
## 26        C026   37            37    5050000             5050000
## 27        C027   NA            28    4800000             4800000
## 28        C028   29            29    4700000             4700000
## 29        C029   25            25    5300000             5300000

6.4 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 
##               8              16               4               1

6.5 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 
## 25  4

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

7 Bagian IV — Penanganan Outlier

7.1 Visualisasi dengan boxplot

Boxplot digunakan untuk membantu melihat kandidat nilai ekstrem pada pendapatan.

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

7.2 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% 
##         4700000         5050000          350000         4175000         5575000

7.3 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               5e+08
## 19        C019   Sari              -5e+05

7.4 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               5e+08           5575000
## 19        C019              -5e+05           4175000

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

8 Bagian V — Transformasi Data

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

8.1 Normalisasi min–maks

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

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

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.42             4500000        0.00999001
## 2         C002   25        0.50             4800000        0.01058941
## 3         C003   23        0.46             5200000        0.01138861
## 4         C004   50        1.00             4800000        0.01058941
## 5         C005   27        0.54             4900000        0.01078921
## 6         C006   NA        0.56             5100000        0.01118881
## 7         C007   31        0.62           500000000        1.00000000
## 8         C008   29        0.58             4700000        0.01038961
## 9         C009   22        0.44             4600000        0.01018981
## 10        C010   35        0.70             5300000        0.01158841
## 11        C011   28        0.56             4800000        0.01058941
## 12        C012   24        0.48             4550000        0.01008991
## 13        C013   26        0.52             4650000        0.01028971
## 14        C014   30        0.60             4750000        0.01048951
## 15        C015   33        0.66             4800000        0.01058941
## 16        C016   19        0.38             4400000        0.00979021
## 17        C017   45        0.90             5400000        0.01178821
## 18        C018   38        0.76             4950000        0.01088911
## 19        C019   27        0.54             -500000        0.00000000
## 20        C020   29        0.58             4850000        0.01068931
## 21        C021    0        0.00             4700000        0.01038961
## 22        C022   34        0.68             4900000        0.01078921
## 23        C023   26        0.52             4800000        0.01058941
## 24        C024   41        0.82             5250000        0.01148851
## 25        C025   23        0.46             4650000        0.01028971
## 26        C026   37        0.74             5050000        0.01108891
## 27        C027   NA        0.56             4800000        0.01058941
## 28        C028   29        0.58             4700000        0.01038961
## 29        C029   25        0.50             5300000        0.01158841

8.2 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  -0.856       -0.188
## 2  -0.413       -0.184
## 3  -0.635       -0.180
## 4   2.359       -0.184
## 5  -0.191       -0.183
## 6  -0.080       -0.181
## 7   0.252        5.199
## 8   0.031       -0.185
## 9  -0.746       -0.186
## 10  0.696       -0.179
## 11 -0.080       -0.184
## 12 -0.524       -0.187
## 13 -0.302       -0.186
## 14  0.141       -0.185
## 15  0.474       -0.184
## 16 -1.078       -0.189
## 17  1.805       -0.178
## 18  1.028       -0.183
## 19 -0.191       -0.242
## 20  0.031       -0.184
## 21 -3.185       -0.185
## 22  0.585       -0.183
## 23 -0.302       -0.184
## 24  1.361       -0.179
## 25 -0.635       -0.186
## 26  0.918       -0.182
## 27 -0.080       -0.184
## 28  0.031       -0.185
## 29 -0.413       -0.179

8.3 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] -5e-04  5e-01

8.4 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.00999001   -0.1875086
## 2         C002             4800000        0.01058941   -0.1842473
## 3         C003             5200000        0.01138861   -0.1798988
## 4         C004             4800000        0.01058941   -0.1842473
## 5         C005             4900000        0.01078921   -0.1831601
## 6         C006             5100000        0.01118881   -0.1809859
## 7         C007           500000000        1.00000000    5.1991463
## 8         C008             4700000        0.01038961   -0.1853344
## 9         C009             4600000        0.01018981   -0.1864215
## 10        C010             5300000        0.01158841   -0.1788117
## 11        C011             4800000        0.01058941   -0.1842473
## 12        C012             4550000        0.01008991   -0.1869650
## 13        C013             4650000        0.01028971   -0.1858779
## 14        C014             4750000        0.01048951   -0.1847908
## 15        C015             4800000        0.01058941   -0.1842473
## 16        C016             4400000        0.00979021   -0.1885957
## 17        C017             5400000        0.01178821   -0.1777246
## 18        C018             4950000        0.01088911   -0.1826166
## 19        C019             -500000        0.00000000   -0.2418643
## 20        C020             4850000        0.01068931   -0.1837037
## 21        C021             4700000        0.01038961   -0.1853344
## 22        C022             4900000        0.01078921   -0.1831601
## 23        C023             4800000        0.01058941   -0.1842473
## 24        C024             5250000        0.01148851   -0.1793552
## 25        C025             4650000        0.01028971   -0.1858779
## 26        C026             5050000        0.01108891   -0.1815295
## 27        C027             4800000        0.01058941   -0.1842473
## 28        C028             4700000        0.01038961   -0.1853344
## 29        C029             5300000        0.01158841   -0.1788117
##    pendapatan_decimal
## 1             0.00450
## 2             0.00480
## 3             0.00520
## 4             0.00480
## 5             0.00490
## 6             0.00510
## 7             0.50000
## 8             0.00470
## 9             0.00460
## 10            0.00530
## 11            0.00480
## 12            0.00455
## 13            0.00465
## 14            0.00475
## 15            0.00480
## 16            0.00440
## 17            0.00540
## 18            0.00495
## 19           -0.00050
## 20            0.00485
## 21            0.00470
## 22            0.00490
## 23            0.00480
## 24            0.00525
## 25            0.00465
## 26            0.00505
## 27            0.00480
## 28            0.00470
## 29            0.00530

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

9 Bagian VI — Integrasi Data

9.1 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
# Customer yang ada di pelanggan tetapi tidak ada di transaksi
setdiff(pelanggan$customer_id, transaksi_raw$cust_id)
##  [1] "C020" "C021" "C022" "C023" "C024" "C025" "C026" "C027" "C028" "C029"
# Customer yang ada di transaksi tetapi tidak ada di pelanggan
setdiff(transaksi_raw$cust_id, pelanggan$customer_id)
## [1] "C999"

9.2 Menyamakan nama kunci

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

9.3 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               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                1        2.5e+05
## 12        C012     Lina                4        1.4e+06
## 13        C013     Made                6        2.2e+06
## 14        C014     Nita                9        4.5e+06
## 15        C015      Oki                3        1.0e+06
## 16        C016    Putri               NA             NA
## 17        C017     Qori                7        2.8e+06
## 18        C018     Rian                2        7.0e+05
## 19        C019     Sari               10        4.2e+06
## 20        C020     Tono               NA             NA
## 21        C021      Umi               NA             NA
## 22        C022     Vino               NA             NA
## 23        C023     Wati               NA             NA
## 24        C024 Xaverius               NA             NA
## 25        C025     Yani               NA             NA
## 26        C026     Zaki               NA             NA
## 27        C027      Ayu               NA             NA
## 28        C028     Bayu               NA             NA
## 29        C029    Cinta               NA             NA

9.4 Validasi hasil integrasi

# Apakah jumlah baris berubah secara tidak wajar?
c(sebelum = nrow(pelanggan), sesudah = nrow(data_terintegrasi))
## sebelum sesudah 
##      29      29
# 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 
##               11               11

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
## 20        C020     Tono
## 21        C021      Umi
## 22        C022     Vino
## 23        C023     Wati
## 24        C024 Xaverius
## 25        C025     Yani
## 26        C026     Zaki
## 27        C027      Ayu
## 28        C028     Bayu
## 29        C029    Cinta

9.5 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

10 Bagian VII — Dataset Final dan Audit Akhir

10.1 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             4500000
## 2         C002     Budi   25       Pekanbaru       Aktif             4800000
## 3         C003    Citra   23       Pekanbaru       Aktif             5200000
## 4         C004     Dodi   50           Dumai       Aktif             4800000
## 5         C005      Eka   27       Pekanbaru Tidak Aktif             4900000
## 6         C006     Fani   28           Dumai Tidak Aktif             5100000
## 7         C007   Gilang   31       Pekanbaru       Aktif           500000000
## 8         C008     Hana   29            Siak       Aktif             4700000
## 9         C009    Indra   22       Pekanbaru       Aktif             4600000
## 10        C010     Joko   35           Dumai Tidak Aktif             5300000
## 11        C011     Kiki   28 Tidak diketahui       Aktif             4800000
## 12        C012     Lina   24       Pekanbaru       Aktif             4550000
## 13        C013     Made   26       Pekanbaru       Aktif             4650000
## 14        C014     Nita   30           Dumai Tidak Aktif             4750000
## 15        C015      Oki   33       Pekanbaru       Aktif             4800000
## 16        C016    Putri   19            Siak       Aktif             4400000
## 17        C017     Qori   45           Dumai       Aktif             5400000
## 18        C018     Rian   38       Pekanbaru Tidak Aktif             4950000
## 19        C019     Sari   27       Pekanbaru       Aktif             -500000
## 20        C020     Tono   29       Pekanbaru Tidak Aktif             4850000
## 21        C021      Umi    0           Dumai       Aktif             4700000
## 22        C022     Vino   34            Siak       Aktif             4900000
## 23        C023     Wati   26       Pekanbaru       Aktif             4800000
## 24        C024 Xaverius   41           Dumai Tidak Aktif             5250000
## 25        C025     Yani   23       Pekanbaru       Aktif             4650000
## 26        C026     Zaki   37       Pekanbaru       Aktif             5050000
## 27        C027      Ayu   28           Dumai Tidak Aktif             4800000
## 28        C028     Bayu   29            Siak       Aktif             4700000
## 29        C029    Cinta   25       Pekanbaru Tidak Aktif             5300000
##    pendapatan_missing outlier_pendapatan usia_minmax pendapatan_minmax
## 1                   0              FALSE        0.42        0.00999001
## 2                   1              FALSE        0.50        0.01058941
## 3                   0              FALSE        0.46        0.01138861
## 4                   0              FALSE        1.00        0.01058941
## 5                   0              FALSE        0.54        0.01078921
## 6                   0              FALSE        0.56        0.01118881
## 7                   0               TRUE        0.62        1.00000000
## 8                   0              FALSE        0.58        0.01038961
## 9                   0              FALSE        0.44        0.01018981
## 10                  0              FALSE        0.70        0.01158841
## 11                  1              FALSE        0.56        0.01058941
## 12                  0              FALSE        0.48        0.01008991
## 13                  0              FALSE        0.52        0.01028971
## 14                  0              FALSE        0.60        0.01048951
## 15                  1              FALSE        0.66        0.01058941
## 16                  0              FALSE        0.38        0.00979021
## 17                  0              FALSE        0.90        0.01178821
## 18                  0              FALSE        0.76        0.01088911
## 19                  0               TRUE        0.54        0.00000000
## 20                  0              FALSE        0.58        0.01068931
## 21                  0              FALSE        0.00        0.01038961
## 22                  0              FALSE        0.68        0.01078921
## 23                  1              FALSE        0.52        0.01058941
## 24                  0              FALSE        0.82        0.01148851
## 25                  0              FALSE        0.46        0.01028971
## 26                  0              FALSE        0.74        0.01108891
## 27                  0              FALSE        0.56        0.01058941
## 28                  0              FALSE        0.58        0.01038961
## 29                  0              FALSE        0.50        0.01158841
##    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                      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
## 20                      0              0.0e+00
## 21                      0              0.0e+00
## 22                      0              0.0e+00
## 23                      0              0.0e+00
## 24                      0              0.0e+00
## 25                      0              0.0e+00
## 26                      0              0.0e+00
## 27                      0              0.0e+00
## 28                      0              0.0e+00
## 29                      0              0.0e+00

10.2 Audit akhir

audit_akhir <- audit_data(data_final)
audit_akhir
##                   atribut      tipe jumlah_missing persen_missing jumlah_unik
## 1             customer_id character              0              0          29
## 2                    nama character              0              0          29
## 3                    usia   numeric              0              0          21
## 4                    kota character              0              0           4
## 5                  status character              0              0           2
## 6     pendapatan_imputasi   numeric              0              0          19
## 7      pendapatan_missing   integer              0              0           2
## 8      outlier_pendapatan   logical              0              0           2
## 9             usia_minmax   numeric              0              0          21
## 10      pendapatan_minmax   numeric              0              0          19
## 11 jumlah_transaksi_final   numeric              0              0          12
## 12   total_purchase_final   numeric              0              0          19
# Memeriksa kembali duplikasi identifier
sum(duplicated(data_final$customer_id))
## [1] 0
# Memeriksa kembali missing value
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

10.3 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      30      29
## 2 Duplikasi customer_id       1       0
## 3   Total missing value       7       0
## 4    Kategori kota unik      11       4
## 5  Kategori status unik       9       2

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

11 Kesimpulan

Praktikum ini memperlihatkan alur praproses data yang sistematis: mengevaluasi struktur dasar data, merapikan variabel kategorik, mengeliminasi duplikasi, memperbaiki data anomali, menangani nilai kosong, serta memetakan outlier. Alur ini ditutup dengan normalisasi numerik, integrasi antar-dataset, dan pemeriksaan kualitas secara menyeluruh.

Hal terpenting yang dapat disimpulkan adalah data preprocessing menuntut alasan logis pada setiap perlakuan data. Menghapus outlier atau membuang missing value secara spontan bukanlah langkah yang bijak, sebagaimana proses integrasi data juga memerlukan konfirmasi ulang agar integritas data tetap terjaga tanpa menimbulkan anomali baru.

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

13 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