21 분 소요

클러스터링, 군집화

  • 데이터가 주어졌을 때, 여러 개의 그룹으로 나누는 것
  • 유사한 특성을 가진 그룹을 발견해내는 일

  • 내부 멤버들 간의 사이(Intra-clust)는 가깝고, 그룬 간 사이(Inter-cluster)는 멀게 그룹을 만드는 것
  • 그룹에 대한 정답이 있으면 분류 문제이지만 클러스터링은 비지도 학습으로 어느 그룹에 있는지 정답은 없다.

언제 사용할까?

유사한 뉴스 그룹 : 문서 군집화

가까운 위치 좌표끼리 묶기

유사한 유저군 나누기 : 마켓 세분화

  • 시장을 적절한 수로 나누고, 각 시장, 타겟 별로 효과적인 정책을 찾아낸다.

SNS 관심사 기반, 자율 주행 이미지 인식 클러스터링

종류

Partition-based Clustering

  • 미리 군집(그룹)의 수를 정해두고 클러스터링하는 방식
  • 대표적인 알고리즘
    • K-means
    • K-Medoids

### K-means Clustering

  1. 사용자가 미리 군집수(k) 정의
  2. 처음에는 랜덤으로 k개의 중심점(Centroid)를 정함 - 각 군집은 하나의 중심(Centroid)를 가지고 있다.
  3. 데이터 point를 돌면서 가까운 중심점이 있는 그룹에 각 데이터를 할당 - 이 때 각 개체 간 거리는 Euclidean distance 사용
  4. 모두 그룹을 할당했다면, 각 그룹마다 새 중심점(Centroid) - 클러스터 내 평균점을 새로 구함
  5. 3-4 단계를 반복하다가 더 이상 그룹 이동이 일어나지 않으면 멈춤

Hierarchical based Clustering

  • 여러개의 군집 중에서 가장 유사도가 높은 혹은 거리가 가까운 군집 두 개를 선택하여 하나로 합치면서 군집 개수를 줄여가는 방법으로 agglomerative clustering(합체 군집)라고도 한다.

  • 가까운 데이터를 서로 묶기 위해서는 먼저 각 군집 간 거리를 계산해야 한다.

Centroid Distance

  • 각 군집의 중심점 사이의 거리를 계산하는 방법
  • 계층 클러스터링이 아니더라도 사용할 수 있는 방법

Median Distance

  • Hierarchical based Clustering에서 사용할 수 있는 방법
  • 군집 u가 군집 s와 군집 t의 결합으로 생성된 군집이라면, 중심점을 새로 계산하지 않고, 기존 s와 t의 중심점의 평균을 사용한다 -> 더 빠른 계산 가능

Density based Clustering

  • 데이터가 밀집한 정도, 밀도를 이용한 클러스터링 방법

DBSCAN clustering

  • 군집의 개수를 사용자가 지정할 필요가 없다.
  • 초기 데이터로부터 근접한 데이터를 찾아나가는 방법으로 군집을 확장
  • 필요한 파라미터는 2가지로 ‘근접하다’를 정의
    • 최소거리 a (다른 점들을 이웃으로 묶기 위함)
    • 최소 데이터 개수 b (밀집 지역으로 정의하기 위함)
  • 최소거리 a안에 있는 데이터는 이웃이다.
  • 최소거리 a안에 최소 데이터 개수 b 이상의 데이터가 있으면, 이 데이터를 core로 정의
  • core데이터는 하나의 클러스터를 형성하고, 그 core와 a 거리 내에 있는 점들은 같은 클러스터로 분류

  • K-means는 중심점을 기준으로 그룹을 형성하기 때문에, 원의 형태로 군집이 만들어짐
  • 서로 이웃한 데이터들을 같은 클러스터에 포함시키기 때문에 불특정한 모양의 클러스터가 형성

정답이 없는 클러스터링, 어떻게 평가할까?

이미 정답이 있는 분류문제와 달리 성능 기준을 만들기 어렵다. 따라서 기준을 사용할 수 있지만, 그 중 클러스터에 대한 정답이 없는 경우 사용할 수 있는 기준을 알아보자

Silhouette Coefficient: 실루엣 계수

  • 모든 데이터 쌍 (i, j)에 대한 거리나 dissimilarity를 구한다.
    • a_i : i와 같은 군집에 속한 원소들의 평균 거리
    • b_i : i와 다른 군집 중 가장 가까운 군집까지의 평균 거리

-> 만약 a 같은 군집 내 평균 거리가 더 가깝다면 양수, 다른 군집과의 거리가 가깝다면 음수가 나온다.


다양한 클러스터링 방법을 통해 유저 세그먼트를 나눠보자

라이브러리 불러오기

import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from matplotlib import colors

👣 데이터 살펴보기

# 나의 구글 드라이브를 mount (colaboratory 노트북이 떠있는 위치에서 드라이브의 파일에 접근할 수 있게 만드는 것) 하는 명령어
from google.colab import drive
drive.mount('/content/drive')
# 판다스로 데이터 불러오기
# 아래에 customer_personality_analysis.csv 가 있는 경로로 이동합니다
# 경로 설정
DRIVE_PATH = "/content/drive/MyDrive/" # 내 드라이브의 경로이다
FILE_PATH_IN_MY_DRIVE = "zerobase/유저 데이터 분석/유저 군집 분석하기/data/customer_personality_analysis.csv" # 내 드라이브 내 파일이 있는 경로
PATH = DRIVE_PATH +  FILE_PATH_IN_MY_DRIVE

df = pd.read_csv(PATH , sep="\t") # csv 파일 읽어오기
df.head()

