Introduction

(Giới thiệu)

Trong chủ đề này, chúng ta sẽ sử dụng thuật toán phân cụm k-mean trong R để phân chia khách hàng thành các nhóm riêng biệt dựa trên thói quen mua hàng. Phân cụm k-mean là một kỹ thuật học máy không giám sát, có nghĩa là chúng ta không cần có mục tiêu để phân cụm. Tất cả những gì chúng ta cần làm là định dạng dữ liệu theo cách mà thuật toán có thể xử lý và để nó xác định các phân khúc hoặc cụm khách hàng. Điều này làm cho việc phân cụm k-mean trở nên tuyệt vời cho phân tích thăm dò cũng như điểm xuất phát để phân tích chi tiết hơn. Chúng ta sẽ xem qua một ví dụ có liên quan bằng cách sử dụng tập dữ liệu xe đạp Cannondale từ kho lưu trữ GitHub của dự án orderSimulatoR.

Table of Contents

(Nội dung chính)
  • Cách thức hoạt động của k-Mean (How k-Means Works)
  • Kết nối dữ liệu (Getting Started)
  • Phát triển giả thuyết về xu hướng khách hàng (Developing a Hypothesis for Customer Trends)
  • Thao tác với khung dữ liệu (Manipulating the Data Frame)
  • Phân cụm khách hàng bằng k-Means (k-Means Clustering)
    • Chạy thuật toán k-Means (Running the k-Means Algorithm)
    • Phân cụm khách hàng vào mỗi phân khúc (Which Customers are in Each Segment?)
    • Xác định sở thích, đặc điểm của các phân khúc khách hàng (Determining the Preferences of the Customer Segments)
    • Đánh giá kết quả (Reviewing Results)
  • Tổng kết lại (Recap)
  • Tham khảo (Further Reading)
  • Các cập nhật mới (Updates)

How K-Means Works

(Cách thức hoạt động của K-Means)

Thuật toán phân cụm k-means hoạt động bằng cách tìm các nhóm giống nhau dựa trên khoảng cách Euclide, thước đo khoảng cách hoặc độ tương tự. Chúng ta chọn k-groups để phân cụm, và thuật toán tìm ra trọng tâm tốt nhất cho k-groups. Sau đó, chúng ta có thể sử dụng các nhóm đó để xác định các yếu tố mà các khách hàng trong nhóm có liên quan. Đối với khách hàng, đây sẽ là sở thích mua hàng của họ.

Getting Started

(Kết nối dữ liệu)

Để bắt đầu, chúng ta cần một số đơn đặt hàng để đánh giá. Chúng ta sẽ sử dụng bikes data set, có thể truy xuất tại đây. Trước tiên, chúng tôi sẽ tải dữ liệu bằng cách sử dụng gói xlsx để đọc tệp Excel.

library(xlsx)
customers <- read.xlsx("./data/bikeshops.xlsx", sheetIndex = 1)
products <- read.xlsx("./data/bikes.xlsx", sheetIndex = 1) 
orders <- read.xlsx("./data/orders.xlsx", sheetIndex = 1) 

Tiếp theo, chúng ta sẽ chuyển dữ liệu sang định dạng có thể sử dụng được, điển hình cho truy vấn SQL từ cơ sở dữ liệu ERP. Đoạn mã sau hợp nhất các khung dữ liệu khách hàng, sản phẩm và đơn đặt hàng bằng cách sử dụng gói dplyr.

# Combine orders, customers, and products data frames --------------------------
library(dplyr)

orders.extended <- merge(orders, customers, by.x = "customer.id", by.y="bikeshop.id")
orders.extended <- merge(orders.extended, products, by.x = "product.id", by.y = "bike.id")

orders.extended <- orders.extended %>%
  mutate(price.extended = price * quantity) %>%
  select(order.date, order.id, order.line, bikeshop.name, model,
         quantity, price, price.extended, category1, category2, frame) %>%
  arrange(order.id, order.line)

# Preview the data
orders.extended %>%
  DT::datatable(
    extensions = 'FixedColumns',
    options = list(
      dom = 't',
      scrollX = TRUE,
      fixedColumns = TRUE,
      scrollY = 420,
      scroller = TRUE
    )
  )

