# ============================================================
# BAGIAN 1 — LOAD PACKAGE
# ============================================================
library(readxl)
## Warning: package 'readxl' was built under R version 4.3.3
library(ggplot2)
## Warning: package 'ggplot2' was built under R version 4.3.3
library(dplyr)
## Warning: package 'dplyr' was built under R version 4.3.3
##
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
##
## filter, lag
## The following objects are masked from 'package:base':
##
## intersect, setdiff, setequal, union
library(tidyr)
## Warning: package 'tidyr' was built under R version 4.3.3
library(lubridate)
## Warning: package 'lubridate' was built under R version 4.3.3
##
## Attaching package: 'lubridate'
## The following objects are masked from 'package:base':
##
## date, intersect, setdiff, union
library(RColorBrewer)
library(corrplot)
## Warning: package 'corrplot' was built under R version 4.3.3
## corrplot 0.95 loaded
library(scales)
# ============================================================
# BAGIAN 2 — IMPORT & BERSIHKAN DATA
# ============================================================
data <- read_excel(
"C:/Users/ASUS/OneDrive/Documents/CRMK3_PUAN KOPI GUNMAL/DATA PUAN KOPI GUNMAL_APRIL-SEPTEMBER 2025.xlsx",
sheet = "PEMASUKAN"
)
# Rename kolom agar lebih mudah diproses
colnames(data) <- c(
"Tanggal",
"Cup_Dingin", "Cup_Panas", "Air_Mineral",
"Espresso", "Kapiten",
"Cookies", "Fudgy_Brownies", "Fudgy_Brownies_Aya",
"Cireng", "Singkong", "Kentang", "Dimsum",
"Pentol", "Tahu_Bakso",
"Mie_Goreng", "Mie_Kuah", "Nasi_Goreng"
)
data$Tanggal <- as.Date(data$Tanggal)
produk_cols <- names(data)[-1]
for (col in produk_cols) {
data[[col]] <- suppressWarnings(as.numeric(as.character(data[[col]])))
}
data <- data %>% filter(!is.na(Tanggal))
# ── Penanganan Outlier ──────────────────────────────────────
# Espresso tanggal 2025-06-28 tercatat 48 (kemungkinan error input).
# Nilai diganti dengan median Espresso hari-hari lain.
espresso_median <- median(data$Espresso[data$Espresso < 10], na.rm = TRUE)
data$Espresso[data$Espresso >= 10] <- espresso_median
cat("Outlier Espresso (>= 10) diganti dengan median:", espresso_median, "\n")
## Outlier Espresso (>= 10) diganti dengan median: 1
cat("\nData dimuat:", nrow(data), "hari |",
format(min(data$Tanggal)), "s.d.", format(max(data$Tanggal)), "\n\n")
##
## Data dimuat: 183 hari | 2025-03-29 s.d. 2025-09-30
# ============================================================
# BAGIAN 3 — HARGA SATUAN (Rp) — dari sheet MENU PUAN KOPI
# ============================================================
# Cup Dingin/Panas = harga Tuan (kopi susu dasar) = Rp 15.000
# Espresso = Rp 5.000
# Kapiten = Kopi Soda/Kapiten = Rp 30.000
# Air Mineral = Rp 8.000
# Cookies = Rp 10.000
# Fudgy Brownies = Rp 22.000 | Fudgy Brownies Aya = Rp 20.000
# Cireng/Singkong = Rp 20.000 | Kentang = Rp 23.000
# Dimsum/Pentol = Rp 25.000 | Tahu Bakso = Rp 27.000
# Mie Goreng/Kuah = Rp 23.000 | Nasi Goreng = Rp 25.000
harga_satuan <- c(
Cup_Dingin = 15000,
Cup_Panas = 15000,
Air_Mineral = 8000,
Espresso = 5000,
Kapiten = 30000,
Cookies = 10000,
Fudgy_Brownies = 22000,
Fudgy_Brownies_Aya = 20000,
Cireng = 20000,
Singkong = 20000,
Kentang = 23000,
Dimsum = 25000,
Pentol = 25000,
Tahu_Bakso = 27000,
Mie_Goreng = 23000,
Mie_Kuah = 23000,
Nasi_Goreng = 25000
)
# ============================================================
# BAGIAN 4 — STATISTIK DESKRIPTIF
# ============================================================
stat_desc <- data %>%
select(-Tanggal) %>%
summarise(across(everything(), list(
Mean = ~round(mean(., na.rm = TRUE), 2),
Median = ~median(., na.rm = TRUE),
SD = ~round(sd(., na.rm = TRUE), 2),
Min = ~min(., na.rm = TRUE),
Max = ~max(., na.rm = TRUE),
Total = ~sum(., na.rm = TRUE)
))) %>%
pivot_longer(
everything(),
names_to = c("Produk", ".value"),
names_sep = "_(?=[^_]+$)"
) %>%
mutate(Produk = gsub("_", " ", Produk))
cat("=== STATISTIK DESKRIPTIF ===\n")
## === STATISTIK DESKRIPTIF ===
print(stat_desc)
## # A tibble: 17 × 7
## Produk Mean Median SD Min Max Total
## <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 Cup Dingin 150. 147 29.5 83 255 27380
## 2 Cup Panas 2.62 2 2.23 0 13 480
## 3 Air Mineral 6.96 6 4.13 0 21 1273
## 4 Espresso 1.27 1 1.36 0 7 231
## 5 Kapiten 0.37 0 0.68 0 4 67
## 6 Cookies 4.1 3 4.04 0 30 750
## 7 Fudgy Brownies 0.21 0 0.77 0 6 39
## 8 Fudgy Brownies Aya 0.96 0 1.6 0 6 175
## 9 Cireng 6.1 6 3.36 0 18 1117
## 10 Singkong 1.87 2 1.44 0 7 342
## 11 Kentang 4.09 4 2.39 0 11 748
## 12 Dimsum 1.49 1 1.42 0 7 272
## 13 Pentol 0.75 1 0.87 0 5 137
## 14 Tahu Bakso 1.47 1 1.29 0 8 269
## 15 Mie Goreng 1.23 1 1.41 0 8 225
## 16 Mie Kuah 1.17 1 1.54 0 9 215
## 17 Nasi Goreng 1.74 2 1.38 0 6 319
# ============================================================
# BAGIAN 5 — TOTAL PENJUALAN PER PRODUK & KATEGORI
# ============================================================
total_produk <- data %>%
select(-Tanggal) %>%
summarise(across(everything(), ~sum(., na.rm = TRUE))) %>%
pivot_longer(everything(), names_to = "Produk", values_to = "Total") %>%
mutate(
Produk = gsub("_", " ", Produk),
Kategori = case_when(
Produk %in% c("Cup Dingin", "Cup Panas",
"Espresso", "Kapiten", "Air Mineral") ~ "Minuman",
Produk %in% c("Cookies", "Fudgy Brownies",
"Fudgy Brownies Aya") ~ "Kue & Snack",
TRUE ~ "Makanan"
)
) %>%
arrange(desc(Total))
# ============================================================
# BAGIAN 6 — VISUALISASI PENJUALAN (VIZ 1–7)
# ============================================================
# VIZ 1: Total Penjualan per Produk
# Menampilkan semua 17 produk, sudah mencakup informasi "top produk"
# sehingga tidak perlu chart Top 10 terpisah.
ggplot(total_produk,
aes(x = reorder(Produk, Total), y = Total, fill = Kategori)) +
geom_col(width = 0.7) +
geom_text(aes(label = comma(Total)), hjust = -0.1, size = 3.2) +
coord_flip() +
scale_fill_manual(values = c("Kue & Snack" = "#6F3811",
"Makanan" = "#A0522D",
"Minuman" = "#C68642")) +
scale_y_continuous(labels = comma, expand = expansion(mult = c(0, 0.18))) +
labs(title = "Total Penjualan per Produk (Maret–September 2025)",
subtitle = "Puan Kopi Gunung Malang",
x = NULL, y = "Jumlah Terjual (Unit)", fill = "Kategori",
caption = "Gambar 1. Total penjualan tiap produk selama 183 hari pengamatan.") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold", color = "#3E1C00"),
plot.caption = element_text(hjust = 0, color = "#6F3811", size = 9))

