1 Memulai Preprocessing Data

Bayangkan sebuah perusahaan e-commerce sedang menyiapkan data pelanggannya untuk digunakan dalam analisis. Tim analitik memperoleh dua tabel utama: data pelanggan dan data transaksi. Data pelanggan berisi informasi seperti identitas pelanggan, usia, pendapatan, kota, dan status, sedangkan data transaksi mencatat jumlah transaksi dan total pembelian.

Data yang diterima belum sepenuhnya siap digunakan. Beberapa informasi masih kosong, terdapat perbedaan penulisan pada kategori kota dan status, ada customer_id yang tercatat lebih dari satu kali, dan ditemukan nilai yang terlihat tidak wajar. Kedua tabel juga menggunakan nama identifier yang berbeda sehingga perlu diperiksa sebelum digabungkan.

Karena itu, data perlu dipersiapkan secara bertahap sebelum digunakan untuk analisis lebih lanjut. Prosesnya dimulai dengan melihat kondisi awal data, kemudian membersihkan masalah yang ditemukan, menangani nilai yang hilang, memeriksa nilai ekstrem, melakukan transformasi, menggabungkan data transaksi, dan akhirnya mengevaluasi kondisi dataset yang telah diproses.

2 Menyiapkan Data yang Akan Diproses

Kode berikut digunakan untuk menyiapkan data awal yang akan diproses. Struktur data dipertahankan agar setiap tahap preprocessing dapat dijalankan secara berurutan dari kondisi awal yang sama.

Membuat data pelanggan mentah

Bagian ini menyiapkan tabel pelanggan yang akan menjadi data utama dalam proses preprocessing. Data sengaja dimulai dari kondisi yang belum bersih agar setiap masalah dapat diperiksa dan ditangani pada tahap berikutnya.

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
Melihat data transaksi

Tabel transaksi ditampilkan untuk melihat isi data sebelum masuk ke tahap pemeriksaan dan integrasi.

transaksi_raw

3 Melihat Kondisi Data Sebelum Dibersihkan

3.1 Memahami Struktur Data

Sebelum mengubah apa pun, saya terlebih dahulu melihat bentuk data yang akan diproses. Langkah ini penting karena saya perlu mengetahui jumlah observasi, nama atribut, tipe data, serta gambaran nilai yang terdapat di dalam setiap kolom.

Dengan cara ini, keputusan pada tahap berikutnya tidak dibuat hanya berdasarkan dugaan, tetapi berdasarkan kondisi data yang benar-benar terlihat.

Memeriksa dimensi data

Saya menggunakan dim() untuk mengetahui jumlah baris dan kolom pada dataset. Informasi ini memberi gambaran awal mengenai ukuran data yang sedang diproses.

dim(pelanggan_raw)
## [1] 12  6
Memeriksa nama variabel

Dengan names(), saya melihat seluruh nama kolom yang tersedia. Langkah ini membantu memastikan atribut yang akan diperiksa pada tahap berikutnya sudah dikenali dengan jelas.

names(pelanggan_raw)
## [1] "customer_id" "nama"        "usia"        "pendapatan"  "kota"       
## [6] "status"
Memeriksa struktur dan tipe data

Fungsi str() digunakan untuk melihat struktur dataset sekaligus tipe data setiap variabel. Dari sini dapat diketahui apakah sebuah atribut terbaca sebagai numerik, karakter, atau tipe lainnya.

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" ...
Melihat beberapa baris awal

Saya menampilkan beberapa observasi pertama untuk melihat isi data secara langsung. Pemeriksaan ini membantu menemukan pola atau penulisan nilai yang mungkin tidak terlihat hanya dari nama kolom.

head(pelanggan_raw)
Melihat ringkasan statistik

Ringkasan statistik digunakan untuk memperoleh gambaran cepat mengenai nilai numerik dan karakteristik umum setiap variabel.

