군집화 실습 - 고객 세그먼테이션(Customer Segmentation)
고객 세그먼테이션이란?
- 다양한 기준으로 고객을 분류하는 기법을 지칭한다. CRM(고객 관계 관리)나 마케팅의 중요 기반 요소다.
- 고객 분류가 사용되는 대부분의 비즈니스가 상품 판매에 중점을 두고 있기 때문에 어떤 상품을 얼마나 많은 비용을 써서 얼마나 자주 사용하는가에 기반한 정보로 분류하는 것이 보통이다.
- 고객 세그먼테이션의 주요 목표는 타겟 마케팅이다. 고객을 여러 특성에 맞게 세분화해서 그 유형에 따라 맞춤형 마케팅이나 서비스를 제공하는 것.
- 어떤 요소를 기반으로 군집화할 것인가를 결정하는 것이 중요한데, 이번엔 기본적 고객 분석 요소안 RFM 기법을 이용해보자
- Recency: 가장 최근 상품 구입일에서 오늘까지의 기간
- Frequency: 상품 구매 횟수
- Monetary Value: 총 구매 금액
데이터셋 로딩과 데이터 클렌징
import pandas as pd
import datetime
import math
import numpy as np
import matplotlib.pyplot as plt
retail_df = pd.read_excel('/content/Online Retail.xlsx')
retail_df.head(3)
InvoiceNo | StockCode | Description | Quantity | InvoiceDate | UnitPrice | CustomerID | Country | |
---|---|---|---|---|---|---|---|---|
0 | 536365 | 85123A | WHITE HANGING HEART T-LIGHT HOLDER | 6 | 2010-12-01 08:26:00 | 2.55 | 17850.0 | United Kingdom |
1 | 536365 | 71053 | WHITE METAL LANTERN | 6 | 2010-12-01 08:26:00 | 3.39 | 17850.0 | United Kingdom |
2 | 536365 | 84406B | CREAM CUPID HEARTS COAT HANGER | 8 | 2010-12-01 08:26:00 | 2.75 | 17850.0 | United Kingdom |
retail_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 541909 entries, 0 to 541908
Data columns (total 8 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 InvoiceNo 541909 non-null object
1 StockCode 541909 non-null object
2 Description 540455 non-null object
3 Quantity 541909 non-null int64
4 InvoiceDate 541909 non-null datetime64[ns]
5 UnitPrice 541909 non-null float64
6 CustomerID 406829 non-null float64
7 Country 541909 non-null object
dtypes: datetime64[ns](1), float64(2), int64(1), object(4)
memory usage: 33.1+ MB
- CustomerID의 null값이 매우 많음
- → 고객 식별 번호가 없는 데이터는 필요가 없기 때문에 삭제
- 오류 데이터 삭제, Quantity나 UnitPrice가 0보다 작은 경우.→ 이 데이터도 모두 삭제
- → Quantity가 0인 경우는 반환을 뜻하는 것으로, 이 경우 InvoiceNo의 앞자리가 C로 시작한다.
retail_df = retail_df[retail_df['Quantity'] > 0]
retail_df = retail_df[retail_df['UnitPrice'] > 0]
retail_df = retail_df[retail_df['CustomerID'].notnull()]
print(retail_df.shape)
retail_df.isnull().sum()
(397884, 8)
InvoiceNo 0
StockCode 0
Description 0
Quantity 0
InvoiceDate 0
UnitPrice 0
CustomerID 0
Country 0
dtype: int64
retail_df['Country'].value_counts()[:5]
United Kingdom 354321
Germany 9040
France 8341
EIRE 7236
Spain 2484
Name: Country, dtype: int64
다른 국가의 데이터는 모두 제외하자
retail_df = retail_df[retail_df['Country'] == 'United Kingdom']
print(retail_df.shape)
(354321, 8)
RFM 기반 데이터 가공
#주문 금액 데이터를 만들기 위해 'UnitPrice'와 'Quantity'를 곱하기
#CustomerID도 편리하게 식별하기 위해 int형으로 바꿔주기
retail_df['sale_amount'] = retail_df['UnitPrice'] * retail_df['Quantity']
retail_df['CustomerID'] = retail_df['CustomerID'].astype(int)
개인 고객과 소매점의 주문이 섞여있기 때문이다.
#주문 건수, 주문 금액 top5 고객 추출
print(retail_df['CustomerID'].value_counts().head(5))
print(retail_df.groupby('CustomerID')['sale_amount'].sum().sort_values(ascending=False)[:5])
17841 7847
14096 5111
12748 4595
14606 2700
15311 2379
Name: CustomerID, dtype: int64
CustomerID
18102 259657.30
17450 194550.79
16446 168472.50
17511 91062.38
16029 81024.84
Name: sale_amount, dtype: float64
retail_df.groupby(['InvoiceNo', 'StockCode'])['InvoiceNo'].count().mean()
1.028702077315023
주문번호+상품코드 기준의 데이터를 고객 기준의 RFM 데이터로 변경하자
이를 위해서 주문번호 기준의 데이터를 개별 CustomerID 기준의 데이터로 groupby하여 새로운 데이터프레임을 만들어야 한다.
#데이터프레임의 groupby에서 mutiple 연산을 가능하게 하기 위해 agg() 활용
#Recency는 InvoiceDate 칼럼의 Max()에서 데이터 가공
#Frequency는 InvoiceNo 칼럼의 count()
#Monetary Value는 sale_amount 칼럼의 sum()
aggregations = {
'InvoiceDate' : 'max',
'InvoiceNo' : 'count',
'sale_amount' : 'sum'}
cust_df = retail_df.groupby('CustomerID').agg(aggregations)
#groupby된 결과 칼럼값을 Recency, Frequency, Monetary로 변경
cust_df = cust_df.rename(columns= {'InvoiceDate' : 'Recency',
'InvoiceNo' : 'Frequency',
'sale_amount' : 'Monetary'})
cust_df = cust_df.reset_index()
cust_df.head(3)
CustomerID | Recency | Frequency | Monetary | |
---|---|---|---|---|
0 | 12346 | 2011-01-18 10:01:00 | 1 | 77183.60 |
1 | 12747 | 2011-12-07 14:34:00 | 103 | 4196.01 |
2 | 12748 | 2011-12-09 12:20:00 | 4595 | 33719.73 |
2010년 12월 1일 ~ 2011년 12월 9일까지의 데이터이므로 오늘 날짜는 2011년 12월 10일을 현재 날짜로 간주하고 일자(days) 데이터만 추출하여 생성하자
import datetime as dt
cust_df['Recency'] = dt.datetime(2011,12,10) - cust_df['Recency']
cust_df['Recency'] = cust_df['Recency'].apply(lambda x: x.days+1)
print('cust_df 로우와 컬럼 건수는 ',cust_df.shape)
cust_df.head(3)
cust_df 로우와 컬럼 건수는 (3920, 4)
CustomerID | Recency | Frequency | Monetary | |
---|---|---|---|---|
0 | 12346 | 326 | 1 | 77183.60 |
1 | 12747 | 3 | 103 | 4196.01 |
2 | 12748 | 1 | 4595 | 33719.73 |
RFM 기반 고객 세그먼테이션
- 앞서 말했듯 소매업과 개인 고객 주문은 주문 횟수와 금액에서 큰 차이를 나타낸다. 이로 인해 왜곡된 데이터 분포도를 가지게 되어 군집화가 한쪽 군집에만 쏠리는 현상이 발생하게 된다.
- 먼저 칼럼별 히스토그램을 확인하고 왜곡된 데이터 분포도에서 군집화를 수행하면 어떻게 되는지 살펴보자
fig, (ax1, ax2, ax3) = plt.subplots(figsize=(12,4), nrows=1, ncols=3)
ax1.set_title('Recency Histogram')
ax1.hist(cust_df['Recency'])
ax2.set_title('Frequency Histogram')
ax2.hist(cust_df['Frequency'])
ax3.set_title('Monetary Histogram')
ax3.hist(cust_df['Monetary'])
plt.show()
특히, Frequency, Monetary의 경우 특정 범위에 값이 몰려 있는 매우 심한 왜곡을 보인다.
cust_df[['Recency', 'Frequency', 'Monetary']].describe()
Recency | Frequency | Monetary | |
---|---|---|---|
count | 3920.000000 | 3920.000000 | 3920.000000 |
mean | 92.742092 | 90.388010 | 1864.385601 |
std | 99.533485 | 217.808385 | 7482.817477 |
min | 1.000000 | 1.000000 | 3.750000 |
25% | 18.000000 | 17.000000 | 300.280000 |
50% | 51.000000 | 41.000000 | 652.280000 |
75% | 143.000000 | 99.250000 | 1576.585000 |
max | 374.000000 | 7847.000000 | 259657.300000 |
먼저 데이터셋에 스케일링을 하여 평균과 표준편차를 재조정한 뒤 k-평균을 적용해보자
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score, silhouette_samples
X_features = cust_df[['Recency', 'Frequency', 'Monetary']].values
X_features_scaled = StandardScaler().fit_transform(X_features)
kmeans = KMeans(n_clusters=3, random_state=0)
labels = kmeans.fit_predict(X_features_scaled)
cust_df['cluster_label'] = labels
print('실루엣 스코어: {0:.3f}'.format(silhouette_score(X_features_scaled, labels)))
실루엣 스코어: 0.592
하지만 각 군집별 실루엣 계수값도 확인해야 한다.
군집 개수를 2~5개로 변화시키면서 개별 군집의 실루엣 계수값과 데이터 구성을 살펴보자
### 여러개의 클러스터링 갯수를 List로 입력 받아 각각의 실루엣 계수를 면적으로 시각화한 함수 작성
def visualize_silhouette(cluster_lists, X_features):
from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_samples, silhouette_score
import matplotlib.pyplot as plt
import matplotlib.cm as cm
import math
# 입력값으로 클러스터링 갯수들을 리스트로 받아서, 각 갯수별로 클러스터링을 적용하고 실루엣 개수를 구함
n_cols = len(cluster_lists)
# plt.subplots()으로 리스트에 기재된 클러스터링 만큼의 sub figures를 가지는 axs 생성
fig, axs = plt.subplots(figsize=(4*n_cols, 4), nrows=1, ncols=n_cols)
# 리스트에 기재된 클러스터링 갯수들을 차례로 iteration 수행하면서 실루엣 개수 시각화
for ind, n_cluster in enumerate(cluster_lists):
# KMeans 클러스터링 수행하고, 실루엣 스코어와 개별 데이터의 실루엣 값 계산.
clusterer = KMeans(n_clusters = n_cluster, max_iter=500, random_state=0)
cluster_labels = clusterer.fit_predict(X_features)
sil_avg = silhouette_score(X_features, cluster_labels)
sil_values = silhouette_samples(X_features, cluster_labels)
y_lower = 10
axs[ind].set_title('Number of Cluster : '+ str(n_cluster)+'\n' \
'Silhouette Score :' + str(round(sil_avg,3)) )
axs[ind].set_xlabel("The silhouette coefficient values")
axs[ind].set_ylabel("Cluster label")
axs[ind].set_xlim([-0.1, 1])
axs[ind].set_ylim([0, len(X_features) + (n_cluster + 1) * 10])
axs[ind].set_yticks([]) # Clear the yaxis labels / ticks
axs[ind].set_xticks([0, 0.2, 0.4, 0.6, 0.8, 1])
# 클러스터링 갯수별로 fill_betweenx( )형태의 막대 그래프 표현.
for i in range(n_cluster):
ith_cluster_sil_values = sil_values[cluster_labels==i]
ith_cluster_sil_values.sort()
size_cluster_i = ith_cluster_sil_values.shape[0]
y_upper = y_lower + size_cluster_i
color = cm.nipy_spectral(float(i) / n_cluster)
axs[ind].fill_betweenx(np.arange(y_lower, y_upper), 0, ith_cluster_sil_values, \
facecolor=color, edgecolor=color, alpha=0.7)
axs[ind].text(-0.05, y_lower + 0.5 * size_cluster_i, str(i))
y_lower = y_upper + 10
axs[ind].axvline(x=sil_avg, color="red", linestyle="--")
### 여러개의 클러스터링 갯수를 List로 입력 받아 각각의 클러스터링 결과를 시각화
def visualize_kmeans_plot_multi(cluster_lists, X_features):
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
import pandas as pd
import numpy as np
# plt.subplots()으로 리스트에 기재된 클러스터링 만큼의 sub figures를 가지는 axs 생성
n_cols = len(cluster_lists)
fig, axs = plt.subplots(figsize=(4*n_cols, 4), nrows=1, ncols=n_cols)
# 입력 데이터의 FEATURE가 여러개일 경우 2차원 데이터 시각화가 어려우므로 PCA 변환하여 2차원 시각화
pca = PCA(n_components=2)
pca_transformed = pca.fit_transform(X_features)
dataframe = pd.DataFrame(pca_transformed, columns=['PCA1','PCA2'])
# 리스트에 기재된 클러스터링 갯수들을 차례로 iteration 수행하면서 KMeans 클러스터링 수행하고 시각화
for ind, n_cluster in enumerate(cluster_lists):
# KMeans 클러스터링으로 클러스터링 결과를 dataframe에 저장.
clusterer = KMeans(n_clusters = n_cluster, max_iter=500, random_state=0)
cluster_labels = clusterer.fit_predict(pca_transformed)
dataframe['cluster']=cluster_labels
unique_labels = np.unique(clusterer.labels_)
markers=['o', 's', '^', 'x', '*']
# 클러스터링 결과값 별로 scatter plot 으로 시각화
for label in unique_labels:
label_df = dataframe[dataframe['cluster']==label]
if label == -1:
cluster_legend = 'Noise'
else :
cluster_legend = 'Cluster '+str(label)
axs[ind].scatter(x=label_df['PCA1'], y=label_df['PCA2'], s=70,\
edgecolor='k', marker=markers[label], label=cluster_legend)
axs[ind].set_title('Number of Cluster : '+ str(n_cluster))
axs[ind].legend(loc='upper right')
plt.show()
visualize_silhouette([2, 3, 4, 5], X_features_scaled)
visualize_kmeans_plot_multi([2, 3, 4,5], X_features_scaled)
군집이 3개 이상일 때부터는 데이터셋 개수가 너무 작은 군집이 만들어진다. 이 군집에 속한 데이터는 개수가 작을뿐더러 실루엣 계수값도 상대적으로 매우 작다. 또, 군집 내부에서도 데이터가 광범위하게 퍼져 있다.
군집이 3개일 때는 0번 군집의 데이터 건수가 매우 작고, 군집이 4개일 때는 2, 3번 군집이, 5개일 때는 2,3,4번 군집에 속한 데이터셋 개수가 너무 적고 광범위하게 퍼져 있다.
- 비지도학습 알고리즘의 하나인 군집화의 기능적 의미는 숨어 있는 새로운 집단을 발견하는 것이다.
- 새로운 군집 내의 데이터값을 분석하고 이해함으로써 이 집단에 새로운 의미를 부여할 수 있다. 이를 통해 전체 데이터를 다른 각도로 바라볼 수 있게 만들어준다.
- 데이터셋의 왜곡 정도를 낮추기 위해 로그 변환을 많이 적용한다. 따라서, 전체 데이터를 로그 변환한 뒤에 k-평균을 적용해보자
cust_df['Recency_log'] = np.log1p(cust_df['Recency'])
cust_df['Frequency_log'] = np.log1p(cust_df['Frequency'])
cust_df['Monetary_log'] = np.log1p(cust_df['Monetary'])
X_features = cust_df[['Recency_log', 'Frequency_log', 'Monetary_log']].values
X_features_scaled = StandardScaler().fit_transform(X_features)
kmeans = KMeans(n_clusters=3, random_state=0)
labels = kmeans.fit_predict(X_features_scaled)
cust_df['cluster_label'] = labels
print('실루엣 스코어: {0:.3f}'.format(silhouette_score(X_features_scaled, labels)))
실루엣 스코어: 0.303
하지만 계수의 절대치가 중요한게 아니라 어떻게 개별 군집이 더 균일하게 나뉠 수 있는지가 중요하다.
로그 변환한 데이터셋을 기반으로 실루엣 계수와 군집화 구성을 시각화해보자
visualize_silhouette([2,3,4,5],X_features_scaled)
visualize_kmeans_plot_multi([2,3,4,5],X_features_scaled)
로그 변환으로 데이터를 일차 변환한 후에 군집화를 수행하면 더 나은 결과를 도출할 수 있다.
'Data Science > 파이썬 머신러닝 완벽 가이드' 카테고리의 다른 글
[sklearn] (49) 텍스트 사전 분비 작업(텍스트 전처리) - 텍스트 정규화 (0) | 2023.07.31 |
---|---|
[sklearn] (48) 자연어처리(NLP, Natural Language Processing)와 텍스트 분석(Text Analytics) (0) | 2023.07.28 |
[sklearn] (46) DBSCAN(밀도 기반 클러스터링), make_circles (0) | 2023.07.26 |
[sklearn] (45) 가우시안 혼합 모델, GMM(Gaussian Mixture Model) (0) | 2023.07.25 |
[sklearn] (44) 평균 이동 군집화, Mean Shift, KDE 함수 (0) | 2023.07.25 |