# VIZ 2: Tren Penjualan Harian
data_harian <- data %>%
mutate(Total_Harian = rowSums(select(., -Tanggal), na.rm = TRUE))
ggplot(data_harian, aes(x = Tanggal, y = Total_Harian)) +
geom_line(color = "#A0522D", linewidth = 0.8) +
geom_smooth(method = "loess", color = "#3E1C00",
se = TRUE, fill = "#E8B97E", alpha = 0.3) +
scale_x_date(date_labels = "%b %Y", date_breaks = "1 month") +
scale_y_continuous(labels = comma) +
labs(title = "Tren Penjualan Harian — Total Semua Produk",
subtitle = "Puan Kopi Gunung Malang | Maret–September 2025",
x = NULL, y = "Total Unit Terjual",
caption = "Gambar 2. Garis tipis = penjualan harian; garis tebal = tren umum (LOESS).") +
theme_minimal(base_size = 11) +
theme(plot.title = element_text(face = "bold", color = "#3E1C00"),
plot.caption = element_text(hjust = 0, color = "#6F3811", size = 9))
## `geom_smooth()` using formula = 'y ~ x'

# VIZ 3: Tren Penjualan Minuman per Bulan
tren_bulanan <- data %>%
mutate(Bulan = floor_date(Tanggal, "month")) %>%
group_by(Bulan) %>%
summarise(across(Cup_Dingin:Kapiten, ~sum(., na.rm = TRUE)), .groups = "drop") %>%
pivot_longer(-Bulan, names_to = "Produk", values_to = "Total") %>%
mutate(Produk = gsub("_", " ", Produk))
ggplot(tren_bulanan,
aes(x = Bulan, y = Total, color = Produk, group = Produk)) +
geom_line(linewidth = 1.2) +
geom_point(size = 2.5) +
scale_color_manual(values = c("#3E1C00", "#7B3F00", "#C68642",
"#E8B97E", "#A0522D")) +
scale_x_date(date_labels = "%b %Y", date_breaks = "1 month") +
scale_y_continuous(labels = comma) +
labs(title = "Tren Penjualan Minuman per Bulan",
subtitle = "Puan Kopi Gunung Malang | Maret–September 2025",
x = NULL, y = "Jumlah Terjual", color = "Produk",
caption = "Gambar 3. Tren penjualan bulanan tiap varian minuman.") +
theme_minimal(base_size = 12) +
theme(plot.title = element_text(face = "bold", color = "#3E1C00"),
plot.caption = element_text(hjust = 0, color = "#6F3811", size = 9))

# VIZ 4: Heatmap Penjualan Bulanan per Produk
# Menampilkan pola bulan × produk sekaligus; lebih informatif dari bar chart
# kategori bulanan (VIZ 6 asli tetap dipertahankan karena perspektif berbeda).
heatmap_data <- data %>%
mutate(Bulan = format(Tanggal, "%b %Y")) %>%
group_by(Bulan) %>%
summarise(across(-Tanggal, ~sum(., na.rm = TRUE)), .groups = "drop") %>%
pivot_longer(-Bulan, names_to = "Produk", values_to = "Total") %>%
mutate(
Produk = gsub("_", " ", Produk),
Bulan = factor(Bulan, levels = c("Mar 2025", "Apr 2025", "May 2025",
"Jun 2025", "Jul 2025", "Aug 2025",
"Sep 2025"))
)
ggplot(heatmap_data, aes(x = Bulan, y = Produk, fill = Total)) +
geom_tile(color = "white", linewidth = 0.5) +
geom_text(aes(label = comma(Total)), size = 2.8, color = "white") +
scale_fill_gradient(low = "#FBE9D0", high = "#3E1C00", labels = comma) +
labs(title = "Heatmap Penjualan Bulanan per Produk",
subtitle = "Puan Kopi Gunung Malang | Maret–September 2025",
x = NULL, y = NULL, fill = "Total\nTerjual",
caption = "Gambar 4. Warna lebih gelap = penjualan lebih tinggi.") +
theme_minimal(base_size = 11) +
theme(plot.title = element_text(face = "bold", color = "#3E1C00"),
axis.text.x = element_text(angle = 30, hjust = 1),
plot.caption = element_text(hjust = 0, color = "#6F3811", size = 9))

# VIZ 5: Proporsi Penjualan per Kategori
total_kategori <- total_produk %>%
group_by(Kategori) %>%
summarise(Total = sum(Total), .groups = "drop") %>%
mutate(
Persen = round(Total / sum(Total) * 100, 1),
Label = paste0(Kategori, "\n", Persen, "%")
)
ggplot(total_kategori, aes(x = "", y = Total, fill = Kategori)) +
geom_col(width = 1) +
coord_polar(theta = "y") +
geom_text(aes(label = Label),
position = position_stack(vjust = 0.5),
size = 4, fontface = "bold", color = "white") +
scale_fill_manual(values = c("Kue & Snack" = "#6F3811",
"Makanan" = "#A0522D",
"Minuman" = "#C68642")) +
labs(title = "Proporsi Penjualan per Kategori",
subtitle = "Puan Kopi Gunung Malang | Maret–September 2025",
caption = "Gambar 5. Kontribusi tiap kategori terhadap total penjualan.") +
theme_void() +
theme(plot.title = element_text(face = "bold", hjust = 0.5, color = "#3E1C00"),
plot.subtitle = element_text(hjust = 0.5, color = "#6F3811"),
plot.caption = element_text(hjust = 0.5, color = "#6F3811", size = 9),
legend.position = "none")

# VIZ 6: Penjualan per Kategori per Bulan
bulanan_kategori <- data %>%
mutate(Bulan = format(Tanggal, "%b %Y")) %>%
group_by(Bulan) %>%
summarise(
Minuman = sum(Cup_Dingin + Cup_Panas + Espresso +
Kapiten + Air_Mineral, na.rm = TRUE),
Kue_Snack = sum(Cookies + Fudgy_Brownies +
Fudgy_Brownies_Aya, na.rm = TRUE),
Makanan = sum(Cireng + Singkong + Kentang + Dimsum +
Pentol + Tahu_Bakso + Mie_Goreng +
Mie_Kuah + Nasi_Goreng, na.rm = TRUE),
.groups = "drop"
) %>%
pivot_longer(-Bulan, names_to = "Kategori", values_to = "Total") %>%
mutate(Bulan = factor(Bulan, levels = c("Mar 2025", "Apr 2025", "May 2025",
"Jun 2025", "Jul 2025", "Aug 2025",
"Sep 2025")))
ggplot(bulanan_kategori,
aes(x = Bulan, y = Total, fill = Kategori)) +
geom_col(position = "dodge", width = 0.7) +
scale_fill_manual(values = c("Kue_Snack" = "#6F3811",
"Makanan" = "#A0522D",
"Minuman" = "#C68642")) +
scale_y_continuous(labels = comma) +
labs(title = "Total Penjualan per Kategori per Bulan",
subtitle = "Puan Kopi Gunung Malang | Maret–September 2025",
x = NULL, y = "Jumlah Terjual", fill = "Kategori",
caption = "Gambar 6. Perbandingan penjualan tiga kategori setiap bulan.") +
theme_minimal(base_size = 11) +
theme(plot.title = element_text(face = "bold", color = "#3E1C00"),
axis.text.x = element_text(angle = 30, hjust = 1),
plot.caption = element_text(hjust = 0, color = "#6F3811", size = 9))