summary(pelanggan_raw)
##     customer_id        nama         usia          pendapatan       
##  Length   :12   Length   :12   Min.   : 21.00   Min.   :  4500000  
##  N.unique :11   N.unique :11   1st Qu.: 24.00   1st Qu.:  4725000  
##  N.blank  : 0   N.blank  : 0   Median : 28.00   Median :  5000000  
##  Min.nchar: 4   Min.nchar: 3   Mean   : 38.73   Mean   : 54440000  
##  Max.nchar: 4   Max.nchar: 6   3rd Qu.: 33.00   3rd Qu.:  5275000  
##                                Max.   :150.00   Max.   :500000000  
##                                NAs    :1        NAs    :2          
##         kota          status  
##  Length   :12   Length   :12  
##  N.unique : 9   N.unique : 8  
##  N.blank  : 0   N.blank  : 0  
##  Min.nchar: 3   Min.nchar: 1  
##  Max.nchar:10   Max.nchar:11  
##  NAs      : 1                 
## 

3.2 Mencari Masalah pada Data

Setelah mengetahui struktur data, saya mulai mencari masalah yang nantinya perlu ditangani. Fokus pemeriksaan awal diarahkan pada hal-hal yang bisa terlihat langsung dari dataset, seperti nilai kosong, duplikasi, kategori yang ditulis dengan cara berbeda, dan nilai numerik yang berada di luar rentang yang wajar.

Tahap ini belum bertujuan untuk memperbaiki data. Saya hanya ingin mengetahui terlebih dahulu masalah apa saja yang benar-benar ada.

Menghitung jumlah missing value

Kode ini menghitung berapa banyak nilai yang masih kosong pada setiap atribut. Hasilnya menjadi dasar untuk menentukan bagian data mana yang perlu ditangani.

# Banyak missing value per atribut
colSums(is.na(pelanggan_raw))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           1           2           1           0
Menghitung persentase missing value

Selain jumlah absolut, saya menghitung persentase nilai yang hilang agar proporsinya dapat dibandingkan antarvariabel.

# Persentase missing value per atribut
round(colMeans(is.na(pelanggan_raw)) * 100, 2)
## customer_id        nama        usia  pendapatan        kota      status 
##        0.00        0.00        8.33       16.67        8.33        0.00
Memeriksa duplikasi baris

Pemeriksaan ini digunakan untuk mengetahui apakah terdapat baris yang tercatat lebih dari satu kali secara keseluruhan.

# Banyak baris duplikat penuh
sum(duplicated(pelanggan_raw))
## [1] 1
Memeriksa duplikasi customer_id

Karena customer_id digunakan sebagai identitas pelanggan, saya memeriksa apakah ada identifier yang muncul lebih dari sekali.

# Banyak customer_id yang berulang
sum(duplicated(pelanggan_raw$customer_id))
## [1] 1
Melihat kategori kota

Saya melihat seluruh kategori kota yang muncul untuk mengetahui apakah satu kota ditulis dalam beberapa bentuk berbeda.

# Kategori yang tercatat pada atribut kota dan status
sort(unique(pelanggan_raw$kota))
## [1] " PKU"       "Dumai"      "DUMAI"      "pekanbaru"  "Pekanbaru" 
## [6] "PEKANBARU"  "Pekanbaru " "PKU"        "Siak"
Melihat kategori status

Pemeriksaan kategori status dilakukan untuk menemukan variasi penulisan yang sebenarnya mungkin merujuk pada status yang sama.

sort(unique(pelanggan_raw$status))
## [1] "A"           "ACTIVE"      "aktif"       "Aktif"       "AKTIF"      
## [6] "nonaktif"    "Tidak aktif" "Tidak Aktif"
Memeriksa rentang usia

Rentang usia diperiksa untuk menemukan nilai yang berada di luar batas yang masuk akal bagi data pelanggan.

# Rentang atribut numerik
range(pelanggan_raw$usia, na.rm = TRUE)
## [1]  21 150
Memeriksa rentang pendapatan

Saya memeriksa rentang pendapatan untuk melihat apakah terdapat nilai yang negatif atau sangat jauh dibandingkan observasi lainnya.

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

3.3 Membuat Fungsi Ringkasan Kualitas

Membuat fungsi audit kualitas data

Fungsi ini merangkum beberapa pemeriksaan kualitas data sehingga kondisi dataset dapat dievaluasi secara lebih terstruktur.

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

Yang saya cari dari tahap ini: atribut mana yang memiliki masalah dan perlu diperiksa lebih lanjut. Hasil pemeriksaan belum otomatis menjadi alasan untuk menghapus atau mengganti nilai; keputusan tersebut baru dibuat setelah jenis masalahnya dipahami.

