Nama : Christya Ruth Sellomitha Lubis
NIM : 2403113670
Kelas : Statistika B
Mata Kuliah : Data Mining
Tanggal : 25 August 2026
Topik Tugas : Data Preprocessing menggunakan
RStudio
Tugas ini berisi tahapan data preprocessing yang saya kerjakan menggunakan RStudio, mulai dari eksplorasi data awal, audit kualitas data, pembersihan data (data cleaning), penanganan missing value, penanganan outlier, transformasi data, hingga integrasi dua sumber data yang berbeda. Data yang digunakan adalah data simulasi pelanggan dan data transaksi, yang sengaja dibuat mengandung berbagai masalah kualitas data (nilai hilang, duplikasi, inkonsistensi kategori, dan nilai tidak wajar) agar dapat dipraktikkan proses pembersihannya secara menyeluruh.
Seluruh proses dikerjakan menggunakan bahasa pemrograman R versi 4.4.1 pada RStudio.
Berikut adalah data awal yang saya gunakan. Data
pelanggan_raw berisi informasi identitas dan atribut
demografis pelanggan, sedangkan transaksi_raw berisi
ringkasan transaksi tiap 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
)
pelanggan_rawDapat dilihat sekilas bahwa data pelanggan_raw
mengandung beberapa masalah, misalnya customer_id “C010”
tercatat dua kali, nilai usia sebesar 150 tahun yang tidak
masuk akal, pendapatan sebesar Rp500.000.000 yang jauh di
luar rentang wajar, serta penulisan kota dan
status yang tidak konsisten (huruf besar/kecil, spasi
berlebih, singkatan).
## [1] 12 6
## [1] "customer_id" "nama" "usia" "pendapatan" "kota"
## [6] "status"
## '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" ...
## 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
##
##
##
##
Dari str() dan summary() terlihat bahwa
kolom usia dan pendapatan bertipe numerik,
sedangkan customer_id, nama,
kota, dan status bertipe karakter. Nilai
maksimum usia (150) dan pendapatan (500 juta)
langsung terlihat tidak wajar dibandingkan nilai kuartil lainnya.
Sebelum melakukan pembersihan, saya terlebih dahulu mengaudit kualitas data secara menyeluruh: jumlah dan persentase missing value, duplikasi data, kategori yang tidak konsisten, serta rentang nilai atribut numerik.
## customer_id nama usia pendapatan kota status
## 0 0 1 2 1 0
## customer_id nama usia pendapatan kota status
## 0.00 0.00 8.33 16.67 8.33 0.00
## [1] 1
## [1] 1
## [1] " PKU" "Dumai" "DUMAI" "pekanbaru" "Pekanbaru"
## [6] "PEKANBARU" "Pekanbaru " "PKU" "Siak"
## [1] "A" "ACTIVE" "aktif" "Aktif" "AKTIF"
## [6] "nonaktif" "Tidak aktif" "Tidak Aktif"
Terlihat bahwa atribut kota memiliki variasi penulisan
untuk kota yang sama (misalnya “PKU”, ” PKU”, “Pekanbaru”, “PEKANBARU”,
“pekanbaru”, “Pekanbaru” yang seharusnya merujuk ke kota yang sama).
Atribut status juga memiliki masalah serupa (misalnya “A”,
“Aktif”, “aktif”, “AKTIF” yang seharusnya sama-sama berarti
“Aktif”).
## [1] 21 150
## [1] 4.5e+06 5.0e+08
Rentang usia 21–150 tahun dan rentang pendapatan Rp4.500.000–Rp500.000.000 mengindikasikan adanya nilai ekstrem yang perlu diperiksa lebih lanjut, apakah termasuk kesalahan input atau outlier yang sah.
Agar audit dapat dilakukan berulang kali secara konsisten (misalnya
untuk membandingkan kondisi data sebelum dan sesudah
preprocessing), saya membuat fungsi
audit_data().
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_awalRingkasan temuan audit awal: data mengandung 1 baris
duplikat penuh, 1 customer_id yang berulang, missing
value pada atribut usia (8,33%),
pendapatan (16,67%), dan kota (8,33%), serta
kategori teks yang tidak konsisten pada kota (10 kategori
unik, seharusnya 4) dan status (8 kategori unik, seharusnya
2).
Langkah pertama adalah menyalin data mentah ke variabel kerja
pelanggan, kemudian membersihkan spasi berlebih dan
menyeragamkan huruf menjadi kecil agar proses pencocokan kategori lebih
mudah.
pelanggan <- pelanggan_raw
# 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"
## [1] "a" "active" "aktif" "nonaktif" "tidak aktif"
Setelah spasi dan kapitalisasi diseragamkan, jumlah variasi penulisan sudah berkurang. Selanjutnya saya memetakan setiap variasi ke kategori baku yang sesuai.
# 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"
## [1] "Aktif" "Tidak Aktif"
Atribut kota sekarang hanya memiliki 4 kategori baku
(Pekanbaru, Dumai, Siak, dan NA), sedangkan
status hanya memiliki 2 kategori baku (Aktif dan Tidak
Aktif).
# Menampilkan seluruh baris dengan customer_id yang berulang
pelanggan[duplicated(pelanggan$customer_id) |
duplicated(pelanggan$customer_id, fromLast = TRUE), ]Karena kedua baris dengan customer_id “C010” memiliki
nilai yang identik pada seluruh atribut, saya mempertahankan kemunculan
pertama saja dan menghapus baris duplikatnya.
# Mempertahankan kemunculan pertama untuk customer_id yang identik
pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL
dim(pelanggan)## [1] 11 6
Menggunakan aturan bisnis bahwa usia pelanggan yang wajar berada pada
rentang 15–100 tahun, saya memeriksa apakah ada nilai usia
di luar rentang tersebut.
# Kandidat usia tidak valid berdasarkan aturan bisnis 15–100 tahun
pelanggan[pelanggan$usia < 15 | pelanggan$usia > 100, ]Pelanggan “C004” (Dodi) tercatat berusia 150 tahun. Setelah
dikonfirmasi ke sumber data asli, nilai yang benar adalah 50 tahun
(kemungkinan kesalahan input berupa angka tambahan di depan). Tidak
ditemukan nilai pendapatan negatif.
Setiap perubahan yang dilakukan terhadap data saya catat dalam sebuah log agar proses preprocessing tetap dapat ditelusuri (traceable).
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## customer_id nama usia pendapatan kota status
## 0 0 1 2 1 0
## [1] 11
## [1] 8
## [1] 27.27
Jika baris yang mengandung missing value langsung dihapus (listwise deletion), sekitar 27,27% data akan hilang. Karena proporsi ini cukup besar untuk jumlah data yang sedikit, saya memilih untuk melakukan imputasi dibandingkan menghapus baris tersebut.
Karena atribut pendapatan memiliki nilai ekstrem
(Rp500.000.000), saya menggunakan median sebagai nilai
imputasi karena median lebih tahan (robust) terhadap outlier
dibandingkan rata-rata.
mean_pendapatan <- mean(pelanggan$pendapatan, na.rm = TRUE)
median_pendapatan <- median(pelanggan$pendapatan, na.rm = TRUE)
mean_pendapatan## [1] 59900000
## [1] 4900000
pelanggan$pendapatan_imputasi <- pelanggan$pendapatan
pelanggan$pendapatan_imputasi[is.na(pelanggan$pendapatan_imputasi)] <-
median_pendapatanTerlihat jelas bahwa rata-rata (Rp59.900.000) jauh lebih besar dibanding median (Rp4.900.000) akibat pengaruh nilai ekstrem milik “C007”, sehingga median menjadi pilihan imputasi yang lebih tepat.
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 atribut kategorik kota yang bersifat nominal, saya
tidak menggunakan median/rata-rata melainkan kategori baru “Tidak
diketahui” agar informasi bahwa data tersebut memang hilang tetap
tersimpan.
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
Saya juga menambahkan kolom indikator biner untuk menandai baris mana
saja yang sebelumnya memiliki nilai pendapatan yang hilang,
agar informasi ini tidak sepenuhnya hilang setelah imputasi.
pelanggan$pendapatan_missing <- as.integer(is.na(pelanggan$pendapatan))
table(pelanggan$pendapatan_missing)##
## 0 1
## 9 2
par(mfrow = c(1, 2))
hist(pelanggan$pendapatan,
main = "Sebelum Imputasi",
xlab = "Pendapatan", col = "#F6D186", breaks = 8)
hist(pelanggan$pendapatan_imputasi,
main = "Sesudah Imputasi Median",
xlab = "Pendapatan", col = "#FCE8B8", breaks = 8)boxplot(pelanggan$pendapatan_imputasi,
horizontal = TRUE,
col = "#F6D186",
main = "Boxplot Pendapatan",
xlab = "Pendapatan")Untuk mendeteksi outlier secara objektif, saya menggunakan metode IQR (Interquartile Range) dengan batas 1,5 kali IQR di bawah Q1 dan di atas Q3.
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
pelanggan$outlier_pendapatan <-
pelanggan$pendapatan_imputasi < batas_bawah |
pelanggan$pendapatan_imputasi > batas_atas
pelanggan[pelanggan$outlier_pendapatan,
c("customer_id", "nama", "pendapatan_imputasi")]Berdasarkan metode IQR, hanya pelanggan “C007” (Gilang) yang
teridentifikasi sebagai outlier pada atribut
pendapatan, dengan nilai Rp500.000.000 yang jauh melampaui
batas atas Rp5.750.000.
Karena nilai outlier tersebut kemungkinan bukan murni kesalahan input (bisa jadi pelanggan dengan pendapatan sangat tinggi), saya tidak menghapusnya, melainkan menggunakan teknik winsorizing, yaitu memotong nilai ekstrem agar tidak melebihi batas atas/bawah IQR.
pelanggan$pendapatan_winsor <- pmin(
pmax(pelanggan$pendapatan_imputasi, batas_bawah),
batas_atas
)
pelanggan[pelanggan$outlier_pendapatan,
c("customer_id", "pendapatan_imputasi", "pendapatan_winsor")]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")]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)Pada hasil Z-score, pelanggan “C007” memiliki nilai Z pendapatan sebesar 3,015, jauh di atas pelanggan lain, sekali lagi menegaskan bahwa nilai tersebut adalah outlier.
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"
)]
transformasiKetiga teknik normalisasi menghasilkan skala yang berbeda-beda, namun ketiganya konsisten menunjukkan bahwa nilai pendapatan “C007” berada jauh di luar sebaran data pelanggan lainnya.
pelanggan$pendapatan_winsor_minmax <- minmax(pelanggan$pendapatan_winsor)
plot(pelanggan$pendapatan_minmax,
pelanggan$pendapatan_winsor_minmax,
pch = 19, col = "#D9A441",
xlab = "Min-maks Data Asli",
ylab = "Min-maks Data Winsorized",
main = "Dampak Penanganan Outlier terhadap Normalisasi")
abline(0, 1, col = "#4A4A4A", lty = 2)Titik-titik yang menyimpang dari garis diagonal menunjukkan bagaimana proses winsorizing mengubah hasil normalisasi min-max, khususnya untuk data yang sebelumnya menjadi outlier.
Sebelum menggabungkan data pelanggan dan
transaksi_raw, saya memastikan kedua tabel tidak memiliki
duplikasi pada kolom kunci, serta memeriksa kecocokan
customer_id antar tabel.
## [1] 0
## [1] 0
## [1] "C011"
## [1] "C012"
Pelanggan “C011” tidak memiliki catatan transaksi, sedangkan “C012” pada data transaksi tidak memiliki data pelanggan yang sesuai (kemungkinan pelanggan baru yang belum tercatat di sistem pelanggan).
Saya menggunakan left join (all.x = TRUE)
agar seluruh data pelanggan tetap dipertahankan meskipun tidak memiliki
pasangan transaksi.
transaksi <- transaksi_raw
names(transaksi)[names(transaksi) == "cust_id"] <- "customer_id"
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
## 11 11
## [1] 0
Jumlah baris tidak berubah (tetap 11) dan customer_id
tetap unik, artinya proses merge tidak menghasilkan
duplikasi data.
# Missing value baru akibat tidak ditemukan pasangan transaksi
colSums(is.na(data_terintegrasi[, c("jumlah_transaksi", "total_purchase")]))## jumlah_transaksi total_purchase
## 1 1
# Pelanggan tanpa pasangan transaksi
data_terintegrasi[is.na(data_terintegrasi$jumlah_transaksi),
c("customer_id", "nama")]Karena telah dikonfirmasi bahwa nilai NA pada hasil
integrasi berarti pelanggan tersebut belum pernah bertransaksi, saya
mengisi nilai tersebut dengan 0, bukan dengan median atau rata-rata.
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)
] <- 0data_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## [1] 0
## 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
Setelah seluruh tahapan preprocessing, dataset akhir sudah
tidak lagi memiliki missing value, duplikasi
customer_id, maupun inkonsistensi kategori.
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))
)
)
perbandinganRingkasan hasil akhir: jumlah baris berkurang dari
12 menjadi 11 (setelah menghapus 1 duplikat), duplikasi
customer_id berkurang dari 1 menjadi 0, total missing
value berkurang dari 4 menjadi 0, kategori kota yang
semula 10 variasi menjadi 4 kategori baku, dan kategori
status yang semula 8 variasi menjadi 2 kategori baku.
Melalui tugas ini, saya mempraktikkan tahapan lengkap data
preprocessing menggunakan RStudio, mulai dari audit kualitas data,
pembersihan teks dan duplikasi, koreksi nilai di luar domain, imputasi
missing value dengan mempertimbangkan tipe data (numerik vs
kategorik), deteksi dan penanganan outlier dengan metode IQR
dan winsorizing, transformasi data dengan tiga teknik normalisasi
(min-max, Z-score, dan decimal scaling), hingga integrasi dua sumber
data yang berbeda menggunakan merge(). Setiap keputusan
pembersihan data saya dasarkan pada karakteristik data itu sendiri
(misalnya median dipilih untuk mengatasi skewness akibat
outlier, sementara kategori “Tidak diketahui” dipilih untuk data
kategorik yang hilang) dan seluruh perubahan dicatat dalam log agar
proses tetap dapat ditelusuri.
Tugas Mata Kuliah Data Mining - Christya Ruth Sellomitha Lubis (2403113670) - Statistika B