1 Gambaran Singkat

Data yang terlihat rapi belum tentu siap dipakai untuk analisis. Pada contoh ini saya memakai data pelanggan dan data transaksi yang sengaja memiliki beberapa masalah kecil: ada nilai kosong, penulisan kategori yang berbeda-beda, data ganda, nilai yang terlalu ekstrem, serta nama ID yang belum sama.

Tujuannya bukan sekadar membuat data terlihat bersih. Setiap perubahan dibuat supaya alasan di baliknya jelas dan hasil akhirnya tetap mudah diperiksa.

Data ini mengikuti alur pada materi Data Preprocessing Menggunakan RStudio: mulai dari melihat kondisi awal, membersihkan data, menangani nilai kosong, mengecek outlier, melakukan transformasi, menggabungkan data, sampai melakukan pemeriksaan terakhir. Materi aslinya juga menekankan bahwa perubahan pada data sebaiknya memiliki alasan dan catatan.

2 Menyiapkan Data

Contoh ini cukup dijalankan dengan R dasar, jadi tidak perlu memasang paket tambahan.

knitr::opts_chunk$set(echo = TRUE, message = FALSE, warning = FALSE)
options(stringsAsFactors = FALSE)

2.1 Data pelanggan

Saya mulai dari data pelanggan yang masih mentah. Beberapa nilai memang sengaja dibuat tidak konsisten supaya proses preprocessing bisa terlihat dengan jelas.

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

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

2.2 Data transaksi

Sumber kedua berisi ringkasan transaksi. Perhatikan bahwa kolom ID-nya masih bernama cust_id, sedangkan pada data pelanggan namanya customer_id.

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
)

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

3 Melihat Kondisi Awal

Sebelum mengubah apa pun, saya ingin tahu dulu ukuran data, nama kolom, tipe data, dan contoh isinya.

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

Dari data awal terlihat beberapa hal yang perlu diperhatikan:

  • ada usia 150 yang tidak masuk akal untuk konteks pelanggan pada contoh ini;
  • ada beberapa NA;
  • kota ditulis dalam beberapa bentuk seperti PKU, PEKANBARU, dan Pekanbaru;
  • status juga memiliki beberapa variasi;
  • C010 muncul dua kali;
  • pendapatan 500000000 jauh lebih besar dibanding nilai lainnya.

3.1 Ringkasan kualitas data

Supaya pengecekan tidak dilakukan satu per satu, saya membuat fungsi kecil untuk melihat jumlah nilai kosong dan jumlah nilai unik setiap kolom.

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

4 Merapikan Data Pelanggan

Saya membuat salinan kerja supaya data mentah tetap tersimpan.

pelanggan <- pelanggan_raw

4.1 Menyamakan penulisan kota dan status

Pertama, spasi yang tidak diperlukan dibuang. Setelah itu huruf dibuat kecil sementara agar variasi penulisan lebih mudah dicocokkan.

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"

Sekarang kategori bisa disatukan ke bentuk yang lebih 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"

Dengan cara ini, kategori yang sebenarnya sama tidak lagi dianggap sebagai kategori berbeda hanya karena penulisannya berbeda.

4.2 Menangani data ganda

customer_id seharusnya menjadi penanda unik untuk satu pelanggan. Karena C010 muncul dua kali dengan isi yang sama, satu baris cukup dipertahankan.

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
pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL

dim(pelanggan)
## [1] 11  6

4.3 Memperbaiki nilai domain

Usia 150 pada C004 dianggap sebagai salah input. Pada materi sumber, nilai tersebut dikoreksi menjadi 50 berdasarkan sumber asli.

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

Koreksi seperti ini sebaiknya dilakukan hanya ketika ada dasar yang cukup, bukan sekadar karena angka terlihat aneh.

4.4 Catatan perubahan

Supaya perubahan mudah dilacak, saya simpan ringkasannya.

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

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

5 Mengatasi Nilai Kosong

Sekarang kita lihat posisi NA setelah data dirapikan.

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

