1 Pendahuluan

Tahukah kamu apa itu Data Mining? Data mining adalah proses pengumpulan dan pengolaan data berkuran besar untuk menemukan pola, tren, atau informasi tersembunyi yang berguna. Dengan kata lain, data mining seperti detektif untuk melihat makna tersembunyi yang ada dalam data kita. Dalam prosesnya, terdapat tahapan yang dikenal sebagai data preprocessing yang bertugas untuk mengolah data mentah kita menjadi data yang layak dianalisis.

Setelah menyelesaikan praktikum ini, kita diharapkan mampu:

  • menjelaskan kondisi data yang menyebabkan preprocessing diperlukan
  • mengidentifikasi masalah kualitas data menggunakan R
  • menerapkan data cleaning secara sistematis
  • memilih dan menerapkan strategi penanganan missing values
  • mendeteksi serta mengevaluasi outlier
  • melakukan transformasi atribut numerik
  • mengintegrasikan dua sumber data serta memvalidasi hasilnya

Catatan: Prinsip utama adalah jangan membersihkan data hanya karena sebuah nilai terlihat aneh. Perhatikan dahulu alasan, aturan, dan pertimbangan yang jelas meskipun data tersebut tampak seperti outlier.

1.1 Skenario Praktikum

Bayangkan sebuah perusahaan e-commerce akan melakukan analisis pelanggan yang bersumber dari:

  1. Data Pelanggan, memuat identitas, usia, kota, pendapatan, dan status pelanggan
  2. Data Transaksi, memuat jumlah transaksi dan total pembelian

Data ini belum dapat langsung digunakan karena mengandung nilai hilang, inkonsistensi kategori, duplikasi, nilai ekstrem, dan perbedaan nama identifier antarsumber. Disinilah peran kita untuk menyiapkan data tersebut hingga menjadi satu dataset analisis yang bersih dan siap pakai.

1.2 Persiapan RStudio

Sebelum mulai menulis kode, siapkan dahulu proyeknya:

  1. Buka RStudio.
  2. Pilih File → New Project → New Directory → New Project.
  3. Beri nama proyek, misalnya praktikum_preprocessing.
  4. Simpan file R Markdown ini di dalam folder proyek tersebut.
  5. Klik tombol Knit kapan saja untuk melihat hasilnya dalam bentuk HTML.

Periksa terlebih dahulu lokasi di mana file akan tersimpan dan versi R yang sedang digunakan.

getwd()
## [1] "C:/Users/HP/Downloads"
R.version.string
## [1] "R version 4.4.2 (2024-10-31 ucrt)"

Catatan: Tahap ini hanya menggunakan fungsi dasar R (base R) sehingga dapat langsung dijalankan tanpa memasang paket tambahan apa pun.

2 Membangun Dataset Praktikum

Berikut disajikan dataset yang dapat dipraktikkan. Perhatikan apa saja masalah yang terkandung di dalam data seperti usia yang tidak masuk akal, pendapatan yang hilang, penulisan kota yang berantakan, inkonsistensi status, hingga duplikasi data.

pelanggan_raw <- data.frame(
  customer_id = c("C001", "C002", "C003", "C004", "C005", "C006",
                   "C007", "C008", "C009", "C010", "C010", "C011"),
  nama = c("Ani", "Budi", "Citra", "Dodi", "Eka", "Fani",
           "Gilang", "Hana", "Indra", "Joko", "Joko", "Kiki"),
  usia = c(21, 25, 23, 150, 27, NA, 31, 29, 22, 35, 35, 28),
  pendapatan = c(4500000, NA, 5200000, 4800000, 4900000, 5100000,
                 500000000, 4700000, 4600000, 5300000, 5300000, NA),
  kota = c("Pekanbaru", " PKU", "PEKANBARU", "Dumai", "pekanbaru", "DUMAI",
           "Pekanbaru ", "Siak", "PKU", "Dumai", "Dumai", NA),
  status = c("Aktif", "aktif", "ACTIVE", "A", "Tidak Aktif", "nonaktif",
             "Aktif", "AKTIF", "A", "Tidak aktif", "Tidak aktif", "Aktif"),
  stringsAsFactors = FALSE
)

