ครั้งนี้มาลองทำ Market Basket Analysis (MBA) เป็นเทคนิคที่สำคัญสำหรับ Data Analyst ในการค้นหาความสัมพันธ์ของสินค้าเพื่อวางแผนโปรโมชั่นขายแพ็คคู่ 🛍️ หรืออื่นๆ ในทำนองเดียวกัน เช่น การจัดชั้นวางของในห้างสรรพสินค้า ว่าจะวางอะไรไว้ใกล้กับอะไรดี, หรือใน online shopping อาจจะใช้ในเรื่องการแนะนำสินค้าอะไรต่อไปให้ลูกค้าดี

และหัวใจหลักของ MBA คือการค้นหา กฎ ที่อยู่ในรูปแบบ A→B ซึ่งหมายความว่า “ถ้าลูกค้าซื้อสินค้า A แล้ว (Antecedent) ลูกค้ามีแนวโน้มที่จะซื้อสินค้า B ด้วย (Consequent)”

ในภาษา R (ครั้งนี้) เราจะใช้แพ็กเกจ arules และอัลกอริทึม Apriori เป็นหลัก โดยเริ่มจาก transaction เลย ว่าเริ่มจัดการยังไง คำนวณผลลัพธ์ยังไง และจบยังไง.

การเตรียมข้อมูล (Data Preparation)

มาดูหน้าตาของตาราง transaction การขายสินค้าในแต่ละวันกันก่อน ซึ่งมีหลากลายรูปแบบเลยในการเก็บข้อมูลของแต่ละร้าน แต่สิ่งที่เราจะเอามาใช้วิเคราะห์ MBA ในครั้งนี้ ที่จำเป็นจริงๆ คือ

  • รหัส(คอลัมน์)ที่บอกว่า นี่การซื้อ 1 ครั้ง หรือ 1 ตะกร้า ที่บอกว่าสินค้าพวกนี้เป็นการซื้อในตะกร้าเดียวกัน
  • ชื่อสินค้า เพื่อให้ง่ายต่อการอ่าน/ตีความผลลัพธ์

ซึ่งในฐานข้อมูลเราอาจจะเก็บ timestamp วันที่ที่ซื้อ รหัสแคชเชียร์ จำนวนขาย ราคาต่อหน่วย หน่วยนับ บลาๆ แต่เราจะตัดมาเฉพาะที่จะใช้ก็พอครับ

จากในรูป order_id หนึ่งหมายเลข หมายถึง 1 ตะกร้า ที่ประกอบด้วยสินค้า (product_name) หรือ SKU หลายๆ อย่างที่ลูกค้าหยิบลงตะกร้าแล้วจ่ายเงิน

ในภาษา R เราจะใช้แพ็กเกจ arules และอัลกอริทึม Apriori เป็นหลักครับ เริ่มเลอ…

#### Load library & data ####
library(tidyverse)
library(stringr)
library(arules)

ทำการอ่านไฟล์ข้อมูลที่เราเตรียมมา หรือไฟล์ที่เราได้ทำการอ้อนวอนต่อ data engineer 🙏 ให้เตรียมให้หน่อยย

df_ <- read_csv('RefFiles/ItemsPurchase-2.csv')
df_ <- data.frame(df_)
df_ %>% head(5)

เข้าสู่ขั้น EDA นิดนึงเนอะ เพียงแค่จะดูว่าไฟล์มีขนาดกี่แถว กี่คอลัมน์ มีค่าว่างหรือเปล่า?

#### eda ####
summary(df_)
str(df_)

โดยฟังก์ชัน summary() ก็จะบอกขนาดของค่าว่าง (NA) ให้ด้วย (ถ้ามี) แบบรูปข้างบนเลย, ฟังก์ชัน str() ก็จะดูโครงสร้างของข้อมูล บอกว่าเป็นชนิดไหน มี 1,048,575 แถว (observations) และมี 3 คอลัมน์ (variables)