# VIZ 7: Weekday vs Weekend
data_hari <- data %>%
mutate(
Hari = weekdays(Tanggal),
Jenis_Hari = ifelse(Hari %in% c("Saturday", "Sunday", "Sabtu", "Minggu"),
"Weekend", "Weekday")
) %>%
group_by(Jenis_Hari) %>%
summarise(across(-c(Tanggal, Hari), ~round(mean(., na.rm = TRUE), 2)),
.groups = "drop") %>%
pivot_longer(-Jenis_Hari, names_to = "Produk", values_to = "Rata2") %>%
mutate(Produk = gsub("_", " ", Produk))
ggplot(data_hari,
aes(x = reorder(Produk, Rata2), y = Rata2, fill = Jenis_Hari)) +
geom_col(position = "dodge", width = 0.7) +
coord_flip() +
scale_fill_manual(values = c("Weekday" = "#A0522D", "Weekend" = "#E8B97E")) +
scale_y_continuous(labels = comma) +
labs(title = "Rata-rata Penjualan Harian: Weekday vs Weekend",
subtitle = "Puan Kopi Gunung Malang | Maret–September 2025",
x = NULL, y = "Rata-rata Unit Terjual", fill = "Jenis Hari",
caption = "Gambar 7. Produk dengan nilai Weekend > Weekday cocok dipromosikan di akhir pekan.") +
theme_minimal(base_size = 11) +
theme(plot.title = element_text(face = "bold", color = "#3E1C00"),
legend.position = "bottom",
plot.caption = element_text(hjust = 0, color = "#6F3811", size = 9))

# ============================================================
# BAGIAN 7 — DATA BINER (FONDASI APRIORI)
# ============================================================
data_biner <- data %>%
select(-Tanggal) %>%
mutate(across(everything(), ~ifelse(is.na(.) | . == 0, 0L, 1L)))
data_matrix <- as.matrix(data_biner)
n_trans <- nrow(data_biner)
prod_names <- gsub("_", " ", colnames(data_biner))
# ============================================================
# BAGIAN 8 — THRESHOLD & SUPPORT TIAP PRODUK
# ============================================================
# Catatan support aktual dari data:
# Kapiten = 0.279 → TIDAK lolos (jarang dipesan)
# Fudgy Brownies = 0.087 → TIDAK lolos
# Fudgy Brownies Aya = 0.350 → TIDAK lolos
# Semua produk lain >= 0.530 → lolos
freq_item <- colSums(data_biner)
support_produk <- freq_item / n_trans
names(support_produk) <- prod_names
MIN_SUPPORT <- 0.50
MIN_CONFIDENCE <- 0.60
MIN_LIFT <- 1.0
cat("=== SUPPORT TIAP PRODUK ===\n")
## === SUPPORT TIAP PRODUK ===
print(round(sort(support_produk, decreasing = TRUE), 4))
## Cup Dingin Cireng Air Mineral Kentang
## 1.0000 0.9836 0.9727 0.9508
## Cup Panas Singkong Nasi Goreng Cookies
## 0.8525 0.8142 0.7869 0.7650
## Dimsum Tahu Bakso Espresso Mie Goreng
## 0.7650 0.7268 0.6721 0.6120
## Mie Kuah Pentol Fudgy Brownies Aya Kapiten
## 0.5355 0.5301 0.3497 0.2787
## Fudgy Brownies
## 0.0874
cat("\nParameter Apriori: Support >=", MIN_SUPPORT,
"| Confidence >=", MIN_CONFIDENCE, "| Lift >", MIN_LIFT, "\n")
##
## Parameter Apriori: Support >= 0.5 | Confidence >= 0.6 | Lift > 1
cat("Catatan: Kapiten (0.279), Fudgy Brownies (0.087), Fudgy Brownies Aya (0.350)",
"tidak memenuhi minimum support.\n\n")
## Catatan: Kapiten (0.279), Fudgy Brownies (0.087), Fudgy Brownies Aya (0.350) tidak memenuhi minimum support.
# ============================================================
# BAGIAN 9 — VIZ 8: FREKUENSI PRODUK
# ============================================================
# (Nomor mundur 1 karena VIZ 8 Top-10 asli dihapus — sudah tercakup VIZ 1)
freq_df <- data.frame(
Produk = prod_names,
Frekuensi = as.numeric(freq_item),
Support = round(support_produk, 3)
) %>%
arrange(desc(Frekuensi)) %>%
mutate(
Warna = ifelse(Support >= MIN_SUPPORT, "#A0522D", "#D2A679"),
Label_Support = paste0(Frekuensi, " hari (", round(Support * 100), "%)")
)
ggplot(freq_df, aes(x = reorder(Produk, Frekuensi),
y = Frekuensi, fill = Warna)) +
geom_col(width = 0.7) +
geom_text(aes(label = Label_Support), hjust = -0.05, size = 3, color = "#3E1C00") +
coord_flip() +
scale_fill_identity() +
scale_y_continuous(expand = expansion(mult = c(0, 0.22))) +
geom_hline(yintercept = round(MIN_SUPPORT * n_trans),
linetype = "dashed", color = "#6F3811", linewidth = 0.6) +
annotate("text", x = 1.5, y = round(MIN_SUPPORT * n_trans) + 2,
label = paste0("Min. support (50% = ", round(MIN_SUPPORT * n_trans), " hari)"),
color = "#6F3811", size = 2.8, hjust = 0) +
labs(title = "Frekuensi Kemunculan Tiap Produk",
subtitle = "Warna gelap = memenuhi minimum support (≥50% hari)",
x = NULL, y = "Frekuensi (Hari)",
caption = "Gambar 8. Produk di atas garis putus-putus masuk sebagai frequent item dalam Apriori.") +
theme_minimal(base_size = 11) +
theme(plot.title = element_text(face = "bold", color = "#3E1C00"),
plot.caption = element_text(hjust = 0, color = "#6F3811", size = 9))