transaksi_raw <- data.frame(
  cust_id = c("C001", "C002", "C003", "C004", "C005", "C006",
              "C007", "C008", "C009", "C010", "C012"),
  jumlah_transaksi = c(5, 3, 7, 2, 6, 4, 20, 5, 3, 8, 1),
  total_purchase = c(1500000, 900000, 2700000, 600000, 2100000, 1300000,
                      25000000, 1700000, 800000, 3200000, 250000),
  stringsAsFactors = FALSE
)

pelanggan_raw
transaksi_raw

Berdasarkan hasil yang ada, dapat dilihat bahwa pada baris ke-10 dan ke-11 pada pelanggan_raw tampak identik, usia 150 tahun pada baris ke-4 tentu tidak wajar, dan penulisan kota “Pekanbaru” muncul dalam berbagai bentuk. Kesalahan seperti ini kerap terjadi dan perlu segera ditangani sebelum memulai analisis.

3 Bagian I - Konsep Data Preprocessing

3.1 Memahami Struktur Data

Langkah paling awal sebelum mengubah apa pun adalah mengenali dahulu struktur, dimensi, tipe atribut, dan beberapa observasi awal dari data yang kita miliki.

dim(pelanggan_raw)
## [1] 12  6
names(pelanggan_raw)
## [1] "customer_id" "nama"        "usia"        "pendapatan"  "kota"       
## [6] "status"
str(pelanggan_raw)
## 'data.frame':    12 obs. of  6 variables:
##  $ customer_id: chr  "C001" "C002" "C003" "C004" ...
##  $ nama       : chr  "Ani" "Budi" "Citra" "Dodi" ...
##  $ usia       : num  21 25 23 150 27 NA 31 29 22 35 ...
##  $ pendapatan : num  4.5e+06 NA 5.2e+06 4.8e+06 4.9e+06 5.1e+06 5.0e+08 4.7e+06 4.6e+06 5.3e+06 ...
##  $ kota       : chr  "Pekanbaru" " PKU" "PEKANBARU" "Dumai" ...
##  $ status     : chr  "Aktif" "aktif" "ACTIVE" "A" ...
head(pelanggan_raw)
summary(pelanggan_raw)
##  customer_id            nama                usia          pendapatan       
##  Length:12          Length:12          Min.   : 21.00   Min.   :  4500000  
##  Class :character   Class :character   1st Qu.: 24.00   1st Qu.:  4725000  
##  Mode  :character   Mode  :character   Median : 28.00   Median :  5000000  
##                                        Mean   : 38.73   Mean   : 54440000  
##                                        3rd Qu.: 33.00   3rd Qu.:  5275000  
##                                        Max.   :150.00   Max.   :500000000  
##                                        NA's   :1        NA's   :2          
##      kota              status         
##  Length:12          Length:12         
##  Class :character   Class :character  
##  Mode  :character   Mode  :character  
##                                       
##                                       
##                                       
## 

Berdasarkan paparan singkat yang ada, diperoleh bahwa nilai maksimum usia mencapai 150, dan nilai maksimum pendapatan melonjak jauh dari nilai-nilai lainnya.

3.2 Mengukur Kualitas Awal

Han, Kamber, dan Pei menjelaskan enam dimensi kualitas data, yaitu accuracy, completeness, consistency, timeliness, believability, dan interpretability. Tidak semua dimensi tersebut dapat dihitung hanya dari isi tabel. Misalnya, accuracy memerlukan pembanding terhadap kondisi sebenarnya, sedangkan timeliness membutuhkan informasi waktu pemutakhiran data.

Karena itu, pemeriksaan awal berikut kita fokuskan pada masalah yang memang bisa dideteksi langsung dari dataset.

