1 Identitas Praktikum

Komponen Keterangan
Mata kuliah Data Mining
Topik Data Preprocessing
Nama Hanna Toula Sinaga
NIM 2403113999
Kelas Statistika A 2024

2 Pendahuluan

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.

3 Persiapan

3.1 Pemeriksaan Lingkungan Kerja

lingkungan <- data.frame(
  pemeriksaan = c("Direktori kerja", "Versi R"),
  hasil = c(getwd(), R.version.string),
  stringsAsFactors = FALSE
)
knitr::kable(lingkungan, caption = "Informasi lingkungan pengerjaan")
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.

4 Membangun Dataset Praktikum

4.1 Dataset Pelanggan dan Transaksi

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")
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")
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.

5 Bagian I: Konsep Data Preprocessing

5.1 Memahami Struktur Data

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")
Dimensi data pelanggan
komponen nilai
Jumlah baris 12
Jumlah kolom 6
knitr::kable(struktur, caption = "Struktur atribut pelanggan")
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")
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.

5.2 Mengukur Kualitas 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")
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")
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.

6 Bagian II: Implementasi Data Cleaning

6.1 Membuat Salinan Kerja

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")
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.

6.2 Membersihkan Spasi dan Kapitalisasi

# 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()")
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.

6.3 Menyeragamkan Kategori

# 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")
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.

6.4 Mendeteksi dan Menghapus Duplikasi

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")
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")
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.

6.5 Memeriksa Aturan Domain

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")
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")
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.

6.6 Mengoreksi Nilai Domain dan Membuat Log Perubahan

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")
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")
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.

7 Bagian III: Penanganan Missing Values

7.1 Mengidentifikasi Lokasi Nilai Hilang

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")
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")
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.

7.2 Strategi 1: Menghapus Baris yang Tidak Lengkap

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()")
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.

7.3 Strategi 2: Membandingkan Mean dan Median

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")
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.

7.4 Imputasi Numerik dengan Median

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")
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.

7.5 Strategi 3: Imputasi Kategorik dan Indikator Missing

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")
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 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.

7.6 Membandingkan Distribusi Sebelum dan Sesudah Imputasi

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.

8 Bagian IV: Penanganan Outlier

8.1 Visualisasi dan Batas IQR

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")
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")
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.

8.2 Mempertahankan Nilai Asli dan Membuat Versi Winsorized

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")
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.

9 Bagian V: Transformasi Data

9.1 Min–max, Z-Score, Decimal Scaling, dan log1p

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")
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.

9.2 Dampak Outlier Terhadap Min–Max

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.

10 Bagian VI: Integrasi Data

10.1 Memeriksa Kunci Sebelum Join

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 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.

10.2 Menyelaraskan Nama Identifier

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")
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.

10.3 Left Join dengan Merge()

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")
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.

10.4 Memvalidasi Hasil Integrasi

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")
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")
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.

10.5 Menentukan Makna NA Transaksi dan Mengisi Nilai Final

# 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")
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.

11 Dataset Akhir dan Evaluasi

11.1 Memilih atribut akhir

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")
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")
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.

11.2 Audit Akhir dan Validasi Otomatis

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")
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")
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.

11.3 Perbandingan Sebelum dan Sesudah 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))
  )
)
knitr::kable(perbandingan, caption = "Perbandingan kualitas sebelum dan sesudah preprocessing")
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.

11.4 Menyimpan Hasil

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")
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.

12 Pertanyaan Refleksi

12.1 1. Apakah dataset tanpa missing value selalu lebih berkualitas?

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.

12.2 2. Mengapa outlier tidak boleh otomatis dihapus?

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.

12.3 3. Bagaimana preprocessing dapat menimbulkan bias?

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.

12.4 4. Mengapa parameter imputasi dan transformasi seharusnya dihitung dari data pelatihan?

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.

12.5 5. Apa risiko integrasi data jika identifier tidak unik?

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.

13 Ringkasan

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.

14 Daftar Pustaka

Han, J., Kamber, M., & Pei, J. (2012). Data Mining: Concepts and Techniques (3rd ed.). Morgan Kaufmann. Chapter 3: Data Preprocessing.