R Programming • Data Analysis • Data Preprocessing
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.
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.
Terdapat dua dataset yang digunakan dalam project ini.
Data pertama berisi informasi dasar pelanggan, sedangkan data kedua berisi aktivitas transaksi pelanggan.
| Variabel | Keterangan |
|---|---|
customer_id |
ID pelanggan |
nama |
Nama pelanggan |
usia |
Usia pelanggan |
pendapatan |
Pendapatan pelanggan |
kota |
Kota tempat tinggal |
status |
Status pelanggan |
| Variabel | Keterangan |
|---|---|
cust_id |
ID pelanggan |
jumlah_transaksi |
Jumlah transaksi |
total_purchase |
Total pembelian |
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
)
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
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"
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.
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.
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"
)
| 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.
Setelah mendapatkan gambaran awal melalui audit, saya melakukan pemeriksaan kualitas secara lebih spesifik.
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"
)
| 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.
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.
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"
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
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.
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.
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.
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
)
)
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
)
)
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
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.
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.
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.
Setelah data dibersihkan, saya memeriksa kemungkinan adanya outlier pada pendapatan.
Metode yang digunakan adalah Interquartile Range (IQR).
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.
Setelah proses cleaning, saya mencoba beberapa metode transformasi.
Metode yang digunakan:
Ketiga metode tersebut digunakan untuk melihat bagaimana satu variabel dapat direpresentasikan dalam skala yang berbeda.
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"
)
| 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 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"
)
| 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 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.
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"
)
| 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:
Pada data ini, nilai C007 tetap terlihat ekstrem setelah transformasi.
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.
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.
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.
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"
)
| 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
Setelah dataset akhir terbentuk, audit kembali dilakukan menggunakan fungsi yang sama.
audit_akhir <- audit_data(
data_final
)
kable(
audit_akhir,
caption = "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 |
sum(
duplicated(
data_final$customer_id
)
)
## [1] 0
Hasilnya adalah 0 sehingga seluruh customer_id pada dataset akhir tetap unik.
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"
)
| 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 |
write.csv(
data_final,
"data_final_pelanggan.csv",
row.names = FALSE
)
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.
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.
Dari project ini, saya mendapatkan beberapa pengalaman dalam menggunakan R untuk pengolahan data.
Beberapa hal yang saya pelajari antara lain:
dplyr untuk proses cleaning,left_join() untuk menggabungkan
dataset,Project ini juga membantu saya memahami bahwa kualitas data sangat berpengaruh terhadap proses analisis berikutnya.
Mengenali masalah pada data, memperbaikinya dengan tepat, dan memahami informasi yang dihasilkan.
Nia Andini
Statistika • Data Analysis • R
Programming