군집화란?
이전 내용
군집화 실습 - 고객 세그먼테이션
◆ 고객 세그먼테이션(Customer Segmentation): 다양한 기준으로 고객을 분류하는 기법. CRM(고객관계관리)이나 마케팅의 중요 기반 요소이며, 주요 목표는 타깃 마케팅이다.
중요한 분류 요소는 어떤 상품을 얼마나 많은 비용을 써서 얼마나 자주 사용하는가에 기반한 정보로 분류한다.
※ 타깃 마케팅: 고객을 여러 특성에 맞게 세분화해서 그 유형에 따라 맞춤형 마케팅이나 서비스를 제공하는 것.
고객 세그먼테이션은 고객의 어떤 요소를 기반으로 군집화할 것인가를 결정하는 것이 중요한데, 이 실습에서는 기본적인 고객 분석 요소인 RFM 기법을 이용한다.
- R(Recency): 가장 최근 상품 구입 일에서 오늘까지의 기간
- F(Frequency): 상품 구매 횟수
- M(Monetary Value): 총 구매 금액
데이터 세트 로딩과 데이터 클렌징
하단 링크를 통해 접속한 뒤, 엑셀 파일을 다운로드 받는다.
import pandas as pd
import datetime
import math
import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
retail_df = pd.read_excel("../Data/07. Online Retail.xlsx")
retail_df.head()
retail_df.info()
[데이터 세트의 각 칼럼 및 정보]
- InvoiceNo: 주문번호. 'C'로 시작하는 것은 취소 주문
- StockCode: 제품 코드(Item Code)
- Description: 제품 설명
- Quantity: 주문 제품 건수
- InvoiceData: 주문 일자
- Unit Pirce: 제품 단가
- Customer ID: 고객 번호
- Country: 국가명(주문 고객의 국적)
- 전체 데이터 수: 541,909
- Customer ID의 Null 값이 약 13만 5천개로 많음.
- 그 외 다른 칼럼에도 오류 데이터 존재
▶ 데이터 사전 정제 작업 필요
Null 값 제거 / 오류 데이터 삭제 (Quantity나 Unit Price가 0보다 작은 값, 취소 주문 건)
[데이터 사전 정제 작업 - 1]
불린 인덱싱을 적용해 Quantity > 0, Unit Price > 0 , CustomerID = Not 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()
▶ 전체 데이터 수가 541,909에서 397,884로 줄었으며, Null 값은 칼럼에 존재하지 않는다.
[데이터 사전 정제 작업 - 2]
retail_df['Country'].value_counts()[:5]
Country 칼럼은 주문 고객 국가이다. 주요 주문 고객은 영국인데, 이 외에도 EU의 여러 나라와 영연방 국가들이 포함되어 있다.
영국이 대다수를 차지하므로, 다른 국가의 데이터는 모두 제외한다.
retail_df = retail_df[retail_df['Country']=='United Kingdom']
print(retail_df.shape)
▶ 전체 데이터 수가 354,321 건으로 줄었다.
RFM 기반 데이터 가공
사전 정제된 데이터 기반으로 고객 세그먼테이션 군집화를 RFM 기반으로 수행한다.
이를 위해 필요한 데이터를 가공한다.
- 'UnitPrice'와 'Quantity'를 곱해서 주문 금액 데이터 만듦
- CustomerNo도 더 편리한 식별성을 위해 float 형을 int 형으로 변경
해당 온라인 판매 데이터 세트는 주문 횟수와 주문 금액이 압도적으로 특정 고객에게 많은 특성을 가지고 있는데, 그 이유는 개인 고객의 주문과 소매점의 주문이 함께 포함돼 있기 때문이다.
상위 5위 주문 건수와 주문 금액을 가진 데이터를 추출해서 확인해 본다.
retail_df['Sale_amount'] = retail_df['UnitPrice'] * retail_df['Quantity']
retail_df['CustomerID'] = retail_df['CustomerID'].astype(int)
print(retail_df['CustomerID'].value_counts().head())
print(retail_df.groupby('CustomerID')['Sale_amount'].sum().sort_values(ascending=False)[:5])
▶ 몇몇 특정 고객이 많은 주문 건수와 주문 금액을 가지고 있다.
도한 해당 판매 데이터 세트는 주문번호(InvoiceNo) + 상품코드(StockCode) 레벨의 식별자로 돼 있다.
InvoiceNo + StockCode로 Group by를 수행하면 거의 1에 가깝게 유일한 식별자 레벨이 됨을 알 수 있다.
retail_df.groupby(['InvoiceNo', 'StockCode'])['InvoiceNo'].count().mean()
현재 수행하려는 RFM 기반의 고객 세그먼테이션은 고객 레벨로 주문 기간, 주문 횟수, 주문 금액 데이터를 기반으로 해 세그먼테이션을 수행하는 것이기 때문에 주문번호+상품코드 기준의 데이터를 고객 기준의 RFM 데이터로 변경한다.
이를 위해 주문번호 기준의 데이터를 개별 고객 기준의 데이터로 Group by를 해야 한다.
- 주문번호 기준의 retail_df DataFramedp groupby('CustomerID') 적용하여 CustomerID 기준으로 DataFrame 생성
- DataFrameGroupby 객체에 agg()를 이용하여 서로 다른 aggregation 연산 (count(), max() 등) 수행할 수 있도록 함
- agg() 에 인자로 대상 칼럼들과 aggregation 함수명등를 딕셔너리 형태로 입력하여 칼럼 여러 개의 서로 다른 aggregation 연산 수행 가능하도록 함.
- Frequency (고객별 주문 건수): 'CustomerID'로 groupby() 해서 'InvoiceNo'의 count() aggregation으로 구함.
- Monetary Value(고객별 주문 금액): 'CustomerID'로 groupby()해서 'Sale_amount'의 sum() aggregation으로 구함
- Recency(개별 고객당 가장 최근 주문): 'CustomerID'로 groupby() 해서 'InvoiceDate' 칼럼의 max()로 고객별 가장 최근 주문 일자를 먼저 구함
- Recency(개별 고객당 가장 최근 주문): 온라인 판매 데이터가 2010년 12월 1일에서 2011년 12월 9일까지의 데이터 이므로, 현재 날짜는 2011년 12월 9일에서 하루 더한 2011년 12월 10일로 간주하고 가장 최근의 주문 일자를 뺀 데이터에서 일자 데이터(days)만 추출해 생성
# 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()
# 2011년 12월 10일을 현재 날짜로 간주하고, 가장 최근의 주문 일자를 뺀 일자 데이터 추출
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()
▶ 고객별로 RFM 분석에 필요한 Recency, Frequency, Monetary 칼럼을 모두 생성하였다.
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()
▶ Recency, Frequency, Monetary 모두 왜곡된 데이터 값 분포도를 가지고 있으며, 특히 Frequency, Monetary의 경우 특정 범위에 값이 몰려 있어 왜곡 정도가 매우 심하다.
[각 칼럼의 데이터 값 분포 확인 - 백분위]
cust_df[['Recency', 'Frequency', 'Monetary']].describe()
- Recency: 평균(mean)이 92.7이나, 중위값(50%)인 51보다 크게 높다. max 값은 374로 75%(3/4 분위)인 143보다 훨씬 커서 왜곡 정도가 높음을 알 수 있다.
- Frequency: 평균이 90.3 인데, 3/4분위인 99.2에 가까운데, 이는 max 값 7847을 포함한 상위 몇 개의 큰 값으로 인한 것이다.
- Monetary: 평균은 1864로 3/4분위인 1576보다 크며, 역시 max 값 259657을 포함한 상위 몇 개의 큰 값으로 인함이다.
[데이터 세트에 K-평균 군집 적용]
왜곡 정도가 매우 높은 데이터 세트에 K-평균 군집을 적용하면 중심의 개수를 증가시키더라도 변별력이 떨어지는 군집화가 수행된다. 해당 데이터 세트를 평균과 표준편차를 재조정 한 뒤에 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_labels'] = labels
print('실루엣 스코어는 : {0:.3f}'.format(silhouette_score(X_features_scaled, labels)))
▶ 군집을 3개로 구성할 경우 전체 군집의 평균 실루엣 계수인 실루엣 스코어는 0.576이 나왔고, 이는 비교적 안정적인 수치이다.
[각 개별 군집의 실루엣 계수 값과 데이터 구성 확인]
visualize_silhouette() 함수와 visualize_kmeans_plot_multi() 함수를 사용하여 군집 개수를 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="--")
import numpy as np
from sklearn.cluster import KMeans
from sklearn.decomposition import PCA
import pandas as pd
import matplotlib.pyplot as plt
def visualize_kmeans_plot_multi(cluster_lists, X_features):
"""여러 개의 클러스터링 갯수를 리스트로 입력받아 각각의 클러스터링 결과를 시각화합니다.
Args:
cluster_lists (list): 각기 다른 클러스터 개수로 구성된 리스트.
X_features (array-like): 클러스터링 할 특성 데이터.
"""
n_cols = len(cluster_lists)
fig, axs = plt.subplots(figsize=(4 * n_cols, 4), nrows=1, ncols=n_cols)
# 입력 데이터의 FEATURE가 여러 개일 경우 2차원 시각화를 위해 PCA 변환 수행
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):
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]
cluster_legend = 'Cluster ' + str(label)
if markers[label % len(markers)] == 'x':
axs[ind].scatter(x=label_df['PCA1'], y=label_df['PCA2'], s=70,
marker=markers[label % len(markers)], label=cluster_legend)
else:
axs[ind].scatter(x=label_df['PCA1'], y=label_df['PCA2'], s=70,
marker=markers[label % len(markers)], label=cluster_legend, edgecolors='k')
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)
▶ 지나치게 왜곡된 데이터 세트는 K-평균과 같은 거리 기반 군집화 알고리즘에서 지나치게 일반적인 군집화 결과를 초래하게 된다.
[로그 변환으로 데이터 세트 왜곡 정도를 낮춘 뒤 K-평균 알고리즘 적용]
데이터 세트의 왜곡 정도를 낮추기 위해 가장 자주 사용되는 방법은 데이터 값에 로그(Log)를 적용하는 로그 변환이다.
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_samples, silhouette_score
# Recency, Frequency, 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)))
▶ 실루엣 스코어는 로그 변환하기 전보다 떨어지나, 실루엣 스코어의 절대치가 중요한 것이 아니라, 어떻게 개별 군집이 더 균일하게 나뉠 수 있는지가 더 중요하다.
[로그 변환한 데이터 세트의 실루엣 계수와 군집화 구성 시각화]
왜곡된 데이터에 대해서는 로그 변환으로 데이터를 일차 변환한 후에 군집화를 수행하면 더 나은 결과를 도출할 수 있다.
visualize_silhouette([2,3,4,5],X_features_scaled)
visualize_kmeans_plot_multi([2,3,4,5],X_features_scaled)
▶ 실루엣 스코어는 로그 변환하기 전보다 떨어지지만 로그 변환 전보다 더 균일하게 군집화가 구성된다.
다음 내용
[출처]
파이썬 머신러닝 완벽 가이드
분석 공부 블로그 - T Story
'[파이썬 Projects] > <파이썬 머신러닝>' 카테고리의 다른 글
[머신러닝] 텍스트 분석: 텍스트 정규화 (0) | 2024.10.29 |
---|---|
[머신러닝] 텍스트 분석 (0) | 2024.10.28 |
[머신러닝] 군집화: DBSCAN (0) | 2024.10.27 |
[머신러닝] 군집화: GMM (0) | 2024.10.27 |
[머신러닝] 군집화: 평균 이동 (0) | 2024.10.25 |