Preprocessing Data with RStudio

Preprocessing Data

Data yang diperoleh tidak selalu berada dalam kondisi yang langsung dapat digunakan untuk analisis. Pada praktik ini, data pelanggan sengaja dibuat memiliki beberapa permasalahan sehingga setiap tahap preprocessing dapat terlihat secara langsung.

Alurnya dibuat sederhana: lihat kondisi data → temukan masalah → lakukan perbaikan → periksa kembali hasilnya.

Langkah 1 - Membangun Dataset

Kita mulai dengan memasukkan data secara langsung menggunakan data.frame(). Dengan cara ini, kondisi data awal dapat dilihat tanpa perlu membaca file eksternal.

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

Ada beberapa kondisi yang sengaja terlihat pada data awal, misalnya nilai NA, penulisan kota dan status yang belum seragam, customer_id yang berulang, usia 150 tahun, serta pendapatan 500 juta yang jauh lebih tinggi daripada observasi lainnya.

Data transaksi juga memiliki cust_id yang tidak seluruhnya memiliki pasangan pada data pelanggan.

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

Langkah 2 - Melihat Kondisi Data

Sebelum melakukan perubahan, kita lihat terlebih dahulu ukuran, nama variabel, struktur, 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

Ringkasan numerik dan jumlah nilai yang hilang juga perlu diperiksa.

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  
##                                       
##                                       
##                                       
## 
colSums(is.na(pelanggan_raw))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           1           2           1           0
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

Dari pemeriksaan awal, terlihat bahwa usia, pendapatan, dan kota memiliki nilai yang hilang.

Kita juga dapat memeriksa apakah terdapat baris yang sama atau customer_id yang berulang.

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

Kategori yang tercatat pada variabel kota dan status juga diperiksa.

sort(unique(pelanggan_raw$kota))
## [1] " PKU"       "Dumai"      "DUMAI"      "pekanbaru"  "Pekanbaru" 
## [6] "PEKANBARU"  "Pekanbaru " "PKU"        "Siak"
sort(unique(pelanggan_raw$status))
## [1] "A"           "ACTIVE"      "aktif"       "Aktif"       "AKTIF"      
## [6] "nonaktif"    "Tidak aktif" "Tidak Aktif"

Untuk variabel numerik, kita lihat rentangnya.

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

Hasil pemeriksaan ini belum menjadi keputusan untuk mengubah data. Pemeriksaan awal hanya membantu menunjukkan bagian mana yang perlu diperhatikan lebih lanjut.

Ringkasan Kondisi Awal

Agar pemeriksaan lebih ringkas, kita dapat membuat fungsi audit sederhana.

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

Hasil audit digunakan sebagai gambaran awal, bukan sebagai keputusan otomatis untuk melakukan cleaning.

Langkah 3 - Membersihkan Data

Agar data mentah tetap dapat dibandingkan dengan hasil preprocessing, kita bekerja pada salinan data.

pelanggan <- pelanggan_raw

Membersihkan Spasi dan Kapitalisasi

Kita mulai dari masalah yang paling sederhana: spasi dan perbedaan penggunaan huruf.

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"

Setelah huruf diseragamkan, kategori yang sebenarnya sama masih dapat memiliki nama berbeda. Karena itu, kita lanjutkan dengan standardisasi berdasarkan aturan domain.

Menyeragamkan Kategori

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

Setelah standardisasi, kategori kota menjadi Dumai, Pekanbaru, dan Siak, sedangkan status menjadi Aktif dan Tidak Aktif.

Mendeteksi dan Menghapus Duplikasi

Kita tampilkan terlebih dahulu baris yang memiliki customer_id 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

Terlihat bahwa C010 tercatat dua kali dengan informasi yang sama. Karena dalam dataset ini satu customer_id mewakili satu pelanggan, kita mempertahankan kemunculan pertama.

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

rownames(pelanggan) <- NULL

dim(pelanggan)
## [1] 11  6

Kita pastikan kembali bahwa tidak ada customer_id yang berulang.

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

Penghapusan duplikasi bergantung pada aturan data. Jika satu pelanggan memang boleh memiliki beberapa baris, maka penghapusan berdasarkan customer_id tidak tepat.

