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.
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.
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_rawTabel transaksi ditampilkan untuk melihat isi data sebelum masuk ke tahap pemeriksaan dan integrasi.
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.
Saya menggunakan dim() untuk mengetahui jumlah baris dan
kolom pada dataset. Informasi ini memberi gambaran awal mengenai ukuran
data yang sedang diproses.
## [1] 12 6
Dengan names(), saya melihat seluruh nama kolom yang
tersedia. Langkah ini membantu memastikan atribut yang akan diperiksa
pada tahap berikutnya sudah dikenali dengan jelas.
## [1] "customer_id" "nama" "usia" "pendapatan" "kota"
## [6] "status"
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.
## '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" ...
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.
Ringkasan statistik digunakan untuk memperoleh gambaran cepat mengenai nilai numerik dan karakteristik umum setiap variabel.
## 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
##
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.
Kode ini menghitung berapa banyak nilai yang masih kosong pada setiap atribut. Hasilnya menjadi dasar untuk menentukan bagian data mana yang perlu ditangani.
## customer_id nama usia pendapatan kota status
## 0 0 1 2 1 0
Selain jumlah absolut, saya menghitung persentase nilai yang hilang agar proporsinya dapat dibandingkan antarvariabel.
## customer_id nama usia pendapatan kota status
## 0.00 0.00 8.33 16.67 8.33 0.00
Pemeriksaan ini digunakan untuk mengetahui apakah terdapat baris yang tercatat lebih dari satu kali secara keseluruhan.
## [1] 1
Karena customer_id digunakan sebagai identitas
pelanggan, saya memeriksa apakah ada identifier yang muncul lebih dari
sekali.
## [1] 1
Saya melihat seluruh kategori kota yang muncul untuk mengetahui apakah satu kota ditulis dalam beberapa bentuk berbeda.
## [1] " PKU" "Dumai" "DUMAI" "pekanbaru" "Pekanbaru"
## [6] "PEKANBARU" "Pekanbaru " "PKU" "Siak"
Pemeriksaan kategori status dilakukan untuk menemukan variasi penulisan yang sebenarnya mungkin merujuk pada status yang sama.
## [1] "A" "ACTIVE" "aktif" "Aktif" "AKTIF"
## [6] "nonaktif" "Tidak aktif" "Tidak Aktif"
Rentang usia diperiksa untuk menemukan nilai yang berada di luar batas yang masuk akal bagi data pelanggan.
## [1] 21 150
Saya memeriksa rentang pendapatan untuk melihat apakah terdapat nilai yang negatif atau sangat jauh dibandingkan observasi lainnya.
## [1] 4.5e+06 5.0e+08
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_awalYang 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.
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.
Data mentah tidak langsung diubah. Saya membuat objek kerja terlebih dahulu agar data awal tetap tersedia sebagai pembanding.
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"
Kode ini digunakan untuk melihat seluruh kategori status yang terdapat pada data. Pemeriksaan ini membantu menemukan variasi penulisan sebelum kategori tersebut diseragamkan.
## [1] "a" "active" "aktif" "nonaktif" "tidak aktif"
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.
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"
Kode ini digunakan untuk melihat nilai usia yang perlu diperiksa lebih lanjut, terutama untuk menemukan nilai yang berada di luar rentang yang masuk akal.
## [1] "Aktif" "Tidak Aktif"
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.
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), ]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, satucustomer_iddiperlakukan sebagai satu pelanggan sehingga kemunculan yang berulang ditangani dengan mempertahankan kemunculan pertama.
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, ]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.
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.
Nilai usia yang telah teridentifikasi tidak masuk akal diperbaiki menggunakan informasi yang tersedia sehingga tidak langsung dibuang dari dataset.
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_perubahanBagian ini digunakan untuk melihat kondisi data setelah nilai yang kosong ditangani dan membandingkannya dengan kondisi sebelumnya.
## customer_id nama usia pendapatan kota status
## 0 0 1 2 1 0
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.
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.
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.
## [1] 11
Kode ini digunakan untuk memperoleh batas bawah dan batas atas berdasarkan aturan IQR sehingga kandidat outlier dapat ditentukan secara konsisten.
## [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.
Setelah batas IQR diperoleh, kode berikut digunakan untuk menandai observasi yang berada di luar batas tersebut sebagai kandidat outlier.
## [1] 27.27
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.
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
Bagian ini digunakan untuk melihat kembali nilai yang ditandai sebagai kandidat outlier sebelum menentukan perlakuan terhadapnya.
## [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.
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")]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.
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
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.
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
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.
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)Kode ini digunakan untuk melihat hasil beberapa metode transformasi secara berdampingan sehingga perubahan skala data dapat dibandingkan.
Dari perbandingan ini, yang perlu diperhatikan bukan hanya apakah
NAsudah hilang, tetapi juga apakah bentuk distribusi berubah setelah nilai kosong diisi.
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.
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")Visualisasi memberi petunjuk awal, kemudian kandidat outlier dihitung menggunakan aturan IQR. Dengan cara ini, penentuan kandidat tidak hanya bergantung pada tampilan grafik.
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
Setelah batas bawah dan atas diperoleh, setiap observasi diperiksa untuk melihat apakah nilainya berada di luar rentang tersebut.
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")]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.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.
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)}. \]
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")]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}. \]
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)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
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.
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"
)]
transformasiKarena 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.
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.
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.
Setelah kedua tabel digabungkan, kode ini digunakan untuk memeriksa apakah hasil integrasi sudah memiliki struktur dan jumlah observasi yang sesuai.
## [1] 0
Identifier pada data transaksi diperiksa terlebih dahulu untuk memastikan tidak ada pengulangan yang dapat mengganggu proses penggabungan.
## [1] 0
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.
## [1] "C011"
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.
## [1] "C012"
Hasil setdiff() menunjukkan pelanggan yang tidak
memiliki transaksi dan transaksi yang tidak memiliki pasangan data
pelanggan.
Seperti data pelanggan, data transaksi juga dibuat sebagai objek kerja sebelum dilakukan penyesuaian.
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.
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.
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
Setelah penggabungan, duplikasi diperiksa kembali karena proses integrasi dapat menghasilkan lebih dari satu baris untuk identifier tertentu jika kuncinya tidak unik.
## [1] 0
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
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")]NA pada Data TransaksiSetelah 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.
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)
] <- 0Setelah 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.
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_finalSebelum 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.
Setelah seluruh tahapan preprocessing selesai, audit akhir dilakukan untuk memastikan perubahan yang telah dibuat tidak meninggalkan masalah kualitas data yang seharusnya masih ditangani.
Duplikasi pada identifier diperiksa sekali lagi sebagai validasi akhir sebelum dataset digunakan.
## [1] 0
Saya memastikan kembali kondisi missing value pada dataset akhir agar tidak ada nilai kosong yang terlewat.
## 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
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.
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))
)
)
perbandinganDataset yang telah selesai diproses disimpan agar hasilnya dapat digunakan kembali untuk analisis berikutnya.
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.
Han, J., Kamber, M., & Pei, J. (2012). Data Mining: Concepts and Techniques (3rd ed.). Morgan Kaufmann. Chapter 3: Data Preprocessing.