# ============================================================
# BAGIAN 10 — FREQUENT 1-ITEMSETS
# ============================================================
freq_1 <- data.frame(
Itemset = prod_names,
Support = round(support_produk, 4),
Count = as.numeric(freq_item)
) %>%
filter(Support >= MIN_SUPPORT) %>%
arrange(desc(Support)) %>%
mutate(
No = row_number(),
Support_Pct = paste0(round(Support * 100, 1), "%"),
Interpretasi = paste0("Terjual ", Count, " dari ", n_trans, " hari")
) %>%
select(No, Itemset, Count, Support_Pct, Interpretasi)
cat("=== FREQUENT 1-ITEMSETS ===\n")
## === FREQUENT 1-ITEMSETS ===
print(freq_1)
## No Itemset Count Support_Pct Interpretasi
## Cup Dingin 1 Cup Dingin 183 100% Terjual 183 dari 183 hari
## Cireng 2 Cireng 180 98.4% Terjual 180 dari 183 hari
## Air Mineral 3 Air Mineral 178 97.3% Terjual 178 dari 183 hari
## Kentang 4 Kentang 174 95.1% Terjual 174 dari 183 hari
## Cup Panas 5 Cup Panas 156 85.2% Terjual 156 dari 183 hari
## Singkong 6 Singkong 149 81.4% Terjual 149 dari 183 hari
## Nasi Goreng 7 Nasi Goreng 144 78.7% Terjual 144 dari 183 hari
## Cookies 8 Cookies 140 76.5% Terjual 140 dari 183 hari
## Dimsum 9 Dimsum 140 76.5% Terjual 140 dari 183 hari
## Tahu Bakso 10 Tahu Bakso 133 72.7% Terjual 133 dari 183 hari
## Espresso 11 Espresso 123 67.2% Terjual 123 dari 183 hari
## Mie Goreng 12 Mie Goreng 112 61.2% Terjual 112 dari 183 hari
## Mie Kuah 13 Mie Kuah 98 53.5% Terjual 98 dari 183 hari
## Pentol 14 Pentol 97 53% Terjual 97 dari 183 hari
cat("Total:", nrow(freq_1), "produk memenuhi minimum support\n\n")
## Total: 14 produk memenuhi minimum support
# ============================================================
# BAGIAN 11 — ALGORITMA APRIORI
# ============================================================
cols <- colnames(data_biner)
n_cols <- length(cols)
rules_list <- list()
cat("Menjalankan Apriori...\n")
## Menjalankan Apriori...
for (i in 1:(n_cols - 1)) {
for (j in (i + 1):n_cols) {
sup_ij <- sum(data_biner[[i]] == 1 & data_biner[[j]] == 1) / n_trans
if (sup_ij < MIN_SUPPORT) next
sup_i <- sum(data_biner[[i]]) / n_trans
sup_j <- sum(data_biner[[j]]) / n_trans
# i -> j
conf_ij <- sup_ij / sup_i
lift_ij <- conf_ij / sup_j
if (conf_ij >= MIN_CONFIDENCE && lift_ij > MIN_LIFT) {
rules_list[[length(rules_list) + 1]] <- data.frame(
Antecedent = gsub("_", " ", cols[i]),
Consequent = gsub("_", " ", cols[j]),
Support = round(sup_ij, 3),
Confidence = round(conf_ij, 3),
Lift = round(lift_ij, 3),
Count = round(sup_ij * n_trans)
)
}
# j -> i
conf_ji <- sup_ij / sup_j
lift_ji <- conf_ji / sup_i
if (conf_ji >= MIN_CONFIDENCE && lift_ji > MIN_LIFT) {
rules_list[[length(rules_list) + 1]] <- data.frame(
Antecedent = gsub("_", " ", cols[j]),
Consequent = gsub("_", " ", cols[i]),
Support = round(sup_ij, 3),
Confidence = round(conf_ji, 3),
Lift = round(lift_ji, 3),
Count = round(sup_ij * n_trans)
)
}
}
}
rules_df <- bind_rows(rules_list) %>%
arrange(desc(Lift), desc(Confidence)) %>%
mutate(
No = row_number(),
Rule_Str = paste0("{", Antecedent, "} -> {", Consequent, "}"),
Kekuatan = case_when(
Lift >= 1.5 & Confidence >= 0.80 ~ "Sangat Kuat",
Lift >= 1.2 & Confidence >= 0.70 ~ "Kuat",
TRUE ~ "Cukup"
)
) %>%
select(No, Rule_Str, Antecedent, Consequent,
Support, Confidence, Lift, Count, Kekuatan)
cat("Selesai.", nrow(rules_df), "rules ditemukan.\n\n")
## Selesai. 80 rules ditemukan.
cat("Support = proporsi hari kedua produk terjual bersamaan\n")
## Support = proporsi hari kedua produk terjual bersamaan
cat("Confidence = peluang beli Consequent jika sudah beli Antecedent\n")
## Confidence = peluang beli Consequent jika sudah beli Antecedent
cat("Lift = kekuatan asosiasi; >1 = hubungan positif\n\n")
## Lift = kekuatan asosiasi; >1 = hubungan positif
print(rules_df)
## No Rule_Str Antecedent Consequent Support Confidence
## 1 1 {Mie Goreng} -> {Nasi Goreng} Mie Goreng Nasi Goreng 0.514 0.839
## 2 2 {Nasi Goreng} -> {Mie Goreng} Nasi Goreng Mie Goreng 0.514 0.653
## 3 3 {Espresso} -> {Cookies} Espresso Cookies 0.546 0.813
## 4 4 {Espresso} -> {Tahu Bakso} Espresso Tahu Bakso 0.519 0.772
## 5 5 {Cookies} -> {Espresso} Cookies Espresso 0.546 0.714
## 6 6 {Tahu Bakso} -> {Espresso} Tahu Bakso Espresso 0.519 0.714
## 7 7 {Tahu Bakso} -> {Dimsum} Tahu Bakso Dimsum 0.579 0.797
## 8 8 {Dimsum} -> {Tahu Bakso} Dimsum Tahu Bakso 0.579 0.757
## 9 9 {Espresso} -> {Singkong} Espresso Singkong 0.568 0.846
## 10 10 {Singkong} -> {Espresso} Singkong Espresso 0.568 0.698
## 11 11 {Mie Goreng} -> {Cup Panas} Mie Goreng Cup Panas 0.541 0.884
## 12 12 {Cup Panas} -> {Mie Goreng} Cup Panas Mie Goreng 0.541 0.635
## 13 13 {Tahu Bakso} -> {Singkong} Tahu Bakso Singkong 0.612 0.842
## 14 14 {Singkong} -> {Tahu Bakso} Singkong Tahu Bakso 0.612 0.752
## 15 15 {Espresso} -> {Nasi Goreng} Espresso Nasi Goreng 0.546 0.813
## 16 16 {Nasi Goreng} -> {Espresso} Nasi Goreng Espresso 0.546 0.694
## 17 17 {Nasi Goreng} -> {Singkong} Nasi Goreng Singkong 0.661 0.840
## 18 18 {Singkong} -> {Nasi Goreng} Singkong Nasi Goreng 0.661 0.812
## 19 19 {Pentol} -> {Kentang} Pentol Kentang 0.519 0.979
## 20 20 {Nasi Goreng} -> {Kentang} Nasi Goreng Kentang 0.770 0.979
## 21 21 {Kentang} -> {Nasi Goreng} Kentang Nasi Goreng 0.770 0.810
## 22 22 {Espresso} -> {Kentang} Espresso Kentang 0.656 0.976
## 23 23 {Kentang} -> {Espresso} Kentang Espresso 0.656 0.690
## 24 24 {Singkong} -> {Kentang} Singkong Kentang 0.792 0.973
## 25 25 {Kentang} -> {Singkong} Kentang Singkong 0.792 0.833
## 26 26 {Cookies} -> {Cup Panas} Cookies Cup Panas 0.667 0.871
## 27 27 {Cup Panas} -> {Cookies} Cup Panas Cookies 0.667 0.782
## 28 28 {Tahu Bakso} -> {Cookies} Tahu Bakso Cookies 0.568 0.782
## 29 29 {Cookies} -> {Tahu Bakso} Cookies Tahu Bakso 0.568 0.743
## 30 30 {Nasi Goreng} -> {Cup Panas} Nasi Goreng Cup Panas 0.683 0.868
## 31 31 {Cup Panas} -> {Nasi Goreng} Cup Panas Nasi Goreng 0.683 0.801
## 32 32 {Pentol} -> {Cireng} Pentol Cireng 0.530 1.000
## 33 33 {Tahu Bakso} -> {Cireng} Tahu Bakso Cireng 0.727 1.000
## 34 34 {Mie Goreng} -> {Cireng} Mie Goreng Cireng 0.612 1.000
## 35 35 {Mie Kuah} -> {Cireng} Mie Kuah Cireng 0.536 1.000
## 36 36 {Cireng} -> {Tahu Bakso} Cireng Tahu Bakso 0.727 0.739
## 37 37 {Cireng} -> {Mie Goreng} Cireng Mie Goreng 0.612 0.622
## 38 38 {Dimsum} -> {Kentang} Dimsum Kentang 0.738 0.964
## 39 39 {Mie Goreng} -> {Kentang} Mie Goreng Kentang 0.590 0.964
## 40 40 {Kentang} -> {Dimsum} Kentang Dimsum 0.738 0.776
## 41 41 {Kentang} -> {Mie Goreng} Kentang Mie Goreng 0.590 0.621
## 42 42 {Dimsum} -> {Air Mineral} Dimsum Air Mineral 0.754 0.986
## 43 43 {Air Mineral} -> {Dimsum} Air Mineral Dimsum 0.754 0.775
## 44 44 {Tahu Bakso} -> {Kentang} Tahu Bakso Kentang 0.699 0.962
## 45 45 {Kentang} -> {Tahu Bakso} Kentang Tahu Bakso 0.699 0.736
## 46 46 {Nasi Goreng} -> {Cireng} Nasi Goreng Cireng 0.781 0.993
## 47 47 {Cireng} -> {Nasi Goreng} Cireng Nasi Goreng 0.781 0.794
## 48 48 {Dimsum} -> {Cireng} Dimsum Cireng 0.760 0.993
## 49 49 {Mie Kuah} -> {Kentang} Mie Kuah Kentang 0.514 0.959
## 50 50 {Cookies} -> {Singkong} Cookies Singkong 0.628 0.821
## 51 51 {Mie Goreng} -> {Singkong} Mie Goreng Singkong 0.503 0.821
## 52 52 {Singkong} -> {Cookies} Singkong Cookies 0.628 0.772
## 53 53 {Cireng} -> {Dimsum} Cireng Dimsum 0.760 0.772
## 54 54 {Singkong} -> {Mie Goreng} Singkong Mie Goreng 0.503 0.617
## 55 55 {Espresso} -> {Cireng} Espresso Cireng 0.667 0.992
## 56 56 {Cookies} -> {Nasi Goreng} Cookies Nasi Goreng 0.607 0.793
## 57 57 {Nasi Goreng} -> {Cookies} Nasi Goreng Cookies 0.607 0.771
## 58 58 {Cireng} -> {Espresso} Cireng Espresso 0.667 0.678
## 59 59 {Singkong} -> {Air Mineral} Singkong Air Mineral 0.798 0.980
## 60 60 {Nasi Goreng} -> {Air Mineral} Nasi Goreng Air Mineral 0.770 0.979
## 61 61 {Air Mineral} -> {Singkong} Air Mineral Singkong 0.798 0.820
## 62 62 {Air Mineral} -> {Nasi Goreng} Air Mineral Nasi Goreng 0.770 0.792
## 63 63 {Cookies} -> {Air Mineral} Cookies Air Mineral 0.749 0.979
## 64 64 {Air Mineral} -> {Cookies} Air Mineral Cookies 0.749 0.770
## 65 65 {Air Mineral} -> {Cireng} Air Mineral Cireng 0.962 0.989
## 66 66 {Kentang} -> {Cireng} Kentang Cireng 0.940 0.989
## 67 67 {Cireng} -> {Air Mineral} Cireng Air Mineral 0.962 0.978
## 68 68 {Cireng} -> {Kentang} Cireng Kentang 0.940 0.956
## 69 69 {Tahu Bakso} -> {Cup Panas} Tahu Bakso Cup Panas 0.623 0.857
## 70 70 {Cup Panas} -> {Tahu Bakso} Cup Panas Tahu Bakso 0.623 0.731
## 71 71 {Kentang} -> {Air Mineral} Kentang Air Mineral 0.929 0.977
## 72 72 {Air Mineral} -> {Kentang} Air Mineral Kentang 0.929 0.955
## 73 73 {Singkong} -> {Cireng} Singkong Cireng 0.803 0.987
## 74 74 {Cireng} -> {Singkong} Cireng Singkong 0.803 0.817
## 75 75 {Cookies} -> {Cireng} Cookies Cireng 0.754 0.986
## 76 76 {Cup Panas} -> {Air Mineral} Cup Panas Air Mineral 0.831 0.974
## 77 77 {Air Mineral} -> {Cup Panas} Air Mineral Cup Panas 0.831 0.854
## 78 78 {Cireng} -> {Cookies} Cireng Cookies 0.754 0.767
## 79 79 {Dimsum} -> {Singkong} Dimsum Singkong 0.623 0.814
## 80 80 {Singkong} -> {Dimsum} Singkong Dimsum 0.623 0.765
## Lift Count Kekuatan
## 1 1.067 94 Cukup
## 2 1.067 94 Cukup
## 3 1.063 100 Cukup
## 4 1.063 95 Cukup
## 5 1.063 100 Cukup
## 6 1.063 95 Cukup
## 7 1.042 106 Cukup
## 8 1.042 106 Cukup
## 9 1.038 104 Cukup
## 10 1.038 104 Cukup
## 11 1.037 99 Cukup
## 12 1.037 99 Cukup
## 13 1.034 112 Cukup
## 14 1.034 112 Cukup
## 15 1.033 100 Cukup
## 16 1.033 100 Cukup
## 17 1.032 121 Cukup
## 18 1.032 121 Cukup
## 19 1.030 95 Cukup
## 20 1.030 141 Cukup
## 21 1.030 141 Cukup
## 22 1.026 120 Cukup
## 23 1.026 120 Cukup
## 24 1.023 145 Cukup
## 25 1.023 145 Cukup
## 26 1.022 122 Cukup
## 27 1.022 122 Cukup
## 28 1.022 104 Cukup
## 29 1.022 104 Cukup
## 30 1.018 125 Cukup
## 31 1.018 125 Cukup
## 32 1.017 97 Cukup
## 33 1.017 133 Cukup
## 34 1.017 112 Cukup
## 35 1.017 98 Cukup
## 36 1.017 133 Cukup
## 37 1.017 112 Cukup
## 38 1.014 135 Cukup
## 39 1.014 108 Cukup
## 40 1.014 135 Cukup
## 41 1.014 108 Cukup
## 42 1.013 138 Cukup
## 43 1.013 138 Cukup
## 44 1.012 128 Cukup
## 45 1.012 128 Cukup
## 46 1.010 143 Cukup
## 47 1.010 143 Cukup
## 48 1.009 139 Cukup
## 49 1.009 94 Cukup
## 50 1.009 115 Cukup
## 51 1.009 92 Cukup
## 52 1.009 115 Cukup
## 53 1.009 139 Cukup
## 54 1.009 92 Cukup
## 55 1.008 122 Cukup
## 56 1.008 111 Cukup
## 57 1.008 111 Cukup
## 58 1.008 122 Cukup
## 59 1.007 146 Cukup
## 60 1.007 141 Cukup
## 61 1.007 146 Cukup
## 62 1.007 141 Cukup
## 63 1.006 137 Cukup
## 64 1.006 137 Cukup
## 65 1.005 176 Cukup
## 66 1.005 172 Cukup
## 67 1.005 176 Cukup
## 68 1.005 172 Cukup
## 69 1.005 114 Cukup
## 70 1.005 114 Cukup
## 71 1.004 170 Cukup
## 72 1.004 170 Cukup
## 73 1.003 147 Cukup
## 74 1.003 147 Cukup
## 75 1.002 138 Cukup
## 76 1.002 152 Cukup
## 77 1.002 152 Cukup
## 78 1.002 138 Cukup
## 79 1.000 114 Cukup
## 80 1.000 114 Cukup
# ============================================================
# BAGIAN 12 — VISUALISASI ASSOCIATION RULES (VIZ 9–12)
# ============================================================
# VIZ 9: Scatter Plot Support vs Confidence
# (VIZ 11 asli dihapus — scatter berlabel sering overlap & tidak menambah info baru
# di luar yang sudah ada di VIZ 9 + bar chart Top Rules)
ggplot(rules_df, aes(x = Support, y = Confidence, size = Lift, color = Lift)) +
geom_point(alpha = 0.8) +
scale_color_gradient(low = "#F2D0A4", high = "#3E1C00") +
scale_size_continuous(range = c(3, 10)) +
labs(title = "Association Rules: Support vs Confidence",
subtitle = "Ukuran & warna titik = nilai Lift",
x = "Support", y = "Confidence",
color = "Lift", size = "Lift",
caption = "Gambar 9. Titik kanan atas = rules terbaik (sering terjadi & asosiasi kuat).") +
theme_minimal(base_size = 11) +
theme(plot.title = element_text(face = "bold", color = "#3E1C00"),
plot.caption = element_text(hjust = 0, color = "#6F3811", size = 9))

