K-means 클러스터링 3차원 - k-means keulleoseuteoling 3chawon

오늘은 k-means clustering 에 대해서 설명해드릴까 합니다.

이 알고리즘은 너무너무너무너무 간단합니다.

생각을 하면서 잘 따라오세요 ~

먼저 우리는 임의의 N차원 벡터를 N차원 공간의 한 점으로 나타낼 수 가 있습니다.

예를들면 3차원 벡터는 3차원 공간상의 점 하나입니다..

우리는 위 그림을 보고 

아 대충 보기에 3개의 그룹으로 나눌 수 있겠구나 ~~

생각할 수 있어요

그런데  컴퓨터는 그렇게 생각할 수 있을까요 ?..

컴퓨터는 그렇게 생각할 수가 없습니다

그래서 우리는 실제 라벨 없이, 주어진 데이터만으로 컴퓨터가 그룹을 찾게 만들어줘야해요 ...

이 방법 중 하나가 우리가 공부할 k-means clustering이에요. 

근데 생각해보니까 트루라벨이 없네..? 우린 주어진 데이터만으로 군집화를 진행해야합니다.

따라서.. clustering 은 비교사학습(Unsupervised Learning)이라고 볼 수 있어요.

어쨌건.. 어떻게 이제 군집화를 시킬것인가..? 

우리는 간단하게 유클리디안 거리(Euclidian Distance)를 이용할겁니다.

거리에는 유클리디안뿐만 아니라 마할라노비스(Mahalanobis) 거리 등이 있습니다.

유클리디안 거리는 간단하게 우리가 중학교때인가 배운 수식.. 두 점사이의 거리를 계산하는 공식.. 을 이용하면돼요 

공식은 아래와 같습니다.

이 공식을 통해서 우리는 한점 v와 다른 한 점 v_1의 거리를 구할 수 있어요 ..

조금 더 설명해드리자면 d는 점의 차원을 의미하고, k는 차원에 속한 요소들의 인덱스를 의미해요..

자 그럼 우리는 이제 랜덤으로 k개의 점을 뿌려줍니다. 

여기서 k는 우리가 몇개로 군집화 할거냐... 하는 걸 정해주는 부분입니다 ^^

그러니까 이 알고리즘의 이름을 해석하자면 k개의 평균으로 군집화 !이런게 되겠죠 

우리는 여기서는 설명의 편의를 위해 2개로 합시다..

그런 다음에 2개의 점에대해서 현재 뿌려진 모든 점들의 거리를 구해요 

그리고 두 점 중 가까운.. (만약 세점이라면 가장 가까운) 그룹에 속한다고 표시해둡니다.

그러면 우리는 원래 가지고 있던 점들이 두 개의 점에 의해서 두개의 그룹으로 나뉘는 것을 알 수 있어요

그러면 우리는 각 그룹에 대한 평균 벡터를 각각 계산할 수 있어요 .

자 여기가 중요합니다.

그럼 !! 이제 새로운 두개의 점이 생기죠 ~?~~

그렇다면 다시 이 새로 생긴 두 평균 벡터에 의해서 모든 점과의 거리를 계산할 수 있습니다.

그리고 다시 거리를 비교하여 점들을 두개의 군집으로 나눌 수 있겠죠 ..

그런담에 .. 다시 ~ 평균을 구하고~ 다시 거리를 구하고 군집으로나누고... 

이렇게 반복해주면서 

어떠한 임계값(threshold)만큼 평균이 이동하지않거나 원하는 횟수만큼 반복이 완료되었을때의 

클러스터들을 반환하는것이 k-means clustering 입니다. 

끗. 너무 간만에써서 귀차니즘이..장난아니네요 ㅋㅋ 

안녕하세요, Everly입니다. 

정말 오랫만에 '파이썬 분석 실무 테크닉' 공부한 부분을 리뷰하는데요! (3달 만이군요..) 앞으로는 좀 더 자주 업로드해보도록 하겠습니다 :)

오늘은 지난 포스팅에서 다뤘던 스포츠 센터(편의상 헬스장) 데이터를 바탕으로, 이 헬스장을 사용하는 고객이 어떤 유형이 있고, 고객의 행동을 에측하는 데이터 분석을 공부해봅니다.

