Eksplorasi & Preprocessing Data Pelanggan

Data Cleaning, Transformation & Integration

R Programming   •   Data Analysis   •   Data Preprocessing

R Data Cleaning Data Analysis Data Transformation Data Integration

Project ini merupakan proses pengolahan data pelanggan dari kondisi awal hingga menjadi dataset yang lebih rapi dan siap digunakan untuk analisis.

Data berasal dari dua sumber, yaitu data pelanggan dan data transaksi. Fokus utama project adalah memahami masalah pada data, menentukan aturan pembersihan, melakukan transformasi, kemudian menggabungkan kedua sumber data tersebut.

Latar Belakang

Sebuah perusahaan e-commerce ingin melakukan analisis terhadap pelanggan. Data yang tersedia berasal dari dua sumber berbeda.

Data pelanggan memuat identitas pelanggan, usia, kota, pendapatan, dan status pelanggan. Sementara itu, data transaksi berisi jumlah transaksi dan total pembelian.

Pada kondisi awal, data belum dapat langsung digunakan karena terdapat beberapa persoalan seperti nilai hilang, penulisan kategori yang tidak konsisten, duplikasi, nilai ekstrem, serta perbedaan nama identifier antar dataset.

Karena itu, sebelum masuk ke tahap analisis, data perlu diperiksa dan dipersiapkan terlebih dahulu.

Prinsip pengolahan data:

Data tidak dibersihkan hanya karena sebuah nilai terlihat berbeda. Setiap perubahan harus memiliki alasan, aturan yang jelas, dan dapat dijelaskan kembali.

Data yang Digunakan

Terdapat dua dataset yang digunakan dalam project ini.

Data pertama berisi informasi dasar pelanggan, sedangkan data kedua berisi aktivitas transaksi pelanggan.

Data Pelanggan

Variabel Keterangan
customer_id ID pelanggan
nama Nama pelanggan
usia Usia pelanggan
pendapatan Pendapatan pelanggan
kota Kota tempat tinggal
status Status pelanggan

Data Transaksi

Variabel Keterangan
cust_id ID pelanggan
jumlah_transaksi Jumlah transaksi
total_purchase Total pembelian

Menyiapkan Dataset

Saya mulai dengan membuat dataset pelanggan yang akan digunakan dalam proses eksplorasi.

Data ini sengaja dibuat memiliki beberapa permasalahan sederhana agar proses preprocessing dapat dilakukan secara lengkap.

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

Kemudian saya membuat dataset transaksi.

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

Eksplorasi Data Awal

Sebelum melakukan perubahan apa pun, data asli disimpan terlebih dahulu. Dengan menyimpan pelanggan_awal, kita memiliki salinan kondisi data yang benar-benar belum mengalami preprocessing.

pelanggan_awal <- pelanggan

Melihat Data

head(pelanggan_awal)

Dataset awal terdiri dari 12 observasi dan 6 variabel.

dim(pelanggan_awal)
## [1] 12  6

Nama variabel kemudian diperiksa untuk memastikan struktur dataset.

names(pelanggan_awal)
## [1] "customer_id" "nama"        "usia"        "pendapatan"  "kota"       
## [6] "status"

Struktur Data

Saya melihat tipe data dari setiap variabel.

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

Variabel usia dan pendapatan merupakan variabel numerik. Sementara itu, customer_id, nama, kota, dan status merupakan variabel karakter.

Ringkasan Data

summary(pelanggan_awal)
##  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  
##                                       
##                                       
##                                       
## 

Ringkasan ini memberikan gambaran awal mengenai distribusi variabel numerik serta kategori pada variabel lainnya.

Audit Awal Data

Sebelum melakukan pemeriksaan dan pembersihan data, saya membuat fungsi sederhana untuk melihat kondisi awal setiap atribut. Audit ini mencakup tipe data, jumlah dan persentase missing value, serta jumlah nilai unik.

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

kable(
  audit_awal,
  caption = "Audit Awal Dataset Pelanggan"
)
Audit Awal Dataset Pelanggan
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

Dari audit awal terlihat bahwa beberapa atribut memiliki missing value, terutama pendapatan dengan persentase 16,67%. Sementara itu, status memiliki jumlah nilai unik yang lebih sedikit dibandingkan jumlah baris sehingga perlu diperiksa lebih lanjut.

Audit ini belum menjadi keputusan cleaning. Hasilnya digunakan sebagai gambaran awal untuk menentukan atribut mana yang perlu diperiksa lebih lanjut pada tahap pemeriksaan kualitas data.

Pemeriksaan Kualitas Data

Setelah mendapatkan gambaran awal melalui audit, saya melakukan pemeriksaan kualitas secara lebih spesifik.

Missing Value

Missing value diperiksa menggunakan is.na().

colSums(
  is.na(pelanggan_awal)
)
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           1           2           1           0

Persentase missing value juga dihitung agar proporsinya lebih mudah dibandingkan.