Memeriksa Aturan Domain

Selanjutnya kita memeriksa nilai yang secara logis tidak sesuai.

Untuk usia, kita gunakan rentang 15–100 tahun sebagai aturan pemeriksaan.

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>

Nilai usia 150 pada C004 melanggar aturan tersebut. Misalkan setelah memeriksa sumber asli, diketahui bahwa nilai yang benar adalah 50.

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

Untuk pendapatan, kita dapat memeriksa apakah terdapat nilai 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>

Tidak ditemukan pendapatan negatif pada data ini.

Mencatat Perubahan

Supaya perubahan yang dilakukan tidak hanya tersimpan di dalam syntax, kita buat catatan singkat.

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

Langkah 4 - Menangani Missing Values

Setelah masalah penulisan, duplikasi, dan nilai domain diperiksa, kita kembali pada nilai yang hilang.

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

Kita lihat baris yang memiliki sedikitnya 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

Terdapat beberapa pilihan penanganan. Kita lihat terlebih dahulu apa yang terjadi jika seluruh baris yang tidak lengkap dihapus.

Strategi 1 — Menghapus Baris

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

nrow(pelanggan)
## [1] 11
nrow(pelanggan_complete)
## [1] 8

Persentase observasi yang hilang jika strategi ini digunakan:

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

Pada data ini, penghapusan seluruh baris tidak lengkap menghilangkan sebagian observasi. Karena itu, kita bandingkan dengan pendekatan imputasi.

Strategi 2 — Imputasi Mean dan Median

Pendapatan memiliki satu nilai yang sangat ekstrem, yaitu 500 juta. Karena itu, mean dan median dibandingkan terlebih dahulu.

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

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

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

Mean jauh lebih besar daripada median karena dipengaruhi oleh nilai 500 juta. Untuk contoh ini, median lebih stabil sehingga digunakan sebagai nilai imputasi.

pelanggan$pendapatan_imputasi <- pelanggan$pendapatan

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

Untuk usia, kita juga menggunakan median.

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

pelanggan$usia_imputasi <- pelanggan$usia

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

Kita bandingkan nilai sebelum dan sesudah imputasi.

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

Strategi 3 — Imputasi Nilai Kategorik

Untuk kota, kita tidak langsung mengganti nilai yang hilang dengan modus. Kita gunakan kategori eksplisit Tidak diketahui agar informasi bahwa nilai awalnya tidak tersedia tetap terlihat.

pelanggan$kota_imputasi <- pelanggan$kota

pelanggan$kota_imputasi[
  is.na(pelanggan$kota_imputasi)
] <- "Tidak diketahui"

table(
  pelanggan$kota_imputasi,
  useNA = "ifany"
)
## 
##           Dumai       Pekanbaru            Siak Tidak diketahui 
##               3               6               1               1

Menambahkan Indikator Missing

Selain mengisi nilai yang hilang, kita dapat menyimpan informasi bahwa suatu nilai awalnya memang missing.

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

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

Nilai 1 menunjukkan bahwa pendapatan awalnya hilang, sedangkan 0 menunjukkan bahwa pendapatan tersedia sejak awal.

Membandingkan Sebelum dan Sesudah Imputasi

Kita dapat melihat perubahan distribusi pendapatan secara visual.

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

Perbandingan ini membantu melihat apakah bentuk distribusi berubah secara berlebihan setelah imputasi.

Langkah 5 - Menangani Outlier

Setelah missing value ditangani, kita melihat nilai yang sangat jauh dari sebagian besar observasi.

Visualisasi dengan Boxplot

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

Terlihat adanya satu nilai yang jauh lebih tinggi daripada sebagian besar pendapatan lainnya.

Menghitung Batas IQR

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

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

IQR_pendapatan <- Q3 - Q1

batas_bawah <- Q1 - 1.5 * IQR_pendapatan
batas_atas <- Q3 + 1.5 * IQR_pendapatan

c(
  batas_bawah = batas_bawah,
  batas_atas = batas_atas
)
## batas_bawah.25%  batas_atas.75% 
##         4150000         5750000

Menandai Kandidat Outlier