Ada beberapa pilihan ketika menemukan nilai kosong. Tidak semuanya harus dihapus.

5.1 Kalau semua baris langsung dihapus

Sebagai pembanding, berikut yang terjadi jika hanya baris lengkap yang dipertahankan.

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

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

Pada data ini, penghapusan langsung membuang sekitar 27,27% baris. Karena jumlah datanya kecil, saya memilih mempertahankan baris dan mengisi nilai numerik yang kosong.

5.2 Mengisi usia dan pendapatan

Mean pendapatan sangat dipengaruhi oleh nilai ekstrem, sedangkan median berada di sekitar pola mayoritas data. Karena itu, median lebih masuk akal untuk contoh ini.

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

mean_pendapatan
## [1] 59900000
median_pendapatan
## [1] 4900000
pelanggan$pendapatan_imputasi <- pelanggan$pendapatan
pelanggan$pendapatan_imputasi[
  is.na(pelanggan$pendapatan_imputasi)
] <- median_pendapatan

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

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

pelanggan[, c(
  "customer_id", "usia", "usia_imputasi",
  "pendapatan", "pendapatan_imputasi"
)]
##    customer_id usia usia_imputasi pendapatan pendapatan_imputasi
## 1         C001   21          21.0    4.5e+06             4.5e+06
## 2         C002   25          25.0         NA             4.9e+06
## 3         C003   23          23.0    5.2e+06             5.2e+06
## 4         C004   50          50.0    4.8e+06             4.8e+06
## 5         C005   27          27.0    4.9e+06             4.9e+06
## 6         C006   NA          27.5    5.1e+06             5.1e+06
## 7         C007   31          31.0    5.0e+08             5.0e+08
## 8         C008   29          29.0    4.7e+06             4.7e+06
## 9         C009   22          22.0    4.6e+06             4.6e+06
## 10        C010   35          35.0    5.3e+06             5.3e+06
## 11        C011   28          28.0         NA             4.9e+06

5.3 Nilai kota yang kosong

Untuk kota, saya tidak ingin menebak. Nilai kosong diberi label Tidak diketahui supaya ketidakpastian 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

5.4 Menyimpan informasi tentang missing value

Kolom indikator bisa membantu mengetahui mana data pendapatan yang awalnya kosong.

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

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

5.5 Melihat perubahan distribusi

Saya juga bisa membandingkan bentuk distribusi pendapatan sebelum dan setelah imputasi.

par(mfrow = c(1, 2))

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

hist(
  pelanggan$pendapatan_imputasi,
  main = "Setelah imputasi median",
  xlab = "Pendapatan",
  breaks = 8
)

par(mfrow = c(1, 1))

6 Melihat dan Menilai Outlier

Nilai ekstrem tidak otomatis berarti salah. Karena itu saya tandai dulu, baru memutuskan apa yang harus dilakukan.

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

6.1 Batas IQR

Saya menggunakan aturan IQR untuk menentukan 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         5150000          400000         4150000         5750000

Batas yang diperoleh adalah sekitar 4,15 juta sampai 5,75 juta. Dengan batas tersebut, pendapatan C007 menjadi 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

6.2 Tidak langsung menghapus outlier

Pendapatan 500 juta memang sangat jauh dari nilai pelanggan lainnya, tetapi belum tentu salah. Bisa saja itu pelanggan dengan karakteristik berbeda.

Untuk melihat pengaruhnya, saya membuat versi winsorized tanpa mengubah nilai asli.

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

7 Mengubah Skala Data

Nilai usia dan pendapatan memiliki skala yang berbeda. Beberapa metode analisis akan lebih nyaman jika angka-angka tersebut ditransformasikan.

7.1 Min-max

Rumus sederhananya 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.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

Karena C007 sangat besar, nilai tersebut menjadi 1 dan sebagian besar pendapatan lainnya berada sangat dekat dengan 0.

7.2 Z-score

Z-score menunjukkan posisi suatu nilai terhadap rata-rata dalam satuan 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.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

