Sebagai langkah awal, proyek RStudio sebaiknya disiapkan terlebih dahulu agar seluruh berkas praktikum tersusun dalam satu folder kerja yang sama.
praktikum_preprocessing.Pada tahap awal, dua hal penting yang perlu diperiksa adalah lokasi direktori kerja serta versi R yang sedang digunakan.
getwd()
## [1] "C:/Users/ASUS/Downloads"
R.version.string
## [1] "R version 4.5.2 (2025-10-31 ucrt)"
Seluruh tahapan pada modul ini menggunakan fungsi bawaan R (base R), sehingga praktikum dapat langsung dijalankan tanpa instalasi paket tambahan.
Dataset yang digunakan terdiri dari dataset pelanggan dan dataset transaksi. Pada kondisi awal, data masih memiliki beberapa ketidakteraturan sehingga cocok untuk memperagakan berbagai teknik preprocessing.
Data pelanggan berisi atribut seperti customer_id, nama, usia, pendapatan, kota, dan status. Adapun data transaksi memuat informasi yang berkaitan dengan aktivitas transaksi setiap 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 kedua tabel tersebut kemudian dapat diperiksa melalui tampilan berikut.
pelanggan_raw
## customer_id nama usia pendapatan kota status
## 1 C001 Ani 21 4.5e+06 Pekanbaru Aktif
## 2 C002 Budi 25 NA PKU aktif
## 3 C003 Citra 23 5.2e+06 PEKANBARU ACTIVE
## 4 C004 Dodi 150 4.8e+06 Dumai A
## 5 C005 Eka 27 4.9e+06 pekanbaru Tidak Aktif
## 6 C006 Fani NA 5.1e+06 DUMAI nonaktif
## 7 C007 Gilang 31 5.0e+08 Pekanbaru Aktif
## 8 C008 Hana 29 4.7e+06 Siak AKTIF
## 9 C009 Indra 22 4.6e+06 PKU A
## 10 C010 Joko 35 5.3e+06 Dumai Tidak aktif
## 11 C010 Joko 35 5.3e+06 Dumai Tidak aktif
## 12 C011 Kiki 28 NA <NA> Aktif
transaksi_raw
## cust_id jumlah_transaksi total_purchase
## 1 C001 5 1.5e+06
## 2 C002 3 9.0e+05
## 3 C003 7 2.7e+06
## 4 C004 2 6.0e+05
## 5 C005 6 2.1e+06
## 6 C006 4 1.3e+06
## 7 C007 20 2.5e+07
## 8 C008 5 1.7e+06
## 9 C009 3 8.0e+05
## 10 C010 8 3.2e+06
## 11 C012 1 2.5e+05
Sebelum melakukan proses pembersihan, struktur dasar data perlu dikenali terlebih dahulu, mulai dari ukuran data, nama kolom, hingga tipe datanya.
Dimensi data. Fungsi dim() digunakan
untuk mengetahui jumlah baris dan kolom data secara bersamaan.
dim(pelanggan_raw)
## [1] 12 6
Nama Variabel. Untuk melihat atribut atau nama
variabel yang tersedia, fungsi names() dapat digunakan.
names(pelanggan_raw)
## [1] "customer_id" "nama" "usia" "pendapatan" "kota"
## [6] "status"
Struktur Data. Jika ingin memperoleh gambaran
struktur secara menyeluruh, termasuk tipe setiap kolom dan contoh
nilainya, str() merupakan fungsi yang praktis.
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" ...
Cuplikan data. head() menampilkan
beberapa baris awal sehingga bentuk data dapat diperiksa tanpa harus
melihat seluruh isi tabel.
head(pelanggan_raw)
## customer_id nama usia pendapatan kota status
## 1 C001 Ani 21 4500000 Pekanbaru Aktif
## 2 C002 Budi 25 NA PKU aktif
## 3 C003 Citra 23 5200000 PEKANBARU ACTIVE
## 4 C004 Dodi 150 4800000 Dumai A
## 5 C005 Eka 27 4900000 pekanbaru Tidak Aktif
## 6 C006 Fani NA 5100000 DUMAI nonaktif
Statistik Deskriptif. Terakhir,
summary() memberikan ringkasan tiap kolom, baik numerik
maupun kategorikal, seperti nilai minimum, maksimum, dan rata-rata pada
variabel numerik.
summary(pelanggan_raw)
## customer_id nama usia pendapatan
## Length:12 Length:12 Min. : 21.00 Min. : 4500000
## Class :character Class :character 1st Qu.: 24.00 1st Qu.: 4725000
## Mode :character Mode :character Median : 28.00 Median : 5000000
## Mean : 38.73 Mean : 54440000
## 3rd Qu.: 33.00 3rd Qu.: 5275000
## Max. :150.00 Max. :500000000
## NA's :1 NA's :2
## kota status
## Length:12 Length:12
## Class :character Class :character
## Mode :character Mode :character
##
##
##
##
Setelah struktur diketahui, kualitas data perlu diperiksa untuk menemukan missing value, duplikasi, kategori yang belum konsisten, maupun nilai numerik yang berada di luar batas kewajaran.
Missing Value
colSums(is.na(pelanggan_raw))
## customer_id nama usia pendapatan kota status
## 0 0 1 2 1 0
Jika pemeriksaan tersebut dinyatakan dalam persentase, hasilnya dapat dilihat sebagai berikut.
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
Duplikasi Data
sum(duplicated(pelanggan_raw))
## [1] 1
Selanjutnya, khusus pada kolom customer_id, perlu
diperiksa apakah terdapat ID pelanggan yang tercatat lebih dari
sekali.
sum(duplicated(pelanggan_raw$customer_id))
## [1] 1
Konsistensi Kategori
sort(unique(pelanggan_raw$kota))
## [1] " PKU" "Dumai" "DUMAI" "pekanbaru" "Pekanbaru"
## [6] "PEKANBARU" "Pekanbaru " "PKU" "Siak"
Pemeriksaan yang sama juga dilakukan terhadap kolom
status.
sort(unique(pelanggan_raw$status))
## [1] "A" "ACTIVE" "aktif" "Aktif" "AKTIF"
## [6] "nonaktif" "Tidak aktif" "Tidak Aktif"
Pemeriksaan Nilai Numerik
range(pelanggan_raw$usia, na.rm = TRUE)
## [1] 21 150
Hal serupa kemudian diperiksa pada variabel pendapatan.
range(pelanggan_raw$pendapatan, na.rm = TRUE)
## [1] 4.5e+06 5.0e+08
Menuliskan pemeriksaan yang sama berulang kali akan kurang efisien.
Oleh sebab itu, dibuat fungsi audit_data() untuk merangkum
indikator kualitas, yaitu tipe data, jumlah dan persentase missing
value, serta jumlah nilai unik dalam satu tabel.
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
Melalui tabel audit tersebut, atribut yang memiliki missing value tinggi atau jumlah nilai unik yang tidak wajar dapat segera dikenali untuk diperiksa lebih lanjut.
Supaya data mentah (pelanggan_raw) tetap tersedia
sebagai pembanding, proses cleaning dilakukan pada salinannya, yaitu
objek pelanggan, tanpa mengubah data awal.
pelanggan <- pelanggan_raw
Kolom teks kota dan status masih
menunjukkan perbedaan akibat spasi tambahan serta penggunaan huruf besar
dan kecil. Kondisi ini dirapikan terlebih dahulu sebelum standardisasi
kategori dilakukan.
Spasi pada bagian awal dan akhir teks dihapus menggunakan
trimws():
pelanggan$kota <- trimws(pelanggan$kota)
pelanggan$status <- trimws(pelanggan$status)
Selanjutnya, seluruh teks diubah menjadi huruf kecil agar pencocokan kategori berikutnya tidak dipengaruhi oleh perbedaan kapitalisasi.
pelanggan$kota <- tolower(pelanggan$kota)
pelanggan$status <- tolower(pelanggan$status)
Hasilnya terlihat dari berkurangnya variasi unik pada kedua kolom, walaupun kategorinya belum seluruhnya seragam.
sort(unique(pelanggan$kota))
## [1] "dumai" "pekanbaru" "pku" "siak"
sort(unique(pelanggan$status))
## [1] "a" "active" "aktif" "nonaktif" "tidak aktif"
Setelah spasi dan kapitalisasi diperbaiki, masih terdapat singkatan atau istilah berbeda yang sebenarnya memiliki arti sama, misalnya “pku” dan “pekanbaru” serta “active” dan “a”. Karena itu, diperlukan pemetaan kategori secara manual.
Kategori kota “pku” dan “pekanbaru” disatukan menjadi label standar “Pekanbaru”, demikian pula untuk kategori kota lainnya:
pelanggan$kota[pelanggan$kota %in% c("pku", "pekanbaru")] <- "Pekanbaru"
pelanggan$kota[pelanggan$kota == "dumai"] <- "Dumai"
pelanggan$kota[pelanggan$kota == "siak"] <- "Siak"
Langkah serupa diterapkan pada kolom status dengan mengelompokkan seluruh variasi “aktif” dan “tidak aktif” ke label yang sama:
pelanggan$status[pelanggan$status %in% c("aktif", "active", "a")] <- "Aktif"
pelanggan$status[pelanggan$status %in% c("tidak aktif", "nonaktif")] <- "Tidak Aktif"
Pada tahap ini, kedua kolom telah memiliki kategori yang seragam dan tidak lagi memiliki variasi dengan makna yang sama.
sort(unique(pelanggan$kota))
## [1] "Dumai" "Pekanbaru" "Siak"
sort(unique(pelanggan$status))
## [1] "Aktif" "Tidak Aktif"
Perlu diingat bahwa C010 tercatat dua kali pada
customer_id. Sebelum menghapus salah satunya, kedua baris
diperiksa terlebih dahulu untuk memastikan apakah isinya identik.
pelanggan[duplicated(pelanggan$customer_id) |
duplicated(pelanggan$customer_id, fromLast = TRUE), ]
## customer_id nama usia pendapatan kota status
## 10 C010 Joko 35 5300000 Dumai Tidak Aktif
## 11 C010 Joko 35 5300000 Dumai Tidak Aktif
Karena kedua baris terbukti identik, kemunculan kedua dapat dihapus
menggunakan duplicated(), kemudian nomor baris disusun
kembali.
pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL
dim(pelanggan)
## [1] 11 6
Selain duplikasi, nilai yang melanggar aturan logis atau domain juga perlu diperiksa. Sebagai contoh, usia manusia secara umum dapat diperiksa pada rentang 15–100 tahun.
pelanggan[pelanggan$usia < 15 | pelanggan$usia > 100, ]
## customer_id nama usia pendapatan kota status
## 4 C004 Dodi 150 4800000 Dumai Aktif
## NA <NA> <NA> NA NA <NA> <NA>
Pemeriksaan domain juga diterapkan pada pendapatan, karena nilai negatif tidak sesuai dengan konteks bisnis pada data ini.
pelanggan[pelanggan$pendapatan < 0, ]
## customer_id nama usia pendapatan kota status
## NA <NA> <NA> NA NA <NA> <NA>
## NA.1 <NA> <NA> NA NA <NA> <NA>
Berdasarkan hasil investigasi, usia 150 tahun merupakan kesalahan
input dan nilai yang benar adalah 50 tahun. Oleh karena itu, baris
dengan customer_id “C004” dikoreksi.
pelanggan$usia[pelanggan$customer_id == "C004"] <- 50
Setiap keputusan dalam proses cleaning sebaiknya dicatat, tidak hanya tersimpan di dalam kode. Log perubahan berfungsi sebagai dokumentasi, 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 kategori dan duplikasi dibersihkan, tahap berikutnya adalah menangani nilai yang hilang. Langkah awalnya adalah mengetahui kolom mana saja yang mengandung NA.
colSums(is.na(pelanggan))
## customer_id nama usia pendapatan kota status
## 0 0 1 2 1 0
Mengetahui kolom yang memiliki NA saja belum memadai; baris yang
terdampak juga perlu diperiksa secara langsung menggunakan
complete.cases().
pelanggan[!complete.cases(pelanggan), ]
## customer_id nama usia pendapatan kota status
## 2 C002 Budi 25 NA Pekanbaru Aktif
## 6 C006 Fani NA 5100000 Dumai Tidak Aktif
## 11 C011 Kiki 28 NA <NA> Aktif
Alternatif paling sederhana adalah menghapus baris yang memiliki
nilai hilang (listwise deletion). Pada praktikum ini langkah
tersebut hanya sebagai simulasi; objek pelanggan tetap
dipertahankan dan hasilnya disimpan sebagai
pelanggan_complete.
pelanggan_complete <- pelanggan[complete.cases(pelanggan), ]
nrow(pelanggan)
## [1] 11
nrow(pelanggan_complete)
## [1] 8
Persentase data yang hilang jika strategi tersebut diterapkan dapat dihitung sebagai berikut.
round((1 - nrow(pelanggan_complete) / nrow(pelanggan)) * 100, 2)
## [1] 27.27
Nilai persentase tersebut menunjukkan konsekuensi penghapusan baris; semakin besar angkanya, semakin besar pula risiko kehilangan informasi yang masih dapat dimanfaatkan.
Pilihan lain yang dapat mempertahankan lebih banyak data adalah imputasi, yaitu mengganti nilai yang hilang dengan suatu estimasi. Dua pendekatan umum yang dibandingkan adalah mean dan median.
Karena variabel pendapatan mengandung nilai ekstrem (outlier), mean dan median perlu dibandingkan terlebih dahulu untuk memilih nilai pengganti yang paling representatif.
mean_pendapatan <- mean(pelanggan$pendapatan, na.rm = TRUE)
median_pendapatan <- median(pelanggan$pendapatan, na.rm = TRUE)
mean_pendapatan
## [1] 59900000
median_pendapatan
## [1] 4900000
Perbedaan keduanya cukup besar, yang menunjukkan adanya pengaruh outlier terhadap mean. Oleh sebab itu, median dipilih karena relatif lebih tahan terhadap nilai ekstrem.
pelanggan$pendapatan_imputasi <- pelanggan$pendapatan
pelanggan$pendapatan_imputasi[is.na(pelanggan$pendapatan_imputasi)] <-
median_pendapatan
Metode yang sama juga diterapkan pada kolom usia agar pendekatan imputasinya tetap konsisten.
median_usia <- median(pelanggan$usia, na.rm = TRUE)
pelanggan$usia_imputasi <- pelanggan$usia
pelanggan$usia_imputasi[is.na(pelanggan$usia_imputasi)] <- median_usia
Nilai sebelum dan sesudah imputasi dapat dibandingkan secara berdampingan melalui tampilan berikut.
pelanggan[, c("customer_id", "usia", "usia_imputasi",
"pendapatan", "pendapatan_imputasi")]
## customer_id usia usia_imputasi pendapatan pendapatan_imputasi
## 1 C001 21 21.0 4.5e+06 4.5e+06
## 2 C002 25 25.0 NA 4.9e+06
## 3 C003 23 23.0 5.2e+06 5.2e+06
## 4 C004 50 50.0 4.8e+06 4.8e+06
## 5 C005 27 27.0 4.9e+06 4.9e+06
## 6 C006 NA 27.5 5.1e+06 5.1e+06
## 7 C007 31 31.0 5.0e+08 5.0e+08
## 8 C008 29 29.0 4.7e+06 4.7e+06
## 9 C009 22 22.0 4.6e+06 4.6e+06
## 10 C010 35 35.0 5.3e+06 5.3e+06
## 11 C011 28 28.0 NA 4.9e+06
Variabel kategorik seperti kota tidak selalu tepat jika
langsung diisi dengan modus karena dapat menghasilkan asumsi lokasi yang
belum tentu benar. Karena itu, nilai yang hilang diberi label eksplisit
“Tidak diketahui” agar status missing tetap dapat ditelusuri.
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
Imputasi dapat menghilangkan jejak bahwa suatu nilai sebelumnya merupakan data hilang. Untuk mempertahankan informasi tersebut, dibuat sebuah indikator biner.
pelanggan$pendapatan_missing <- as.integer(is.na(pelanggan$pendapatan))
table(pelanggan$pendapatan_missing)
##
## 0 1
## 9 2
Nilai numerik saja belum tentu cukup untuk melihat dampak imputasi. Histogram yang ditampilkan berdampingan dapat membantu mengevaluasi perubahan bentuk distribusi setelah pengisian missing value.
par(mfrow = c(1, 2))
hist(pelanggan$pendapatan,
main = "Sebelum Imputasi",
xlab = "Pendapatan", col = "skyblue", breaks = 8)
hist(pelanggan$pendapatan_imputasi,
main = "Sesudah Imputasi Median",
xlab = "Pendapatan", col = "lightgreen", breaks = 8)
par(mfrow = c(1, 1))
Histogram menunjukkan bahwa imputasi membuat sebagian nilai terkonsentrasi di sekitar median sehingga variasi data sedikit berkurang. Karena itu, evaluasi tetap perlu dilakukan meskipun seluruh NA sudah terisi.
Sebelum menentukan batas outlier secara matematis, boxplot dapat digunakan untuk memperoleh gambaran visual mengenai nilai ekstrem pada pendapatan.
boxplot(pelanggan$pendapatan_imputasi,
horizontal = TRUE,
col = "lightblue",
main = "Boxplot Pendapatan",
xlab = "Pendapatan")
Untuk menentukan batas outlier secara lebih objektif, metode IQR (Interquartile Range) digunakan dalam menghitung batas bawah dan batas atas.
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
Setiap nilai yang berada di luar rentang [batas_bawah, batas_atas] ditandai sebagai kandidat outlier melalui sebuah kolom logika.
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
Keberadaan outlier tidak selalu berarti data harus dihapus. Setidaknya ada tiga kemungkinan penyebab yang perlu dipertimbangkan terlebih dahulu:
Pada kasus ini outlier tetap dipertahankan. Namun, untuk melihat pengaruh metode lain, teknik winsorizing digunakan sebagai perbandingan dengan cara membatasi nilai ekstrem tanpa menghapus observasinya.
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
Perlu diperhatikan bahwa winsorizing pada bagian ini hanya merupakan demonstrasi tambahan. Metode tersebut sebaiknya digunakan sesuai tujuan analisis dan selalu didokumentasikan agar nilai yang telah dibatasi tidak dianggap sebagai nilai asli.
Metode pertama untuk mentransformasi variabel numerik adalah normalisasi min–maks, yang mengubah nilai ke dalam rentang \([0,1]\) dengan 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
Jika min–maks menghasilkan rentang [0,1], z-score menunjukkan posisi suatu nilai terhadap 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
Metode ketiga adalah decimal scaling. Prinsipnya cukup sederhana, yaitu membagi nilai dengan pangkat 10 tertentu hingga hasilnya 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 tersebut menghasilkan skala numerik yang berbeda meskipun diterapkan pada variabel yang sama. Menampilkannya berdampingan memudahkan perbandingan hasil transformasi.
transformasi <- pelanggan[, c(
"customer_id", "pendapatan_imputasi", "pendapatan_minmax",
"pendapatan_z", "pendapatan_decimal"
)]
transformasi
## customer_id pendapatan_imputasi pendapatan_minmax pendapatan_z
## 1 C001 4.5e+06 0.0000000000 -0.3041235
## 2 C002 4.9e+06 0.0008072654 -0.3014440
## 3 C003 5.2e+06 0.0014127144 -0.2994343
## 4 C004 4.8e+06 0.0006054490 -0.3021138
## 5 C005 4.9e+06 0.0008072654 -0.3014440
## 6 C006 5.1e+06 0.0012108981 -0.3001042
## 7 C007 5.0e+08 1.0000000000 3.0151095
## 8 C008 4.7e+06 0.0004036327 -0.3027837
## 9 C009 4.6e+06 0.0002018163 -0.3034536
## 10 C010 5.3e+06 0.0016145308 -0.2987645
## 11 C011 4.9e+06 0.0008072654 -0.3014440
## pendapatan_decimal
## 1 0.0045
## 2 0.0049
## 3 0.0052
## 4 0.0048
## 5 0.0049
## 6 0.0051
## 7 0.5000
## 8 0.0047
## 9 0.0046
## 10 0.0053
## 11 0.0049
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 yang jauh dari garis diagonal merah memperlihatkan pengaruh outlier dalam membuat sebagian besar nilai min–maks berada pada rentang yang sempit. Karena itu, pemeriksaan outlier sebaiknya dilakukan sebelum menentukan metode transformasi.
Sebelum kedua tabel digabungkan, kolom kunci penghubung, yaitu
customer_id pada data pelanggan dan cust_id
pada data transaksi, perlu diperiksa agar tidak mengandung duplikasi dan
dapat dipasangkan dengan benar.
sum(duplicated(pelanggan$customer_id))
## [1] 0
sum(duplicated(transaksi_raw$cust_id))
## [1] 0
Selanjutnya, perlu diketahui pelanggan mana yang belum memiliki data transaksi.
setdiff(pelanggan$customer_id, transaksi_raw$cust_id)
## [1] "C011"
Sebaliknya, perlu diperiksa apakah terdapat transaksi yang berasal dari pelanggan yang tidak tercatat pada tabel pelanggan.
setdiff(transaksi_raw$cust_id, pelanggan$customer_id)
## [1] "C012"
Kedua hasil setdiff() tersebut menunjukkan bahwa relasi
kedua tabel tidak sepenuhnya 1-ke-1: terdapat pelanggan tanpa transaksi
dan terdapat transaksi yang tidak memiliki pasangan pelanggan yang
valid.
Agar merge() dapat menggunakan kunci yang sama, kolom
cust_id pada tabel transaksi terlebih dahulu diubah namanya
menjadi customer_id, mengikuti tabel pelanggan.
transaksi <- transaksi_raw
names(transaksi)[names(transaksi) == "cust_id"] <- "customer_id"
Setelah nama kolom kunci diseragamkan, kedua dataset dapat
digabungkan 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
Hasil join perlu divalidasi dan tidak langsung dianggap benar. Beberapa pemeriksaan berikut digunakan untuk memastikan proses penggabungan berjalan sesuai harapan.
Apakah jumlah baris mengalami perubahan setelah proses penggabungan?
c(sebelum = nrow(pelanggan), sesudah = nrow(data_terintegrasi))
## sebelum sesudah
## 11 11
Apakah customer_id masih unik setelah penggabungan, atau
muncul duplikasi baru akibat relasi many-to-many?
sum(duplicated(data_terintegrasi$customer_id))
## [1] 0
Apakah terdapat missing value baru akibat pelanggan yang tidak memiliki pasangan transaksi?
colSums(is.na(data_terintegrasi[, c("jumlah_transaksi", "total_purchase")]))
## jumlah_transaksi total_purchase
## 1 1
Pelanggan mana saja yang tidak mempunyai pasangan transaksi tersebut?
data_terintegrasi[is.na(data_terintegrasi$jumlah_transaksi),
c("customer_id", "nama")]
## customer_id nama
## 11 C011 Kiki
Hal ini perlu diperhatikan karena NA pada kolom transaksi setelah join dapat memiliki dua arti yang berbeda.
NA hanya boleh diubah menjadi 0 jika telah dipastikan bahwa artinya memang tidak ada transaksi. Tanpa verifikasi, penggantian tersebut dapat menutupi masalah pada proses 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)
] <- 0
Dari seluruh variabel yang terbentuk selama preprocessing, termasuk
variabel perantara seperti usia, kota, dan
pendapatan mentah, hanya variabel hasil yang diperlukan
yang dipertahankan dalam dataset final dengan nama yang lebih
sederhana.
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
Seperti pemeriksaan awal, dataset yang telah melalui seluruh tahapan preprocessing perlu diaudit kembali untuk memastikan missing value dan duplikasi telah ditangani.
Fungsi audit_data() yang telah dibuat pada Bagian I
digunakan kembali pada tahap ini:
audit_akhir <- audit_data(data_final)
audit_akhir
## atribut tipe jumlah_missing persen_missing jumlah_unik
## 1 customer_id character 0 0 11
## 2 nama character 0 0 11
## 3 usia numeric 0 0 11
## 4 kota character 0 0 4
## 5 status character 0 0 2
## 6 pendapatan_imputasi numeric 0 0 9
## 7 pendapatan_missing integer 0 0 2
## 8 outlier_pendapatan logical 0 0 2
## 9 usia_minmax numeric 0 0 11
## 10 pendapatan_minmax numeric 0 0 9
## 11 jumlah_transaksi_final numeric 0 0 9
## 12 total_purchase_final numeric 0 0 11
Selanjutnya dilakukan pemeriksaan duplikasi pada
customer_id:
sum(duplicated(data_final$customer_id))
## [1] 0
Terakhir, jumlah nilai hilang pada setiap kolom dirangkum sebagai berikut:
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
Untuk mengevaluasi dampak preprocessing secara keseluruhan, beberapa indikator utama dari data mentah dan data akhir dibandingkan 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 tahap terakhir, dataset final dan log seluruh perubahan disimpan dalam format CSV sehingga dapat digunakan kembali untuk analisis berikutnya tanpa mengulang preprocessing dari awal.
write.csv(data_final,
"data_pelanggan_setelah_preprocessing.csv",
row.names = FALSE)
write.csv(log_perubahan,
"log_perubahan_preprocessing.csv",
row.names = FALSE)
Secara umum, tahapan preprocessing yang dilakukan dalam praktikum ini dapat dirangkum sebagai berikut:
Kualitas hasil preprocessing pada akhirnya ditentukan oleh kemampuan analis memahami konteks data dan tujuan analisis. R berperan sebagai alat untuk menjalankan keputusan tersebut, bukan sebagai penentu keputusan.
Han, J., Kamber, M., & Pei, J. (2012). Data Mining: Concepts and Techniques (3rd ed.). Morgan Kaufmann. Chapter 3: Data Preprocessing.