| Komponen | Keterangan |
|---|---|
| Mata kuliah | Data Mining |
| Pertemuan | 03 |
| Topik | Data Preprocessing |
| Perangkat lunak | R dan RStudio |
| Mahasiswa | Nadya Destriani |
| NIM | 2403113644 |
Sebuah perusahaan e-commerce ingin melakukan analisis terhadap pelanggan. Data berasal dari dua sumber:
Data belum dapat langsung digunakan karena mengandung nilai hilang, kategori tidak konsisten, duplikasi, nilai ekstrem, dan perbedaan nama identifier. Mahasiswa diminta menyiapkan data tersebut sampai menjadi satu dataset analisis yang bersih.
praktikum_preprocessing.getwd()
## [1] "D:/TUGAS KULIAH/SEM 5/Data mining"
R.version.string
## [1] "R version 4.5.3 (2026-03-11 ucrt)"
Modul ini menggunakan fungsi dasar R sehingga dapat dijalankan tanpa memasang paket tambahan.
Jalankan kode berikut. Dataset sengaja dibuat “kotor” agar seluruh tahap preprocessing dapat dipraktikkan.
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_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 perubahan, periksa struktur, dimensi, tipe atribut, dan beberapa observasi awal.
dim(pelanggan_raw)
## [1] 12 6
names(pelanggan_raw)
## [1] "customer_id" "nama" "usia" "pendapatan" "kota"
## [6] "status"
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" ...
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
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
##
##
##
##
Data pelanggan pada kondisi awal terdiri dari 12 observasi dan 6
variabel. Beberapa masalah sudah terlihat dari struktur tersebut, yaitu
adanya NA pada usia dan pendapatan, usia 150 tahun,
pendapatan 500 juta, serta variasi penulisan kota dan status.
:contentReferenceoaicite:5
Enam dimensi kualitas data yang dibahas Han, Kamber, dan Pei meliputi accuracy, completeness, consistency, timeliness, believability, dan interpretability. Tidak semua dimensi dapat dihitung hanya dari isi tabel. Misalnya, accuracy memerlukan pembanding terhadap kondisi sebenarnya dan timeliness membutuhkan informasi waktu pemutakhiran.
Pemeriksaan awal berikut berfokus pada masalah yang dapat dideteksi dari dataset.
# Banyak missing value per atribut
colSums(is.na(pelanggan_raw))
## customer_id nama usia pendapatan kota status
## 0 0 1 2 1 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 8.33 16.67 8.33 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"
# Rentang atribut numerik
range(pelanggan_raw$usia, na.rm = TRUE)
## [1] 21 150
range(pelanggan_raw$pendapatan, na.rm = TRUE)
## [1] 4.5e+06 5.0e+08
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
Interpretasi: hasil audit bukan keputusan cleaning. Audit hanya memberi sinyal atribut mana yang perlu diperiksa lebih lanjut.
Data mentah dipertahankan sebagai arsip sehingga perubahan dilakukan pada objek baru.
pelanggan <- pelanggan_raw
Pendekatan ini memungkinkan kondisi sebelum preprocessing tetap dapat dibandingkan dengan kondisi setelah preprocessing.
# 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"
Standardisasi dilakukan menggunakan aturan domain. Dalam praktik nyata, aturan perlu dikonfirmasi melalui kamus data atau pemilik data.
# 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"
Dengan demikian, variasi aktif, ACTIVE, dan
A diperlakukan sebagai kategori yang sama. Demikian pula
Tidak aktif dan nonaktif diseragamkan menjadi
Tidak Aktif.
# Menampilkan seluruh baris dengan customer_id yang berulang
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
Ditemukan dua baris dengan customer_id C010. Karena
customer_id diasumsikan sebagai identifier unik untuk satu
pelanggan, kemunculan kedua dianggap sebagai duplikasi.
# Mempertahankan kemunculan pertama untuk customer_id yang identik
pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL
dim(pelanggan)
## [1] 11 6
Catatan: menghapus duplikasi hanya aman jika setiap customer_id memang harus mewakili satu pelanggan. Jika satu pelanggan boleh memiliki banyak baris, tindakan ini justru salah.
# Kandidat usia tidak valid berdasarkan aturan bisnis 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>
# Kandidat pendapatan tidak valid jika bernilai negatif
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>
pelanggan$usia[pelanggan$customer_id == "C004"] <- 50
Nilai usia 150 melanggar aturan domain. Misalkan pemeriksaan terhadap formulir asli menunjukkan bahwa nilai yang benar adalah 50.
Agar proses dapat ditelusuri, perubahan utama didokumentasikan.
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
colSums(is.na(pelanggan))
## customer_id nama usia pendapatan kota status
## 0 0 1 2 1 0
Untuk melihat baris yang memiliki minimal satu nilai hilang:
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
Pada kondisi setelah cleaning, missing value terdapat pada usia C006, pendapatan C002 dan C011, serta kota C011. Kondisi ini perlu ditangani berdasarkan jenis atribut dan karakteristik datanya.
Strategi complete case analysis ditunjukkan sebagai pembanding.
pelanggan_complete <- pelanggan[complete.cases(pelanggan), ]
cat("Jumlah data sebelum penghapusan :", nrow(pelanggan), "\n")
## Jumlah data sebelum penghapusan : 11
cat("Jumlah data setelah penghapusan :", nrow(pelanggan_complete), "\n")
## Jumlah data setelah penghapusan : 8
persen_hilang <- round(
(1 - nrow(pelanggan_complete) / nrow(pelanggan)) * 100,
2
)
cat("Persentase data yang hilang :", persen_hilang, "%\n")
## Persentase data yang hilang : 27.27 %
Pada data ini, menghapus seluruh baris yang tidak lengkap akan menghilangkan sebagian observasi. Karena ukuran dataset hanya kecil, penghapusan baris berpotensi menyebabkan kehilangan informasi yang cukup besar. Oleh karena itu, strategi ini tidak dipilih sebagai metode utama.
Pendapatan memiliki nilai ekstrem sebesar 500 juta sehingga mean dan median perlu dibandingkan.
mean_pendapatan <- mean(
pelanggan$pendapatan,
na.rm = TRUE
)
median_pendapatan <- median(
pelanggan$pendapatan,
na.rm = TRUE
)
cat("Mean pendapatan :", mean_pendapatan, "\n")
## Mean pendapatan : 59900000
cat("Median pendapatan :", median_pendapatan, "\n")
## Median pendapatan : 4900000
Mean sangat dipengaruhi oleh nilai 500 juta, sedangkan median relatif stabil. Oleh karena itu, median dipilih sebagai nilai imputasi pendapatan.
pelanggan$pendapatan_imputasi <- pelanggan$pendapatan
pelanggan$pendapatan_imputasi[
is.na(pelanggan$pendapatan_imputasi)
] <- median_pendapatan
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"
)
]
## 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
Median usia pada data ini adalah 27,5 sehingga usia C006 diisi dengan 27,5.
Untuk kota, nilai hilang tidak langsung diganti dengan modus. Sebagai
alternatif, digunakan kategori eksplisit
Tidak diketahui.
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
Pendekatan ini menjaga informasi bahwa nilai awal memang tidak tersedia. Dengan demikian, ketidakpastian tidak disamarkan sebagai seolah-olah merupakan kategori yang benar-benar diketahui.
Untuk pendapatan, dibuat variabel indikator yang menunjukkan apakah nilai awalnya hilang.
pelanggan$pendapatan_missing <-
as.integer(is.na(pelanggan$pendapatan))
table(pelanggan$pendapatan_missing)
##
## 0 1
## 9 2
Nilai:
0 = pendapatan tersedia pada data awal;1 = pendapatan pada data awal merupakan missing
value.par(mfrow = c(1, 2))
hist(
pelanggan$pendapatan,
main = "Sebelum Imputasi",
xlab = "Pendapatan",
breaks = 8
)
hist(
pelanggan$pendapatan_imputasi,
main = "Sesudah Imputasi Median",
xlab = "Pendapatan",
breaks = 8
)
par(mfrow = c(1, 1))
Perbandingan distribusi digunakan untuk memastikan bahwa imputasi
tidak dilakukan secara buta. Menghilangkan seluruh NA belum
tentu berarti kualitas data otomatis meningkat karena imputasi dapat
mengubah distribusi dan variasi data.
boxplot(
pelanggan$pendapatan_imputasi,
horizontal = TRUE,
main = "Boxplot Pendapatan",
xlab = "Pendapatan"
)
Boxplot menunjukkan adanya nilai yang sangat jauh dari sebagian besar observasi.
q1 <- quantile(
pelanggan$pendapatan_imputasi,
0.25,
na.rm = TRUE
)
q3 <- quantile(
pelanggan$pendapatan_imputasi,
0.75,
na.rm = TRUE
)
iqr <- IQR(
pelanggan$pendapatan_imputasi,
na.rm = TRUE
)
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
Berdasarkan aturan IQR, nilai di luar interval:
\[ [Q_1 - 1.5(IQR),\ Q_3 + 1.5(IQR)] \]
ditandai sebagai kandidat outlier.
pelanggan$outlier_pendapatan <-
!is.na(pelanggan$pendapatan_imputasi) &
(
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
Hasilnya menunjukkan C007 sebagai kandidat outlier dengan pendapatan 500 juta. :contentReferenceoaicite:6
Outlier tidak langsung dihapus. Ada beberapa kemungkinan interpretasi:
Dalam kasus ini, tidak terdapat bukti bahwa pendapatan C007 merupakan kesalahan input. Oleh karena itu, nilai tersebut dipertahankan pada data utama dan hanya dibuat versi winsorized sebagai pembanding.
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
Winsorization di sini digunakan sebagai demonstrasi analitis, bukan sebagai pengganti otomatis terhadap data asli.
Transformasi dilakukan agar atribut numerik dapat dibandingkan dalam skala yang lebih sesuai untuk metode data mining tertentu.
Normalisasi min–maks mengubah nilai menjadi rentang 0 sampai 1.
\[ x' = \frac{x-\min(x)} {\max(x)-\min(x)} \]
minmax <- function(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
Normalisasi min–maks membuat kedua atribut berada pada skala yang sama. Namun, metode ini sensitif terhadap nilai ekstrem karena nilai minimum dan maksimum menjadi penentu seluruh rentang.
Z-score menggunakan rumus:
\[ 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
Z-score menunjukkan posisi suatu observasi relatif terhadap rata-rata dalam satuan simpangan baku.
Decimal scaling dilakukan dengan membagi nilai dengan \(10^j\), dengan \(j\) dipilih sehingga nilai absolut maksimum menjadi kurang dari 1.
decimal_scaling <- function(x) {
max_abs <- max(
abs(x),
na.rm = TRUE
)
if (max_abs == 0) {
return(x)
}
j <- ceiling(log10(max_abs + 1))
x / (10^j)
}
pelanggan$usia_decimal <-
decimal_scaling(pelanggan$usia_imputasi)
pelanggan$pendapatan_decimal <-
decimal_scaling(pelanggan$pendapatan_imputasi)
pelanggan[
,
c(
"customer_id",
"pendapatan_imputasi",
"pendapatan_decimal"
)
]
## customer_id pendapatan_imputasi pendapatan_decimal
## 1 C001 4.5e+06 0.0045
## 2 C002 4.9e+06 0.0049
## 3 C003 5.2e+06 0.0052
## 4 C004 4.8e+06 0.0048
## 5 C005 4.9e+06 0.0049
## 6 C006 5.1e+06 0.0051
## 7 C007 5.0e+08 0.5000
## 8 C008 4.7e+06 0.0047
## 9 C009 4.6e+06 0.0046
## 10 C010 5.3e+06 0.0053
## 11 C011 4.9e+06 0.0049
# Membandingkan metode transformasi
transformasi <- pelanggan[
,
c(
"customer_id",
"pendapatan_imputasi",
"pendapatan_minmax",
"pendapatan_z",
"pendapatan_decimal"
)
]
# Membulatkan hanya kolom numerik
transformasi[] <- lapply(
transformasi,
function(x) {
if (is.numeric(x)) {
round(x, 4)
} else {
x
}
}
)
transformasi
## customer_id pendapatan_imputasi pendapatan_minmax pendapatan_z
## 1 C001 4.5e+06 0.0000 -0.3041
## 2 C002 4.9e+06 0.0008 -0.3014
## 3 C003 5.2e+06 0.0014 -0.2994
## 4 C004 4.8e+06 0.0006 -0.3021
## 5 C005 4.9e+06 0.0008 -0.3014
## 6 C006 5.1e+06 0.0012 -0.3001
## 7 C007 5.0e+08 1.0000 3.0151
## 8 C008 4.7e+06 0.0004 -0.3028
## 9 C009 4.6e+06 0.0002 -0.3035
## 10 C010 5.3e+06 0.0016 -0.2988
## 11 C011 4.9e+06 0.0008 -0.3014
## 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
Ketiga metode menghasilkan skala yang berbeda walaupun berasal dari atribut yang sama. Min–maks menghasilkan rentang 0–1, z-score berpusat pada rata-rata dengan simpangan baku sebagai skala, sedangkan decimal scaling hanya menggeser posisi desimal.
Untuk memperlihatkan dampak nilai ekstrem, dibandingkan hasil min–maks data asli dengan data yang telah di-winsorize.
pelanggan$pendapatan_winsor_minmax <-
minmax(pelanggan$pendapatan_winsor)
plot(
pelanggan$pendapatan_minmax,
pelanggan$pendapatan_winsor_minmax,
pch = 19,
xlab = "Min-maks Data Asli",
ylab = "Min-maks Data Winsorized",
main = "Dampak Outlier terhadap Normalisasi"
)
abline(
a = 0,
b = 1,
lty = 2
)
Nilai 500 juta menyebabkan sebagian besar pendapatan yang berada pada kisaran 4–5 juta menjadi sangat dekat dengan nol setelah min–max normalization. Ini menunjukkan bahwa deteksi outlier sebaiknya dipertimbangkan sebelum menentukan metode transformasi. :contentReferenceoaicite:7
Sebelum menggabungkan data, struktur identifier diperiksa.
cat(
"Jumlah customer_id unik pada pelanggan :",
length(unique(pelanggan$customer_id)),
"\n"
)
## Jumlah customer_id unik pada pelanggan : 11
cat(
"Jumlah cust_id unik pada transaksi :",
length(unique(transaksi_raw$cust_id)),
"\n"
)
## Jumlah cust_id unik pada transaksi : 11
cat(
"Customer tanpa transaksi :",
sum(
!pelanggan$customer_id %in%
transaksi_raw$cust_id
),
"\n"
)
## Customer tanpa transaksi : 1
cat(
"Transaksi tanpa data pelanggan :",
sum(
!transaksi_raw$cust_id %in%
pelanggan$customer_id
),
"\n"
)
## Transaksi tanpa data pelanggan : 1
Hasil pemeriksaan menunjukkan terdapat identifier C012
pada sumber transaksi yang tidak memiliki pasangan pada sumber
pelanggan.
Nama identifier pada kedua sumber berbeda:
customer_id;cust_id.Agar dapat digabungkan, nama identifier pada transaksi diselaraskan.
transaksi <- transaksi_raw
names(transaksi)[
names(transaksi) == "cust_id"
] <- "customer_id"
transaksi
## customer_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
Karena data pelanggan merupakan sumber utama, digunakan left
join melalui fungsi 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
Penggunaan all.x = TRUE memastikan seluruh pelanggan
tetap dipertahankan walaupun tidak memiliki pasangan transaksi. Hal ini
menghasilkan NA untuk transaksi C011.
:contentReferenceoaicite:8
c(
sebelum = nrow(pelanggan),
sesudah = nrow(data_terintegrasi)
)
## sebelum sesudah
## 11 11
Jumlah baris tidak seharusnya bertambah secara tidak wajar karena identifier pelanggan telah dibuat unik sebelum proses integrasi.
sum(
duplicated(data_terintegrasi$customer_id)
)
## [1] 0
colSums(
is.na(
data_terintegrasi[
,
c(
"jumlah_transaksi",
"total_purchase"
)
]
)
)
## jumlah_transaksi total_purchase
## 1 1
C011 tidak memiliki pasangan transaksi pada sumber kedua sehingga
menghasilkan NA. Kondisi tersebut berbeda dengan missing
value pada pendapatan: missing akibat integrasi memiliki makna bahwa
tidak ditemukan pasangan transaksi pada sumber transaksi.
Untuk latihan ini, diasumsikan bahwa tidak ditemukannya transaksi berarti pelanggan belum memiliki transaksi yang tercatat dalam sumber transaksi.
Oleh karena itu, NA pada jumlah transaksi dan total
pembelian diubah menjadi nol.
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
data_terintegrasi[
,
c(
"customer_id",
"jumlah_transaksi_final",
"total_purchase_final"
)
]
## customer_id jumlah_transaksi_final total_purchase_final
## 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 C011 0 0.0e+00
Keputusan mengisi nol harus dipahami sebagai keputusan berbasis makna
bisnis. Jika NA sebenarnya berarti “data transaksi belum
tersedia” dan bukan “tidak ada transaksi”, maka menggantinya dengan nol
akan menimbulkan bias.
Dataset akhir menyimpan atribut identitas, atribut yang telah dibersihkan, hasil imputasi, indikator missing, indikator outlier, hasil transformasi, dan atribut transaksi.
data_final <- data_terintegrasi[
,
c(
"customer_id",
"nama",
"kota_imputasi",
"status",
"usia_imputasi",
"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 kota status usia pendapatan_imputasi
## 1 C001 Ani Pekanbaru Aktif 21.0 4.5e+06
## 2 C002 Budi Pekanbaru Aktif 25.0 4.9e+06
## 3 C003 Citra Pekanbaru Aktif 23.0 5.2e+06
## 4 C004 Dodi Dumai Aktif 50.0 4.8e+06
## 5 C005 Eka Pekanbaru Tidak Aktif 27.0 4.9e+06
## 6 C006 Fani Dumai Tidak Aktif 27.5 5.1e+06
## 7 C007 Gilang Pekanbaru Aktif 31.0 5.0e+08
## 8 C008 Hana Siak Aktif 29.0 4.7e+06
## 9 C009 Indra Pekanbaru Aktif 22.0 4.6e+06
## 10 C010 Joko Dumai Tidak Aktif 35.0 5.3e+06
## 11 C011 Kiki Tidak diketahui Aktif 28.0 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
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 kota character 0 0 4
## 4 status character 0 0 2
## 5 usia numeric 0 0 11
## 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
Pemeriksaan khusus:
cat(
"Duplikasi customer_id :",
sum(duplicated(data_final$customer_id)),
"\n"
)
## Duplikasi customer_id : 0
cat(
"Total missing value :",
sum(is.na(data_final)),
"\n"
)
## Total missing value : 0
Dataset akhir diharapkan tidak memiliki missing value pada atribut
yang dipilih untuk analisis dan tidak memiliki identifier pelanggan yang
duplikat. Pada hasil yang menjadi acuan, audit akhir menunjukkan seluruh
atribut akhir memiliki nol missing value dan customer_id
tetap unik. :contentReferenceoaicite:9
perbandingan <- data.frame(
indikator = c(
"Jumlah baris",
"Jumlah kolom",
"Duplikasi customer_id",
"Total missing value",
"Kategori kota unik",
"Kategori status unik"
),
sebelum = c(
nrow(pelanggan_raw),
ncol(pelanggan_raw),
sum(duplicated(pelanggan_raw$customer_id)),
sum(is.na(pelanggan_raw)),
length(unique(na.omit(pelanggan_raw$kota))),
length(unique(na.omit(pelanggan_raw$status)))
),
sesudah = c(
nrow(data_final),
ncol(data_final),
sum(duplicated(data_final$customer_id)),
sum(is.na(data_final)),
length(unique(na.omit(data_final$kota))),
length(unique(na.omit(data_final$status)))
)
)
library(knitr)
kable(
perbandingan,
caption = "Perbandingan kondisi data sebelum dan sesudah preprocessing"
)
| indikator | sebelum | sesudah |
|---|---|---|
| Jumlah baris | 12 | 11 |
| Jumlah kolom | 6 | 12 |
| Duplikasi customer_id | 1 | 0 |
| Total missing value | 4 | 0 |
| Kategori kota unik | 9 | 4 |
| Kategori status unik | 8 | 2 |
Perbandingan tersebut menunjukkan bahwa tujuan preprocessing bukan sekadar mengurangi jumlah masalah secara mekanis. Yang lebih penting adalah memastikan bahwa perubahan tersebut menghasilkan data yang lebih konsisten dan sesuai dengan tujuan analisis.
write.csv(
data_final,
"data_pelanggan_setelah_preprocessing.csv",
row.names = FALSE
)
write.csv(
log_perubahan,
"log_perubahan_preprocessing.csv",
row.names = FALSE
)
Tidak. Dataset tanpa missing value belum tentu lebih berkualitas
karena nilai yang hilang dapat diganti dengan nilai yang tidak tepat.
Misalnya, mengganti seluruh missing value menggunakan mean dapat
mengurangi variasi data dan mengubah distribusi. Selain itu, missing
value sendiri dapat mengandung informasi mengenai proses pengumpulan
data. Oleh karena itu, kualitas dataset harus dinilai berdasarkan
ketepatan, konsistensi, relevansi, dan validitas data, bukan hanya
berdasarkan jumlah NA.
Pada preprocessing ini, misalnya, missing pada pendapatan ditangani
menggunakan median karena terdapat nilai ekstrem yang membuat mean
kurang representatif. Sementara itu, missing pada kota diberi kategori
Tidak diketahui agar ketidakpastian tetap terlihat.
Outlier tidak selalu merupakan kesalahan. Nilai ekstrem dapat merupakan observasi yang benar-benar terjadi pada populasi. Pada data ini, pendapatan C007 sebesar 500 juta memang sangat jauh dibandingkan pelanggan lain, tetapi tidak terdapat bukti bahwa nilai tersebut merupakan kesalahan input.
Jika nilai tersebut langsung dihapus, informasi mengenai pelanggan dengan karakteristik ekstrem akan hilang. Oleh karena itu, outlier sebaiknya terlebih dahulu diperiksa menggunakan konteks domain, sumber data, dan tujuan analisis.
Dalam preprocessing ini, C007 dipertahankan dalam data utama dan Winsorization hanya digunakan sebagai pembanding. Pendekatan tersebut mengikuti prinsip bahwa outlier harus dievaluasi sebelum ditentukan tindakannya. :contentReferenceoaicite:10
Preprocessing dapat menimbulkan bias ketika keputusan yang dibuat selama cleaning mengubah representasi data secara sistematis.
Contohnya adalah imputasi mean. Jika distribusi pendapatan sangat menceng, mean dapat dipengaruhi oleh beberapa nilai ekstrem sehingga mengganti missing value menggunakan mean dapat memberikan nilai yang tidak representatif.
Bias juga dapat muncul ketika baris dengan missing value langsung dihapus. Jika missing value lebih banyak terjadi pada kelompok tertentu, penghapusan tersebut dapat membuat kelompok tersebut menjadi kurang terwakili dalam dataset.
Dengan demikian, preprocessing harus mempertimbangkan karakteristik data dan tujuan analisis, bukan hanya menghasilkan dataset yang terlihat lebih rapi.
Jika preprocessing digunakan dalam machine learning, parameter seperti mean, median, standar deviasi, nilai minimum, dan maksimum seharusnya dihitung hanya dari data training.
Alasannya adalah untuk mencegah data leakage. Jika informasi dari data testing digunakan ketika menghitung parameter preprocessing, maka informasi mengenai distribusi data testing secara tidak langsung masuk ke dalam proses pelatihan.
Akibatnya, performa model pada data testing dapat terlihat lebih baik daripada kondisi sebenarnya.
Karena itu, alur yang benar adalah:
Data training
↓
Hitung parameter preprocessing
↓
Transform training
↓
Gunakan parameter yang sama
↓
Transform testing
↓
Evaluasi model
Identifier yang tidak unik dapat menyebabkan hasil join
menggandakan baris. Misalnya, apabila satu customer_id
muncul dua kali pada data pelanggan dan memiliki satu pasangan
transaksi, proses join dapat menghasilkan lebih dari satu baris untuk
pelanggan tersebut.
Akibatnya jumlah transaksi, total pembelian, atau statistik lainnya dapat terhitung lebih dari satu kali.
Pada preprocessing ini, duplikasi C010 harus ditangani sebelum
integrasi. Setelah proses tersebut, customer_id diperiksa
kembali untuk memastikan identifier tetap unik. Pendekatan ini penting
karena integrasi yang secara sintaksis berhasil belum tentu benar secara
substantif.
Selain mengikuti seluruh alur utama praktikum, versi ini menambahkan beberapa sudut pandang.
Pertama, dilakukan audit kualitas data secara eksplisit sebelum cleaning sehingga perubahan dapat dibandingkan secara kuantitatif.
Kedua, missing value tidak diperlakukan sama. Missing pada variabel
numerik dipertimbangkan menggunakan median, sedangkan missing pada
variabel kategorik diberi kategori Tidak diketahui.
Ketiga, outlier tidak langsung dihapus. Pendapatan ekstrem C007 dipertahankan pada dataset utama dan dibuat versi Winsorization sebagai pembanding.
Keempat, proses integrasi divalidasi dari sisi jumlah baris, keunikan identifier, dan missing value yang muncul setelah join.
Kelima, dibuat log perubahan preprocessing sehingga setiap keputusan cleaning dapat ditelusuri.
Alur preprocessing yang dilakukan dalam praktik ini adalah:
Preprocessing pada akhirnya bukan sekadar proses teknis untuk membuat data menjadi “bersih”. Preprocessing merupakan rangkaian keputusan yang menentukan informasi apa yang dipertahankan, diubah, atau dihilangkan dari dataset. Karena itu, alasan setiap keputusan perlu dapat dijelaskan dan didokumentasikan.
Han, J., Kamber, M., & Pei, J. (2012). Data Mining: Concepts and Techniques (3rd ed.). Morgan Kaufmann. Chapter 3: Data Preprocessing.