Manipulating the Data Frame

(Khai phá dữ liệu)

Tiếp theo, chúng ta cần một kế hoạch tấn công khai phá dữ liệu để triển khai phân cụm trên dữ liệu của mình. Chúng ta sẽ sử dụng giả thuyết của mình để hướng dẫn. Trước tiên, chúng ta cần chuyển khung dữ liệu sang định dạng có lợi cho việc phân cụm các mẫu xe đạp theo mã khách hàng. Thứ hai, chúng ta sẽ cần điều chỉnh giá thành các biến phân loại đại diện cho cao/cao cấp và thấp/giá cả phải chăng. Cuối cùng, chúng ta sẽ cần chia tỷ lệ số lượng mẫu xe đạp mà khách hàng đã mua để thuật toán k-mean tính trọng số mua hàng của từng khách hàng một cách đồng đều .

Đầu tiên, chúng ta sẽ giải quyết việc định dạng khung dữ liệu để phân cụm. Chúng ta cần phân bổ khách hàng theo số lượng mẫu xe đạp đã mua.

# Group by model & model features, summarize by quantity purchased -------------
library(tidyr)  # Needed for spread function

customerTrends <- orders.extended %>%
        group_by(bikeshop.name, model, category1, category2, frame, price) %>%
        summarise(total.qty = sum(quantity)) %>%
        spread(bikeshop.name, total.qty)

customerTrends[is.na(customerTrends)] <- 0  # Remove NA's

Tiếp theo, chúng ta cần chuyển đổi đơn giá thành các biến cao/thấp được phân loại. Một cách để thực hiện việc này là sử dụng hàm cut2() từ gói Hmisc. Chúng ta sẽ phân đoạn giá thành cao/thấp theo giá trung bình. Chọn g = 2 sẽ chia đơn giá thành hai nửa bằng cách sử dụng giá trị trung vị làm điểm phân chia.

# Convert price to binary high/low category ------------------------------------
library(Hmisc)  # Needed for cut2 function
customerTrends$price <- cut2(customerTrends$price, g=2)

Cuối cùng, chúng ta cần chia tỷ lệ số lượng quan sát. Số lượng không được điều chỉnh gây ra vấn đề cho thuật toán k-mean. Một số khách hàng lớn hơn những khách hàng khác nghĩa là họ mua số lượng lớn hơn. May mắn thay, chúng ta có thể giải quyết vấn đề này bằng cách chuyển đổi số lượng đặt hàng của khách hàng thành tỷ lệ của tổng số xe đạp mà khách hàng đã mua. Hàm ma trận prop.table() cung cấp một cách thuận tiện để thực hiện việc này. Một cách khác là sử dụng hàm scale() để chuẩn hóa dữ liệu. Tuy nhiên, điều này khó hiểu hơn định dạng tỷ lệ.

# Convert customer purchase quantity to percentage of total quantity -----------
customerTrends.mat <- as.matrix(customerTrends[,-(1:5)])  # Drop first five columns
customerTrends.mat <- prop.table(customerTrends.mat, margin = 2)  # column-wise pct
customerTrends <- bind_cols(customerTrends[,1:5], as.data.frame(customerTrends.mat))

Khung dữ liệu cuối cùng (Top 05 được hiển thị bên dưới) hiện đã sẵn sàng để phân cụm.

# View data post manipulation --------------------------------------------------
customerTrends %>% 
  DT::datatable(
    extensions = 'FixedColumns',
    options = list(
      dom = 't',
      scrollX = TRUE,
      fixedColumns = TRUE,
      scrollY = 420,
      scroller = TRUE
    )
  )

K-Means Clustering

(Phân nhóm khách hàng bằng K-Means)

Bây giờ chúng ta đã sẵn sàng thực hiện phân cụm k-mean để phân đoạn cơ sở khách hàng của mình. Hãy coi các cụm như các nhóm trong cơ sở khách hàng. Trước khi bắt đầu, chúng ta cần chọn số lượng nhóm khách hàng, k, sẽ được tính toán xác định. Cách tốt nhất để làm điều này là suy nghĩ về cơ sở khách hàng và giả thuyết của chúng ta. Chúng ta tin rằng có nhiều khả năng sẽ có ít nhất bốn nhóm khách hàng vì xe đạp leo núi và xe đạp đường trường cũng như sở thích cao cấp và bình dân. Chúng tôi cũng tin rằng có thể có nhiều hơn vì một số khách hàng có thể không quan tâm đến giá cả nhưng vẫn có thể thích một loại xe đạp cụ thể. Tuy nhiên, chúng ta sẽ giới hạn các cụm ở con số 08 vì nhiều khả năng sẽ phù hợp quá mức với các phân khúc.