round(
  colMeans(is.na(pelanggan_awal)) * 100,
  2
)
## customer_id        nama        usia  pendapatan        kota      status 
##        0.00        0.00        8.33       16.67        8.33        0.00

Saya kemudian membuat tabel ringkasan.

missing_data <- data.frame(

  Variabel = names(pelanggan_awal),

  Jumlah_Missing = colSums(
    is.na(pelanggan_awal)
  ),

  Persentase_Missing = round(
    colMeans(is.na(pelanggan_awal)) * 100,
    2
  )

)

kable(
  missing_data,
  caption = "Missing Value pada Dataset Awal"
)
Missing Value pada Dataset Awal
Variabel Jumlah_Missing Persentase_Missing
customer_id customer_id 0 0.00
nama nama 0 0.00
usia usia 1 8.33
pendapatan pendapatan 2 16.67
kota kota 1 8.33
status status 0 0.00

Missing value ditemukan pada usia, pendapatan, dan kota.

Data Duplikat

Selanjutnya saya memeriksa apakah terdapat baris yang sama.

sum(
  duplicated(pelanggan_awal)
)
## [1] 1

Untuk melihat baris yang terindikasi duplikat:

pelanggan[
  duplicated(pelanggan_awal) |
  duplicated(pelanggan_awal, fromLast = TRUE),
]

Saya juga memeriksa duplikasi pada ID pelanggan.

sum(
  duplicated(pelanggan_awal$customer_id)
)
## [1] 1

Terdapat satu ID yang tercatat lebih dari satu kali, yaitu C010.

Karena customer_id digunakan sebagai identitas pelanggan, kondisi ini perlu ditangani sebelum proses berikutnya.

Cleaning Data

Menyeragamkan Nama Kota

Variabel kota memiliki beberapa bentuk penulisan yang berbeda.

Misalnya Pekanbaru, PEKANBARU, pekanbaru, dan PKU sebenarnya merujuk pada kota yang sama.

Saya menyeragamkan kategori tersebut agar dapat diperlakukan sebagai satu kategori.

pelanggan <- pelanggan %>%

  mutate(

    kota = trimws(kota),

    kota = case_when(

      tolower(kota) %in%
        c("pku", "pekanbaru") ~
        "Pekanbaru",

      tolower(kota) == "dumai" ~
        "Dumai",

      tolower(kota) == "siak" ~
        "Siak",

      TRUE ~ kota

    )

  )

Kategori kota setelah cleaning:

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

Menyeragamkan Status

Variabel status juga memiliki beberapa bentuk penulisan yang berbeda.

Kategori tersebut diseragamkan menjadi Aktif dan Tidak Aktif.

pelanggan <- pelanggan %>%

  mutate(

    status = trimws(status),

    status = case_when(

      tolower(status) %in%
        c("aktif", "active", "a") ~
        "Aktif",

      tolower(status) %in%
        c("tidak aktif", "nonaktif") ~
        "Tidak Aktif",

      TRUE ~ status

    )

  )

Distribusi status setelah cleaning:

table(
  pelanggan$status,
  useNA = "ifany"
)
## 
##       Aktif Tidak Aktif 
##           8           4

Gambaran Status Pelanggan

Untuk melihat hasil penyeragaman kategori secara visual, saya membuat diagram batang.

status_data <- pelanggan %>%
  count(status)

ggplot(
  status_data,
  aes(
    x = status,
    y = n
  )
) +

  geom_col(
    width = 0.2,
    fill = "#6B8EAD"
  ) +

  geom_text(
    aes(label = n),
    vjust = -0.4,
    fontface = "bold",
    size = 4
  ) +

  labs(
    title = "Distribusi Status Pelanggan",
    subtitle = "Jumlah pelanggan berdasarkan status",
    x = NULL,
    y = "Jumlah Pelanggan"
  ) +

  theme_minimal() +

  theme(
    plot.title = element_text(
      size = 15,
      face = "bold"
    ),

    plot.subtitle = element_text(
      color = "#607D8B"
    ),

    panel.grid.minor = element_blank()
  )

Setelah kategori diseragamkan, distribusi pelanggan dapat dilihat dengan lebih jelas.

Menghapus ID yang Berulang

Karena satu pelanggan cukup memiliki satu customer_id, salah satu baris yang sama kemudian dihapus.

pelanggan <- pelanggan %>%

  distinct(
    customer_id,
    .keep_all = TRUE
  )

rownames(pelanggan) <- NULL

Pengecekan dilakukan kembali.

sum(
  duplicated(
    pelanggan$customer_id
  )
)
## [1] 0

Setelah proses ini, setiap pelanggan hanya memiliki satu ID.

Menangani Missing Value

Setelah mengetahui jenis missing value yang ada, saya menggunakan pendekatan yang berbeda sesuai dengan jenis variabel.

Untuk variabel numerik digunakan median, sedangkan untuk variabel kategorik kota digunakan kategori "Tidak diketahui".

