TOP
class="layout-aside-left paging-number">
본문 바로가기
[파이썬 Projects]/<파이썬 머신러닝>

[머신러닝] 군집화: 실습 - 고객 세그먼테이션

by 기록자_Recordian 2024. 10. 28.
728x90
반응형
군집화란?
 

[머신러닝] 군집화 (Clustering)

군집화(Clustering) [군집]군집은 비슷한 샘플을 클러스터 또는 비슷한 샘플의 그룹으로 할당하는 작업으로, 데이터 분석, 고객 분류, 추천 시스템, 검색 엔진, 이미지 분할, 준지도 학습, 차원 축소

puppy-foot-it.tistory.com


이전 내용
 

[머신러닝] 군집화: DBSCAN

군집화란? [머신러닝] 군집화 (Clustering)군집화(Clustering) [군집]군집은 비슷한 샘플을 클러스터 또는 비슷한 샘플의 그룹으로 할당하는 작업으로, 데이터 분석, 고객 분류, 추천 시스템, 검색 엔

puppy-foot-it.tistory.com


군집화 실습 - 고객 세그먼테이션

 

◆ 고객 세그먼테이션(Customer Segmentation): 다양한 기준으로 고객을 분류하는 기법. CRM(고객관계관리)이나 마케팅의 중요 기반 요소이며, 주요 목표는 타깃 마케팅이다.

중요한 분류 요소는 어떤 상품을 얼마나 많은 비용을 써서 얼마나 자주 사용하는가에 기반한 정보로 분류한다.

※ 타깃 마케팅: 고객을 여러 특성에 맞게 세분화해서 그 유형에 따라 맞춤형 마케팅이나 서비스를 제공하는 것.

 

고객 세그먼테이션은 고객의 어떤 요소를 기반으로 군집화할 것인가를 결정하는 것이 중요한데, 이 실습에서는 기본적인 고객 분석 요소인 RFM 기법을 이용한다.

  • R(Recency): 가장 최근 상품 구입 일에서 오늘까지의 기간
  • F(Frequency): 상품 구매 횟수
  • M(Monetary Value): 총 구매 금액

데이터 세트 로딩과 데이터 클렌징

 

하단 링크를 통해 접속한 뒤, 엑셀 파일을 다운로드 받는다.

 

UCI Machine Learning Repository

This dataset is licensed under a Creative Commons Attribution 4.0 International (CC BY 4.0) license. This allows for the sharing and adaptation of the datasets for any purpose, provided that the appropriate credit is given.

archive.ics.uci.edu

 

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

728x90
반응형