4 Membersihkan Data

4.1 Membuat Data Kerja

Sebelum melakukan perubahan, data mentah tetap dipertahankan dan saya membuat objek kerja bernama pelanggan. Dengan begitu, data awal masih tersedia sebagai pembanding ketika ingin melihat perubahan yang terjadi selama preprocessing.

Membuat data kerja

Data mentah tidak langsung diubah. Saya membuat objek kerja terlebih dahulu agar data awal tetap tersedia sebagai pembanding.

pelanggan <- pelanggan_raw

4.2 Menyeragamkan Penulisan Kategori

Membersihkan spasi pada kategori

Langkah ini menghapus spasi yang tidak diperlukan pada nilai kategori agar penulisan yang sebenarnya sama tidak dianggap sebagai kategori berbeda.

# 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"
Melihat kategori status

Kode ini digunakan untuk melihat seluruh kategori status yang terdapat pada data. Pemeriksaan ini membantu menemukan variasi penulisan sebelum kategori tersebut diseragamkan.

sort(unique(pelanggan$status))
## [1] "a"           "active"      "aktif"       "nonaktif"    "tidak aktif"

4.3 Menyeragamkan Kategori

Nilai yang sebenarnya merujuk pada kategori yang sama masih ditulis dengan format berbeda. Saya menyeragamkan penulisan tersebut terlebih dahulu agar satu kategori tidak terbaca sebagai beberapa kategori hanya karena perbedaan spasi atau kapitalisasi.

Menyeragamkan kategori kota

Beberapa bentuk penulisan kota yang memiliki arti sama dipetakan ke satu kategori 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"
Memeriksa nilai usia

Kode ini digunakan untuk melihat nilai usia yang perlu diperiksa lebih lanjut, terutama untuk menemukan nilai yang berada di luar rentang yang masuk akal.

sort(unique(pelanggan$status))
## [1] "Aktif"       "Tidak Aktif"

4.4 Memeriksa Duplikasi

Setelah penulisan kategori diseragamkan, saya memeriksa apakah satu customer_id muncul lebih dari sekali. Karena pada data ini satu customer_id digunakan untuk mewakili satu pelanggan, kemunculan yang berulang perlu diperiksa sebelum data dilanjutkan ke tahap berikutnya.

Memeriksa customer_id yang berulang

Karena customer_id menjadi identitas utama pelanggan, saya memeriksa apakah ada identifier yang muncul lebih dari satu kali. Hasil pemeriksaan ini digunakan untuk menentukan apakah data mengandung pelanggan yang tercatat berulang.

# Menampilkan seluruh baris dengan customer_id yang berulang
pelanggan[duplicated(pelanggan$customer_id) |
          duplicated(pelanggan$customer_id, fromLast = TRUE), ]
Menghapus duplikasi customer_id

Setelah duplikasi diperiksa, baris yang memiliki customer_id berulang ditangani dengan aturan yang sudah ditetapkan pada kasus ini.

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

dim(pelanggan)
## [1] 11  6

Catatan: penghapusan duplikasi bergantung pada arti customer_id. Pada proses ini, satu customer_id diperlakukan sebagai satu pelanggan sehingga kemunculan yang berulang ditangani dengan mempertahankan kemunculan pertama.

4.5 Memeriksa Nilai yang Tidak Masuk Akal

Memeriksa nilai usia di luar rentang wajar

Saya memeriksa usia yang berada di luar rentang 15–100 tahun untuk menemukan nilai yang perlu ditinjau lebih lanjut. Pemeriksaan ini membantu membedakan nilai yang masih masuk akal dari nilai yang kemungkinan merupakan kesalahan pencatatan.

# Kandidat usia tidak valid berdasarkan aturan bisnis 15–100 tahun
pelanggan[pelanggan$usia < 15 | pelanggan$usia > 100, ]
Memeriksa pendapatan negatif

Pendapatan kemudian diperiksa untuk memastikan tidak terdapat nilai negatif. Jika ditemukan, nilai tersebut perlu ditinjau karena tidak sesuai dengan makna variabel pendapatan pada kasus ini.

# Kandidat pendapatan tidak valid jika bernilai negatif
pelanggan[pelanggan$pendapatan < 0, ]