pelanggan$outlier_pendapatan <-
  pelanggan$pendapatan_imputasi < batas_bawah |
  pelanggan$pendapatan_imputasi > batas_atas

pelanggan[
  pelanggan$outlier_pendapatan,
  c(
    "customer_id",
    "nama",
    "pendapatan_imputasi"
  )
]
##   customer_id   nama pendapatan_imputasi
## 7        C007 Gilang               5e+08

Pada data ini, C007 menjadi kandidat outlier karena memiliki pendapatan 500 juta.

Mengevaluasi Tindakan

Nilai ekstrem tidak otomatis berarti salah. Ada beberapa kemungkinan: nilai tersebut bisa merupakan kesalahan input, observasi valid tetapi ekstrem, atau berasal dari populasi yang berbeda.

Pada contoh ini, nilai 500 juta dipertahankan. Untuk melihat dampak penanganan outlier, kita buat 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

Nilai ekstrem tidak dihapus dari data asli; versi winsorized hanya digunakan untuk melihat bagaimana perubahan tersebut memengaruhi transformasi berikutnya.

Langkah 6 - Transformasi Data

Setelah data diperiksa, kita dapat mengubah skala variabel numerik jika diperlukan.

Normalisasi Min–Maks

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

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

minmax <- function(x) {
  if (all(is.na(x))) {
    return(rep(NA_real_, length(x)))
  }

  rentang <- max(x, na.rm = TRUE) - min(x, na.rm = TRUE)

  if (rentang == 0) {
    return(rep(0, length(x)))
  }

  (x - min(x, na.rm = TRUE)) / rentang
}

pelanggan$usia_minmax <- minmax(
  pelanggan$usia_imputasi
)

pelanggan$pendapatan_minmax <- minmax(
  pelanggan$pendapatan_imputasi
)

pelanggan[
  ,
  c(
    "customer_id",
    "usia",
    "usia_minmax",
    "pendapatan_imputasi",
    "pendapatan_minmax"
  )
]
##    customer_id usia usia_minmax pendapatan_imputasi pendapatan_minmax
## 1         C001   21  0.00000000             4.5e+06      0.0000000000
## 2         C002   25  0.13793103             4.9e+06      0.0008072654
## 3         C003   23  0.06896552             5.2e+06      0.0014127144
## 4         C004   50  1.00000000             4.8e+06      0.0006054490
## 5         C005   27  0.20689655             4.9e+06      0.0008072654
## 6         C006   NA  0.22413793             5.1e+06      0.0012108981
## 7         C007   31  0.34482759             5.0e+08      1.0000000000
## 8         C008   29  0.27586207             4.7e+06      0.0004036327
## 9         C009   22  0.03448276             4.6e+06      0.0002018163
## 10        C010   35  0.48275862             5.3e+06      0.0016145308
## 11        C011   28  0.24137931             4.9e+06      0.0008072654

Terlihat bahwa nilai pendapatan 500 juta menjadi 1, sementara sebagian besar pendapatan lainnya berada sangat dekat dengan 0. Kondisi ini menunjukkan bahwa outlier dapat memengaruhi hasil normalisasi.

Normalisasi Z-Score

Z-score menggunakan rata-rata dan simpangan baku.

\[ z = \frac{x-\bar{x}}{s} \]

pelanggan$usia_z <- as.numeric(
  scale(pelanggan$usia_imputasi)
)

pelanggan$pendapatan_z <- as.numeric(
  scale(pelanggan$pendapatan_imputasi)
)

round(
  pelanggan[
    ,
    c("usia_z", "pendapatan_z")
  ],
  3
)
##    usia_z pendapatan_z
## 1  -0.984       -0.304
## 2  -0.489       -0.301
## 3  -0.737       -0.299
## 4   2.604       -0.302
## 5  -0.242       -0.301
## 6  -0.180       -0.300
## 7   0.253        3.015
## 8   0.006       -0.303
## 9  -0.860       -0.303
## 10  0.748       -0.299
## 11 -0.118       -0.301

Decimal Scaling

decimal_scale <- function(x) {
  maks <- max(abs(x), na.rm = TRUE)

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

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

  x / (10 ^ j)
}