##Memeriksa Usia Sebelum mengisi nilai kosong, saya mengecek apakah ada usia yang tidak masuk akal.

pelanggan %>%

  filter(
    usia < 15 |
    usia > 100
  )

Ditemukan satu nilai usia sebesar 150 tahun.

Pada project ini, nilai tersebut saya ubah menjadi 50 tahun sebagai contoh koreksi kesalahan input.

pelanggan <- pelanggan %>%

  mutate(

    usia = ifelse(
      usia == 150,
      50,
      usia
    )

  )

Pada data sebenarnya, koreksi seperti ini sebaiknya dilakukan berdasarkan informasi dari sumber data.

Menyimpan Data Pendapatan Sebelum Imputasi

Nilai pendapatan sebelum imputasi disimpan agar dapat dibandingkan setelah proses dilakukan.

pendapatan_sebelum <-
  pelanggan$pendapatan

usia_sebelum <-
  pelanggan$usia

##Mengisi Usia yang Kosong Median usia dihitung terlebih dahulu.

median_usia <- median(
  pelanggan$usia,
  na.rm = TRUE
)

median_usia
## [1] 27.5

Nilai kosong kemudian diisi menggunakan median.

pelanggan <- pelanggan %>%

  mutate(

    usia_imputasi = ifelse(
      is.na(usia),
      median_usia,
      usia
    )

  )

Mengisi Pendapatan yang Kosong

Median pendapatan dihitung dari nilai yang tersedia.

median_pendapatan <- median(
  pelanggan$pendapatan,
  na.rm = TRUE
)

median_pendapatan
## [1] 4900000

Missing value kemudian diganti menggunakan median pendapatan.

pelanggan <- pelanggan %>%

  mutate(

    pendapatan_imputasi = ifelse(
      is.na(pendapatan),
      median_pendapatan,
      pendapatan
    )

  )

Mengisi Kota yang Kosong

Untuk kota, nilai kosong diganti dengan “Tidak diketahui”.

pelanggan <- pelanggan %>%

  mutate(

    kota = ifelse(
      is.na(kota),
      "Tidak diketahui",
      kota
    )

  )

Kategori kota kemudian diperiksa kembali.

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

Menggunakan Hasil Imputasi

Setelah proses imputasi selesai, hasil imputasi digunakan sebagai nilai akhir.

pelanggan <- pelanggan %>%

  mutate(

    usia = usia_imputasi,

    pendapatan =
      pendapatan_imputasi

  )

Saya tetap menyimpan kolom usia_imputasi dan pendapatan_imputasi untuk melihat hasil proses preprocessing.

Pemeriksaan Setelah Cleaning

Setelah proses cleaning dan imputasi, saya memeriksa kembali missing value.

colSums(
  is.na(
    pelanggan %>%
      select(
        customer_id,
        nama,
        usia,
        pendapatan,
        kota,
        status
      )
  )
)
## customer_id        nama        usia  pendapatan        kota      status 
##           0           0           0           0           0           0

Pada variabel utama pelanggan, missing value sudah ditangani.

Perubahan Pendapatan Setelah Imputasi

Untuk melihat perbedaan sebelum dan sesudah imputasi, saya membuat histogram pendapatan.

hist(

  pendapatan_sebelum,

  main =
    "Pendapatan Sebelum Imputasi",

  xlab =
    "Pendapatan",

  ylab =
    "Frekuensi",

  col =
    "#E8F1F7",

  border =
    "#6B8EAD",

  breaks =
    10

)

Distribusi setelah imputasi:

hist(

  pelanggan$pendapatan,

  main =
    "Pendapatan Setelah Imputasi",

  xlab =
    "Pendapatan",

  ylab =
    "Frekuensi",

  col =
    "#EDF4F8",

  border =
    "#7C9AAF",

  breaks =
    10

)

Perubahan utama terjadi pada nilai yang sebelumnya kosong. Setelah imputasi, nilai tersebut diisi menggunakan median pendapatan.

Mendeteksi Outlier

Setelah data dibersihkan, saya memeriksa kemungkinan adanya outlier pada pendapatan.

Metode yang digunakan adalah Interquartile Range (IQR).

Boxplot Pendapatan

boxplot(

  pelanggan$pendapatan,

  main =
    "Distribusi Pendapatan Pelanggan",

  ylab =
    "Pendapatan",

  col =
    "#DCEAF3",

  border =
    "#4F6D8A"

)

Boxplot menunjukkan adanya satu nilai yang jauh dari sebagian besar observasi.

Untuk melihatnya secara lebih terukur, saya menghitung Q1, Q3, dan IQR.

Q1 <- quantile(
  pelanggan$pendapatan,
  0.25,
  na.rm = TRUE
)

Q3 <- quantile(
  pelanggan$pendapatan,
  0.75,
  na.rm = TRUE
)

IQR_pendapatan <- IQR(
  pelanggan$pendapatan,
  na.rm = TRUE
)