7.3 Decimal scaling

Cara lain adalah mengecilkan angka dengan pangkat 10 yang sesuai.

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

7.4 Melihat ketiga hasil 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.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

Perbandingan ini memperlihatkan bahwa satu data dapat direpresentasikan dengan skala yang berbeda. Pemilihan metode sebaiknya mengikuti kebutuhan analisis, bukan sekadar memilih metode yang paling mudah.

8 Menggabungkan Data Pelanggan dan Transaksi

Setelah data pelanggan cukup rapi, data transaksi bisa disatukan.

8.1 Mengecek kecocokan ID

Pertama saya cek apakah ada ID yang hanya muncul di salah satu sumber.

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

Hasilnya menunjukkan C011 tidak memiliki pasangan transaksi, sedangkan C012 ada di data transaksi tetapi tidak ada di data pelanggan.

8.2 Menyamakan nama ID

transaksi <- transaksi_raw

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

8.3 Melakukan left join

Saya mempertahankan seluruh pelanggan, sehingga informasi pelanggan yang tidak punya transaksi tetap muncul.

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

8.4 Memeriksa hasil penggabungan

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

Hasilnya tetap memiliki 11 pelanggan. C011 menjadi NA pada bagian transaksi karena memang tidak mempunyai pasangan pada sumber transaksi.

8.5 Apakah NA transaksi boleh menjadi nol?

Kalau sudah dipastikan bahwa NA memang berarti pelanggan belum pernah bertransaksi, nilai tersebut bisa diubah menjadi 0. Kalau belum ada kepastian, lebih aman mempertahankan NA.

Pada contoh ini saya anggap NA berarti belum ada transaksi.

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

9 Membentuk Dataset Akhir

Saya pilih kolom yang memang masih berguna untuk analisis dan menyederhanakan nama kolom imputasi.

data_final <- data_terintegrasi[, c(
  "customer_id",
  "nama",
  "usia_imputasi",
  "kota_imputasi",
  "status",
  "pendapatan_imputasi",
  "pendapatan_missing",
  "outlier_pendapatan",
  "usia_minmax",
  "pendapatan_minmax",
  "jumlah_transaksi_final",
  "total_purchase_final"
)]

names(data_final)[
  names(data_final) == "kota_imputasi"
] <- "kota"

names(data_final)[
  names(data_final) == "usia_imputasi"
] <- "usia"

data_final
##    customer_id   nama usia            kota      status pendapatan_imputasi
## 1         C001    Ani 21.0       Pekanbaru       Aktif             4.5e+06
## 2         C002   Budi 25.0       Pekanbaru       Aktif             4.9e+06
## 3         C003  Citra 23.0       Pekanbaru       Aktif             5.2e+06
## 4         C004   Dodi 50.0           Dumai       Aktif             4.8e+06
## 5         C005    Eka 27.0       Pekanbaru Tidak Aktif             4.9e+06
## 6         C006   Fani 27.5           Dumai Tidak Aktif             5.1e+06
## 7         C007 Gilang 31.0       Pekanbaru       Aktif             5.0e+08
## 8         C008   Hana 29.0            Siak       Aktif             4.7e+06
## 9         C009  Indra 22.0       Pekanbaru       Aktif             4.6e+06
## 10        C010   Joko 35.0           Dumai Tidak Aktif             5.3e+06
## 11        C011   Kiki 28.0 Tidak diketahui       Aktif             4.9e+06
##    pendapatan_missing outlier_pendapatan usia_minmax pendapatan_minmax
## 1                   0              FALSE  0.00000000      0.0000000000
## 2                   1              FALSE  0.13793103      0.0008072654
## 3                   0              FALSE  0.06896552      0.0014127144
## 4                   0              FALSE  1.00000000      0.0006054490
## 5                   0              FALSE  0.20689655      0.0008072654
## 6                   0              FALSE  0.22413793      0.0012108981
## 7                   0               TRUE  0.34482759      1.0000000000
## 8                   0              FALSE  0.27586207      0.0004036327
## 9                   0              FALSE  0.03448276      0.0002018163
## 10                  0              FALSE  0.48275862      0.0016145308
## 11                  1              FALSE  0.24137931      0.0008072654
##    jumlah_transaksi_final total_purchase_final
## 1                       5              1.5e+06
## 2                       3              9.0e+05
## 3                       7              2.7e+06
## 4                       2              6.0e+05
## 5                       6              2.1e+06
## 6                       4              1.3e+06
## 7                      20              2.5e+07
## 8                       5              1.7e+06
## 9                       3              8.0e+05
## 10                      8              3.2e+06
## 11                      0              0.0e+00