Dari pemeriksaan rentang, terdapat usia 150 tahun yang tidak masuk akal untuk data pelanggan. Setelah informasi awal pelanggan diperiksa, nilai tersebut diketahui seharusnya 50. Karena terdapat dasar yang jelas untuk melakukan koreksi, nilai tersebut diperbaiki, bukan dihapus.

Mengoreksi nilai usia

Nilai usia yang telah teridentifikasi tidak masuk akal diperbaiki menggunakan informasi yang tersedia sehingga tidak langsung dibuang dari dataset.

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

4.6 Mencatat Perubahan yang Dilakukan

Membuat log perubahan preprocessing

Setiap perubahan penting dicatat agar proses cleaning dapat ditelusuri kembali dan pembaca mengetahui apa yang diubah pada data.

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 kondisi data asli"
  ),
  stringsAsFactors = FALSE
)

log_perubahan

5 Menangani Missing Values

5.1 Menentukan Bagian Data yang Masih Kosong

Membandingkan hasil imputasi

Bagian ini digunakan untuk melihat kondisi data setelah nilai yang kosong ditangani dan membandingkannya dengan kondisi sebelumnya.

colSums(is.na(pelanggan))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           1           2           1           0
Menangani observasi dengan missing value

Pada bagian ini, saya melihat kembali observasi yang masih memiliki nilai kosong sebelum menentukan perlakuan yang paling sesuai. Dengan begitu, keputusan penanganan missing value dibuat berdasarkan kondisi data yang terlihat.

# Baris yang memiliki sedikitnya satu missing value
pelanggan[!complete.cases(pelanggan), ]

5.2 Mencoba Pendekatan Pertama: Menghapus Baris

Setelah cleaning, masih terdapat beberapa nilai yang kosong. Salah satu cara paling sederhana adalah menghapus seluruh baris yang memiliki NA. Saya menjalankan pendekatan ini terlebih dahulu sebagai pembanding, bukan langsung menjadikannya sebagai keputusan akhir.

Kode berikut hanya mendemonstrasikan strategi. Kita tidak langsung mengganti objek pelanggan.

Menangani observasi dengan missing value

Pada bagian ini, saya melihat kembali observasi yang masih memiliki nilai kosong sebelum menentukan perlakuan yang paling sesuai. Dengan begitu, keputusan penanganan missing value dibuat berdasarkan kondisi data yang terlihat.

pelanggan_complete <- pelanggan[complete.cases(pelanggan), ]
nrow(pelanggan)
## [1] 11
Menghitung batas outlier

Kode ini digunakan untuk memperoleh batas bawah dan batas atas berdasarkan aturan IQR sehingga kandidat outlier dapat ditentukan secara konsisten.

nrow(pelanggan_complete)
## [1] 8

Dari hasil tersebut, saya dapat melihat konsekuensi penghapusan baris terhadap ukuran dataset. Ini penting karena metode yang terlihat sederhana dapat menghilangkan cukup banyak observasi.

Menandai kandidat outlier

Setelah batas IQR diperoleh, kode berikut digunakan untuk menandai observasi yang berada di luar batas tersebut sebagai kandidat outlier.

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

5.3 Mencoba Pendekatan Kedua: Mengisi Nilai Numerik yang Hilang

Alternatif berikutnya adalah mempertahankan observasi dan mengganti nilai yang kosong dengan nilai tertentu. Untuk pendapatan, saya membandingkan mean dan median terlebih dahulu karena terdapat nilai yang sangat jauh dari sebagian besar observasi.

Pendapatan memiliki nilai ekstrem. Bandingkan mean dan median sebelum menentukan imputasi.

Menghitung mean pendapatan

Saya menghitung mean pendapatan sebagai salah satu kandidat nilai pengganti untuk pendapatan yang kosong.

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

mean_pendapatan
## [1] 59900000
Membandingkan kondisi outlier

Bagian ini digunakan untuk melihat kembali nilai yang ditandai sebagai kandidat outlier sebelum menentukan perlakuan terhadapnya.

median_pendapatan
## [1] 4900000

Hasil perbandingan menunjukkan bahwa mean jauh lebih tinggi daripada median. Perbedaan tersebut berkaitan dengan nilai pendapatan 500 juta yang menarik rata-rata ke atas. Karena itu, pada contoh ini median digunakan sebagai nilai pengganti yang lebih stabil.

