install.packages(c(“readxl”,“dplyr”,“tidyr”,“stringr”,“lubridate”,“ggplot2”,“knitr”,“rmarkdown”,“car”,“fastDummies”))

Letakkan data_sampel.xlsx di folder yang sama dengan file .Rmd ini, lalu klik Knit.

Pertemuan 01: Pengantar Eksplorasi Data

Memahami data

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.

Memeriksa kualitas

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

Menentukan tindakan

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

Tahapan preprocessing

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

Data wrangling

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

Pertemuan 02: Identifikasi dan Cleaning Data

Metadata (sesuai PPT Kelompok 4)

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

Data dictionary dan key

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

Identifikasi variabel dan ekstraksi

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

Pemeriksaan kualitas

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

Strategi cleaning

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)

Proses cleaning

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

Validasi hasil cleaning

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

Pertemuan 03: Pemeriksaan Karakteristik Data

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.

Ringkasan numerik

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

Histogram, density plot, dan boxplot

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

Normalitas

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.

Variasi antar kelompok dan homogenitas

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.

Pertemuan 04: Integrasi dan Validasi Data

Key dan tabel referensi

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

Row binding (menambah observasi)

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

Join (menambah informasi)

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

Integrasi yang dipakai

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>

Validasi hasil integrasi

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

Pertemuan 05: Transformasi Data Kategorik I

Encoding mengubah representasi kategori, bukan informasi yang mendasarinya. Angka hasil encoding adalah kode, bukan nilai kuantitatif.

Label encoding

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

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

One-hot encoding

# 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")
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")
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.

Validasi hasil encoding

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

Pertemuan 06: Transformasi Data Kategorik II

Alur: identifikasi tipe kategori, tentukan karakteristiknya, pilih teknik, terapkan, periksa hasil, dan evaluasi kesesuaiannya. Studi kasus Titanic pada materi diterapkan ke data cafe.

Memilih teknik per variabel

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

Tahap 1: menyiapkan data analisis

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

Tahap 2: menerapkan encoding

# 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().

Tahap 3: membandingkan representasi

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

Perbandingan dan dampak

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.

Keputusan berdasarkan kebutuhan analisis

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.

Pertemuan 07: Transformasi Data Numerik

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.

Pemeriksaan rentang

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

Normalisasi Min-Max dan standardisasi Z-Score

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

Transformasi fungsi invers

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

Perbandingan teknik

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

Mean dan SD sebelum dan sesudah transformasi

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

Kesimpulan

  1. Cleaning (P01-P02). Dari 1.000 transaksi sampel terdapat 521 nilai bermasalah pada Item, Quantity, dan Location (kosong, ERROR, UNKNOWN). Semuanya ditangani dengan aturan yang jelas sehingga sisa data bermasalah menjadi 0, dan data asli tetap utuh.
  2. Karakteristik (P03). Quantity bernilai bulat sehingga distribusinya berbentuk tangga. Penyebaran antar kelompok diperiksa dengan boxplot dan uji Levene.
  3. Integrasi (P04). Data digabung dengan tabel kode Item dan Location memakai left_join. Jumlah baris tetap, tidak ada duplikasi, dan tidak ada transaksi tanpa pasangan.
  4. Encoding (P05-P06). Item dan Location diubah dengan label dan one-hot encoding, sedangkan kelas total belanja dengan ordinal encoding. Pemilihan teknik didasarkan pada makna kategori.
  5. Transformasi numerik (P07). Min-Max dan Z-Score menyamakan skala Quantity, Price, dan Total, sedangkan fungsi invers mengubah bentuk distribusi.
# 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)