파이썬 텍스트 클러스터링 시각화 - paisseon tegseuteu keulleoseuteoling sigaghwa

Updated: June 21, 2021

파이썬 머신러닝 완벽가이드 교재를 토대로 공부한 내용입니다.

실습과정에서 필요에 따라 내용의 누락 및 추가, 수정사항이 있습니다.

기본 세팅

import numpy as np import pandas as pd import matplotlib as mpl import matplotlib.pyplot as plt import seaborn as sns import warnings

%matplotlib inline %config InlineBackend.figure_format = 'retina' mpl.rc('font', family='NanumGothic') # 폰트 설정 mpl.rc('axes', unicode_minus=False) # 유니코드에서 음수 부호 설정 # 차트 스타일 설정 sns.set(font="NanumGothic", rc={"axes.unicode_minus":False}, style='darkgrid') plt.rc("figure", figsize=(10,8)) warnings.filterwarnings("ignore")

1. Online Retail 데이터

실습 예제로는 UCI Machine Learning Repository에서 다운 받을 수 있는 Online Retail 데이터를 사용한다.

해당 데이터로 고객 세그먼테이션(다양한 기준으로 고객을 분류하는 기법)을 진행할 것이다.

고객 세그먼테이션에선 어떤 요소를 기반으로 군집화할 것인지가 중요하다.

여기서는 RFM기법을 사용해본다.

  • RECENCY (R): 가장 최근 상품 구입 일에서 오늘까지의 기간

  • FREQUENCY (F): 상품 구매 횟수

  • MONETARY VALUE (M): 총 구매 금액

1.1 데이터 핸들링

데이터 구조

  • InvoiceNo: 주문번호 (C로 시작하는 것은 취소 주문)

  • StockCode: 제품 코드

  • Description: 제품 설명

  • Quantity: 주문 제품 건수

  • InvoiceDate: 주문 일자

  • UnitPrice: 제품 단가

  • CustomerID: 고객 번호

  • Country: 주문 고객의 국적

retail_df = pd.read_excel("./Online_Retail.xlsx") retail_df.head()

InvoiceNoStockCodeDescriptionQuantityInvoiceDateUnitPriceCustomerIDCountry01234
536365 85123A WHITE HANGING HEART T-LIGHT HOLDER 6 2010-12-01 08:26:00 2.55 17850.0 United Kingdom
536365 71053 WHITE METAL LANTERN 6 2010-12-01 08:26:00 3.39 17850.0 United Kingdom
536365 84406B CREAM CUPID HEARTS COAT HANGER 8 2010-12-01 08:26:00 2.75 17850.0 United Kingdom
536365 84029G KNITTED UNION FLAG HOT WATER BOTTLE 6 2010-12-01 08:26:00 3.39 17850.0 United Kingdom
536365 84029E RED WOOLLY HOTTIE WHITE HEART. 6 2010-12-01 08:26:00 3.39 17850.0 United Kingdom

<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

  • 데이터는 541,909 x 8로 이루어져 있다.

  • Description, CustomerID는 결측이 존재하며, CustomerID는 약 13만 건 정도의 결측값이 존재한다.

  • 여기선 고객 세그먼테이션을 수행하므로 CustomerID가 결측이면 모두 제거한다.

countmeanstdmin25%50%75%maxQuantityUnitPriceCustomerID
541909.0 9.552250 218.081158 -80995.00 1.00 3.00 10.00 80995.0
541909.0 4.611114 96.759853 -11062.06 1.25 2.08 4.13 38970.0
406829.0 15287.690570 1713.600303 12346.00 13953.00 15152.00 16791.00 18287.0

  • Quantity는 주문 제품 건수로 음수가 존재해선 안되는데 min 값에 음수가 있다.

  • UnitPrice 역시 제품 단가로 음수가 존재해선 안되는데 min 값에 음수가 있다.

  • 사실 여기서 음수는 취소된 건수, 단가를 의미한다고 한다(InvoiceNo가 C로 시작).

  • 여기선 음수값들은 모두 제거하고 분석을 진행하도록 한다.

# 데이터 핸들링 con_1 = retail_df.Quantity > 0 con_2 = retail_df.UnitPrice > 0 con_3 = retail_df.CustomerID.notnull() retail_df = retail_df[con_1 & con_2 & con_3] print(retail_df.shape) print(retail_df.isnull().sum())