# ID,Year_Birth,Education
# ID\tYear\t
ID Year_Birth Education Marital_Status Income Kidhome Teenhome Dt_Customer Recency MntWines ... NumWebVisitsMonth AcceptedCmp3 AcceptedCmp4 AcceptedCmp5 AcceptedCmp1 AcceptedCmp2 Complain Z_CostContact Z_Revenue Response
0 5524 1957 Graduation Single 58138.0 0 0 04-09-2012 58 635 ... 7 0 0 0 0 0 0 3 11 1
1 2174 1954 Graduation Single 46344.0 1 1 08-03-2014 38 11 ... 5 0 0 0 0 0 0 3 11 0
2 4141 1965 Graduation Together 71613.0 0 0 21-08-2013 26 426 ... 4 0 0 0 0 0 0 3 11 0
3 6182 1984 Graduation Together 26646.0 1 0 10-02-2014 26 11 ... 6 0 0 0 0 0 0 3 11 0
4 5324 1981 PhD Married 58293.0 1 0 19-01-2014 94 173 ... 5 0 0 0 0 0 0 3 11 0
print("데이터 전체의 행 수: ",len(df))
print("데이터 컬럼 수: ",len(df.columns))

=>

데이터 전체의  :  2240
데이터 컬럼 :  29

데이터 컬럼 종류

People (사람)

  • ID: Customer’s unique identifier
  • Year_Birth: Customer’s birth year
  • Education: Customer’s education level (교육 수준)
  • Marital_Status: Customer’s marital status (결혼 상태)
  • Income: Customer’s yearly household income
  • Kidhome: Number of children in customer’s household (어린 아이의 수)
  • Teenhome: Number of teenagers in customer’s household (10대 수)
  • Dt_Customer: Date of customer’s enrollment with the company (서비스 가입 날짜)
  • Recency: Number of days since customer’s last purchase (마지막으로 구매한 날로부터 얼마가 지났는지)
  • Complain: 1 if customer complained in the last 2 years, 0 otherwise

Products (상품)

  • MntWines: Amount spent on wine in last 2 years
  • MntFruits: Amount spent on fruits in last 2 years
  • MntMeatProducts: Amount spent on meat in last 2 years
  • MntFishProducts: Amount spent on fish in last 2 years
  • MntSweetProducts: Amount spent on sweets in last 2 years
  • MntGoldProds: Amount spent on gold in last 2 years

Promotion (프로모션)

  • NumDealsPurchases: Number of purchases made with a discount (할인 받아 구매한 수)
  • AcceptedCmp1: 1 if customer accepted the offer in the 1st campaign, 0 otherwise
  • AcceptedCmp2: 1 if customer accepted the offer in the 2nd campaign, 0 otherwise
  • AcceptedCmp3: 1 if customer accepted the offer in the 3rd campaign, 0 otherwise
  • AcceptedCmp4: 1 if customer accepted the offer in the 4th campaign, 0 otherwise
  • AcceptedCmp5: 1 if customer accepted the offer in the 5th campaign, 0 otherwise
  • Response: 1 if customer accepted the offer in the last campaign, 0 otherwise

Place (구매 장소)

  • NumWebPurchases: Number of purchases made through the company’s web site
  • NumCatalogPurchases: Number of purchases made using a catalogue
  • NumStorePurchases: Number of purchases made directly in stores
  • NumWebVisitsMonth: Number of visits to company’s web site in the last month

👣 데이터 정제하기: Cleaning

  • 결측치 (Missing Values)와 이상치 (Outliers) 를 제거하자
  • 데이터를 바로 사용할 수 있으면 좋겠지만, 바로 사용할 수 있는 경우가 많지 않다.
    • 결측치가 있을 수 있고, 컴퓨터가 이해할 수 있는 형태 (숫자형)으로 바꿔주어야 하는 경우도 있다.
    • Outlier 가 너무 크다면, 이상치로 인해 모델이 왜곡될 수 있다.

아래 데이터를 살펴보자

# .info() 함수는 데이터에 대한 전반적인 정보를 나타냅니다. 
# df를 구성하는 행과 열의 크기, 컬럼명, 컬럼을 구성하는 값의 자료형 등을 출력해줍니다.
df.info()

=>

<class 'pandas.core.frame.DataFrame'>
RangeIndex: 2240 entries, 0 to 2239
Data columns (total 29 columns):
 #   Column               Non-Null Count  Dtype  
---  ------               --------------  -----  
 0   ID                   2240 non-null   int64  
 1   Year_Birth           2240 non-null   int64  
 2   Education            2240 non-null   object 
 3   Marital_Status       2240 non-null   object 
 4   Income               2216 non-null   float64
 5   Kidhome              2240 non-null   int64  
 6   Teenhome             2240 non-null   int64  
 7   Dt_Customer          2240 non-null   object 
 8   Recency              2240 non-null   int64  
 9   MntWines             2240 non-null   int64  
 10  MntFruits            2240 non-null   int64  
 11  MntMeatProducts      2240 non-null   int64  
 12  MntFishProducts      2240 non-null   int64  
 13  MntSweetProducts     2240 non-null   int64  
 14  MntGoldProds         2240 non-null   int64  
 15  NumDealsPurchases    2240 non-null   int64  
 16  NumWebPurchases      2240 non-null   int64  
 17  NumCatalogPurchases  2240 non-null   int64  
 18  NumStorePurchases    2240 non-null   int64  
 19  NumWebVisitsMonth    2240 non-null   int64  
 20  AcceptedCmp3         2240 non-null   int64  
 21  AcceptedCmp4         2240 non-null   int64  
 22  AcceptedCmp5         2240 non-null   int64  
 23  AcceptedCmp1         2240 non-null   int64  
 24  AcceptedCmp2         2240 non-null   int64  
 25  Complain             2240 non-null   int64  
 26  Z_CostContact        2240 non-null   int64  
 27  Z_Revenue            2240 non-null   int64  
 28  Response             2240 non-null   int64  
dtypes: float64(1), int64(25), object(3)
memory usage: 507.6+ KB
  • 4 Income : non-null인 row의 수는 2216 개 - 비어있는 값이 24개 정도 있다.
  • 7 Dt_Customer : 날짜형이지만 날짜가 아닌 String(object) 로 표시되어 있다.

