Sebelum masuk ke tahap teknis, proyek RStudio perlu disiapkan lebih dulu agar seluruh file praktikum tersimpan rapi dalam satu folder kerja.
praktikum_preprocessing.Dua hal paling dasar yang perlu dipastikan di awal adalah lokasi direktori kerja dan versi R yang sedang digunakan.
## [1] "D:/SEMESTER 5/2. DATA MINING"
## [1] "R version 4.4.2 (2024-10-31 ucrt)"
Karena seluruh tahapan pada modul ini hanya memanfaatkan fungsi bawaan R (base R), praktikum dapat langsung dijalankan tanpa perlu memasang paket tambahan apa pun.
Dataset yang dipakai, yaitu dataset pelanggan dan dataset transaksi.Kondisi awal dataset masih mengandung beberapa ketidakteraturan sehingga dapat digunakan untuk memperlihatkan berbagai teknik preprocessing.
Data pelanggan memuat informasi seperti customer_id, nama, usia, pendapatan, kota, dan status. Sementara itu, data transaksi berisi informasi yang berkaitan dengan aktivitas transaksi pelanggan.
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
)Isi dari kedua tabel tersebut dapat dilihat pada tampilan berikut.
## 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
## 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
Sebelum data “dibersihkan”, struktur dasarnya perlu dipahami lebih dulu — berapa ukurannya, kolom apa saja yang ada, dan bagaimana tipe datanya.
Dimensi data. Fungsi dim() memberi tahu
berapa jumlah baris dan kolom sekaligus dalam satu perintah.
## [1] 12 6
Nama Variabel. Untuk mengetahui atribut apa saja
yang tersedia, cukup panggil names().
## [1] "customer_id" "nama" "usia" "pendapatan" "kota"
## [6] "status"
Struktur Data. Bila ingin melihat gambaran
menyeluruh — tipe data tiap kolom sekaligus beberapa contoh nilainya —
str() adalah pilihan yang paling praktis.
## '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" ...
Cuplikan data. head() menampilkan
beberapa baris pertama saja, berguna untuk sekadar mengintip bentuk data
tanpa harus memuat seluruh tabel.
## 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
Statistik Deskriptif. Terakhir,
summary() merangkum setiap kolom secara numerik maupun
kategorikal — mulai dari nilai minimum, maksimum, hingga rata-rata untuk
kolom angka.
## customer_id nama usia pendapatan
## Length:12 Length:12 Min. : 21.00 Min. : 4500000
## Class :character Class :character 1st Qu.: 24.00 1st Qu.: 4725000
## Mode :character Mode :character Median : 28.00 Median : 5000000
## Mean : 38.73 Mean : 54440000
## 3rd Qu.: 33.00 3rd Qu.: 5275000
## Max. :150.00 Max. :500000000
## NA's :1 NA's :2
## kota status
## Length:12 Length:12
## Class :character Class :character
## Mode :character Mode :character
##
##
##
##
Setelah struktur data dipahami, langkah berikutnya adalah menilai kualitasnya: apakah ada nilai hilang, data yang terduplikasi, penulisan kategori yang tidak seragam, atau nilai yang berada di luar rentang wajar.
Missing Value
## customer_id nama usia pendapatan kota status
## 0 0 1 2 1 0
Jika dilihat dalam bentuk persentase, gambarannya menjadi seperti ini.
## customer_id nama usia pendapatan kota status
## 0.00 0.00 8.33 16.67 8.33 0.00
DUplikasi Data
## [1] 1
Khusus untuk kolom customer_id, apakah ada ID pelanggan
yang muncul lebih dari satu kali?
## [1] 1
Konsistensi Kategori
## [1] " PKU" "Dumai" "DUMAI" "pekanbaru" "Pekanbaru"
## [6] "PEKANBARU" "Pekanbaru " "PKU" "Siak"
Pola serupa juga dicek pada kolom status.
## [1] "A" "ACTIVE" "aktif" "Aktif" "AKTIF"
## [6] "nonaktif" "Tidak aktif" "Tidak Aktif"
Pemeriksaan Nilai Numerik
## [1] 21 150
Begitu pula pada kolom pendapatan.
## [1] 4.5e+06 5.0e+08
Mengulang kode pemeriksaan satu per satu seperti di atas cukup
memakan waktu bila dilakukan berkali-kali. Karena itu, dibuat satu
fungsi audit_data() yang merangkum semua indikator kualitas
— tipe data, jumlah dan persentase missing value, serta banyaknya nilai
unik — dalam satu tabel ringkas.
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
Dari tabel audit ini, atribut dengan persentase missing tinggi atau jumlah nilai unik yang mencurigakan langsung terlihat, sehingga bisa jadi prioritas untuk diperiksa lebih lanjut.
Agar data mentah (pelanggan_raw) tetap bisa dijadikan
pembanding di akhir, seluruh proses pembersihan dilakukan pada sebuah
salinan bernama pelanggan, bukan pada data aslinya.
Dua kolom teks kota dan status masih
memiliki variasi penulisan yang murni disebabkan oleh spasi berlebih dan
perbedaan huruf besar/kecil. Ini dirapikan lebih dulu sebelum masuk ke
standarisasi kategori yang lebih kompleks.
Menghilangkan spasi di awal dan akhir teks dengan
trimws():
Lalu seluruh huruf diseragamkan menjadi huruf kecil supaya proses pencocokan kategori pada tahap berikutnya tidak sensitif terhadap kapitalisasi.
Efeknya cukup terlihat — jumlah variasi unik pada kedua kolom sudah mulai berkurang meski belum sepenuhnya seragam.
## [1] "dumai" "pekanbaru" "pku" "siak"
## [1] "a" "active" "aktif" "nonaktif" "tidak aktif"
Spasi dan kapitalisasi sudah rapi, namun beberapa kategori masih merupakan singkatan atau istilah berbeda untuk makna yang sama — misalnya “pku” dan “pekanbaru”, atau “active” dan “a”. Pemetaan manual diperlukan di sini.
Kota “pku” dan “pekanbaru” digabung menjadi satu label baku “Pekanbaru”, begitu juga kota lainnya:
pelanggan$kota[pelanggan$kota %in% c("pku", "pekanbaru")] <- "Pekanbaru"
pelanggan$kota[pelanggan$kota == "dumai"] <- "Dumai"
pelanggan$kota[pelanggan$kota == "siak"] <- "Siak"Pola yang sama diterapkan pada kolom status, dengan mengelompokkan seluruh variasi “aktif” di satu sisi dan “tidak aktif” di sisi lain:
pelanggan$status[pelanggan$status %in% c("aktif", "active", "a")] <- "Aktif"
pelanggan$status[pelanggan$status %in% c("tidak aktif", "nonaktif")] <- "Tidak Aktif"Sekarang kedua kolom sudah memiliki kategori yang benar-benar seragam, tanpa duplikasi makna.
## [1] "Dumai" "Pekanbaru" "Siak"
## [1] "Aktif" "Tidak Aktif"
Ingat bahwa C010 sempat muncul dua kali pada
customer_id. Sebelum dihapus, baris-baris yang terlibat
perlu ditampilkan dulu agar terlihat jelas datanya identik atau
tidak.
## customer_id nama usia pendapatan kota status
## 10 C010 Joko 35 5300000 Dumai Tidak Aktif
## 11 C010 Joko 35 5300000 Dumai Tidak Aktif
Karena kedua baris tersebut memang identik, kemunculan kedua dapat
dibuang dengan aman menggunakan duplicated(), lalu nomor
baris dirapikan ulang.
pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL
dim(pelanggan)## [1] 11 6
Selain duplikasi, nilai yang secara logika tidak masuk akal (domain violation) juga perlu diperiksa. Contohnya, usia manusia yang wajar biasanya berada di rentang 15–100 tahun.
## customer_id nama usia pendapatan kota status
## 4 C004 Dodi 150 4800000 Dumai Aktif
## NA <NA> <NA> NA NA <NA> <NA>
Aturan yang sama berlaku untuk pendapatan — nilai negatif jelas tidak masuk akal secara bisnis.
## customer_id nama usia pendapatan kota status
## NA <NA> <NA> NA NA <NA> <NA>
## NA.1 <NA> <NA> NA NA <NA> <NA>
Hasil investigasi lapangan menunjukkan bahwa usia 150 tahun tadi
sebenarnya adalah kesalahan input, dan nilai yang benar adalah 50 tahun.
Koreksi pun dilakukan langsung pada baris dengan
customer_id “C004”.
Setiap keputusan yang diambil selama proses cleaning sebaiknya didokumentasikan, bukan hanya diketahui dari kode. Log ini penting sebagai bentuk transparansi dan jejak audit.
log_perubahan <- data.frame(
tahap = c("Standardisasi", "Standardisasi", "Deduplikasi", "Koreksi domain"),
atribut = c("kota", "status", "customer_id", "usia"),
tindakan = c(
"PKU dan variasi kapital menjadi Pekanbaru",
"ACTIVE/A menjadi Aktif; nonaktif menjadi Tidak Aktif",
"Menghapus kemunculan kedua C010",
"Usia C004 dikoreksi dari 150 menjadi 50 berdasarkan sumber asli"
),
stringsAsFactors = FALSE
)
log_perubahan## tahap atribut
## 1 Standardisasi kota
## 2 Standardisasi status
## 3 Deduplikasi customer_id
## 4 Koreksi domain usia
## tindakan
## 1 PKU dan variasi kapital menjadi Pekanbaru
## 2 ACTIVE/A menjadi Aktif; nonaktif menjadi Tidak Aktif
## 3 Menghapus kemunculan kedua C010
## 4 Usia C004 dikoreksi dari 150 menjadi 50 berdasarkan sumber asli
Setelah proses cleaning kategori dan duplikasi selesai, giliran nilai hilang yang ditangani. Langkah pertama selalu sama: cari tahu di kolom mana saja NA itu berada.
## customer_id nama usia pendapatan kota status
## 0 0 1 2 1 0
Mengetahui kolomnya saja belum cukup — baris spesifik yang mengandung
nilai hilang juga perlu dilihat langsung, dengan bantuan
complete.cases().
## 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
Cara paling sederhana adalah membuang seluruh baris yang mengandung
nilai hilang (listwise deletion). Perlu ditekankan bahwa ini
hanya simulasi — objek pelanggan yang asli tidak diubah,
hasilnya disimpan terpisah di pelanggan_complete.
## [1] 11
## [1] 8
Berapa persen data yang akan hilang jika strategi ini benar-benar diterapkan?
## [1] 27.27
Angka ini memberi gambaran seberapa “mahal” strategi penghapusan baris — semakin tinggi persentasenya, semakin besar risiko kehilangan informasi yang sebenarnya masih berguna.
Alternatif yang lebih hemat data adalah imputasi, yaitu mengisi nilai yang hilang dengan estimasi tertentu. Dua pilihan paling umum adalah rata-rata (mean) dan nilai tengah (median).
Karena kolom pendapatan diketahui memiliki nilai ekstrem (outlier), mean dan median perlu dibandingkan dulu sebelum menentukan mana yang lebih representatif.
mean_pendapatan <- mean(pelanggan$pendapatan, na.rm = TRUE)
median_pendapatan <- median(pelanggan$pendapatan, na.rm = TRUE)
mean_pendapatan## [1] 59900000
## [1] 4900000
Selisih keduanya cukup jauh — pertanda mean sudah terpengaruh outlier. Karena itu, median dipilih sebagai nilai pengganti karena lebih tahan terhadap nilai ekstrem.
pelanggan$pendapatan_imputasi <- pelanggan$pendapatan
pelanggan$pendapatan_imputasi[is.na(pelanggan$pendapatan_imputasi)] <-
median_pendapatanPerlakuan yang sama, dengan alasan konsistensi metode, diterapkan pula pada kolom usia.
median_usia <- median(pelanggan$usia, na.rm = TRUE)
pelanggan$usia_imputasi <- pelanggan$usia
pelanggan$usia_imputasi[is.na(pelanggan$usia_imputasi)] <- median_usiaPerbandingan nilai sebelum dan sesudah imputasi dapat dilihat berdampingan berikut ini.
## 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
Kolom kategorik seperti kota tidak bisa langsung diisi
dengan modus, sebab hal itu berisiko menciptakan asumsi lokasi yang
sebenarnya tidak diketahui kebenarannya. Pendekatan yang lebih jujur
adalah memberi label eksplisit “Tidak diketahui”, sehingga nilai hilang
tetap dapat ditelusuri, bukan disamarkan seolah-olah datanya
lengkap.
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
Salah satu kelemahan imputasi adalah informasi “nilai ini awalnya hilang” bisa hilang begitu saja setelah diisi. Kolom indikator biner dibuat untuk menjaga jejak tersebut tetap ada.
pelanggan$pendapatan_missing <- as.integer(is.na(pelanggan$pendapatan))
table(pelanggan$pendapatan_missing)##
## 0 1
## 9 2
Angka saja terkadang kurang menggambarkan dampak imputasi secara utuh. Visualisasi histogram berdampingan membantu melihat apakah bentuk distribusi berubah signifikan setelah nilai hilang diisi.
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)Terlihat bahwa imputasi cenderung memadatkan sebagian nilai di sekitar median, sehingga variasi data sedikit mengecil. Ini mengingatkan bahwa proses evaluasi tidak boleh berhenti hanya karena seluruh NA sudah terisi.
Sebelum menghitung batas outlier secara matematis, boxplot memberi gambaran visual cepat mengenai keberadaan nilai-nilai ekstrem pada pendapatan.
boxplot(pelanggan$pendapatan_imputasi,
horizontal = TRUE,
col = "lightblue",
main = "Boxplot Pendapatan",
xlab = "Pendapatan")Agar batas outlier tidak sekadar bersifat visual dan subjektif, metode IQR (Interquartile Range) digunakan untuk menghitung batas bawah dan batas atas secara pasti.
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
Nilai apa pun yang berada di luar rentang [batas_bawah, batas_atas] ditandai sebagai kandidat outlier melalui kolom logika baru.
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
Menemukan outlier bukan berarti otomatis harus dihapus. Ada tiga kemungkinan penyebab yang perlu dipertimbangkan lebih dulu:
Pada kasus ini, outlier tetap dipertahankan (tidak dihapus), namun sebagai perbandingan dampak metode, teknik winsorizing diterapkan untuk membatasi nilai ekstrem tanpa membuang datanya.
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
Penting dicatat: winsorizing di sini hanya demonstrasi tambahan. Terapkan hanya jika sesuai tujuan analisis, dan selalu dokumentasikan bahwa nilai ekstrem telah dibatasi — jangan sampai pembaca laporan mengira nilai tersebut asli.
Cara pertama untuk mentransformasi data numerik adalah normalisasi min–maks, yang memetakan seluruh nilai ke rentang \([0,1]\) menggunakan rumus:
\[ 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
Berbeda dari min–maks yang terikat rentang [0,1], normalisasi z-score justru mengukur seberapa jauh sebuah nilai menyimpang dari rata-rata dalam satuan standar deviasi:
\[ z = \frac{x-\bar{x}}{s}. \]
pelanggan$usia_z <- as.numeric(scale(pelanggan$usia_imputasi))
pelanggan$pendapatan_z <- as.numeric(scale(pelanggan$pendapatan_imputasi))
round(pelanggan[, c("usia_z", "pendapatan_z")], 3)## usia_z pendapatan_z
## 1 -0.984 -0.304
## 2 -0.489 -0.301
## 3 -0.737 -0.299
## 4 2.604 -0.302
## 5 -0.242 -0.301
## 6 -0.180 -0.300
## 7 0.253 3.015
## 8 0.006 -0.303
## 9 -0.860 -0.303
## 10 0.748 -0.299
## 11 -0.118 -0.301
Pendekatan ketiga, decimal scaling, sedikit lebih sederhana secara konsep: nilai cukup dibagi dengan pangkat 10 tertentu sampai seluruh nilai berada dalam rentang [-1, 1].
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 metode di atas punya efek numerik yang berbeda meskipun berasal dari kolom yang sama. Menampilkannya bersisian memudahkan melihat perbedaan skala hasil masing-masing metode.
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
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)Titik-titik yang menyimpang jauh dari garis diagonal merah menunjukkan betapa outlier dapat menekan sebagian besar nilai min–maks lainnya ke rentang yang sangat sempit. Inilah alasan mengapa deteksi outlier semestinya dilakukan sebelum memilih metode transformasi, bukan sesudahnya.
Sebelum dua tabel digabungkan, kolom kunci penghubungnya —
customer_id di data pelanggan dan cust_id di
data transaksi — perlu dipastikan bersih dari duplikasi dan diketahui
kecocokannya.
## [1] 0
## [1] 0
Pelanggan mana saja yang tidak memiliki data transaksi?
## [1] "C011"
Sebaliknya, apakah ada transaksi milik pelanggan yang tidak terdaftar di tabel pelanggan?
## [1] "C012"
Kedua hasil setdiff() di atas menegaskan bahwa hubungan
antar tabel ini tidak sepenuhnya 1-ke-1 — ada pelanggan tanpa transaksi,
dan ada transaksi tanpa pasangan pelanggan yang valid.
Agar merge() dapat mengenali kolom kunci yang sama, nama
kolom cust_id pada tabel transaksi disamakan terlebih
dahulu menjadi customer_id, mengikuti penamaan pada tabel
pelanggan.
Dengan nama kolom kunci yang sudah selaras, kedua tabel dapat
digabung menggunakan merge().
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
Proses join tidak boleh langsung dipercaya begitu saja; beberapa pengecekan berikut memastikan hasilnya benar.
Apakah jumlah baris berubah setelah digabung?
## sebelum sesudah
## 11 11
Apakah customer_id tetap unik pasca-penggabungan, atau
justru muncul duplikasi baru akibat relasi many-to-many?
## [1] 0
Adakah nilai hilang baru yang muncul akibat pelanggan tanpa pasangan transaksi?
## jumlah_transaksi total_purchase
## 1 1
Pelanggan mana saja tepatnya yang tidak memiliki pasangan transaksi tersebut?
## customer_id nama
## 11 C011 Kiki
Ini adalah keputusan yang mudah terlewat: NA pada kolom transaksi setelah join sebenarnya bisa punya dua makna yang sangat berbeda.
Nilai NA baru boleh diganti menjadi 0 apabila makna pertama sudah benar-benar dikonfirmasi kebenarannya — mengubahnya tanpa verifikasi berisiko menyembunyikan masalah pencocokan data.
# 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)
] <- 0Dari seluruh kolom yang telah dibuat sepanjang proses — termasuk
kolom-kolom perantara seperti usia, kota, atau
pendapatan versi mentah — hanya kolom hasil akhir yang
dipertahankan untuk dataset final, lalu diberi nama yang lebih
ringkas.
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
Sama seperti di awal, dataset yang sudah melalui seluruh tahapan preprocessing ini perlu diaudit ulang untuk memastikan tidak ada lagi nilai hilang atau duplikasi yang tersisa.
Fungsi audit_data() yang sama dari Bagian I dipakai
kembali di sini:
## 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
Pengecekan duplikasi pada customer_id:
## [1] 0
Dan rekap total nilai hilang per kolom:
## 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
Untuk melihat sejauh mana dampak preprocessing secara keseluruhan, beberapa indikator kunci dibandingkan antara kondisi data mentah dan kondisi data final dalam satu tabel ringkas.
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
Sebagai langkah penutup, dataset final beserta log seluruh perubahan yang telah dilakukan disimpan ke dalam file CSV agar dapat dipakai kembali untuk analisis selanjutnya tanpa perlu mengulang proses preprocessing dari awal.
Secara garis besar, alur preprocessing yang telah dilalui pada praktikum ini adalah:
Baik-buruknya hasil preprocessing pada akhirnya bergantung pada keputusan analis dalam memahami konteks data dan tujuan analisis — R hanyalah alat untuk menjalankan keputusan tersebut, bukan penentu keputusan itu sendiri.
Han, J., Kamber, M., & Pei, J. (2012). Data Mining: Concepts and Techniques (3rd ed.). Morgan Kaufmann. Chapter 3: Data Preprocessing.