Mengisi missing value pendapatan

Nilai pendapatan yang kosong diisi menggunakan pendekatan yang telah ditentukan pada tahap imputasi. Hasil pengisian disimpan pada variabel yang sesuai agar perubahan dari nilai awal tetap dapat ditelusuri.

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

5.4 Menangani Nilai Kategorik yang Hilang

Untuk kota, pendekatannya berbeda karena atribut ini bersifat kategorik. Daripada menebak kota pelanggan yang hilang menggunakan kategori tertentu, nilai tersebut diberi label Tidak diketahui sehingga informasi bahwa nilainya memang tidak tersedia tetap terlihat.

Nilai kota yang hilang tidak selalu tepat diisi dengan modus. Untuk latihan ini kita gunakan kategori eksplisit Tidak diketahui agar ketidakpastian tidak disembunyikan.

Mengisi missing value kota

Untuk variabel kota, nilai yang kosong ditangani dengan memberikan kategori yang secara jelas menunjukkan bahwa informasi kota tidak tersedia. Pendekatan ini mempertahankan observasi tanpa membuat kategori yang tidak memiliki dasar dari data.

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.5 Menyimpan Informasi bahwa Nilai Pernah Hilang

Mengisi NA dapat membuat data terlihat lengkap, tetapi proses tersebut juga berisiko menghilangkan informasi bahwa nilai awalnya memang kosong. Karena itu, saya menambahkan indikator missing untuk mempertahankan informasi tersebut.

Indikator dapat mempertahankan informasi bahwa suatu nilai awalnya hilang.

Membuat indikator missing

Indikator ini menyimpan informasi bahwa suatu nilai sebelumnya merupakan missing, meskipun setelah imputasi kolom tersebut sudah tidak kosong.

pelanggan$pendapatan_missing <- as.integer(is.na(pelanggan$pendapatan))
table(pelanggan$pendapatan_missing)
## 
## 0 1 
## 9 2

5.6 Memeriksa Dampak Imputasi

Setelah imputasi dilakukan, saya tidak langsung menganggap data sudah lebih baik. Distribusi data sebelum dan sesudah imputasi perlu dibandingkan untuk melihat apakah pengisian nilai kosong mengubah pola data secara berarti.

Membandingkan distribusi sebelum dan sesudah imputasi

Saya membandingkan distribusi sebelum dan sesudah imputasi untuk melihat apakah pengisian nilai kosong mengubah pola data secara berarti.

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)

Membandingkan hasil transformasi

Kode ini digunakan untuk melihat hasil beberapa metode transformasi secara berdampingan sehingga perubahan skala data dapat dibandingkan.

par(mfrow = c(1, 1))

Dari perbandingan ini, yang perlu diperhatikan bukan hanya apakah NA sudah hilang, tetapi juga apakah bentuk distribusi berubah setelah nilai kosong diisi.

6 Memeriksa Nilai yang Terlalu Ekstrem

6.1 Melihat Kandidat Outlier

Setelah menangani nilai yang hilang, saya memeriksa apakah terdapat nilai yang sangat jauh dari sebagian besar data. Boxplot digunakan terlebih dahulu agar posisi nilai ekstrem dapat terlihat secara visual.

Mendeteksi kandidat outlier dengan boxplot

Boxplot digunakan sebagai pemeriksaan visual awal untuk melihat apakah terdapat nilai yang jauh dari sebagian besar observasi.

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

6.2 Menentukan Batas Berdasarkan IQR

Visualisasi memberi petunjuk awal, kemudian kandidat outlier dihitung menggunakan aturan IQR. Dengan cara ini, penentuan kandidat tidak hanya bergantung pada tampilan grafik.

Menghitung batas outlier dengan IQR

Kuartil pertama, kuartil ketiga, dan IQR digunakan untuk menentukan batas bawah dan batas atas. Batas tersebut kemudian menjadi dasar untuk mengidentifikasi observasi yang berpotensi menjadi 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

6.3 Menandai Observasi yang Melewati Batas

Setelah batas bawah dan atas diperoleh, setiap observasi diperiksa untuk melihat apakah nilainya berada di luar rentang tersebut.

Menandai kandidat outlier

Setelah batas IQR diperoleh, observasi yang berada di luar batas tersebut diberi penanda agar dapat dievaluasi lebih lanjut.

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