결측치 제거하기

  • 결측치를 제거하는 방법은 크게 3가지이다.
    • 결측치의 비중이 작다면 제거한다.
    • 결측치를 빈도가 가장 높은 값이나 평균으로 채운다.
    • 결측치를 예측하는 모델을 만들어 예측값으로 채운다.
  • 해당 데이터에서는 24개로 결측치가 많지 않기 때문에 제거한다.
# missing value 가 있는 row 를 제거한다.
# 결측치가 있는 경우 1. 평균치로 채우거나 2. 예측하거나 3. 데이터가 많지 않으면 제거한다.
df = df.dropna()
len(df)

=>

2216

날짜 데이터 정제하기

  • 가입한지 얼마 되지 않은 고객과 가장 오래된 고객을 구해보자
# string으로 된 date 를 datetime 함수를 쓰기 위해 datetime 형태로 바꾼다.
# Dt_Customer: 가입한 날짜
df["Dt_Customer"] = pd.to_datetime(df["Dt_Customer"])
for i in df["Dt_Customer"][:5]:
  print(i.date())
  
=>

2012-04-09
2014-08-03
2013-08-21
2014-10-02
2014-01-19
dates = []
for i in df["Dt_Customer"]:
    i = i.date()
    dates.append(i)  
    
print(dates)
print(max(dates))
print(min(dates))

=>

[datetime.date(2012, 4, 9), datetime.date(2014, 8, 3), datetime.date(2013, 8, 21), ... , datetime.date(2014, 1, 25), datetime.date(2014, 1, 24), datetime.date(2012, 10, 15)]
2014-12-06
2012-01-08
  • 날짜를 숫자로! 가입 날짜(date)를 가입한 후 지난 일수(int)로 바꿔준다.
days = [] # 데이터를 담을 수 있는 빈 list
recent_date = max(dates) # 2014-12-06, 
# 가장 최근 가입일 (해당 날짜를 기준으로 기존 날짜의 값을 빼준다.)
for date in dates:
    day_difference = recent_date - date
    days.append(day_difference)
df["Customer_For"] = days


df["Customer_For"].head()

=>

0   971 days
1   125 days
2   472 days
3    65 days
4   321 days
Name: Customer_For, dtype: timedelta64[ns]
# 날짜를 숫자로 type 을 변경해준다
df["Customer_For"] = pd.to_numeric(df["Customer_For"], errors="coerce")

# errors: error는 총 3개의 옵션이 존재합니다.
# errors = 'ignore' -> 만약 숫자로 변경할 수 없는 데이터라면 숫자로 변경하지 않고 원본 데이터를 그대로 반환합니다.
# errors = 'coerce' -> 만약 숫자로 변경할 수 없는 데이터라면 기존 데이터를 지우고 NaN으로 설정하여 반환합니다.
# errors = 'raise' -> 만약 숫자로 변경할 수 없는 데이터라면 에러를 일으키며 코드를 중단합니다.

카테고리 데이터 정리하기

  • 앞에서 본 데이터 중 카테고리 형태의 데이터는 Education 과 Marital_Status :)
df[['Education', 'Marital_Status']].head(10)
Education Marital_Status
0 Graduation Single
1 Graduation Single
2 Graduation Together
3 Graduation Together
4 PhD Married
5 Master Together
6 Graduation Divorced
7 PhD Married
8 PhD Together
9 PhD Together
df["Marital_Status"].value_counts()

=>

Married     864
Together    580
Single      480
Divorced    232
Widow        77
Alone         3
Absurd        2
YOLO          2
Name: Marital_Status, dtype: int64
df["Education"].value_counts()

=>

Graduation    1127
PhD            486
Master         370
2n Cycle       203
Basic           54
Name: Education, dtype: int64
# Marital_Status 정리하기, 파트너와 같이 사는지, 혼자사는지 여부
df["Living_With"]=(
    df["Marital_Status"]
    .replace(
      {"Married":"Partner", 
       "Together":"Partner", 
       "Absurd":"Alone", 
       "Widow":"Alone", 
       "YOLO":"Alone", 
       "Divorced":"Alone", 
       "Single":"Alone"
    })
)

# the number of children, Kidhome 과 Teenhome 을 분리하지 않고 합쳐준다.
df["Children"]=df["Kidhome"]+df["Teenhome"]

# 위의 데이터를 통해 가족 사이즈도 구할 수 있다.
df["Family_Size"] = (
    df["Living_With"].replace({"Alone": 1, "Partner":2})
    + df["Children"]
)

# 아이가 있는지, 없는지
df["Is_Parent"] = np.where(df.Children> 0, 1, 0)
# 교육 상태 정리하기
df["Education"]=df["Education"].replace({"Basic":"Undergraduate","2n Cycle":"Undergraduate", "Graduation":"Graduate", "Master":"Postgraduate", "PhD":"Postgraduate"})
# 생년 월일을 통해 나이를 구할 수 있다.
df["Age"] = 2021-df["Year_Birth"]

# 다양한 잡화 구매를 더해서 총 사용한 비용 Spent 를 구한다.
df["Spent"] = df["MntWines"]+ df["MntFruits"]+ df["MntMeatProducts"]+ df["MntFishProducts"]+ df["MntSweetProducts"]+ df["MntGoldProds"]
# 모두 동일한 값, 필요없는 컬럼
df.Z_CostContact.value_counts()
df.Z_Revenue.value_counts()

=>

11    2216
Name: Z_Revenue, dtype: int64
# 컬럼명 짧게 변경
df=df.rename(columns={"MntWines": "Wines","MntFruits":"Fruits","MntMeatProducts":"Meat","MntFishProducts":"Fish","MntSweetProducts":"Sweets","MntGoldProds":"Gold"})

# 중복되거나 필요없는 컬럼 제거
to_drop = ["Marital_Status", "Dt_Customer", "Z_CostContact", "Z_Revenue", "Year_Birth", "ID"]
df = df.drop(to_drop, axis=1)