Q1
##     25% 
## 4750000
Q3
##     75% 
## 5150000
IQR_pendapatan
## [1] 4e+05

Batas outlier dihitung menggunakan rumus:

Q1 - 1.5 × IQR

dan

Q3 + 1.5 × IQR

batas_bawah <-
  Q1 - 1.5 * IQR_pendapatan

batas_atas <-
  Q3 + 1.5 * IQR_pendapatan

batas_bawah
##     25% 
## 4150000
batas_atas
##     75% 
## 5750000

Observasi yang berada di luar batas tersebut:

outlier_pendapatan <- pelanggan %>%

  filter(

    pendapatan < batas_bawah |
    pendapatan > batas_atas

  ) %>%

  select(
    customer_id,
    nama,
    pendapatan
  )

outlier_pendapatan

C007 menjadi observasi yang paling menonjol karena memiliki pendapatan sebesar Rp500.000.000.

Nilai ekstrem tidak langsung dihapus. Dalam data nyata, nilai tersebut perlu diperiksa kembali untuk menentukan apakah merupakan kesalahan pencatatan atau memang menggambarkan kondisi sebenarnya.

Transformasi Data

Setelah proses cleaning, saya mencoba beberapa metode transformasi.

Metode yang digunakan:

  • Min-Max,
  • Z-score,
  • Decimal Scaling.

Ketiga metode tersebut digunakan untuk melihat bagaimana satu variabel dapat direpresentasikan dalam skala yang berbeda.

Min-Max Normalization

Min-Max mengubah nilai ke dalam rentang 0 sampai 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

}

Fungsi kemudian diterapkan pada usia dan pendapatan.

pelanggan$usia_minmax <-
  minmax(
    pelanggan$usia
  )

pelanggan$pendapatan_minmax <-
  minmax(
    pelanggan$pendapatan
  )

Hasil transformasi:

tabel_minmax <- pelanggan %>%

  select(
    customer_id,
    usia,
    usia_minmax,
    pendapatan,
    pendapatan_minmax
  )

kable(
  tabel_minmax,
  digits = 3,
  caption = "Hasil Min-Max Normalization"
)
Hasil Min-Max Normalization
customer_id usia usia_minmax pendapatan pendapatan_minmax
C001 21.0 0.000 4.5e+06 0.000
C002 25.0 0.138 4.9e+06 0.001
C003 23.0 0.069 5.2e+06 0.001
C004 50.0 1.000 4.8e+06 0.001
C005 27.0 0.207 4.9e+06 0.001
C006 27.5 0.224 5.1e+06 0.001
C007 31.0 0.345 5.0e+08 1.000
C008 29.0 0.276 4.7e+06 0.000
C009 22.0 0.034 4.6e+06 0.000
C010 35.0 0.483 5.3e+06 0.002
C011 28.0 0.241 4.9e+06 0.001

Pada pendapatan, C007 mendapatkan nilai 1 karena memiliki nilai maksimum.

Akibatnya, sebagian besar pendapatan pelanggan lainnya berada dekat dengan 0.

Z-Score

Z-score digunakan untuk melihat posisi suatu observasi terhadap rata-rata.

pelanggan$usia_z <-
  as.numeric(
    scale(
      pelanggan$usia
    )
  )

pelanggan$pendapatan_z <-
  as.numeric(
    scale(
      pelanggan$pendapatan
    )
  )

Hasil standardisasi:

tabel_zscore <- pelanggan %>%

  select(
    customer_id,
    usia_z,
    pendapatan_z
  ) %>%

  mutate(

    usia_z =
      round(usia_z, 3),

    pendapatan_z =
      round(pendapatan_z, 3)

  )

kable(
  tabel_zscore,
  caption = "Hasil Standardisasi Z-Score"
)
Hasil Standardisasi Z-Score
customer_id usia_z pendapatan_z
C001 -0.984 -0.304
C002 -0.489 -0.301
C003 -0.737 -0.299
C004 2.604 -0.302
C005 -0.242 -0.301
C006 -0.180 -0.300
C007 0.253 3.015
C008 0.006 -0.303
C009 -0.860 -0.303
C010 0.748 -0.299
C011 -0.118 -0.301

Nilai positif menunjukkan posisi di atas rata-rata, sedangkan nilai negatif menunjukkan posisi di bawah rata-rata.

Pendapatan C007 memiliki nilai Z-score yang jauh lebih tinggi dibandingkan pelanggan lainnya. Hal ini menunjukkan bahwa pendapatan tersebut berada jauh di atas rata-rata.

Decimal Scaling

Decimal scaling dilakukan dengan membagi nilai menggunakan pangkat 10 tertentu.

decimal_scale <- function(x) {

  maks <-
    max(
      abs(x),
      na.rm = TRUE
    )

  if (maks == 0) {

    return(x)

  }

  j <-
    ceiling(
      log10(maks + 1)
    )

  x / (10 ^ j)

}