pelanggan$pendapatan_decimal <-
  decimal_scale(
    pelanggan$pendapatan_imputasi
  )

range(
  pelanggan$pendapatan_decimal,
  na.rm = TRUE
)
## [1] 0.0045 0.5000

Membandingkan Metode Transformasi

Agar perbedaannya lebih mudah dilihat, kita letakkan beberapa hasil transformasi dalam satu tabel.

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

transformasi
##    customer_id pendapatan_imputasi pendapatan_minmax pendapatan_z
## 1         C001             4.5e+06      0.0000000000   -0.3041235
## 2         C002             4.9e+06      0.0008072654   -0.3014440
## 3         C003             5.2e+06      0.0014127144   -0.2994343
## 4         C004             4.8e+06      0.0006054490   -0.3021138
## 5         C005             4.9e+06      0.0008072654   -0.3014440
## 6         C006             5.1e+06      0.0012108981   -0.3001042
## 7         C007             5.0e+08      1.0000000000    3.0151095
## 8         C008             4.7e+06      0.0004036327   -0.3027837
## 9         C009             4.6e+06      0.0002018163   -0.3034536
## 10        C010             5.3e+06      0.0016145308   -0.2987645
## 11        C011             4.9e+06      0.0008072654   -0.3014440
##    pendapatan_decimal
## 1              0.0045
## 2              0.0049
## 3              0.0052
## 4              0.0048
## 5              0.0049
## 6              0.0051
## 7              0.5000
## 8              0.0047
## 9              0.0046
## 10             0.0053
## 11             0.0049

Tidak ada satu metode transformasi yang selalu paling baik. Pemilihan metode bergantung pada karakteristik data dan tujuan analisis.

Dampak Outlier terhadap Normalisasi

Untuk melihat dampaknya secara langsung, kita bandingkan 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,
  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
)

Outlier dapat membuat sebagian besar nilai min–maks berada pada rentang yang sangat sempit. Karena itu, pemeriksaan outlier perlu dilakukan sebelum menentukan transformasi yang akan digunakan.

Langkah 7 - Integrasi Data

Data yang digunakan dalam analisis tidak selalu berasal dari satu tabel. Pada contoh ini, informasi pelanggan dan transaksi berada pada dua sumber yang berbeda.

Memeriksa Kunci pada Kedua Sumber

Pertama, kita pastikan tidak terdapat identifier yang berulang.

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

Selanjutnya, kita lihat identifier yang hanya muncul pada salah satu sumber.

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

Hasilnya menunjukkan bahwa C011 terdapat pada data pelanggan tetapi tidak memiliki transaksi, sedangkan C012 terdapat pada data transaksi tetapi tidak memiliki pasangan pada data pelanggan.

Menyelaraskan Nama Identifier

Nama identifier pada kedua sumber belum sama: customer_id pada data pelanggan dan cust_id pada data transaksi.

Kita samakan terlebih dahulu.

transaksi <- transaksi_raw

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

Menggabungkan Data

Kita gunakan merge() dengan all.x = TRUE sehingga seluruh pelanggan tetap dipertahankan.

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

C011 tetap muncul meskipun tidak memiliki transaksi. Nilai transaksi untuk pelanggan tersebut menjadi NA.

Memvalidasi Hasil Integrasi

Kita periksa apakah jumlah baris berubah secara tidak wajar.

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

Kemudian kita pastikan customer_id tetap unik.

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

Kita juga periksa missing value yang muncul pada atribut transaksi.

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

Pelanggan yang tidak memiliki pasangan transaksi dapat ditampilkan dengan:

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

NA pada transaksi tidak otomatis boleh diganti menjadi 0. Nilai tersebut bisa berarti pelanggan memang belum pernah bertransaksi, tetapi bisa juga berarti data transaksi tidak tersedia. Penggantian menjadi 0 hanya dilakukan jika definisinya sudah dipastikan.

Sebagai contoh, jika sudah dikonfirmasi bahwa NA memang berarti belum pernah bertransaksi:

data_terintegrasi$jumlah_transaksi_final <-
  data_terintegrasi$jumlah_transaksi

data_terintegrasi$total_purchase_final <-
  data_terintegrasi$total_purchase