จากตัวอย่างที่แอดแสดงข้างบนจะเห็นว่าหัวคอลัมน์ มันขัดใจนิดนึง ขอตัดเข้าพาร์ท transform แก้ตารางกับปรับ type ก่อนครับ

# list columns
colnames(df_) <- tolower(colnames(df_))

# rename columns 
names(df_)[1] <- 'order_id'
names(df_)[3] <- 'product_name'
colnames(df_)

# convert type
df <- df_ %>%
  select( order_id, product_name ) %>%
  mutate( order_id = as.character(order_id) )

summary(df)

เริ่มจากการปรับหัวคอลัมน์เป็นตัวพิมพ์เล็ก tolower(), เปลี่ยนชื่อคอลัมน์, สุดท้ายมาเลือกเฉพาะคอลัมน์ที่ต้องการ select(), และแปลงชนิดข้อมูล mutate() ทั้งหมดเก็บในตัวแปรใหม่ชื่อ df แล้ว summary() ออกมาดูอีกครั้ง

เข้าสู่ session: Data preparation เป็นการรวมสินค้าแต่ละตะกร้า (order_id) หรือใบเสร็จเดียวกัน ให้อยู่ใน 1 แถว โดยใช้ตัวคั่นเป็น comma จะได้ผลลัพธ์แบบนี้เลย

#### data preparation ####
# การรวมรายการสินค้าในแต่ละ Invoice/Order ID
trans_df <- df %>%  
  filter( !is.na(product_name) ) %>% 
  group_by( order_id ) %>% 
  summarise(
    items = str_c(product_name, collapse = ',')
  )

sample_n(trans_df, 5)
  • ใช้ order_id เป็นตัวระบุการทำรายการ (Transaction)
  • ใช้ product_name เป็นรายการสินค้า

จากนั้นก็มาแปลงให้เป็นรูปแบบ Transactions: โดย arules ต้องการไฟล์ในรูปแบบ transaction (เหมือนกับไฟล์ csv ที่แต่ละแถวคือหนึ่งตะกร้าสินค้า) เราจะบันทึก ‘transaction_data’ ที่รวมสินค้าแล้วออกมาเป็นไฟล์ชั่วคราวแล้วอ่านกลับเข้าไปใหม่ เก็บไว้ที่ตัวแปร trans

# แปลงเป็นรูปแบบ Transactions
url_file <-"RefFiles/trans.csv"
write.csv(trans_df$items, url_file, quote = F, row.names = F)

trans <- read.transactions(url_file, format = "basket", sep = ",", skip = 1)

# ตรวจสอบข้อมูลเบื้องต้น
inspect(head(trans))
itemFrequencyPlot(trans, topN = 10) # สินค้าในตะกร้าที่ซื้อบ่อย

ลองตรวจสอบข้อมูลเบื้องต้นนิดนึงหลังจากอ่านไฟล์เข้ามา คำสั่ง inspect() คือสำรวจดูว่าแต่ละตะกร้ามีสินค้าอะไรบ้าง และลงพล็อตกราฟสินค้าที่ถูกซื้อบ่อยที่สุด 10 อันดับออกมา

คร่าวๆ เราจะเห็นว่า 🍌 Banana ขายดีเวอร์ สินค้าส่วนใหญ่เป็นอาหารสดกลุ่มผักผลไม้ นั่นเอง

การสร้าง Association Rules ด้วย Apriori

เราจะหากฎความสัมพันธ์โดยใช้ฟังก์ชัน apriori() เน้นการค้นหากฎที่มีสินค้าเพียง 2 ชิ้น (A => B หรือ B => A) ซึ่งหมายถึงการซื้อสินค้าคู่กันเพื่อตอบโจทย์การ “ขายคู่” ที่เราตั้งไว้ด้านบน

โดยเราต้องตั้งกฏประมาณนี้:

  • ค้นหากฎที่มีสินค้าเพียง 2 ชิ้น (A => B หรือ B => A) ซึ่งหมายถึงการซื้อสินค้าคู่กัน
  • supp (Support): กำหนดความถี่ขั้นต่ำ (เช่น 0.005 = 0.5% ของรายการทั้งหมด)
  • conf (Confidence): กำหนดความมั่นใจขั้นต่ำ (เช่น 0.3 = 30%)
  • maxlen: กำหนดจำนวนสินค้าสูงสุดในกฎ (maxlen=2 เพื่อให้ได้กฎแบบ “คู่”)