Fungsi kemudian diterapkan pada pendapatan.

pelanggan$pendapatan_decimal <-
  decimal_scale(
    pelanggan$pendapatan
  )

Rentang hasil decimal scaling:

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

Hasil transformasi hanya mengubah skala angka. Pengaruh nilai ekstrem masih tetap terlihat.

Membandingkan Hasil Transformasi

Untuk melihat perbedaan ketiga metode, saya membuat satu tabel perbandingan.

transformasi <- pelanggan %>%

  select(

    customer_id,

    pendapatan,

    pendapatan_minmax,

    pendapatan_z,

    pendapatan_decimal

  )

kable(

  transformasi,

  digits = 3,

  caption =
    "Perbandingan Transformasi Pendapatan"

)
Perbandingan Transformasi Pendapatan
customer_id pendapatan pendapatan_minmax pendapatan_z pendapatan_decimal
C001 4.5e+06 0.000 -0.304 0.004
C002 4.9e+06 0.001 -0.301 0.005
C003 5.2e+06 0.001 -0.299 0.005
C004 4.8e+06 0.001 -0.302 0.005
C005 4.9e+06 0.001 -0.301 0.005
C006 5.1e+06 0.001 -0.300 0.005
C007 5.0e+08 1.000 3.015 0.500
C008 4.7e+06 0.000 -0.303 0.005
C009 4.6e+06 0.000 -0.303 0.005
C010 5.3e+06 0.002 -0.299 0.005
C011 4.9e+06 0.001 -0.301 0.005

Dari hasil tersebut:

  • Min-Max menghasilkan nilai pada rentang 0 sampai 1.
  • Z-score menunjukkan posisi observasi terhadap rata-rata.
  • Decimal Scaling mengubah skala angka tanpa mengubah pola dasarnya.

Pada data ini, nilai C007 tetap terlihat ekstrem setelah transformasi.

Melihat Pengaruh Outlier

Untuk melihat pengaruh outlier terhadap Min-Max, saya mencoba Winsorization sebagai perbandingan.

Q1_w <- quantile(
  pelanggan$pendapatan,
  0.25,
  na.rm = TRUE
)

Q3_w <- quantile(
  pelanggan$pendapatan,
  0.75,
  na.rm = TRUE
)

IQR_w <- IQR(
  pelanggan$pendapatan,
  na.rm = TRUE
)

lower_w <-
  Q1_w - 1.5 * IQR_w

upper_w <-
  Q3_w + 1.5 * IQR_w

pelanggan$pendapatan_winsor <-
  pmin(
    pmax(
      pelanggan$pendapatan,
      lower_w
    ),
    upper_w
  )

pelanggan$pendapatan_winsor_minmax <-
  minmax(
    pelanggan$pendapatan_winsor
  )

Kemudian dibandingkan dengan data awal.

plot(
  pelanggan$pendapatan_minmax,
  pelanggan$pendapatan_winsor_minmax,
  pch = 19,
  col = "steelblue",
  xlab = "Min-Max Data Awal",
  ylab = "Min-Max Setelah Winsorization",
  main = "Pengaruh Outlier terhadap Min-Max"
)

abline(
  0,
  1,
  col = "tomato",
  lty = 2,
  lwd = 2
)

Dari perbandingan tersebut terlihat bahwa nilai ekstrem dapat memengaruhi hasil Min-Max. Setelah nilai ekstrem dibatasi, penyebaran data menjadi lebih proporsional.

Integrasi Data

MSetelah data pelanggan selesai dibersihkan, tahap berikutnya adalah menggabungkannya dengan data transaksi. Sebelum melakukan penggabungan, saya memastikan terlebih dahulu bahwa customer_id pada kedua dataset dapat digunakan sebagai kunci.

Memeriksa Kunci Data

Hal pertama yang diperiksa adalah apakah terdapat ID yang duplikat pada masing-masing dataset.

sum(duplicated(pelanggan$customer_id))
## [1] 0
sum(duplicated(transaksi$customer_id))
## [1] 0

Hasil pemeriksaan menunjukkan bahwa tidak terdapat ID pelanggan yang duplikat pada kedua dataset. Artinya, setiap pelanggan dan transaksi memiliki identifier yang unik.

Selanjutnya, saya memeriksa ID yang hanya terdapat pada salah satu dataset.

setdiff(
  pelanggan$customer_id,
  transaksi$customer_id
)
##  [1] "C001" "C002" "C003" "C004" "C005" "C006" "C007" "C008" "C009" "C010"
## [11] "C011"

Artinya, C011 terdapat pada data pelanggan tetapi tidak ditemukan pada data transaksi.

Kemudian dilakukan pemeriksaan sebaliknya.

setdiff(
  transaksi$customer_id,
  pelanggan$customer_id
)
## NULL

Jadi, terdapat dua kondisi yang perlu diperhatikan:

