Studi Kasus dan Pendahuluan

Dalam dunia e-commerce, data profil pengguna dan riwayat transaksi umumnya disimpan dalam basis data yang terpisah. Ketika data ini diekstrak untuk kebutuhan pemodelan atau analisis lanjutan, kita sering kali dihadapkan pada masalah kualitas data mentah (raw data). Kendala yang umum ditemukan meliputi kekosongan data (missing values), kesalahan input yang tidak logis, duplikasi observasi, hingga format penulisan teks yang tidak konsisten.

Apabila data yang belum dibersihkan ini langsung diproses, hasil analisis statistik yang diperoleh kemungkinan besar akan bias atau tidak akurat. Oleh karena itu, dimulai dari audit awal, pembersihan data, penanganan nilai ekstrem (outliers), transformasi skala, hingga integrasi data untuk menghasilkan dataset yang valid dan terstandardisasi.


Persiapan Lingkungan Kerja RStudio

Sebelum memulai pengolahan, penting untuk memverifikasi direktori kerja dan versi R ang digunakan guna memastikan reproduksibilitas kode di kemudian hari.

getwd()
## [1] "D:/Semester 5"
R.version.string
## [1] "R version 4.6.0 (2026-04-24 ucrt)"

Makna: Output di atas mengonfirmasi lokasi folder tempat proyek ini berjalan dan versi komputasi R yang aktif. Seluruh proses di bawah ini murni menggunakan paket dasar (base R).

Membangun Dataset Mentah

Kita mendefinisikan dua kerangka data: pelanggan_raw (profil demografis) dan transaksi_raw (perilaku pembelian).

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

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

pelanggan_raw
##    customer_id   nama usia pendapatan       kota      status
## 1         C001    Ani   21    4.5e+06  Pekanbaru       Aktif
## 2         C002   Budi   25         NA        PKU       aktif
## 3         C003  Citra   23    5.2e+06  PEKANBARU      ACTIVE
## 4         C004   Dodi  150    4.8e+06      Dumai           A
## 5         C005    Eka   27    4.9e+06  pekanbaru Tidak Aktif
## 6         C006   Fani   NA    5.1e+06      DUMAI    nonaktif
## 7         C007 Gilang   31    5.0e+08 Pekanbaru        Aktif
## 8         C008   Hana   29    4.7e+06       Siak       AKTIF
## 9         C009  Indra   22    4.6e+06        PKU           A
## 10        C010   Joko   35    5.3e+06      Dumai Tidak aktif
## 11        C010   Joko   35    5.3e+06      Dumai Tidak aktif
## 12        C011   Kiki   28         NA       <NA>       Aktif
transaksi_raw
##    cust_id jumlah_transaksi total_purchase
## 1     C001                5        1.5e+06
## 2     C002                3        9.0e+05
## 3     C003                7        2.7e+06
## 4     C004                2        6.0e+05
## 5     C005                6        2.1e+06
## 6     C006                4        1.3e+06
## 7     C007               20        2.5e+07
## 8     C008                5        1.7e+06
## 9     C009                3        8.0e+05
## 10    C010                8        3.2e+06
## 11    C012                1        2.5e+05

Makna: Dataset mentah telah berhasil dimuat. Jika diperhatikan sekilas, terdapat beberapa inkonsistensi pada data pelanggan, seperti perbedaan format penulisan nama kota dan status. Selain itu, terdapat data usia yang tidak logis (150 tahun), yang perlu ditangani pada tahap selanjutnya.


Bagian I: Konsep Data Preprocessing & Audit Awal

Langkah pertama adalah melakukan pengecekan struktur tipe data dan melihat ringkasan deskriptifnya.

