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.
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ọ.
Để 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
)
)
Việc phát triển một giả thuyết là cần thiết vì giả thuyết này sẽ hướng dẫn các quyết định của chúng ta về cách xây dựng dữ liệu theo cách phân khúc khách hàng. Đối với các đơn đặt hàng của Cannondale, giả thuyết của chúng ta là các cửa hàng xe đạp mua các mẫu xe đạp Cannondale dựa trên các đặc điểm như Xe đạp leo núi hoặc Xe đạp đường trường và mức giá (cao/cao cấp hoặc thấp/giá cả phải chăng). Mặc dù chúng ta sẽ sử dụng mẫu xe đạp để phân nhóm, nhưng các đặc điểm của mẫu xe đạp (ví dụ: giá, chủng loại, v.v.) sẽ được sử dụng để đánh giá sở thích của các nhóm khách hàng (xem thêm về điều này sau đó).
Để bắt đầu, chúng ta sẽ cần một đơn vị đo lường để tập hợp lại. Chúng ta có thể chọn số lượng đã mua hoặc tổng giá trị mua hàng. Chúng ta sẽ chọn số lượng đã mua vì tổng giá trị có thể bị sai lệch bởi đơn giá xe đạp. Ví dụ: một chiếc xe đạp cao cấp có thể được bán với giá cao hơn gấp 10 lần so với một chiếc xe đạp giá cả phải chăng, điều này có thể che giấu thói quen mua số lượng.
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
)
)
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.
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 = 4 và maxClust =
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")
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"
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:
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 |
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)
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ả:
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.
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.
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.
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.