ซึ่งเดี๋ยว Support, Confidence จะอธิบายต่อ (หัวข้อ: ตัววัดประสิทธิภาพ Matrics) ด้านล่างครับ ใจเย็นๆ

#### การสร้าง Association Rules ด้วย Apriori ###
rules <- apriori(trans, 
                 parameter = list(supp = 0.005, conf = 0.3, maxlen = 2), 
                 control = list(verbose = FALSE))

อาจต้องลองปรับค่า ‘supp’ (Support), ‘conf’ (Confidence) ให้เหมาะสมกับขนาดข้อมูลของเรา หากได้กฎน้อยไป ให้ลดค่า, หากได้กฎเยอะไป ให้เพิ่มค่าดูได้ครับ

การค้นหาคู่สินค้าที่ดีที่สุด

เนื่องจากเราต้องการหา “คู่สินค้า” ที่มีการซื้อมากที่สุด เพื่อจัดโปรโมชั่นขายแพ็คคู่ จึงควรให้ความสำคัญกับ Support และ Lift ที่สูง

  • Support สูง: แสดงว่าเป็นคู่ที่ถูกซื้อบ่อยมากที่สุด
  • Lift สูง: แสดงว่าเป็นคู่ที่มีความสัมพันธ์กัน “เป็นพิเศษ” (ไม่ใช่แค่เป็นสินค้าขายดีทั่วไป)

มาอีกแล้วคำศัพท์ อธิบายด้านล่าง (หัวข้อ: ตัววัดประสิทธิภาพ Matrics) ครับ

#### การค้นหาคู่สินค้าที่ดีที่สุด ####
# จัดเรียงกฎตาม Lift สูงสุด และ Support สูงสุด
rules_sorted <- sort(rules, by = c("lift", "support"), decreasing = TRUE)

# กรองเฉพาะกฎที่มี Support และ Lift สูงๆ (อาจจะเลือก Top N)
top_rules <- inspect(head(rules_sorted, n = 10))
print(top_rules)

กฎที่ได้จะอยู่ในรูปแบบ {สินค้า A} => {สินค้า B} ซึ่งสินค้าคู่ที่น่าสนใจที่สุดคือสินค้าที่อยู่ใน 10 อันดับแรก โดยพิจารณาจาก:

  1. Lift: ควรมีค่า > 1 มากๆ (ยิ่งสูงยิ่งดี)
  2. Support: ควรมีค่าสูงพอสมควร (ความถี่ที่เกิดขึ้นจริง)
  3. Confidence: ควรมีค่าสูง (ถ้าซื้อ A โอกาสซื้อ B ก็สูง)

การตีความของคู่สินค้าอันดับแรก Organic Navel Orange 🍊(ส้มไร้เมล็ด) กับ Bag of Organic Bananas 🍌(กล้วยออแกนิค): ถ้าผลลัพธ์ออกมาเช่นนี้ หมายความว่า:

  • คู่สินค้า Organic Navel Orange (ส้มไร้เมล็ด) และ Bag of Organic Bananas (กล้วยออแกนิค) ถูกซื้อร่วมกันใน 0.5% ของรายการทั้งหมด — จาก Support 0.005091639
  • คนที่ซื้อ Organic Navel Orange (ส้มไร้เมล็ด) มีโอกาส 35% ที่จะซื้อ Bag of Organic Bananas (กล้วยออแกนิค) — จาก Confidence 0.3498965
  • การซื้อ Organic Navel Orange (ส้มไร้เมล็ด) เพิ่มโอกาสซื้อ Bag of Organic Bananas (กล้วยออแกนิค) ถึง 3 เท่า เมื่อเทียบกับการซื้อ Bag of Organic Bananas (กล้วยออแกนิค) ทั่วๆ ไป — จาก Lift 3.077822