-C011 memiliki data pelanggan tetapi tidak memiliki pasangan pada data transaksi. -C012 terdapat pada data transaksi tetapi tidak memiliki data pelanggan.

Kondisi ini penting diperiksa sebelum proses penggabungan agar hasil integrasi tidak salah diinterpretasikan.

##Menyamakan Nama Identifier Pada data yang digunakan, identifier pelanggan pada kedua sumber sudah disiapkan agar menggunakan nama customer_id.

Jika masih menggunakan nama cust_id pada data transaksi, nama variabel tersebut disamakan terlebih dahulu.

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

Setelah nama identifier sama, kedua dataset dapat digabungkan berdasarkan customer_id.

##Menggabungkan Data Pelanggan dan Transaksi Setelah kedua dataset memiliki nama ID yang sama, saya melakukan left_join() menggunakan merge().

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

Hasil penggabungan kemudian dilihat pada beberapa variabel utama.

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

Hasilnya akan menunjukkan data pelanggan beserta informasi transaksinya. Untuk C011, nilai transaksi menjadi NA karena ID tersebut tidak ditemukan pada sumber data transaksi.

Penggunaan all.x = TRUE dipilih karena saya ingin mempertahankan seluruh data pelanggan. Dengan demikian, pelanggan yang belum mempunyai pasangan transaksi tetap muncul di dataset hasil integrasi.

##Memeriksa Hasil Penggabungan Setelah proses merge(), saya melakukan pemeriksaan kembali untuk memastikan tidak ada perubahan jumlah data yang tidak diharapkan.

c(
  sebelum = nrow(pelanggan),
  sesudah = nrow(data_terintegrasi)
)
## sebelum sesudah 
##      11      11

Jumlah baris tetap 11, sehingga proses penggabungan tidak menyebabkan data pelanggan bertambah secara tidak wajar.

Selanjutnya, saya memeriksa kembali apakah customer_id masih unik.

sum(
  duplicated(
    data_terintegrasi$customer_id
  )
)
## [1] 0

Tidak ditemukan ID pelanggan yang duplikat pada dataset hasil integrasi.

##Memeriksa Missing Value dari Data Transaksi Setelah penggabungan, saya memeriksa missing value pada variabel transaksi.

colSums(
  is.na(
    data_terintegrasi[
      ,
      c(
        "jumlah_transaksi",
        "total_purchase"
      )
    ]
  )
)
## jumlah_transaksi   total_purchase 
##                1                1

Missing value tersebut berasal dari C011 yang tidak memiliki pasangan pada data transaksi.

Untuk memastikan pelanggan mana yang mengalami kondisi tersebut:

data_terintegrasi[
  is.na(
    data_terintegrasi$jumlah_transaksi
  ),
  c(
    "customer_id",
    "nama"
  )
]

Jadi, C011 tetap dipertahankan dalam dataset karena data pelanggan tersebut memang tersedia. Hanya saja, informasi transaksinya belum ditemukan pada sumber data yang digunakan.

##Menentukan Perlakuan untuk Nilai NA

Pada tahap ini, NA pada variabel transaksi tidak langsung saya ubah menjadi 0.

Hal ini karena NA dan 0 memiliki arti yang berbeda. Nilai 0 berarti pelanggan diketahui tidak melakukan transaksi, sedangkan NA berarti informasi transaksinya tidak tersedia atau tidak memiliki pasangan pada data yang digunakan.

Karena belum ada informasi yang memastikan bahwa C011 benar-benar belum pernah melakukan transaksi, nilai NA dipertahankan terlebih dahulu.

data_terintegrasi <- data_terintegrasi %>%

  mutate(

    jumlah_transaksi_final = ifelse(
      is.na(jumlah_transaksi),
      0,
      jumlah_transaksi
    ),

    total_purchase_final = ifelse(
      is.na(total_purchase),
      0,
      total_purchase
    )

  )

Jika pada kondisi sebenarnya sudah dipastikan bahwa NA tersebut berarti tidak pernah melakukan transaksi, barulah nilai tersebut dapat diubah menjadi 0.

Contohnya:

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

Dengan demikian, C011 memiliki:

jumlah_transaksi_final = 0 total_purchase_final = 0

Keputusan ini perlu dijelaskan dalam laporan karena 0 hanya tepat digunakan apabila ketiadaan baris transaksi memang dianggap sebagai tidak adanya transaksi yang tercatat.

Dataset Akhir dan Evaluasi

Setelah seluruh proses selesai, dipilih atribut yang akan digunakan pada dataset akhir.

data_final <- data_terintegrasi %>%

  transmute(

    customer_id = customer_id,

    nama = nama,

    usia = usia,

    kota = kota,

    status = status,

    pendapatan = pendapatan,

    usia_minmax = usia_minmax,

    pendapatan_minmax = pendapatan_minmax,

    jumlah_transaksi_final =
      jumlah_transaksi_final,

    total_purchase_final =
      total_purchase_final

  )