dim(pelanggan_raw)
## [1] 12  6
names(pelanggan_raw)
## [1] "customer_id" "nama"        "usia"        "pendapatan"  "kota"       
## [6] "status"
str(pelanggan_raw)
## 'data.frame':    12 obs. of  6 variables:
##  $ customer_id: chr  "C001" "C002" "C003" "C004" ...
##  $ nama       : chr  "Ani" "Budi" "Citra" "Dodi" ...
##  $ usia       : num  21 25 23 150 27 NA 31 29 22 35 ...
##  $ pendapatan : num  4.5e+06 NA 5.2e+06 4.8e+06 4.9e+06 5.1e+06 5.0e+08 4.7e+06 4.6e+06 5.3e+06 ...
##  $ kota       : chr  "Pekanbaru" " PKU" "PEKANBARU" "Dumai" ...
##  $ status     : chr  "Aktif" "aktif" "ACTIVE" "A" ...
head(pelanggan_raw)
##   customer_id  nama usia pendapatan      kota      status
## 1        C001   Ani   21    4500000 Pekanbaru       Aktif
## 2        C002  Budi   25         NA       PKU       aktif
## 3        C003 Citra   23    5200000 PEKANBARU      ACTIVE
## 4        C004  Dodi  150    4800000     Dumai           A
## 5        C005   Eka   27    4900000 pekanbaru Tidak Aktif
## 6        C006  Fani   NA    5100000     DUMAI    nonaktif
summary(pelanggan_raw)
##     customer_id        nama         usia          pendapatan       
##  Length   :12   Length   :12   Min.   : 21.00   Min.   :  4500000  
##  N.unique :11   N.unique :11   1st Qu.: 24.00   1st Qu.:  4725000  
##  N.blank  : 0   N.blank  : 0   Median : 28.00   Median :  5000000  
##  Min.nchar: 4   Min.nchar: 3   Mean   : 38.73   Mean   : 54440000  
##  Max.nchar: 4   Max.nchar: 6   3rd Qu.: 33.00   3rd Qu.:  5275000  
##                                Max.   :150.00   Max.   :500000000  
##                                NAs    :1        NAs    :2          
##         kota          status  
##  Length   :12   Length   :12  
##  N.unique : 9   N.unique : 8  
##  N.blank  : 0   N.blank  : 0  
##  Min.nchar: 3   Min.nchar: 1  
##  Max.nchar:10   Max.nchar:11  
##  NAs      : 1                 
## 

Makna: Fungsi str() membantu kita memverifikasi bahwa tipe data untuk setiap kolom sudah tepat (karakter atau numerik). Melalui ringkasan summary(), kita dapat melihat adanya indikasi nilai ekstrem, yakni nilai maksimum usia yang mencapai 150 dan pendapatan yang menyentuh angka 500 juta. Hal ini merupakan sinyal bahwa distribusi data kita kemungkinan besar menceng (skewed).

Selanjutnya, kita akan mengevaluasi jumlah missing value dan konsistensi kategori.

# 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

Untuk mempermudah pelaporan, kita buat fungsi kustom audit_data yang memetakan metrik permasalahan per variabel.

audit_data <- function(data) { 
  data.frame( 
    atribut = names(data), 
    tipe = sapply(data, function(x) class(x)[1]), 
    jumlah_missing = sapply(data, function(x) sum(is.na(x))), 
    persen_missing = round(sapply(data, function(x) mean(is.na(x)) * 100), 2), 
    jumlah_unik = sapply(data, function(x) length(unique(x))), 
    row.names = NULL 
  )
}
audit_awal <- audit_data(pelanggan_raw)
audit_awal
##       atribut      tipe jumlah_missing persen_missing jumlah_unik
## 1 customer_id character              0           0.00          11
## 2        nama character              0           0.00          11
## 3        usia   numeric              1           8.33          11
## 4  pendapatan   numeric              2          16.67          10
## 5        kota character              1           8.33          10
## 6      status character              0           0.00           8

Makna: Audit ini membuktikan tingginya noise kategorik. Atribut kota memiliki 10 entitas unik padahal secara geografis jumlah kotanya lebih sedikit, ini murni akibat inkonsistensi spasi dan huruf kapital. Terdapat juga persentase data kosong terbesar pada kolom pendapatan (16.67%).


Bagian II: Implementasi Data Cleaning

Pembersihan teks (penghapusan whitespace berlebih dan konversi huruf kecil) diperlukan sebagai syarat mutlak sebelum melakukan standardisasi nilai (pengelompokan).

pelanggan <- pelanggan_raw

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

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

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

Sekarang kita lakukan standardisasi berdasarkan aturan business 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("tidak aktif", "nonaktif")] <- "Tidak Aktif"

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

Selanjutnya adalah resolusi duplikasi rekaman (redundansi). Jika ada satu customer_id yang berulang dengan riwayat informasi yang persis sama, ini akan menimbulkan pembobotan ganda saat pengujian analitik, sehingga salah satunya wajib dieliminasi.

# Menampilkan seluruh baris dengan customer_id yang berulang
pelanggan[duplicated(pelanggan$customer_id) | duplicated(pelanggan$customer_id, fromLast = TRUE), ]
##    customer_id nama usia pendapatan  kota      status
## 10        C010 Joko   35    5300000 Dumai Tidak Aktif
## 11        C010 Joko   35    5300000 Dumai Tidak Aktif
# Mempertahankan kemunculan pertama untuk customer_id yang identik
pelanggan <- pelanggan[!duplicated(pelanggan$customer_id), ]
rownames(pelanggan) <- NULL
dim(pelanggan)
## [1] 11  6