Running the K-Means Algorithm

(Thực thi thuật toán K-Means)

Thực hiện các bước thực thi hàm như sau:

1.Chuyển đổi khung dữ liệu customerTrends thành kmeansDat.t. Mô hình và các tính năng bị loại bỏ nên chỉ còn lại các cột khách hàng. Khung dữ liệu được chuyển đổi để có khách hàng ở dạng hàng và mô hình ở dạng cột. Hàm kmeans() yêu cầu định dạng này.

2.Thực hiện hàm kmeans() để phân cụm các phân khúc khách hàng. Chúng ta đặt minClust = 4maxClust = 8. Từ giả thuyết của mình, chúng ta kỳ vọng sẽ có ít nhất bốn và nhiều nhất là sáu nhóm khách hàng. Điều này là do sở thích của khách hàng dự kiến sẽ thay đổi theo mức giá (cao/thấp) và loại xe (leo núi và xe đạp). Ngoài ra, có thể có các nhóm khác. Ngoài tám phân khúc, số lượng phân khúc có thể quá nhiều, không phù hợp.

3.Sử dụng hàm silhouette() để thu được độ rộng của hình bóng. Silhouette là một kỹ thuật phân cụm giúp xác thực các nhóm cụm tốt nhất. Hàm Silhouette() từ gói cụm cho phép chúng ta lấy chiều rộng trung bình của hình bóng, sẽ được sử dụng để xác định theo chương trình kích thước cụm tối ưu.

# Running the k-means algorithm -------------------------------------------------
library(cluster) # Needed for silhouette function

kmeansDat <- customerTrends[,-(1:5)]  # Extract only customer columns
kmeansDat.t <- t(kmeansDat)  # Get customers in rows and products in columns

# Setup for k-means loop 
km.out <- list()
sil.out <- list()
x <- vector()
y <- vector()
minClust <- 4      # Hypothesized minimum number of segments
maxClust <- 8      # Hypothesized maximum number of segments

# Compute k-means clustering over various clusters, k, from minClust to maxClust
for (centr in minClust:maxClust) {
        i <- centr-(minClust-1) # relevels start as 1, and increases with centr
        set.seed(11) # For reproducibility
        km.out[i] <- list(kmeans(kmeansDat.t, centers = centr, nstart = 50))
        sil.out[i] <- list(silhouette(km.out[[i]][[1]], dist(kmeansDat.t)))
        # Used for plotting silhouette average widths
        x[i] = centr  # value of k
        y[i] = summary(sil.out[[i]])[[4]]  # Silhouette average width
}

Tiếp theo, chúng ta vẽ đồ thị chiều rộng trung bình của hình bóng để lựa chọn các cụm. Cụm tốt nhất là cụm có chiều rộng trung bình của hình bóng lớn nhất, hóa ra là 5 cụm.

# Plot silhouette results to find best number of clusters; closer to 1 is better
library(ggplot2)
ggplot(data = data.frame(x, y), aes(x, y)) + 
  geom_point(size=3) + 
  geom_line() +
  xlab("Number of Cluster Centers") +
  ylab("Silhouette Average Width") +
  ggtitle("Silhouette Average Width as Cluster Center Varies")

Which Customers are in Each Segment?

(Phân cụm khách hàng vào mỗi phân khúc)

Bây giờ chúng ta đã phân cụm dữ liệu, chúng ta có thể kiểm tra các nhóm để tìm ra khách hàng nào được nhóm lại với nhau. Mã bên dưới nhóm tên khách hàng theo cụm từ X1 đến X5.

# Get customer names that are in each segment ----------------------------------