Dataset akhir kemudian diperiksa untuk memastikan struktur dan isinya sudah sesuai.

kable(
  data_final,
  digits = 3,
  caption = "Dataset Akhir Setelah Preprocessing dan Integrasi"
)
Dataset Akhir Setelah Preprocessing dan Integrasi
customer_id nama usia kota status pendapatan usia_minmax pendapatan_minmax jumlah_transaksi_final total_purchase_final
C001 Ani 21.0 Pekanbaru Aktif 4.5e+06 0.000 0.000 5 1.5e+06
C002 Budi 25.0 Pekanbaru Aktif 4.9e+06 0.138 0.001 3 9.0e+05
C003 Citra 23.0 Pekanbaru Aktif 5.2e+06 0.069 0.001 7 2.7e+06
C004 Dodi 50.0 Dumai Aktif 4.8e+06 1.000 0.001 2 6.0e+05
C005 Eka 27.0 Pekanbaru Tidak Aktif 4.9e+06 0.207 0.001 6 2.1e+06
C006 Fani 27.5 Dumai Tidak Aktif 5.1e+06 0.224 0.001 4 1.3e+06
C007 Gilang 31.0 Pekanbaru Aktif 5.0e+08 0.345 1.000 20 2.5e+07
C008 Hana 29.0 Siak Aktif 4.7e+06 0.276 0.000 5 1.7e+06
C009 Indra 22.0 Pekanbaru Aktif 4.6e+06 0.034 0.000 3 8.0e+05
C010 Joko 35.0 Dumai Tidak Aktif 5.3e+06 0.483 0.002 8 3.2e+06
C011 Kiki 28.0 Tidak diketahui Aktif 4.9e+06 0.241 0.001 0 0.0e+00

Struktur dataset akhir:

str(data_final)
## 'data.frame':    11 obs. of  10 variables:
##  $ customer_id           : chr  "C001" "C002" "C003" "C004" ...
##  $ nama                  : chr  "Ani" "Budi" "Citra" "Dodi" ...
##  $ usia                  : num  21 25 23 50 27 27.5 31 29 22 35 ...
##  $ kota                  : chr  "Pekanbaru" "Pekanbaru" "Pekanbaru" "Dumai" ...
##  $ status                : chr  "Aktif" "Aktif" "Aktif" "Aktif" ...
##  $ pendapatan            : num  4.5e+06 4.9e+06 5.2e+06 4.8e+06 4.9e+06 5.1e+06 5.0e+08 4.7e+06 4.6e+06 5.3e+06 ...
##  $ usia_minmax           : num  0 0.138 0.069 1 0.207 ...
##  $ pendapatan_minmax     : num  0 0.000807 0.001413 0.000605 0.000807 ...
##  $ jumlah_transaksi_final: num  5 3 7 2 6 4 20 5 3 8 ...
##  $ total_purchase_final  : num  1.5e+06 9.0e+05 2.7e+06 6.0e+05 2.1e+06 1.3e+06 2.5e+07 1.7e+06 8.0e+05 3.2e+06 ...

Jumlah baris dan kolom:

dim(data_final)
## [1] 11 10

Audit Akhir

Setelah dataset akhir terbentuk, audit kembali dilakukan menggunakan fungsi yang sama.

audit_akhir <- audit_data(
  data_final
)

kable(
  audit_akhir,
  caption = "Audit Akhir Dataset"
)
Audit Akhir Dataset
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 numeric 0 0 9
usia_minmax numeric 0 0 11
pendapatan_minmax numeric 0 0 9
jumlah_transaksi_final numeric 0 0 9
total_purchase_final numeric 0 0 11

Memeriksa Duplikasi Dataset Akhir

sum(
  duplicated(
    data_final$customer_id
  )
)
## [1] 0

Hasilnya adalah 0 sehingga seluruh customer_id pada dataset akhir tetap unik.

Memeriksa Missing Value Dataset Akhir

colSums(
  is.na(data_final)
)
##            customer_id                   nama                   usia 
##                      0                      0                      0 
##                   kota                 status             pendapatan 
##                      0                      0                      0 
##            usia_minmax      pendapatan_minmax jumlah_transaksi_final 
##                      0                      0                      0 
##   total_purchase_final 
##                      0

Missing value pada jumlah_transaksi dan total_purchase tidak selalu menunjukkan masalah pada data pelanggan.

Contohnya, C011 terdapat pada data pelanggan tetapi tidak memiliki pasangan pada data transaksi.

Artinya, pelanggan tersebut belum memiliki data transaksi pada dataset yang digunakan.

##Membandingkan Kondisi Sebelum dan Sesudah Untuk melihat perubahan kualitas data, kondisi dataset awal dibandingkan dengan dataset akhir.

