데이터 출처: https://archive.ics.uci.edu/ml/datasets/online+retail
고객 세그먼테이션: 다양한 기준으로 고객을 분류하는 기법
사는 지역, 결혼 여부, 성별, 소득, 직업, 월별 사용액, 최근 구매 상품, 구매 주기, ...
고객 분석 요소인 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(io='Online Retail.xlsx')
retail_df.head(3)
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인 데이터는 제거하자
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
Country가 United Kingdom인 것 외에는 제거하자
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)
구매 금액 칼럼을 만들자
총 금액 = 개수(quantity) x 단위 금액(UnitPrice)
retail_df['CustomerID'] = retail_df['CustomerID'].astype(int)
retail_df['sale_amount'] = retail_df['Quantity'] * retail_df['UnitPrice']
구매 횟수가 많은 고객을 확인하고, 총 구매 금액이 많은 고객 확인
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개씩 주문함
고객 기준으로 Recency, Frequency, Monetary 가공을 진행하자
Pandas할 때 공부했던 agg 집계 함수 사용
- Recency: 최근 상품 구입 일에서 오늘까지의 기간
- Frequency: 상품 구매 횟수
- Monetary Value: 총 구매 금액
# DataFrame의 groupby() 의 multiple 연산을 위해 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)
가장 최근 주문 날짜를 확인하자
cust_df['Recency'].max()
Timestamp('2011-12-09 12:49:00')
오늘이 하루 뒤인 2011-12-10이라고 가정하고, Recency를 다시 구해보자
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)
cust_df.head(3)
Recency, Frequency, Monetary 분포도를 확인하자
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'])
장기간 구매하지 않은 고객들이 꽤 있고, 삼품 구매 횟수와 총 구매 금액이 특정 고객에게 몰려있다.
이것 때문에 뒤에 군집화할 때 문제가 발생할 수 있겠다.
cust_df[['Recency','Frequency','Monetary']].describe()
셋 다 중위수보다 평균값이 훨씬 크다.
K-Means로 군집화 후 실루엣 계수 평가까지 진행해보자
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
실루엣 스코어만 보면 꽤 좋은 수치다.
시각화해보자
첫 번째 함수는 실루엣 스코어 그래프를 그려주는 것이고,
두 번째 함수는 PCA로 2차원으로 최적화 후 데이터의 분포를 보여주는 것임
이전에 했던 scikit-learn에서 제공한 시각화 함수의 변형임
### 여러개의 클러스터링 갯수를 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()
군집 수를 [2, 3, 4, 5] 로 반복하면서 시각화하자
visualize_silhouette([2,3,4,5],X_features_scaled)
visualize_kmeans_plot_multi([2,3,4,5],X_features_scaled)
스코어 자체는 괜찮지만, 너무 촘촘하게 되어있어서 몇 개로 군집화하든지 제대로 된 결과가 나오지 않는다. 저 주황색 네모부분이 너무 촘촘하다.
변환 후 재 시각화를 해보자
### Log 변환을 통해 데이터 변환
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score, silhouette_samples
# Recency, Frequecny, Monetary 컬럼에 np.log1p() 로 Log Transformation
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'])
# Log Transformation 데이터에 StandardScaler 적용
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
실루엣 스코어 자체는 좀 떨어졌다.
시각화 해보자
변환하니까 어느정도 정규분포 모양이 되었는지, 아까보다 훨씬 잘 퍼져있다.
정답은 없지만, 4개의 군집 정도가 적당해보인다.