install.packages(c(“readxl”,“dplyr”,“tidyr”,“stringr”,“lubridate”,“ggplot2”,“knitr”,“rmarkdown”,“car”,“fastDummies”))
Letakkan
data_sampel.xlsxdi folder yang sama dengan file.Rmdini, lalu klik Knit.
Pertanyaan awal: apa unit observasinya, apa variabelnya, apa tipenya, bagaimana rentang nilainya, adakah nilai kosong dan nilai tidak masuk akal?
# Data dibaca apa adanya (semua teks) dan tidak akan diubah
data_mentah <- read_excel("data_sampel.xlsx", col_types = "text")
dim(data_mentah)
## [1] 1000 8
glimpse(data_mentah)
## Rows: 1,000
## Columns: 8
## $ Transaction.ID <chr> "TXN_2176024", "TXN_6327139", "TXN_5488764", "TXN_953…
## $ Item <chr> "Coffee", "ERROR", "Coffee", "Salad", "Sandwich", "Sm…
## $ Quantity <chr> "5", "4", "4", "1", "5", "3", "3", "1", "3", "1", "ER…
## $ Price.Per.Unit <chr> "ERROR", "4.0", "2.0", "5.0", "4.0", "4.0", "5.0", "5…
## $ Total.Spent <chr> "10.0", "16.0", "8.0", "5.0", "20.0", "12.0", "15.0",…
## $ Payment.Method <chr> "Digital Wallet", NA, "Digital Wallet", "Digital Wall…
## $ Location <chr> "In-store", "Takeaway", "Takeaway", NA, NA, "In-store…
## $ Transaction.Date <chr> "44960", "ERROR", "45107", "45162", "45017", "45084",…
head(data_mentah)
## # A tibble: 6 × 8
## Transaction.ID Item Quantity Price.Per.Unit Total.Spent Payment.Method
## <chr> <chr> <chr> <chr> <chr> <chr>
## 1 TXN_2176024 Coffee 5 ERROR 10.0 Digital Wallet
## 2 TXN_6327139 ERROR 4 4.0 16.0 <NA>
## 3 TXN_5488764 Coffee 4 2.0 8.0 Digital Wallet
## 4 TXN_9530003 Salad 1 5.0 5.0 Digital Wallet
## 5 TXN_3753993 Sandwich 5 4.0 20.0 Credit Card
## 6 TXN_2251128 Smoothie 3 4.0 12.0 Credit Card
## # ℹ 2 more variables: Location <chr>, Transaction.Date <chr>
Unit observasi adalah satu transaksi. Data sampel berisi 1000 baris dan 8 variabel, diambil dari 10% data asli (10.000 transaksi). Semua kolom terbaca sebagai teks, jadi angka dan tanggal perlu dikonversi.
ringkas_masalah <- data_mentah %>%
pivot_longer(everything(), names_to = "variabel", values_to = "nilai") %>%
group_by(variabel) %>%
summarise(kosong = sum(is.na(nilai)),
ERROR = sum(nilai == "ERROR", na.rm = TRUE),
UNKNOWN = sum(nilai == "UNKNOWN", na.rm = TRUE), .groups = "drop") %>%
mutate(total_masalah = kosong + ERROR + UNKNOWN,
persen = round(100 * total_masalah / nrow(data_mentah), 1)) %>%
arrange(desc(total_masalah))
kable(ringkas_masalah, caption = "Masalah per variabel (data mentah)")
| variabel | kosong | ERROR | UNKNOWN | total_masalah | persen |
|---|---|---|---|---|---|
| Location | 322 | 37 | 35 | 394 | 39.4 |
| Payment.Method | 255 | 29 | 22 | 306 | 30.6 |
| Item | 28 | 29 | 26 | 83 | 8.3 |
| Total.Spent | 20 | 12 | 19 | 51 | 5.1 |
| Price.Per.Unit | 20 | 14 | 16 | 50 | 5.0 |
| Quantity | 15 | 14 | 15 | 44 | 4.4 |
| Transaction.Date | 11 | 11 | 14 | 36 | 3.6 |
| Transaction.ID | 0 | 0 | 0 | 0 | 0.0 |
sum(duplicated(data_mentah)) # baris duplikat
## [1] 0
sum(duplicated(data_mentah$Transaction.ID)) # ID duplikat
## [1] 0
Menemukan masalah tidak sama dengan langsung memperbaikinya. Pilihan tindakan: correct, remove, impute, transform, retain.
tribble(
~masalah, ~tindakan, ~alasan,
"ERROR / UNKNOWN / kosong", "Correct", "Bukan nilai sah, dijadikan NA dulu",
"Item kosong", "Impute", "Dipulihkan dari harga per unit",
"Quantity kosong", "Impute", "Dihitung dari Total / Harga",
"Location kosong", "Impute", "Diisi dengan modus (kategori terbanyak)",
"Angka & tanggal terbaca teks", "Transform", "Diubah ke numerik dan Date",
"Item 'cookie' vs 'Cookie'", "Correct", "Standardisasi huruf kapital"
) %>% kable()
| masalah | tindakan | alasan |
|---|---|---|
| ERROR / UNKNOWN / kosong | Correct | Bukan nilai sah, dijadikan NA dulu |
| Item kosong | Impute | Dipulihkan dari harga per unit |
| Quantity kosong | Impute | Dihitung dari Total / Harga |
| Location kosong | Impute | Diisi dengan modus (kategori terbanyak) |
| Angka & tanggal terbaca teks | Transform | Diubah ke numerik dan Date |
| Item ‘cookie’ vs ‘Cookie’ | Correct | Standardisasi huruf kapital |
tribble(
~jenis, ~dikerjakan_pada, ~isi,
"Cleaning", "Pertemuan 02", "Placeholder -> NA, imputasi Item/Quantity/Location",
"Transformation", "Pertemuan 05 - 07", "Encoding kategorik dan transformasi numerik",
"Integration", "Pertemuan 04", "Row binding dan join dengan tabel kode",
"Reduction", "Pertemuan 02", "Mengambil variabel yang relevan saja"
) %>% kable()
| jenis | dikerjakan_pada | isi |
|---|---|---|
| Cleaning | Pertemuan 02 | Placeholder -> NA, imputasi Item/Quantity/Location |
| Transformation | Pertemuan 05 - 07 | Encoding kategorik dan transformasi numerik |
| Integration | Pertemuan 04 | Row binding dan join dengan tabel kode |
| Reduction | Pertemuan 02 | Mengambil variabel yang relevan saja |
Operasi wrangling dicoba pada data yang sudah diubah ke tipe yang
benar (data_tipe), yaitu placeholder menjadi
NA dan angka/tanggal dikonversi. Belum ada imputasi di
tahap ini.
data_tipe <- data_mentah %>%
mutate(across(everything(), ~ na_if(str_trim(.x), ""))) %>%
mutate(across(everything(), ~ if_else(.x %in% placeholder, NA_character_, .x))) %>%
mutate(across(c(Quantity, Price.Per.Unit, Total.Spent), as.numeric),
Transaction.Date = parse_tanggal(Transaction.Date))
# SELECT: memilih variabel
data_tipe %>% select(Transaction.ID, Item, Total.Spent) %>% head()
## # A tibble: 6 × 3
## Transaction.ID Item Total.Spent
## <chr> <chr> <dbl>
## 1 TXN_2176024 Coffee 10
## 2 TXN_6327139 <NA> 16
## 3 TXN_5488764 Coffee 8
## 4 TXN_9530003 Salad 5
## 5 TXN_3753993 Sandwich 20
## 6 TXN_2251128 Smoothie 12
# FILTER: memilih observasi
data_tipe %>% filter(Item == "Coffee", Location == "Takeaway") %>% head()
## # A tibble: 6 × 8
## Transaction.ID Item Quantity Price.Per.Unit Total.Spent Payment.Method
## <chr> <chr> <dbl> <dbl> <dbl> <chr>
## 1 TXN_5488764 Coffee 4 2 8 Digital Wallet
## 2 TXN_7153455 Coffee 4 2 8 Digital Wallet
## 3 TXN_7833800 Coffee 2 2 4 <NA>
## 4 TXN_2849719 Coffee 5 2 10 Digital Wallet
## 5 TXN_5763254 Coffee 3 NA 6 Digital Wallet
## 6 TXN_8153957 Coffee 1 2 2 Digital Wallet
## # ℹ 2 more variables: Location <chr>, Transaction.Date <date>
# TRANSFORM: mengubah bentuk/nilai
data_tipe %>% mutate(Item = str_to_title(Item), Quantity = as.integer(Quantity)) %>%
select(Item, Quantity) %>% head()
## # A tibble: 6 × 2
## Item Quantity
## <chr> <int>
## 1 Coffee 5
## 2 <NA> 4
## 3 Coffee 4
## 4 Salad 1
## 5 Sandwich 5
## 6 Smoothie 3
# CREATE: membuat variabel baru
data_tipe %>%
mutate(Bulan = month(Transaction.Date, label = TRUE, abbr = FALSE),
Total_hitung = Quantity * Price.Per.Unit) %>%
select(Transaction.Date, Bulan, Quantity, Price.Per.Unit, Total_hitung) %>% head()
## # A tibble: 6 × 5
## Transaction.Date Bulan Quantity Price.Per.Unit Total_hitung
## <date> <ord> <dbl> <dbl> <dbl>
## 1 2023-02-03 February 5 NA NA
## 2 NA <NA> 4 4 16
## 3 2023-06-30 June 4 2 8
## 4 2023-08-24 August 1 5 5
## 5 2023-04-01 April 5 4 20
## 6 2023-06-07 June 3 4 12
# JOIN: menggabungkan dataset
kategori_menu <- tribble(
~Item, ~Kategori,
"Coffee", "Minuman", "Tea", "Minuman", "Juice", "Minuman", "Smoothie", "Minuman",
"Cake", "Makanan", "Cookie", "Makanan", "Salad", "Makanan", "Sandwich", "Makanan"
)
data_tipe %>% mutate(Item = str_to_title(Item)) %>%
left_join(kategori_menu, by = "Item") %>%
select(Transaction.ID, Item, Kategori) %>% head()
## # A tibble: 6 × 3
## Transaction.ID Item Kategori
## <chr> <chr> <chr>
## 1 TXN_2176024 Coffee Minuman
## 2 TXN_6327139 <NA> <NA>
## 3 TXN_5488764 Coffee Minuman
## 4 TXN_9530003 Salad Makanan
## 5 TXN_3753993 Sandwich Makanan
## 6 TXN_2251128 Smoothie Minuman
# RESHAPE: mengubah struktur data (long -> wide)
data_tipe %>% mutate(Item = str_to_title(Item)) %>%
filter(!is.na(Item), !is.na(Location)) %>%
count(Item, Location) %>%
pivot_wider(names_from = Location, values_from = n, values_fill = 0)
## # A tibble: 8 × 3
## Item `In-store` Takeaway
## <chr> <int> <int>
## 1 Cake 28 42
## 2 Coffee 24 38
## 3 Cookie 32 40
## 4 Juice 33 31
## 5 Salad 38 28
## 6 Sandwich 41 43
## 7 Smoothie 29 34
## 8 Tea 50 29
Setelah wrangling dilakukan Explore Again: periksa jumlah baris, hasil filter dan join, serta apakah variabel baru masuk akal.
c(data_mentah = nrow(data_mentah), data_tipe = nrow(data_tipe))
## data_mentah data_tipe
## 1000 1000
tribble(
~aspek, ~keterangan,
"What", "Data transaksi penjualan kafe (Cafe Sales - Dirty Data)",
"Why", "Latihan membersihkan data: missing value, nilai tidak valid, dan kekurangan lainnya",
"Who", "Diterbitkan oleh Ahmad Mohamed",
"How", "Data diambil dari Kaggle, lalu dianalisis",
"When", "Diterbitkan tahun 2023",
"Where", "Cafe",
"Unit", "Satu baris = satu transaksi penjualan",
"Format", "CSV (Kaggle); file sampel di sini berformat .xlsx"
) %>% kable()
| aspek | keterangan |
|---|---|
| What | Data transaksi penjualan kafe (Cafe Sales - Dirty Data) |
| Why | Latihan membersihkan data: missing value, nilai tidak valid, dan kekurangan lainnya |
| Who | Diterbitkan oleh Ahmad Mohamed |
| How | Data diambil dari Kaggle, lalu dianalisis |
| When | Diterbitkan tahun 2023 |
| Where | Cafe |
| Unit | Satu baris = satu transaksi penjualan |
| Format | CSV (Kaggle); file sampel di sini berformat .xlsx |
tribble(
~variabel, ~deskripsi, ~tipe_seharusnya,
"Transaction.ID", "ID transaksi (key)", "String",
"Item", "Menu yang dibeli", "Kategorik (nominal)",
"Quantity", "Jumlah item", "Numerik/diskrit",
"Price.Per.Unit", "Harga per item", "Numerik/kontinu",
"Total.Spent", "Total belanja", "Numerik/kontinu",
"Payment.Method", "Metode pembayaran", "Kategorik (nominal)",
"Location", "In-store atau Takeaway", "Kategorik (nominal)",
"Transaction.Date", "Tanggal transaksi", "Tanggal"
) %>% kable()
| variabel | deskripsi | tipe_seharusnya |
|---|---|---|
| Transaction.ID | ID transaksi (key) | String |
| Item | Menu yang dibeli | Kategorik (nominal) |
| Quantity | Jumlah item | Numerik/diskrit |
| Price.Per.Unit | Harga per item | Numerik/kontinu |
| Total.Spent | Total belanja | Numerik/kontinu |
| Payment.Method | Metode pembayaran | Kategorik (nominal) |
| Location | In-store atau Takeaway | Kategorik (nominal) |
| Transaction.Date | Tanggal transaksi | Tanggal |
n_distinct(data_mentah$Transaction.ID) == nrow(data_mentah) # key harus unik
## [1] TRUE
Variabel yang dibersihkan pada PPT Kelompok 4 adalah Item,
Quantity, dan Location. Price.Per.Unit dan Total.Spent dipakai
sebagai alat bantu pemulihan nilai, sedangkan Payment.Method tidak
dipakai. Data asli (data_mentah) tidak diubah.
variabel_fokus <- c("Item", "Quantity", "Location")
masalah <- data_mentah %>%
select(all_of(variabel_fokus)) %>%
pivot_longer(everything(), names_to = "variabel", values_to = "nilai") %>%
group_by(variabel) %>%
summarise(data_kosong = sum(is.na(nilai)),
data_error = sum(nilai == "ERROR", na.rm = TRUE),
data_unknown = sum(nilai == "UNKNOWN", na.rm = TRUE), .groups = "drop") %>%
mutate(total = data_kosong + data_error + data_unknown) %>%
arrange(match(variabel, variabel_fokus))
bind_rows(masalah, summarise(masalah, variabel = "TOTAL", across(-variabel, sum))) %>%
kable(caption = "Jumlah nilai bermasalah pada 3 variabel fokus")
| variabel | data_kosong | data_error | data_unknown | total |
|---|---|---|---|---|
| Item | 28 | 29 | 26 | 83 |
| Quantity | 15 | 14 | 15 | 44 |
| Location | 322 | 37 | 35 | 394 |
| TOTAL | 365 | 80 | 76 | 521 |
Total 521 nilai (sel) bermasalah. Perlu diingat ini jumlah sel, bukan jumlah transaksi.
# Jumlah BARIS yang punya minimal satu masalah pada 3 variabel fokus
sum(apply(data_mentah[variabel_fokus], 1, function(r) any(na_atau_ph(r))))
## [1] 464
cek <- data_tipe
tribble(
~dimensi, ~pemeriksaan, ~hasil,
"Completeness", "Sel kosong/ERROR/UNKNOWN (3 variabel fokus)", sum(masalah$total),
"Validity", "Angka <= 0", sum(cek$Quantity <= 0 | cek$Price.Per.Unit <= 0 |
cek$Total.Spent <= 0, na.rm = TRUE),
"Consistency", "Total <> Quantity x Harga (baris lengkap)", sum(abs(cek$Quantity * cek$Price.Per.Unit -
cek$Total.Spent) > 1e-6, na.rm = TRUE),
"Uniqueness", "Transaction.ID duplikat", sum(duplicated(cek$Transaction.ID)),
"Timeliness", "Tanggal tidak valid", sum(is.na(cek$Transaction.Date))
) %>% mutate(hasil = as.character(hasil)) %>% kable(caption = "Pemeriksaan per dimensi kualitas")
| dimensi | pemeriksaan | hasil |
|---|---|---|
| Completeness | Sel kosong/ERROR/UNKNOWN (3 variabel fokus) | 521 |
| Validity | Angka <= 0 | 0 |
| Consistency | Total <> Quantity x Harga (baris lengkap) | 0 |
| Uniqueness | Transaction.ID duplikat | 0 |
| Timeliness | Tanggal tidak valid | 36 |
| Variabel | Masalah | Cara perbaikan |
|---|---|---|
| Item | kosong, ERROR, UNKNOWN | Dipulihkan dari harga per unit (jika satu harga dimiliki dua item, dipilih yang paling sering muncul) |
| Quantity | kosong, ERROR, UNKNOWN | Dihitung dari Total.Spent / Price.Per.Unit |
| Location | kosong, ERROR, UNKNOWN | Diisi modus (kategori terbanyak) |
# 1. Standardisasi: placeholder -> NA (sudah di data_tipe), huruf kapital pada Item
bersih_dasar <- data_tipe %>% mutate(Item = str_to_title(Item))
# 2. Tabel bantu dari data yang valid
harga_item <- bersih_dasar %>%
filter(!is.na(Item), !is.na(Price.Per.Unit)) %>%
distinct(Item, Price.Per.Unit) %>%
select(Item, harga_dari_item = Price.Per.Unit)
item_dari_harga <- bersih_dasar %>%
filter(!is.na(Item), !is.na(Price.Per.Unit)) %>%
count(Price.Per.Unit, Item) %>%
group_by(Price.Per.Unit) %>%
slice_max(n, n = 1, with_ties = FALSE) %>%
ungroup() %>%
select(harga = Price.Per.Unit, item_dari_harga = Item)
# 3. Perbaikan Item, Quantity, Location (+ Price & Total sebagai variabel pendukung)
data_cleaning <- bersih_dasar %>%
left_join(harga_item, by = "Item") %>%
mutate(harga = coalesce(Price.Per.Unit, harga_dari_item, Total.Spent / Quantity)) %>%
left_join(item_dari_harga, by = "harga") %>%
mutate(
Item_valid = coalesce(Item, item_dari_harga, modus(Item)),
Quantity_valid = coalesce(Quantity, Total.Spent / harga),
Quantity_valid = coalesce(Quantity_valid, as.numeric(modus(Quantity))), # cadangan terakhir
Location_valid = coalesce(Location, modus(Location)),
Price_valid = coalesce(harga, harga_item$harga_dari_item[match(Item_valid, harga_item$Item)]),
Total_valid = coalesce(Total.Spent, Quantity_valid * Price_valid)
) %>%
select(Transaction.ID, Transaction.Date, Item_valid, Quantity_valid,
Location_valid, Price_valid, Total_valid)
kable(head(data_mentah[c("Transaction.ID", variabel_fokus)], 6), caption = "Data mentah")
| Transaction.ID | Item | Quantity | Location |
|---|---|---|---|
| TXN_2176024 | Coffee | 5 | In-store |
| TXN_6327139 | ERROR | 4 | Takeaway |
| TXN_5488764 | Coffee | 4 | Takeaway |
| TXN_9530003 | Salad | 1 | NA |
| TXN_3753993 | Sandwich | 5 | NA |
| TXN_2251128 | Smoothie | 3 | In-store |
kable(head(data_cleaning[c("Transaction.ID", "Item_valid", "Quantity_valid", "Location_valid")], 6),
caption = "Data setelah cleaning")
| Transaction.ID | Item_valid | Quantity_valid | Location_valid |
|---|---|---|---|
| TXN_2176024 | Coffee | 5 | In-store |
| TXN_6327139 | Smoothie | 4 | Takeaway |
| TXN_5488764 | Coffee | 4 | Takeaway |
| TXN_9530003 | Salad | 1 | Takeaway |
| TXN_3753993 | Sandwich | 5 | Takeaway |
| TXN_2251128 | Smoothie | 3 | In-store |
data.frame(
variabel = c("Item_valid", "Quantity_valid", "Location_valid", "Price_valid", "Total_valid"),
sisa_NA = c(sum(is.na(data_cleaning$Item_valid)), sum(is.na(data_cleaning$Quantity_valid)),
sum(is.na(data_cleaning$Location_valid)), sum(is.na(data_cleaning$Price_valid)),
sum(is.na(data_cleaning$Total_valid))),
sisa_error = c(sum(data_cleaning$Item_valid %in% placeholder), 0,
sum(data_cleaning$Location_valid %in% placeholder), 0, 0)
) %>% kable(caption = "Sisa data bermasalah setelah cleaning (0 = tuntas)")
| variabel | sisa_NA | sisa_error |
|---|---|---|
| Item_valid | 0 | 0 |
| Quantity_valid | 0 | 0 |
| Location_valid | 0 | 0 |
| Price_valid | 0 | 0 |
| Total_valid | 0 | 0 |
nrow(data_cleaning) == nrow(data_mentah) # jumlah baris tidak berubah
## [1] TRUE
count(data_cleaning, Item_valid)
## # A tibble: 8 × 2
## Item_valid n
## <chr> <int>
## 1 Cake 149
## 2 Coffee 118
## 3 Cookie 110
## 4 Juice 105
## 5 Salad 124
## 6 Sandwich 112
## 7 Smoothie 147
## 8 Tea 135
count(data_cleaning, Location_valid)
## # A tibble: 2 × 2
## Location_valid n
## <chr> <int>
## 1 In-store 300
## 2 Takeaway 700
Variabel tanggal tidak termasuk fokus cleaning, sehingga tanggal yang
tidak valid tetap NA.
Setelah data bersih, kita melihat kembali karakteristiknya: bentuk distribusi, normalitas, dan variasi antar kelompok. Variabel numerik fokus adalah Quantity (bulat 1 sampai 5), ditambah Total sebagai pembanding.
data_cleaning %>%
summarise(across(c(Quantity_valid, Total_valid),
list(rata2 = ~ mean(.x, na.rm = TRUE), median = ~ median(.x, na.rm = TRUE),
sd = ~ sd(.x, na.rm = TRUE), skewness = ~ skew(.x)))) %>%
pivot_longer(everything(), names_to = c("variabel", ".value"),
names_pattern = "(.*)_(rata2|median|sd|skewness)") %>%
kable(digits = 2)
| variabel | rata2 | median | sd | skewness |
|---|---|---|---|---|
| Quantity_valid | 3.08 | 3 | 1.44 | -0.06 |
| Total_valid | 9.16 | 8 | 6.06 | 0.72 |
ggplot(data_cleaning, aes(x = Quantity_valid)) +
geom_histogram(bins = 20, fill = "#8E44AD", color = "white", na.rm = TRUE) +
labs(title = "Histogram Quantity", x = "Quantity", y = "Frekuensi") + theme_minimal()
ggplot(data_cleaning, aes(x = Quantity_valid)) +
geom_density(fill = "#D2B4DE", na.rm = TRUE) +
labs(title = "Density Plot Quantity", x = "Quantity") + theme_minimal()
ggplot(data_cleaning, aes(y = Quantity_valid)) +
geom_boxplot(fill = "#F5B041", na.rm = TRUE) +
labs(title = "Boxplot Quantity", y = "Quantity") + theme_minimal()
ggplot(data_cleaning, aes(x = Total_valid)) +
geom_histogram(bins = 20, fill = "#5B2C6F", color = "white", na.rm = TRUE) +
labs(title = "Histogram Total", x = "Total", y = "Frekuensi") + theme_minimal()
ggplot(data_cleaning, aes(sample = Quantity_valid)) +
stat_qq(na.rm = TRUE) + stat_qq_line(color = "red", na.rm = TRUE) +
labs(title = "Q-Q Plot Quantity") + theme_minimal()
shapiro.test(na.omit(data_cleaning$Quantity_valid))
##
## Shapiro-Wilk normality test
##
## data: na.omit(data_cleaning$Quantity_valid)
## W = 0.88136, p-value < 2.2e-16
Titik yang mengikuti garis berarti mendekati distribusi acuan. Karena Quantity bernilai bulat 1 sampai 5, Q-Q plot berbentuk tangga. Data tidak normal bukan berarti data buruk, maknanya harus dikaitkan dengan tujuan analisis.
ggplot(data_cleaning, aes(x = Location_valid, y = Quantity_valid)) +
geom_boxplot(fill = "#D2B4DE", na.rm = TRUE) +
labs(title = "Quantity menurut Location", x = "Location", y = "Quantity") + theme_minimal()
ggplot(data_cleaning, aes(x = Item_valid, y = Total_valid)) +
geom_boxplot(fill = "#D2B4DE", na.rm = TRUE) +
labs(title = "Total menurut Item", x = "Item", y = "Total") +
theme_minimal() + theme(axis.text.x = element_text(angle = 45, hjust = 1))
data_lev <- data_cleaning %>% mutate(across(c(Item_valid, Location_valid), factor))
car::leveneTest(Quantity_valid ~ Location_valid, data = data_lev)
## Levene's Test for Homogeneity of Variance (center = median)
## Df F value Pr(>F)
## group 1 1.4378 0.2308
## 998
car::leveneTest(Total_valid ~ Item_valid, data = data_lev)
## Levene's Test for Homogeneity of Variance (center = median)
## Df F value Pr(>F)
## group 7 54.51 < 2.2e-16 ***
## 992
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Jika p-value kecil, penyebaran berbeda antar kelompok. Homogen tidak berarti lebih baik, hanya karakteristik yang relevan pada konteks tertentu.
Key penghubung adalah Item dan Location. Tabel kode dari PPT Kelompok 4 dipakai sebagai tabel referensi.
kode_item <- tribble(
~Item_valid, ~Item_code,
"Cake", 1, "Coffee", 2, "Cookie", 3, "Juice", 4,
"Salad", 5, "Sandwich", 6, "Smoothie", 7, "Tea", 8
)
kode_location <- tribble(~Location_valid, ~Location_code, "In-store", 1, "Takeaway", 2)
# Pemeriksaan key: format konsisten? (character(0) berarti semua key cocok)
setdiff(unique(data_cleaning$Item_valid), kode_item$Item_valid)
## character(0)
setdiff(unique(data_cleaning$Location_valid), kode_location$Location_valid)
## character(0)
# Key tabel referensi unik?
!any(duplicated(kode_item$Item_valid)) & !any(duplicated(kode_location$Location_valid))
## [1] TRUE
data_1 <- data_cleaning %>% slice(1:500)
data_2 <- data_cleaning %>% slice(501:n())
data_gabungan <- bind_rows(data_1, data_2)
c(data_1 = nrow(data_1), data_2 = nrow(data_2),
gabungan = nrow(data_gabungan), data_cleaning = nrow(data_cleaning))
## data_1 data_2 gabungan data_cleaning
## 500 500 1000 1000
Agar perbedaan jenis join terlihat, dibuat simulasi
tabel referensi: Tea dihapus dan Soup
ditambahkan (menu tanpa transaksi).
ref_simulasi <- kode_item %>% filter(Item_valid != "Tea") %>%
bind_rows(tibble(Item_valid = "Soup", Item_code = 9))
hasil_inner <- inner_join(data_cleaning, ref_simulasi, by = "Item_valid")
hasil_left <- left_join (data_cleaning, ref_simulasi, by = "Item_valid")
hasil_right <- right_join(data_cleaning, ref_simulasi, by = "Item_valid")
hasil_full <- full_join (data_cleaning, ref_simulasi, by = "Item_valid")
tibble(jenis = c("inner_join", "left_join", "right_join", "full_join"),
baris = c(nrow(hasil_inner), nrow(hasil_left), nrow(hasil_right), nrow(hasil_full)),
keterangan = c("Hanya yang cocok (transaksi Tea hilang)",
"Semua transaksi dipertahankan (Tea mendapat kode NA)",
"Semua baris referensi dipertahankan (Soup tanpa transaksi)",
"Semua baris dari kedua tabel")) %>%
kable(caption = "Jumlah baris menurut jenis join (simulasi)")
| jenis | baris | keterangan |
|---|---|---|
| inner_join | 865 | Hanya yang cocok (transaksi Tea hilang) |
| left_join | 1000 | Semua transaksi dipertahankan (Tea mendapat kode NA) |
| right_join | 866 | Semua baris referensi dipertahankan (Soup tanpa transaksi) |
| full_join | 1001 | Semua baris dari kedua tabel |
Masalah setelah join: key tidak cocok, duplikasi, missing value, dan many-to-many. Contoh duplikasi: jika tabel referensi memuat key ganda, jumlah baris bertambah.
ref_ganda <- bind_rows(kode_item, filter(kode_item, Item_valid == "Tea")) # Tea tercatat dua kali
hasil_ganda <- left_join(data_cleaning, ref_ganda, by = "Item_valid")
c(sebelum = nrow(data_cleaning), sesudah = nrow(hasil_ganda))
## sebelum sesudah
## 1000 1135
hasil_ganda %>% count(Transaction.ID) %>% filter(n > 1) %>% nrow() # transaksi yang tergandakan
## [1] 135
Dengan tabel referensi lengkap dipakai left_join agar semua transaksi tetap ada.
data_gabung <- data_cleaning %>%
left_join(kode_item, by = "Item_valid") %>%
left_join(kode_location, by = "Location_valid")
head(data_gabung)
## # A tibble: 6 × 9
## Transaction.ID Transaction.Date Item_valid Quantity_valid Location_valid
## <chr> <date> <chr> <dbl> <chr>
## 1 TXN_2176024 2023-02-03 Coffee 5 In-store
## 2 TXN_6327139 NA Smoothie 4 Takeaway
## 3 TXN_5488764 2023-06-30 Coffee 4 Takeaway
## 4 TXN_9530003 2023-08-24 Salad 1 Takeaway
## 5 TXN_3753993 2023-04-01 Sandwich 5 Takeaway
## 6 TXN_2251128 2023-06-07 Smoothie 3 In-store
## # ℹ 4 more variables: Price_valid <dbl>, Total_valid <dbl>, Item_code <dbl>,
## # Location_code <dbl>
nrow(data_cleaning); nrow(kode_item); nrow(data_gabung)
## [1] 1000
## [1] 8
## [1] 1000
dim(data_gabung)
## [1] 1000 9
colSums(is.na(data_gabung)) # missing setelah join
## Transaction.ID Transaction.Date Item_valid Quantity_valid
## 0 36 0 0
## Location_valid Price_valid Total_valid Item_code
## 0 0 0 0
## Location_code
## 0
data_gabung %>% count(Transaction.ID) %>% filter(n > 1) # duplikasi key (kosong = aman)
## # A tibble: 0 × 2
## # ℹ 2 variables: Transaction.ID <chr>, n <int>
anti_join(data_cleaning, kode_item, by = "Item_valid") # transaksi tanpa pasangan
## # A tibble: 0 × 7
## # ℹ 7 variables: Transaction.ID <chr>, Transaction.Date <date>,
## # Item_valid <chr>, Quantity_valid <dbl>, Location_valid <chr>,
## # Price_valid <dbl>, Total_valid <dbl>
anti_join(kode_item, data_cleaning, by = "Item_valid") # referensi tanpa transaksi
## # A tibble: 0 × 2
## # ℹ 2 variables: Item_valid <chr>, Item_code <dbl>
tribble(
~pemeriksaan, ~hasil,
"Jumlah observasi tidak berubah", nrow(data_gabung) == nrow(data_cleaning),
"Transaction.ID tetap unik", !any(duplicated(data_gabung$Transaction.ID)),
"Missing baru pada kode Item/Location", sum(is.na(data_gabung$Item_code)) + sum(is.na(data_gabung$Location_code)),
"Unmatched: transaksi tanpa pasangan", nrow(anti_join(data_cleaning, kode_item, by = "Item_valid")),
"Unmatched: referensi tanpa transaksi", nrow(anti_join(kode_item, data_cleaning, by = "Item_valid"))
) %>% mutate(hasil = as.character(hasil)) %>% kable(caption = "Checklist validasi integrasi")
| pemeriksaan | hasil |
|---|---|
| Jumlah observasi tidak berubah | 1 |
| Transaction.ID tetap unik | 1 |
| Missing baru pada kode Item/Location | 0 |
| Unmatched: transaksi tanpa pasangan | 0 |
| Unmatched: referensi tanpa transaksi | 0 |
JOIN berhasil belum berarti integrasi berhasil, hasilnya harus selalu divalidasi.
Encoding mengubah representasi kategori, bukan informasi yang mendasarinya. Angka hasil encoding adalah kode, bukan nilai kuantitatif.
item_factor <- factor(data_cleaning$Item_valid)
levels(item_factor)
## [1] "Cake" "Coffee" "Cookie" "Juice" "Salad" "Sandwich" "Smoothie"
## [8] "Tea"
Item_code_label <- as.numeric(item_factor)
loc_factor <- factor(data_cleaning$Location_valid)
levels(loc_factor)
## [1] "In-store" "Takeaway"
Location_code_label <- as.numeric(loc_factor)
# Tabel kode (sama seperti PPT Kelompok 4)
data.frame(Item = levels(item_factor), Code = seq_along(levels(item_factor))) %>%
kable(caption = "Label encoding Item")
| Item | Code |
|---|---|
| Cake | 1 |
| Coffee | 2 |
| Cookie | 3 |
| Juice | 4 |
| Salad | 5 |
| Sandwich | 6 |
| Smoothie | 7 |
| Tea | 8 |
data.frame(Location = levels(loc_factor), Code = seq_along(levels(loc_factor))) %>%
kable(caption = "Label encoding Location")
| Location | Code |
|---|---|
| In-store | 1 |
| Takeaway | 2 |
Item dan Location tidak memiliki urutan alami, sehingga kode 1, 2, 3 bukan tingkatan.
Ordinal encoding dipakai bila kategori punya urutan. Di data ini
urutan muncul pada kelas total belanja (Rendah <
Sedang < Tinggi), yang dibuat dari Total_valid.
data_enc <- data_cleaning %>%
mutate(kelas_total = case_when(Total_valid < 5 ~ "Rendah",
Total_valid < 15 ~ "Sedang",
Total_valid >= 15 ~ "Tinggi"))
# Cara 1: tentukan kode, lalu cocokkan
kelas_code <- c("Rendah" = 1, "Sedang" = 2, "Tinggi" = 3)
kode_manual <- unname(kelas_code[data_enc$kelas_total])
# Cara 2: factor dengan urutan (ordered = TRUE)
kelas_factor <- factor(data_enc$kelas_total, levels = c("Rendah", "Sedang", "Tinggi"), ordered = TRUE)
kelas_factor[1:5]
## [1] Sedang Tinggi Sedang Sedang Tinggi
## Levels: Rendah < Sedang < Tinggi
kelas_total_code <- as.numeric(kelas_factor)
all(kode_manual == kelas_total_code, na.rm = TRUE) # kedua cara harus sama
## [1] TRUE
data_enc$kelas_total_code <- kelas_total_code
data.frame(data_enc$kelas_total, kelas_total_code)[1:6, ]
## data_enc.kelas_total kelas_total_code
## 1 Sedang 2
## 2 Tinggi 3
## 3 Sedang 2
## 4 Sedang 2
## 5 Tinggi 3
## 6 Sedang 2
# Cara manual dengan ifelse (Location)
data.frame(
Location = data_cleaning$Location_valid,
In.store = ifelse(data_cleaning$Location_valid == "In-store", 1, 0),
Takeaway = ifelse(data_cleaning$Location_valid == "Takeaway", 1, 0)
) %>% head()
## Location In.store Takeaway
## 1 In-store 1 0
## 2 Takeaway 0 1
## 3 Takeaway 0 1
## 4 Takeaway 0 1
## 5 Takeaway 0 1
## 6 In-store 1 0
# Dengan model.matrix() (sama seperti PPT Kelompok 4)
onehot_location <- data.frame(model.matrix(~ Location_valid - 1, data = data_cleaning))
onehot_item <- data.frame(model.matrix(~ Item_valid - 1, data = data_cleaning))
kable(head(cbind(Location = data_cleaning$Location_valid, onehot_location)), caption = "One-hot Location")
| Location | Location_validIn.store | Location_validTakeaway |
|---|---|---|
| In-store | 1 | 0 |
| Takeaway | 0 | 1 |
| Takeaway | 0 | 1 |
| Takeaway | 0 | 1 |
| Takeaway | 0 | 1 |
| In-store | 1 | 0 |
kable(head(cbind(Item = data_cleaning$Item_valid, onehot_item)), caption = "One-hot Item")
| Item | Item_validCake | Item_validCoffee | Item_validCookie | Item_validJuice | Item_validSalad | Item_validSandwich | Item_validSmoothie | Item_validTea |
|---|---|---|---|---|---|---|---|---|
| Coffee | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| Smoothie | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
| Coffee | 0 | 1 | 0 | 0 | 0 | 0 | 0 | 0 |
| Salad | 0 | 0 | 0 | 0 | 1 | 0 | 0 | 0 |
| Sandwich | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 0 |
| Smoothie | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 0 |
One-hot encoding tidak memberi ranking, hanya menunjukkan keanggotaan kategori.
tribble(
~pemeriksaan, ~hasil,
"Semua kategori Item terwakili (kolom = kategori)", ncol(onehot_item) == n_distinct(data_cleaning$Item_valid),
"Semua kategori Location terwakili", ncol(onehot_location) == n_distinct(data_cleaning$Location_valid),
"Jumlah baris tetap", nrow(onehot_item) == nrow(data_cleaning),
"Tiap baris tepat satu '1' (Item)", all(rowSums(onehot_item) == 1),
"Tiap baris tepat satu '1' (Location)", all(rowSums(onehot_location) == 1),
"Kode label sama dengan tabel kode referensi", all(Item_code_label == data_gabung$Item_code) &
all(Location_code_label == data_gabung$Location_code)
) %>% kable(caption = "Validasi encoding (TRUE = lolos)")
| pemeriksaan | hasil |
|---|---|
| Semua kategori Item terwakili (kolom = kategori) | TRUE |
| Semua kategori Location terwakili | TRUE |
| Jumlah baris tetap | TRUE |
| Tiap baris tepat satu ‘1’ (Item) | TRUE |
| Tiap baris tepat satu ‘1’ (Location) | TRUE |
| Kode label sama dengan tabel kode referensi | TRUE |
Alur: identifikasi tipe kategori, tentukan karakteristiknya, pilih teknik, terapkan, periksa hasil, dan evaluasi kesesuaiannya. Studi kasus Titanic pada materi diterapkan ke data cafe.
tribble(
~variabel, ~karakteristik, ~teknik,
"Item_valid", "Nominal, 8 kategori", "One-hot",
"Location_valid", "Nominal, 2 kategori", "Binary 0/1 atau one-hot",
"kelas_total", "Ordinal (Rendah<Sedang<Tinggi)", "Ordinal",
"Quantity_valid", "Numerik", "Tetap numerik",
"Takeaway", "Target biner (contoh)", "0/1"
) %>% kable()
| variabel | karakteristik | teknik |
|---|---|---|
| Item_valid | Nominal, 8 kategori | One-hot |
| Location_valid | Nominal, 2 kategori | Binary 0/1 atau one-hot |
| kelas_total | Ordinal (Rendah<Sedang<Tinggi) | Ordinal |
| Quantity_valid | Numerik | Tetap numerik |
| Takeaway | Target biner (contoh) | 0/1 |
data_model <- data_enc %>%
mutate(Takeaway = as.numeric(Location_valid == "Takeaway")) %>% # target biner contoh
select(Takeaway, Item_valid, kelas_total_code, Quantity_valid, Total_valid) %>%
mutate(Item_valid = as.factor(Item_valid))
str(data_model)
## tibble [1,000 × 5] (S3: tbl_df/tbl/data.frame)
## $ Takeaway : num [1:1000] 0 1 1 1 1 0 1 1 0 1 ...
## $ Item_valid : Factor w/ 8 levels "Cake","Coffee",..: 2 7 2 5 6 7 5 5 4 2 ...
## $ kelas_total_code: num [1:1000] 2 3 2 2 3 2 3 2 2 1 ...
## $ Quantity_valid : num [1:1000] 5 4 4 1 5 3 3 1 3 1 ...
## $ Total_valid : num [1:1000] 10 16 8 5 20 12 15 5 9 2 ...
colSums(is.na(data_model))
## Takeaway Item_valid kelas_total_code Quantity_valid
## 0 0 0 0
## Total_valid
## 0
# One-hot dengan dummy_cols (fastDummies)
data_final <- dummy_cols(data_model,
select_columns = "Item_valid",
remove_first_dummy = FALSE,
remove_selected_columns = TRUE)
head(data_final)
## # A tibble: 6 × 12
## Takeaway kelas_total_code Quantity_valid Total_valid Item_valid_Cake
## <dbl> <dbl> <dbl> <dbl> <int>
## 1 0 2 5 10 0
## 2 1 3 4 16 0
## 3 1 2 4 8 0
## 4 1 2 1 5 0
## 5 1 3 5 20 0
## 6 0 2 3 12 0
## # ℹ 7 more variables: Item_valid_Coffee <int>, Item_valid_Cookie <int>,
## # Item_valid_Juice <int>, Item_valid_Salad <int>, Item_valid_Sandwich <int>,
## # Item_valid_Smoothie <int>, Item_valid_Tea <int>
# One-hot dengan model.matrix untuk satu variabel
head(model.matrix(~ Item_valid - 1, data = data_model))
## Item_validCake Item_validCoffee Item_validCookie Item_validJuice
## 1 0 1 0 0
## 2 0 0 0 0
## 3 0 1 0 0
## 4 0 0 0 0
## 5 0 0 0 0
## 6 0 0 0 0
## Item_validSalad Item_validSandwich Item_validSmoothie Item_validTea
## 1 0 0 0 0
## 2 0 0 1 0
## 3 0 0 0 0
## 4 1 0 0 0
## 5 0 1 0 0
## 6 0 0 1 0
Catatan: pada model.matrix(~ A + B - 1) hanya variabel
pertama yang mendapat semua kolom dummy, sedangkan variabel berikutnya
kehilangan satu level. Untuk one-hot penuh pada beberapa variabel,
gunakan dummy_cols().
dim(data_model) # ukuran data awal
## [1] 1000 5
dim(data_final) # ukuran setelah encoding
## [1] 1000 12
names(data_final)
## [1] "Takeaway" "kelas_total_code" "Quantity_valid"
## [4] "Total_valid" "Item_valid_Cake" "Item_valid_Coffee"
## [7] "Item_valid_Cookie" "Item_valid_Juice" "Item_valid_Salad"
## [10] "Item_valid_Sandwich" "Item_valid_Smoothie" "Item_valid_Tea"
nlevels(data_model$Item_valid) # kategori Item
## [1] 8
nlevels(factor(data_cleaning$Location_valid)) # kategori Location
## [1] 2
tribble(
~aspek, ~one_hot, ~ordinal,
"Representasi", "Beberapa kolom indikator", "Satu kolom kode",
"Urutan kategori", "Tidak diperlukan", "Diperlukan",
"Variabel nominal", "Sesuai", "Umumnya tidak sesuai",
"Jumlah kolom", "Bertambah", "Relatif tetap",
"Risiko", "Dimensi data bertambah", "Urutan semu pada nominal"
) %>% kable()
| aspek | one_hot | ordinal |
|---|---|---|
| Representasi | Beberapa kolom indikator | Satu kolom kode |
| Urutan kategori | Tidak diperlukan | Diperlukan |
| Variabel nominal | Sesuai | Umumnya tidak sesuai |
| Jumlah kolom | Bertambah | Relatif tetap |
| Risiko | Dimensi data bertambah | Urutan semu pada nominal |
# Dummy variable trap: semua dummy Item selalu berjumlah 1 (redundan bila ada intercept)
all(rowSums(select(data_final, starts_with("Item_valid_"))) == 1)
## [1] TRUE
# Solusi: buang satu kolom dummy (k-1)
data_regresi <- dummy_cols(data_model, select_columns = "Item_valid",
remove_first_dummy = TRUE, remove_selected_columns = TRUE)
c(one_hot_penuh = ncol(data_final), k_minus_1 = ncol(data_regresi))
## one_hot_penuh k_minus_1
## 12 11
Masalah yang dapat muncul: false ordering (kode Item dianggap tingkatan), dimensi meningkat pada kategori banyak, dan kategori baru yang tidak ada di data pelatihan.
tribble(
~skenario, ~keputusan,
"Tabel frekuensi Item per Location", "Tidak perlu encoding, pakai factor langsung",
"Grafik jumlah transaksi per Item", "Tidak perlu encoding numerik, factor sudah cukup",
"Model prediksi Takeaway (klasifikasi)","One-hot Item (k-1 bila ada intercept), ordinal kelas_total, Quantity numerik"
) %>% kable()
| skenario | keputusan |
|---|---|
| Tabel frekuensi Item per Location | Tidak perlu encoding, pakai factor langsung |
| Grafik jumlah transaksi per Item | Tidak perlu encoding numerik, factor sudah cukup |
| Model prediksi Takeaway (klasifikasi) | One-hot Item (k-1 bila ada intercept), ordinal kelas_total, Quantity numerik |
Encoding bukan pengganti penanganan missing value, jadi data harus sudah bersih sebelum encoding.
Variabel numerik pada data ini punya rentang berbeda: Quantity (1 sampai 5), Price (1 sampai 5), dan Total (1 sampai 25). Pada algoritma berbasis jarak (misalnya K-Means), variabel berskala besar akan mendominasi.
vars <- c("Quantity_valid", "Price_valid", "Total_valid")
data_cleaning %>%
summarise(across(all_of(vars), list(min = ~ min(.x, na.rm = TRUE), max = ~ max(.x, na.rm = TRUE),
mean = ~ mean(.x, na.rm = TRUE), sd = ~ sd(.x, na.rm = TRUE)))) %>%
pivot_longer(everything(), names_to = c("variabel", ".value"),
names_pattern = "(.*)_(min|max|mean|sd)") %>%
kable(digits = 2, caption = "Statistik awal sebelum transformasi")
| variabel | min | max | mean | sd |
|---|---|---|---|---|
| Quantity_valid | 1 | 5 | 3.08 | 1.44 |
| Price_valid | 1 | 5 | 2.97 | 1.27 |
| Total_valid | 1 | 25 | 9.16 | 6.06 |
ggplot(data_cleaning, aes(x = Total_valid)) +
geom_histogram(bins = 20, fill = "steelblue", color = "white", na.rm = TRUE) +
labs(title = "Distribusi Total Sebelum Transformasi", x = "Total", y = "Frekuensi") +
theme_minimal()
Min-Max: x’ = (x - min) / (max - min). Z-Score: z = (x - rata-rata) / simpangan baku.
data_transform <- data_cleaning %>%
mutate(across(all_of(vars),
list(MinMax = min_max, Z = ~ as.numeric(scale(.x))),
.names = "{.col}_{.fn}"))
# Verifikasi Z-Score: rata-rata mendekati 0 dan SD mendekati 1
mean(data_transform$Total_valid_Z, na.rm = TRUE)
## [1] -9.814625e-17
sd(data_transform$Total_valid_Z, na.rm = TRUE)
## [1] 1
range(data_transform$Total_valid_MinMax, na.rm = TRUE) # harus 0 sampai 1
## [1] 0 1
Fungsi invers tidak boleh dipakai pada nilai nol (hasilnya
Inf), jadi periksa dulu.
data_cleaning %>%
summarise(min_val = min(Total_valid, na.rm = TRUE),
jumlah_nol = sum(Total_valid == 0, na.rm = TRUE))
## # A tibble: 1 × 2
## min_val jumlah_nol
## <dbl> <int>
## 1 1 0
data_transform <- data_transform %>%
mutate(Total_InvSq = 1 / (Total_valid^2),
Total_InvCu = 1 / (Total_valid^3))
bandingkan <- data_transform %>%
select(Total_valid, Total_valid_MinMax, Total_valid_Z, Total_InvSq, Total_InvCu) %>%
pivot_longer(everything(), names_to = "versi", values_to = "nilai") %>%
mutate(versi = factor(versi, levels = c("Total_valid", "Total_valid_MinMax", "Total_valid_Z",
"Total_InvSq", "Total_InvCu")))
bandingkan %>%
group_by(versi) %>%
summarise(min = min(nilai, na.rm = TRUE), max = max(nilai, na.rm = TRUE),
mean = mean(nilai, na.rm = TRUE), sd = sd(nilai, na.rm = TRUE), .groups = "drop") %>%
kable(digits = 3, caption = "Statistik deskriptif antar teknik")
| versi | min | max | mean | sd |
|---|---|---|---|---|
| Total_valid | 1.000 | 25.000 | 9.156 | 6.063 |
| Total_valid_MinMax | 0.000 | 1.000 | 0.340 | 0.253 |
| Total_valid_Z | -1.345 | 2.613 | 0.000 | 1.000 |
| Total_InvSq | 0.002 | 1.000 | 0.074 | 0.172 |
| Total_InvCu | 0.000 | 1.000 | 0.045 | 0.164 |
ggplot(bandingkan, aes(x = nilai)) +
geom_histogram(bins = 20, fill = "#8E44AD", color = "white", na.rm = TRUE) +
facet_wrap(~ versi, scales = "free") +
labs(title = "Distribusi Total menurut Teknik Transformasi", x = "Nilai", y = "Frekuensi") +
theme_minimal()
Min-Max dan Z-Score hanya mengubah skala sehingga bentuk distribusi tetap, sedangkan fungsi invers mengubah bentuknya secara signifikan.
ringkas <- function(df) df %>%
pivot_longer(everything(), names_to = "variabel", values_to = "nilai") %>%
group_by(variabel) %>%
summarise(mean = mean(nilai, na.rm = TRUE), sd = sd(nilai, na.rm = TRUE), .groups = "drop")
bind_rows(
ringkas(select(data_transform, all_of(vars))),
ringkas(select(data_transform, ends_with("_MinMax"))),
ringkas(select(data_transform, ends_with("_Z")))
) %>% kable(digits = 3, caption = "Mean dan SD: asli, Min-Max, dan Z-Score")
| variabel | mean | sd |
|---|---|---|
| Price_valid | 2.966 | 1.272 |
| Quantity_valid | 3.075 | 1.437 |
| Total_valid | 9.156 | 6.063 |
| Price_valid_MinMax | 0.492 | 0.318 |
| Quantity_valid_MinMax | 0.519 | 0.359 |
| Total_valid_MinMax | 0.340 | 0.253 |
| Price_valid_Z | 0.000 | 1.000 |
| Quantity_valid_Z | 0.000 | 1.000 |
| Total_valid_Z | 0.000 | 1.000 |
Setelah Min-Max atau Z-Score, ketiga variabel berada pada skala sebanding sehingga tidak ada yang mendominasi perhitungan jarak. Jika terdapat outlier, Min-Max paling rentan karena bergantung pada nilai minimum dan maksimum, sedangkan Z-Score lebih stabil (periksa boxplot sebelum memilih).
left_join. Jumlah baris tetap,
tidak ada duplikasi, dan tidak ada transaksi tanpa pasangan.# Opsional: simpan hasil akhir
write.csv(data_gabung, "cafe_sales_terintegrasi.csv", row.names = FALSE)
write.csv(data_final, "cafe_sales_siap_analisis.csv", row.names = FALSE)