# VIZ 10: Heatmap Lift Antar Produk
ggplot(rules_df, aes(x = Consequent, y = Antecedent, fill = Lift)) +
geom_tile(color = "white") +
geom_text(aes(label = round(Lift, 2)), size = 2.8, color = "white") +
scale_fill_gradient(low = "#FBE9D0", high = "#3E1C00") +
labs(title = "Heatmap Kekuatan Asosiasi (Lift)",
subtitle = "Baris = produk pemicu | Kolom = produk yang cenderung dibeli juga",
x = "Consequent", y = "Antecedent", fill = "Lift",
caption = "Gambar 10. Kotak gelap = asosiasi sangat kuat antar dua produk.") +
theme_minimal(base_size = 10) +
theme(plot.title = element_text(face = "bold", color = "#3E1C00"),
axis.text.x = element_text(angle = 45, hjust = 1),
plot.caption = element_text(hjust = 0, color = "#6F3811", size = 9))

# VIZ 11: Bar Chart Top Rules by Lift
ggplot(rules_df %>% head(15),
aes(x = reorder(Rule_Str, Lift), y = Lift, fill = Kekuatan)) +
geom_col(width = 0.7) +
geom_text(aes(label = paste0("Lift=", Lift, " | Conf=", Confidence)),
hjust = -0.05, size = 2.8, color = "#3E1C00") +
coord_flip() +
scale_fill_manual(values = c("Sangat Kuat" = "#3E1C00",
"Kuat" = "#A0522D",
"Cukup" = "#C68642")) +
scale_y_continuous(expand = expansion(mult = c(0, 0.3))) +
labs(title = "Top 15 Association Rules Berdasarkan Lift",
subtitle = "Dasar kuantitatif rekomendasi product bundling",
x = NULL, y = "Lift", fill = "Kekuatan Rules",
caption = "Gambar 11. {A} -> {B}: jika pelanggan beli A, rekomendasikan B.") +
theme_minimal(base_size = 10) +
theme(plot.title = element_text(face = "bold", color = "#3E1C00"),
plot.caption = element_text(hjust = 0, color = "#6F3811", size = 9))