Memeriksa dan mengoreksi nilai yang melanggar hukum logika data (domain rule violation).

# Kandidat usia tidak valid berdasarkan aturan bisnis 15–100 tahun
pelanggan[pelanggan$usia < 15 | pelanggan$usia > 100, ]
##    customer_id nama usia pendapatan  kota status
## 4         C004 Dodi  150    4800000 Dumai  Aktif
## NA        <NA> <NA>   NA         NA  <NA>   <NA>
# Kandidat pendapatan tidak valid jika bernilai negatif
pelanggan[pelanggan$pendapatan < 0, ]
##      customer_id nama usia pendapatan kota status
## NA          <NA> <NA>   NA         NA <NA>   <NA>
## NA.1        <NA> <NA>   NA         NA <NA>   <NA>
# Mengoreksi nilai usia berdasarkan verifikasi berkas formulir asli
pelanggan$usia[pelanggan$customer_id == "C004"] <- 50

Kita merekam setiap jejak rekayasa data ini ke dalam log agar proses preprocessing dapat diaudit dan dipertanggungjawabkan.

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

Makna Bagian II: Penghapusan redundansi data memastikan bahwa hasil akhir analisis tidak akan bias terhadap observasi yang terinput secara berulang. Standardisasi level faktor juga akan membuat model lebih akurat membaca bobot parameter setiap kelompok.


Bagian III: Penanganan Missing Values

Kekosongan data dapat ditangani dengan menghapus baris (listwise deletion) atau menebak nilainya menggunakan metode pendekatan statis (imputasi parameter sentral).

# Mengidentifikasi lokasi nilai hilang
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), ]
##    customer_id nama usia pendapatan      kota      status
## 2         C002 Budi   25         NA Pekanbaru       Aktif
## 6         C006 Fani   NA    5100000     Dumai Tidak Aktif
## 11        C011 Kiki   28         NA      <NA>       Aktif

Jika kita menghapus seluruh observasi yang tidak lengkap, terjadi pengurangan dimensi sampel yang cukup drastis:

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

Menghapus 27.27% data menyebabkan hilangnya banyak informasi potensial. Karena itu, digunakan metode imputasi. Khusus untuk atribut pendapatan yang sangat terdistorsi oleh satu nilai ekstrem (500 juta), kita membandingkan nilai mean dan median.

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

Karena mean ikut terseret menjadi sangat besar (hampir 60 juta), estimasi ini rentan bias. Median (sekitar 4,9 juta) dipilih karena memiliki sifat ketahanan yang tinggi (robust) terhadap distribusi yang skewed.

# Imputasi pendapatan dengan median
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

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

# Membuat flag untuk melacak baris mana yang sebelumnya di-imputasi
pelanggan$pendapatan_missing <- as.integer(is.na(pelanggan$pendapatan))
table(pelanggan$pendapatan_missing)
## 
## 0 1 
## 9 2

Mari bandingkan bentuk sebaran distribusinya melalui histogram.

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

Makna: Imputasi nilai menggunakan median menjaga pemusatan data (tendensi sentral) tetap realistis di kelas mayoritas. Penggunaan matriks variabel indikator dummy (pendapatan_missing) juga sangat berguna jika kita ingin memastikan bahwa proses tebakan kita ini tidak menghasilkan kesalahan spesifikasi sistematis saat dilibatkan ke dalam fungsi regresi.


Bagian IV: Penanganan Outlier

Deteksi pencilan (outlier) dilakukan dengan metode non-parametrik (berbasis rentang kuartil / Interquartile Range).

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

# Menghitung pagar IQR
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

Identifikasi baris data yang melewati ambang batas tersebut:

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

pelanggan[pelanggan$outlier_pendapatan, c("customer_id", "nama", "pendapatan_imputasi")]
##   customer_id   nama pendapatan_imputasi
## 7        C007 Gilang               5e+08

Salah satu cara menangani outlier yang ingin dipertahankan (bukan dihapus) adalah dengan melakukan operasi pemangkasan batas (winsorizing).

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

pelanggan[pelanggan$outlier_pendapatan, c("customer_id", "pendapatan_imputasi", "pendapatan_winsor")]
##   customer_id pendapatan_imputasi pendapatan_winsor
## 7        C007               5e+08           5750000