perbandingan <- data.frame(

  indikator = c(
    "Jumlah baris",
    "Duplikasi customer_id",
    "Total missing value",
    "Kategori kota unik",
    "Kategori status unik"
  ),

  sebelum = c(

    nrow(pelanggan_awal),

    sum(
      duplicated(
        pelanggan_awal$customer_id
      )
    ),

    sum(
      is.na(pelanggan_awal)
    ),

    length(
      unique(
        na.omit(
          pelanggan_awal$kota
        )
      )
    ),

    length(
      unique(
        na.omit(
          pelanggan_awal$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
      )
    )
  )
)

kable(
  perbandingan,
  caption = "Perbandingan Kondisi Sebelum dan Sesudah Preprocessing"
)
Perbandingan Kondisi Sebelum dan Sesudah Preprocessing
indikator sebelum sesudah
Jumlah baris 12 11
Duplikasi customer_id 1 0
Total missing value 4 0
Kategori kota unik 9 4
Kategori status unik 8 2

Menyimpan hasil

write.csv(
  data_final,
  "data_final_pelanggan.csv",
  row.names = FALSE
)

Temuan Utama

Beberapa hal yang terlihat dari proses preprocessing ini adalah:

1. Data awal memiliki beberapa masalah kualitas

Terdapat missing value, duplikasi ID, kategori yang belum konsisten, dan satu nilai usia yang tidak sesuai dengan konteks data.

2. Kategori perlu diseragamkan

Variabel kota dan status memiliki beberapa bentuk penulisan yang berbeda meskipun merujuk pada kategori yang sama.

3. Nilai ekstrem perlu diperiksa

C007 memiliki pendapatan Rp500.000.000 dan terdeteksi sebagai outlier berdasarkan metode IQR.

4. Transformasi tidak menghilangkan nilai ekstrem

Min-Max, Z-score, dan decimal scaling hanya mengubah representasi atau skala data. Nilai ekstrem tetap memiliki pengaruh terhadap hasil.

5. Tidak semua pelanggan memiliki transaksi

Setelah left_join(), terdapat pelanggan yang tidak memiliki pasangan data transaksi. Kondisi tersebut menghasilkan NA pada variabel transaksi dan perlu dibedakan dari missing value pada data awal.

Kesimpulan

Project ini menunjukkan proses preprocessing data pelanggan menggunakan R mulai dari kondisi awal sampai menghasilkan dataset yang lebih terstruktur.

Pada tahap eksplorasi awal ditemukan beberapa masalah, yaitu missing value, kategori yang belum konsisten, duplikasi ID, dan nilai usia yang tidak wajar.

Data kemudian dibersihkan dengan menyeragamkan kategori kota dan status, menghapus duplikasi berdasarkan customer_id, serta melakukan koreksi pada nilai usia yang tidak sesuai.

Missing value pada variabel numerik ditangani menggunakan median, sedangkan missing value pada variabel kota diganti dengan kategori "Tidak diketahui".

Setelah itu dilakukan deteksi outlier menggunakan metode IQR. Hasilnya menunjukkan bahwa C007 memiliki pendapatan yang jauh lebih tinggi dibandingkan pelanggan lainnya. Nilai tersebut tidak langsung dihapus karena masih perlu dilihat berdasarkan konteks data.

Saya juga mencoba tiga metode transformasi, yaitu Min-Max, Z-score, dan decimal scaling. Hasilnya menunjukkan bahwa setiap metode memiliki karakteristik yang berbeda dan nilai ekstrem tetap dapat memengaruhi hasil transformasi.

Pada tahap integrasi, data pelanggan digabungkan dengan data transaksi menggunakan left_join() berdasarkan customer_id. Dari proses tersebut dapat diketahui bahwa tidak semua pelanggan memiliki data transaksi.

Secara keseluruhan, project ini menjadi latihan untuk memahami bahwa preprocessing bukan hanya tentang menghapus data yang bermasalah, tetapi juga tentang memahami kondisi data sebelum menentukan tindakan yang tepat.

Apa yang Saya Pelajari

Dari project ini, saya mendapatkan beberapa pengalaman dalam menggunakan R untuk pengolahan data.

Beberapa hal yang saya pelajari antara lain:

  • melakukan eksplorasi dataset menggunakan R,
  • menggunakan dplyr untuk proses cleaning,
  • menangani missing value,
  • memeriksa duplikasi,
  • menyeragamkan data kategorik,
  • mendeteksi nilai yang tidak wajar,
  • menggunakan IQR untuk mendeteksi outlier,
  • melakukan normalisasi dan standardisasi,
  • memahami perbedaan beberapa metode transformasi,
  • menggunakan left_join() untuk menggabungkan dataset,
  • serta melakukan pemeriksaan kembali setelah proses preprocessing.

Project ini juga membantu saya memahami bahwa kualitas data sangat berpengaruh terhadap proses analisis berikutnya.

From Data to Insight

Mengenali masalah pada data, memperbaikinya dengan tepat, dan memahami informasi yang dihasilkan.

Nia Andini
Statistika • Data Analysis • R Programming