pelanggan[pelanggan$outlier_pendapatan,
          c("customer_id", "nama", "pendapatan_imputasi")]

6.4 Memutuskan Perlakuan terhadap Outlier

Menjadi outlier tidak otomatis berarti sebuah nilai salah. Karena itu, setelah kandidat ditemukan, saya perlu melihat konteksnya sebelum menentukan tindakan.

Pada data ini terdapat pendapatan 500 juta. Nilai tersebut memang sangat ekstrem, tetapi tidak ada informasi yang menunjukkan bahwa angka tersebut merupakan kesalahan input. Karena itu, nilai tersebut tidak langsung dihapus. Versi winsorized kemudian digunakan untuk melihat bagaimana pembatasan nilai ekstrem dapat memengaruhi hasil transformasi.
Membuat versi winsorized

Pada tahap ini dibuat versi data dengan nilai ekstrem yang dibatasi melalui winsorizing. Versi asli tetap dipertahankan sehingga dampak metode ini dapat dibandingkan tanpa kehilangan data awal.

pelanggan$pendapatan_winsor <- pmin(
  pmax(pelanggan$pendapatan_imputasi, batas_bawah),
  batas_atas
)

pelanggan[pelanggan$outlier_pendapatan,
          c("customer_id", "pendapatan_imputasi", "pendapatan_winsor")]

Pada proses ini, winsorizing digunakan untuk melihat dampaknya terhadap transformasi. Nilai asli tetap tersedia sehingga perubahan akibat metode tersebut dapat dibandingkan.

7 Menyamakan Skala Data

7.1 Normalisasi Min–Maks

Setelah kondisi data numerik diperiksa, saya membandingkan beberapa cara untuk mengubah skala variabel. Metode pertama adalah min–maks, yang membawa nilai ke rentang 0 sampai 1.

Rumus yang digunakan adalah

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

Membuat fungsi normalisasi min–maks

Fungsi ini menyiapkan transformasi min–maks yang mengubah nilai ke skala yang lebih seragam berdasarkan nilai minimum dan maksimum.

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

7.2 Normalisasi Z-Score

Metode berikutnya adalah z-score. Berbeda dari min–maks, metode ini menyatakan posisi suatu nilai relatif terhadap rata-rata dan simpangan baku.

Rumus yang digunakan adalah

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

Menerapkan normalisasi z-score

Z-score digunakan untuk menyatakan nilai berdasarkan jaraknya dari 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)

7.3 Decimal Scaling

Membuat fungsi decimal scaling

Fungsi ini digunakan untuk melakukan transformasi decimal scaling sehingga skala angka dapat diperkecil dengan mempertahankan urutan nilainya.

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 Perbedaan Hasil Transformasi

Ketiga hasil transformasi kemudian diletakkan berdampingan. Tujuannya bukan memilih metode hanya karena hasil angkanya terlihat lebih kecil, tetapi melihat bagaimana skala data berubah setelah masing-masing metode diterapkan.

Menyusun hasil transformasi untuk perbandingan

Hasil dari beberapa metode transformasi disusun berdampingan agar perbedaannya dapat dilihat dan dievaluasi.

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

transformasi

7.5 Melihat Pengaruh Outlier terhadap Skala

Karena sebelumnya ditemukan nilai yang sangat ekstrem, saya juga melihat bagaimana keberadaan nilai tersebut memengaruhi normalisasi. Perbandingan dengan versi yang telah di-winsorize membantu menunjukkan bahwa satu nilai ekstrem dapat memengaruhi skala observasi lainnya.

Membandingkan normalisasi dengan dan tanpa winsorizing

Hasil normalisasi dibandingkan sebelum dan sesudah winsorizing untuk melihat apakah pembatasan nilai ekstrem memberikan perubahan pada skala data.

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)

Yang terlihat dari perbandingan ini: nilai ekstrem dapat membuat sebagian besar observasi lain berada pada rentang yang relatif sempit setelah normalisasi. Ini menunjukkan mengapa kondisi data perlu diperiksa sebelum menentukan transformasi yang akan digunakan.

8 Menggabungkan Dua Tabel Data

8.1 Memastikan Kunci Penggabungan