(397884, 8) InvoiceNo 0 StockCode 0 Description 0 Quantity 0 InvoiceDate 0 UnitPrice 0 CustomerID 0 Country 0 dtype: int64

  • 앞서 설정한 조건으로 데이터를 삭제하여 397,884건만 남겨두었다.

  • 더불어서 Description의 결측값도 모두 삭제되었다.

# 주문 국적 확인 retail_df.Country.value_counts()[:10]

United Kingdom 354321 Germany 9040 France 8341 EIRE 7236 Spain 2484 Netherlands 2359 Belgium 2031 Switzerland 1841 Portugal 1462 Australia 1182 Name: Country, dtype: int64

  • 주문 국적은 전체 데이터의 대부분이 영국으로 나타나 영국만 남기기로 한다.

# 주문 국적 영국만 남기기 retail_df = retail_df[retail_df["Country"] == "United Kingdom"] print(retail_df.shape)

# 데이터 추가 가공 retail_df["sale_amount"] = retail_df["Quantity"] * retail_df["UnitPrice"] retail_df["CustomerID"] = retail_df["CustomerID"].astype(int)

  • Quantity(주문 제품 건수)와 UnitPrice(제품 단가)를 곱하여 sale_amount(주문 금액)을 생성하였다.

  • CustomerID는 기존 float형을 int형으로 바꿔주었다.

# 주문번호 + 상품코드 print(retail_df.groupby(["InvoiceNo", "StockCode"]).size().mean())

  • 주문번호 + 상품코드 갯수의 평균은 거의 1로서 주문번호 + 상품코드는 식별자 레벨로 사용 가능하다.

  • 다만 여기선 RFM 기반의 고객 세그먼테이션으로 고객 레벨로 분석을 해야한다.

  • 따라서, 주문번호 + 상품코드 기준 데이터를 고객 기준의 RFM 데이터로 변경한다.

# Recency (R): 가장 최근 상품 구입 일에서 오늘까지의 기간 # Frequency (F): 상품 구매 횟수 # Monetary Value (M): 총 구매 금액 aggregations = { "InvoiceDate": "max", # 최근 구입 일자 "InvoiceNo": "count", # 상품 구매 횟수 "sale_amount": "sum" # 총 구매 금액 } # 고객 기준의 RFM 데이터 cust_df = retail_df.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.head(3)

CustomerIDRecencyFrequencyMonetary012
12346 2011-01-18 10:01:00 1 77183.60
12747 2011-12-07 14:34:00 103 4196.01
12748 2011-12-09 12:20:00 4595 33719.73

  • RFM 데이터를 만들기 위해 기존 컬럼을 agg()를 활용해서 각각 다른 연산을 수행하였다.

  • Recency의 경우 현재 최근 구입 날짜로 변경해서 한번 더 작업이 필요하다.

import datetime as dt # Recency cust_df['Recency'] = dt.datetime(2011,12,10) - cust_df['Recency'] cust_df['Recency'] = cust_df['Recency'].apply(lambda x: x.days+1) cust_df.head(3)

CustomerIDRecencyFrequencyMonetary012
12346 326 1 77183.60
12747 3 103 4196.01
12748 1 4595 33719.73

  • 현재 사용하는 데이터는 2010년 12월 1일 ~ 2011년 12월 9일까지의 데이터이다.

  • 따라서 Recency를 구할 때 오늘까지의 기간은 2011년 12월 10일까지의 기간으로 계산하였다.

  • 지금까지의 가공으로 고객 기준의 RFM 데이터는 3,920 x 4로 이루어져있다.

fig, axs = plt.subplots(1,3, figsize=(15,4)) sns.histplot(cust_df['Recency'], kde=True, ax=axs[0]) axs[0].set_title('Recency Histogram') sns.histplot(cust_df['Frequency'], kde=True, ax=axs[1]) axs[1].set_title('Frequency Histogram') sns.histplot(cust_df['Monetary'], kde=True, ax=axs[2]) axs[2].set_title('Monetary Histogram') plt.show()

  • cust_df의 R,F,M의 분포를 확인하였다.

  • 모두 치우친 분포로 나타났으며 특히, F와 M은 매우 심하게 치우쳐있다.

  • 이는 이 데이터가 개인 고객과 소매업체의 대규모 주문이 포함되어 있어 나타난 현상이다.