# Banyak missing value per atribut
colSums(is.na(pelanggan_raw))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           1           2           1           0
# Persentase missing value per atribut
round(colMeans(is.na(pelanggan_raw)) * 100, 2)
## customer_id        nama        usia  pendapatan        kota      status 
##        0.00        0.00        8.33       16.67        8.33        0.00
# Banyak baris duplikat penuh
sum(duplicated(pelanggan_raw))
## [1] 1
# Banyak customer_id yang berulang
sum(duplicated(pelanggan_raw$customer_id))
## [1] 1
# Kategori yang tercatat pada atribut kota dan status
sort(unique(pelanggan_raw$kota))
## [1] " PKU"       "Dumai"      "DUMAI"      "pekanbaru"  "Pekanbaru" 
## [6] "PEKANBARU"  "Pekanbaru " "PKU"        "Siak"
sort(unique(pelanggan_raw$status))
## [1] "A"           "ACTIVE"      "aktif"       "Aktif"       "AKTIF"      
## [6] "nonaktif"    "Tidak aktif" "Tidak Aktif"
# Rentang atribut numerik
range(pelanggan_raw$usia, na.rm = TRUE)
## [1]  21 150
range(pelanggan_raw$pendapatan, na.rm = TRUE)
## [1] 4.5e+06 5.0e+08

Catatan: Perhatikan bagaimana atribut kota sebenarnya hanya berisi empat kota, tetapi tercatat dalam sembilan variasi penulisan yang berbeda. Begitu pula atribut status yang seharusnya hanya dua kategori.

3.3 Membuat Fungsi Ringkasan Kualitas

Untuk memudahkan dalam penggunaan berulang, jadikan satu fungsi untuk mengefisiensi pengerjaan.

audit_data <- function(data) {
  data.frame(
    atribut = names(data),
    tipe = sapply(data, function(x) class(x)[1]),
    jumlah_missing = sapply(data, function(x) sum(is.na(x))),
    persen_missing = round(sapply(data, function(x) mean(is.na(x)) * 100), 2),
    jumlah_unik = sapply(data, function(x) length(unique(x))),
    row.names = NULL
  )
}

audit_awal <- audit_data(pelanggan_raw)
audit_awal

Catatan: Hasil audit ini bukanlah keputusan cleaning itu sendiri. Audit hanya memberi sinyal atribut mana yang perlu diperiksa lebih lanjut, keputusan penanganannya tetap perlu dipertimbangkan satu per satu.

4 Bagian II - Implementasi Data Cleaning

4.1 Membuat Salinan Kerja

Sebelum mengubah apa pun, kita pertahankan dahulu data mentahnya dan bekerja di atas salinan. Dengan begitu, pelanggan_raw tetap bisa dijadikan pembanding di akhir praktikum nanti.

pelanggan <- pelanggan_raw

4.2 Membersihkan Spasi dan Kapitalisasi

Salah satu penyebab paling umum kategori tampak berbeda padahal maknanya sama adalah spasi tersembunyi di awal atau akhir teks, serta perbedaan huruf besar-kecil. trimws() menangani yang pertama, tolower() menangani yang kedua.

# Menghapus spasi di awal dan akhir
pelanggan$kota <- trimws(pelanggan$kota)
pelanggan$status <- trimws(pelanggan$status)

# Menyeragamkan huruf menjadi kecil untuk mempermudah pencocokan
pelanggan$kota <- tolower(pelanggan$kota)
pelanggan$status <- tolower(pelanggan$status)

sort(unique(pelanggan$kota))
## [1] "dumai"     "pekanbaru" "pku"       "siak"
sort(unique(pelanggan$status))
## [1] "a"           "active"      "aktif"       "nonaktif"    "tidak aktif"

Variasi penulisan sudah berkurang banyak, tetapi belum sepenuhnya seragam. “pku” dan “pekanbaru” misalnya, secara makna adalah kota yang sama namun masih tercatat sebagai dua kategori berbeda.

4.3 Menyeragamkan Kategori

Tahap ini disebut standardisasi kategori dan dilakukan berdasarkan aturan domain. Dalam praktiknya, aturan ini idealnya dikonfirmasi terlebih dulu melalui kamus data atau langsung ke pemilik data, bukan ditebak langsung.

# Standardisasi kota
pelanggan$kota[pelanggan$kota %in% c("pku", "pekanbaru")] <- "Pekanbaru"
pelanggan$kota[pelanggan$kota == "dumai"] <- "Dumai"
pelanggan$kota[pelanggan$kota == "siak"] <- "Siak"

# Standardisasi status
pelanggan$status[pelanggan$status %in% c("aktif", "active", "a")] <- "Aktif"
pelanggan$status[pelanggan$status %in% c("tidak aktif", "nonaktif")] <- "Tidak Aktif"