# ── Buat co_matrix lebih awal (dibutuhkan VIZ 12) ──
co_matrix <- t(data_matrix) %*% data_matrix
diag(co_matrix) <- 0
colnames(co_matrix) <- gsub("_", " ", colnames(co_matrix))
rownames(co_matrix) <- gsub("_", " ", rownames(co_matrix))
# VIZ 12: Heatmap Co-occurrence Antar Produk
# Lebih mudah dibaca: baris × kolom = pasangan produk,
# warna = seberapa sering keduanya terjual di hari yang sama.
co_heat <- as.data.frame(as.table(co_matrix)) %>%
rename(Produk1 = Var1, Produk2 = Var2, CoOccurrence = Freq)
ggplot(co_heat, aes(x = Produk2, y = Produk1, fill = CoOccurrence)) +
geom_tile(color = "white", linewidth = 0.4) +
geom_text(aes(label = ifelse(CoOccurrence > 0, CoOccurrence, "")),
size = 2.5, color = "white") +
scale_fill_gradient(low = "#FBE9D0", high = "#3E1C00", labels = comma) +
labs(title = "Heatmap Co-occurrence Antar Produk",
subtitle = "Angka = jumlah hari kedua produk terjual bersamaan",
x = NULL, y = NULL, fill = "Hari\nBersamaan",
caption = "Gambar 12. Warna gelap = pasangan produk yang paling sering muncul di hari yang sama.") +
theme_minimal(base_size = 10) +
theme(plot.title = element_text(face = "bold", color = "#3E1C00"),
axis.text.x = element_text(angle = 45, hjust = 1),
plot.caption = element_text(hjust = 0, color = "#6F3811", size = 9))

# ============================================================
# BAGIAN 13 — CO-OCCURRENCE MATRIX & VIZ 13
# ============================================================
co_df <- as.data.frame(as.table(co_matrix)) %>%
rename(Produk1 = Var1, Produk2 = Var2, CoOccurrence = Freq) %>%
filter(as.character(Produk1) < as.character(Produk2), CoOccurrence > 0) %>%
arrange(desc(CoOccurrence)) %>%
head(20) %>%
mutate(Pasangan = paste(Produk1, "-", Produk2))
ggplot(co_df,
aes(x = reorder(Pasangan, CoOccurrence), y = CoOccurrence)) +
geom_col(fill = "#A0522D", alpha = 0.9, width = 0.7) +
geom_text(aes(label = paste0(CoOccurrence, " hari")),
hjust = -0.1, size = 3.2, color = "#3E1C00") +
coord_flip() +
scale_y_continuous(expand = expansion(mult = c(0, 0.18))) +
labs(title = "Top 20 Pasangan Produk dengan Co-occurrence Tertinggi",
subtitle = "Puan Kopi Gunung Malang | Maret–September 2025",
x = NULL, y = "Jumlah Hari Terjual Bersamaan",
caption = "Gambar 13. Pasangan produk yang paling sering muncul di hari yang sama.") +
theme_minimal(base_size = 11) +
theme(plot.title = element_text(face = "bold", color = "#3E1C00"),
plot.caption = element_text(hjust = 0, color = "#6F3811", size = 9))

# ============================================================
# BAGIAN 14 — KORELASI ANTAR PRODUK (VIZ 14)
# ============================================================
data_biner_cor <- data_biner %>% select(where(~ sd(., na.rm = TRUE) > 0))
cor_matrix <- cor(data_biner_cor, use = "pairwise.complete.obs")
cor_matrix[is.nan(cor_matrix)] <- 0
cor_matrix[is.na(cor_matrix)] <- 0
colnames(cor_matrix) <- gsub("_", " ", colnames(cor_matrix))
rownames(cor_matrix) <- gsub("_", " ", rownames(cor_matrix))
corrplot(
cor_matrix,
method = "color",
type = "upper",
order = "hclust",
addCoef.col = "#3E1C00",
number.cex = 0.65,
tl.cex = 0.8,
tl.col = "#3E1C00",
col = colorRampPalette(c("#FBE9D0", "#C68642", "#3E1C00"))(200),
title = "Gambar 14: Korelasi Antar Produk — Puan Kopi Gunung Malang",
mar = c(0, 0, 3, 0)
)
mtext("Kotak gelap = korelasi positif kuat (sering dibeli bersamaan).",
side = 1, line = 1, cex = 0.72, col = "#6F3811", adj = 0)

# ============================================================
# BAGIAN 15 — REKOMENDASI PRODUCT BUNDLING + HARGA AKTUAL
# ============================================================
bundle_dari_rules <- rules_df %>%
filter(Lift > MIN_LIFT, Confidence >= MIN_CONFIDENCE) %>%
arrange(desc(Lift), desc(Confidence)) %>%
mutate(
Rekomendasi_Bundle = paste0(Antecedent, " + ", Consequent),
Harga_A = harga_satuan[gsub(" ", "_", Antecedent)],
Harga_B = harga_satuan[gsub(" ", "_", Consequent)],
Harga_Normal = Harga_A + Harga_B,
Diskon_Pct = case_when(
Kekuatan == "Sangat Kuat" ~ 0.15,
Kekuatan == "Kuat" ~ 0.10,
TRUE ~ 0.05
),
Harga_Bundle = round(Harga_Normal * (1 - Diskon_Pct) / 500) * 500,
Hemat = Harga_Normal - Harga_Bundle,
Dasar = paste0(Count, " hari | Conf ",
round(Confidence * 100), "% | Lift ", Lift)
) %>%
select(Rekomendasi_Bundle, Harga_Normal, Harga_Bundle, Hemat,
Support, Confidence, Lift, Count, Kekuatan, Diskon_Pct, Dasar)
top_bundle <- bundle_dari_rules %>%
distinct(Rekomendasi_Bundle, .keep_all = TRUE) %>%
head(4) %>%
mutate(
No = row_number(),
Nama_Bundle = c("Nongkrong Kit",
"Work From Cafe Pack",
"Healing Combo",
"Makan Bareng Bundle")[1:4],
Target_Segmen = c(
"Gen Z & milenial yang nongkrong sore/malam",
"Mahasiswa dan pekerja yang bawa laptop",
"Pelanggan yang butuh me-time atau istirahat",
"Pelanggan yang datang untuk makan"
)[1:4]
)
# ============================================================
# BAGIAN 16 — VISUALISASI BUNDLE (VIZ 15–16)
# ============================================================
# VIZ 15: Kekuatan Asosiasi per Bundle
# (VIZ 18 asli dihapus — metrik Support/Confidence/Lift sudah tertera
# sebagai label di chart ini, sehingga bar chart 3 metrik terpisah redundan)
ggplot(bundle_dari_rules %>%
distinct(Rekomendasi_Bundle, .keep_all = TRUE) %>%
head(12),
aes(x = reorder(Rekomendasi_Bundle, Lift), y = Lift, fill = Kekuatan)) +
geom_col(width = 0.7) +
geom_text(
aes(label = paste0("Lift=", Lift, " Conf=", round(Confidence * 100), "%",
" (", Count, " hari)")),
hjust = -0.04, size = 2.7, color = "#3E1C00"
) +
coord_flip() +
scale_fill_manual(values = c("Sangat Kuat" = "#3E1C00",
"Kuat" = "#A0522D",
"Cukup" = "#C68642")) +
scale_y_continuous(expand = expansion(mult = c(0, 0.35))) +
labs(title = "Rekomendasi Product Bundling — Kekuatan Asosiasi",
subtitle = "Setiap pasangan diturunkan langsung dari hasil MBA",
x = NULL, y = "Lift", fill = "Kekuatan",
caption = "Gambar 15. Warna gelap = prioritas utama implementasi bundling.") +
theme_minimal(base_size = 10) +
theme(plot.title = element_text(face = "bold", color = "#3E1C00", size = 11),
plot.caption = element_text(hjust = 0, color = "#6F3811", size = 9),
legend.position = "right")

