본문 바로가기

ML with SckitLearn

온라인쇼핑몰을 위한 RFM Segmentation 실전 연습

반응형

온라인 판매 데이터를 기반으로 고객 세그멘테이션을 군집화 기반으로 수행해 보겠습니다.

하기 페이지에서 Data Folder를 클릭 후 엑셀 데이터 DataSet을 다운로드합니다. 이 데이터는 541만 Row의 데이터로 RFM Segmentation 실습을 하기에 좋습니다.

 

archive.ics.uci.edu/ml/datasets/online+retail#

 

UCI Machine Learning Repository: Online Retail Data Set

Online Retail Data Set Download: Data Folder, Data Set Description Abstract: This is a transnational data set which contains all the transactions occurring between 01/12/2010 and 09/12/2011 for a UK-based and registered non-store online retail. Data Set Ch

archive.ics.uci.edu

 


Data Loding & Organization

 

 

우선 하기와 같이 엑셀을 로딩하기 위해서는 xlrd Library를 설치해야 합니다. 'pip install xlrd'로Library를설치한후실행합니다.

from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score, silhouette_samples

import pandas as pd
import datetime
import math
import numpy as np
import matplotlib.pyplot as plt
import Common_Module.Common_Module as CM

retailDF = pd.read_excel(io='C:\\Users\\HANA\\PycharmProjects\\HANATOUR\\ML_Learn\\OnlineRetail.xlsx')
print(retailDF.info())
retailDF = retailDF[retailDF['Quantity']>0]
retailDF = retailDF[retailDF['UnitPrice']>0]
retailDF = retailDF[retailDF['CustomerID'].notnull()]
print(retailDF.shape)
print(retailDF.isnull().sum())
print(retailDF['Country'].value_counts()[:5])
retailDF = retailDF[retailDF['Country']=='United Kingdom']
print(retailDF.shape)

 

'retailDF.info()'를 통해 데이터 세트의 전체 건수, 칼럼 타입, Null 개수를 확인하고, 총 54만 건 중 CustomerID가 Null인 값을 제외하고 데이터의 대부분을 차지하는 United Kingdom만 데이타 Set로 사용합니다. 

#print(retailDF.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
None

#print(retailDF.shape)
(397884, 8)

#print(retailDF.isnull().sum())
InvoiceNo      0
StockCode      0
Description    0
Quantity       0
InvoiceDate    0
UnitPrice      0
CustomerID     0
Country        0
dtype: int64

#print(retailDF['Country'].value_counts()[:5])
United Kingdom    354321
Germany             9040
France              8341
EIRE                7236
Spain               2484
Name: Country, dtype: int64

#print(retailDF.shape)
(354321, 8)

 

 


RFM DataSet 만들기

 

실제 사용할 RFM Data 정보를 만들게 됩니다. 하기 데이터를 이해하기 위해서는 lambda와 agg()의 사용법을 알아야 합니다.

retailDF['sale_amount'] = retailDF['Quantity'] * retailDF['UnitPrice']
retailDF['CustomerID'] = retailDF['CustomerID'].astype(int)
print(retailDF['CustomerID'].value_counts().head())
print(retailDF.groupby('CustomerID')['sale_amount'].sum().sort_values(ascending=False)[:5])

aggregations = {
    'InvoiceDate': 'max',
    'InvoiceNo': 'count',
    'sale_amount': 'sum'
}
cust_df = retailDF.groupby('CustomerID').agg(aggregations)
cust_df = cust_df.rename(columns={'InvoiceDate':'Recency', 'InvoiceNo':'Frequency', 'sale_amount':'Monetary'})
cust_df = cust_df.reset_index()
cust_df['Recency'] = datetime.datetime(2021,10,2) - cust_df['Recency']
cust_df['Recency'] = cust_df['Recency'].apply(lambda x:x.days+1)
print(cust_df)

 

Row를 가장 많이 가지고 있는 CustomerID와 Sales_Amount가 가장 높은 순으로 정렬해 보다. 그 후 RFM값을 정리해서 보여준다.

#print(retailDF['CustomerID'].value_counts().head())
17841    7847
14096    5111
12748    4595
14606    2700
15311    2379
Name: CustomerID, dtype: int64