sort(unique(pelanggan$kota))
## [1] "Dumai"     "Pekanbaru" "Siak"
sort(unique(pelanggan$status))
## [1] "Aktif"       "Tidak Aktif"

Sekarang atribut kota sudah rapi menjadi tiga kategori, dan status menjadi dua kategori sebagaimana seharusnya.

4.4 Mendeteksi dan Menghapus Duplikasi

Selanjutnya kita periksa baris mana saja yang customer_id-nya berulang.

# Menampilkan seluruh baris dengan customer_id yang berulang
pelanggan[duplicated(pelanggan$customer_id) |
            duplicated(pelanggan$customer_id, fromLast = TRUE), ]
# Mempertahankan kemunculan pertama untuk customer_id yang identik
pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL
dim(pelanggan)
## [1] 11  6

Catatan: Menghapus duplikasi seperti ini hanya aman dilakukan jika setiap customer_id memang seharusnya mewakili satu pelanggan. Jika dalam konteks data lain satu pelanggan boleh memiliki banyak baris (misalnya baris per transaksi), tindakan menghapus duplikasi justru keliru dan bisa menghilangkan informasi penting.

4.5 Memeriksa Aturan Domain

Aturan domain adalah batasan nilai yang masuk akal berdasarkan pengetahuan tentang data itu sendiri. Usia pelanggan misalnya, secara wajar berada pada rentang tertentu.

# Kandidat usia tidak valid berdasarkan aturan bisnis 15-100 tahun
pelanggan[pelanggan$usia < 15 | pelanggan$usia > 100, ]
# Kandidat pendapatan tidak valid jika bernilai negatif
pelanggan[pelanggan$pendapatan < 0, ]

Nilai usia 150 pada baris customer_id “C004” jelas melanggar aturan domain yang kita tetapkan. Misalkan setelah ditelusuri ke formulir pendaftaran asli, ternyata nilai yang benar adalah 50.

pelanggan$usia[pelanggan$customer_id == "C004"] <- 50

4.6 Membuat Log Perubahan

Setiap perubahan yang kita lakukan terhadap data sebaiknya dicatat. Log ini berguna sebagai dokumentasi, sekaligus memudahkan siapa pun untuk menelusuri kembali alasan di balik setiap keputusan.

log_perubahan <- data.frame(
  tahap = c("Standardisasi", "Standardisasi", "Deduplikasi", "Koreksi domain"),
  atribut = c("kota", "status", "customer_id", "usia"),
  tindakan = c(
    "PKU dan variasi kapital menjadi Pekanbaru",
    "ACTIVE/A menjadi Aktif; nonaktif menjadi Tidak Aktif",
    "Menghapus kemunculan kedua C010",
    "Usia C004 dikoreksi dari 150 menjadi 50 berdasarkan sumber asli"
  ),
  stringsAsFactors = FALSE
)
log_perubahan

5 Bagian III - Penanganan Missing Values

5.1 Mengidentifikasi Lokasi Nilai Hilang

Sebelum memilih strategi penanganan, kita perlu tahu dulu persis di mana saja nilai yang hilang berada.

colSums(is.na(pelanggan))
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           1           2           1           0
# Baris yang memiliki sedikitnya satu missing value
pelanggan[!complete.cases(pelanggan), ]

Ada tiga baris yang tidak lengkap, masing-masing kehilangan atribut yang berbeda, yaitu: pendapatan, usia, dan kota.

5.2 Strategi 1: Menghapus Baris

Strategi paling sederhana adalah membuang seluruh baris yang tidak lengkap. Kode berikut hanya mendemonstrasikan strategi tersebut secara terpisah; objek pelanggan yang sedang kita kerjakan tidak langsung ditimpa.

pelanggan_complete <- pelanggan[complete.cases(pelanggan), ]
nrow(pelanggan)
## [1] 11
nrow(pelanggan_complete)
## [1] 8
round((1 - nrow(pelanggan_complete) / nrow(pelanggan)) * 100, 2)
## [1] 27.27

Catatan: Menghapus baris berarti kehilangan sekitar seperempat data hanya karena tiga atribut yang berbeda-beda kebetulan hilang pada baris yang berlainan. Untuk dataset sekecil ini, kehilangan sebanyak itu tentu sangat disayangkan. Strategi ini lebih masuk akal ketika proporsi baris tidak lengkap kecil dan datanya berlimpah.