# VIZ 16: Perbandingan Harga Satuan vs Harga Bundle (Top 8)
harga_viz <- bundle_dari_rules %>%
distinct(Rekomendasi_Bundle, .keep_all = TRUE) %>%
head(8) %>%
select(Rekomendasi_Bundle, Harga_Normal, Harga_Bundle) %>%
pivot_longer(-Rekomendasi_Bundle, names_to = "Tipe", values_to = "Harga") %>%
mutate(Tipe = recode(Tipe,
"Harga_Normal" = "Harga Satuan",
"Harga_Bundle" = "Harga Bundle"))
ggplot(harga_viz,
aes(x = reorder(Rekomendasi_Bundle, Harga), y = Harga, fill = Tipe)) +
geom_col(position = "dodge", width = 0.7) +
geom_text(aes(label = paste0("Rp ", format(Harga, big.mark = "."))),
position = position_dodge(width = 0.7),
hjust = -0.05, size = 2.6, color = "#3E1C00") +
coord_flip() +
scale_fill_manual(values = c("Harga Satuan" = "#D2A679",
"Harga Bundle" = "#3E1C00")) +
scale_y_continuous(
labels = function(x) paste0("Rp ", format(x, big.mark = ".")),
expand = expansion(mult = c(0, 0.4))
) +
labs(title = "Perbandingan Harga Satuan vs Harga Bundle",
subtitle = "Selisih harga = nilai penghematan yang ditawarkan kepada pelanggan",
x = NULL, y = "Harga (Rp)", fill = NULL,
caption = "Gambar 16. Batang gelap = harga bundle (lebih murah dari beli satuan). Harga dari MENU PUAN KOPI.") +
theme_minimal(base_size = 10) +
theme(plot.title = element_text(face = "bold", color = "#3E1C00"),
plot.caption = element_text(hjust = 0, color = "#6F3811", size = 9),
legend.position = "top")
## Warning in prettyNum(.Internal(format(x, trim, digits, nsmall, width, 3L, :
## 'big.mark' and 'decimal.mark' are both '.', which could be confusing
## Warning in prettyNum(.Internal(format(x, trim, digits, nsmall, width, 3L, :
## 'big.mark' and 'decimal.mark' are both '.', which could be confusing