data_terintegrasi$jumlah_transaksi_final[
  is.na(data_terintegrasi$jumlah_transaksi_final)
] <- 0

data_terintegrasi$total_purchase_final[
  is.na(data_terintegrasi$total_purchase_final)
] <- 0

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

Langkah 8 - Pemeriksaan Akhir

Setelah seluruh proses dilakukan, kita kembali memeriksa dataset yang dihasilkan.

audit_akhir <- audit_data(
  data_terintegrasi
)

audit_akhir
##                     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           9.09          11
## 4                pendapatan   numeric              2          18.18          10
## 5                      kota character              1           9.09           4
## 6                    status character              0           0.00           2
## 7       pendapatan_imputasi   numeric              0           0.00           9
## 8             usia_imputasi   numeric              0           0.00          11
## 9             kota_imputasi character              0           0.00           4
## 10       pendapatan_missing   integer              0           0.00           2
## 11       outlier_pendapatan   logical              0           0.00           2
## 12        pendapatan_winsor   numeric              0           0.00           9
## 13              usia_minmax   numeric              0           0.00          11
## 14        pendapatan_minmax   numeric              0           0.00           9
## 15                   usia_z   numeric              0           0.00          11
## 16             pendapatan_z   numeric              0           0.00           9
## 17       pendapatan_decimal   numeric              0           0.00           9
## 18 pendapatan_winsor_minmax   numeric              0           0.00           9
## 19         jumlah_transaksi   numeric              1           9.09           9
## 20           total_purchase   numeric              1           9.09          11
## 21   jumlah_transaksi_final   numeric              0           0.00           9
## 22     total_purchase_final   numeric              0           0.00          11

Kita juga melihat kondisi duplikasi dan missing value.

sum(
  duplicated(
    data_terintegrasi$customer_id
  )
)
## [1] 0
colSums(is.na(data_terintegrasi))
##              customer_id                     nama                     usia 
##                        0                        0                        1 
##               pendapatan                     kota                   status 
##                        2                        1                        0 
##      pendapatan_imputasi            usia_imputasi            kota_imputasi 
##                        0                        0                        0 
##       pendapatan_missing       outlier_pendapatan        pendapatan_winsor 
##                        0                        0                        0 
##              usia_minmax        pendapatan_minmax                   usia_z 
##                        0                        0                        0 
##             pendapatan_z       pendapatan_decimal pendapatan_winsor_minmax 
##                        0                        0                        0 
##         jumlah_transaksi           total_purchase   jumlah_transaksi_final 
##                        1                        1                        0 
##     total_purchase_final 
##                        0

Ringkasan Sebelum dan Sesudah

Agar perubahan lebih mudah dilihat, kita buat ringkasan sederhana.

data.frame(
  pemeriksaan = c(
    "Jumlah baris pelanggan",
    "Duplicate customer_id",
    "Missing usia",
    "Missing pendapatan",
    "Missing kota"
  ),
  sebelum = c(
    nrow(pelanggan_raw),
    sum(duplicated(pelanggan_raw$customer_id)),
    sum(is.na(pelanggan_raw$usia)),
    sum(is.na(pelanggan_raw$pendapatan)),
    sum(is.na(pelanggan_raw$kota))
  ),
  sesudah = c(
    nrow(pelanggan),
    sum(duplicated(pelanggan$customer_id)),
    sum(is.na(pelanggan$usia_imputasi)),
    sum(is.na(pelanggan$pendapatan_imputasi)),
    sum(is.na(pelanggan$kota_imputasi))
  )
)
##              pemeriksaan sebelum sesudah
## 1 Jumlah baris pelanggan      12      11
## 2  Duplicate customer_id       1       0
## 3           Missing usia       1       0
## 4     Missing pendapatan       2       0
## 5           Missing kota       1       0

Perbandingan tersebut menunjukkan bahwa kondisi data berubah setelah melalui proses preprocessing. Perubahan tidak hanya dilihat dari jumlah NA, tetapi juga dari konsistensi kategori, duplikasi, nilai domain, dan hasil transformasi.

Data Setelah Preprocessing

Berikut adalah data pelanggan setelah melalui tahapan pemeriksaan dan pembersihan. Beberapa kolom tambahan dipertahankan agar proses yang dilakukan tetap dapat dilacak.

