Pada praktikum ini digunakan contoh data pelanggan dan data transaksi. Dataset dibuat secara sengaja dengan beberapa masalah kualitas data agar proses preprocessing dapat didemonstrasikan.
Masalah yang akan diatasi yaitu:
pelanggan_raw <- data.frame(
customer_id = c(
"M101","M102","M103","M104","M105",
"M106","M107","M108","M109","M110",
"M110","M112","M113","M114"
),
nama = c(
"Rani","Dimas","Salsa","Fajar","Nadia",
"Rizky","Tio","Maya","Arif","Putri",
"Putri","Kevin","Lala","Bagas"
),
usia = c(
22, 27, NA, 31, 145,
26, 29, 24, 38, 33,
33, NA, 21, 30
),
pendapatan = c(
4200000, NA, 5100000, 5600000, 4900000,
4800000, 75000000, 4600000, NA, 5300000,
5300000, 4700000, 4400000, 5200000
),
kota = c(
"Pekanbaru"," PKU","PEKANBARU","Dumai",
"dumai","DUMAI","Pekanbaru ","PEKANBARU",
"Siak","pku","pku","Pekanbaru",
NA,"DUMAI"
),
status = c(
"Aktif","ACTIVE","aktif","A",
"Tidak aktif","nonaktif","AKTIF","Aktif",
"Tidak Aktif","A","A","active",
"nonaktif","Aktif"
),
stringsAsFactors = FALSE
)
pelanggan_raw## [1] 14 6
## [1] "customer_id" "nama" "usia" "pendapatan" "kota"
## [6] "status"
## 'data.frame': 14 obs. of 6 variables:
## $ customer_id: chr "M101" "M102" "M103" "M104" ...
## $ nama : chr "Rani" "Dimas" "Salsa" "Fajar" ...
## $ usia : num 22 27 NA 31 145 26 29 24 38 33 ...
## $ pendapatan : num 4.2e+06 NA 5.1e+06 5.6e+06 4.9e+06 4.8e+06 7.5e+07 4.6e+06 NA 5.3e+06 ...
## $ kota : chr "Pekanbaru" " PKU" "PEKANBARU" "Dumai" ...
## $ status : chr "Aktif" "ACTIVE" "aktif" "A" ...
## customer_id nama usia pendapatan
## Length:14 Length:14 Min. : 21.00 Min. : 4200000
## Class :character Class :character 1st Qu.: 25.50 1st Qu.: 4675000
## Mode :character Mode :character Median : 29.50 Median : 5000000
## Mean : 38.25 Mean :10758333
## 3rd Qu.: 33.00 3rd Qu.: 5300000
## Max. :145.00 Max. :75000000
## NA's :2 NA's :2
## kota status
## Length:14 Length:14
## Class :character Class :character
## Mode :character Mode :character
##
##
##
##
Sebelum melakukan perubahan, struktur, dimensi, tipe atribut, dan beberapa observasi awal perlu diperiksa terlebih dahulu.
Enam dimensi kualitas data yang digunakan dalam pembahasan adalah:
## customer_id nama usia pendapatan kota status
## 0 0 2 2 1 0
## customer_id nama usia pendapatan kota status
## 0.00 0.00 14.29 14.29 7.14 0.00
## [1] 1
## [1] 1
## [1] " PKU" "dumai" "Dumai" "DUMAI" "Pekanbaru"
## [6] "PEKANBARU" "Pekanbaru " "pku" "Siak"
## [1] "A" "active" "ACTIVE" "aktif" "Aktif"
## [6] "AKTIF" "nonaktif" "Tidak aktif" "Tidak Aktif"
## [1] 21 145
## [1] 4.2e+06 7.5e+07
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_awalInterpretasi: Pemeriksaan awal menunjukkan kondisi setiap variabel dalam dataset, meliputi tipe data, jumlah dan persentase nilai yang hilang, serta jumlah nilai unik. Informasi tersebut dapat digunakan untuk mengetahui variabel yang perlu diperiksa lebih lanjut sebelum dilakukan proses pembersihan dan pengolahan data.
pelanggan$kota <- trimws(
pelanggan$kota
)
pelanggan$status <- trimws(
pelanggan$status
)
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"
pelanggan$kota[
pelanggan$kota %in%
c("pku", "pekanbaru")
] <- "Pekanbaru"
pelanggan$kota[
pelanggan$kota == "dumai"
] <- "Dumai"
pelanggan$kota[
pelanggan$kota == "siak"
] <- "Siak"pelanggan$status[
pelanggan$status %in%
c("aktif","active","a")
] <- "Aktif"
pelanggan$status[
pelanggan$status %in%
c("tidak aktif","nonaktif")
] <- "Tidak Aktif"## [1] "Dumai" "Pekanbaru" "Siak"
## [1] "Aktif" "Tidak Aktif"
pelanggan[
duplicated(pelanggan$customer_id) |
duplicated(
pelanggan$customer_id,
fromLast = TRUE
),
]Pada dataset ini M110 muncul dua kali dengan informasi
yang identik.
Karena customer_id dirancang sebagai identifier unik
pelanggan, salah satu kemunculan dapat dihapus.
pelanggan <- pelanggan[
!duplicated(pelanggan$customer_id),
]
rownames(pelanggan) <- NULL
dim(pelanggan)## [1] 13 6
Untuk contoh ini digunakan aturan:
Nilai usia 145 merupakan salah satu kesalahan input.
Misalkan setelah diperiksa pada formulir sumber, usia yang benar
adalah 45.
log_perubahan <- data.frame(
tahap = c(
"Standardisasi",
"Standardisasi",
"Deduplikasi",
"Koreksi domain"
),
atribut = c(
"kota",
"status",
"customer_id",
"usia"
),
tindakan = c(
"Menyamakan PKU dan variasi kapitalisasi menjadi Pekanbaru",
"Menyamakan ACTIVE/A/aktif menjadi Aktif dan nonaktif menjadi Tidak Aktif",
"Menghapus kemunculan kedua M110",
"Usia M105 dikoreksi dari 145 menjadi 45 berdasarkan sumber"
),
stringsAsFactors = FALSE
)
log_perubahan## customer_id nama usia pendapatan kota status
## 0 0 2 2 1 0
## [1] 13
## [1] 8
Pendapatan memiliki nilai ekstrem sehingga mean dan median perlu dibandingkan.
mean_pendapatan <- mean(
pelanggan$pendapatan,
na.rm = TRUE
)
median_pendapatan <- median(
pelanggan$pendapatan,
na.rm = TRUE
)
mean_pendapatan## [1] 11254545
## [1] 4900000
Karena terdapat nilai pendapatan yang sangat besar, median lebih robust terhadap nilai ekstrem.
Untuk kota yang tidak diketahui, 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
## 4 7 1 1
pelanggan$pendapatan_missing <-
as.integer(
is.na(
pelanggan$pendapatan
)
)
table(
pelanggan$pendapatan_missing
)##
## 0 1
## 11 2
Indikator missing berguna karena setelah imputasi, informasi bahwa nilai aslinya hilang tidak sepenuhnya hilang dari dataset.
par(
mfrow = c(1, 2)
)
hist(
pelanggan$pendapatan,
main = "Sebelum Imputasi",
xlab = "Pendapatan",
col = "#f8c9db",
breaks = 8
)
hist(
pelanggan$pendapatan_imputasi,
main = "Sesudah Imputasi Median",
xlab = "Pendapatan",
col = "#f48fb1",
breaks = 8
)Setelah imputasi, evaluasi tidak berhenti hanya karena jumlah
NA menjadi nol. Distribusi sebelum dan sesudah imputasi
harus dibandingkan.
boxplot(
pelanggan$pendapatan_imputasi,
horizontal = TRUE,
col = "#f6a9c3",
main = "Boxplot Pendapatan",
xlab = "Pendapatan"
)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%
## 4700000 5200000 500000 3950000 5950000
pelanggan$outlier_pendapatan <-
pelanggan$pendapatan_imputasi <
batas_bawah |
pelanggan$pendapatan_imputasi >
batas_atas
pelanggan[
pelanggan$outlier_pendapatan,
c(
"customer_id",
"nama",
"pendapatan_imputasi"
)
]Outlier tidak otomatis dihapus.
Nilai ekstrem dapat merupakan:
Dalam contoh ini pendapatan ekstrem dipertahankan.
Sebagai demonstrasi, dibuat versi winsorized.
pelanggan$pendapatan_winsor <-
pmin(
pmax(
pelanggan$pendapatan_imputasi,
batas_bawah
),
batas_atas
)
pelanggan[
pelanggan$outlier_pendapatan,
c(
"customer_id",
"pendapatan_imputasi",
"pendapatan_winsor"
)
]Winsorizing hanya digunakan sebagai demonstrasi. Tindakan terhadap outlier harus disesuaikan dengan tujuan analisis dan didokumentasikan.
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_imputasi",
"usia_minmax",
"pendapatan_imputasi",
"pendapatan_minmax"
)
]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
)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.042 0.750
transformasi <-
pelanggan[
,
c(
"customer_id",
"pendapatan_imputasi",
"pendapatan_minmax",
"pendapatan_z",
"pendapatan_decimal"
)
]
transformasipelanggan$pendapatan_winsor_minmax <-
minmax(
pelanggan$pendapatan_winsor
)
plot(
pelanggan$pendapatan_minmax,
pelanggan$pendapatan_winsor_minmax,
pch = 19,
col = "#c2185b",
xlab = "Min-Max Data Asli",
ylab = "Min-Max Data Winsorized",
main =
"Dampak Outlier terhadap Normalisasi"
)
abline(
0,
1,
col = "#f6a9c3",
lty = 2
)Outlier dapat membuat sebagian besar nilai lain menjadi sangat dekat dengan nol pada normalisasi min–max. Karena itu, deteksi outlier penting sebelum memilih transformasi.
Selain data pelanggan, dibuat sumber data transaksi terpisah.
transaksi_raw <- data.frame(
cust_code = c(
"M101","M102","M103","M104",
"M105","M106","M107","M108",
"M109","M110","M115"
),
jumlah_transaksi = c(
4,6,3,8,2,5,15,4,7,9,3
),
total_purchase = c(
1200000,
1800000,
950000,
2600000,
700000,
1500000,
8200000,
1400000,
3100000,
4200000,
900000
),
stringsAsFactors = FALSE
)
transaksi_raw## [1] 0
## [1] 0
## [1] "M112" "M113" "M114"
## [1] "M115"
setdiff() digunakan untuk menemukan identifier yang
hanya muncul pada salah satu sumber data.
data_terintegrasi <-
merge(
pelanggan,
transaksi,
by = "customer_id",
all.x = TRUE
)
data_terintegrasi[
,
c(
"customer_id",
"nama",
"jumlah_transaksi",
"total_purchase"
)
]all.x = TRUE mempertahankan seluruh pelanggan walaupun
pelanggan tersebut tidak memiliki pasangan transaksi.
## sebelum sesudah
## 13 13
## jumlah_transaksi total_purchase
## 3 3
NA setelah integrasi dapat memiliki arti yang
berbeda.
Karena itu NA tidak boleh otomatis diubah
menjadi 0.
Sebagai contoh, apabila definisi data telah dikonfirmasi bahwa
NA berarti pelanggan 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
)
] <- 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) == "usia_imputasi"
] <- "usia"
names(data_final)[
names(data_final) == "kota_imputasi"
] <- "kota"
data_finalDalam machine learning, parameter preprocessing seperti median, mean, standar deviasi, nilai minimum, dan maksimum sebaiknya dihitung dari data training saja.
Hal ini mencegah data leakage, yaitu informasi dari data testing secara tidak sengaja masuk ke proses pembentukan model.
set.seed(2026)
n_train <-
floor(
0.8 *
nrow(data_final)
)
id_train <-
sample(
seq_len(
nrow(data_final)
),
n_train
)
train_data <-
data_final[
id_train,
]
test_data <-
data_final[
-id_train,
]## [1] 4900000
min_train_income <-
min(
train_data$pendapatan_imputasi,
na.rm = TRUE
)
max_train_income <-
max(
train_data$pendapatan_imputasi,
na.rm = TRUE
)
c(
min_train_income,
max_train_income
)## [1] 4.2e+06 7.5e+07
train_data$income_minmax <-
(
train_data$pendapatan_imputasi -
min_train_income
) /
(
max_train_income -
min_train_income
)
head(
train_data[
,
c(
"customer_id",
"pendapatan_imputasi",
"income_minmax"
)
]
)Jika minimum, maksimum, mean, median, atau standar deviasi dihitung dari gabungan training dan testing, informasi testing ikut memengaruhi proses preprocessing.
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
)
)
)
)
perbandinganTidak.
Dataset tanpa missing value belum tentu lebih berkualitas karena nilai yang hilang mungkin telah dihapus atau diimputasi dengan cara yang tidak tepat. Mengubah seluruh missing value menjadi nilai tertentu dapat menghilangkan informasi mengenai ketidakpastian data.
Kualitas data harus dilihat secara lebih luas, termasuk accuracy, completeness, consistency, timeliness, believability, dan interpretability.
Jadi, dataset dengan sedikit missing value tetapi banyak nilai salah dapat lebih buruk daripada dataset yang memiliki beberapa missing value namun nilai lainnya akurat.
Outlier tidak selalu merupakan kesalahan.
Nilai ekstrem dapat merupakan:
Misalnya pendapatan seseorang yang jauh lebih tinggi daripada mayoritas pelanggan belum tentu salah. Jika nilai tersebut benar, menghapusnya justru menghilangkan informasi penting.
Karena itu outlier perlu dideteksi, diperiksa, kemudian dievaluasi. Materi juga menekankan tiga kemungkinan tindakan: koreksi jika terbukti salah, mempertahankan jika valid, atau memisahkan segmen jika berasal dari populasi berbeda.
Preprocessing dapat menimbulkan bias apabila keputusan cleaning membuat sebagian kelompok lebih banyak kehilangan atau diubah datanya daripada kelompok lain.
Contohnya:
Jika kelompok tertentu lebih sering memiliki missing value lalu barisnya lebih banyak dihapus, distribusi sampel akhir dapat berbeda dari populasi aslinya.
Karena itu preprocessing bukan sekadar proses teknis, tetapi merupakan serangkaian keputusan yang dapat memengaruhi hasil analisis.
Karena penggunaan seluruh dataset dapat menyebabkan data leakage.
Misalnya kita menghitung median, mean, standar deviasi, minimum, atau maksimum menggunakan data training dan testing sekaligus. Informasi dari testing kemudian ikut menentukan bagaimana data training diproses.
Akibatnya evaluasi model menjadi terlalu optimistis karena model secara tidak langsung telah memperoleh informasi dari data testing.
Prosedur yang benar adalah:
Training data → hitung parameter → gunakan parameter pada training dan testing.
Dengan demikian testing tetap menjadi data yang benar-benar belum digunakan dalam proses pembentukan model.
Identifier yang tidak unik dapat menyebabkan many-to-many matching atau penggandaan baris saat dua dataset digabungkan.
Contohnya, jika satu customer_id muncul dua kali pada
tabel pelanggan dan dua kali pada tabel transaksi, proses join dapat
menghasilkan empat kombinasi baris.
Akibatnya:
Karena itu sebelum integrasi harus diperiksa apakah identifier memang unik pada tabel yang seharusnya memiliki satu record per entitas. Materi juga secara eksplisit memeriksa duplikasi key sebelum melakukan integrasi.
Alur preprocessing yang dilakukan adalah:
Data preprocessing bukan sekadar membuat dataset menjadi “bersih”.
Preprocessing merupakan proses pengambilan keputusan. Setiap perubahan terhadap data harus memiliki alasan, mempertimbangkan konteks data, dapat ditelusuri melalui dokumentasi, dan dievaluasi kembali setelah dilakukan.
Dataset yang baik bukan hanya dataset yang tidak memiliki
NA, tetapi dataset yang akurat, konsisten, dapat
dipercaya, dapat dipahami, dan sesuai dengan tujuan
analisis.
Han, J., Kamber, M., & Pei, J. (2012). Data Mining: Concepts and Techniques (3rd ed.). Morgan Kaufmann. Chapter 3: Data Preprocessing.