cust_df.describe().iloc[:,1:]

RecencyFrequencyMonetarycountmeanstdmin25%50%75%max
3920.000000 3920.000000 3920.000000
92.742092 90.388010 1864.385601
99.533485 217.808385 7482.817477
1.000000 1.000000 3.750000
18.000000 17.000000 300.280000
51.000000 41.000000 652.280000
143.000000 99.250000 1576.585000
374.000000 7847.000000 259657.300000

  • 각 컬럼들의 평균은 중앙값보다 훨씬 크게 나타나며, 75% 분위수에 비해 최대값이 훨씬 크다.

  • 왜곡이 심한 데이터를 이용해서 군집분석을 진행해보자.

1.2 군집분석

실루엣 계수 및 군집 시각화 함수는 교재 소스코드를 사용하였다.

실루엣 계수 시각화 함수

### 여러개의 클러스터링 갯수를 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="--")

군집갯수별 시각화 함수

### 여러개의 클러스터링 갯수를 List로 입력 받아 각각의 클러스터링 결과를 시각화 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()

1.2.1 StandardScaler

from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans from sklearn.metrics import silhouette_samples, silhouette_score # StandardScaler X_features = cust_df[['Recency','Frequency','Monetary']].values X_features_scaled = StandardScaler().fit_transform(X_features) # KMeans kmeans = KMeans(n_clusters=3, random_state=0) labels = kmeans.fit_predict(X_features_scaled) cust_df["cluster_label"] = labels # 실루엣 스코어 silhouette_kmeans = silhouette_score(X_features_scaled, labels) print(f"실루엣 스코어: {silhouette_kmeans:.3f}")

  • KMeans 군집분석을 진행하였고 피처는 표준화하였다.

  • 실루엣 스코어가 0.592로 제법 좋은 수치로 나타났다.

  • 각 군집별 실루엣 스코어를 확인해보자.

visualize_silhouette([2,3,4,5],X_features_scaled) visualize_kmeans_plot_multi([2,3,4,5],X_features_scaled)

  • 군집수가 2일 때는 0번 군집의 실루엣 계수가 대부분 평균 아래이다.

  • 군집수가 3인 경우는 2번 군집, 4인 경우는 2,3번 군집의 데이터 건수가 매우 작다.

  • 이는 앞서 왜곡된 분포를 확인했듯이 소매업체의 대규모 주문이 소수의 군집을 이룬것이다.

  • 이렇게 특이한 데이터 셋을 분리하는 것이 군집화의 목표 중 하나이다.

  • 다만 이 경우 왜곡정도가 너무 심해 굳이 군집분석하지 않아도 충분히 분리 가능하다.

  • 실제 이런 자료를 다루는 도메인이 있다면 이런 결과를 미리 알았을 것이다.

1.2.2 로그 변환

# 로그 변환 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']) # StandardScaler X_features = cust_df[['Recency_log','Frequency_log','Monetary_log']].values X_features_scaled = StandardScaler().fit_transform(X_features) # KMeans kmeans = KMeans(n_clusters=3, random_state=0) labels = kmeans.fit_predict(X_features_scaled) cust_df["cluster_label"] = labels # 실루엣 스코어 silhouette_kmeans = silhouette_score(X_features_scaled, labels) print(f"실루엣 스코어: {silhouette_kmeans:.3f}")

  • 이번엔 왜곡 정도를 낮추기 위해 피처 로그 변환을 적용하였다.

  • 나머지 과정은 동일하게 수행하였을 때 실루엣 스코어는 0.303으로 낮게 나타났다.

visualize_silhouette([2,3,4,5],X_features_scaled) visualize_kmeans_plot_multi([2,3,4,5],X_features_scaled)

  • 전체 실루엣 스코어가 감소한 대신 각 군집별 실루엣 스코어가 균일하게 나타났다.

  • 이처럼 왜곡된 데이터 셋은 로그 변환으로 1차 변환 후 적용하면 더 나은 결과가 나오기도 한다.

  • 참고로 위 시각화 코드를 자세히 보면 실루엣 스코어에서의 군집은 입력한 피처를 사용한다.

  • 반면 군집 시각화 함수는 입력한 피처를 받아 PCA 적용 후 군집을 만든다.

Toplist

최신 우편물

태그