pelanggan
##    customer_id   nama usia pendapatan      kota      status pendapatan_imputasi
## 1         C001    Ani   21    4.5e+06 Pekanbaru       Aktif             4.5e+06
## 2         C002   Budi   25         NA Pekanbaru       Aktif             4.9e+06
## 3         C003  Citra   23    5.2e+06 Pekanbaru       Aktif             5.2e+06
## 4         C004   Dodi   50    4.8e+06     Dumai       Aktif             4.8e+06
## 5         C005    Eka   27    4.9e+06 Pekanbaru Tidak Aktif             4.9e+06
## 6         C006   Fani   NA    5.1e+06     Dumai Tidak Aktif             5.1e+06
## 7         C007 Gilang   31    5.0e+08 Pekanbaru       Aktif             5.0e+08
## 8         C008   Hana   29    4.7e+06      Siak       Aktif             4.7e+06
## 9         C009  Indra   22    4.6e+06 Pekanbaru       Aktif             4.6e+06
## 10        C010   Joko   35    5.3e+06     Dumai Tidak Aktif             5.3e+06
## 11        C011   Kiki   28         NA      <NA>       Aktif             4.9e+06
##    usia_imputasi   kota_imputasi pendapatan_missing outlier_pendapatan
## 1           21.0       Pekanbaru                  0              FALSE
## 2           25.0       Pekanbaru                  1              FALSE
## 3           23.0       Pekanbaru                  0              FALSE
## 4           50.0           Dumai                  0              FALSE
## 5           27.0       Pekanbaru                  0              FALSE
## 6           27.5           Dumai                  0              FALSE
## 7           31.0       Pekanbaru                  0               TRUE
## 8           29.0            Siak                  0              FALSE
## 9           22.0       Pekanbaru                  0              FALSE
## 10          35.0           Dumai                  0              FALSE
## 11          28.0 Tidak diketahui                  1              FALSE
##    pendapatan_winsor usia_minmax pendapatan_minmax       usia_z pendapatan_z
## 1            4500000  0.00000000      0.0000000000 -0.984199667   -0.3041235
## 2            4900000  0.13793103      0.0008072654 -0.489287834   -0.3014440
## 3            5200000  0.06896552      0.0014127144 -0.736743750   -0.2994343
## 4            4800000  1.00000000      0.0006054490  2.603911118   -0.3021138
## 5            4900000  0.20689655      0.0008072654 -0.241831918   -0.3014440
## 6            5100000  0.22413793      0.0012108981 -0.179967939   -0.3001042
## 7            5750000  0.34482759      1.0000000000  0.253079914    3.0151095
## 8            4700000  0.27586207      0.0004036327  0.005623998   -0.3027837
## 9            4600000  0.03448276      0.0002018163 -0.860471709   -0.3034536
## 10           5300000  0.48275862      0.0016145308  0.747991747   -0.2987645
## 11           4900000  0.24137931      0.0008072654 -0.118103960   -0.3014440
##    pendapatan_decimal pendapatan_winsor_minmax
## 1              0.0045                     0.00
## 2              0.0049                     0.32
## 3              0.0052                     0.56
## 4              0.0048                     0.24
## 5              0.0049                     0.32
## 6              0.0051                     0.48
## 7              0.5000                     1.00
## 8              0.0047                     0.16
## 9              0.0046                     0.08
## 10             0.0053                     0.64
## 11             0.0049                     0.32

Untuk kebutuhan analisis berikutnya, data terintegrasi dapat dilihat sebagai berikut.