🌵 คู่สินค้าที่มี Lift สูง, Support สูงพอสมควร คือคู่ที่เราควรนำไปจัดโปรโมชั่น “ซื้อคู่” ครับ 👍


ครั้งนี้แอดหา data sample อยู่นานมาก กว่าจะได้มาโค้ด หลักๆ คือสินค้าที่เขา mockup ไว้ใน Kaggle แอดอธิบายไม่ได้ว่ามันคืออะไร แฮะๆ จนมาจบที่ข้อมูลชุดนี้ หวังว่าคุณผู้อ่านที่แวะแมา จะทำความเข้าใจไปพร้อมๆ กับแอดนะครับ

[Optional] ตัววัดประสิทธิภาพ (Metrics)

Support

โดย Support (ความถี่หรือความบ่อย) 🛒 สัดส่วนของรายการซื้อสินค้า A และ B (คู่กัน) ต่อจำนวนรายการซื้อทั้งหมด นั่นคือมันวัดความถี่ในการปรากฏร่วมกันของสินค้า A และ B ในชุดข้อมูลของเราครับ

เนื่องจากค่า support จะเกิดขึ้นในทุกคู่สินค้า (A→B) ตามความถี่ของการซื้อสินค้า จึงต้องกำหนดขั้นต่ำ Minimum Support ที่เราสนใจ เพราะหากค่า support ยิ่งเยอะขึ้น เราก็จะยิ่งมั่นใจว่าจะเกิดเหตุการณ์ซื้อแบบเดียวกันนี้บ่อยขึ้น

จึงต้องมีการตั้ง Minimum Support ช่วยให้เรา กรองกฎที่เกิดขึ้นน้อยครั้ง ซึ่งมักเป็นสัญญาณรบกวน (Noise) ออกไป (จากที่เราใส่ parameter = list(supp = 0.005) ในกฏนั่นเอง) และเน้นเฉพาะ ชุดสินค้าที่ถูกซื้อร่วมกันบ่อย จนเป็นที่น่าสนใจในทางธุรกิจ

ที่นี้ R คำนวณตัวเลขยังไง ก่อนที่มันจะคำนวนมาให้เรา

# [optional] 
# หาจำนวนรายการซื้อทั้งหมด
length(trans_df$order_id)

# สร้างรายการสินค้าตามความถี่ทั้งหมด และดึงเฉพาะ supp >= 0.005
frequent_itemsets <- apriori(trans, 
                             parameter = list(supp = 0.005, target = "frequent itemsets"))

# กรองหา Itemset ที่ประกอบด้วย (A) และ (B) assume ว่าเราจะคำนวณ 2 ไอเทมนี้
a <- "Organic Navel Orange" 
b <- "Bag of Organic Bananas" 
itemset_AB <- subset(frequent_itemsets, items %in% c( a, b ) & size(items) == 2)

# ดูผลลัพธ์: จะได้ Support Count(A U B) + แล้วมองหารายการที่สนใจ
inspect(itemset_AB)

Confidence

ทีนี้แค่ Support อย่างเดียวอาจยังไม่พอ เพราะเราต้องการทราบว่า “ในบรรดาลูกค้าที่ซื้อ A ทั้งหมด มีกี่เปอร์เซ็นต์ที่ซื้อ B ตามมาด้วย?” และสิ่งนี้คือ Confidence (ความเชื่อมั่น) จะเข้ามาตอบโจทย์นี้ครับ

Confidence คือการวัดความน่าจะเป็นที่จะเกิดเหตุการณ์ B เมื่อรู้ว่าเหตุการณ์ A เกิดขึ้นแล้ว (กฎ A→B) มีความน่าเชื่อถือแค่ไหน จากสูตร

ที่นี้ R คำนวนยังไงกัน

# หลังจากคำนวน frequent_itemsets แล้ว
a <- "Organic Navel Orange" 
b <- "Bag of Organic Bananas" 