# Get attributes of optimal k-means output
maxSilRow <- which.max(y)          # Row number of max silhouette value
optimalClusters <- x[maxSilRow]    # Number of clusters
km.out.best <- km.out[[maxSilRow]] # k-means output of best cluster

# Create list of customer names for each cluster
clusterNames <- list()
clusterList <- list()
for (clustr in 1:optimalClusters) {
  clusterNames[clustr] <- paste0("X", clustr)
  clusterList[clustr] <- list(
    names(
        km.out.best$cluster[km.out.best$cluster == clustr]
        )
    )
}
names(clusterList) <- clusterNames

print(clusterList)
## $X1
## [1] "Philadelphia Bike Shop" "San Antonio Bike Shop" 
## 
## $X2
## [1] "Cincinnati Speed"          "Columbus Race Equipment"  
## [3] "Las Vegas Cycles"          "Louisville Race Equipment"
## [5] "San Francisco Cruisers"    "Wichita Speed"            
## 
## $X3
## [1] "Ann Arbor Speed"              "Austin Cruisers"             
## [3] "Indianapolis Velocipedes"     "Miami Race Equipment"        
## [5] "Nashville Cruisers"           "New Orleans Velocipedes"     
## [7] "Oklahoma City Race Equipment" "Seattle Race Equipment"      
## 
## $X4
## [1] "Ithaca Mountain Climbers"     "Pittsburgh Mountain Machines"
## [3] "Tampa 29ers"                 
## 
## $X5
##  [1] "Albuquerque Cycles"    "Dallas Cycles"         "Denver Bike Shop"     
##  [4] "Detroit Cycles"        "Kansas City 29ers"     "Los Angeles Cycles"   
##  [7] "Minneapolis Bike Shop" "New York Cycles"       "Portland Bi-peds"     
## [10] "Providence Bi-peds"    "Phoenix Bi-peds"

Determining the Preferences of the Customer Segments

(Xác định sở thích, đặc điểm của các phân khúc khách hàng)

Cách dễ nhất để xác định sở thích của khách hàng là kiểm tra các yếu tố liên quan đến mẫu xe (ví dụ: mức giá, chủng loại xe đạp, v.v.). Các thuật toán nâng cao để phân loại các nhóm có thể được sử dụng nếu có nhiều yếu tố, nhưng thông thường điều này không cần thiết vì các xu hướng có xu hướng thay đổi đột ngột. Mã bên dưới gắn trọng tâm k-means vào các mẫu và danh mục xe đạp để kiểm tra xu hướng.

# Combine cluster centroids with bike models for feature inspection ------------
custSegmentCntrs <- t(km.out.best$centers)  # Get centroids for groups
colnames(custSegmentCntrs) <- make.names(colnames(custSegmentCntrs))
customerTrends.clustered <- bind_cols(customerTrends[,1:5], as.data.frame(custSegmentCntrs))

Bây giờ, chuyển sang kiểm tra từng cụm:

Cluster 1

Chúng ta sẽ sắp xếp theo mười mẫu xe đạp hàng đầu của cụm 1 theo thứ tự giảm dần. Chúng ta có thể nhanh chóng nhận thấy rằng 10 mẫu xe được mua nhiều nhất chủ yếu là xe cao cấp và xe leo núi. Tất cả trừ một mẫu đều có khung carbon.

# Arrange top 10 bike models by cluster in descending order --------------------
attach(customerTrends.clustered)  # Allows ordering by column name
knitr::kable(head(customerTrends.clustered[order(-X1), c(1:5, 6)], 10))
model category1 category2 frame price X1
Slice Ultegra Road Triathalon Carbon [ 415, 3500) 0.0554531
Trigger Carbon 4 Mountain Over Mountain Carbon [ 415, 3500) 0.0304147
CAAD12 105 Road Elite Road Aluminum [ 415, 3500) 0.0270792
Beast of the East 3 Mountain Trail Aluminum [ 415, 3500) 0.0263331
Trail 1 Mountain Sport Aluminum [ 415, 3500) 0.0249397
Bad Habit 1 Mountain Trail Aluminum [ 415, 3500) 0.0229976
F-Si Carbon 4 Mountain Cross Country Race Carbon [ 415, 3500) 0.0224490
CAAD12 Disc 105 Road Elite Road Aluminum [ 415, 3500) 0.0209568
Synapse Disc 105 Road Endurance Road Aluminum [ 415, 3500) 0.0209568
Trail 2 Mountain Sport Aluminum [ 415, 3500) 0.0203094
Cluster 2