5.3 Strategi 2: Imputasi Mean dan Median

Untuk atribut numerik, nilai yang hilang bisa diisi dengan ukuran pemusatan seperti mean atau median. Sebelum memilih, ada baiknya membandingkan keduanya terlebih dahulu, terutama jika distribusi datanya miring.

mean_pendapatan <- mean(pelanggan$pendapatan, na.rm = TRUE)
median_pendapatan <- median(pelanggan$pendapatan, na.rm = TRUE)
mean_pendapatan
## [1] 59900000
median_pendapatan
## [1] 4900000

Selisih keduanya sangat jauh. Ini karena satu nilai pendapatan yang sangat ekstrem (500 juta) menarik nilai mean jauh ke atas yang menjadi salah satu kelemahan penggunaan mean juga, sementara median relatif tidak terpengaruh. Untuk kasus seperti ini, median adalah pilihan yang lebih stabil.

pelanggan$pendapatan_imputasi <- pelanggan$pendapatan
pelanggan$pendapatan_imputasi[is.na(pelanggan$pendapatan_imputasi)] <- median_pendapatan

# Imputasi usia dengan median
median_usia <- median(pelanggan$usia, na.rm = TRUE)
pelanggan$usia_imputasi <- pelanggan$usia
pelanggan$usia_imputasi[is.na(pelanggan$usia_imputasi)] <- median_usia

pelanggan[, c("customer_id", "usia", "usia_imputasi",
              "pendapatan", "pendapatan_imputasi")]

5.4 Strategi 3: Imputasi Nilai Kategorik

Untuk atribut kategorik seperti kota, mengisi nilai hilang dengan modus bukan selalu pilihan yang tepat, sebab cara ini berpotensi menyembunyikan fakta bahwa data tersebut sebenarnya tidak diketahui. Pada kesempatan ini kita memilih menggunakan kategori eksplisit “Tidak diketahui” agar ketidakpastian tersebut tetap terlihat, bukan tersamarkan.

pelanggan$kota_imputasi <- pelanggan$kota
pelanggan$kota_imputasi[is.na(pelanggan$kota_imputasi)] <- "Tidak diketahui"

table(pelanggan$kota_imputasi, useNA = "ifany")
## 
##           Dumai       Pekanbaru            Siak Tidak diketahui 
##               3               6               1               1

5.5 Menambahkan Indikator Missing

Setelah nilai hilang diisi, jejak bahwa nilai tersebut awalnya hilang bisa saja hilang juga dari catatan kita. Indikator biner berikut menjaga informasi tersebut tetap tersedia untuk analisis lanjutan.

pelanggan$pendapatan_missing <- as.integer(is.na(pelanggan$pendapatan))
table(pelanggan$pendapatan_missing)
## 
## 0 1 
## 9 2

5.6 Membandingkan Distribusi Sebelum dan Sesudah Imputasi

Mengisi nilai yang hilang bukan tindakan tanpa konsekuensi. Mari kita lihat bagaimana bentuk distribusi pendapatan berubah setelah proses imputasi.

par(mfrow = c(1, 2))
hist(pelanggan$pendapatan, main = "Sebelum Imputasi",
     xlab = "Pendapatan", col = "skyblue", breaks = 8)
hist(pelanggan$pendapatan_imputasi, main = "Sesudah Imputasi Median",
     xlab = "Pendapatan", col = "lightgreen", breaks = 8)

par(mfrow = c(1, 1))

Catatan: Imputasi dapat mengubah bentuk distribusi dan mengecilkan variasi data. Oleh karena itu, proses evaluasi tidak boleh dianggap selesai hanya karena seluruh nilai NA sudah terisi.

6 Bagian IV - Penanganan Outlier

6.1 Visualisasi dengan Boxplot

Cara paling cepat untuk melihat keberadaan nilai ekstrem adalah dengan menggunakan boxplot.

boxplot(pelanggan$pendapatan_imputasi, horizontal = TRUE,
        col = "lightblue", main = "Boxplot Pendapatan",
        xlab = "Pendapatan")