itemset_AB <- subset(frequent_itemsets, items %in% c( a, b ) & size(items) == 2
inspect(itemset_AB)

# กรองหา Itemset ที่ประกอบด้วย a และมีขนาดเท่ากับ 1
itemset_A <- subset(frequent_itemsets, items %in% c( a ) & size(items) == 1)
inspect(itemset_A) 

แม้ค่า Support และ Confidence ของกฎ A→B นั้นมี ข้อจำกัด อยู่บ้าง เพียงแค่ว่าการซื้อ A แล้วจะมีการซื้อ B ต่อ แต่แล้วเราจะรู้ได้อย่างไรว่ามีความมันมีความสัมพันธ์กันจริงๆ ไม่ใช่เหตุการณ์สินค้านั้นลดราคาพร้อมกันแล้วเกิดการซื้อขึ้น สถิติตัวสุดท้ายที่เข้ามาแก้ไขปัญหานี้คือ Lift

ข้อจำกัดของ Confidence คือมันอาจสูงได้เพียงเพราะสินค้า B (เช่น นม หรือไข่) ถูกซื้อบ่อยมากอยู่แล้ว โดยไม่เกี่ยวกับว่าลูกค้าซื้อ A หรือไม่เลย นั่นคือเราวัดโดยไม่ได้เทียบกับ ความถี่พื้นฐาน ของสินค้า B

เราจึงต้องมีการแยกแยะระหว่าง ความสัมพันธ์ที่แท้จริง (Association) กับ เหตุการณ์บังเอิญ (Coincidence)

Lift

Lift คือการวัดว่าโอกาสที่ลูกค้าจะซื้อ B เพิ่มขึ้น กี่เท่า เมื่อลูกค้ารู้ว่าซื้อ A ไปแล้ว โดยเทียบกับโอกาสที่ลูกค้าจะซื้อ B อยู่แล้วตามปกติ (Baseline Probability) โดย Lift จะวัดว่ากฎ A→B มีความแข็งแกร่งและน่าสนใจ เกินกว่าการเกิดร่วมกันแบบสุ่ม หรือไม่

ซึ่งมีสูตรคำนวณคือ

โดยทั่วไป Lift ที่สูง (สูงกว่า 1) คือสิ่งที่เรามองหา แต่มาลองดูความหมายอื่นด้วยนะครับ

ค่า Liftการตีความ (ความสัมพันธ์)ความหมาย
เท่ากับ 1เป็นอิสระต่อกัน (Independent)การซื้อ A ไม่ได้มีผลต่อโอกาสในการซื้อ B กฎไม่มีความน่าสนใจ
มากกว่า 1ความสัมพันธ์เชิงบวก (Positive Association)การซื้อ A เพิ่ม โอกาสในการซื้อ B อย่างชัดเจน นี่คือ กฎที่แข็งแกร่งและน่าสนใจ
น้อยกว่า 1ความสัมพันธ์เชิงลบ (Negative Association)การซื้อ A ลด โอกาสในการซื้อ B สินค้าอาจเป็น สินค้าทดแทนกัน (Substitutes)

แล้ว R หาค่า lift มาด้วยวิธีการ:

# เราได้หา Confidence(A→B) ไปแล้วก่อนหน้านี้น้า: 0.349896
b <- "Bag of Organic Bananas" 
itemset_B <- subset(frequent_itemsets, items %in% c( b ) & size(items) == 1)
inspect(itemset_B) 



Leave a Reply

Your email address will not be published. Required fields are marked *

Search

About

Feasible เว็บไซต์ที่นำเสนออาชีพปัจจุบันที่เรา (เจ้าของเว็บ) กำลังทำ ไม่ว่าจะเป็น นักวิเคราะห์ข้อมูล นักเรียน นักอ่าน นักฟาร์ม และอีกหลากหลายมุมมอง เรียกได้ว่าเป็น ‘แกงโฮะ’ เลยล่ะ ฮ่าๆๆ ติดตาม Content ที่จะทำออกมาได้เรื่อยๆ นะครับ ขอบคุณที่เข้ามาเยี่ยมกัน 😁✌️

Social Icons