RFM 분석은 CRM(고객 관계 관리)
분야에서 고객의 가치를 분석하는데 사용되는 분석방법이다.
Recency
: 얼마나 최근에 구매하였는가? (최근성)Frequency
: 얼마나 자주 구매(방문)하였는가? (행동 빈도)Monetary
: 얼마나 많이 구매하였는가? (구매 금액)import numpy as np
import pandas as pd
import datetime as dt
# 시각화
import matplotlib.pyplot as plt
import seaborn as sns
# ML 알고리즘
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
# 오류 무시
import warnings
warnings.filterwarnings('ignore')
import pandas as pd
df = pd.read_excel('Online Retail.xlsx')
print(df.shape)
df.head()
데이터 정보, 기술통계, 결측값, 중복값 등을 살펴보고 처리해준다.
# 결측치 있는 행 제거
df = df.dropna(subset=['CustomerID'])
# 중복값 제거
df = df.drop_duplicates()
# 음수 값과 0원 제거
df = df[(df['Quantity'] > 0) & (df['UnitPrice'] > 0)]
df.describe()
# 총 금액을 나타내는 Total Sum Column
df['TotalSum'] = df['UnitPrice'] * df['Quantity']
# Data Preparation Steps
print('Min Invoice Date : ', df.InvoiceDate.dt.date.min())
print('Max Invoice Date : ', df.InvoiceDate.dt.date.max())
df.head()
Min Invoice Date : 2010-12-01
Max Invoice Date : 2011-12-09
snapshot_date = df['InvoiceDate'].max() + dt.timedelta(days=1)
snapshot_date
Timestamp('2011-12-10 12:50:00')
# RFM 계산
rfm = df.groupby(['CustomerID']).agg({'InvoiceDate': lambda x : (snapshot_date - x.max()).days,
'InvoiceNo':'count',
'TotalSum': 'sum'})
rfm = rfm.rename(columns={'InvoiceDate':'Recency','InvoiceNo':'Frequency','TotalSum':'MonetaryValue'})
rfm
<참고>
위의 InvoiceDate의 lambda 계산식은 groupby를 통해 전체 중 마지막 주문 날짜+1을 한 것과 고객별 가장 최근 주문 날짜와의 차이를 구해 day만 추출해내기 위함이다. 아래 결과 이미지를 보면 dtype이 timedelta 형식이므로 row마다 계산하여 일수만을 추출한다.
snapshot_date - df['InvoiceDate']
# RFM 세그먼트 구축
r_labels = range(4, 0, -1) # 4, 3, 2, 1
f_labels = range(1, 5) # 1, 2, 3, 4
m_labels = range(1, 5) # 1, 2, 3, 4
r_quartiles = pd.qcut(rfm['Recency'], q=4, labels=r_labels)
f_quartiles = pd.qcut(rfm['Frequency'], q=4, labels=f_labels)
m_quartiles = pd.qcut(rfm['MonetaryValue'], q=4, labels=m_labels)
# assign() 함수를 통해 한 번에 여러 번수를 만들 수 있음
rfm = rfm.assign(R=r_quartiles, F=f_quartiles, M=m_quartiles)
def add_rfm(x):
return str(str(x['R']) + str(x['F']) + str(x['M']))
rfm['RFM_Segment'] = rfm.apply(add_rfm,axis=1 )
rfm['RFM_Score'] = rfm[['R','F','M']].sum(axis=1)
rfm
각 세그먼트별 빈도수를 파악한다.
rfm.groupby(['RFM_Segment']).size().sort_values(ascending=False)
하단 세그먼트인 '111'인 경우의 데이터를 확인한다.
rfm[rfm['RFM_Segment'] == '111'].head()
RFM Score별로 Recency, Frequency, Monetary의 통계량을 확인한다.
# RFM Score Summary
rfm.groupby('RFM_Score').agg({'Recency':'mean',
'Frequency':'mean',
'MonetaryValue':['mean', 'count']}).round(1)
RFM 점수를 사용하여 고객을 골드, 실버, 브론즈 세그먼트로 분류한다.
def segments(df):
if df['RFM_Score'] > 9:
return 'Gold'
elif df['RFM_Score'] <= 5:
return 'Bronze'
else:
return 'Silver'
rfm['General_Segment'] = rfm.apply(segments, axis=1)
rfm.groupby('General_Segment').agg({'Recency':'mean',
'Frequency':'mean',
'MonetaryValue':['mean', 'count']}).round(1)
K-means를 사용하기 전 다음과 같은 가정을 확인해야 한다.
평균과 분산이 동일하지 않다는 문제점을 발견하였다.
Scikit-learn 라이브러리의 Scaler를 사용하여 변수 크기 조정한다.
rfm_rfm = rfm[['Recency','Frequency','MonetaryValue']]
print(rfm_rfm.describe())
데이터 분포를 시각화한다.
fig, ax = plt.subplots(figsize=(10, 12))
plt.subplot(3, 1, 1);
sns.distplot(rfm['Recency'], label='Recency')
plt.subplot(3, 1, 2);
sns.distplot(rfm['Frequency'], label='Frequency')
plt.subplot(3, 1, 3);
sns.distplot(rfm['MonetaryValue'], label='Monetary')
plt.style.use('fivethirtyeight')
plt.tight_layout()
plt.show()
변수의 비대칭 분포(데이터 치우침)라는 문제점도 있다.
이를 해결하기 위해 로그 변환(양의 값만 해당)은 왜도를 관리한다.
다음과 같은 일련의 구조화 사전 처리 단계를 사용한다.
- 데이터 왜곡 해제 - 로그 변환
- 동일한 평균값으로 표준화
- 동일한 표준 편차로 조정
- 클러스터링에 사용할 별도의 어레이로 저장
rfm_log = rfm[['Recency', 'Frequency', 'MonetaryValue']].apply(np.log, axis=1).round(3) # 또는 rfm_log = np.log(rfm2)
# 시각화
f,ax = plt.subplots(figsize=(10, 12))
plt.subplot(3, 1, 1); sns.distplot(rfm_log['Recency'], label = 'Recency')
plt.subplot(3, 1, 2); sns.distplot(rfm_log['Frequency'], label = 'Frequency')
plt.subplot(3, 1, 3); sns.distplot(rfm_log['MonetaryValue'], label = 'Monetary Value')
plt.style.use('fivethirtyeight')
plt.tight_layout()
plt.show()
과정은 다음과 같다.
- 데이터 전처리
- 군집 수 선택
- 사전 처리된 데이터에서 k-평균 군집화 실행
- 각 클러스터의 평균 RFM 값 분석
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
scaler.fit(rfm_log)
rfm_scaled = scaler.transform(rfm_log)
군집의 수를 정하는 방법은 다음과 같다.
- 시각적 방법 - 팔꿈치 기준
- 수학적 방법 - 실루엣 계수
- 실험 및 해석
from sklearn.cluster import KMeans
ks = range(1, 8)
inertias = []
for k in ks:
# Create a KMeans Clusters
kc = KMeans(n_clusters=k, random_state=42)
kc.fit(rfm_scaled)
inertias.append(kc.inertia_)
# Plot ks vs inertias
fig, ax = plt.subplots(figsize=(15, 8))
plt.plot(ks, inertias, '-o')
plt.xlabel('Number of Clusters, k')
plt.ylabel('Inertia')
plt.xticks(ks)
plt.style.use('ggplot')
plt.title('What is the Best Number for KMeans?')
plt.show()
KMeans = 3
으로 결정한다.
# 클러스터링
kc = KMeans(n_clusters=3, random_state=1)
kc.fit(rfm_scaled)
# 군집 Label 컬럼 생성
cluster_labels = kc.labels_ # 0, 1, 2
# 각 클러스터의 평균 RFM 값 및 크기 계산:
rfm_k3 = rfm2.assign(K_Cluster=cluster_labels)
rfm_k3.groupby('K_Cluster').agg({'Recency':'mean',
'Frequency':'mean',
'MonetaryValue':['mean', 'count']}).round(0)
rfm_normalized = pd.DataFrame(rfm_scaled, index=rfm2.index, columns=rfm2.columns)
rfm_normalized['K_Cluster'] = kc.labels_
rfm_normalized['General_Segment'] = rfm['General_Segment']
rfm_normalized = rfm_normalized.reset_index()
rfm_normalized
rfm_melt = pd.melt(rfm_normalized,
id_vars=['CustomerID','General_Segment','K_Cluster'],
value_vars=['Recency', 'Frequency', 'MonetaryValue'],
var_name='Metric',value_name='Value')
rfm_melt
# 시각화
f, (ax1, ax2) = plt.subplots(1,2, figsize=(15, 8))
sns.lineplot(x='Metric', y='Value', hue='General_Segment', data=rfm_melt, ax=ax1)
sns.lineplot(x='Metric', y='Value', hue='K_Cluster', data=rfm_melt, ax=ax2)
plt.suptitle("Snake Plot of RFM",fontsize=24)
plt.show()
세그먼트 속성의 상대적 중요도
# 비율이 0에서 멀어질수록 총 모집단에 상대적인 세그먼트에 대한 속성이 더 중요하다.
cluster_avg = rfm_k3.groupby(['K_Cluster']).mean()
population_avg = rfm2.mean()
relative_imp = cluster_avg / population_avg - 1
relative_imp.round(2)
# 총 평균을 사용하여 비례 차이를 계산한다.
cluster_avg = rfm.groupby('General_Segment').mean().iloc[:, 0:3]
total_avg = rfm.iloc[:, 0:3].mean()
prop_rfm = cluster_avg / total_avg - 1
prop_rfm.round(2)
RFM 분위수와 K-Means 클러스터링 방법으로 두 가지 종류의 세분화를 만들었다.
# heatmap with RFM
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
sns.heatmap(data=relative_imp, annot=True, fmt='.2f', cmap='Blues',ax=ax1)
ax1.set(title = "Heatmap of K-Means")
# a snake plot with K-Means
sns.heatmap(prop_rfm, cmap= 'Oranges', fmt= '.2f', annot = True,ax=ax2)
ax2.set(title = "Heatmap of RFM quantile")
plt.suptitle("Heat Map of RFM",fontsize=20) #make title fontsize subtitle
plt.show()