| Komponen | Keterangan |
|---|---|
| Mata kuliah | Data Mining |
| Topik | Data Preprocessing |
| Nama | Hanna Toula Sinaga |
| NIM | 2403113999 |
| Kelas | Statistika A 2024 |
Data preprocessing adalah tahap untuk menyiapkan data agar lebih konsisten, lengkap, dan sesuai dengan kebutuhan analisis. Praktikum ini menggunakan dataset pelanggan dan dataset transaksi buatan, dengan skenario sebagai berikut: Sebuah perusahaan e-commerce akan melakukan analisis pelanggan. Data berasal dari dua sumber:
data pelanggan, yang memuat identitas, usia, kota, pendapatan, dan status pelanggan; data transaksi, yang memuat jumlah transaksi dan total pembelian
Data sengaja dibuat memiliki masalah kualitas agar seluruh proses dapat diamati.
Setiap tahapan pada praktikum ini mengikutu suatu pola, yakni kode → output terpilih → interpretasi hasil. Output mentah yang panjang tidak dicetak sekaligus melainkan hanya hasil ringkasnya saja yang dimunculkan dalam format tabel.
Pembaruan pada praktikum ini: audit kualitas dibuat
sebagai tabel ringkas; setiap perubahan dicatat dalam log; indikator
missing dipertahankan; outlier tidak hanya ditandai tetapi dibandingkan
dengan versi winsorized; ditambahkan transformasi log1p;
kunci integrasi diuji sebelum join; dan dataset akhir diperiksa
menggunakan validasi otomatis.
lingkungan <- data.frame(
pemeriksaan = c("Direktori kerja", "Versi R"),
hasil = c(getwd(), R.version.string),
stringsAsFactors = FALSE
)
knitr::kable(lingkungan, caption = "Informasi lingkungan pengerjaan")
| pemeriksaan | hasil |
|---|---|
| Direktori kerja | C:/Users/IDEAPAD/Downloads |
| Versi R | R version 4.6.1 (2026-06-24 ucrt) |
Penjelasan: pemeriksaan lingkungan dilakukan agar lokasi file hasil dan versi R dapat direproduksi. File CSV yang dihasilkan pada bagian akhir akan tersimpan di direktori kerja yang ditampilkan pada tabel.
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")
)
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)
)
pelanggan_tampil <- pelanggan_raw
pelanggan_tampil$pendapatan <- fmt_rupiah(pelanggan_tampil$pendapatan)
transaksi_tampil <- transaksi_raw
transaksi_tampil$total_purchase <- fmt_rupiah(transaksi_tampil$total_purchase)
knitr::kable(pelanggan_tampil, caption = "Dataset pelanggan mentah")
| customer_id | nama | usia | pendapatan | kota | status |
|---|---|---|---|---|---|
| C001 | Ani | 21 | 4.500.000 | Pekanbaru | Aktif |
| C002 | Budi | 25 | NA | PKU | aktif |
| C003 | Citra | 23 | 5.200.000 | PEKANBARU | ACTIVE |
| C004 | Dodi | 150 | 4.800.000 | Dumai | A |
| C005 | Eka | 27 | 4.900.000 | pekanbaru | Tidak Aktif |
| C006 | Fani | NA | 5.100.000 | DUMAI | nonaktif |
| C007 | Gilang | 31 | 500.000.000 | Pekanbaru | Aktif |
| C008 | Hana | 29 | 4.700.000 | Siak | AKTIF |
| C009 | Indra | 22 | 4.600.000 | PKU | A |
| C010 | Joko | 35 | 5.300.000 | Dumai | Tidak aktif |
| C010 | Joko | 35 | 5.300.000 | Dumai | Tidak aktif |
| C011 | Kiki | 28 | NA | NA | Aktif |
knitr::kable(transaksi_tampil, caption = "Dataset transaksi mentah")
| cust_id | jumlah_transaksi | total_purchase |
|---|---|---|
| C001 | 5 | 1.500.000 |
| C002 | 3 | 900.000 |
| C003 | 7 | 2.700.000 |
| C004 | 2 | 600.000 |
| C005 | 6 | 2.100.000 |
| C006 | 4 | 1.300.000 |
| C007 | 20 | 25.000.000 |
| C008 | 5 | 1.700.000 |
| C009 | 3 | 800.000 |
| C010 | 8 | 3.200.000 |
| C012 | 1 | 250.000 |
Penjelasan: dataset pelanggan terdiri atas 12 baris
dan 6 atribut. Data mentah mengandung satu customer_id
berulang, satu usia yang tidak wajar, dua pendapatan hilang, satu kota
hilang, serta variasi ejaan kota dan status. Dataset transaksi
menggunakan nama kunci cust_id, bukan
customer_id, dan memiliki kode C012 yang belum
terdapat pada data pelanggan.
dimensi <- data.frame(
komponen = c("Jumlah baris", "Jumlah kolom"),
nilai = c(nrow(pelanggan_raw), ncol(pelanggan_raw))
)
struktur <- data.frame(
atribut = names(pelanggan_raw),
tipe_data = sapply(pelanggan_raw, function(x) class(x)[1]),
jumlah_observasi = sapply(pelanggan_raw, length),
stringsAsFactors = FALSE
)
rownames(struktur) <- NULL
head_tampil <- pelanggan_raw[seq_len(min(6, nrow(pelanggan_raw))), ]
head_tampil$pendapatan <- fmt_rupiah(head_tampil$pendapatan)
knitr::kable(dimensi, caption = "Dimensi data pelanggan")
| komponen | nilai |
|---|---|
| Jumlah baris | 12 |
| Jumlah kolom | 6 |
knitr::kable(struktur, caption = "Struktur atribut pelanggan")
| atribut | tipe_data | jumlah_observasi |
|---|---|---|
| customer_id | character | 12 |
| nama | character | 12 |
| usia | numeric | 12 |
| pendapatan | numeric | 12 |
| kota | character | 12 |
| status | character | 12 |
knitr::kable(head_tampil, caption = "Enam observasi pertama")
| customer_id | nama | usia | pendapatan | kota | status |
|---|---|---|---|---|---|
| C001 | Ani | 21 | 4.500.000 | Pekanbaru | Aktif |
| C002 | Budi | 25 | NA | PKU | aktif |
| C003 | Citra | 23 | 5.200.000 | PEKANBARU | ACTIVE |
| C004 | Dodi | 150 | 4.800.000 | Dumai | A |
| C005 | Eka | 27 | 4.900.000 | pekanbaru | Tidak Aktif |
| C006 | Fani | NA | 5.100.000 | DUMAI | nonaktif |
Penjelasan: data memiliki 12 observasi dan 6
variabel. Atribut usia dan pendapatan bertipe
numerik sehingga dapat dihitung dan ditransformasikan. Atribut identitas
serta kategori masih bertipe karakter. Pemeriksaan struktur dilakukan
sebelum cleaning agar perubahan dapat dibandingkan dengan kondisi
awal.
audit_data <- function(data) {
hasil <- 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))),
stringsAsFactors = FALSE
)
rownames(hasil) <- NULL
hasil
}
audit_awal <- audit_data(pelanggan_raw)
ringkasan_kualitas <- data.frame(
indikator = c("Total missing value", "Duplikasi seluruh baris",
"Duplikasi customer_id", "Rentang usia", "Rentang pendapatan"),
hasil = c(
sum(is.na(pelanggan_raw)),
sum(duplicated(pelanggan_raw)),
sum(duplicated(pelanggan_raw$customer_id)),
paste(range(pelanggan_raw$usia, na.rm = TRUE), collapse = " sampai "),
paste(fmt_rupiah(range(pelanggan_raw$pendapatan, na.rm = TRUE)), collapse = " sampai ")
),
stringsAsFactors = FALSE
)
knitr::kable(audit_awal, digits = 2, caption = "Audit kualitas per atribut")
| atribut | tipe | jumlah_missing | persen_missing | jumlah_unik |
|---|---|---|---|---|
| customer_id | character | 0 | 0.00 | 11 |
| nama | character | 0 | 0.00 | 11 |
| usia | numeric | 1 | 8.33 | 11 |
| pendapatan | numeric | 2 | 16.67 | 10 |
| kota | character | 1 | 8.33 | 10 |
| status | character | 0 | 0.00 | 8 |
knitr::kable(ringkasan_kualitas, caption = "Ringkasan masalah kualitas awal")
| indikator | hasil |
|---|---|
| Total missing value | 4 |
| Duplikasi seluruh baris | 1 |
| Duplikasi customer_id | 1 |
| Rentang usia | 21 sampai 150 |
| Rentang pendapatan | 4.500.000 sampai 500.000.000 |
Penjelasan: terdapat 4 missing value, yaitu 1 pada
usia, 2 pada pendapatan, dan 1 pada
kota. Terdapat 1 baris duplikat penuh sekaligus 1
pengulangan customer_id. Rentang usia 21–150 menunjukkan
adanya kandidat pelanggaran domain, sedangkan rentang pendapatan yang
sangat lebar menunjukkan adanya kandidat outlier.
Ket.: pada versi ini audit tidak berhenti pada
colSums(is.na()). Audit dirangkum per atribut dan
dilengkapi persentase missing serta jumlah kategori unik sehingga hasil
diagnosis dapat langsung dibaca tanpa menafsirkan banyak output
konsol.
pelanggan <- pelanggan_raw
log_perubahan <- data.frame(
tahap = character(),
atribut = character(),
tindakan = character(),
alasan = character(),
stringsAsFactors = FALSE
)
catatan_log <- function(tahap, atribut, tindakan, alasan) {
log_perubahan <<- rbind(
log_perubahan,
data.frame(tahap = tahap, atribut = atribut,
tindakan = tindakan, alasan = alasan,
stringsAsFactors = FALSE)
)
}
status_salinan <- data.frame(
objek = c("pelanggan_raw", "pelanggan"),
jumlah_baris = c(nrow(pelanggan_raw), nrow(pelanggan)),
keterangan = c("Data mentah, tidak diubah", "Salinan kerja untuk preprocessing")
)
knitr::kable(status_salinan, caption = "Pemisahan data mentah dan data kerja")
| objek | jumlah_baris | keterangan |
|---|---|---|
| pelanggan_raw | 12 | Data mentah, tidak diubah |
| pelanggan | 12 | Salinan kerja untuk preprocessing |
Penjelasan: data mentah dipertahankan sebagai
pembanding. Seluruh perubahan dilakukan pada objek
pelanggan, sehingga proses dapat ditelusuri dan hasil
sebelum–sesudah dapat dihitung secara objektif.
# Menghapus spasi awal/akhir dan menyamakan huruf kecil untuk proses pencocokan.
pelanggan$kota <- tolower(trimws(pelanggan$kota))
pelanggan$status <- tolower(trimws(pelanggan$status))
kategori_setelah_format <- data.frame(
atribut = c("kota", "status"),
kategori_setelah_trim_lower = c(
paste(sort(unique(na.omit(pelanggan$kota))), collapse = ", "),
paste(sort(unique(na.omit(pelanggan$status))), collapse = ", ")
)
)
knitr::kable(kategori_setelah_format, caption = "Kategori setelah trimws() dan tolower()")
| atribut | kategori_setelah_trim_lower |
|---|---|
| kota | dumai, pekanbaru, pku, siak |
| status | a, active, aktif, nonaktif, tidak aktif |
Penjelasan: spasi tambahan telah dihilangkan dan
perbedaan huruf besar-kecil tidak lagi menghalangi pencocokan kategori.
Namun, pku dan pekanbaru masih dianggap teks
yang berbeda; karena itu diperlukan langkah standardisasi berbasis
aturan domain.
# 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("nonaktif", "tidak aktif")] <- "Tidak Aktif"
catatan_log("Standardisasi", "kota", "PKU dan variasi ejaan menjadi Pekanbaru",
"Menghindari kategori kota yang terpecah")
catatan_log("Standardisasi", "status", "ACTIVE/A/aktif menjadi Aktif; nonaktif menjadi Tidak Aktif",
"Menghindari kategori status yang terpecah")
kategori_baku <- data.frame(
atribut = c("kota", "status"),
jumlah_kategori = c(length(unique(na.omit(pelanggan$kota))),
length(unique(na.omit(pelanggan$status)))),
kategori_baku = c(
paste(sort(unique(na.omit(pelanggan$kota))), collapse = ", "),
paste(sort(unique(na.omit(pelanggan$status))), collapse = ", ")
)
)
knitr::kable(kategori_baku, caption = "Kategori setelah standardisasi")
| atribut | jumlah_kategori | kategori_baku |
|---|---|---|
| kota | 3 | Dumai, Pekanbaru, Siak |
| status | 2 | Aktif, Tidak Aktif |
Penjelasan: kategori kota yang semula terpecah menjadi beberapa ejaan diringkas menjadi Dumai, Pekanbaru, dan Siak. Kategori status diringkas menjadi Aktif dan Tidak Aktif. Standardisasi ini penting karena analisis frekuensi atau pengelompokan akan keliru jika satu kategori yang sama ditulis dalam banyak bentuk.
baris_duplikat <- pelanggan[
duplicated(pelanggan$customer_id) |
duplicated(pelanggan$customer_id, fromLast = TRUE),
]
baris_duplikat_tampil <- baris_duplikat
baris_duplikat_tampil$pendapatan <- fmt_rupiah(baris_duplikat_tampil$pendapatan)
knitr::kable(baris_duplikat_tampil, caption = "Baris dengan customer_id berulang")
| customer_id | nama | usia | pendapatan | kota | status | |
|---|---|---|---|---|---|---|
| 10 | C010 | Joko | 35 | 5.300.000 | Dumai | Tidak Aktif |
| 11 | C010 | Joko | 35 | 5.300.000 | Dumai | Tidak Aktif |
jumlah_sebelum_deduplikasi <- nrow(pelanggan)
pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL
jumlah_sesudah_deduplikasi <- nrow(pelanggan)
catatan_log("Deduplikasi", "customer_id", "Mempertahankan kemunculan pertama",
"Satu customer_id mewakili satu pelanggan pada skenario ini")
ringkasan_deduplikasi <- data.frame(
indikator = c("Jumlah baris sebelum", "Jumlah baris sesudah", "Baris dihapus", "Duplikasi key sesudah"),
nilai = c(jumlah_sebelum_deduplikasi, jumlah_sesudah_deduplikasi,
jumlah_sebelum_deduplikasi - jumlah_sesudah_deduplikasi,
sum(duplicated(pelanggan$customer_id)))
)
knitr::kable(ringkasan_deduplikasi, caption = "Dampak deduplikasi")
| indikator | nilai |
|---|---|
| Jumlah baris sebelum | 12 |
| Jumlah baris sesudah | 11 |
| Baris dihapus | 1 |
| Duplikasi key sesudah | 0 |
Penjelasan: dua baris untuk C010
identik, sehingga satu baris dipertahankan dan jumlah data berkurang
dari 12 menjadi 11. Penghapusan ini adalah tepat untuk skenario/kasus
ini karena customer_id didefinisikan sebagai kunci unik
pelanggan. Pada data transaksi, aturan ini tidak boleh diterapkan
sembarangan karena satu pelanggan dapat memiliki banyak transaksi.
kandidat_usia <- pelanggan[
!is.na(pelanggan$usia) & (pelanggan$usia < 15 | pelanggan$usia > 100),
]
kandidat_pendapatan <- pelanggan[
!is.na(pelanggan$pendapatan) & pelanggan$pendapatan < 0,
]
kandidat_usia_tampil <- kandidat_usia
kandidat_usia_tampil$pendapatan <- fmt_rupiah(kandidat_usia_tampil$pendapatan)
kandidat_pendapatan_tampil <- kandidat_pendapatan
kandidat_pendapatan_tampil$pendapatan <- fmt_rupiah(kandidat_pendapatan_tampil$pendapatan)
knitr::kable(kandidat_usia_tampil, caption = "Kandidat pelanggaran domain usia 15–100 tahun")
| customer_id | nama | usia | pendapatan | kota | status | |
|---|---|---|---|---|---|---|
| 4 | C004 | Dodi | 150 | 4.800.000 | Dumai | Aktif |
knitr::kable(kandidat_pendapatan_tampil, caption = "Kandidat pendapatan negatif")
| customer_id | nama | usia | pendapatan | kota | status |
|---|
Penjelasan: hanya C004 yang melanggar
aturan usia karena tercatat berusia 150 tahun. Kemudian. tidak ada
pendapatan yang negatif. Serta, nilai yang melanggar domain tidak
langsung dihapus, karena nilai harus dikonfirmasi terlebih dahulu
terhadap sumber asli.
usia_lama <- pelanggan$usia[pelanggan$customer_id == "C004"]
pelanggan$usia[pelanggan$customer_id == "C004" & pelanggan$usia == 150] <- 50
usia_baru <- pelanggan$usia[pelanggan$customer_id == "C004"]
catatan_log("Koreksi domain", "usia", "C004: 150 menjadi 50",
"Nilai dikonfirmasi sebagai salah input berdasarkan sumber asli")
perubahan_domain <- data.frame(
customer_id = "C004",
atribut = "usia",
nilai_sebelum = usia_lama,
nilai_sesudah = usia_baru,
keputusan = "Koreksi, bukan menghapus baris"
)
knitr::kable(perubahan_domain, caption = "Koreksi nilai yang melanggar domain")
| customer_id | atribut | nilai_sebelum | nilai_sesudah | keputusan |
|---|---|---|---|---|
| C004 | usia | 150 | 50 | Koreksi, bukan menghapus baris |
knitr::kable(log_perubahan, caption = "Log perubahan sampai tahap domain")
| tahap | atribut | tindakan | alasan |
|---|---|---|---|
| Standardisasi | kota | PKU dan variasi ejaan menjadi Pekanbaru | Menghindari kategori kota yang terpecah |
| Standardisasi | status | ACTIVE/A/aktif menjadi Aktif; nonaktif menjadi Tidak Aktif | Menghindari kategori status yang terpecah |
| Deduplikasi | customer_id | Mempertahankan kemunculan pertama | Satu customer_id mewakili satu pelanggan pada skenario ini |
| Koreksi domain | usia | C004: 150 menjadi 50 | Nilai dikonfirmasi sebagai salah input berdasarkan sumber asli |
Penjelasan: usia C004 dikoreksi dari
150 menjadi 50 tanpa menghapus pelanggan tersebut. Log perubahan
menunjukkan apa yang diubah, atribut yang terdampak, dan alasan
perubahan. Dokumentasi ini menjadi pembeda penting karena proses
preprocessing dapat diaudit ulang.
missing_per_atribut <- data.frame(
atribut = names(pelanggan),
jumlah_missing = colSums(is.na(pelanggan)),
persen_missing = round(colMeans(is.na(pelanggan)) * 100, 2)
)
baris_tidak_lengkap <- pelanggan[!complete.cases(pelanggan), ]
baris_tidak_lengkap_tampil <- baris_tidak_lengkap
baris_tidak_lengkap_tampil$pendapatan <- fmt_rupiah(baris_tidak_lengkap_tampil$pendapatan)
knitr::kable(missing_per_atribut, digits = 2, caption = "Missing value per atribut")
| atribut | jumlah_missing | persen_missing | |
|---|---|---|---|
| customer_id | customer_id | 0 | 0.00 |
| nama | nama | 0 | 0.00 |
| usia | usia | 1 | 9.09 |
| pendapatan | pendapatan | 2 | 18.18 |
| kota | kota | 1 | 9.09 |
| status | status | 0 | 0.00 |
knitr::kable(baris_tidak_lengkap_tampil, caption = "Baris yang memiliki sedikitnya satu missing value")
| customer_id | nama | usia | pendapatan | kota | status | |
|---|---|---|---|---|---|---|
| 2 | C002 | Budi | 25 | NA | Pekanbaru | Aktif |
| 6 | C006 | Fani | NA | 5.100.000 | Dumai | Tidak Aktif |
| 11 | C011 | Kiki | 28 | NA | NA | Aktif |
Penjelasan: setelah cleaning dan deduplikasi,
terdapat tiga pelanggan yang belum lengkap: C002 memiliki
pendapatan hilang, C006 memiliki usia hilang, dan
C011 memiliki pendapatan serta kota hilang. Ketiganya masih
memiliki customer_id yang valid sehingga tidak langsung
dihapus.
pelanggan_complete <- pelanggan[complete.cases(pelanggan), ]
perbandingan_hapus <- data.frame(
indikator = c("Baris sebelum penghapusan", "Baris sesudah penghapusan",
"Baris yang hilang", "Persentase baris yang hilang"),
nilai = c(
nrow(pelanggan),
nrow(pelanggan_complete),
nrow(pelanggan) - nrow(pelanggan_complete),
paste0(round((1 - nrow(pelanggan_complete) / nrow(pelanggan)) * 100, 2), "%")
)
)
knitr::kable(perbandingan_hapus, caption = "Simulasi strategi complete.cases()")
| indikator | nilai |
|---|---|
| Baris sebelum penghapusan | 11 |
| Baris sesudah penghapusan | 8 |
| Baris yang hilang | 3 |
| Persentase baris yang hilang | 27.27% |
Penjelasan: jika semua baris tidak lengkap dihapus, 3 dari 11 baris akan hilang atau sekitar 27,27%. Strategi ini hanya digunakan sebagai simulasi pembanding, bukan strategi utama, karena ukuran dataset kecil dan setiap customer masih memiliki informasi yang berguna.
mean_pendapatan <- mean(pelanggan$pendapatan, na.rm = TRUE)
median_pendapatan <- median(pelanggan$pendapatan, na.rm = TRUE)
median_usia <- median(pelanggan$usia, na.rm = TRUE)
perbandingan_pemusatan <- data.frame(
atribut = c("Pendapatan", "Pendapatan", "Usia"),
metode = c("Mean", "Median", "Median"),
nilai = c(mean_pendapatan, median_pendapatan, median_usia),
nilai_format = c(fmt_rupiah(mean_pendapatan), fmt_rupiah(median_pendapatan), fmt_num(median_usia, 1))
)
knitr::kable(perbandingan_pemusatan[, c("atribut", "metode", "nilai_format")],
col.names = c("Atribut", "Metode", "Nilai"),
caption = "Perbandingan nilai pemusatan untuk imputasi")
| Atribut | Metode | Nilai |
|---|---|---|
| Pendapatan | Mean | 59.900.000 |
| Pendapatan | Median | 4.900.000 |
| Usia | Median | 27.5 |
Penjelasan: mean pendapatan menjadi sangat besar
karena nilai C007 sebesar Rp500.000.000, sedangkan median
pendapatan tetap berada di sekitar mayoritas pelanggan. Oleh karena itu,
median dipilih untuk imputasi pendapatan dan usia karena lebih stabil
terhadap nilai ekstrem.
pelanggan$usia_imputasi <- pelanggan$usia
pelanggan$usia_imputasi[is.na(pelanggan$usia_imputasi)] <- median_usia
pelanggan$pendapatan_imputasi <- pelanggan$pendapatan
pelanggan$pendapatan_imputasi[is.na(pelanggan$pendapatan_imputasi)] <- median_pendapatan
imputasi_numerik <- pelanggan[, c("customer_id", "usia", "usia_imputasi",
"pendapatan", "pendapatan_imputasi")]
imputasi_numerik$pendapatan <- fmt_rupiah(imputasi_numerik$pendapatan)
imputasi_numerik$pendapatan_imputasi <- fmt_rupiah(imputasi_numerik$pendapatan_imputasi)
catatan_log("Missing value", "usia", "Imputasi median",
paste("Median usia =", median_usia))
catatan_log("Missing value", "pendapatan", "Imputasi median",
paste("Median pendapatan =", fmt_rupiah(median_pendapatan)))
knitr::kable(imputasi_numerik, caption = "Perbandingan nilai sebelum dan sesudah imputasi numerik")
| customer_id | usia | usia_imputasi | pendapatan | pendapatan_imputasi |
|---|---|---|---|---|
| C001 | 21 | 21.0 | 4.500.000 | 4.500.000 |
| C002 | 25 | 25.0 | NA | 4.900.000 |
| C003 | 23 | 23.0 | 5.200.000 | 5.200.000 |
| C004 | 50 | 50.0 | 4.800.000 | 4.800.000 |
| C005 | 27 | 27.0 | 4.900.000 | 4.900.000 |
| C006 | NA | 27.5 | 5.100.000 | 5.100.000 |
| C007 | 31 | 31.0 | 500.000.000 | 500.000.000 |
| C008 | 29 | 29.0 | 4.700.000 | 4.700.000 |
| C009 | 22 | 22.0 | 4.600.000 | 4.600.000 |
| C010 | 35 | 35.0 | 5.300.000 | 5.300.000 |
| C011 | 28 | 28.0 | NA | 4.900.000 |
Penjelasan: nilai hilang pada usia
milik C006 diisi dengan median usia, sedangkan pendapatan
hilang milik C002 dan C011 diisi dengan median
pendapatan. Lalu, kolom asli tetap dipertahankan agar perbedaan antara
data asli dan data hasil imputasi dapat dilihat.
pelanggan$kota_imputasi <- pelanggan$kota
pelanggan$kota_imputasi[is.na(pelanggan$kota_imputasi)] <- "Tidak diketahui"
pelanggan$usia_missing <- as.integer(is.na(pelanggan$usia))
pelanggan$pendapatan_missing <- as.integer(is.na(pelanggan$pendapatan))
pelanggan$kota_missing <- as.integer(is.na(pelanggan$kota))
frekuensi_kota <- as.data.frame(table(pelanggan$kota_imputasi), stringsAsFactors = FALSE)
names(frekuensi_kota) <- c("kota", "jumlah")
indikator_missing <- data.frame(
indikator = c("usia_missing", "pendapatan_missing", "kota_missing"),
jumlah_nilai_asli_hilang = c(sum(pelanggan$usia_missing),
sum(pelanggan$pendapatan_missing),
sum(pelanggan$kota_missing))
)
catatan_log("Missing value", "kota", "Mengganti NA dengan Tidak diketahui",
"Ketidakpastian lokasi tidak disamarkan menjadi kota tertentu")
knitr::kable(frekuensi_kota, caption = "Distribusi kota setelah imputasi kategorik")
| kota | jumlah |
|---|---|
| Dumai | 3 |
| Pekanbaru | 6 |
| Siak | 1 |
| Tidak diketahui | 1 |
knitr::kable(indikator_missing, caption = "Indikator nilai yang semula hilang")
| indikator | jumlah_nilai_asli_hilang |
|---|---|
| usia_missing | 1 |
| pendapatan_missing | 2 |
| kota_missing | 1 |
Penjelasan: kota yang hilang tidak diisi dengan
modus karena hal tersebut dapat membuat Pekanbaru tampak lebih dominan
secara artifisial. Label Tidak diketahui mempertahankan
makna bahwa informasi lokasi tidak tersedia. Indikator missing menjaga
informasi bahwa nilai tersebut awalnya kosong meskipun kolom imputasi
sudah lengkap.
par(mfrow = c(1, 2), mar = c(4, 4, 3, 1))
hist(pelanggan$pendapatan,
main = "Sebelum imputasi", xlab = "Pendapatan",
col = "#9ecae1", border = "white", breaks = 8)
hist(pelanggan$pendapatan_imputasi,
main = "Sesudah imputasi median", xlab = "Pendapatan",
col = "#a1d99b", border = "white", breaks = 8)
par(mfrow = c(1, 1))
Penjelasan: imputasi median menambahkan nilai pada posisi missing tanpa mengubah nilai pendapatan yang sudah tersedia. Histogram masih dipengaruhi oleh pendapatan ekstrem C007, sehingga tahap outlier tetap diperlukan sebelum menentukan transformasi yang paling sesuai.
boxplot(pelanggan$pendapatan_imputasi, horizontal = TRUE,
col = "#9ecae1", border = "#2b4c6f",
main = "Boxplot pendapatan setelah imputasi",
xlab = "Pendapatan")
q1 <- quantile(pelanggan$pendapatan_imputasi, 0.25, na.rm = TRUE)
q3 <- quantile(pelanggan$pendapatan_imputasi, 0.75, na.rm = TRUE)
iqr_pendapatan <- IQR(pelanggan$pendapatan_imputasi, na.rm = TRUE)
batas_bawah <- q1 - 1.5 * iqr_pendapatan
batas_atas <- q3 + 1.5 * iqr_pendapatan
batas_iqr <- data.frame(
ukuran = c("Q1", "Q3", "IQR", "Batas bawah", "Batas atas"),
nilai = c(q1, q3, iqr_pendapatan, batas_bawah, batas_atas),
nilai_format = fmt_rupiah(c(q1, q3, iqr_pendapatan, batas_bawah, batas_atas))
)
pelanggan$outlier_pendapatan <- pelanggan$pendapatan_imputasi < batas_bawah |
pelanggan$pendapatan_imputasi > batas_atas
outlier_tampil <- pelanggan[pelanggan$outlier_pendapatan,
c("customer_id", "nama", "pendapatan_imputasi", "outlier_pendapatan")]
outlier_tampil$pendapatan_imputasi <- fmt_rupiah(outlier_tampil$pendapatan_imputasi)
knitr::kable(batas_iqr[, c("ukuran", "nilai_format")],
col.names = c("Ukuran", "Nilai"), caption = "Batas deteksi outlier berbasis IQR")
| Ukuran | Nilai |
|---|---|
| Q1 | 4.750.000 |
| Q3 | 5.150.000 |
| IQR | 400.000 |
| Batas bawah | 4.150.000 |
| Batas atas | 5.750.000 |
knitr::kable(outlier_tampil, caption = "Observasi yang ditandai sebagai kandidat outlier")
| customer_id | nama | pendapatan_imputasi | outlier_pendapatan | |
|---|---|---|---|---|
| 7 | C007 | Gilang | 500.000.000 | TRUE |
Penjelasan: aturan IQR menandai C007
sebagai kandidat outlier karena pendapatannya jauh lebih tinggi daripada
pelanggan lain. Namun demikian, status outlier adalah sinyal untuk
pemeriksaan lebih lanjut, bukan otomatis menjadi bukti bahwa data
tersebut salah.
pelanggan$pendapatan_winsor <- pmin(
pmax(pelanggan$pendapatan_imputasi, batas_bawah),
batas_atas
)
perbandingan_outlier <- pelanggan[, c("customer_id", "pendapatan_imputasi",
"outlier_pendapatan", "pendapatan_winsor")]
perbandingan_outlier$pendapatan_imputasi <- fmt_rupiah(perbandingan_outlier$pendapatan_imputasi)
perbandingan_outlier$pendapatan_winsor <- fmt_rupiah(perbandingan_outlier$pendapatan_winsor)
catatan_log("Outlier", "pendapatan", "Menandai outlier dan membuat versi winsorized",
"Nilai ekstrem dipertahankan karena mungkin merupakan pelanggan valid")
knitr::kable(perbandingan_outlier, caption = "Nilai asli dibandingkan dengan nilai winsorized")
| customer_id | pendapatan_imputasi | outlier_pendapatan | pendapatan_winsor |
|---|---|---|---|
| C001 | 4.500.000 | FALSE | 4.500.000 |
| C002 | 4.900.000 | FALSE | 4.900.000 |
| C003 | 5.200.000 | FALSE | 5.200.000 |
| C004 | 4.800.000 | FALSE | 4.800.000 |
| C005 | 4.900.000 | FALSE | 4.900.000 |
| C006 | 5.100.000 | FALSE | 5.100.000 |
| C007 | 500.000.000 | TRUE | 5.750.000 |
| C008 | 4.700.000 | FALSE | 4.700.000 |
| C009 | 4.600.000 | FALSE | 4.600.000 |
| C010 | 5.300.000 | FALSE | 5.300.000 |
| C011 | 4.900.000 | FALSE | 4.900.000 |
Penjelasan: nilai asli tetap tersedia untuk analisis
bisnis, sedangkan pendapatan_winsor disediakan untuk
analisis statistik yang sensitif terhadap nilai ekstrem. Strategi ini
lebih informatif daripada langsung menghapus C007 karena kemungkinan
pelanggan tersebut memang berada pada segmen premium.
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
}
decimal_scale <- function(x) {
maksimum <- max(abs(x), na.rm = TRUE)
if (maksimum == 0) return(x)
j <- ceiling(log10(maksimum + 1))
x / (10^j)
}
pelanggan$usia_minmax <- minmax(pelanggan$usia_imputasi)
pelanggan$pendapatan_minmax <- minmax(pelanggan$pendapatan_imputasi)
pelanggan$usia_z <- as.numeric(scale(pelanggan$usia_imputasi))
pelanggan$pendapatan_z <- as.numeric(scale(pelanggan$pendapatan_imputasi))
pelanggan$pendapatan_decimal <- decimal_scale(pelanggan$pendapatan_imputasi)
# kompresi skala menggunakan log1p.
pelanggan$pendapatan_log1p <- log1p(pelanggan$pendapatan_imputasi)
transformasi_tampil <- data.frame(
customer_id = pelanggan$customer_id,
pendapatan_asli = fmt_rupiah(pelanggan$pendapatan_imputasi),
minmax = fmt_num(pelanggan$pendapatan_minmax, 6),
z_score = fmt_num(pelanggan$pendapatan_z, 4),
decimal_scaling = fmt_num(pelanggan$pendapatan_decimal, 6),
log1p = fmt_num(pelanggan$pendapatan_log1p, 4)
)
knitr::kable(transformasi_tampil, caption = "Perbandingan beberapa metode transformasi pendapatan")
| customer_id | pendapatan_asli | minmax | z_score | decimal_scaling | log1p |
|---|---|---|---|---|---|
| C001 | 4.500.000 | 0.000000 | -0.3041 | 0.004500 | 15.3196 |
| C002 | 4.900.000 | 0.000807 | -0.3014 | 0.004900 | 15.4047 |
| C003 | 5.200.000 | 0.001413 | -0.2994 | 0.005200 | 15.4642 |
| C004 | 4.800.000 | 0.000605 | -0.3021 | 0.004800 | 15.3841 |
| C005 | 4.900.000 | 0.000807 | -0.3014 | 0.004900 | 15.4047 |
| C006 | 5.100.000 | 0.001211 | -0.3001 | 0.005100 | 15.4448 |
| C007 | 500.000.000 | 1.000000 | 3.0151 | 0.500000 | 20.0301 |
| C008 | 4.700.000 | 0.000404 | -0.3028 | 0.004700 | 15.3631 |
| C009 | 4.600.000 | 0.000202 | -0.3035 | 0.004600 | 15.3416 |
| C010 | 5.300.000 | 0.001615 | -0.2988 | 0.005300 | 15.4832 |
| C011 | 4.900.000 | 0.000807 | -0.3014 | 0.004900 | 15.4047 |
Penjelasan: min–max menghasilkan rentang 0–1 tetapi
sangat dipengaruhi oleh nilai maksimum C007. Z-score menunjukkan jarak
relatif terhadap rata-rata. Decimal scaling mengecilkan digit tanpa
memusatkan data. Kemudian, transformasi log1p mengompresi
perbedaan skala sehingga jarak antara pendapatan ekstrem dan pendapatan
normal tidak terlalu mendominasi.
pendapatan_asli_minmax <- minmax(pelanggan$pendapatan_imputasi)
pendapatan_winsor_minmax <- minmax(pelanggan$pendapatan_winsor)
plot(pendapatan_asli_minmax, pendapatan_winsor_minmax,
pch = 19, col = "#1f4e79",
xlab = "Min–max pendapatan asli",
ylab = "Min–max pendapatan winsorized",
main = "Perbandingan skala sebelum dan sesudah winsorization")
abline(0, 1, col = "#d62728", lty = 2, lwd = 2)
Penjelasan: perbandingan menunjukkan bahwa satu nilai ekstrem dapat membuat sebagian besar pendapatan normal berkumpul sangat dekat dengan nol pada skala min–max. Versi winsorized disini telah memberi rentang yang lebih proporsional. Namun, perlu di-ingat bahwa pemilihan versi data tetap bergantung pada tujuan analisis, bukan hanya semata-mata pada tampilan grafik.
transaksi <- transaksi_raw
pemeriksaan_kunci <- data.frame(
pemeriksaan = c("Duplikasi customer_id pelanggan",
"Duplikasi cust_id transaksi",
"Pelanggan tanpa transaksi",
"Transaksi tanpa pelanggan"),
hasil = c(
sum(duplicated(pelanggan$customer_id)),
sum(duplicated(transaksi$cust_id)),
paste(setdiff(pelanggan$customer_id, transaksi$cust_id), collapse = ", "),
paste(setdiff(transaksi$cust_id, pelanggan$customer_id), collapse = ", ")
)
)
knitr::kable(pemeriksaan_kunci, caption = "Pemeriksaan kualitas key sebelum integrasi")
| pemeriksaan | hasil |
|---|---|
| Duplikasi customer_id pelanggan | 0 |
| Duplikasi cust_id transaksi | 0 |
| Pelanggan tanpa transaksi | C011 |
| Transaksi tanpa pelanggan | C012 |
Penjelasan: customer_id pada data
pelanggan sudah unik dan cust_id pada data transaksi juga
tidak berulang. C011 tidak memiliki transaksi, sedangkan
C012 tidak memiliki data pelanggan. Kedua kondisi ini harus
diketahui sebelum proses join agar missing value hasil integrasi tidak
disalahartikan.
names(transaksi)[names(transaksi) == "cust_id"] <- "customer_id"
nama_key <- data.frame(
sumber = c("Data pelanggan", "Data transaksi"),
nama_key_setelah_penyelarasan = c("customer_id", names(transaksi)[1])
)
knitr::kable(nama_key, caption = "Penyamaan nama identifier antar-sumber")
| sumber | nama_key_setelah_penyelarasan |
|---|---|
| Data pelanggan | customer_id |
| Data transaksi | customer_id |
Penjelasan: kedua sumber sekarang menggunakan nama
key yang sama, yaitu customer_id. Penyamaan nama mengurangi
risiko salah menyebut kolom ketika melakukan integrasi dan membuat kode
join lebih mudah dibaca.
data_terintegrasi <- merge(
pelanggan,
transaksi,
by = "customer_id",
all.x = TRUE,
sort = FALSE
)
hasil_join <- data_terintegrasi[, c("customer_id", "nama",
"jumlah_transaksi", "total_purchase")]
hasil_join$total_purchase <- fmt_rupiah(hasil_join$total_purchase)
knitr::kable(hasil_join, caption = "Hasil left join data pelanggan dan transaksi")
| customer_id | nama | jumlah_transaksi | total_purchase |
|---|---|---|---|
| C001 | Ani | 5 | 1.500.000 |
| C002 | Budi | 3 | 900.000 |
| C003 | Citra | 7 | 2.700.000 |
| C004 | Dodi | 2 | 600.000 |
| C005 | Eka | 6 | 2.100.000 |
| C006 | Fani | 4 | 1.300.000 |
| C007 | Gilang | 20 | 25.000.000 |
| C008 | Hana | 5 | 1.700.000 |
| C009 | Indra | 3 | 800.000 |
| C010 | Joko | 8 | 3.200.000 |
| C011 | Kiki | NA | NA |
Penjelasan: all.x = TRUE mempertahankan
seluruh pelanggan dari tabel kiri. Oleh karena itu, C011
tetap muncul meskipun kolom transaksi masih NA. Kemudian,
transaksi C012 tidak masuk ke data akhir karena transaksi
tersebut tidak memiliki pasangan pada data master pelanggan.
validasi_join <- data.frame(
pemeriksaan = c("Jumlah baris sebelum join", "Jumlah baris sesudah join",
"Duplikasi customer_id sesudah join",
"Missing jumlah_transaksi", "Missing total_purchase"),
hasil = c(
nrow(pelanggan),
nrow(data_terintegrasi),
sum(duplicated(data_terintegrasi$customer_id)),
sum(is.na(data_terintegrasi$jumlah_transaksi)),
sum(is.na(data_terintegrasi$total_purchase))
)
)
pelanggan_tanpa_transaksi <- data_terintegrasi[
is.na(data_terintegrasi$jumlah_transaksi), c("customer_id", "nama")
]
knitr::kable(validasi_join, caption = "Validasi struktur hasil integrasi")
| pemeriksaan | hasil |
|---|---|
| Jumlah baris sebelum join | 11 |
| Jumlah baris sesudah join | 11 |
| Duplikasi customer_id sesudah join | 0 |
| Missing jumlah_transaksi | 1 |
| Missing total_purchase | 1 |
knitr::kable(pelanggan_tanpa_transaksi, caption = "Pelanggan tanpa pasangan transaksi")
| customer_id | nama | |
|---|---|---|
| 11 | C011 | Kiki |
Penjelasan: jumlah baris tetap 11 dan tidak ada
duplikasi key baru. Satu missing value muncul pada masing-masing atribut
transaksi karena C011 tidak memiliki pasangan transaksi.
Ini adalah missing value akibat proses integrasi, bukan missing value
dari data pelanggan awal.
# Pada skenario praktikum, NA transaksi ditafsirkan sebagai 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
transaksi_final_tampil <- data_terintegrasi[, c("customer_id", "nama",
"jumlah_transaksi",
"jumlah_transaksi_final",
"total_purchase",
"total_purchase_final")]
transaksi_final_tampil$total_purchase <- fmt_rupiah(transaksi_final_tampil$total_purchase)
transaksi_final_tampil$total_purchase_final <- fmt_rupiah(transaksi_final_tampil$total_purchase_final)
knitr::kable(transaksi_final_tampil, caption = "Perbandingan transaksi sebelum dan sesudah pengisian NA")
| customer_id | nama | jumlah_transaksi | jumlah_transaksi_final | total_purchase | total_purchase_final |
|---|---|---|---|---|---|
| C001 | Ani | 5 | 5 | 1.500.000 | 1.500.000 |
| C002 | Budi | 3 | 3 | 900.000 | 900.000 |
| C003 | Citra | 7 | 7 | 2.700.000 | 2.700.000 |
| C004 | Dodi | 2 | 2 | 600.000 | 600.000 |
| C005 | Eka | 6 | 6 | 2.100.000 | 2.100.000 |
| C006 | Fani | 4 | 4 | 1.300.000 | 1.300.000 |
| C007 | Gilang | 20 | 20 | 25.000.000 | 25.000.000 |
| C008 | Hana | 5 | 5 | 1.700.000 | 1.700.000 |
| C009 | Indra | 3 | 3 | 800.000 | 800.000 |
| C010 | Joko | 8 | 8 | 3.200.000 | 3.200.000 |
| C011 | Kiki | NA | 0 | NA | 0 |
Penjelasan: pengisian NA menjadi nol hanya dilakukan setelah makna bisnis ditentukan. Pada skenario ini, NA dianggap berarti pelanggan belum pernah bertransaksi. Jika dalam kasus nyata NA berarti data gagal dikumpulkan, maka nilai tersebut seharusnya tetap NA atau diberi indikator khusus.
data_final <- data_terintegrasi[, c(
"customer_id", "nama", "usia_imputasi", "kota_imputasi", "status",
"pendapatan_imputasi", "pendapatan_missing", "outlier_pendapatan",
"usia_minmax", "pendapatan_minmax", "pendapatan_z",
"pendapatan_decimal", "pendapatan_log1p", "pendapatan_winsor",
"jumlah_transaksi_final", "total_purchase_final"
)]
names(data_final)[names(data_final) == "usia_imputasi"] <- "usia"
names(data_final)[names(data_final) == "kota_imputasi"] <- "kota"
# Tabel tampilan dipecah menjadi dua agar terbaca pada layar biasa.
# File CSV tetap menyimpan seluruh atribut teknis dan hasil transformasi.
data_final_profil <- data_final[, c("customer_id", "nama", "usia", "kota", "status",
"pendapatan_imputasi", "pendapatan_missing",
"outlier_pendapatan")]
data_final_profil$pendapatan_imputasi <- fmt_rupiah(data_final_profil$pendapatan_imputasi)
data_final_transaksi <- data_final[, c("customer_id", "jumlah_transaksi_final",
"total_purchase_final")]
data_final_transaksi$total_purchase_final <- fmt_rupiah(data_final_transaksi$total_purchase_final)
knitr::kable(data_final_profil, caption = "Dataset akhir: profil pelanggan")
| customer_id | nama | usia | kota | status | pendapatan_imputasi | pendapatan_missing | outlier_pendapatan |
|---|---|---|---|---|---|---|---|
| C001 | Ani | 21.0 | Pekanbaru | Aktif | 4.500.000 | 0 | FALSE |
| C002 | Budi | 25.0 | Pekanbaru | Aktif | 4.900.000 | 1 | FALSE |
| C003 | Citra | 23.0 | Pekanbaru | Aktif | 5.200.000 | 0 | FALSE |
| C004 | Dodi | 50.0 | Dumai | Aktif | 4.800.000 | 0 | FALSE |
| C005 | Eka | 27.0 | Pekanbaru | Tidak Aktif | 4.900.000 | 0 | FALSE |
| C006 | Fani | 27.5 | Dumai | Tidak Aktif | 5.100.000 | 0 | FALSE |
| C007 | Gilang | 31.0 | Pekanbaru | Aktif | 500.000.000 | 0 | TRUE |
| C008 | Hana | 29.0 | Siak | Aktif | 4.700.000 | 0 | FALSE |
| C009 | Indra | 22.0 | Pekanbaru | Aktif | 4.600.000 | 0 | FALSE |
| C010 | Joko | 35.0 | Dumai | Tidak Aktif | 5.300.000 | 0 | FALSE |
| C011 | Kiki | 28.0 | Tidak diketahui | Aktif | 4.900.000 | 1 | FALSE |
knitr::kable(data_final_transaksi, caption = "Dataset akhir: ringkasan transaksi")
| customer_id | jumlah_transaksi_final | total_purchase_final |
|---|---|---|
| C001 | 5 | 1.500.000 |
| C002 | 3 | 900.000 |
| C003 | 7 | 2.700.000 |
| C004 | 2 | 600.000 |
| C005 | 6 | 2.100.000 |
| C006 | 4 | 1.300.000 |
| C007 | 20 | 25.000.000 |
| C008 | 5 | 1.700.000 |
| C009 | 3 | 800.000 |
| C010 | 8 | 3.200.000 |
| C011 | 0 | 0 |
Penjelasan: Disini dataset akhir tetap mempertahankan 11 pelanggan unik. Kemudian, kolom utamanya sudah bersih dari missing value, kota juga sudah memiliki label yang konsisten, status dengan hanya dua kategori, dan transaksi pelanggan tanpa pasangan telah direpresentasikan sebagai nol sesuai asumsi skenario. Atribut teknis seperti indikator missing, status outlier, dan hasil transformasi juga tetap tersedia untuk analisis lanjutan.
audit_akhir <- audit_data(data_final)
validasi_akhir <- data.frame(
pemeriksaan = c(
"Jumlah baris akhir sama dengan pelanggan setelah deduplikasi",
"customer_id unik",
"Tidak ada missing value pada dataset akhir",
"Seluruh usia berada pada rentang 15–100",
"Seluruh pendapatan imputasi tidak negatif",
"Jumlah transaksi final tidak negatif",
"Total purchase final tidak negatif"
),
status = c(
nrow(data_final) == nrow(pelanggan),
sum(duplicated(data_final$customer_id)) == 0,
sum(is.na(data_final)) == 0,
all(data_final$usia >= 15 & data_final$usia <= 100),
all(data_final$pendapatan_imputasi >= 0),
all(data_final$jumlah_transaksi_final >= 0),
all(data_final$total_purchase_final >= 0)
)
)
validasi_akhir$status <- ifelse(validasi_akhir$status, "LULUS", "PERLU DIPERIKSA")
knitr::kable(audit_akhir, digits = 2, caption = "Audit kualitas dataset akhir")
| atribut | tipe | jumlah_missing | persen_missing | jumlah_unik |
|---|---|---|---|---|
| customer_id | character | 0 | 0 | 11 |
| nama | character | 0 | 0 | 11 |
| usia | numeric | 0 | 0 | 11 |
| kota | character | 0 | 0 | 4 |
| status | character | 0 | 0 | 2 |
| pendapatan_imputasi | numeric | 0 | 0 | 9 |
| pendapatan_missing | integer | 0 | 0 | 2 |
| outlier_pendapatan | logical | 0 | 0 | 2 |
| usia_minmax | numeric | 0 | 0 | 11 |
| pendapatan_minmax | numeric | 0 | 0 | 9 |
| pendapatan_z | numeric | 0 | 0 | 9 |
| pendapatan_decimal | numeric | 0 | 0 | 9 |
| pendapatan_log1p | numeric | 0 | 0 | 9 |
| pendapatan_winsor | numeric | 0 | 0 | 9 |
| jumlah_transaksi_final | numeric | 0 | 0 | 9 |
| total_purchase_final | numeric | 0 | 0 | 11 |
knitr::kable(validasi_akhir, caption = "Validasi otomatis dataset akhir")
| pemeriksaan | status |
|---|---|
| Jumlah baris akhir sama dengan pelanggan setelah deduplikasi | LULUS |
| customer_id unik | LULUS |
| Tidak ada missing value pada dataset akhir | LULUS |
| Seluruh usia berada pada rentang 15–100 | LULUS |
| Seluruh pendapatan imputasi tidak negatif | LULUS |
| Jumlah transaksi final tidak negatif | LULUS |
| Total purchase final tidak negatif | LULUS |
if (any(validasi_akhir$status != "LULUS")) {
stop("Terdapat pemeriksaan validasi yang belum lulus.")
}
Penjelasan: audit akhir menunjukkan tidak ada
missing value dan tidak ada duplikasi customer_id. Validasi
otomatis memastikan hasil preprocessing tidak hanya tampak rapi secara
visual, tetapi juga memenuhi aturan struktural dan domain yang telah
ditetapkan.
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))
)
)
knitr::kable(perbandingan, caption = "Perbandingan kualitas sebelum dan sesudah preprocessing")
| indikator | sebelum | sesudah |
|---|---|---|
| Jumlah baris | 12 | 11 |
| Duplikasi customer_id | 1 | 0 |
| Total missing value | 4 | 0 |
| Kategori kota unik | 10 | 4 |
| Kategori status unik | 8 | 2 |
Penjelasan: jumlah baris turun dari 12 menjadi 11 karena satu duplikasi dihapus. Duplikasi key turun dari 1 menjadi 0, total missing value turun dari 4 menjadi 0, kategori kota menjadi lebih konsisten, dan kategori status menyusut menjadi dua kategori baku. Perubahan ini menunjukkan bahwa data lebih siap dianalisis, tetapi tetap harus digunakan bersama dokumentasi keputusan preprocessing.
write.csv(data_final, "data_pelanggan_setelah_preprocessing.csv", row.names = FALSE)
write.csv(log_perubahan, "log_perubahan_preprocessing.csv", row.names = FALSE)
write.csv(audit_awal, "audit_awal.csv", row.names = FALSE)
write.csv(audit_akhir, "audit_akhir.csv", row.names = FALSE)
file_hasil <- data.frame(
nama_file = c("data_pelanggan_setelah_preprocessing.csv",
"log_perubahan_preprocessing.csv",
"audit_awal.csv", "audit_akhir.csv"),
fungsi = c("Dataset final untuk analisis",
"Riwayat keputusan preprocessing",
"Audit sebelum preprocessing", "Audit sesudah preprocessing"),
lokasi = getwd()
)
knitr::kable(file_hasil, caption = "File keluaran yang berhasil disimpan")
| nama_file | fungsi | lokasi |
|---|---|---|
| data_pelanggan_setelah_preprocessing.csv | Dataset final untuk analisis | C:/Users/IDEAPAD/Downloads |
| log_perubahan_preprocessing.csv | Riwayat keputusan preprocessing | C:/Users/IDEAPAD/Downloads |
| audit_awal.csv | Audit sebelum preprocessing | C:/Users/IDEAPAD/Downloads |
| audit_akhir.csv | Audit sesudah preprocessing | C:/Users/IDEAPAD/Downloads |
Penjelasan: empat file di atas disimpan agar hasil analisis dan prosesnya dapat digunakan kembali (direproduksi). File dataset final dapat digunakan untuk analisis, sedangkan file log dan dua audit berfungsi sebagai dokumentasi serta bukti bahwa perubahan data dapat ditelusuri.
Tidak selalu. Dataset tanpa missing value memang lebih mudah diproses, tetapi kualitasnya bergantung pada cara missing value ditangani. Menghapus semua baris tidak lengkap dapat mengurangi ukuran sampel dan menghilangkan kelompok tertentu. Imputasi juga dapat mengurangi variasi apabila satu nilai dipaksakan ke banyak baris. Pada praktikum ini, imputasi median digunakan untuk variabel numerik dan label tidak diketahui untuk kota, sedangkan indikator missing tetap disimpan.
Outlier dapat merupakan kesalahan input, tetapi dapat juga merupakan observasi yang benar dan memiliki makna bisnis. Pendapatan C007 yang sangat besar mungkin saja menunjukkan pelanggan premium. Karena itu, nilai tersebut ditandai, bukan langsung dihapus. Versi winsorized dibuat sebagai alternatif analisis, sementara nilai asli tetap disimpan untuk menjaga informasi.
Bias dapat muncul ketika baris tertentu lebih sering dihapus, kategori yang hilang diganti secara tidak tepat, atau nilai ekstrem selalu dianggap salah. Misalnya, menghapus seluruh pelanggan dengan pendapatan hilang dapat mengurangi representasi pelanggan yang datanya tidak lengkap. Mengganti kota hilang dengan Pekanbaru juga dapat memperbesar proporsi kota tersebut secara artifisial. Setiap keputusan harus dikaitkan dengan konteks data dan dicatat.
Dalam analisis prediktif, mean, median, minimum, maksimum, parameter z-score, dan batas outlier harus dihitung dari data pelatihan saja. Jika data uji ikut digunakan saat menghitung parameter, informasi dari data uji masuk ke proses pelatihan dan menyebabkan kebocoran data. Akibatnya, kinerja model dapat terlihat lebih baik daripada kemampuan sebenarnya pada data baru.
Identifier yang tidak unik dapat menyebabkan satu baris pelanggan bergabung dengan beberapa baris transaksi secara tidak terkendali. Nilai transaksi dapat terhitung berulang dan jumlah baris hasil join membengkak. Identifier yang tidak cocok juga dapat menyebabkan data transaksi atau pelanggan hilang. Karena itu, keunikan key, perbedaan key antarsumber, jumlah baris sebelum–sesudah join, dan missing value hasil join harus selalu diperiksa.
Alur preprocessing dalam laporan ini adalah memahami struktur dan kualitas awal, membuat salinan kerja, membersihkan spasi serta kategori, menangani duplikasi dan pelanggaran domain, membandingkan strategi missing value, melakukan imputasi dengan indikator missing, mendeteksi dan mengevaluasi outlier, melakukan beberapa transformasi numerik, mengintegrasikan data melalui key yang tervalidasi, serta mengaudit dataset akhir.
Han, J., Kamber, M., & Pei, J. (2012). Data Mining: Concepts and Techniques (3rd ed.). Morgan Kaufmann. Chapter 3: Data Preprocessing.