Satu titik tampak jauh terpisah dari titik-titik lainnya, menandakan adanya outlier yang perlu diperiksa lebih lanjut.

6.2 Menghitung Batas IQR

Salah satu cara menentukan batas outlier secara kuantitatif adalah dengan aturan IQR (interquartile range): nilai di luar 1.5 kali IQR dari kuartil pertama dan ketiga dianggap sebagai kandidat outlier.

q1 <- quantile(pelanggan$pendapatan_imputasi, 0.25)
q3 <- quantile(pelanggan$pendapatan_imputasi, 0.75)
iqr <- IQR(pelanggan$pendapatan_imputasi)

batas_bawah <- q1 - 1.5 * iqr
batas_atas <- q3 + 1.5 * iqr

c(Q1 = q1, Q3 = q3, IQR = iqr, batas_bawah = batas_bawah, batas_atas = batas_atas)
##          Q1.25%          Q3.75%             IQR batas_bawah.25%  batas_atas.75% 
##         4750000         5150000          400000         4150000         5750000

6.3 Menandai Kandidat Outlier

pelanggan$outlier_pendapatan <- pelanggan$pendapatan_imputasi < batas_bawah |
  pelanggan$pendapatan_imputasi > batas_atas

pelanggan[pelanggan$outlier_pendapatan,
          c("customer_id", "nama", "pendapatan_imputasi")]

6.4 Mengevaluasi Tindakan

Nilai 500 juta pada pelanggan “Gilang” tidak serta-merta kita hapus. Setidaknya ada tiga kemungkinan yang perlu dipertimbangkan terlebih dahulu:

  • Kesalahan input: koreksi nilai tersebut jika angka yang benar dapat diverifikasi dari sumber lain.
  • Observasi valid tetapi ekstrem: pertahankan nilainya, atau gunakan metode analisis yang robust terhadap outlier.
  • Populasi berbeda: pisahkan segmen tersendiri, misalnya jika ternyata pelanggan korporasi tercampur dengan pelanggan ritel.

Pada kesempatan ini, nilai tersebut kita pertahankan apa adanya, dan sebagai pembanding kita buat versi winsorized, yaitu nilai ekstrem yang dibatasi hingga batas atas IQR.

pelanggan$pendapatan_winsor <- pmin(
  pmax(pelanggan$pendapatan_imputasi, batas_bawah),
  batas_atas
)

pelanggan[pelanggan$outlier_pendapatan,
          c("customer_id", "pendapatan_imputasi", "pendapatan_winsor")]

Catatan: winsorizing Di sini hanya sebagai demonstrasi tambahan. Terapkan metode ini hanya jika sesuai dengan tujuan analisis, dan jangan lupa mendokumentasikan bahwa nilai ekstrem telah dibatasi.

7 Bagian V - Transformasi Data

7.1 Normalisasi Min-Maks

Normalisasi min-maks memetakan nilai ke dalam rentang [0, 1] menggunakan rumus berikut.