df.head()
Education Income Kidhome Teenhome Recency Wines Fruits Meat Fish Sweets ... AcceptedCmp2 Complain Response Customer_For Living_With Children Family_Size Is_Parent Age Spent
0 Graduate 58138.0 0 0 58 635 88 546 172 88 ... 0 0 1 83894400000000000 Alone 0 1 0 64 1617
1 Graduate 46344.0 1 1 38 11 1 6 2 1 ... 0 0 0 10800000000000000 Alone 2 3 1 67 27
2 Graduate 71613.0 0 0 26 426 49 127 111 21 ... 0 0 0 40780800000000000 Partner 0 2 0 56 776
3 Graduate 26646.0 1 0 26 11 4 20 10 3 ... 0 0 0 5616000000000000 Partner 1 3 1 37 53
4 Postgraduate 58293.0 1 0 94 173 43 118 46 27 ... 0 0 0 27734400000000000 Partner 1 3 1 40 422

Outlier 제거하기 우리가 정리한 데이터를 다시 한 번 살펴보자

 df.describe()
Income Kidhome Teenhome Recency Wines Fruits Meat Fish Sweets Gold ... AcceptedCmp1 AcceptedCmp2 Complain Response Customer_For Children Family_Size Is_Parent Age Spent
count 2216.000000 2216.000000 2216.000000 2216.000000 2216.000000 2216.000000 2216.000000 2216.000000 2216.000000 2216.000000 ... 2216.000000 2216.000000 2216.000000 2216.000000 2.216000e+03 2216.000000 2216.000000 2216.000000 2216.000000 2216.000000
mean 52247.251354 0.441787 0.505415 49.012635 305.091606 26.356047 166.995939 37.637635 27.028881 43.965253 ... 0.064079 0.013538 0.009477 0.150271 4.423735e+16 0.947202 2.592509 0.714350 52.179603 607.075361
std 25173.076661 0.536896 0.544181 28.948352 337.327920 39.793917 224.283273 54.752082 41.072046 51.815414 ... 0.244950 0.115588 0.096907 0.357417 2.008532e+16 0.749062 0.905722 0.451825 11.985554 602.900476
min 1730.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 0.000000 ... 0.000000 0.000000 0.000000 0.000000 0.000000e+00 0.000000 1.000000 0.000000 25.000000 5.000000
25% 35303.000000 0.000000 0.000000 24.000000 24.000000 2.000000 16.000000 3.000000 1.000000 9.000000 ... 0.000000 0.000000 0.000000 0.000000 2.937600e+16 0.000000 2.000000 0.000000 44.000000 69.000000
50% 51381.500000 0.000000 0.000000 49.000000 174.500000 8.000000 68.000000 12.000000 8.000000 24.500000 ... 0.000000 0.000000 0.000000 0.000000 4.432320e+16 1.000000 3.000000 1.000000 51.000000 396.500000
75% 68522.000000 1.000000 1.000000 74.000000 505.000000 33.000000 232.250000 50.000000 33.000000 56.000000 ... 0.000000 0.000000 0.000000 0.000000 5.927040e+16 1.000000 3.000000 1.000000 62.000000 1048.000000
max 666666.000000 2.000000 2.000000 99.000000 1493.000000 199.000000 1725.000000 259.000000 262.000000 321.000000 ... 1.000000 1.000000 1.000000 1.000000 9.184320e+16 3.000000 5.000000 1.000000 128.000000 2525.000000

-> Income 과 Age 의 Max 를 보자. Outlier 가 숨어있는 것 같다!

# 색상 지정
sns.set(rc={"axes.facecolor":"#FFF9ED","figure.facecolor":"#FFF9ED"})
pallet = ["#682F2F", "#9E726F", "#D6B2B1", "#B9C0C9", "#9F8A78", "#F3AB60"]
cmap = colors.ListedColormap(["#682F2F", "#9E726F", "#D6B2B1", "#B9C0C9", "#9F8A78", "#F3AB60"])

# 아래의 정해진 컬럼들 사이의 상관관계를 그래프로 확인해 본다.
To_Plot = [ "Income", "Recency", "Customer_For", "Age", "Spent", "Is_Parent"]
print("Reletive Plot Of Some Selected Features: A Data Subset")
sns.pairplot(df[To_Plot], hue= "Is_Parent", palette=(["#682F2F","#F3AB60"]))
# Outlier (Age, Income) 삭제
df = df[(df["Age"]< 90)]
df = df[(df["Income"] < 600000)]
print("Outlier 제거 후 데이터 row 수:", len(df))


# box plot 이나 분포를 보고도 알 수 있음.

=>

Outlier 제거  데이터 row : 2212

💡 Correlation Coefficients

  • correlation (상관성)이란?
    • 상관성은 두 변수간의 “선형적” 관계의 정도를 의미
    • -1 ~ 1 사이를 가지며 1에 가까울 수록 양의 선형관계, -1에 가까울 수록 음의 선형관계가 강하다는 것을 의미