Setelah data pelanggan diproses, tahap berikutnya adalah menggabungkannya dengan data transaksi. Sebelum melakukan penggabungan, saya terlebih dahulu memeriksa identifier pada kedua tabel.

Hal ini penting karena penggabungan hanya dapat dilakukan dengan benar jika identifier yang digunakan memang sesuai dan tidak menimbulkan pasangan yang keliru.

Memeriksa hasil penggabungan

Setelah kedua tabel digabungkan, kode ini digunakan untuk memeriksa apakah hasil integrasi sudah memiliki struktur dan jumlah observasi yang sesuai.

sum(duplicated(pelanggan$customer_id))
## [1] 0
Memeriksa duplikasi identifier transaksi

Identifier pada data transaksi diperiksa terlebih dahulu untuk memastikan tidak ada pengulangan yang dapat mengganggu proses penggabungan.

sum(duplicated(transaksi_raw$cust_id))
## [1] 0
Mencari pelanggan tanpa pasangan transaksi

Saya memeriksa identifier pelanggan yang tidak memiliki pasangan pada tabel transaksi. Informasi ini penting untuk memahami apakah ada pelanggan yang memang belum memiliki transaksi atau tidak memiliki pasangan saat proses integrasi.

setdiff(pelanggan$customer_id, transaksi_raw$cust_id)
## [1] "C011"
Mencari transaksi tanpa pasangan pelanggan

Pemeriksaan ini dilakukan dari arah sebaliknya, yaitu mencari transaksi yang identifier-nya tidak ditemukan pada data pelanggan. Hasilnya membantu memastikan bahwa setiap transaksi memiliki pasangan data pelanggan yang sesuai.

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

Hasil setdiff() menunjukkan pelanggan yang tidak memiliki transaksi dan transaksi yang tidak memiliki pasangan data pelanggan.

8.2 Menyamakan Nama Identifier

Membuat data transaksi kerja

Seperti data pelanggan, data transaksi juga dibuat sebagai objek kerja sebelum dilakukan penyesuaian.

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

8.3 Menggabungkan Kedua Tabel

Setelah nama identifier diseragamkan, kedua data digabungkan menggunakan merge() dengan pendekatan left join. Dengan cara ini, seluruh pelanggan dari data utama tetap dipertahankan meskipun belum memiliki pasangan pada data transaksi.

Menggabungkan data pelanggan dan transaksi

Kedua tabel kemudian digabungkan menggunakan identifier yang telah diselaraskan sehingga informasi transaksi dapat dipasangkan dengan data pelanggan.

data_terintegrasi <- merge(
  pelanggan,
  transaksi,
  by = "customer_id",
  all.x = TRUE
)

data_terintegrasi[, c(
  "customer_id", "nama", "jumlah_transaksi", "total_purchase"
)]

Pengaturan all.x = TRUE memastikan bahwa seluruh pelanggan dari data utama tetap ada dalam hasil penggabungan. Jika pasangan transaksi tidak ditemukan, kondisi tersebut akan terlihat sebagai NA.

8.4 Memastikan Hasil Penggabungan Tidak Bermasalah

Memeriksa kondisi dataset akhir

Kode ini digunakan sebagai pemeriksaan akhir untuk memastikan dataset yang telah diproses berada dalam kondisi yang siap digunakan.

# Apakah jumlah baris berubah secara tidak wajar?
c(sebelum = nrow(pelanggan), sesudah = nrow(data_terintegrasi))
## sebelum sesudah 
##      11      11
Memeriksa duplikasi setelah integrasi

Setelah penggabungan, duplikasi diperiksa kembali karena proses integrasi dapat menghasilkan lebih dari satu baris untuk identifier tertentu jika kuncinya tidak unik.

# Apakah customer_id tetap unik?
sum(duplicated(data_terintegrasi$customer_id))
## [1] 0
Memeriksa missing value setelah integrasi

Missing value diperiksa kembali untuk mengetahui apakah ada informasi yang menjadi kosong akibat tidak ditemukannya pasangan pada tabel lain.

# Missing value baru akibat tidak ditemukan pasangan transaksi
colSums(is.na(data_terintegrasi[, c("jumlah_transaksi", "total_purchase")]))
## jumlah_transaksi   total_purchase 
##                1                1
Mencari pelanggan tanpa transaksi

