Data preprocessing menjadi salah satu tahapan yang penting dalam data mining karena data yang diperoleh pada awalnya belum tentu berada dalam kondisi yang siap dianalisis. Pada data mentah dapat ditemukan berbagai permasalahan, seperti missing value, data ganda, perbedaan penulisan kategori, nilai yang tidak sesuai dengan aturan, serta nilai yang terlalu jauh dari pengamatan lainnya (outlier).
Pada tugas ini, tahapan yang digunakan mengacu pada materi Data Preprocessing Menggunakan RStudio. Prosesnya dimulai dengan melihat kondisi awal data, dilanjutkan dengan data cleaning, penanganan missing values, pemeriksaan outlier, transformasi variabel numerik, penggabungan dua sumber data, serta pemeriksaan kembali data setelah preprocessing. Seluruh proses dikerjakan menggunakan fungsi dasar R sehingga tidak membutuhkan package tambahan.
Setelah mengikuti praktikum ini, beberapa kemampuan yang diharapkan dapat diperoleh adalah:
Perintah berikut digunakan untuk melihat lokasi direktori kerja serta mengetahui versi R yang sedang digunakan.
getwd()
## [1] "C:/Users/User/Downloads"
R.version.string
## [1] "R version 4.5.1 (2025-06-13 ucrt)"
Pada bagian ini digunakan 10 data pelanggan mentah
yang sengaja dibuat mengandung beberapa permasalahan agar setiap tahap
preprocessing dapat diterapkan. Permasalahan tersebut berupa satu data
customer_id yang berulang, missing value,
penulisan kategori yang belum seragam, nilai usia yang tidak sesuai,
serta nilai pendapatan yang jauh lebih besar dibandingkan data
lainnya.
pelanggan_raw <- data.frame(
customer_id = c("C001", "C002", "C003", "C004", "C005",
"C006", "C007", "C008", "C009", "C010"),
nama = c("Ani", "Budi", "Citra", "Dodi", "Eka",
"Fani", "Gilang", "Hana", "Indra", "Joko"),
usia = c(21, 25, 23, 150, 27, NA, 31, 29, 22, 35),
pendapatan = c(4500000, NA, 5200000, 4800000, 4900000,
5100000, 500000000, 4700000, 4600000, NA),
kota = c("Pekanbaru", " PKU", "PEKANBARU", "Dumai", "pekanbaru",
"DUMAI", "Pekanbaru ", "Siak", "PKU", NA),
status = c("Aktif", "aktif", "ACTIVE", "A", "Tidak Aktif",
"nonaktif", "Aktif", "AKTIF", "A", "Tidak aktif"),
stringsAsFactors = FALSE
)
# Tambahkan satu baris duplikat untuk latihan deduplikasi.
pelanggan_raw <- rbind(
pelanggan_raw,
pelanggan_raw[pelanggan_raw$customer_id == "C010", ]
)
transaksi_raw <- data.frame(
cust_id = c("C001", "C002", "C003", "C004", "C005",
"C006", "C007", "C008", "C009", "C999"),
jumlah_transaksi = c(5, 3, 7, 2, 6, 4, 20, 5, 3, 5),
total_purchase = c(1500000, 900000, 2700000, 600000, 2100000,
1300000, 25000000, 1700000, 800000, 1200000),
stringsAsFactors = FALSE
)
pelanggan_raw
transaksi_raw
Langkah ini dilakukan untuk memperoleh gambaran awal mengenai ukuran data, nama variabel, tipe masing-masing variabel, beberapa observasi pertama, serta ringkasan statistiknya.
dim(pelanggan_raw)
## [1] 11 6
names(pelanggan_raw)
## [1] "customer_id" "nama" "usia" "pendapatan" "kota"
## [6] "status"
str(pelanggan_raw)
## 'data.frame': 11 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 NA ...
## $ kota : chr "Pekanbaru" " PKU" "PEKANBARU" "Dumai" ...
## $ status : chr "Aktif" "aktif" "ACTIVE" "A" ...
head(pelanggan_raw)
summary(pelanggan_raw)
## customer_id nama usia pendapatan
## Length:11 Length:11 Min. : 21.0 Min. : 4500000
## Class :character Class :character 1st Qu.: 23.5 1st Qu.: 4675000
## Mode :character Mode :character Median : 28.0 Median : 4850000
## Mean : 39.8 Mean : 66725000
## 3rd Qu.: 34.0 3rd Qu.: 5125000
## Max. :150.0 Max. :500000000
## NA's :1 NA's :3
## kota status
## Length:11 Length:11
## Class :character Class :character
## Mode :character Mode :character
##
##
##
##
# Banyak missing value per atribut
colSums(is.na(pelanggan_raw))
## customer_id nama usia pendapatan kota status
## 0 0 1 3 2 0
# Persentase missing value per atribut
round(colMeans(is.na(pelanggan_raw)) * 100, 2)
## customer_id nama usia pendapatan kota status
## 0.00 0.00 9.09 27.27 18.18 0.00
# Banyak baris duplikat penuh
sum(duplicated(pelanggan_raw))
## [1] 1
# Banyak customer_id yang berulang
sum(duplicated(pelanggan_raw$customer_id))
## [1] 1
# Kategori yang tercatat pada atribut kota dan status
sort(unique(pelanggan_raw$kota))
## [1] " PKU" "Dumai" "DUMAI" "pekanbaru" "Pekanbaru"
## [6] "PEKANBARU" "Pekanbaru " "PKU" "Siak"
sort(unique(pelanggan_raw$status))
## [1] "A" "ACTIVE" "aktif" "Aktif" "AKTIF"
## [6] "nonaktif" "Tidak aktif" "Tidak Aktif"
range(pelanggan_raw$usia, na.rm = TRUE)
## [1] 21 150
range(pelanggan_raw$pendapatan, na.rm = TRUE)
## [1] 4.5e+06 5.0e+08
Fungsi berikut digunakan untuk merangkum kondisi setiap atribut sehingga proses pemeriksaan kualitas data dapat dilakukan dengan lebih teratur.
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
pelanggan <- pelanggan_raw
Data awal masih menunjukkan perbedaan dalam penggunaan spasi serta huruf kapital. Oleh karena itu, spasi dibersihkan terlebih dahulu, kemudian seluruh teks diubah menjadi huruf kecil agar proses penyamaan kategori lebih mudah dilakukan.
# 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"
sort(unique(pelanggan$status))
## [1] "a" "active" "aktif" "nonaktif" "tidak aktif"
Setelah proses awal tersebut, setiap kategori kemudian disamakan ke dalam bentuk label yang seragam.
# 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"
sort(unique(pelanggan$status))
## [1] "Aktif" "Tidak Aktif"
Variabel customer_id berperan sebagai identitas unik
setiap pelanggan. Karena itu, data dengan customer_id yang
muncul lebih dari sekali perlu diperiksa terlebih dahulu.
# Menampilkan seluruh baris dengan customer_id yang berulang
pelanggan[duplicated(pelanggan$customer_id) |
duplicated(pelanggan$customer_id, fromLast = TRUE), ]
Salah satu baris C010 yang tercatat dua kali kemudian
dihilangkan sehingga setiap pelanggan hanya memiliki satu identitas dan
tersisa 9 pelanggan unik.
# Mempertahankan kemunculan pertama untuk customer_id yang identik
pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL
dim(pelanggan)
## [1] 10 6
Pemeriksaan domain dilakukan untuk menemukan nilai yang tidak sesuai dengan batas yang telah ditetapkan berdasarkan konteks data. Dalam contoh ini, rentang usia yang dianggap wajar adalah 15–100 tahun.
# Kandidat usia tidak valid berdasarkan aturan bisnis 15–100 tahun
pelanggan[pelanggan$usia < 15 | pelanggan$usia > 100, ]
Nilai pendapatan yang bernilai negatif juga diperiksa karena dapat menunjukkan adanya ketidaksesuaian dengan aturan data.
# Kandidat pendapatan tidak valid jika bernilai negatif
pelanggan[pelanggan$pendapatan < 0, ]
Usia pelanggan C004 yang tercatat sebesar 150 kemudian dikoreksi menjadi 50 dengan mengacu pada sumber data aslinya.
pelanggan$usia[pelanggan$customer_id == "C004"] <- 50
Setiap perubahan dicatat agar seluruh proses preprocessing dapat diketahui dan ditelusuri kembali.
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
colSums(is.na(pelanggan))
## customer_id nama usia pendapatan kota status
## 0 0 1 2 1 0
# Baris yang memiliki sedikitnya satu missing value
pelanggan[!complete.cases(pelanggan), ]
Sebagai salah satu contoh penanganan, baris yang tidak memiliki
missing value dapat dipilih menggunakan
complete.cases(). Selanjutnya jumlah data sebelum dan
sesudah proses tersebut dibandingkan untuk melihat dampaknya.
pelanggan_complete <- pelanggan[complete.cases(pelanggan), ]
nrow(pelanggan)
## [1] 10
nrow(pelanggan_complete)
## [1] 7
# Persentase baris yang terhapus
round((1 - nrow(pelanggan_complete) / nrow(pelanggan)) * 100, 2)
## [1] 30
Pada variabel numerik, median digunakan sebagai salah satu alternatif untuk mengisi nilai yang hilang. Nilai mean turut dihitung sehingga keduanya dapat dibandingkan sebelum menentukan metode yang digunakan.
mean_pendapatan <- mean(pelanggan$pendapatan, na.rm = TRUE)
median_pendapatan <- median(pelanggan$pendapatan, na.rm = TRUE)
mean_pendapatan
## [1] 66725000
median_pendapatan
## [1] 4850000
Selanjutnya dibuat variabel hasil imputasi untuk pendapatan dan usia, sementara nilai pada kolom aslinya tetap dipertahankan agar kondisi awal data masih dapat diketahui.
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")]
Nilai kota yang tidak tersedia diberi label
Tidak diketahui. Dengan cara ini, informasi bahwa nilai
tersebut memang tidak diketahui tetap dipertahankan dan tidak dianggap
sebagai bagian dari kategori tertentu.
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
## 2 6 1 1
Variabel indikator biner ditambahkan untuk menunjukkan apakah suatu nilai pendapatan pada kondisi awal merupakan missing value.
pelanggan$pendapatan_missing <- as.integer(is.na(pelanggan$pendapatan))
table(pelanggan$pendapatan_missing)
##
## 0 1
## 8 2
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))
Boxplot digunakan sebagai visualisasi awal untuk mengetahui adanya nilai pendapatan yang berpotensi menjadi nilai ekstrem.
boxplot(pelanggan$pendapatan_imputasi,
horizontal = TRUE,
col = "lightblue",
main = "Boxplot Pendapatan",
xlab = "Pendapatan")
Metode IQR menentukan kandidat outlier berdasarkan nilai Q1, Q3, dan IQR yang kemudian digunakan untuk memperoleh batas bawah serta 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%
## 4725000 5050000 325000 4237500 5537500
Pengamatan yang berada di luar batas IQR diberi tanda sebagai kandidat outlier. Namun, hasil penandaan tersebut tidak langsung dijadikan alasan untuk menghapus data.
pelanggan$outlier_pendapatan <-
pelanggan$pendapatan_imputasi < batas_bawah |
pelanggan$pendapatan_imputasi > batas_atas
pelanggan[pelanggan$outlier_pendapatan,
c("customer_id", "nama", "pendapatan_imputasi")]
Sebagai salah satu contoh perlakuan terhadap nilai ekstrem, nilai yang melewati batas dapat disesuaikan ke batas bawah atau batas atas melalui metode winsorization.
pelanggan$pendapatan_winsor <- pmin(
pmax(pelanggan$pendapatan_imputasi, batas_bawah),
batas_atas
)
pelanggan[pelanggan$outlier_pendapatan,
c("customer_id", "pendapatan_imputasi", "pendapatan_winsor")]
Transformasi dilakukan untuk menyesuaikan skala pada atribut numerik. Dalam praktikum ini digunakan tiga pendekatan, yaitu normalisasi min–maks, z-score, dan decimal scaling.
Untuk mengubah nilai ke dalam rentang [0,1], normalisasi min–maks menggunakan rumus berikut:
\[x' = \frac{x - x_{min}}{x_{max} - x_{min}}\]
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 z-score menyatakan suatu nilai berdasarkan posisinya terhadap rata-rata dan simpangan baku data.
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)
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
transformasi <- pelanggan[, c(
"customer_id", "pendapatan_imputasi", "pendapatan_minmax",
"pendapatan_z", "pendapatan_decimal"
)]
transformasi
Perbandingan berikut digunakan untuk melihat perubahan hasil normalisasi min–maks setelah dilakukan penanganan terhadap outlier.
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)
Sebelum kedua dataset digabungkan, identifier perlu diperiksa terlebih dahulu untuk memastikan proses integrasi tidak menyebabkan baris menjadi berlipat atau menghasilkan pasangan data yang keliru.
sum(duplicated(pelanggan$customer_id))
## [1] 0
sum(duplicated(transaksi_raw$cust_id))
## [1] 0
# Customer yang ada di pelanggan tetapi tidak ada di transaksi
setdiff(pelanggan$customer_id, transaksi_raw$cust_id)
## [1] "C010"
# Customer yang ada di transaksi tetapi tidak ada di pelanggan
setdiff(transaksi_raw$cust_id, pelanggan$customer_id)
## [1] "C999"
transaksi <- transaksi_raw
names(transaksi)[names(transaksi) == "cust_id"] <- "customer_id"
Fungsi merge(..., all.x = TRUE) digunakan agar seluruh
data pelanggan tetap dipertahankan, sedangkan informasi transaksi hanya
ditambahkan ketika memiliki customer_id yang sesuai.
data_terintegrasi <- merge(
pelanggan,
transaksi,
by = "customer_id",
all.x = TRUE
)
data_terintegrasi[, c(
"customer_id", "nama", "jumlah_transaksi", "total_purchase"
)]
# Apakah jumlah baris berubah secara tidak wajar?
c(sebelum = nrow(pelanggan), sesudah = nrow(data_terintegrasi))
## sebelum sesudah
## 10 10
# Apakah customer_id tetap unik?
sum(duplicated(data_terintegrasi$customer_id))
## [1] 0
# Missing value baru akibat tidak ditemukan pasangan transaksi
colSums(is.na(data_terintegrasi[, c("jumlah_transaksi", "total_purchase")]))
## jumlah_transaksi total_purchase
## 1 1
Data pelanggan yang tidak menemukan pasangan transaksi kemudian ditampilkan agar kondisi tersebut dapat diperiksa lebih lanjut.
data_terintegrasi[is.na(data_terintegrasi$jumlah_transaksi),
c("customer_id", "nama")]
Apabila hasil pemeriksaan menunjukkan bahwa NA
menandakan pelanggan memang belum melakukan transaksi, nilai tersebut
dapat diperlakukan sebagai 0.
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
Pada dataset akhir hanya variabel yang dianggap diperlukan untuk proses analisis selanjutnya yang dipertahankan.
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
audit_akhir <- audit_data(data_final)
audit_akhir
# Memeriksa kembali duplikasi identifier
sum(duplicated(data_final$customer_id))
## [1] 0
# Memeriksa kembali missing value
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
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
write.csv(data_final,
"data_pelanggan_setelah_preprocessing.csv",
row.names = FALSE)
write.csv(log_perubahan,
"log_perubahan_preprocessing.csv",
row.names = FALSE)
Dari seluruh proses yang telah dilakukan, dapat dilihat bahwa preprocessing berlangsung melalui beberapa tahap yang saling berkaitan. Proses dimulai dengan memeriksa struktur dan kualitas data, kemudian dilanjutkan dengan penyeragaman kategori, penanganan duplikasi dan kesalahan domain, pengelolaan missing values, pemeriksaan outlier, transformasi variabel numerik, integrasi data, serta pemeriksaan akhir terhadap hasil yang diperoleh.
Hal yang perlu diperhatikan dari proses ini adalah bahwa preprocessing tidak hanya berfokus pada penggunaan kode. Setiap perubahan pada data perlu didasarkan pada alasan yang jelas. Nilai outlier tidak selalu harus dihapus, missing value tidak selalu tepat jika langsung dibuang, dan hasil penggabungan data tetap perlu diperiksa agar tidak muncul duplikasi maupun permasalahan baru.
Data mentah
↓
Eksplorasi dan audit awal
↓
Standardisasi kategori
↓
Deduplikasi dan pemeriksaan domain
↓
Penanganan missing value
↓
Deteksi dan evaluasi outlier
↓
Transformasi data
↓
Integrasi data
↓
Audit akhir
↓
Data siap digunakan untuk analisis/data mining