Setelah menyelesaikan praktikum, mahasiswa mampu:
Sebuah perusahaan e-commerce akan melakukan analisis pelanggan. Data berasal dari dua sumber:
Data belum dapat langsung digunakan karena mengandung nilai hilang, kategori tidak konsisten, duplikasi, nilai ekstrem, dan perbedaan nama identifier. Mahasiswa diminta menyiapkan data tersebut sampai menjadi satu dataset analisis yang bersih.
Prinsip utama: jangan membersihkan data hanya karena sebuah nilai terlihat aneh. Setiap perubahan harus memiliki alasan, aturan, dan catatan.
getwd()
## [1] "C:/Users/Lenovo/Downloads"
R.version.string
## [1] "R version 4.4.1 (2024-06-14 ucrt)"
Modul ini menggunakan fungsi dasar R sehingga dapat dijalankan tanpa memasang paket tambahan. Berikut penjelasannya :
getwd() digunakan untuk
mengetahui lokasi file yang dibaca atau disimpan selama analisis di
R.R.version.string digunakan
untuk mengetahui versi R yang sedang digunakan.Jalankan kode berikut. Dataset sengaja dibuat “berantakan/mengandung kesalahan” agar seluruh tahap preprocessing dapat dipraktikkan.
options(scipen = 999)
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 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
## 7 C007 Gilang 31 500000000 Pekanbaru Aktif
## 8 C008 Hana 29 4700000 Siak AKTIF
## 9 C009 Indra 22 4600000 PKU A
## 10 C010 Joko 35 5300000 Dumai Tidak aktif
## 11 C010 Joko 35 5300000 Dumai Tidak aktif
## 12 C011 Kiki 28 NA <NA> Aktif
Interpretasi:
customer_id,
nama, usia,
pendapatan,
kota, dan
status. 2.Terdapat data duplikat, yaitu
customer_id C010 muncul dua kali dengan
informasi yang sama.pendapatan untuk Budi (C002) dan Kiki
(C011).kota pada C011 juga memiliki
nilai NA.status juga memiliki
penulisan yang tidak konsisten, seperti Aktif, AKTIF, A, aktif, dan
Tidak aktif.Secara keseluruhan, data masih berupa data mentah dan perlu dilakukan pembersihan (data cleaning) sebelum digunakan untuk analisis lebih lanjut.
Berikut data transaksi setiap pelanggan
options(scipen = 999)
transaksi_raw
## cust_id jumlah_transaksi total_purchase
## 1 C001 5 1500000
## 2 C002 3 900000
## 3 C003 7 2700000
## 4 C004 2 600000
## 5 C005 6 2100000
## 6 C006 4 1300000
## 7 C007 20 25000000
## 8 C008 5 1700000
## 9 C009 3 800000
## 10 C010 8 3200000
## 11 C012 1 250000
Interpretasi :
cust_id,
jumlah_transaksi, dan
total_purchase.cust_id menunjukkan identitas
pelanggan yang melakukan transaksi.jumlah_transaksi menunjukkan banyaknya
transaksi yang dilakukan oleh masing-masing pelanggan.total_purchase menunjukkan total nilai
pembelian pelanggan.Data transaksi ini dapat digunakan untuk melihat aktivitas dan nilai pembelian setiap pelanggan.Sebelum analisis lebih lanjut, data perlu diperiksa untuk memastikan tidak terdapat kesalahan atau ketidaksesuaian data.
Sebelum melakukan perubahan, periksa struktur, dimensi, tipe atribut, dan beberapa observasi awal.
dim(pelanggan_raw)
## [1] 12 6
Interpretasi:
dim(pelanggan_raw) menghasilkan
12 dan 6.customer_id,
nama, usia,
pendapatan,
kota, dan
status.Dengan demikian, struktur awal data
pelanggan_raw adalah 12 observasi dan 6
variabel. Informasi ini digunakan sebagai gambaran awal untuk mengetahui
ukuran dataset sebelum dilakukan proses pembersihan dan analisis
data.
names(pelanggan_raw)
## [1] "customer_id" "nama" "usia" "pendapatan" "kota"
## [6] "status"
Interpretasi:
names(pelanggan_raw) digunakan
untuk menampilkan nama-nama variabel atau kolom yang terdapat dalam
dataset.pelanggan_raw memiliki 6
variabel, yaitu:customer_id → identitas
pelanggan.nama → nama pelanggan.usia → usia pelanggan.pendapatan → pendapatan
pelanggan.kota → kota tempat pelanggan.status → status pelanggan.Hasil tersebut menunjukkan bahwa nama kolom sudah tersedia dan dapat digunakan sebagai acuan dalam proses pengolahan serta pembersihan data.
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 4500000 NA 5200000 4800000 4900000 5100000 500000000 4700000 4600000 5300000 ...
## $ kota : chr "Pekanbaru" " PKU" "PEKANBARU" "Dumai" ...
## $ status : chr "Aktif" "aktif" "ACTIVE" "A" ...
Interpretasi
str(pelanggan_raw) digunakan
untuk melihat struktur dan tipe data dari setiap variabel dalam
dataset.pelanggan_raw terdiri dari 12
observasi dan 6 variabel.customer_id,
nama, kota,
dan status bertipe character
(chr), sehingga berisi data berupa teks.usia dan
pendapatan bertipe numeric
(num), sehingga dapat digunakan dalam perhitungan
numerik. Terdapat nilai NA yang menunjukkan adanya data yang
hilang.kota dan
status masih memiliki penulisan yang tidak
seragam, seperti “Pekanbaru, PKU, PEKANBARU”, serta “Aktif, aktif, dan
AKTIF”.Hasil ini menunjukkan bahwa struktur data sudah terbentuk dengan baik, tetapi masih diperlukan pengecekan nilai yang hilang dan konsistensi data sebelum dilakukan analisis lebih lanjut.
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 hasil
summary(pelanggan_raw), dataset pelanggan
terdiri dari 12 observasi dengan variabel numerik
usia dan
pendapatan, serta variabel karakter berupa
customer_id,
nama, kota,
dan status. Variabel
usia memiliki rata-rata 28,73 tahun,
dengan usia termuda 21 tahun dan tertua 150 tahun. Nilai 150 tahun
terlihat tidak wajar sehingga perlu diperiksa sebagai kemungkinan
kesalahan input. Terdapat 1 data usia yang hilang (NA).
Pada variabel pendapatan, nilai minimum
adalah Rp4.500.000, maksimum Rp500.000.000, dan rata-ratanya
Rp54.440.000. Terdapat 2 data pendapatan yang hilang (NA). Perbedaan
yang cukup besar antara nilai maksimum dan nilai lainnya menunjukkan
adanya nilai pendapatan yang sangat tinggi, yaitu Rp500.000.000,
sehingga perlu diperiksa lebih lanjut.
Sementara itu, variabel customer_id,
nama, kota,
dan status merupakan data bertipe
karakter. Hasil ringkasan ini menunjukkan bahwa sebelum analisis
dilakukan, data perlu melalui tahap pemeriksaan dan pembersihan,
terutama pada data yang hilang, nilai usia yang tidak wajar, serta nilai
pendapatan yang sangat tinggi
Enam dimensi kualitas data yang dibahas Han, Kamber, dan Pei meliputi accuracy, completeness, consistency, timeliness, believability, dan interpretability. Tidak semua dimensi dapat dihitung hanya dari isi tabel. Misalnya, accuracy memerlukan pembanding terhadap kondisi sebenarnya dan timeliness membutuhkan informasi waktu pemutakhiran.
Pemeriksaan awal berikut berfokus pada masalah yang dapat dideteksi 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
Berdasarkan hasil
colSums(is.na(pelanggan_raw)), dapat
diketahui jumlah missing value (data yang hilang) pada setiap
variabel dalam dataset pelanggan:
customer_id,
nama dan
status memiliki 0 missing value,
sehingga seluruh data pelanggan tersedia.usia dan
kota memiliki 1 missing value,
sehingga masing masing variabel terdapat satu pelanggan yang belum
memiliki data usia dan data informasi kota.pendapatan memiliki 2
missing value, sehingga terdapat dua pelanggan yang belum
memiliki data pendapatan.Secara keseluruhan, missing value terdapat pada tiga variabel, yaitu usia, pendapatan, dan kota. Data tersebut perlu ditangani pada tahap pembersihan agar tidak memengaruhi hasil analisis selanjutnya.
# 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
Berdasarkan hasil perhitungan, variabel
usia dan
kota memiliki missing value
sebesar 8,33, sedangkan pendapatan
memiliki missing value sebesar 16,67%. Sementara itu,
customer_id,
nama, dan
status tidak memiliki missing
value sehingga persentasenya 0.00. Dengan demikian,
pendapatan memiliki persentase data hilang
paling tinggi dan perlu diperhatikan dalam proses pembersihan data.
# Banyak baris duplikat penuh
sum(duplicated(pelanggan_raw))
## [1] 1
# Banyak customer_id yang berulang
sum(duplicated(pelanggan_raw$customer_id))
## [1] 1
Berdasarkan hasil pemeriksaan, terdapat 1 baris data yang
terduplikasi pada dataset pelanggan_raw.
Pemeriksaan berdasarkan customer_id juga
menunjukkan terdapat 1 data customer_id
yang berulang, sehingga data-data duplikat tersebut perlu diperiksa dan
ditangani agar tidak memengaruhi hasil analisis.
# 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"
Berdasarkan hasil pemeriksaan, terdapat ketidakkonsistenan penulisan
pada atribut kota dan
status. Pada atribut
kota, satu kota ditulis dalam beberapa
bentuk, seperti “Pekanbaru”, “pekanbaru”, “PEKANBARU”, dan “PKU”,
sedangkan pada atribut status terdapat
variasi seperti “Aktif”, “aktif”, “AKTIF”, “ACTIVE”, dan “A”. Kondisi
ini menunjukkan bahwa kategori belum seragam sehingga perlu dilakukan
standardisasi penulisan agar setiap kategori memiliki format yang
konsisten.
# Rentang atribut numerik
range(pelanggan_raw$usia, na.rm = TRUE)
## [1] 21 150
range(pelanggan_raw$pendapatan, na.rm = TRUE)
## [1] 4500000 500000000
Berdasarkan hasil pemeriksaan tersebut, variabel
usia memiliki rentang 21 – 150 tahun,
sedangkan pendapatan memiliki rentang Rp
4.500.000 – Rp 500.000.000. Nilai usia 150 tahun dan pendapatan
Rp500.000.000 terlihat sangat ekstrem dibandingkan nilai lainnya
sehingga terindikasi sebagai adanya data outlier dan perlu
diperiksa lebih lanjut sebelum dilakukan analisis.
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
Berikut ini Hasil secara keseluruhan, pemeriksaan ini memberikan gambaran umum mengenai kondisi kualitas data sebelum dilakukan proses cleaning lebih lanjut. Hasil audit bukan keputusan cleaning. Audit hanya memberi sinyal atribut mana yang perlu diperiksa lebih lanjut.
Pertahankan data mentah agar setiap perubahan dapat dilacak.
pelanggan <- pelanggan_raw
pelanggan
## 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
## 7 C007 Gilang 31 500000000 Pekanbaru Aktif
## 8 C008 Hana 29 4700000 Siak AKTIF
## 9 C009 Indra 22 4600000 PKU A
## 10 C010 Joko 35 5300000 Dumai Tidak aktif
## 11 C010 Joko 35 5300000 Dumai Tidak aktif
## 12 C011 Kiki 28 NA <NA> Aktif
# 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"
Tahapan standardisasi ini berfungsi untuk menunjukkan bahwa spasi
yang tidak diperlukan dan perbedaan kapitalisasi telah dibersihkan,
sehingga kategori pada variabel kota
menjadi seragam dalam huruf kecil. Kategori yang tersisa adalah “dumai”,
“pekanbaru”, “pku”, dan “siak”, yang selanjutnya dapat digunakan sebagai
dasar untuk proses penyeragaman kategori sesuai aturan yang telah
ditentukan.
Standardisasi dilakukan menggunakan aturan domain. Dalam praktik nyata, aturan perlu dikonfirmasi melalui kamus data atau pemilik data.
# 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"
Tahap standardisasi ini menunjukkan bahwa seluruh kategori pada
variabel kota dan
status telah berhasil diseragamkan.
Kategori kota kini terdiri dari Dumai, Pekanbaru, dan Siak, sedangkan
kategori status menjadi Aktif dan Tidak Aktif. Dengan demikian,
perbedaan penulisan seperti kapitalisasi, singkatan, dan variasi
kategori telah diperbaiki sehingga data lebih konsisten dan siap
digunakan untuk tahap preprocessing berikutnya.
# 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
Hasil pemeriksaan menunjukkan bahwa terdapat satu
customer_id yang terduplikasi, yaitu
“C010”, yang muncul sebanyak dua kali dengan informasi yang sama.
Setelah dilakukan penghapusan baris dengan mempertahankan kemunculan
pertama, jumlah data berkurang dari 12 menjadi 11 baris, sementara
jumlah variabel tetap 6 kolom. Dengan demikian, data duplikat berhasil
dihapus tanpa mengubah struktur variabel.
Catatan: menghapus duplikasi hanya aman jika setiap
customer_idmemang harus mewakili satu pelanggan. Jika satu pelanggan boleh memiliki banyak baris, tindakan ini justru salah.
# 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>
Nilai usia 150 melanggar aturan domain. Misalkan pemeriksaan terhadap formulir asli menunjukkan bahwa nilai yang benar adalah 50.
pelanggan$usia[pelanggan$customer_id == "C004"] <- 50
Pemeriksaan aturan domain dilakukan untuk memastikan bahwa nilai dalam dataset sesuai dengan batas atau aturan yang telah ditentukan. Pada tahap ini, usia diperiksa berdasarkan rentang 15–100 tahun dan pendapatan diperiksa agar tidak bernilai negatif. Hasil pemeriksaan menunjukkan bahwa usia 150 tahun pada C004 tidak memenuhi aturan domain, sehingga setelah diverifikasi sebagai kesalahan input, nilainya dikoreksi menjadi 50 tahun.
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
Pembuatan log_perubahan dilakukan untuk
mendokumentasikan setiap perubahan yang diterapkan pada dataset selama
proses data cleaning. Tabel ini mencatat tahap proses, atribut
yang mengalami perubahan, serta tindakan yang dilakukan, sehingga
perubahan data dapat ditelusuri dengan jelas. Pada hasil tersebut,
proses yang dilakukan meliputi standardisasi pada atribut
kota dan
status, deduplikasi pada
customer_id dengan menghapus kemunculan
kedua C010, serta koreksi domain pada atribut
usia, yaitu mengubah usia C004 dari 150
menjadi 50 berdasarkan sumber asli. Dengan adanya
log_perubahan, proses pembersihan data
menjadi lebih transparan, sistematis, terdokumentasi, dan dapat
dipertanggungjawabkan, karena setiap perubahan memiliki alasan dan
catatan yang jelas.
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
Berdasarkan hasil identifikasi tersebut, terdapat missing
value pada tiga variabel, yaitu usia
sebanyak 1 data, pendapatan sebanyak 2
data, dan kota sebanyak 1 data. Variabel
customer_id,
nama, dan
status tidak memiliki missing
value. Dari pemeriksaan baris yang tidak lengkap, terdapat 3
pelanggan, yaitu C002 dengan data pendapatan yang hilang, C006 dengan
data usia yang hilang, serta C011 dengan data pendapatan dan kota yang
hilang. Hasil ini menjadi dasar untuk menentukan strategi penanganan
missing value pada tahap selanjutnya.
Kode berikut hanya mendemonstrasikan strategi. Kita tidak langsung
mengganti objek pelanggan.
pelanggan_complete <- pelanggan[complete.cases(pelanggan), ]
nrow(pelanggan)
## [1] 11
nrow(pelanggan_complete)
## [1] 8
Dari hasil analisis yang dilakukan, diketahui bahwa data awal
pelanggan memiliki total 11 baris observasi. Kemudian, diterapkan
strategi pertama yaitu penghapusan baris (listwise deletion)
dengan menggunakan fungsi
complete.cases(), yang berfungsi untuk
memilih hanya baris-baris yang tidak memiliki satupun nilai kosong
(missing value) di seluruh kolomnya. Setelah proses ini,
diperoleh objek baru bernama
pelanggan_complete yang hanya menyisakan 8
baris data lengkap. Artinya, terdapat 3 baris yang terpaksa harus
dibuang karena mengandung setidaknya satu nilai yang hilang.
Pertanyaan: berapa persen data yang hilang jika seluruh baris tidak lengkap dihapus?
round((1 - nrow(pelanggan_complete) / nrow(pelanggan)) * 100, 2)
## [1] 27.27
Ketika kita menghitung proporsi data yang hilang akibat penghapusan ini, rumus yang digunakan adalah membandingkan jumlah baris yang tersisa dengan jumlah baris awal, lalu dikonversi ke dalam persentase kerugian data. Perhitungan tersebut menghasilkan angka 27,27%. Ini berarti lebih dari seperempat dari total dataset asli (tepatnya 3 dari 11 baris) harus dikorbankan hanya karena setiap baris tersebut memiliki kekurangan pada satu atau lebih variabel. Meskipun metode ini sederhana dan mudah diterapkan, konsekuensinya sangat signifikan karena kita kehilangan porsi data yang tidak kecil.
Kerugian sebesar 27,27% bukanlah angka yang bisa diabaikan, terutama jika ukuran sampel awal memang sudah terbatas. Dengan membuang hampir sepertiga data, kita berisiko mengurangi kekuatan statistik (statistical power), meningkatkan bias jika data yang hilang tidak terjadi secara acak (misalnya jika baris yang hilang memiliki karakteristik tertentu), dan tentu saja menyusutkan informasi yang tersedia untuk analisis lebih lanjut. Dalam konteks ini, strategi penghapusan baris memang hanya ditunjukkan sebagai demonstrasi awal, bukan sebagai rekomendasi akhir.
Jika pola data yang kosong bersifat acak, imputasi bisa menjadi pilihan yang lebih bijak untuk mempertahankan ukuran sampel, sedangkan jika ternyata data hilang secara sistematis, penghapusan baris justru bisa memperparah kesalahan inferensi. Oleh karena itu, persentase 27,27% ini adalah peringatan awal bahwa data perlu ditangani lebih hati-hati, dan keputusan untuk menghapus baris sebaiknya tidak diambil tanpa eksplorasi lebih lanjut terhadap pola missing value yang ada.
Pada bagian ini, kita mulai memasuki strategi imputasi, yaitu mengisi nilai kosong (missing values) dengan nilai pengganti tertentu, sebagai alternatif dari penghapusan baris yang telah dilakukan sebelumnya.Pendapatan memiliki nilai ekstrem. Bandingkan mean dan median sebelum menentukan imputasi.
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 distribusi pendapatan dalam data ini sangat tidak simetris
atau miring (skewed) yang disebabkan oleh adanya satu nilai
ekstrem sebesar 500 juta, maka nilai rata-rata (mean) menjadi tidak
representatif karena sangat terpengaruh oleh pencilan tersebut. Hal ini
terbukti dari output yang menunjukkan bahwa
mean_pendapatan adalah 59.900.000,
sedangkan median_pendapatan hanya
4.900.000. Perbedaan yang sangat jauh ini mengindikasikan bahwa mean
terpengaruh oleh nilai 500 juta, sehingga tidak mencerminkan pusat data
secara umum. Oleh karena itu, dalam contoh ini, median dipilih sebagai
ukuran pemusatan yang lebih stabil dan robust terhadap pencilan.
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")]
## customer_id usia usia_imputasi pendapatan pendapatan_imputasi
## 1 C001 21 21.0 4500000 4500000
## 2 C002 25 25.0 NA 4900000
## 3 C003 23 23.0 5200000 5200000
## 4 C004 50 50.0 4800000 4800000
## 5 C005 27 27.0 4900000 4900000
## 6 C006 NA 27.5 5100000 5100000
## 7 C007 31 31.0 500000000 500000000
## 8 C008 29 29.0 4700000 4700000
## 9 C009 22 22.0 4600000 4600000
## 10 C010 35 35.0 5300000 5300000
## 11 C011 28 28.0 NA 4900000
Proses imputasi dilakukan dengan mengganti nilai kosong dengan nilai median. Untuk variabel pendapatan, dibuat kolom baru bernama pendapatan_imputasi yang awalnya merupakan salinan dari kolom pendapatan asli. Kemudian, pada kolom baru tersebut, semua nilai NA (kosong) diganti dengan nilai median pendapatan yang sudah dihitung sebelumnya, yaitu 4.900.000. Proses yang sama juga diterapkan pada variabel usia, di mana dibuat kolom usia_imputasi yang merupakan salinan dari usia, lalu nilai NA pada kolom baru tersebut diisi dengan median usia (yang dihitung terlebih dahulu dari data yang tersedia). Dengan cara ini, tidak ada baris yang dihapus, melainkan setiap nilai kosong digantikan oleh nilai pengganti yang dianggap paling mewakili kecenderungan pusat data untuk masing-masing variabel.
Dengan demikian, strategi imputasi ini memungkinkan kita untuk mempertahankan seluruh 11 baris data tanpa kehilangan informasi, tidak seperti strategi penghapusan baris sebelumnya yang menghilangkan 27,27% data. Namun, perlu diingat bahwa imputasi dengan median ini juga memiliki kelemahan, yaitu dapat mengurangi variabilitas alami data dan berpotensi memperkenalkan bias jika proporsi data yang hilang cukup besar atau jika pola ketidakhadirannya tidak acak. Namun, untuk tahap eksplorasi pendekatan ini jauh lebih baik dibandingkan membuang baris secara langsung, terutama ketika ukuran sampel terbatas dan distribusi data tidak normal.
Nilai kota yang hilang tidak selalu tepat diisi dengan modus. Untuk
latihan ini kita gunakan kategori eksplisit Tidak diketahui
agar ketidakpastian tidak disembunyikan.
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
Pada strategi ketiga ini, penanganan data hilang difokuskan pada variabel kategorik, yaitu kota. Berbeda dengan variabel numerik yang menggunakan median, untuk variabel kota yang dilakukan adalah tidak menggunakan modus (nilai paling sering muncul) karena akan memberi kesan seolah-olah kita mengetahui kota tersebut padahal sebenarnya hanya menebak. Sebagai gantinya, nilai NA pada kolom kota diisi dengan kategori eksplisit “Tidak diketahui” agar ketidakpastian tetap terjaga dan tidak disembunyikan. Dari hasil tabel, terlihat bahwa setelah imputasi terdapat 3 pelanggan di Dumai, 6 di Pekanbaru, 1 di Siak, dan 1 pelanggan dengan kategori “Tidak diketahui”, yang menunjukkan bahwa hanya ada satu baris dengan data kota yang hilang.
Indikator dapat mempertahankan informasi bahwa suatu nilai awalnya hilang.
pelanggan$pendapatan_missing <- as.integer(is.na(pelanggan$pendapatan))
table(pelanggan$pendapatan_missing)
##
## 0 1
## 9 2
Selain itu, untuk variabel pendapatan juga ditambahkan indikator
missing berupa kolom
pendapatan_missing yang bernilai 1 jika
pendapatan asli hilang dan 0 jika tersedia. Dari tabel terlihat ada 9
baris dengan data pendapatan lengkap dan 2 baris yang awalnya kosong.
Indikator ini sangat berguna karena mempertahankan informasi bahwa suatu
nilai pernah hilang, sehingga ketika kita sudah mengisi nilai tersebut
dengan median, model atau analisis tetap bisa mengetahui pola
ketidakhadiran data asli. Dengan menggabungkan imputasi nilai dan
indikator missing, kita tidak hanya memperbaiki data tetapi juga menjaga
transparansi dan kualitas analisis secara keseluruhan.
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)
Dari kedua histogram yang ditampilkan, kita bisa membandingkan distribusi pendapatan sebelum dan sesudah imputasi dengan median. Pada histogram Sebelum Imputasi, terlihat bahwa data sangat miring ke kanan dengan satu nilai ekstrem di sekitar 500 juta, sementara sebagian besar data berada di kisaran rendah. Setelah imputasi median (Sesudah Imputasi Median), distribusi berubah karena dua nilai yang hilang kini diisi dengan median 4,9 juta. Akibatnya, frekuensi pada rentang nilai rendah (di sekitar median) meningkat, sedangkan bentuk distribusi secara keseluruhan tetap mempertahankan kemiringan yang sama karena nilai ekstrem 500 juta masih dipertahankan. Imputasi median berhasil mengisi kekosongan tanpa mengganggu struktur distribusi secara drastis, namun perlu diingat bahwa variabilitas data sedikit berkurang karena nilai imputasi tidak bervariasi seperti data asli. Perbandingan ini penting untuk memastikan bahwa proses imputasi tidak mengubah karakteristik utama data secara berarti.
par(mfrow = c(1, 1))
Setelah membandingkan histogram, kode menutup tata letak grafik
dengan par(mfrow = c(1, 1)) untuk
mengembalikan pengaturan plot ke mode standar. Imputasi dapat mengubah
distribusi dan mengecilkan variasi. Oleh karena itu, evaluasi tidak
berhenti setelah semua NA hilang. Proses
imputasi, terutama dengan nilai tunggal seperti median, dapat mengubah
distribusi data asli dan mengecilkan variasi alami karena nilai-nilai
yang diimputasi cenderung berkerumun di sekitar pusat data. Akibatnya,
ukuran penyebaran seperti standar deviasi bisa mengecil, dan hubungan
antarvariabel bisa terganggu. Oleh karena itu, evaluasi harus
dilanjutkan dengan memeriksa apakah perubahan yang terjadi masih dapat
diterima untuk tujuan analisis, misalnya dengan membandingkan statistik
deskriptif, visualisasi distribusi, atau bahkan menguji sensitivitas
model terhadap metode imputasi yang berbeda. Intinya, mengisi nilai
hilang hanyalah satu langkah, tetapi validasi dampaknya terhadap
kualitas data merupakan bagian yang tidak kalah penting. Imputasi dapat
mengubah distribusi dan mengecilkan variasi. Karena itu, evaluasi tidak
berhenti setelah semua NA hilang.
boxplot(pelanggan$pendapatan_imputasi,
horizontal = TRUE,
col = "lightblue",
main = "Boxplot Pendapatan",
xlab = "Pendapatan")
Pada bagian ini, dilakukan visualisasi untuk mendeteksi
outlier (pencilan) pada variabel
pendapatan_imputasi menggunakan boxplot
horizontal. Dari boxplot yang dihasilkan, terlihat bahwa sebagian besar
data pendapatan berada disekitar nilai rendah hingga sedang, yang
ditunjukkan oleh kotak (box) yang sempit di sisi kiri sumbu. Namun,
terdapat satu titik yang sangat jauh di sebelah kanan, yaitu pada nilai
sekitar “500 juta”, yang muncul sebagai lingkaran terisolasi di luar
garis “whisker” (batas atas). Titik ini adalah outlier yang
jelas karena nilainya melampaui batas wajar dari sebaran data utama.
Outlier ini sebenarnya adalah nilai ekstrem yang sudah
teridentifikasi sebelumnya sebagai penyebab kemiringan distribusi.
Dengan boxplot, kita bisa secara visual mengonfirmasi bahwa data
pendapatan memiliki satu pencilan ekstrem yang perlu mendapat perhatian
khusus, karena outlier seperti ini dapat mempengaruhi berbagai
analisis statistik, seperti mean, regresi, atau pengujian hipotesis.
Langkah selanjutnya biasanya adalah memutuskan apakah outlier
ini perlu dihapus, ditransformasi, atau dipertahankan dengan berbagai
pertimbangan. Boxplot ini menjadi visualisasi awal yang efektif untuk
mengidentifikasi keberadaan dan tingkat keparahan outlier
sebelum menentukan strategi penanganannya.
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
Setelah visualisasi boxplot dilakukan, langkah selanjutnya adalah menghitung secara numerik batas-batas untuk mendeteksi outlier menggunakan metode “IQR (Interquartile Range)”. Dari perhitungan, diperoleh kuartil pertama (Q1) sebesar 4.750.000” dan kuartil ketiga (Q3) sebesar 5.150.000, sehingga nilai IQR yang diperoleh adalah 400.000.
Dengan menggunakan aturan q1 - (1,5 × IQR) dan q3 + (1,5 × IQR) , batas bawah berada di 4.150.000 dan batas atas di 5.750.000. Ini berarti bahwa setiap nilai pendapatan di bawah 4.150.000 atau di atas 5.750.000 secara statistik dianggap sebagai outlier. Dari hasil ini, jelas terlihat bahwa nilai ekstrem 500 juta jauh melampaui batas atas 5,75 juta,
sehingga terkonfirmasi secara kuantitatif sebagai outlier yang sangat ekstrem. Perhitungan ini memberikan dasar objektif untuk memutuskan apakah outlier tersebut perlu ditangani, misalnya dengan transformasi data, winsorizing (mengganti nilai ekstrem dengan nilai batas), atau tetap dipertahankan. Metode IQR ini konsisten dengan visualisasi boxplot sebelumnya dan memberikan ukuran yang lebih presisi untuk mendukung pengambilan keputusan.
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 500000000
Setelah batas IQR ditetapkan, langkah berikutnya adalah menandai
kandidat outlier secara otomatis dengan membuat kolom baru
bernama outlier_pendapatan. Kolom ini
berisi nilai logika TRUE jika nilai
pendapatan imputasi berada di bawah batas bawah (4.150.000) atau di atas
batas atas (5.750.000), dan FALSE jika
sebaliknya. Kemudian, dilakukan filter untuk menampilkan hanya
baris-baris yang teridentifikasi sebagai outlier, dengan memilih kolom
customer_id,
nama, dan
pendapatan_imputasi.
Dari hasil output, terlihat bahwa hanya satu baris data yang memenuhi kriteria, yaitu pelanggan dengan ID “C007” bernama “Gilang”, yang memiliki nilai pendapatan imputasi sebesar 500 juta. Temuan ini mengonfirmasi bahwa dari seluruh data, hanya ada satu nilai ekstrem yang benar-benar menyimpang jauh dari sebaran utama. Dengan menandai outlier secara eksplisit, kita sekarang memiliki fleksibilitas untuk memutuskan apakah data ini akan dipertahankan, dihapus, atau ditransformasi dalam analisis selanjutnya.
Nilai 500 juta tidak otomatis dihapus. Pertimbangkan tiga kemungkinan:
Pada modul ini, nilai dipertahankan dan dibuat variabel versi winsorized hanya untuk membandingkan dampak metode.
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 500000000 5750000
Kode menggunakan dua fungsi utama untuk winsorizing
(membatasi nilai ekstrem). pmax()
berfungsi membandingkan setiap nilai pendapatan dengan batas bawah
(4.150.000) dan mengambil yang lebih besar, sehingga nilai di bawah
batas bawah dinaikkan ke batas tersebut.
pmin() kemudian membandingkan hasilnya
dengan batas atas (5.750.000) dan mengambil yang lebih kecil, sehingga
nilai di atas batas atas diturunkan ke batas tersebut. Kombinasi
keduanya menghasilkan nilai yang “diapit” di antara batas bawah dan
atas. Pada output, outlier C007 dengan nilai
500.000.000 diubah menjadi 5.750.000 (batas atas), sementara data lain
tetap.
Winsorizing adalah demonstrasi tambahan. Terapkan hanya jika sesuai dengan tujuan analisis dan dokumentasikan bahwa nilai ekstrem telah dibatasi.
Rumus untuk rentang \([0,1]\) adalah
\[ 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_imputasi", "usia_minmax",
"pendapatan_imputasi", "pendapatan_minmax")]
## customer_id usia_imputasi usia_minmax pendapatan_imputasi pendapatan_minmax
## 1 C001 21.0 0.00000000 4500000 0.0000000000
## 2 C002 25.0 0.13793103 4900000 0.0008072654
## 3 C003 23.0 0.06896552 5200000 0.0014127144
## 4 C004 50.0 1.00000000 4800000 0.0006054490
## 5 C005 27.0 0.20689655 4900000 0.0008072654
## 6 C006 27.5 0.22413793 5100000 0.0012108981
## 7 C007 31.0 0.34482759 500000000 1.0000000000
## 8 C008 29.0 0.27586207 4700000 0.0004036327
## 9 C009 22.0 0.03448276 4600000 0.0002018163
## 10 C010 35.0 0.48275862 5300000 0.0016145308
## 11 C011 28.0 0.24137931 4900000 0.0008072654
Kode ini menerapkan normalisasi min-maks (min-max
normalization) pada variabel
usia_imputasi dan
pendapatan_imputasi untuk menskalakan
nilai-nilai ke rentang [0,1] menggunakan rumus \(x' = \frac{x - \min(x)}{\max(x) -
\min(x)}\).
Hasilnya, pada kolom usia_minmax, usia
terendah 21 tahun menjadi 0, usia tertinggi 50 tahun menjadi 1, dan
nilai lainnya menyebar proporsional di antaranya. Sementara pada
pendapatan_minmax, nilai terkecil
4.500.000 menjadi 0 dan nilai terbesar 500.000.000 (outlier
C007) menjadi 1, sedangkan sebagian besar pendapatan lainnya hanya
berada di kisaran 0 hingga 0,0016 karena rentang data sangat lebar
akibat outlier. Normalisasi ini berguna untuk menyamakan skala
antarvariabel yang berbeda satuan, misalnya sebelum menggunakan
algoritma berbasis jarak seperti KNN atau SVM. Namun, perlu diperhatikan
bahwa outlier ekstrem seperti 500 juta membuat sebagian besar
nilai pendapatan terkonsentrasi di dekat 0, sehingga informasi variasi
antar pelanggan ritel menjadi kurang terlihat.
Rumus z-score: \[ 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)
## usia_z pendapatan_z
## 1 -0.984 -0.304
## 2 -0.489 -0.301
## 3 -0.737 -0.299
## 4 2.604 -0.302
## 5 -0.242 -0.301
## 6 -0.180 -0.300
## 7 0.253 3.015
## 8 0.006 -0.303
## 9 -0.860 -0.303
## 10 0.748 -0.299
## 11 -0.118 -0.301
Metode ini menerapkan normalisasi z-score pada variabel
usia_imputasi dan
pendapatan_imputasi menggunakan fungsi
bawaan scale(), yang secara otomatis
menghitung z-score dengan rumus \(z = \frac{x
- \bar{x}}{s}\). Fungsi
as.numeric() digunakan untuk mengubah
hasil menjadi vektor numerik biasa. Dari output yang dibulatkan hingga 3
desimal, terlihat bahwa nilai usia_z
berkisar antara -0,984 hingga 2,604, yang berarti usia pelanggan C004
(50 tahun) berada 2,6 standar deviasi di atas rata-rata usia, sehingga
tergolong cukup tinggi tetapi masih dalam batas wajar.
Sementara pada pendapatan_z, hampir
semua pelanggan ritel memiliki nilai sekitar -0,3 (berarti sedikit di
bawah rata-rata), sedangkan pelanggan C007 dengan pendapatan 500 juta
memiliki z-score 3,015, yang berarti nilainya berada 3 standar deviasi
di atas rata-rata dan secara statistik tergolong outlier
ekstrem. Normalisasi z-score ini sangat berguna untuk menyetarakan skala
antarvariabel yang berbeda satuan, serta memudahkan identifikasi
outlier secara kuantitatif karena nilai z-score menunjukkan
seberapa jauh suatu nilai menyimpang dari rata-rata dalam satuan standar
deviasi.
Decimal scaling adalah teknik normalisasi yang membagi setiap nilai dengan pangkat 10 tertentu, sehingga semua nilai berada dalam rentang antara -1 dan 1 (atau 0 dan 1 jika semua data positif).
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
Cara kerja fungsi decimal_scale()
adalah sebagai berikut:
max(abs(x)));j) dengan rumus
ceiling(log10(maks + 1)), yaitu jumlah
digit dari nilai terbesar;Dalam kasus ini, nilai maksimum pendapatan imputasi adalah
500.000.000, sehingga log10(500.000.001) ≈
8,7, dibulatkan ke atas menjadi 9, lalu semua nilai dibagi
dengan \(10^9\) atau 1.000.000.000.
Hasilnya, rentang nilai menjadi antara 0,0045 (untuk pendapatan 4,5
juta) hingga 0,5000 (untuk 500 juta).
Kelebihan decimal scaling adalah mudah diimplementasikan dan tetap mempertahankan proporsi relatif antarnilai, tetapi kelemahannya adalah jika ada outlier ekstrem, sebagian besar data akan terkonsentrasi di dekat 0. Teknik ini cocok digunakan ketika data sudah dalam skala yang relatif seragam dan tidak terlalu dipengaruhi oleh outlier.
transformasi <- pelanggan[, c(
"customer_id", "pendapatan_imputasi", "pendapatan_minmax",
"pendapatan_z", "pendapatan_decimal"
)]
transformasi
## customer_id pendapatan_imputasi pendapatan_minmax pendapatan_z
## 1 C001 4500000 0.0000000000 -0.3041235
## 2 C002 4900000 0.0008072654 -0.3014440
## 3 C003 5200000 0.0014127144 -0.2994343
## 4 C004 4800000 0.0006054490 -0.3021138
## 5 C005 4900000 0.0008072654 -0.3014440
## 6 C006 5100000 0.0012108981 -0.3001042
## 7 C007 500000000 1.0000000000 3.0151095
## 8 C008 4700000 0.0004036327 -0.3027837
## 9 C009 4600000 0.0002018163 -0.3034536
## 10 C010 5300000 0.0016145308 -0.2987645
## 11 C011 4900000 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
Tabel ini membandingkan hasil tiga metode normalisasi pada variabel
pendapatan: min-max,
z-score, dan
decimal scaling. Dari data
terlihat bahwa metode min-max menghasilkan nilai antara 0 dan 1, di mana
pendapatan terkecil (4,5 juta) menjadi 0 dan pendapatan terbesar (500
juta) menjadi 1, namun karena outlier 500 juta, hampir semua
pendapatan ritel lainnya hanya berada di kisaran 0,0002 hingga 0,0016,
sehingga perbedaan di antara mereka nyaris tidak terlihat.
Metode z-score menghasilkan nilai dengan rata-rata 0 dan standar deviasi 1, di mana hampir semua pelanggan ritel memiliki z-score sekitar -0,3 (berarti sedikit di bawah rata-rata), sementara pelanggan C007 dengan pendapatan 500 juta memiliki z-score yang sangat tinggi sekitar 3,015, sehingga outlier ini sangat menonjol secara statistik.
Metode decimal scaling membagi semua nilai dengan 1.000.000.000, menghasilkan rentang 0,0045 hingga 0,5000, di mana outlier 500 juta menjadi 0,5000 sementara pelanggan ritel lainnya hanya berkisar antara 0,0045 hingga 0,0053, lagi-lagi membuat perbedaan antar pelanggan ritel menjadi sulit dibedakan karena skala yang didominasi oleh outlier.
Dari ketiga metode, z-score adalah metode yang paling informatif karena secara eksplisit menunjukkan seberapa jauh setiap nilai menyimpang dari rata-rata dalam satuan standar deviasi, sehingga memudahkan identifikasi outlier dan perbandingan antar pelanggan secara relatif, sementara min-maks dan decimal scaling sangat terpengaruh oleh outlier ekstrem sehingga informasi variasi di antara sebagian besar data menjadi hilang.
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)
Grafik ini membandingkan hasil normalisasi min-maks antara data asli
(sumbu X) dan data yang sudah diwinsorize (sumbu Y). Dari
sebaran titik terlihat bahwa pada data asli, hampir semua nilai
terkonsentrasi di dekat 0 (kiri bawah), karena adanya satu
outlier ekstrem yang mendominasi rentang data. Setelah data
di-winsorize (outlier dibatasi ke batas atas), nilai-nilai
min-maks menjadi lebih tersebar dan membentuk pola yang lebih linier,
terlihat dari titik-titik yang menyebar ke arah kanan atas. Garis
putus-putus merah menunjukkan tren bahwa winsorizing membuat
distribusi nilai normalisasi menjadi lebih merata.
Outlier ekstrem dapat menekan sebagian besar nilai min-maks ke rentang yang sangat sempit (mendekati 0), sehingga perbedaan antar pelanggan ritel menjadi tidak terlihat. Inilah sebabnya mengapa deteksi dan penanganan outlier sangat penting dilakukan sebelum memilih teknik transformasi atau normalisasi. Tanpa penanganan, informasi berharga dari sebagian besar data bisa hilang hanya karena satu nilai yang sangat menyimpang.
sum(duplicated(pelanggan$customer_id))
## [1] 0
sum(duplicated(transaksi_raw$cust_id))
## [1] 0
Pada bagian Integrasi Data ini, langkah pertama adalah memeriksa
kunci pada kedua sumber data untuk memastikan tidak ada duplikasi dan
mengetahui kesesuaian antar tabel. Dari hasil
sum(duplicated()), terlihat bahwa tidak
ada duplikasi ID pelanggan di kedua sumber, baik pada
pelanggan$customer_id maupun
transaksi_raw$cust_id, yang berarti
masing-masing ID bersifat unik.
setdiff(pelanggan$customer_id, transaksi_raw$cust_id)
## [1] "C011"
setdiff(transaksi_raw$cust_id, pelanggan$customer_id)
## [1] "C012"
Namun, fungsi setdiff() mengungkapkan
adanya ketidaksesuaian: ID “C011” ada di data pelanggan tetapi tidak ada
di data transaksi, artinya pelanggan tersebut tidak memiliki riwayat
transaksi apapun. Sebaliknya, ID “C012” ada di data transaksi tetapi
tidak ada di data pelanggan, artinya ada transaksi yang tercatat tetapi
tidak memiliki data profil pelanggan yang bersesuaian. Temuan ini
penting karena menunjukkan bahwa kedua sumber data tidak sepenuhnya
tumpang tindih, sehingga perlu diputuskan apakah akan menggabungkan
secara inner join (hanya yang beririsan), left join
(mempertahankan semua pelanggan), atau right join
(mempertahankan semua transaksi), tergantung pada tujuan analisis.
Pemeriksaan ini juga menjadi dasar untuk membersihkan atau melengkapi
data sebelum integrasi dilakukan. Intinya, hasil
setdiff() menunjukkan pelanggan yang tidak
memiliki transaksi dan transaksi yang tidak memiliki pasangan data
pelanggan.
transaksi <- transaksi_raw
names(transaksi)[names(transaksi) == "cust_id"] <- "customer_id"
Pada Tahap ini, dilakukan penyelarasan nama identifier agar
kedua sumber data memiliki nama kolom kunci yang sama. Data transaksi
yang semula bernama transaksi_raw disalin
ke objek baru bernama transaksi untuk
menjaga agar data mentah tetap aman. Kemudian, fungsi
names(transaksi)[names(transaksi) == "cust_id"] <- "customer_id"
digunakan untuk mengganti nama kolom
cust_id menjadi
customer_id. Dengan demikian, kolom kunci
pada tabel transaksi kini memiliki nama yang sama dengan tabel pelanggan
(customer_id), sehingga memudahkan proses
penggabungan (join) antar tabel. Penyelarasan nama ini
merupakan langkah penting dalam integrasi data untuk menghindari
kebingungan dan kesalahan saat melakukan operasi gabungan.
merge()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 1500000
## 2 C002 Budi 3 900000
## 3 C003 Citra 7 2700000
## 4 C004 Dodi 2 600000
## 5 C005 Eka 6 2100000
## 6 C006 Fani 4 1300000
## 7 C007 Gilang 20 25000000
## 8 C008 Hana 5 1700000
## 9 C009 Indra 3 800000
## 10 C010 Joko 8 3200000
## 11 C011 Kiki NA NA
Pada langkah ini, dilakukan penggabungan (join) antara tabel
pelanggan dan
transaksi menggunakan fungsi
merge() dengan metode left join
(all.x = TRUE). Parameter
by = "customer_id" menunjukkan bahwa kedua
tabel digabung berdasarkan kolom
customer_id yang sudah diselaraskan
namanya sebelumnya. Left join berarti semua baris dari tabel
pelanggan akan dipertahankan, dan kolom-kolom dari tabel transaksi akan
ditambahkan jika ada kecocokan ID. jika tidak ada transaksi untuk suatu
pelanggan, maka kolom transaksi akan bernilai
NA.Hasilnya disimpan dalam objek
data_terintegrasi.
Dari output yang ditampilkan, terlihat bahwa ada 11 baris data
pelanggan, dan hanya 10 baris yang memiliki data transaksi, karena
pelanggan dengan ID C011 yang tidak memiliki data transaksi. Data
terintegrasi ini sekarang memuat informasi pelanggan beserta jumlah
transaksi dan total pembelian, sehingga siap untuk analisis lanjutan
seperti segmentasi pelanggan berdasarkan perilaku belanja. Perintah
all.x = TRUE mempertahankan seluruh
pelanggan meskipun tidak memiliki pasangan transaksi.
Setelah proses integrasi data selesai, langkah selanjutnya adalah melakukan validasi untuk memastikan bahwa penggabungan berjalan dengan benar dan tidak menimbulkan masalah. Validasi ini mencakup pemeriksaan jumlah baris, keunikan kunci, serta kemunculan nilai hilang baru akibat proses penggabungan.
# Apakah jumlah baris berubah secara tidak wajar?
c(sebelum = nrow(pelanggan), sesudah = nrow(data_terintegrasi))
## sebelum sesudah
## 11 11
jumlah baris data diperiksa dengan membandingkan sebelum dan sesudah integrasi. Hasilnya menunjukkan bahwa jumlah baris tetap yaitu 11 baris, baik pada tabel pelanggan asli maupun pada data terintegrasi. Hal ini sesuai dengan yang diharapkan karena metode yang digunakan adalah left join, yang mempertahankan semua baris dari tabel pelanggan meskipun ada pelanggan yang tidak memiliki transaksi.
# Apakah customer_id tetap unik?
sum(duplicated(data_terintegrasi$customer_id))
## [1] 0
Kemudian, dilakukan pemeriksaan terhadap kemungkinan duplikasi pada
kolom kunci customer_id. Hasilnya
menunjukkan nilai 0, artinya tidak ada ID pelanggan yang terduplikasi
dalam data terintegrasi. Dengan demikian, struktur kunci tetap unik dan
tidak terjadi penggandaan data akibat proses penggabungan.
# Missing value baru akibat tidak ditemukan pasangan transaksi
colSums(is.na(data_terintegrasi[, c("jumlah_transaksi", "total_purchase")]))
## jumlah_transaksi total_purchase
## 1 1
Selanjutnya, dilakukan pengecekan terhadap nilai hilang (NA) yang
baru muncul pada kolom hasil integrasi, yaitu
jumlah_transaksi dan
total_purchase. Dari hasil, terlihat bahwa
masing-masing kolom memiliki 1 nilai NA. Nilai hilang ini muncul secara
wajar karena pelanggan dengan ID C011 (bernama Kiki) tidak memiliki
riwayat transaksi sama sekali, sehingga kolom transaksinya otomatis
kosong ketika digabungkan.
# Pelanggan tanpa pasangan transaksi
data_terintegrasi[is.na(data_terintegrasi$jumlah_transaksi),
c("customer_id", "nama")]
## customer_id nama
## 11 C011 Kiki
Untuk memastikan hasil tersebut, dilakukan penyaringan terhadap baris
yang memiliki nilai kosong (NA) pada
jumlah_transaksi. Hasilnya mengonfirmasi
bahwa hanya pelanggan C011 bernama Kiki yang tidak memiliki pasangan
data transaksi. Validasi ini sangat penting untuk memastikan bahwa
proses integrasi tidak menimbulkan masalah seperti duplikasi, perubahan
jumlah baris yang tidak wajar, atau nilai hilang yang tidak terduga,
serta memberikan pemahaman yang jelas tentang karakteristik data hasil
penggabungan.
NA pada transaksi dapat berarti dua hal berbeda:
Nilai hanya boleh diubah menjadi 0 jika definisinya sudah dikonfirmasi.
# 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
Setelah validasi integrasi selesai, langkah berikutnya adalah
memutuskan bagaimana menangani nilai NA
yang muncul pada kolom transaksi, yaitu
jumlah_transaksi dan
total_purchase. Nilai
NA ini memerlukan perhatian khusus karena
dapat memiliki dua makna yang sangat berbeda, yaitu pelanggan tersebut
belum pernah bertransaksi sama sekali atau data transaksinya tidak
tersedia karena gagal dipindahkan atau direkam. Perbedaan makna ini
sangat krusial karena akan mempengaruhi analisis selanjutnya, seperti
perhitungan rata-rata pembelian atau segmentasi pelanggan.
Oleh karena itu, keputusan untuk mengubah
NA menjadi 0 hanya boleh dilakukan jika
kita sudah mengonfirmasi definisi dari nilai hilang tersebut. Dalam
contoh ini, diasumsikan bahwa setelah dilakukan verifikasi, disimpulkan
bahwa NA memang berarti pelanggan belum
pernah bertransaksi. Berdasarkan asumsi ini, maka pengisian nol menjadi
tindakan yang tepat.
Proses pengisian dilakukan dengan membuat kolom baru bernama
jumlah_transaksi_final dan
total_purchase_final sebagai salinan dari
kolom asli. Kemudian, pada kolom baru tersebut, semua nilai
NA diganti dengan 0 menggunakan indeks
logika is.na(). Hasilnya, pelanggan C011
yang sebelumnya memiliki NA pada
transaksi, kini memiliki nilai 0 pada kedua kolom transaksi, yang secara
jelas mengindikasikan bahwa ia tidak memiliki riwayat pembelian.
Pendekatan ini mencerminkan langkah yang baik dalam pengolahan data, yaitu tidak mengubah nilai asli secara permanen (dengan membuat kolom baru) dan tidak mengubah nilai tanpa dasar yang jelas. Dengan demikian, jika di kemudian hari ternyata asumsi tersebut salah, kita masih memiliki data asli yang utuh untuk diperbaiki.
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 4500000
## 2 C002 Budi 25.0 Pekanbaru Aktif 4900000
## 3 C003 Citra 23.0 Pekanbaru Aktif 5200000
## 4 C004 Dodi 50.0 Dumai Aktif 4800000
## 5 C005 Eka 27.0 Pekanbaru Tidak Aktif 4900000
## 6 C006 Fani 27.5 Dumai Tidak Aktif 5100000
## 7 C007 Gilang 31.0 Pekanbaru Aktif 500000000
## 8 C008 Hana 29.0 Siak Aktif 4700000
## 9 C009 Indra 22.0 Pekanbaru Aktif 4600000
## 10 C010 Joko 35.0 Dumai Tidak Aktif 5300000
## 11 C011 Kiki 28.0 Tidak diketahui Aktif 4900000
## 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 1500000
## 2 3 900000
## 3 7 2700000
## 4 2 600000
## 5 6 2100000
## 6 4 1300000
## 7 20 25000000
## 8 5 1700000
## 9 3 800000
## 10 8 3200000
## 11 0 0
Pada tahap akhir, dilakukan pemilihan atribut yang akan digunakan
dalam dataset final. Dari data terintegrasi, dipilih 12 kolom yang
paling relevan, meliputi identitas pelanggan
(customer_id,
nama), hasil imputasi
(usia_imputasi,
kota_imputasi,
pendapatan_imputasi), status pelanggan,
indikator data hilang
(pendapatan_missing), penanda
outlier (outlier_pendapatan),
hasil normalisasi (usia_minmax,
pendapatan_minmax), serta kolom transaksi
final (jumlah_transaksi_final,
total_purchase_final).
Setelah itu, dilakukan penyederhanaan nama kolom agar lebih ringkas.
kota_imputasi diubah menjadi
kota, dan
usia_imputasi menjadi
usia. Perubahan ini dilakukan karena
proses imputasi sudah selesai, sehingga tidak perlu lagi membedakan
kolom asli dan hasil imputasi dalam dataset final.
Dataset final memiliki 11 baris dan 12 kolom, tanpa nilai hilang.
Semua atribut sudah dalam format siap pakai, termasuk kolom hasil
transformasi dan indikator yang memberikan fleksibilitas untuk analisis
lanjutan. Dengan demikian, data_final siap
digunakan untuk segmentasi pelanggan, pemodelan prediktif, atau laporan
bisnis.
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
Audit akhir dilakukan untuk memastikan kualitas dataset final.
Hasilnya menunjukkan tidak ada nilai hilang pada seluruh atribut, yang
berarti proses imputasi dan pengisian nol telah berhasil membersihkan
semua NA. Selain itu, kolom
jumlah_unik memberikan informasi tentang
variasi nilai pada setiap atribut. Misalnya,
customer_id dan
nama memiliki 11 nilai unik (sesuai dengan
jumlah pelanggan), kota memiliki 4 nilai
unik (Dumai, Pekanbaru, Siak, Tidak Diketahui), dan
status memiliki 2 nilai unik (Aktif/Tidak
Aktif). Untuk kolom numerik,
pendapatan_imputasi memiliki 9 nilai unik
karena ada dua pelanggan dengan pendapatan yang sama setelah imputasi,
sedangkan usia memiliki 11 nilai unik
karena tidak ada duplikasi usia. Audit ini menegaskan bahwa data final
sudah bersih, konsisten, dan siap digunakan untuk analisis lanjutan.
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
Pada tahap akhir verifikasi, dilakukan dua pemeriksaan kualitas
utama. Pertama, pengecekan duplikasi pada
customer_id menghasilkan nilai 0, yang
berarti tidak ada ID pelanggan yang terduplikasi dan setiap baris
mewakili pelanggan unik. Kedua, pengecekan nilai hilang menggunakan
colSums(is.na()) menunjukkan bahwa semua
kolom bernilai 0, artinya tidak ada satupun nilai kosong yang tersisa di
seluruh atribut dataset final, termasuk kolom hasil imputasi,
normalisasi, dan transaksi.
Dengan terpenuhinya kedua kriteria tersebut tanpa duplikasi dan tanpa nilai hilang, dataset final dinyatakan valid dan siap digunakan untuk analisis lanjutan tanpa perlu khawatir akan adanya masalah kualitas data yang mendasar.
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
Tabel perbandingan ini menunjukkan perubahan kondisi data sebelum dan sesudah ditangani secara ringkas dan terukur. Sebelum diproses, data awal memiliki 12 baris, namun setelah dibersihkan dan diintegrasikan, jumlah baris menjadi 11 karena satu baris duplikat dihapus. Hal ini terbukti dari indikator duplikasi yang turun dari 1 menjadi 0. Total missing value juga berhasil dihilangkan seluruhnya, dari sebelumnya ada 4 nilai kosong menjadi 0 setelah imputasi dan pengisian nol.
Perubahan paling mencolok terlihat pada kategori unik
kota dan
status.
Kota unik menyusut drastis dari 10 menjadi
4, karena data kota asli memiliki banyak variasi penulisan (misalnya
“Pekanbaru”, “pekanbaru”, “PEKANBARU”) yang setelah dibersihkan
distandarisasi menjadi hanya 4 kategori: Dumai, Pekanbaru, Siak, dan
Tidak Diketahui. Demikian pula status unik
berkurang dari 8 menjadi 2, setelah dilakukan standarisasi nilai status
menjadi hanya “Aktif” dan “Tidak Aktif”.
Secara keseluruhan, perbandingan ini membuktikan bahwa proses pembersihan dan transformasi data berhasil meningkatkan kualitas data secara signifikan. proses ini membuat data dengan duplikasi dapat dihilangkan, missing value dapat ditangani, dan nilai-nilai kategorik distandarisasi sehingga dataset menjadi lebih bersih, konsisten, dan siap untuk analisis lanjutan.
write.csv(data_final,
"data_pelanggan_setelah_preprocessing.csv",
row.names = FALSE)
write.csv(log_perubahan,
"log_perubahan_preprocessing.csv",
row.names = FALSE)
Alur yang telah dilakukan adalah:
Preprocessing merupakan proses pengambilan keputusan. Kode R hanya menjalankan keputusan tersebut; kualitas hasil tetap bergantung pada pemahaman data, tujuan analisis, dan dokumentasi perubahan.
Han, J., Kamber, M., & Pei, J. (2012). Data Mining: Concepts and Techniques (3rd ed.). Morgan Kaufmann. Chapter 3: Data Preprocessing.