Saya mencari pelanggan yang tidak memiliki pasangan pada tabel transaksi untuk mengetahui dampak penggabungan data nantinya.

# Pelanggan tanpa pasangan transaksi
data_terintegrasi[is.na(data_terintegrasi$jumlah_transaksi),
                  c("customer_id", "nama")]

8.5 Menentukan Arti NA pada Data Transaksi

Setelah penggabungan, muncul pertanyaan penting: apakah NA pada transaksi berarti pelanggan tidak pernah bertransaksi, atau justru menunjukkan bahwa informasi transaksi tidak tersedia?

Keduanya memiliki arti yang berbeda. Karena itu, nilai tersebut tidak boleh otomatis diganti menjadi 0 tanpa memastikan definisi datanya. Pada contoh ini, penggantian menjadi 0 dilakukan sebagai contoh apabila memang sudah dikonfirmasi bahwa NA berarti belum pernah bertransaksi.

Menyiapkan variabel transaksi akhir

Variabel transaksi akhir disiapkan sesuai dengan aturan yang digunakan dalam kasus ini setelah makna nilai yang kosong dipahami.

# Contoh jika telah dikonfirmasi bahwa NA 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

9 Memeriksa Hasil Akhir Preprocessing

9.1 Membentuk Dataset yang Akan Digunakan

Setelah seluruh tahap selesai, saya mengambil atribut yang dibutuhkan dari hasil integrasi. Pada tahap ini fokusnya bukan lagi membersihkan data, tetapi memastikan hasil akhir memiliki variabel yang dibutuhkan untuk analisis selanjutnya.

Membentuk dataset akhir

Setelah seluruh proses selesai, variabel yang diperlukan disusun menjadi dataset yang akan digunakan sebagai hasil akhir preprocessing.

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

9.2 Mengecek Kembali Kondisi Dataset

Sebelum menyatakan preprocessing selesai, saya memeriksa kembali struktur, ringkasan, missing value, dan duplikasi. Pemeriksaan akhir ini menjadi kontrol agar masalah yang sudah ditangani tidak muncul kembali atau terlewat setelah proses integrasi.

Melakukan audit akhir dataset

Setelah seluruh tahapan preprocessing selesai, audit akhir dilakukan untuk memastikan perubahan yang telah dibuat tidak meninggalkan masalah kualitas data yang seharusnya masih ditangani.

audit_akhir <- audit_data(data_final)
audit_akhir
Memeriksa duplikasi dataset akhir

Duplikasi pada identifier diperiksa sekali lagi sebagai validasi akhir sebelum dataset digunakan.

sum(duplicated(data_final$customer_id))
## [1] 0
Memeriksa missing value dataset akhir

Saya memastikan kembali kondisi missing value pada dataset akhir agar tidak ada nilai kosong yang terlewat.

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

9.3 Membandingkan Data Sebelum dan Sesudah

Perbandingan terakhir digunakan untuk melihat perubahan yang terjadi sepanjang workflow. Dengan begitu, pembaca dapat melihat bukan hanya dataset akhirnya, tetapi juga apa yang berubah dari kondisi awal.

Memeriksa duplikasi customer_id

Sebagai validasi akhir, customer_id diperiksa kembali untuk memastikan satu pelanggan tidak tercatat lebih dari satu kali pada dataset hasil preprocessing.

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

9.4 Menyimpan Hasil Preprocessing

Menyimpan dataset hasil preprocessing

Dataset yang telah selesai diproses disimpan agar hasilnya dapat digunakan kembali untuk analisis berikutnya.

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

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

10 Ringkasan Workflow

Workflow yang dijalankan pada proses ini dapat diringkas sebagai berikut:

memahami data → membersihkan data → menangani nilai hilang → memeriksa outlier → mengubah skala data → mengintegrasikan dua tabel data → memeriksa hasil akhir.

Yang paling penting dari proses ini bukan sekadar membuat data terlihat “bersih”. Setiap perubahan perlu dapat dijelaskan: masalah apa yang ditemukan, mengapa suatu tindakan dipilih, dan bagaimana kondisi data berubah setelah tindakan tersebut dilakukan.

Dengan alur tersebut, dataset akhir dapat ditelusuri kembali ke kondisi awalnya dan setiap tahap preprocessing memiliki alasan yang jelas.

11 Daftar Pustaka

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