df.corr()
Income Kidhome Teenhome Recency Wines Fruits Meat Fish Sweets Gold ... AcceptedCmp1 AcceptedCmp2 Complain Response Customer_For Children Family_Size Is_Parent Age Spent
Income 1.000000 -0.514523 0.034565 0.007965 0.688209 0.507354 0.692279 0.520040 0.523599 0.388299 ... 0.327524 0.104036 -0.027900 0.161387 -0.027892 -0.343529 -0.286638 -0.403132 0.199977 0.792740
Kidhome -0.514523 1.000000 -0.039066 0.010623 -0.497203 -0.373258 -0.439031 -0.388643 -0.377843 -0.354922 ... -0.174261 -0.081911 0.037067 -0.077901 -0.057731 0.688081 0.583250 0.520355 -0.237497 -0.557949
Teenhome 0.034565 -0.039066 1.000000 0.014392 0.003945 -0.175905 -0.261134 -0.205235 -0.163107 -0.018579 ... -0.145198 -0.015633 0.007746 -0.154402 0.008986 0.698199 0.594481 0.587993 0.361932 -0.137964
Recency 0.007965 0.010623 0.014392 1.000000 0.015981 -0.005257 0.022914 0.000788 0.025244 0.018148 ... -0.021147 -0.001429 0.005713 -0.200114 0.030748 0.018062 0.014717 0.002189 0.015694 0.020479
Wines 0.688209 -0.497203 0.003945 0.015981 1.000000 0.385844 0.568081 0.396915 0.389583 0.391461 ... 0.351610 0.206309 -0.036420 0.246320 0.148745 -0.353356 -0.296702 -0.341994 0.164615 0.892996
Fruits 0.507354 -0.373258 -0.175905 -0.005257 0.385844 1.000000 0.546740 0.593038 0.571474 0.393459 ... 0.192417 -0.009924 -0.002956 0.123007 0.059828 -0.395161 -0.341414 -0.410657 0.013447 0.612129
Meat 0.692279 -0.439031 -0.261134 0.022914 0.568081 0.546740 1.000000 0.572986 0.534624 0.357556 ... 0.313379 0.043549 -0.021017 0.237966 0.071381 -0.504176 -0.429948 -0.574147 0.033622 0.845543
Fish 0.520040 -0.388643 -0.205235 0.000788 0.396915 0.593038 0.572986 1.000000 0.583484 0.426299 ... 0.261712 0.002322 -0.019098 0.108135 0.078042 -0.427482 -0.363522 -0.449596 0.041154 0.641884
Sweets 0.523599 -0.377843 -0.163107 0.025244 0.389583 0.571474 0.534624 0.583484 1.000000 0.356754 ... 0.245113 0.010142 -0.020569 0.116059 0.076345 -0.389152 -0.330705 -0.402064 0.021516 0.606652
Gold 0.388299 -0.354922 -0.018579 0.018148 0.391461 0.393459 0.357556 0.426299 0.356754 1.000000 ... 0.170853 0.050976 -0.030166 0.141096 0.145632 -0.267776 -0.235826 -0.245380 0.059779 0.527101
NumDealsPurchases -0.108207 0.216594 0.386805 0.002591 0.009117 -0.134191 -0.121128 -0.143147 -0.121395 0.053047 ... -0.127586 -0.038064 0.003744 0.003226 0.199994 0.436072 0.373986 0.388593 0.066156 -0.065571
NumWebPurchases 0.459265 -0.372327 0.162239 -0.005680 0.553663 0.302301 0.306841 0.299428 0.333608 0.407873 ... 0.159100 0.034722 -0.013524 0.151084 0.171834 -0.148938 -0.121879 -0.073473 0.162265 0.529095
NumCatalogPurchases 0.696589 -0.504598 -0.112477 0.024197 0.634237 0.485611 0.733787 0.532241 0.494623 0.441656 ... 0.309130 0.099931 -0.018675 0.219912 0.091391 -0.443199 -0.372319 -0.452734 0.125856 0.780250
NumStorePurchases 0.631424 -0.501863 0.049212 -0.000460 0.640219 0.459875 0.486349 0.457885 0.455150 0.390693 ... 0.178462 0.085146 -0.011947 0.035563 0.104245 -0.323823 -0.265916 -0.284891 0.138998 0.675981
NumWebVisitsMonth -0.650257 0.447258 0.130985 -0.018965 -0.321616 -0.417741 -0.539194 -0.446151 -0.422289 -0.245973 ... -0.195200 -0.007483 0.020820 -0.002625 0.255436 0.415558 0.345316 0.475856 -0.120282 -0.498769
AcceptedCmp3 -0.015152 0.016135 -0.042797 -0.032361 0.061360 0.014644 0.018416 -0.000276 0.001660 0.125557 ... 0.095562 0.071649 0.009620 0.253849 -0.006858 -0.019518 -0.026126 -0.005472 -0.061097 0.053037
AcceptedCmp4 0.219633 -0.162111 0.038168 0.017520 0.373349 0.006598 0.091677 0.016058 0.029206 0.024305 ... 0.242681 0.295015 -0.027030 0.180032 0.013873 -0.088427 -0.076698 -0.076936 0.070035 0.249118
AcceptedCmp5 0.395569 -0.204582 -0.190119 0.000233 0.472889 0.208990 0.375252 0.194793 0.258417 0.176628 ... 0.409420 0.222918 -0.008378 0.324891 -0.023459 -0.284635 -0.225671 -0.346693 -0.019025 0.468695
AcceptedCmp1 0.327524 -0.174261 -0.145198 -0.021147 0.351610 0.192417 0.313379 0.261712 0.245113 0.170853 ... 1.000000 0.176595 -0.025018 0.297212 -0.037171 -0.230291 -0.185711 -0.279387 0.011941 0.381354
AcceptedCmp2 0.104036 -0.081911 -0.015633 -0.001429 0.206309 -0.009924 0.043549 0.002322 0.010142 0.050976 ... 0.176595 1.000000 -0.011200 0.169234 0.006030 -0.070037 -0.059505 -0.081575 0.007821 0.136336
Complain -0.027900 0.037067 0.007746 0.005713 -0.036420 -0.002956 -0.021017 -0.019098 -0.020569 -0.030166 ... -0.025018 -0.011200 1.000000 -0.000145 0.041662 0.032181 0.027081 0.018124 0.004602 -0.034135
Response 0.161387 -0.077901 -0.154402 -0.200114 0.246320 0.123007 0.237966 0.108135 0.116059 0.141096 ... 0.297212 0.169234 -0.000145 1.000000 0.175743 -0.167937 -0.218383 -0.203885 -0.020937 0.264443
Customer_For -0.027892 -0.057731 0.008986 0.030748 0.148745 0.059828 0.071381 0.078042 0.076345 0.145632 ... -0.037171 0.006030 0.041662 0.175743 1.000000 -0.034836 -0.027803 -0.005195 -0.021057 0.138590
Children -0.343529 0.688081 0.698199 0.018062 -0.353356 -0.395161 -0.504176 -0.427482 -0.389152 -0.267776 ... -0.230291 -0.070037 0.032181 -0.167937 -0.034836 1.000000 0.849574 0.799802 0.092676 -0.499931
Family_Size -0.286638 0.583250 0.594481 0.014717 -0.296702 -0.341414 -0.429948 -0.363522 -0.330705 -0.235826 ... -0.185711 -0.059505 0.027081 -0.218383 -0.027803 0.849574 1.000000 0.692370 0.078593 -0.424497
Is_Parent -0.403132 0.520355 0.587993 0.002189 -0.341994 -0.410657 -0.574147 -0.449596 -0.402064 -0.245380 ... -0.279387 -0.081575 0.018124 -0.203885 -0.005195 0.799802 0.692370 1.000000 -0.011841 -0.521603
Age 0.199977 -0.237497 0.361932 0.015694 0.164615 0.013447 0.033622 0.041154 0.021516 0.059779 ... 0.011941 0.007821 0.004602 -0.020937 -0.021057 0.092676 0.078593 -0.011841 1.000000 0.115901
Spent 0.792740 -0.557949 -0.137964 0.020479 0.892996 0.612129 0.845543 0.641884 0.606652 0.527101 ... 0.381354 0.136336 -0.034135 0.264443 0.138590 -0.499931 -0.424497 -0.521603 0.115901 1.000000
# correlation matrix 히트맵으로 표현하기
corrmat= df.corr()
plt.figure(figsize=(20,20))  
sns.heatmap(corrmat, annot=True, cmap="Spectral", center=0)

