동작 방식
k-means의 기본 원리는 '유사한 데이터는 Centroid(중심점)로부터 가까이에 모여있다' 이다.
동작 과정
1단계: Centroid 배치
먼저 클러스터의 개수를 의미하는 k를 정해주어야 한다. (*아래 예시에서는 우선 '2'로 설정함) 그리고 k의 값만큼 Centroid를 생성하여 임의로 배치한다.
2단계: 클러스터 형성
생성한 Centroid와 각 데이터 사이의 거리를 계산하여 가까이에 있는 데이터들을 하나의 클러스터로 묶어준다.
3단계: Centroid 위치 갱신
클러스터에 속해있는 데이터들의 중심으로 Centroid의 윛치를 이동한다. 이때, 데이터들 사이의 중심을 찾기 위하여 평균값(means)을 사용한다.
4단계: 클러스터 재형성
새롭게 위치한 Centroid를 기준으로 각 데이터와의 거리를 다시 계산하여 가까운 데이터들을 하나의 클러스터로 묶는다.
5단계: Centroid 위치 갱신
새롭게 형성된 클러스터의 중심으로 Centroid를 다시 이동시킨다.
예시에서는 두 번 정도 위치를 갱신하였을 때, 클러스터가 잘 구분되었지만 실제로는 위의 단계들을 여러 번 반복하여 Centroid의 위치를 계속해서 옮겨 줘야 한다. 그러다가 k개의 Centroid가 더 이상 위치를 갱신하지 않게 되면, 그 위치에서 가장 가까운 거리에 있는 데이터들이 하나의 클러스터가 된다.
모델 학습
모델은 넓게 두 가지 의미가 있는데, 분석 방법론을 의미하기도 하는 것 하나와 분석법을 적용하고 그 결과물을 저장할 수 있는 프로그램을 의미하는 것 두가지가 있다.
학습은 넓은 의미에서 모델에게 데이터를 전달해서 분석시키는 과정이다.
# 모델 학습에 사용될 라이브러리 불러오기
# k-means를 할 때에는 scikit-leanr이라는 라이브러리가 사용됨
from sklearn.cluster import KMeans
# scikit-learn은 데이터 분석을 위한 여러 작업을 간단히 처리해주는 라이브러리
# 클러스터 분석을 위한 여러 도구들도 제공함(https://www.codeit.kr/tutorials/39/scikit-learn-%EC%82%AC%EC%9A%A9%ED%95%B4-%EB%B3%B4%EA%B8%B0, https://scikit-learn.org/stable/user_guide.html)
# k-means(k=2)
model = KMeans(n_clusters=2, random_state=123)
# n_clusters: 클러스터를 몇 개로 나눌지, 즉 k를 몇으로 할 지 결정해주는 파라미터
# k를 몇으로 하냐에 따라 k-means 모델의 성능이 크게 달라진다. 이에 적절한 클러스터의 개수를 구하는게 중요함
# random_state: 여러번 반복해서 모델을 학습시킬 때 동일한 결과가 나올 수 있도록 해주는 난수
# 123으로 지정하면 예시와 동일한 결과를 확인할 수 있음
# 모델 학습
model.fit(scaled_df)
# 선언한 model에 scaled_df를 학습시킨다. 이때, sklearn의 함수인 fit()이 사용된다.
클러스터 시각화
# label 컬럼 생성
scaled_df['label'] = model.predict(scaled_df)
scaled_df
# 각 군집의 중심점
centers = model.cluster_centers_ # 각 군집의 중심점 좌표를 담고 있는 배열, 각 중심점은 모델이 학습한 결과로 각 군집의 중심점을 가져오는 것
# 산점도 그리기
sns.scatterplot(data=scaled_df, x='total_buy_cnt', y='total_price', hue='label', s=200, palette='bright')
# scaled_df 데이터프레임에서 total_buy_cnt(총 구매 횟수)와 total_price(총 구매 금액) 열을 이용하여 산점도를 그림
# 군집의 중심점 시각화
sns.scatterplot(x=centers[:,0], y=centers[:,1], color='black', alpha=0.8, s=400 )
# centers[:,0]와 centers[:,1]은 각각 중심점의 x좌표(총 구매 횟수)와 y좌표(총 구매 금액)를 나타냄
2개의 Cenntroid를 중심으로 클러스터가 잘 구분된 것을 확인할 수 있다.
예제
k-means를 진행해 보고, 각 클러스터에 몇 명의 고객이 할당되었는지 구해 보세요. (k는 4로, random_state는 123으로 지정해 주세요.)
from sklearn.cluster import KMeans
# Kmeans() 함수로 모델 선언(*n_clusters=4, random_state=123)
model = KMeans(n_clusters=4, random_state=123)
# model.fit()으로 데이터 학습
model.fit(scaled_df)
# model.predict(): k-means 패키지에서 학습시킨 모델로 데이터별 라벨을 구하는 함수
scaled_df['label'] = model.predict(scaled_df)
# 클러스터별 할당된 데이터 개수 집계
scaled_df['label'].value_counts()
# 클러스터 시각화
centers = model.cluster_centers_
sns.scatterplot(data=scaled_df, x='total_buy_cnt', y='total_price', hue='label', s=200, palette='bright')
sns.scatterplot(x=centers[:,0], y=centers[:,1], color='black', alpha=0.8, s=400)
k 선정이 중요한 이유
k-means의 성능은 클러스터의 개수(k)에 따라 달라진다.
아래 이미지와 같이 분포된 데이터를 k-means로 클러스터링 할 때 클러스터의 개수를 몇개롤 하느냐에 따라 결과가 달라진다.
왼쪽은 k를 2로 했을 때의 결과이다. 이때 거리가 먼 데이터가 같은 클러스터로 묶였고, 가까이에 있는 데이터는 다른 클러스터로 구분된다. 즉 잘 된 클러스터링이라고 하긴 어렵다.
반면, 오른쪽의 이미지는 클러스터의 개수가 너무 많을 때의 결과이다. 클러스터의 개수가 너무 많으면 클러스터링을 하는 의미가 사라진다. 비슷한 데이터들을 묶어 클러스터별 특성을 확인해야 하는데 k가 너무 많으면 그런 특징들이 잘 나타나지 않기 때문이다.
k를 적절하게 잘 설정하면 아래 이미지와 같이 데이터의 특징에 맞게 클러스터가 잘 나눠지는 것을 볼 수있다. k=3으로 하는게 적절한 것으로 보인다.
최적의 k 선정 기준 - intertia
최적의 클러스터 개수를 찾기 위해선 k-means가 잘 됐다는 것을 판단할 수 있는 기준이 필요하다.
k-means는 k개의 Centroid에 가까이 모여 있는 데이터들을 하나의 클러스터로 묶어 주는 방법이다. 그렇다면 클러스터마다 속한 데이터와 Centroid 사이 거리의 합이 작아아 잘 된 클러스터링이라고 볼 수 있다.
이를 확인하기 위해 사용하는 값이 바로 intertia(이너시아)이다. intertia는 각 클러스터링에 속한 데이터들과 Centroid 사이의 거리를 제곱해서 전부 더한 값이다.
아래 이미지는 7개의 데이터를 두 개의 클러스터로 나눠 본 예시이다. 이렇게 클러스터를 나누면 모든 데이터와 Centroid 사이의 거리가 정해지는데 이 값들을 전부 제곱해서 더한 값이 inertia이다. 참고로 아래 예시의 inertia는 43이다.
Elbow Method
K-means 클러스터링에서 최적의 군집 개수(K)를 결정하기 위한 시각적 도구다. 이 방법은 다양한 K 값에 대해 모델을 학습하고, 각 K에 대한 inertia 값을 계산한 후, 그래프를 통해 최적의 K 값을 선택한다.
개념
inertia 계산: K-means 클러스터링을 여러 개의 K 값에 대해 실행하고, 각 K에 대한 inertia 값을 계산한다. inertia는 군집 내의 데이터 포인트와 해당 군집 중심점 간의 거리의 제곱합이다.
그래프 작성: K 값에 대한 관성 값을 플로팅하여 그래프를 만든다. X축은 군집의 개수(K)이고, Y축은 관성 값이다.
엘보(Elbow) 찾기: 그래프에서 관성 값이 급격히 감소하다가 완만하게 감소하는 지점을 찾는다. 이 지점이 엘보(팔꿈치)로 알려져 있으며, 최적의 K 값을 나타낸다. 엘보 지점에서 군집의 개수를 결정하면, 추가적인 군집 수가 inertia 값을 크게 줄이지 않으면서 모델의 복잡성만 증가시키기 때문이다.
# scaled_df에 추가했던 label 열을 제거
scaled_df = scaled_df.drop(['label'], axis=1)
# 군집화 알고리즘 (예: K-means)은 입력 데이터의 특성만을 사용하여 군집을 형성한다. 이 과정에서 label은 입력 데이터의 일부가 아니라, 모델이 예측한 결과임
# 모델을 학습하고 평가할 때 label 열이 데이터에 포함되어 있으면, 그 열의 정보가 결과에 영향을 미칠 수 있으므로 label 열을 제거하고, 순수한 특성 데이터만을 사용하여 군집화를 수행함
# inertia 값 저장할 리스트
inertias = []
for k in range(1, 16): # k값의 범위 1~15로 지정
model = KMeans(n_clusters=k, random_state=123)
model.fit(scaled_df)
inertias.append(model.inertia_)
# k값에 따른 inertia값 시각화
sns.lineplot(x=range(1, 16), y=inertias, marker='o')
k가 커질수록 inertia는 계속 작아지고 있는 것을 볼 수 있다.
그러면 k가 15일 때 클러스터링이 가장 잘 된 걸까?
그렇지 않다. inertia는 클러스터의 개수가 늘어날 수록 계속 작아진다. 그러다가 클러스터가 데이터의 개수만큼 있을 땐 0이 된다.
하지만 클러스터의 개수가 많아질수록 클러스터링을 하는 의미가 사라지므로 inertia가 작아진다고 무조건 좋은 것은 아니다.
최적의 클러스터 개수는 inertia가 충분히 작지만, 분석 목적에 부합하도록 적당해야 한다. 보통 그 지점은 시각화 한 그래프이 기울기가 급격하게 줄어드는 구간으로 정의한다.
예를 들어 위의 그래프를 보면, k값이 2~3 사이인 구간에서 급격하게 줄어들고 있는 것을 알 수 있다. 여기서 최적의 k값은 2나 3이라고 볼 수있다. 이때 그래프이 모양이 팔꿈치 모양과 닮았다고 하여 'Elbow Method'라고 부른다.
그런데 Elbow Method를 통해 나온 결과를 반드시 따라야하는 것으 ㄴ아니다. 보조 지표 정도로 활용하고, 상황이나 목적에 맞게 클러스터 개수를 조금 다르게 설정해도 괜찮다.
예를 들어 고객들을 좀 더 다양한 세그먼트로 나눠 보고 싶다면 클러스터를 2~3개 대신 5개로 설정할 수도 있다. k값이 2~3 사이일 때 이미 기울기가 급격히 줄어들긴 했지만 5를 기점으로도 기울기 변화가 거의 없어질 정도로 완만해지고 있따고 볼 수 있다.
예제
k-means의 inertia 값을 구할 수 있는 그래프를 그려주세요. Elbow Method를 그리기 위한 클러스터 개수는 1부터 15까지로, random_state는 123으로 설정해 주세요.
inertias = []
for k in range(1, 16):
model = KMeans(n_clusters=k, random_state=123)
model.fit(scaled_data)
inertias.append(model.inertia_)
sns.lineplot(x=range(1, 16), y=inertias, marker='o')
클러스터링 결과 해석
스케일링 된 데이터로 분석하면 고객의 특성을 정확하게 반영하기 어렵다. 그러므로 scaled_df로는 클러스터링까지만 진행하고, 실제 분석은 데이터의 특성이 그대로 남아있는 sales_df를 사용해보자.
model = KMeans(n_clusters=5, random_state=123)
model.fit(scaled_df)
sales_df['label'] = model.predict(scaled_df)
# predict() 함수를 통해 학습된 모델에 기반해 특정 고객이 어느 클러스터로 구분되는지 확인할 수 있음
# 클러스터 시각화
sns.scatterplot(data=sales_df, x='total_buy_cnt', y='total_price', hue='label', palette='bright')
# 각 클러스터에 속해있는 고객 수 확인하기
pd.DataFrame(sales_df['label'].value_counts())
groupby_df = sales_df.groupby('label').mean()
groupby_df
# 평균 개당 가격(price_mean) = 총 구매 금액(total_price) / 총 구매 수량(total_buy_cnt)
groupby_df['price_mean'] = groupby_df['total_price'] / groupby_df['total_buy_cnt']
groupby_df
클러스터 2의 고객들은 총 구매 수량과 금액이 가장 많은 것을 알 수 있다. 하지만 개당 구매 가격은 적은 편이다. 해당 클러스터의 고객들은 가격이 낮은 물품을 많이 구매한다는 특징이 있는 걸로 해석할 수 있다.
클러스터 0의 고객들은 총 구매 수량이 많은 편은 아니지만, 구매한 품목의 평균 금액은 높은 편이다. 수량이 많지 않지만 비싼 물품을 사는 고객들이라는 걸 알 수 있다.
이러한 결과를 바탕으로 프로모션을 진행하면 고객 클러스터별 특징에 따라 맞춤 전략을 짤 수 있다. 저렴한 물품을 여러번 구매하면 사은품을 주는 행사는 2번 클러스터 고객들에게, 가격이 높은 제품의 할인 프로모션은 0번 클러스터 고객들에게 진행하면 효과가 더 좋을 것이다.
k-means의 장점
k-means는 변수들에 대한 배경지식, 역할, 영향도에 대해 모르더라도 데이터 사이의 거리만 구할 수 있다면 쉽게 사용할 수 있다.
또, 알고리즘이 비교적 쉬운 수식으로 이루어졌기 때문에 이해와 해석이 용이하다.
즉, 어떠한 데이터에도 적용하기 쉽고, 모델에 대한 이해와 해석도 간단하게 할 수 있다.
k-means의 단점
1. 최적의 클러스터 개수인 k를 정하는게 어렵다.
Elbow Method로 단서를 얻는 것은 가능하지만 합리적인 추론을 위한 가이드일 뿐 정답이 아니다.
2. 이상치에 영향을 많이 받는다.
이상치가 포함된 데이터일 경우 클러스터의 중심(Centroid)을 업데이트 하는 과정에서 Centroid의 위치가 크게 변동되고, 클러스터가 원하지 않는 방식으로 묶일 수 있다.
이상치가 많은 데이터라면 전처리 과정에서 전부 제거해주거나, k-means 말고 다른 모델을 사용하는게 더 좋을 수 있다.
3. 초기 Centroid의 설정에 따라 결과가 달라진다.
또 적절하지 않은 곳에 배치되면 위치를 너무 많이 옮겨야 해서 연산이 오래 걸릴 수 있고, 경우에 따라서는 특정한 한 곳으로 수렴하지 못하는 경우도 발생할 수 있다.
이러한 이유 때문에 데이터들 간의 거리를 반영하여 전략적으로 초기 Centroid 위치를 찾아 주는 과정이 필요해졌고, k-means++모델이 등장했다.
k-means++는 Centroid가 좀 더 좋은 위치에 잘 배치되도록 해준다. 때문에 일반 k-means보다 안정적이다.
사용방법은 KMeans()를 사용하고, 파라미터로 init='k-means++'를 추가해주면 된다.
from sklearn.cluster import KMeans
model = KMeans(n_clusters=k, init='k-means++')
4. 차원이 높은 데이터에 적용할 때, 성능이 떨어진다. - 차원의 저주
변수가 더 많아질수록 모델의 성능이 더 나빠지는 현상을 '차원의 저주'라고 한다.
1000개의 데이터를 각 차원에 배치해보자.
위의 이미지와 같이 차원이 커질수록 데이터간의 거리가 멀어지고, 전체 공간에서 데이터가 차지하는 비중은 적어진다.
그리고 데이터 간의 거리가 멀어질수록 클러스터링 진행 시 데이터간 유사성을 계산하는게 어렵고 정확성도 떨어진다.
특히, 거리 기반 모델인 k-means에서는 문제가 더 크게 발생한다.
가까운 데이터와 먼 데이터의 구분이 어려워지기 때문에 클러스터를 나누는 성능이 저하될 수 있다. 그러므로 변수가 너무 많은 데이터를 거리 기반 알고리즘으로 클러스터링 하면 문제가 될 수 있다.