\[x' = \frac{x-\min(x)}{\max(x)-\min(x)}\]

minmax <- function(x) {
  if (all(is.na(x))) return(rep(NA_real_, length(x)))
  rentang <- max(x, na.rm = TRUE) - min(x, na.rm = TRUE)
  if (rentang == 0) return(rep(0, length(x)))
  (x - min(x, na.rm = TRUE)) / rentang
}

pelanggan$usia_minmax <- minmax(pelanggan$usia_imputasi)
pelanggan$pendapatan_minmax <- minmax(pelanggan$pendapatan_imputasi)

pelanggan[, c("customer_id", "usia", "usia_minmax",
              "pendapatan_imputasi", "pendapatan_minmax")]

7.2 Normalisasi Z-Score

Z-score mengukur seberapa jauh sebuah nilai dari rata-rata, dalam satuan simpangan baku.

\[z = \frac{x-\bar{x}}{s}\]

pelanggan$usia_z <- as.numeric(scale(pelanggan$usia_imputasi))
pelanggan$pendapatan_z <- as.numeric(scale(pelanggan$pendapatan_imputasi))

round(pelanggan[, c("usia_z", "pendapatan_z")], 3)

Perhatikan nilai z-score pendapatan pada baris ketujuh yang jauh melampaui nilai lainnya, sekali lagi menegaskan keberadaan outlier pada atribut tersebut.

7.3 Decimal Scaling

Decimal scaling menggeser titik desimal berdasarkan nilai absolut terbesar dalam data, sehingga seluruh nilai berada dalam rentang -1 sampai 1.

decimal_scale <- function(x) {
  maks <- max(abs(x), na.rm = TRUE)
  if (maks == 0) return(x)
  j <- ceiling(log10(maks + 1))
  x / (10 ^ j)
}

pelanggan$pendapatan_decimal <- decimal_scale(pelanggan$pendapatan_imputasi)
range(pelanggan$pendapatan_decimal, na.rm = TRUE)
## [1] 0.0045 0.5000

7.4 Membandingkan Metode Transformasi

Ketiga metode transformasi di atas sama-sama valid, namun memberikan tampilan angka yang cukup berbeda. Mari kita bandingkan hasilnya berdampingan.

transformasi <- pelanggan[, c("customer_id", "pendapatan_imputasi",
                               "pendapatan_minmax", "pendapatan_z",
                               "pendapatan_decimal")]
transformasi

7.5 Dampak Outlier terhadap Normalisasi

Mari kita lihat bagaimana keberadaan outlier memengaruhi hasil normalisasi min-maks, dengan membandingkan versi data asli dan versi yang sudah di-winsorize.

pelanggan$pendapatan_winsor_minmax <- minmax(pelanggan$pendapatan_winsor)

plot(pelanggan$pendapatan_minmax, pelanggan$pendapatan_winsor_minmax,
     pch = 19, col = "navy",
     xlab = "Min-Maks Data Asli",
     ylab = "Min-Maks Data Winsorized",
     main = "Dampak Penanganan Outlier terhadap Normalisasi")
abline(0, 1, col = "red", lty = 2)

Catatan: Outlier dapat menekan sebagian besar nilai min-maks ke rentang yang sangat sempit, seperti terlihat pada titik-titik yang berhimpitan di sudut kiri bawah grafik. Inilah alasan mengapa deteksi outlier perlu dilakukan sebelum memilih metode transformasi yang tepat.

8 Bagian VI - Integrasi Data

8.1 Memeriksa Kunci pada Kedua Sumber

Sebelum menggabungkan dua sumber data, kita pastikan dahulu bahwa kunci penggabungannya (identifier) unik dan cocok satu sama lain.

sum(duplicated(pelanggan$customer_id))
## [1] 0
sum(duplicated(transaksi_raw$cust_id))
## [1] 0
setdiff(pelanggan$customer_id, transaksi_raw$cust_id)
## [1] "C011"
setdiff(transaksi_raw$cust_id, pelanggan$customer_id)
## [1] "C012"

Hasil setdiff() di atas menunjukkan bahwa ada pelanggan yang belum memiliki catatan transaksi, dan sebaliknya ada catatan transaksi yang tidak memiliki pasangan data pelanggan.

8.2 Menyelaraskan Nama Identifier

Kedua tabel menggunakan nama kolom identifier yang berbeda, yaitu customer_id dan cust_id. Sebelum digabung, keduanya perlu diselaraskan terlebih dahulu.

transaksi <- transaksi_raw
names(transaksi)[names(transaksi) == "cust_id"] <- "customer_id"

8.3 Melakukan Left Join dengan merge()

Kita menggunakan left join agar seluruh data pelanggan tetap dipertahankan, meskipun ada yang belum memiliki transaksi.

data_terintegrasi <- merge(pelanggan, transaksi, by = "customer_id", all.x = TRUE)

data_terintegrasi[, c("customer_id", "nama", "jumlah_transaksi", "total_purchase")]

Parameter all.x = TRUE inilah yang memastikan seluruh pelanggan dipertahankan, meskipun tidak memiliki pasangan transaksi.

8.4 Memvalidasi Hasil Integrasi

Setelah penggabungan, validasi selalu diperlukan untuk memastikan tidak ada yang berubah secara tidak wajar.

# Apakah jumlah baris berubah secara tidak wajar?
c(sebelum = nrow(pelanggan), sesudah = nrow(data_terintegrasi))
## sebelum sesudah 
##      11      11
# Apakah customer_id tetap unik?
sum(duplicated(data_terintegrasi$customer_id))
## [1] 0
# Missing value baru akibat tidak ditemukan pasangan transaksi
colSums(is.na(data_terintegrasi[, c("jumlah_transaksi", "total_purchase")]))
## jumlah_transaksi   total_purchase 
##                1                1
# Pelanggan tanpa pasangan transaksi
data_terintegrasi[is.na(data_terintegrasi$jumlah_transaksi),
                   c("customer_id", "nama")]

8.5 Mengisi Nol atau Mempertahankan NA?

Ini adalah salah satu keputusan yang sering diambil terlalu cepat. Nilai NA pada transaksi bisa berarti dua hal yang sangat berbeda:

  1. pelanggan tersebut benar-benar belum pernah bertransaksi, atau
  2. data transaksi pelanggan tersebut sebenarnya ada, tetapi tidak tersedia atau gagal dipadankan saat penggabungan.

Nilai NA hanya boleh diubah menjadi 0 apabila makna sesungguhnya sudah dikonfirmasi terlebih dahulu.

# Contoh jika telah dikonfirmasi bahwa NA berarti 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

9 Dataset Akhir dan Evaluasi

9.1 Memilih Atribut Akhir

Setelah melalui seluruh tahapan, saatnya menyusun dataset akhir dengan memilih atribut yang relevan untuk dipakai pada analisis selanjutnya.

data_final <- data_terintegrasi[, c(
  "customer_id", "nama", "usia_imputasi", "kota_imputasi", "status",
  "pendapatan_imputasi", "pendapatan_missing", "outlier_pendapatan",
  "usia_minmax", "pendapatan_minmax",
  "jumlah_transaksi_final", "total_purchase_final"
)]

names(data_final)[names(data_final) == "kota_imputasi"] <- "kota"
names(data_final)[names(data_final) == "usia_imputasi"] <- "usia"

data_final

9.2 Audit Akhir

Fungsi audit_data() yang kita buat di awal praktikum kini terpakai kembali untuk memeriksa kondisi dataset akhir.

audit_akhir <- audit_data(data_final)
audit_akhir
sum(duplicated(data_final$customer_id))
## [1] 0
colSums(is.na(data_final))
##            customer_id                   nama                   usia 
##                      0                      0                      0 
##                   kota                 status    pendapatan_imputasi 
##                      0                      0                      0 
##     pendapatan_missing     outlier_pendapatan            usia_minmax 
##                      0                      0                      0 
##      pendapatan_minmax jumlah_transaksi_final   total_purchase_final 
##                      0                      0                      0

9.3 Membandingkan Kondisi Sebelum dan Sesudah

Dengan demikian, mari kita bandingkan langsung beberapa indikator kualitas data antara kondisi awal dan kondisi akhir.

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))
  )
)
perbandingan