#print(retailDF.groupby('CustomerID')['sale_amount'].sum().sort_values(ascending=False)[:5])
CustomerID
18102    259657.30
17450    194550.79
16446    168472.50
17511     91062.38
16029     81024.84
Name: sale_amount, dtype: float64

#print(cust_df)
      CustomerID  Recency  Frequency  Monetary
0          12346     3910          1  77183.60
1          12747     3587        103   4196.01
2          12748     3585       4595  33719.73
3          12749     3588        199   4090.88
4          12820     3588         59    942.34
...          ...      ...        ...       ...
3915       18280     3862         10    180.60
3916       18281     3765          7     80.82
3917       18282     3592         12    178.05
3918       18283     3588        756   2094.88
3919       18287     3627         70   1837.28

[3920 rows x 4 columns]

 

다음은 RFM데이터에 대한 분포를 확인하기 위한 Desc와 분포도 그래프를 확인하면 데이터의 왜곡 정도가 매우 심하다는 것을 알 수 있습니다. 왜곡 정도가 매우 높은 데이타 세트에 K-Means Cluster를 적용하면 중심의 개수를 증가시키더라도 변별력이 떨어지는 군집화가 수행됩니다. 

print(cust_df[['Recency','Frequency', 'Monetary']].describe())

fig, (ax1, ax2, ax3) = plt.subplots(figsize=(12,4), nrows=1, ncols=3)
ax1.set_title('Recency')
ax1.hist(cust_df['Recency'])

ax2.set_title('Frequency')
ax2.hist(cust_df['Frequency'])

ax3.set_title('Monetary')
ax3.hist(cust_df['Monetary'])

plt.show()
#print(cust_df[['Recency','Frequency', 'Monetary']].describe())
           Recency    Frequency       Monetary
count  3920.000000  3920.000000    3920.000000
mean   3676.742092    90.388010    1864.385601
std      99.533485   217.808385    7482.817477
min    3585.000000     1.000000       3.750000
25%    3602.000000    17.000000     300.280000
50%    3635.000000    41.000000     652.280000
75%    3727.000000    99.250000    1576.585000
max    3958.000000  7847.000000  259657.300000

 

 


K-Means Cluster 적용

 

먼저 데이터 세트를 StandardScaler로 평균과 표준편차를 재조정 한 뒤에 K-Means Cluster를 수행해 보겠습니다. 결과를 보면 Silhoutte Score값이 0.59로 나쁘지 않습니다.  하지만 초기 데이터 자체가 왜곡이 매우 심해 Cluster를 높이는 것이 의미가 없어 보입니다.

X_feature = cust_df[['Recency', 'Frequency', 'Monetary']].values
X_feature_scaled = StandardScaler().fit_transform(X_feature)

kmeans = KMeans(n_clusters=3, random_state=0)
labels = kmeans.fit_predict((X_feature_scaled))
cust_df['cluster_label'] = labels
print(silhouette_score(X_feature_scaled, labels))

CM.visualize_silhouette([2,3,4,5], X_feature_scaled)
#print(silhouette_score(X_feature_scaled, labels))
0.5924537813510862

 

 

데이터 세트의 왜곡 정도를 낮추기 위해 가장 자주 사용되는 방법은 데이터 값에 로그(Log)를 적용하는 로그 변환입니다. 실루엣 스코어는 이전보다 낮은 0.3이 되었지만 절대치는 중요하지 않습니다. 어떻게 개별 군집이 더 균일하게 나뉠 수 있는지가 더 중요합니다.

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_feature_log = cust_df[['Recency_log', 'Frequency_log', 'Monetary_log']].values
X_feature_scaled_log = StandardScaler().fit_transform(X_feature_log)

kmeans = KMeans(n_clusters=3, random_state=0)
labels = kmeans.fit_predict((X_feature_scaled_log))
cust_df['cluster_label_log'] = labels
print(silhouette_score(X_feature_scaled_log, labels))

CM.visualize_silhouette([2,3,4,5], X_feature_scaled_log)
#print(silhouette_score(X_feature_scaled_log, labels))
0.38063927452834523
반응형