👣 데이터 전처리하기: Preprocessing

  • 카테고리 형 데이터를 숫자형으로 바꾸기
  • 피쳐 간 규모를 맞추기 위해 Scaling 하기
  • 모델에 넣을 피쳐 - 차원 축소하기

카테고리 데이터 전처리하기

df.dtypes

=>

Education               object
Income                 float64
Kidhome                  int64
Teenhome                 int64
Recency                  int64
Wines                    int64
Fruits                   int64
Meat                     int64
Fish                     int64
Sweets                   int64
Gold                     int64
NumDealsPurchases        int64
NumWebPurchases          int64
NumCatalogPurchases      int64
NumStorePurchases        int64
NumWebVisitsMonth        int64
AcceptedCmp3             int64
AcceptedCmp4             int64
AcceptedCmp5             int64
AcceptedCmp1             int64
AcceptedCmp2             int64
Complain                 int64
Response                 int64
Customer_For             int64
Living_With             object
Children                 int64
Family_Size              int64
Is_Parent                int64
Age                      int64
Spent                    int64
dtype: object
# 데이터 중에서 object 의 데이터 형태를 가지고 있는 컬럼을 가져와라
s = (df.dtypes == 'object')
s[s.values == True].index
s[s].index 

=>

Index(['Education', 'Living_With'], dtype='object')
# 카테고리 형태의 데이터
s = (df.dtypes == 'object')
object_cols = list(s[s].index)

print("카테고리 데이터 종류:", object_cols)

=>

카테고리 데이터 종류: ['Education', 'Living_With']
# 카테고리 데이터에 라벨 인코더 사용하기
# 라벨 인코더란?
   # 카테고리형 데이터에 숫자를 매핑하여 바꿔준다. 
from sklearn.preprocessing import LabelEncoder

# Education 바꿔주기
LE=LabelEncoder()
df['Education']=df[['Education']].apply(LE.fit_transform)
df['Education'].head()

=>

0    0
1    0
2    0
3    0
4    1
Name: Education, dtype: int64
print(LE.classes_)
print(LE.inverse_transform([1, 2, 2]))

=>

['Graduate' 'Postgraduate' 'Undergraduate']
['Postgraduate' 'Undergraduate' 'Undergraduate']
# Living_With 카테고리 -> 숫자로 바꿔주기
df['Living_With']=df[['Living_With']].apply(LE.fit_transform)
df['Living_With'].head()

=>

0    0
1    0
2    1
3    1
4    1
Name: Living_With, dtype: int64

피쳐 간 규모를 맞추기 위해 Scaling 하기

from sklearn.preprocessing import StandardScaler

ds = df.copy()

# 나중에 클러스터 별 캠페인 반응률을 살펴보기 위해 사용
# 클러스터를 구성하기 위한 피쳐에서는 제거한다.
cols_del = ['AcceptedCmp3', 'AcceptedCmp4', 'AcceptedCmp5', 'AcceptedCmp1','AcceptedCmp2', 'Complain', 'Response']
ds = ds.drop(cols_del, axis=1)
ds.head()
Education Income Kidhome Teenhome Recency Wines Fruits Meat Fish Sweets ... NumCatalogPurchases NumStorePurchases NumWebVisitsMonth Customer_For Living_With Children Family_Size Is_Parent Age Spent
0 0 58138.0 0 0 58 635 88 546 172 88 ... 10 4 7 83894400000000000 0 0 1 0 64 1617
1 0 46344.0 1 1 38 11 1 6 2 1 ... 1 2 5 10800000000000000 0 2 3 1 67 27
2 0 71613.0 0 0 26 426 49 127 111 21 ... 2 10 4 40780800000000000 1 0 2 0 56 776
3 0 26646.0 1 0 26 11 4 20 10 3 ... 0 4 6 5616000000000000 1 1 3 1 37 53
4 1 58293.0 1 0 94 173 43 118 46 27 ... 3 6 5 27734400000000000 1 1 3 1 40 422
#Scaling
scaler = StandardScaler()
scaler.fit(ds) # mean, variance 계산

# 적용
scaled_ds = pd.DataFrame(scaler.transform(ds), columns= ds.columns )
print("All features are now scaled")

=>