Makna: Metode batasan matematis IQR berhasil menemukan anomali ekstrem (500 juta) dan menariknya paksa turun menempel pada batas_atas wajar (5,75 juta). Proses winsorizing mencegah satu titik amatan dominan merusak kestabilan varians data secara keseluruhan.


Bagian V: Transformasi Data

Transformasi rentang numerik penting untuk menghilangkan perbedaan ekuivalensi unit satuan.

Normalisasi min–maks (Pemetaan ulang sebaran absolut menjadi rasio 0 hingga 1).

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)

Normalisasi z-score (Membawa rata-rata matriks menjadi 0 dan ragam menjadi 1).

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

Decimal scaling

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)

Perbandingan hasil dari ketiga metrik transformasi:

transformasi <- pelanggan[, c(
  "customer_id", "pendapatan_imputasi", "pendapatan_minmax", 
  "pendapatan_z", "pendapatan_decimal"
)]
transformasi
##    customer_id pendapatan_imputasi pendapatan_minmax pendapatan_z
## 1         C001             4.5e+06      0.0000000000   -0.3041235
## 2         C002             4.9e+06      0.0008072654   -0.3014440
## 3         C003             5.2e+06      0.0014127144   -0.2994343
## 4         C004             4.8e+06      0.0006054490   -0.3021138
## 5         C005             4.9e+06      0.0008072654   -0.3014440
## 6         C006             5.1e+06      0.0012108981   -0.3001042
## 7         C007             5.0e+08      1.0000000000    3.0151095
## 8         C008             4.7e+06      0.0004036327   -0.3027837
## 9         C009             4.6e+06      0.0002018163   -0.3034536
## 10        C010             5.3e+06      0.0016145308   -0.2987645
## 11        C011             4.9e+06      0.0008072654   -0.3014440
##    pendapatan_decimal
## 1              0.0045
## 2              0.0049
## 3              0.0052
## 4              0.0048
## 5              0.0049
## 6              0.0051
## 7              0.5000
## 8              0.0047
## 9              0.0046
## 10             0.0053
## 11             0.0049

Kita evaluasi secara visual efek fatal jika sebuah observasi anomali belum di-winsorize lalu dipaksa masuk ke dalam fungsi Min-Maks.

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)

Makna: Output grafik secara kuat menegaskan bahwa nilai 500 juta menyebabkan semua rentang sampel yang sehat terhimpit mati mendatar (flattened) di dekat angka nol pada sumbu X. Transformasi ini menjadi bukti visual mengapa analisis sensitivitas kualitas distribusi (deteksi outlier) bersifat wajib dikerjakan sebelum skala fitur.


Bagian VI: Integrasi Data

Pada fase ini, dilakukan penyatuan matriks (join) antara data perilaku transaksi (transaksi_raw) dengan profil utama (pelanggan).

# Memeriksa kunci (key) pada kedua sumber
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"

Langkah relasional dengan metode penggabungan Left Join:

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

data_terintegrasi <- merge( 
  pelanggan, 
  transaksi, 
  by = "customer_id", 
  all.x = TRUE
)
data_terintegrasi[, c("customer_id", "nama", "jumlah_transaksi", "total_purchase")]
##    customer_id   nama jumlah_transaksi total_purchase
## 1         C001    Ani                5        1.5e+06
## 2         C002   Budi                3        9.0e+05
## 3         C003  Citra                7        2.7e+06
## 4         C004   Dodi                2        6.0e+05
## 5         C005    Eka                6        2.1e+06
## 6         C006   Fani                4        1.3e+06
## 7         C007 Gilang               20        2.5e+07
## 8         C008   Hana                5        1.7e+06
## 9         C009  Indra                3        8.0e+05
## 10        C010   Joko                8        3.2e+06
## 11        C011   Kiki               NA             NA

Uji kros-cek paska-penggabungan:

c(sebelum = nrow(pelanggan), sesudah = nrow(data_terintegrasi))
## sebelum sesudah 
##      11      11
sum(duplicated(data_terintegrasi$customer_id))
## [1] 0
# Pelanggan tanpa pasangan transaksi (menyisakan entri NA baru)
data_terintegrasi[is.na(data_terintegrasi$jumlah_transaksi), c("customer_id", "nama")]
##    customer_id nama
## 11        C011 Kiki
# Resolusi NA dengan nilai statis 0 (diasumsikan belum pernah transaksi sama sekali)
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