data_terintegrasi
##    customer_id   nama usia pendapatan      kota      status pendapatan_imputasi
## 1         C001    Ani   21    4.5e+06 Pekanbaru       Aktif             4.5e+06
## 2         C002   Budi   25         NA Pekanbaru       Aktif             4.9e+06
## 3         C003  Citra   23    5.2e+06 Pekanbaru       Aktif             5.2e+06
## 4         C004   Dodi   50    4.8e+06     Dumai       Aktif             4.8e+06
## 5         C005    Eka   27    4.9e+06 Pekanbaru Tidak Aktif             4.9e+06
## 6         C006   Fani   NA    5.1e+06     Dumai Tidak Aktif             5.1e+06
## 7         C007 Gilang   31    5.0e+08 Pekanbaru       Aktif             5.0e+08
## 8         C008   Hana   29    4.7e+06      Siak       Aktif             4.7e+06
## 9         C009  Indra   22    4.6e+06 Pekanbaru       Aktif             4.6e+06
## 10        C010   Joko   35    5.3e+06     Dumai Tidak Aktif             5.3e+06
## 11        C011   Kiki   28         NA      <NA>       Aktif             4.9e+06
##    usia_imputasi   kota_imputasi pendapatan_missing outlier_pendapatan
## 1           21.0       Pekanbaru                  0              FALSE
## 2           25.0       Pekanbaru                  1              FALSE
## 3           23.0       Pekanbaru                  0              FALSE
## 4           50.0           Dumai                  0              FALSE
## 5           27.0       Pekanbaru                  0              FALSE
## 6           27.5           Dumai                  0              FALSE
## 7           31.0       Pekanbaru                  0               TRUE
## 8           29.0            Siak                  0              FALSE
## 9           22.0       Pekanbaru                  0              FALSE
## 10          35.0           Dumai                  0              FALSE
## 11          28.0 Tidak diketahui                  1              FALSE
##    pendapatan_winsor usia_minmax pendapatan_minmax       usia_z pendapatan_z
## 1            4500000  0.00000000      0.0000000000 -0.984199667   -0.3041235
## 2            4900000  0.13793103      0.0008072654 -0.489287834   -0.3014440
## 3            5200000  0.06896552      0.0014127144 -0.736743750   -0.2994343
## 4            4800000  1.00000000      0.0006054490  2.603911118   -0.3021138
## 5            4900000  0.20689655      0.0008072654 -0.241831918   -0.3014440
## 6            5100000  0.22413793      0.0012108981 -0.179967939   -0.3001042
## 7            5750000  0.34482759      1.0000000000  0.253079914    3.0151095
## 8            4700000  0.27586207      0.0004036327  0.005623998   -0.3027837
## 9            4600000  0.03448276      0.0002018163 -0.860471709   -0.3034536
## 10           5300000  0.48275862      0.0016145308  0.747991747   -0.2987645
## 11           4900000  0.24137931      0.0008072654 -0.118103960   -0.3014440
##    pendapatan_decimal pendapatan_winsor_minmax jumlah_transaksi total_purchase
## 1              0.0045                     0.00                5        1.5e+06
## 2              0.0049                     0.32                3        9.0e+05
## 3              0.0052                     0.56                7        2.7e+06
## 4              0.0048                     0.24                2        6.0e+05
## 5              0.0049                     0.32                6        2.1e+06
## 6              0.0051                     0.48                4        1.3e+06
## 7              0.5000                     1.00               20        2.5e+07
## 8              0.0047                     0.16                5        1.7e+06
## 9              0.0046                     0.08                3        8.0e+05
## 10             0.0053                     0.64                8        3.2e+06
## 11             0.0049                     0.32               NA             NA
##    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

Data yang semula memiliki beberapa masalah kini telah melalui proses pemeriksaan, pembersihan, penanganan missing value, pemeriksaan outlier, transformasi, dan integrasi.

Dengan demikian, dataset menjadi lebih siap untuk digunakan pada tahap analisis selanjutnya.

Penutup

Proses preprocessing telah dilakukan secara bertahap mulai dari pemeriksaan kondisi awal data, penanganan duplikasi dan ketidakkonsistenan kategori, pemeriksaan nilai yang tidak wajar, penanganan missing value, hingga deteksi outlier dan transformasi data. Selain itu, data pelanggan dan data transaksi juga diintegrasikan menggunakan customer_id.

Setelah melalui tahapan tersebut, data menjadi lebih terstruktur dan siap digunakan untuk tahap analisis selanjutnya. Proses ini juga menunjukkan bahwa preprocessing bukan sekadar menghapus data yang dianggap bermasalah, tetapi perlu dilakukan melalui pemeriksaan dan pertimbangan terhadap karakteristik masing-masing variabel.