# ============================================================
# BAGIAN 17 — STRATEGI CRM
# ============================================================
strategi_dari_rules <- top_bundle %>%
mutate(
Strategi_CRM = case_when(
Kekuatan == "Sangat Kuat" ~ paste0(
"Tampilkan '", Nama_Bundle, "' di menu utama. Diskon ",
Diskon_Pct * 100, "%. Kasir aktif menawarkan."
),
Kekuatan == "Kuat" ~ paste0(
"Diskon ", Diskon_Pct * 100, "% untuk '", Nama_Bundle,
"'. Promosi via Instagram Story & WhatsApp broadcast."
),
TRUE ~ paste0(
"Cross-selling: saat pelanggan pesan ", Rekomendasi_Bundle,
" sebagai pelengkap."
)
),
Dasar_MBA = paste0("Lift=", Lift, " | Conf=", round(Confidence * 100),
"% | ", Count, " hari bersamaan")
) %>%
select(No, Nama_Bundle, Rekomendasi_Bundle, Target_Segmen,
Harga_Normal, Harga_Bundle, Hemat,
Kekuatan, Strategi_CRM, Dasar_MBA)
cat("\n=== STRATEGI CRM PER BUNDLE ===\n\n")
##
## === STRATEGI CRM PER BUNDLE ===
print(strategi_dari_rules)
## No Nama_Bundle Rekomendasi_Bundle
## 1 1 Nongkrong Kit Mie Goreng + Nasi Goreng
## 2 2 Work From Cafe Pack Nasi Goreng + Mie Goreng
## 3 3 Healing Combo Espresso + Cookies
## 4 4 Makan Bareng Bundle Espresso + Tahu Bakso
## Target_Segmen Harga_Normal Harga_Bundle Hemat
## 1 Gen Z & milenial yang nongkrong sore/malam 48000 45500 2500
## 2 Mahasiswa dan pekerja yang bawa laptop 48000 45500 2500
## 3 Pelanggan yang butuh me-time atau istirahat 15000 14000 1000
## 4 Pelanggan yang datang untuk makan 32000 30500 1500
## Kekuatan
## 1 Cukup
## 2 Cukup
## 3 Cukup
## 4 Cukup
## Strategi_CRM
## 1 Cross-selling: saat pelanggan pesan Mie Goreng + Nasi Goreng sebagai pelengkap.
## 2 Cross-selling: saat pelanggan pesan Nasi Goreng + Mie Goreng sebagai pelengkap.
## 3 Cross-selling: saat pelanggan pesan Espresso + Cookies sebagai pelengkap.
## 4 Cross-selling: saat pelanggan pesan Espresso + Tahu Bakso sebagai pelengkap.
## Dasar_MBA
## 1 Lift=1.067 | Conf=84% | 94 hari bersamaan
## 2 Lift=1.067 | Conf=65% | 94 hari bersamaan
## 3 Lift=1.063 | Conf=81% | 100 hari bersamaan
## 4 Lift=1.063 | Conf=77% | 95 hari bersamaan
strategi_crm_lengkap <- data.frame(
No = 1:6,
Strategi = c("Bundle Pricing", "Loyalty Stamp Card", "Promo Jam Sibuk",
"Weekend Vibes Bundle", "Smart Cross-Selling", "Member Poin Ganda"),
Deskripsi = c(
paste0("Harga bundle lebih murah dari beli satuan (diskon 5–15%). Ditampilkan ",
"sebagai 'rekomendasi kasir' di papan menu."),
"Setiap pembelian bundle mendapat 1 stamp. Kumpulkan 5 stamp = 1 minuman gratis.",
"Harga spesial jam 11.00–14.00 untuk bundle makanan. Menyasar pekerja milenial.",
"Bundle eksklusif Sabtu–Minggu dengan nama & packaging khusus.",
paste0("Kasir dibekali daftar ", nrow(rules_df),
" association rules. Contoh: pelanggan pesan kopi → kasir tawarkan menu pelengkap terkuat."),
"Member dapat poin 2× saat beli bundle. Mendorong loyalitas & meningkatkan nilai transaksi."
),
Target_Segmen = c(
"Gen Z dan milenial, semua bundle",
"Pelanggan reguler Gen Z",
"Milenial pekerja, bundle makanan",
"Gen Z dan milenial, akhir pekan",
"Semua pelanggan",
"Member terdaftar"
),
Dasar_Keputusan = c(
"Lift & Confidence tertinggi dari hasil MBA",
"Pola kunjungan berulang dari data harian",
"Co-occurrence makanan dan minuman tinggi",
"Hasil analisis Weekday vs Weekend",
paste0(nrow(rules_df), " association rules dari data"),
"Strategi retensi berbasis data transaksi"
)
)
cat("\n=== TABEL STRATEGI CRM LENGKAP ===\n\n")
##
## === TABEL STRATEGI CRM LENGKAP ===
print(strategi_crm_lengkap)
## No Strategi
## 1 1 Bundle Pricing
## 2 2 Loyalty Stamp Card
## 3 3 Promo Jam Sibuk
## 4 4 Weekend Vibes Bundle
## 5 5 Smart Cross-Selling
## 6 6 Member Poin Ganda
## Deskripsi
## 1 Harga bundle lebih murah dari beli satuan (diskon 5–15%). Ditampilkan sebagai 'rekomendasi kasir' di papan menu.
## 2 Setiap pembelian bundle mendapat 1 stamp. Kumpulkan 5 stamp = 1 minuman gratis.
## 3 Harga spesial jam 11.00–14.00 untuk bundle makanan. Menyasar pekerja milenial.
## 4 Bundle eksklusif Sabtu–Minggu dengan nama & packaging khusus.
## 5 Kasir dibekali daftar 80 association rules. Contoh: pelanggan pesan kopi → kasir tawarkan menu pelengkap terkuat.
## 6 Member dapat poin 2× saat beli bundle. Mendorong loyalitas & meningkatkan nilai transaksi.
## Target_Segmen Dasar_Keputusan
## 1 Gen Z dan milenial, semua bundle Lift & Confidence tertinggi dari hasil MBA
## 2 Pelanggan reguler Gen Z Pola kunjungan berulang dari data harian
## 3 Milenial pekerja, bundle makanan Co-occurrence makanan dan minuman tinggi
## 4 Gen Z dan milenial, akhir pekan Hasil analisis Weekday vs Weekend
## 5 Semua pelanggan 80 association rules dari data
## 6 Member terdaftar Strategi retensi berbasis data transaksi
# ============================================================
# BAGIAN 18 — EXPORT CSV
# ============================================================
write.csv(
rules_df %>% select(No, Rule_Str, Support, Confidence, Lift, Count, Kekuatan),
"01_association_rules.csv", row.names = FALSE
)
write.csv(freq_1, "02_frequent_itemsets.csv", row.names = FALSE)
write.csv(bundle_dari_rules, "03_rekomendasi_bundling.csv", row.names = FALSE)
write.csv(strategi_crm_lengkap, "04_strategi_crm.csv", row.names = FALSE)
write.csv(total_produk, "05_total_penjualan_produk.csv", row.names = FALSE)
write.csv(co_df, "06_top_cooccurrence.csv", row.names = FALSE)
cat("\nFile CSV tersimpan: 01_association_rules.csv sampai 06_top_cooccurrence.csv\n\n")
##
## File CSV tersimpan: 01_association_rules.csv sampai 06_top_cooccurrence.csv
# ============================================================
# BAGIAN 19 — RINGKASAN AKHIR
# ============================================================
cat("============================================================\n")
## ============================================================
cat(" RINGKASAN AKHIR — Puan Kopi Gunung Malang\n")
## RINGKASAN AKHIR — Puan Kopi Gunung Malang
cat("============================================================\n")
## ============================================================
cat("Periode :", format(min(data$Tanggal)), "s.d.", format(max(data$Tanggal)), "\n")
## Periode : 2025-03-29 s.d. 2025-09-30
cat("Total hari :", nrow(data), "| Jumlah produk: 17\n")
## Total hari : 183 | Jumlah produk: 17
cat("Catatan : Outlier Espresso (48 unit, 28 Jun) diganti dengan median\n")
## Catatan : Outlier Espresso (48 unit, 28 Jun) diganti dengan median
cat("------------------------------------------------------------\n")
## ------------------------------------------------------------
cat("Hasil MBA:\n")
## Hasil MBA:
cat(" Frequent items :", nrow(freq_1),
"produk (support >= 50%) — Kapiten, Fudgy Brownies, Fudgy Brownies Aya tidak lolos\n")
## Frequent items : 14 produk (support >= 50%) — Kapiten, Fudgy Brownies, Fudgy Brownies Aya tidak lolos
cat(" Association rules:", nrow(rules_df), "rules\n")
## Association rules: 80 rules
cat(" Sangat Kuat :", nrow(rules_df %>% filter(Kekuatan == "Sangat Kuat")), "\n")
## Sangat Kuat : 0
cat(" Kuat :", nrow(rules_df %>% filter(Kekuatan == "Kuat")), "\n")
## Kuat : 0
cat("------------------------------------------------------------\n")
## ------------------------------------------------------------
cat("Top 4 Bundle (harga dari MENU PUAN KOPI):\n")
## Top 4 Bundle (harga dari MENU PUAN KOPI):
for (i in 1:nrow(top_bundle)) {
cat(" ", top_bundle$Nama_Bundle[i], "-", top_bundle$Rekomendasi_Bundle[i], "\n")
cat(" Normal: Rp", format(top_bundle$Harga_Normal[i], big.mark = "."),
"| Bundle: Rp", format(top_bundle$Harga_Bundle[i], big.mark = "."),
"| Hemat: Rp", format(top_bundle$Hemat[i], big.mark = "."),
paste0("(", top_bundle$Diskon_Pct[i]*100, "%) | Lift=", top_bundle$Lift[i], ")\n"))
}
## Nongkrong Kit - Mie Goreng + Nasi Goreng
## Warning in prettyNum(.Internal(format(x, trim, digits, nsmall, width, 3L, :
## 'big.mark' and 'decimal.mark' are both '.', which could be confusing
## Warning in prettyNum(.Internal(format(x, trim, digits, nsmall, width, 3L, :
## 'big.mark' and 'decimal.mark' are both '.', which could be confusing
## Warning in prettyNum(.Internal(format(x, trim, digits, nsmall, width, 3L, :
## 'big.mark' and 'decimal.mark' are both '.', which could be confusing
## Normal: Rp 48.000 | Bundle: Rp 45.500 | Hemat: Rp 2.500 (5%) | Lift=1.067)
## Work From Cafe Pack - Nasi Goreng + Mie Goreng
## Warning in prettyNum(.Internal(format(x, trim, digits, nsmall, width, 3L, :
## 'big.mark' and 'decimal.mark' are both '.', which could be confusing
## Warning in prettyNum(.Internal(format(x, trim, digits, nsmall, width, 3L, :
## 'big.mark' and 'decimal.mark' are both '.', which could be confusing
## Warning in prettyNum(.Internal(format(x, trim, digits, nsmall, width, 3L, :
## 'big.mark' and 'decimal.mark' are both '.', which could be confusing
## Normal: Rp 48.000 | Bundle: Rp 45.500 | Hemat: Rp 2.500 (5%) | Lift=1.067)
## Healing Combo - Espresso + Cookies
## Warning in prettyNum(.Internal(format(x, trim, digits, nsmall, width, 3L, :
## 'big.mark' and 'decimal.mark' are both '.', which could be confusing
## Warning in prettyNum(.Internal(format(x, trim, digits, nsmall, width, 3L, :
## 'big.mark' and 'decimal.mark' are both '.', which could be confusing
## Warning in prettyNum(.Internal(format(x, trim, digits, nsmall, width, 3L, :
## 'big.mark' and 'decimal.mark' are both '.', which could be confusing
## Normal: Rp 15.000 | Bundle: Rp 14.000 | Hemat: Rp 1.000 (5%) | Lift=1.063)
## Makan Bareng Bundle - Espresso + Tahu Bakso
## Warning in prettyNum(.Internal(format(x, trim, digits, nsmall, width, 3L, :
## 'big.mark' and 'decimal.mark' are both '.', which could be confusing
## Warning in prettyNum(.Internal(format(x, trim, digits, nsmall, width, 3L, :
## 'big.mark' and 'decimal.mark' are both '.', which could be confusing
## Warning in prettyNum(.Internal(format(x, trim, digits, nsmall, width, 3L, :
## 'big.mark' and 'decimal.mark' are both '.', which could be confusing
## Normal: Rp 32.000 | Bundle: Rp 30.500 | Hemat: Rp 1.500 (5%) | Lift=1.063)
cat("------------------------------------------------------------\n")
## ------------------------------------------------------------
cat("Strategi CRM : 6 strategi | Visualisasi: 16 grafik | CSV: 6 file\n")
## Strategi CRM : 6 strategi | Visualisasi: 16 grafik | CSV: 6 file
cat("============================================================\n")
## ============================================================
# ============================================================
# END OF SCRIPT
# ============================================================