All features are now scaled
#Scaled data to be used for reducing the dimensionality
print("Dataframe to be used for further modelling:")
scaled_ds.head()
Education Income Kidhome Teenhome Recency Wines Fruits Meat Fish Sweets ... NumCatalogPurchases NumStorePurchases NumWebVisitsMonth Customer_For Living_With Children Family_Size Is_Parent Age Spent
0 -0.893586 0.287105 -0.822754 -0.929699 0.310353 0.977660 1.552041 1.690293 2.453472 1.483713 ... 2.503607 -0.555814 0.692181 1.973583 -1.349603 -1.264598 -1.758359 -1.581139 1.018352 1.676245
1 -0.893586 -0.260882 1.040021 0.908097 -0.380813 -0.872618 -0.637461 -0.718230 -0.651004 -0.634019 ... -0.571340 -1.171160 -0.132545 -1.665144 -1.349603 1.404572 0.449070 0.632456 1.274785 -0.963297
2 -0.893586 0.913196 -0.822754 -0.929699 -0.795514 0.357935 0.570540 -0.178542 1.339513 -0.147184 ... -0.229679 1.290224 -0.544908 -0.172664 0.740959 -1.264598 -0.654644 -1.581139 0.334530 0.280110
3 -0.893586 -1.176114 1.040021 -0.929699 -0.795514 -0.872618 -0.561961 -0.655787 -0.504911 -0.585335 ... -0.913000 -0.555814 0.279818 -1.923210 0.740959 0.069987 0.449070 0.632456 -1.289547 -0.920135
4 0.571657 0.294307 1.040021 -0.929699 1.554453 -0.392257 0.419540 -0.218684 0.152508 -0.001133 ... 0.111982 0.059532 -0.132545 -0.822130 0.740959 0.069987 0.449070 0.632456 -1.033114 -0.307562
  • 여기까지 카테고리 데이터를 LabelEncoder 를 통해 숫자형으로 변환
  • Scaler 를 사용하여 데이터를 정규화

💡 Dimension Reduction

  • 앞의 상관계수 행렬에서 봤듯이, 서로 관련있는 피쳐들이 많음.
  • 변수가 너무 많으면, 모델링이 적절하게 되지 않을 수 있음.

-> 우리 데이터에 상관성이 많고, 피쳐가 너무 많은데 클러스터를 수행하려다 보니 PCA(차원 축소 기법)을 사용해서 차원을 축소 시켜 보자.

# PCA 로 3차원으로 데이터를 줄인다.
# sklearn 에서 PCA 모듈을 불러온다.
from sklearn.decomposition import PCA

pca = PCA(n_components=3)
pca.fit(scaled_ds)
PCA_ds = pd.DataFrame(pca.transform(scaled_ds), columns=(["col1","col2", "col3"]))

PCA_ds.describe().T
count mean std min 25% 50% 75% max
col1 2212.0 -2.569775e-17 2.878377 -5.969394 -2.538494 -0.780421 2.383290 7.444305
col2 2212.0 -8.994212e-17 1.706839 -4.312106 -1.328321 -0.158197 1.242240 6.142664
col3 2212.0 7.388103e-17 1.221956 -3.530975 -0.828811 -0.021838 0.800092 6.615874
PCA_ds.head()
col1 col2 col3
0 4.994347 -0.151233 2.647706
1 -2.884455 -0.006707 -1.863811
2 2.617864 -0.720806 -0.254408
3 -2.676036 -1.541954 -0.922951
4 -0.649591 0.209833 -0.021460
#A 3D Projection Of Data In The Reduced Dimension
x =PCA_ds["col1"]
y =PCA_ds["col2"]
z =PCA_ds["col3"]

#3D 플랏 그리기
# https://www.python-graph-gallery.com/370-3d-scatterplot
fig = plt.figure(figsize=(10,8))
ax = fig.add_subplot(111, projection="3d")
ax.scatter(x,y,z, c="maroon", marker="o" )
ax.set_title("A 3D Projection Of Data In The Reduced Dimension")
plt.show()

👣 유저 세그먼트 나누기: Clustering

💡 Elbow Method

데이터를 몇 개로 나눠야 할까?

  • Elbow Method

  • WSS(군집 내 분산)은 작을 수록 군집의 중심에 많이 모여있는 것이므로 WSS(군집 내 분산)이 작을 수록 좋다.
  • 하지만 클러스터를 늘려서 더 이상 작아지지 않는 한계점이 있다면 더 이상 클러스터 수를 증가시키지 않아도 좋다.

# 이미지 출처: https://heung-bae-lee.github.io/2020/05/30/machine_learning_19/

``py from sklearn.cluster import KMeans

