고객 세그멘테이션의 정의와 기법
- 고객 세그멘테이션의 주요 목표는 타깃 마케팅
- 타깃 마케팅 : 고객을 여러 특성에 맞게 세분화해서 그 유형에 따라 맞춤형 마케팅이나 서비스를 제공하는 것
- 고객의 상품 구매 이력에서 출발
- 기본적인 고객 분석 요소인 RFM 기법을 이용해서 고객을 군집화
- RECENCY(R): 가장 최근 상품 구입 일에서 오늘까지의 기간
- FREQUENCY(F): 상품 구매 횟수
- MONETARY VALUE(M) : 총 구매 금액
- UCI 에서 제공하는 Online Retail Data set을 기반으로 한 고객 세그멘테이션 군집화 실습 진행
데이터 세트 로딩과 데이터 클렌징
- 제품 주문 데이터 세트 로딩
- InvoiceNo: 주문번호, ‘C’로 시작하는 것은 취소 주문
- StockCode: 제품 코드(Item Code)
- Description: 제품 설명
- Quantity : 주문 제품 건수
- InvoiceDate: 주문 일자
- UnitPrice: 제품 단가
- CustomerID: 고객 번호
- Country: 국가명(주문 고객의 국적)
#데이터 세트 로딩과 데이터 클렌징
import numpy as np
import datetime
import math
import pandas as pd
import matplotlib.pyplot as plt
%matplotlib inline
retail_df = pd.read_excel(io='Online Retail.xlsx')
retail_df.head(3)
- 데이터 세트의 전체 건수, 칼럼 타입, Null 개수 확인
- 전체 데이터 541,909개
- CustomerID 의 Null 값은 13만 5천 건, 너무 많은 것으로 확인됨
- 다른 칼럼에서의 오류값 확인
# 데이터 정보 확인
retail_df.info()
[output]
<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
- 사전 정제 작업
- Null 데이터 제거: CustomerID의 Null 값이 많은 것으로 확인됨. 고객 세그멘이션을 수행하므로 고객 식별 번호가 없는 데이터는 필요가 없기에 삭제
- 오류 데이터 삭제: 대표적인 오류 데이터는 Quantity 또는 UnitPrice 가 0보다 작은 경우. Quantity 가 0보다 작은 경우는 반환을 뜻하는 값으로 InvoiceNO 앞자리가 ‘C’로 되어있음. 분석의 효율성을 위해 삭제
- 불린 인덱싱 (Quantity > 0, UnitPrice > 0) 을 적용하고 CustomerID Not Null 인 값 필터링
- Country 칼럼의 주요 주문 고객 국가인 영국 데이터만을 필터링
# 데이터 필터링
retail_df = retail_df[retail_df['Quantity']>0]
retail_df = retail_df[retail_df['UnitPrice']>0]
retail_df = retail_df[retail_df['CustomerID'].notnull()]
#country 칼럼 데이터 필터링
retail_df['Country'].value_counts()[:5]
retail_df = retail_df[retail_df['Country']=='United Kingdom']
print(retail_df.isnull().sum()) # 결측값 개수 확인
print(retail_df.shape)
[output]
InvoiceNo 0
StockCode 0
Description 0
Quantity 0
InvoiceDate 0
UnitPrice 0
CustomerID 0
Country 0
sale_amount 0
dtype: int64
(354321, 9)
RFM 기반 데이터 가공
- ‘UnitPrice’(제품 단가), ‘Quantity’(주문 제품 건수)를 곱해서 주문 금액 칼럼 생성, CusmtomerID int형으로 형변환
- 온라인 판매 데이터는 주문 금액, 주문 횟수가 특정 고객에게 많은 특성을 줌
- CustomerID로 group by 하여 TOP-5 구매 횟수 고객 ID 추출
- CustomerID로 group by 하고 주문 금액 칼럼을 더하여 TOP-5 주문 금액 고객 ID 추출
- 온라인 데이터 세트는 ‘InvocieNO(주문번호)’ + ‘StockCode(상품코드)’ 거의 1에 가까운 유일한 식별자 레벨로 되어있음
#RFM 기반 데이터 가공
#주문 금액 칼럼 생성, CustomerID 형변환
retail_df['sale_amount'] = retail_df['Quantity'] * retail_df['UnitPrice']
retail_df['CustomerID'] = retail_df['CustomerID'].astype(int)
#Customer ID 기준 TOP-5 주문 건수, 주문 금액 데이터 추출
print(retail_df['CustomerID'].value_counts().head(5))
print(retail_df.groupby('CustomerID')['sale_amount'].sum().sort_values(ascending=False).head(5))
#고유 식별자 확인
retail_df.groupby(['InvoiceNo', 'StockCode'])['InvoiceNo'].count().mean()
[output]
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
1.028702077315023
- 고객 레벨로 주문 기간, 주문 횟수, 주문 금액 데이터를 기반으로 하여 세그멘테이션을 수행해야하기 때문에 주문번호 기준의 데이터를 개별 고객 기준의 데이터로 groupby 해야함
- ‘Customer ID’로 groupby한 DataFrameGroupby 객체에 agg() 메서드를 이용하여 인자로 대상 칼럼들과 aggregation 함수명들을 딕셔너리 형태로 입력
- Frequency는 고객별 주문 건수이므로 ‘InvoiceNO’의 count() 으로 구함
- Monetary value는 고객별 주문 금액이므로 ‘sale_amount’의 sum() 으로 구함
- Recency의 경우 두 번의 가공 작업을 통해 ‘InvoiceDate’칼럼의 max()로 고객별 가장 최근 주문 일자를 구한 뒤 추후에 가공작업을 별도로 수행
- Recency 칼럼은 오늘 날짜에서 주문 일자를 뺀 날짜, 오늘 날짜를 2011년 12월 10일로 설정
- ‘Customer ID’로 groupby한 DataFrameGroupby 객체에 agg() 메서드를 이용하여 인자로 대상 칼럼들과 aggregation 함수명들을 딕셔너리 형태로 입력
#DataFrame의 groupby()의 multiple 연산을 위해 agg()을 이용
#Recency는 InvoiceDate 칼럼의 max()에서 데이터 가공
#Frequency는 InvoiceDate 칼럼의 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)
#Recency 칼럼 값을 (오늘 날짜 - 최근 주문 날짜)로 변경하여 데이터 가공
import numpy as np
cust_df['Recency'] = datetime.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
RFM 기반 고객 세그멘테이션
- 온라인 판매 데이터는 소매업체의 대규모 주문이 포함되어있어 주문 횟수와 주문 금액에서 개인 고객 주문과 매우 큰 차이를 나타냄. 이로 인해 왜곡된 데이터 분포도를 가짐
- 맥플롯립의 hist()를 통해 각 칼럼의 값 분포도를 확인하면 모두 왜곡된 데이터 값 분포도를 가지고 있음을 확인할 수 있음
- describe()를 통해 각 칼럼의 데이터 값 백분위로 어떻게 값이 분포되어 있는지 확인
- Recency는 평균이 92.7이지만, 50%인 51보다 크게 높음, max 는 374로 75%인 143보다 훨씬 커 왜곡 정도가 심함
- Frequency의 경우 평균이 90.3이지만 max 값 7847를 포함한 상위의 몇 개의 큰 값으로 인해 75%가 99.25에 가까움
- Monetary도 마찬가지로 상위의 큰 값으로 인해 평균은 1864.3으로 75%인 1576.5보다 높은 값이 확인 됨
#hist()를 이용하여 각 칼럼의 값 분포도 확인
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()
#백분위 값으로 데이터 분포도 확인
cust_df[['Recency', 'Frequency', 'Monetary']].describe()
- 왜곡 정도가 매우 높은 데이터 세트에 K-평균을 적용하면 중심의 개수를 증가시키더라도 변별력이 떨어지는 군집화가 수행되기 때문에 StandScaler로 평균과 표준편차를 재조정
- 군집을 3개로 구성할 경우 전체 군집의 평균 실루엣 계수는 0.592
#StandScaler로 평균과 표준편차 재조정
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) # 왜 df 그대로 안하고 ndarray로 바꿔서 스케일링하는거지?
#K-평균 수행
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)))
[output]
실루엣 스코어는 : 0.592
- 각 군집별 평균 실루엣 계수를 군집 개수를 2~5개까지 변화시키면서 확인
- 군집이 2개일 때는 너무 개괄적으로 군집화, 3~5개일 때는 소수의 데이터 세트의 실루엣 계수가 매우 작은 것이 확인됨. 이는 특정 소매점의 대량 주문 구매 데이터로 해석할 수 있음
- 이처럼 지나치게 왜곡된 데이터 세트는 K-평균과 같은 거리 기반 군집화 알고리즘에서 지나치게 일반적인 군집화 결과를 도출하게 됨
#군집 개수별로 군집화 구성을 시각화 하는 함수
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()
### 여러개의 클러스터링 갯수를 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="--")
# 각 군집별 실루엣 계수 값 확인을 위해 군집 개수를 2~5개까지 변화시켜가며 군집화 구성 시각화
visualize_silhouette([2,3,4,5], X_features_scaled)
visualize_kmeans_plot_multi([2,3,4,5],X_features_scaled)
- 비지도학습의 군집화의 기능적 의미는 숨어있는 새로운 집단을 발견하는 것. 새로운 군집 내의 데이터 값을 분석하고 이해함으로써 이 집단에 새로운 의미를 부여할 수 있음
- 데이터 세트의 왜곡정도를 낮추기 위해 로그변환 이용
- 실루엣 스코어는 로그 변환 하기 전보다 떨어지지만 더 균일하게 군집화가 구성됐음을 확인할 수 있음
#데이터의 왜곡 정도를 낮추기 위해 로그 변환
#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 데이터에 StandScaler 적용
X_features = cust_df[['Recency_log', 'Frequency_log', 'Monetary_log']]#.values
X_features_scaled = StandardScaler().fit_transform(X_features)
#K-평균 수행
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)))
[output]
실루엣 스코어는 : 0.303
Ref) 파이썬 머신러닝 완벽가이드
'데이터 > 머신러닝' 카테고리의 다른 글
[NLP] Chapter 8 | 텍스트 분석 (소개 및 기반지식) (1) | 2023.02.02 |
---|---|
[RecSys] Chapter 9 | 추천시스템 (0) | 2023.02.01 |
[Clustering] Chapter 7 | 군집화 (02. 군집 평가) (0) | 2023.01.23 |
[Clustering] Chapter 7 | 군집화 (01. K-평균 알고리즘의 이해) (0) | 2023.01.22 |
[Classification] Chapter 4 | 분류(01. 분류의 개요~02. 결정 트리) (0) | 2022.09.05 |