이번 포스팅에서는 클러스터링(clustering)을 통해 고객의 유형을 나눠 보고,

바로 다음 포스팅에서는 고객의 과거 데이터를 기반으로 예측하는 머신러닝 모델을 만들어 봅니다.


[고객의 소리] 지난번 분석으로 어느 정도 경향을 파악할 수 있었습니다. 아직 전체적인 경향밖에 파악하지 못해서, 이번에는 좀 더 구체적인 분석을 부탁드리려고 합니다.
고객별로 이용 경향이 다르기 때문에 이런 경향을 분석해서 고객별 이용 횟수 같은 것도 예측할 수 있지 않을까라는 생각이 듭니다. 이런 분석도 가능할까요?

지난 포스팅에서 사용했던 데이터를 그대로 사용합니다.

  • use_log.csv : 헬스장 이용이력 데이터. (기간: 2018.04 ~ 2019.03)
  • customer_join.csv : 앞서 [파이썬 데이터 분석 #3] 포스팅에서 가공한, 이용 이력을 포함한 고객 데이터.

지난번 [파이썬 데이터 분석 #3] 포스팅을 통해, 헬스장을 사용하는 고객들의 특징에 대해 몇 가지 인사이트를 도출해 봤었죠. 고객 1인당 월평균 5회 정도를 사용하고 있었고, 지속적으로 이용하는 사람(flag 변수)도 전체의 81%나 있었습니다. 또한 회원 기간의 분포를 살펴보니 10개월을 기점으로 회원 기간 분포가 뚝 끊기는 것을 볼 수 있어, '마의 10개월'을 발견하기도 했습니다.

이러한 인사이트는 전체 고객을 대상으로 본 것이었습니다. 하지만 이번에는 좀 더 구체적으로, 어떤 유형의 고객들이 있는지가 궁금합니다. 자주 이용하는 고객 그룹과, 아닌 그룹이 있지 않을까요? 이를 나누기 위해선 어떤 변수를 사용해서 나눌 수 있을까요? 좀 더 자세히 알아보겠습니다.

Tech 31. 데이터 불러오기

import pandas as pd
ul = pd.read_csv('3장/use_log.csv')
c = pd.read_csv('customer_join.csv')
ul.isnull().sum()
c.isnull().sum()
K-means 클러스터링 3차원 - k-means keulleoseuteoling 3chawon

use_log 데이터는 'ul', customer_join 데이터는 'c'로 불러옵니다.

결측치를 확인해 보니 ul은 없고, c의 경우 end_date(탈퇴시점)에 2842개의 결측치가 있네요. 아직 탈퇴를 하지 않은 회원은 이렇게 end_date에 결측값이 있습니다. 딱히 이상 없는 데이터입니다.

Tech 32. 클러스터링을 활용한 회원 그룹화

※ 클러스터링(Clustering) 이란?

- 군집 분석이라고도 하며, 정답 데이터가 없기 때문에 '비지도학습' 의 일종입니다. 

주로 고객을 그룹화하는 등 공통된 특성을 가진 군집들로 묶는 알고리즘을 의미합니다. 가장 유명한 것은 K-means 클러스터링인데, 이번 장에서도 이를 활용합니다.

K-means 클러스터링 3차원 - k-means keulleoseuteoling 3chawon

클러스터링은 'c' 데이터로 진행합니다. 회원의 이용이력에 따라 고객을 유형화해볼 수 있을 것 같네요. 그래서 회원의 이용이력과 관련한 변수를 사용합니다.

cc = c[['mean', 'median', 'max', 'min', 'mem_period']]
cc.head()

바로 mean, median, max, min, mem_period인데요. 각각 고객별 한달 평균, 중위수, 최대, 최소 이용횟수와 회원 기간(단위: month)입니다. 

c 데이터는 유일한 고객별 이용이력을 담고 있습니다. 이를테면 c 데이터의 0번 행의 고객은, 한 달 평균 4.83회를 사용하고, 한달 중위수 5회, 한달에 최소 2회~최대 8회까지 사용하네요. 

어쨌든 이러한 컬럼들만 따로 떼어 변수 'cc'를 만들어줍니다.

이제 k-means 클러스터링을 실시합니다. 단, 조건이 있습니다.

<조건>
#1. 그룹 개수 설정: 4개
#2. mean, median, max, min 변수는 월 이용 횟수와 관련한 변수이므로 1~8 사이값을 갖지만, mem_period는 이에 비해 값이 너무 크다. -> 그러므로 mem_period에는 표준화를 하자.

2번 조건을 토대로 5개 변수의 값을 평균 0, 표준편차 1을 따르는 정규분포를 따르도록 바꿔줍니다. 이를 '표준화'라고 합니다.

이런 과정을 거치지 않고 그냥 클러스터링을 해버리면 값이 너무 큰 'mem_period' 변수가 가장 중요한 변수로 인식될 수 있고 올바른 클러스터링이 불가능하기 때문에 표준화를 해줍니다.

from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler

#표준화
sc = StandardScaler()
cc_scaled = sc.fit_transform(cc)
pd.DataFrame(cc_scaled) #mean~min, mem_period 5개 변수가 표준화됨.
K-means 클러스터링 3차원 - k-means keulleoseuteoling 3chawon

표준화를 통해 위의 5가지 변수가 이와 같이 바뀌었습니다. 모든 변수의 값이 비슷해졌죠? 이 데이터는 'cc_scaled' 입니다.

#cc_scaled 데이터를 넣어 클러스터링
kmeans = KMeans(n_clusters=4, random_state=0)
clusters = kmeans.fit(cc_scaled)

#클러스터링 변수인 clusters 값을 원본 데이터인 'cc'내에 넣기 
cc['cluster'] = clusters.labels_
cc.head()

위의 코드를 통해 군집이 4개(n_clusters)인 k-means 클러스터링을 수행할 수 있습니다. 

어떤 클러스터가 만들어졌는지는 labels_ 메서드를 통해 알 수 있는데요, 더 쉽게 알아보기 위해 초반에 만들어 둔 'cc' 데이터(변수 5개만 있는 데이터프레임)에 'clsuter'라는 새로운 컬럼을 만들어 클러스터 번호를 넣어줍니다.

K-means 클러스터링 3차원 - k-means keulleoseuteoling 3chawon

이렇게 'cluster' 컬럼이 새로 생겼음을 알 수 있죠.

unique 메서드를 활용하면 cluster가 0, 1, 2, 3 이렇게 4개가 생성되었음을 알 수 있습니다. 이제, k-means 알고리즘이 자동으로 군집화해준 이 클러스터가 어떤 특징을 띠고 있는지를 분석해봅시다.

Tech 33. 클러스터링 결과 분석

#먼저 열 이름을 알아보기 쉽게 한글로 바꾼다.
cc.rename(columns = {'mean':'월평균값', 'median':'월중앙값', 'max':'월최댓값', 'min':'월최솟값', 'mem_period':'회원기간'}, inplace=True)
cc
K-means 클러스터링 3차원 - k-means keulleoseuteoling 3chawon
#cluster를 기준으로 데이터 개수 세기
cc.groupby('cluster').count()
K-means 클러스터링 3차원 - k-means keulleoseuteoling 3chawon
#그룹별 특징을 알아보자! -그룹별 평균값
cc.groupby('cluster').mean()
K-means 클러스터링 3차원 - k-means keulleoseuteoling 3chawon

groupby를 통해 다양한 통계량을 알아봅니다.

count() 메서드를 통해선 각 클러스터(그룹)별로 개수가 몇개인지를 셉니다. 그룹 0이 1334개로 가장 많이 들어있고, 그룹3 > 그룹2 > 그룹 1 순이네요.

다음은 mean() 메서드를 통해 각 클러스터별 평균값을 알아봅니다.

  • 그룹 2는 평균 회원기간은 가장 짧지만(7일), 월평균 이용횟수는 8회로 가장 높네요.
  • 그룹 1은 평균 회원기간도 9일로 짧은데다가 월평균 이용횟수는 3회로 가장 낮습니다.
  • 그룹 0, 그룹 3은 평균 회원기간이 그룹 1, 2보다는 깁니다. 그런데 그룹 3은 그룹 0보다 회원기간은 길지만(36일 > 14일), 월평균 이용횟수는 더 적습니다(4.6회 < 5.5회)

이렇게 그룹의 특징을 파악해보면, 그룹별로 다른 캠페인이나 프로모션을 사용할 수 있겠죠?

여기서는 사용한 변수가 단 5개이지만, 더 특징적인 변수를 포함시킨다면 보다 복잡한 클러스터링도 가능합니다.

이제는 이 클러스터링 결과를 눈으로 보기 쉽도록 그래프로 나타내봅시다.

Tech 34. 클러스터링 결과 시각화(차원 축소 활용)

하지만 문제가 생깁니다. 보통 우리가 그리는 그래프는 2차원입니다. 그런데 여기서 사용한 변수는 5개로, 5차원 그래프는 그리기도 힘들뿐더러 이해하기도 쉽지 않습니다.

여기서 우리는 5개의 변수를 2개의 변수로 줄이는 작업을 통해, 2차원 그래프로 나타내봅니다. 이것이 바로 '차원 축소'인데요, 대표적인 차원 축소 방법인 PCA(Principal Component Analysis)를 수행합니다.

※ 차원 축소란?

- 정보를 되도록 잃지 않게 하면서 새로운 축을 만드는 기법.

여기서 사용하는 차원축소 기법 중 하나인 PCA는 주성분분석이라고도 하며, 5개의 변수를 2개의 주성분(정보를 가장 많이 담고 있는) 으로 만듭니다.

from sklearn.decomposition import PCA
X = cc_scaled.copy()

#객체
pca = PCA(n_components=2)

#적용
pca.fit(X)
x_pca = pca.transform(X)
x_pca
K-means 클러스터링 3차원 - k-means keulleoseuteoling 3chawon

앞서 만들었던 표준화 변수 'cc_scaled'를 copy한 새로운 변수 'X'를 만들어 PCA를 적용했습니다.

결과가 array 형태라 보기가 불편하네요. 보기 쉽게 데이터프레임으로 바꿔봅시다. 클러스터 넘버도 포함해서요! 

#x_pca를 보기 쉽게 데이터프레임으로 만들기
pca_df = pd.DataFrame(x_pca)
pca_df['cluster'] = cc['cluster'] 
pca_df.head()
K-means 클러스터링 3차원 - k-means keulleoseuteoling 3chawon

이와 같이 5개였던 변수가 2개의 주성분으로 바뀌었습니다. 어차피 컬럼만 줄어든 것뿐, 행의 수는 똑같으니까 cc 데이터의 'cluster' 컬럼으로 그대로 가져와도 문제없습니다.

이제는 2개의 주성분 변수를 2차원 그래프로 나타내봅시다. 저는 교재 외의 방법으로 seaborn, matplotlib 라이브러리 2가지를 사용해 만들어 보았습니다. 

import matplotlib.pyplot as plt
import seaborn as sns
axs = plt.subplots()
axs = sns.scatterplot(0, 1, hue='cluster', data=pca_df)
K-means 클러스터링 3차원 - k-means keulleoseuteoling 3chawon
seaborn 라이브러리 사용
for i in sorted(pca_df['cluster'].unique()):
    tmp = pca_df.loc[pca_df['cluster'] == i] #해당하는 클러스터 번호일 때 그림을 그리고, for문 실행하며 위에 덧그림 
    plt.scatter(tmp[0], tmp[1])
    plt.legend(sorted(pca_df['cluster'].unique()))
K-means 클러스터링 3차원 - k-means keulleoseuteoling 3chawon
matplotlib 라이브러리 사용

코드는 seaborn이 더 간단해서 저는 보통 seaborn을 애용하는 편입니다. scatterplot 메서드를 사용해 변수 2개를 넣어주고, 'hue' 옵션을 통해 클러스터별로 색상을 다르게 할 수 있습니다.

matplotlib을 사용하려면 for문을 활용해, 각 클러스터별로 scatter plot을 그리고 위에 덧그리는 방식을 활용합니다. 이를테면 클러스터 0번이 파란색으로 칠해지고, 그 다음 클러스터 1번이 주황색으로 그 위에 덧그려집니다. 이를 위해선 클러스터 번호에 해당하는 i 를 for문의 인자로 사용합니다.

참고로 sorted(pca_df['cluster'].unique()) <- 이 코드에서 sorted를 사용하였는데요, 사실 sorted를 쓰지 않아도 그래프는 비슷합니다. sorted는 정렬을 해주는 함수인데, 얘를 쓰지않으면 클러스터 번호가 뒤죽박죽이 되어서 개인적으로 정렬되어 있는 형태를 좋아하는 편이라(ㅎㅎ,,) 사용해주었습니다.

어쨌든, 결과를 보면 두 그래프 모두 아주 깨끗하게 색깔이 나뉘어 있음을 볼 수 있죠? 

정보를 보존한 채, 깔끔하게 데이터를 축소했음을 알 수 있습니다. (참고로, 주성분의 개수를 적절치 않게 설정하면 그래프가 겹치게 그려집니다.)

이제 이 결과를 활용하여, 탈퇴 회원의 경향을 파악해봅시다. 이번 포스팅의 마지막 파트입니다.

Tech 35. 클러스터별 회원 특징 파악하기(탈퇴, 정기적 이용 여부)

앞서 만든 4개의 클러스터에서 지속회원(탈퇴하지 않은 회원)과 탈퇴회원은 얼마나 있을까요?

탈퇴여부를 파악하고 싶으므로, 'cc' 데이터에 원본 데이터인 'c' 데이터를 붙입니다. 'c' 데이터에 'is_deleted(탈퇴여부)' 열이 포함되어 있기 때문입니다. (참고로 is_deleted 값이 1이면 탈퇴, 0이면 지속회원입니다.)

#앞서 만든 cc에서 지속/탈퇴회원 여부를 알아야 하므로 'is_deleted' 열을 추가한다. (이 열은 c 데이터에 있으므로 둘을 조인)
cc_join= pd.concat([cc, c], axis=1)
cc_join.head()
K-means 클러스터링 3차원 - k-means keulleoseuteoling 3chawon
K-means 클러스터링 3차원 - k-means keulleoseuteoling 3chawon

pd.concat 함수를 사용하여 'cc' 데이터에 'c' 데이터가 조인된 'cc_join' 데이터가 만들어졌습니다.

우리가 필요한 것은 클러스터별 탈퇴여부이므로, 이에 대해 groupby를 해서 해당 회원이 몇 명이나 있는지 알아봅시다.

newdf = cc_join.groupby(['cluster','is_deleted'], as_index=False).count()[['cluster', 'is_deleted', 'customer_id']]
newdf
K-means 클러스터링 3차원 - k-means keulleoseuteoling 3chawon

이렇게 결과가 나왔습니다. 교재에서는 여기서 끝냈지만, 저는 클러스터 및 탈퇴여부별 고객 비율이 궁금했습니다. 이를 자동으로 출력해주면 편하지 않을까 싶어 코드를 이렇게 짜봤습니다.

#위의 newdf에서 cluster별 탈퇴 및 미탈퇴회원 비율을 for문으로 뽑아보자.
de0 = (newdf['is_deleted']==0) #지속회원
de1 = (newdf['is_deleted']==1) #탈퇴회원

for i in range(0, 4):
    tmp = (newdf['cluster']==i)
    print('Cluster '+ str(i) + '의 탈퇴회원 비율은 ' + str(round((newdf.loc[(tmp & de1),  'customer_id'].sum() / newdf.loc[tmp, 'customer_id'].sum())*100, 2)) + '%')
    print('Cluster '+ str(i) + '의 지속회원 비율은 ' + str(round((newdf.loc[(tmp & de0),  'customer_id'].sum() / newdf.loc[tmp, 'customer_id'].sum())*100, 2)) + '%')
    print(' ')
K-means 클러스터링 3차원 - k-means keulleoseuteoling 3chawon

이렇게 데이터프레임의 loc 메서드를 활용하면 쉽게 출력해볼 수 있습니다. (저는 loc 애용자입니다,,)

아무튼 뽑힌 결과를 읽어보면, 클러스터별로 탈퇴회원과 지속회원의 수를 알아볼 수 있네요!

  • 클러스터 1: 탈퇴회원만 존재
  • 클러스터 2,3: 지속회원 >> 탈퇴회원
  • 클러스터 0: 지속회원과 탈퇴회원 수 비슷

앞서,

  • 클러스터 2는 평균 회원기간은 가장 짧지만, 월 평균 이용횟수는 가장 높았다.
  • 클러스터 1은 평균 회원기간도 짧고 월평균 이용횟수도 적은 그룹이다.
  • 클러스터 0, 3은 평균 회원기간은 클러스터 1,2보다는 길다. 그런데 그룹3은 그룹0보다 회원기간은 길지만, 이용횟수는 더 적은 편이다.

였음을 참고하면, 다음과 같이 정리해볼 수 있겠네요!

- 클러스터 1: 가장 안좋은 그룹 (가장 짧게 이용하고, 모두 탈퇴함.)
- 클러스터 2: 초반에 불타오르는 좋은 그룹 (이용횟수가 많고 지속회원이 많다. 그런데 평균 회원기간이 짧아서 초기에 의욕적으로 이용하는 그룹인 듯)
- 클러스터 3: 안정적인 그룹 (회원기간이 길고, 이용횟수는 적으나 지속회원이 많다.)
- 클러스터 0: 회원기간이 길고, 탈퇴회원과 지속회원이 비등비등한 그룹.

다음으로는 정기적 이용여부(flag 컬럼)을 살펴봅니다. 이 또한 앞에서와 마찬가지로, 클러스터 및 flag 컬럼별 회원 수를 세면 되겠죠? (*flag 열은 '파이썬 데이터 분석 #3' 포스팅에서 자체제작한 컬럼인데, 1이면 정기적으로 이용하고 0이면 비정기적으로 이용함을 의미합니다)

newdf2 = cc_join.groupby(['cluster','flag'], as_index=False).count()[['cluster', 'flag', 'customer_id']]
newdf2
K-means 클러스터링 3차원 - k-means keulleoseuteoling 3chawon

마찬가지로 이렇게는 보기가 좀 힘드니 앞서 만든 for문을 살짝 수정해서 비율을 뽑아봅니다.

#마찬가지로 for문으로 비율을 자동출력해보자.
flag0 = (newdf2['flag']==0) #비정기회원
flag1 = (newdf2['flag']==1) #정기회원

for i in range(0, 4):
    tmp = (newdf2['cluster']==i)
    print('Cluster '+ str(i) + '의 정기회원 비율은 ' + str(round((newdf2.loc[(tmp & flag1),  'customer_id'].sum() / newdf2.loc[tmp, 'customer_id'].sum())*100, 2)) + '%')
    print('Cluster '+ str(i) + '의 비정기회원 비율은 ' + str(round((newdf2.loc[(tmp & flag0),  'customer_id'].sum() / newdf2.loc[tmp, 'customer_id'].sum())*100, 2)) + '%')
    print(' ')
K-means 클러스터링 3차원 - k-means keulleoseuteoling 3chawon

결과를 해석해보면 앞서 나타난 결과와 비슷합니다.

  • 클러스터 1: 가장 이용을 적게하는 그룹 -> 정기 35% < 비정기 65%
  • 클러스터 2: 초반에 불타오르는 좋은 그룹 -> 정기 93% >> 비정기 6%
  • 클러스터 3: 안정적 이용그룹 -> 정기 99% >> 비정기 0.1%
  • 클러스터 0: 지속, 탈퇴 비등한 그룹 -> 정기 83% > 비정기 17% (정기가 조금 더 많다)

만일 제가 이 헬스장의 마케터라면 클러스터 2와 3에 해당하는 회원을 꽉 잡을 마케팅을 해볼 것 같네요! 

이렇게 이번 포스팅을 마칩니다. 이번 포스팅에서는 헬스장 고객 데이터에 클러스터링(군집분석)을 활용해, 좀 더 구체적인 데이터 분석을 해보았습니다.

지금까지의 분석을 통해, 회원의 이용 방법(회원의 행동 데이터)가 정말 중요함을 알 수 있었습니다.

다음 포스팅에서는 회원의 "이용 횟수"를 예측하는 모델을 만들어봅니다. 고객의 과거 행동 데이터로부터, 알지 못하는 다음 달 이용횟수를 예측해봅니다. 

감사합니다. 궁금한 점이 있으시면 언제든 댓글 주세요 :)