```py
# Quick examination of elbow method to find numbers of clusters to make.
print('Elbow Method to determine the number of clusters to be formed:')

distortions = []

# 클러스터 개수 1 ~ 10 까지 늘려보면서
# 클러스터 내 거리합을 저장한다.
K = range(1,10)
for k in K:
    kmeanModel = KMeans(n_clusters=k)
    kmeanModel.fit(PCA_ds)
    distortions.append(kmeanModel.inertia_) # within-cluster sum-of-squares criterion
distortions

=>

[28060.968908252147,
 13841.123695080303,
 9625.179269985394,
 7482.577127923258,
 6590.895912178157,
 5885.488242066771,
 5317.942980809662,
 4867.552990155307,
 4545.679730812983]
plt.figure(figsize=(8,6))
plt.plot(K, distortions)
plt.xlabel('k')
plt.ylabel('Distortion')
plt.title('The Elbow Method showing the optimal k')
plt.show()

클러스터링을 해보자!

  • 여기서는 Hierarchical Clustering 을 먼저 적용해 보았다.
from sklearn.cluster import AgglomerativeClustering


# 유사도 기준: affinity = euclidean
AC = AgglomerativeClustering(n_clusters=4)

# fit model and predict clusters
yhat_AC = AC.fit_predict(PCA_ds)
PCA_ds["Clusters"] = yhat_AC

# 기존 데이터에도 넣는다.
df["Clusters"]= yhat_AC
# Plotting the clusters
fig = plt.figure(figsize=(10,8))
ax = plt.subplot(111, projection='3d', label="bla")
ax.scatter(x, y, z, s=40, c=PCA_ds["Clusters"], marker='o', cmap = cmap )
ax.set_title("The Plot Of The Clusters")
plt.show()

👣 모델 평가하기: Evaluation

EDA 를 통해 만들어진 클러스터와 그 특성 파악하기 그룹이 골고루 분포되어 있을까?

# Plotting countplot of clusters
pal = ["#682F2F","#B9C0C9", "#9F8A78","#F3AB60"]
pl = sns.countplot(x=df["Clusters"], palette=pal)
pl.set_title("Distribution Of The Clusters")
plt.show()

클러스터 별 구매금액과 연간 수입을 알아보자

pl = sns.scatterplot(data = df,x=df["Spent"], y=df["Income"],hue=df["Clusters"], palette= pal)
pl.set_title("Cluster's Profile Based On Income And Spending")
plt.legend()
plt.show()


# group 3: high spending & low income (3)
# group 2: low spending & low income (2)
# group 0: high spending & average income (0)
# group 1: high spending & high income (1)

구매를 많이하는 고객층은?

plt.figure()
pl=sns.swarmplot(x=df["Clusters"], y=df["Spent"], color= "#CBEDDD", alpha=0.5 )
pl=sns.boxenplot(x=df["Clusters"], y=df["Spent"], palette=pal)

어떤 클러스터에서 캠페인 반응이 높을까?

# Creating a feature to get a sum of accepted promotions 
df["Total_Promos"] = df["AcceptedCmp1"]+ df["AcceptedCmp2"]+ df["AcceptedCmp3"]+ df["AcceptedCmp4"]+ df["AcceptedCmp5"]

# Plotting count of total campaign accepted.
plt.figure()
pl = sns.countplot(x=df["Total_Promos"],hue=df["Clusters"], palette= pal)
pl.set_title("Count Of Promotion Accepted")
pl.set_xlabel("Number Of Total Accepted Promotions")
plt.show()

# 대부분 반응을 하지 않았다. 프로모션이 더 잘 디자인 될 필요가 있다.

할인에 잘 반응한 그룹은?

# Plotting the number of deals purchased
plt.figure()
pl=sns.boxenplot(y=df["NumDealsPurchases"],x=df["Clusters"], palette= pal)
pl.set_title("Number of Deals Purchased")
plt.show()

가족 구성원과 나이대는 어떻게 될까?

Personal = [ "Income","Kidhome","Teenhome","Customer_For", "Age", "Children", "Family_Size", "Is_Parent", "Education","Living_With"]

for i in Personal:
    plt.figure()
    sns.boxplot(y=df[i], x=df["Clusters"], palette=pal)
    plt.show()

  • 부모이며, 가족 구성원이 2 -4, 한부모 가정, 10대, 비교적 나이가 있음
  • 부모가 아니며, 많아도 2명정도의 가족 구성원, 전 연령에 골고루 있음, 연간 수입이 높음
  • 대부분 부모이며, 최대 4명의 가족 구성원, 10대가 아닌 1명의 어린 아이를 두고 있음, 상대적으로 어림
  • 부모이며, 최대 5명까지의 가족 구성원, 상대적으로 나이가 있고, 수입이 낮음

👣 다양한 클러스터링 모델 사용해보기

💡Density Based Clustering

from sklearn.cluster import DBSCAN
# Initiating the DBSCAN Clustering model 
DP = DBSCAN(eps=0.30, min_samples=9)

# fit model and predict clusters
DP_df = DP.fit_predict(PCA_ds)
PCA_ds["DBSCAN_Clusters"] = DP_df

# Adding the Clusters feature to the orignal dataframe.
df["DBSCAN_Clusters"]= DP_df
# Plotting the clusters
fig = plt.figure(figsize=(10,8))
ax = plt.subplot(111, projection='3d', label="bla")
ax.scatter(x, y, z, s=40, c=PCA_ds["DBSCAN_Clusters"], marker='o', cmap = 'viridis' )
ax.set_title("The Plot Of The Clusters")
plt.show()

# Plotting countplot of clusters
pl = sns.countplot(x=df["DBSCAN_Clusters"])
pl.set_title("Distribution Of The Clusters")
plt.show()

pl = sns.scatterplot(data = df,x=df["Spent"], y=df["Income"],hue=df["DBSCAN_Clusters"])
pl.set_title("Cluster's Profile Based On Income And Spending")
plt.legend()
plt.show()

plt.figure()
pl=sns.swarmplot(x=df["DBSCAN_Clusters"], y=df["Spent"], color= "#CBEDDD", alpha=0.5 )
pl=sns.boxenplot(x=df["DBSCAN_Clusters"], y=df["Spent"])
plt.show()

💡Partition Based Clustering

Kmeans = KMeans(n_clusters=4)
# fit model and predict clusters
Kmeans_df = Kmeans.fit_predict(PCA_ds)
PCA_ds["Kmeans_Clusters"] = Kmeans_df
#Adding the Clusters feature to the orignal dataframe.
df["Kmeans_Clusters"]= Kmeans_df
#Plotting countplot of clusters
pl = sns.countplot(x=df["Kmeans_Clusters"])
pl.set_title("Distribution Of The Clusters")
plt.show()

pl = sns.scatterplot(data = df,x=df["Spent"], y=df["Income"],hue=df["Kmeans_Clusters"])
pl.set_title("Cluster's Profile Based On Income And Spending")
plt.legend()
plt.show()

💡 Silhouette Coefficient

from sklearn.metrics import silhouette_samples

# Hierachical
sample_silhouette_values = silhouette_samples(PCA_ds, PCA_ds["Clusters"])
print("Hierachical",sample_silhouette_values.mean())

# Density - DBSCAN
sample_silhouette_values = silhouette_samples(PCA_ds, PCA_ds["DBSCAN_Clusters"])
print("Density",sample_silhouette_values.mean())


# Kmeans
sample_silhouette_values = silhouette_samples(PCA_ds, PCA_ds["Kmeans_Clusters"])
print("Kmeans",sample_silhouette_values.mean())

=>

Hierachical 0.45273806967143565
Density 0.05700586237988258
Kmeans 0.46554372154673307