Makna: Sistem irisan all.x = TRUE mencegah baris C011 lenyap (berbeda jika memakai matriks inner join). Nilai kekosongan yang tercipta ditambal secara sadar menggunakan 0, mendefinisikan logika bahwa ketiadaan riwayat tabel sama dengan nominal belanja nol rupiah.


Dataset Akhir dan Evaluasi

Tahap pembersihan telah rampung. Sub-set dari seluruh fitur turunan diekstraksi ke dalam satu tabel final yang definitif.

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
##    customer_id   nama usia            kota      status pendapatan_imputasi
## 1         C001    Ani 21.0       Pekanbaru       Aktif             4.5e+06
## 2         C002   Budi 25.0       Pekanbaru       Aktif             4.9e+06
## 3         C003  Citra 23.0       Pekanbaru       Aktif             5.2e+06
## 4         C004   Dodi 50.0           Dumai       Aktif             4.8e+06
## 5         C005    Eka 27.0       Pekanbaru Tidak Aktif             4.9e+06
## 6         C006   Fani 27.5           Dumai Tidak Aktif             5.1e+06
## 7         C007 Gilang 31.0       Pekanbaru       Aktif             5.0e+08
## 8         C008   Hana 29.0            Siak       Aktif             4.7e+06
## 9         C009  Indra 22.0       Pekanbaru       Aktif             4.6e+06
## 10        C010   Joko 35.0           Dumai Tidak Aktif             5.3e+06
## 11        C011   Kiki 28.0 Tidak diketahui       Aktif             4.9e+06
##    pendapatan_missing outlier_pendapatan usia_minmax pendapatan_minmax
## 1                   0              FALSE  0.00000000      0.0000000000
## 2                   1              FALSE  0.13793103      0.0008072654
## 3                   0              FALSE  0.06896552      0.0014127144
## 4                   0              FALSE  1.00000000      0.0006054490
## 5                   0              FALSE  0.20689655      0.0008072654
## 6                   0              FALSE  0.22413793      0.0012108981
## 7                   0               TRUE  0.34482759      1.0000000000
## 8                   0              FALSE  0.27586207      0.0004036327
## 9                   0              FALSE  0.03448276      0.0002018163
## 10                  0              FALSE  0.48275862      0.0016145308
## 11                  1              FALSE  0.24137931      0.0008072654
##    jumlah_transaksi_final total_purchase_final
## 1                       5              1.5e+06
## 2                       3              9.0e+05
## 3                       7              2.7e+06
## 4                       2              6.0e+05
## 5                       6              2.1e+06
## 6                       4              1.3e+06
## 7                      20              2.5e+07
## 8                       5              1.7e+06
## 9                       3              8.0e+05
## 10                      8              3.2e+06
## 11                      0              0.0e+00
# Audit akhir
audit_akhir <- audit_data(data_final)
audit_akhir
##                   atribut      tipe jumlah_missing persen_missing jumlah_unik
## 1             customer_id character              0              0          11
## 2                    nama character              0              0          11
## 3                    usia   numeric              0              0          11
## 4                    kota character              0              0           4
## 5                  status character              0              0           2
## 6     pendapatan_imputasi   numeric              0              0           9
## 7      pendapatan_missing   integer              0              0           2
## 8      outlier_pendapatan   logical              0              0           2
## 9             usia_minmax   numeric              0              0          11
## 10      pendapatan_minmax   numeric              0              0           9
## 11 jumlah_transaksi_final   numeric              0              0           9
## 12   total_purchase_final   numeric              0              0          11
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

Untuk melihat komparasi rasio pemulihan data dan pencapaian kebersihan data, kita proyeksikan perbandingan matrik ukuran ke dalam variabel perbandingan.

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
##               indikator sebelum sesudah
## 1          Jumlah baris      12      11
## 2 Duplikasi customer_id       1       0
## 3   Total missing value       4       0
## 4    Kategori kota unik      10       4
## 5  Kategori status unik       8       2
# Mengekspor hasil
write.csv(data_final, "data_pelanggan_setelah_preprocessing.csv", row.names = FALSE)
write.csv(log_perubahan, "log_perubahan_preprocessing.csv", row.names = FALSE)

Makna Keseluruhan: Kerangka data akhir kini telah disterilkan sepenuhnya; baris ganda dinetralkan, anomali terminimalisir dampaknya, dan tidak terdapat lagi kekosongan observasi. Hasil ekspor CSV (Comma Separated Values) siap didistribusikan sebagai bahan input model probabilistik atau simulasi statistik turunan lainnya.


Kesimpulan Refleksi