Tiếp theo, chúng ta sẽ kiểm tra cụm 2. Chúng ta có thể thấy rằng các mẫu hàng đầu đều là mẫu cấp thấp/giá cả phải chăng. Có sự kết hợp giữa đường và núi cho loại cơ bản cũng như sự kết hợp giữa vật liệu khung.

# Arrange top 10 bike models by cluster in descending order
knitr::kable(head(customerTrends.clustered[order(-X2), c(1:5, 7)], 10))
model category1 category2 frame price X2
Synapse Hi-Mod Disc Red Road Endurance Road Carbon [3500,12790] 0.0246747
Slice Hi-Mod Black Inc. Road Triathalon Carbon [3500,12790] 0.0234936
Supersix Evo Hi-Mod Dura Ace 1 Road Elite Road Carbon [3500,12790] 0.0230529
Slice Hi-Mod Dura Ace D12 Road Triathalon Carbon [3500,12790] 0.0229613
Synapse Hi-Mod Dura Ace Road Endurance Road Carbon [3500,12790] 0.0216716
CAAD12 Red Road Elite Road Aluminum [ 415, 3500) 0.0211963
Synapse Carbon Disc Ultegra Road Endurance Road Carbon [3500,12790] 0.0202393
Supersix Evo Ultegra 3 Road Elite Road Carbon [ 415, 3500) 0.0201873
Supersix Evo Hi-Mod Utegra Road Elite Road Carbon [3500,12790] 0.0198191
Synapse Hi-Mod Disc Black Inc. Road Endurance Road Carbon [3500,12790] 0.0197553
detach(customerTrends.clustered)
Cluster 3, 4 & 5

Kiểm tra cụm 3, 4 và 5 cho kết quả thú vị. Để cho ngắn gọn, chúng tôi sẽ không hiển thị các bảng. Đây là kết quả:

  • Cụm 3: Có xu hướng ưa chuộng xe đạp đường trường có giá thành thấp.
  • Cụm 4: Rất giống Cụm 2 với phần lớn xe đạp ở phân khúc giá rẻ.
  • Cụm 5: Có xu hướng ám chỉ xe đạp đường trường thuộc phân khúc cao cấp.

Reviewing Results

(Đánh giá kết quả)

Sau khi phân cụm xong, ta nên lùi lại một bước và xem lại nội dung của thuật toán. Để phân tích, chúng ta nhận thấy xu hướng rõ ràng của bốn trong số năm nhóm, nhưng hai nhóm (cụm 2 và 4) rất giống nhau. Vì lý do này, có thể hợp lý khi kết hợp hai nhóm này hoặc chuyển từ kết quả k = 5 sang kết quả k = 4.

Recap

(Tổng kết lại)

Quá trình phân khúc khách hàng có thể được thực hiện bằng nhiều thuật toán phân cụm khác nhau. Trong bài phân tích này, chúng ta tập trung vào phân cụm k-mean trong R. Mặc dù thuật toán khá đơn giản để triển khai, nhưng một nửa cuộc chiến là đưa dữ liệu về định dạng chính xác và diễn giải kết quả. Chúng ta đã tiến hành định dạng dữ liệu thứ tự, chạy hàm kmeans() để phân cụm dữ liệu với một số cụm k giả định, sử dụng silhouette() từ gói cụm để xác định số lượng k cụm tối ưu và diễn giải kết quả bằng cách kiểm tra k-means là trọng tâm.

Further Reading

(Tham khảo)

1.Data Smart by John Foreman: Chapter 2 covers k-medians clustering and silhouette analysis, which is very similar to the approach I use here.

Updates

(Nội dung cập nhật)

1.August 28, 2016: There was an issue with the set.seed() being outside of the for-loop in the k-means algorithm preventing the kmeans output from being completely reproducible. The set.seed() was moved inside the for-loop.

2.August 16, 2017: There is an issue with cbind() from base R that resulted in data frames binding incorrectly. The bind_cols() function from dplyr is used instead.