10 Pemeriksaan Terakhir

Sebelum data digunakan lebih lanjut, saya lakukan audit sekali lagi.

audit_akhir <- audit_data(data_final)
audit_akhir
##                   atribut      tipe jumlah_missing persen_missing jumlah_unik
## 1             customer_id character              0              0          11
## 2                    nama character              0              0          11
## 3                    usia   numeric              0              0          11
## 4                    kota character              0              0           4
## 5                  status character              0              0           2
## 6     pendapatan_imputasi   numeric              0              0           9
## 7      pendapatan_missing   integer              0              0           2
## 8      outlier_pendapatan   logical              0              0           2
## 9             usia_minmax   numeric              0              0          11
## 10      pendapatan_minmax   numeric              0              0           9
## 11 jumlah_transaksi_final   numeric              0              0           9
## 12   total_purchase_final   numeric              0              0          11
sum(duplicated(data_final$customer_id))
## [1] 0
colSums(is.na(data_final))
##            customer_id                   nama                   usia 
##                      0                      0                      0 
##                   kota                 status    pendapatan_imputasi 
##                      0                      0                      0 
##     pendapatan_missing     outlier_pendapatan            usia_minmax 
##                      0                      0                      0 
##      pendapatan_minmax jumlah_transaksi_final   total_purchase_final 
##                      0                      0                      0

Hasil akhirnya tidak lagi memiliki nilai kosong pada kolom yang dipakai, dan customer_id tetap unik.

10.1 Sebelum dan sesudah

Supaya perubahan lebih mudah dibaca, berikut perbandingannya.

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

perbandingan
##               indikator sebelum sesudah
## 1          Jumlah baris      12      11
## 2 Duplikasi customer_id       1       0
## 3   Total missing value       4       0
## 4    Kategori kota unik      10       4
## 5  Kategori status unik       8       2

Secara umum, jumlah baris berkurang dari 12 menjadi 11 karena satu duplikasi dihapus. Nilai kosong berhasil ditangani, kategori kota dan status menjadi lebih konsisten, dan ID pelanggan tidak lagi berulang.

11 Menyimpan Hasil

Data akhir dan catatan perubahan bisa disimpan supaya prosesnya mudah digunakan kembali.

write.csv(
  data_final,
  "data_pelanggan_setelah_preprocessing.csv",
  row.names = FALSE
)

write.csv(
  log_perubahan,
  "log_perubahan_preprocessing.csv",
  row.names = FALSE
)

12 Penutup

Dari proses ini terlihat bahwa preprocessing bukan hanya soal menghapus NA atau membuang angka yang besar. Bagian yang lebih penting justru memahami kenapa suatu nilai dianggap bermasalah, memilih perlakuan yang masuk akal, lalu menyimpan jejak perubahan.

Pada contoh ini, data akhirnya sudah lebih konsisten dan siap dipakai sebagai dasar analisis. Namun, nilai ekstrem seperti pendapatan C007 tetap dipertahankan pada data utama karena belum ada bukti bahwa angka tersebut salah. Versi winsorized hanya dibuat untuk melihat dampak metode.

Catatan sumber: struktur latihan, dataset contoh, alur preprocessing, serta nilai hasil yang digunakan dalam dokumen ini mengikuti materi yang diberikan, tetapi penjelasan dan susunan narasinya ditulis ulang dengan bahasa yang lebih natural.