Tabel ini merangkum dengan jelas seberapa jauh kualitas data telah membaik seperti duplikasi hilang, missing value tuntas tertangani, dan kategori kota maupun status menjadi jauh lebih konsisten.

9.4 Menyimpan Hasil

Langkah terakhir adalah menyimpan dataset akhir beserta log perubahannya, agar dapat dipakai kembali pada percobaan berikutnya atau dibagikan kepada pihak lain.

write.csv(data_final, "data_pelanggan_setelah_preprocessing.csv", row.names = FALSE)
write.csv(log_perubahan, "log_perubahan_preprocessing.csv", row.names = FALSE)

10 Ringkasan

Sepanjang materi ini, alur yang telah kita lalui adalah:

  1. memahami struktur dan kualitas awal data;
  2. membersihkan kategori, duplikasi, dan pelanggaran domain;
  3. menangani missing values tanpa menyembunyikan ketidakpastian;
  4. mendeteksi dan mengevaluasi outlier;
  5. mentransformasikan atribut numerik; dan
  6. mengintegrasikan data serta memvalidasi hasilnya.

Catatan: Preprocessing pada dasarnya adalah proses pengambilan keputusan. Kode R hanya menjalankan keputusan tersebut; kualitas hasil akhirnya tetap bergantung pada pemahaman kita terhadap data, tujuan analisis, dan sejauh mana setiap perubahan didokumentasikan dengan baik.

11 Daftar Pustaka

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