오늘은 "클러스터링"에 대해 다루어보겠습니다. 클러스터링은 데이터를 비슷한 그룹으로 나누는 작업인데요, K-Means와 Agglomerative Clustering을 사용해 고객 데이터를 클러스터링하고, PCA 기법을 통해 결과를 개선하겠습니다.
> Kaggle에서 제공하는 고객 데이터를 사용했어요~
import pandas as pd
import numpy as np
from sklearn.preprocessing import MinMaxScaler
import matplotlib.pyplot as plt
import pandas as pd
from sklearn.preprocessing import LabelEncoder
import seaborn as sns
from matplotlib import colors
from sklearn.cluster import KMeans
df = pd.read_csv("customer_segmentation.csv")
df.describe()
df.shape
(2240, 29)
1. 데이터 클리닝
우선 데이터에 null 값이 있는지 확인해본 결과, True 값이 나와서 이를 처리해야 할 필요성이 있어요.
df.isnull().any().any()
True
null_values = df.isnull().sum()
print(null_values[null_values > 0])
Income 24
dtype: int64
df = df.dropna(subset=['Income'])
print("The total number of data-points after removing the rows with missing values are:", len(df))
The total number of data-points after removing the rows with missing values are: 2216
df.info()
<class 'pandas.core.frame.DataFrame'>
Index: 2216 entries, 0 to 2239
Data columns (total 29 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 ID 2216 non-null int64
1 Year_Birth 2216 non-null int64
2 Education 2216 non-null object
3 Marital_Status 2216 non-null object
4 Income 2216 non-null float64
5 Kidhome 2216 non-null int64
6 Teenhome 2216 non-null int64
7 Dt_Customer 2216 non-null object
8 Recency 2216 non-null int64
9 MntWines 2216 non-null int64
10 MntFruits 2216 non-null int64
11 MntMeatProducts 2216 non-null int64
12 MntFishProducts 2216 non-null int64
13 MntSweetProducts 2216 non-null int64
14 MntGoldProds 2216 non-null int64
15 NumDealsPurchases 2216 non-null int64
16 NumWebPurchases 2216 non-null int64
17 NumCatalogPurchases 2216 non-null int64
18 NumStorePurchases 2216 non-null int64
19 NumWebVisitsMonth 2216 non-null int64
20 AcceptedCmp3 2216 non-null int64
21 AcceptedCmp4 2216 non-null int64
22 AcceptedCmp5 2216 non-null int64
23 AcceptedCmp1 2216 non-null int64
24 AcceptedCmp2 2216 non-null int64
25 Complain 2216 non-null int64
26 Z_CostContact 2216 non-null int64
27 Z_Revenue 2216 non-null int64
28 Response 2216 non-null int64
dtypes: float64(1), int64(25), object(3)
memory usage: 519.4+ KB
Dt_Customer 변수는 object 타입으로 되어 있어서, 먼저 이를 datetime 형식으로 변환
# Changing to datetime
df["Dt_Customer"] = pd.to_datetime(df["Dt_Customer"], dayfirst=True)
current_date = pd.to_datetime("today")
각 고객의 가입일부터 현재까지의 일수를 계산하여 Customer_Lifetime_Days라는 새로운 열 생성
df["Customer_Lifetime_Days"] = (current_date - df["Dt_Customer"]).dt.days
Customer_Lifetime_Days 값을 기준으로 고객을 세 가지 수준으로 구분하는 함수 customer_level을 정의한 후, 각 고객에 대해 이를 적용하여 Customer_Level이라는 새로운 열 추가
def customer_level(row):
if row["Customer_Lifetime_Days"] <= 365: # 0-1 year
return "New Customer"
elif 365 < row["Customer_Lifetime_Days"] <= 1095: # 1-3 years
return "Mid-Term Customer"
else: # 3 years
return "Loyal Customer"
df["Customer_Level"] = df.apply(customer_level, axis=1)
print(df[["Dt_Customer", "Customer_Lifetime_Days", "Customer_Level"]].head())
Dt_Customer Customer_Lifetime_Days Customer_Level
0 2012-09-04 4459 Loyal Customer
1 2014-03-08 3909 Loyal Customer
2 2013-08-21 4108 Loyal Customer
3 2014-02-10 3935 Loyal Customer
4 2014-01-19 3957 Loyal Customer
df['Marital_Status'].value_counts()
Marital_Status
Married 857
Together 573
Single 471
Divorced 232
Widow 76
Alone 3
Absurd 2
YOLO 2
Name: count, dtype: int64
marital status을 "Has Partner"와 "No Partner"로 변환하는 작업
df["Living_With"] = df["Marital_Status"].replace({
"Married": "Has Partner",
"Together": "Has Partner",
"Absurd": "No Partner",
"Widow": "No Partner",
"YOLO": "No Partner",
"Divorced": "No Partner",
"Single": "No Partner",
"Alone": "No Partner"
})
# Creating Age column
df["Age"] = 2024-df["Year_Birth"]
# Total payment
df["Total_Payment"] = df["MntWines"]+ df["MntFruits"]+ df["MntMeatProducts"]+ \
df["MntFishProducts"]+ df["MntSweetProducts"]+ df["MntGoldProds"]
# Total children living with
df["Children"]=df["Kidhome"]+df["Teenhome"]
df['Family_Members'] = df["Living_With"].replace({"Has Partner": 2, "No Partner": 1}) + df['Children']
df["Is_Parent"] = np.where(df.Children> 0, 1, 0)
df['Education'].value_counts()
Education
Graduation 1116
PhD 481
Master 365
2n Cycle 200
Basic 54
Name: count, dtype: int64
df["Education"] = df["Education"].replace({
"Basic": "Undergraduate",
"2n Cycle": "Undergraduate",
"Graduation": "Graduate",
"Master": "Postgraduate",
"PhD": "Postgraduate"
})
# Dropping unnecessary columns
to_drop = ["Marital_Status", "Dt_Customer", "Z_CostContact", "Z_Revenue", "Year_Birth", "ID", 'AcceptedCmp3', 'AcceptedCmp4', 'AcceptedCmp5', 'AcceptedCmp1','AcceptedCmp2', 'Complain', 'Response']
df = df.drop(to_drop, axis=1)
df.head()
# Set custom color palette for the plots
sns.set(rc={"axes.facecolor":"#F0F0F0", "figure.facecolor":"#F0F0F0"}) # Set background to light gray
pallet = ["#006994", "#4DB8B1", "#87C1B5", "#A1D9C8", "#AEE1D4", "#C1F2E6"] # Blue-green colors
cmap = colors.ListedColormap(pallet)
# Select columns for plotting (modify this to match the columns you are interested in)
To_Plot = ["Income", "Recency", "Age", "Total_Payment", "Is_Parent"]
# Plot the pairplot with the selected columns and using 'Is_Parent' for color hue
sns.pairplot(df[To_Plot], hue="Is_Parent", palette=pallet)
# Display the plot
plt.show()
# Set custom color palette for the plots
sns.set(rc={"axes.facecolor":"#F0F0F0", "figure.facecolor":"#F0F0F0"}) # Set background to light gray
# Koyu ve açık pembe tonları
pallet = ["#D81B60", "#FF4081", "#FF80AB", "#FFB3C1", "#FFCCE5", "#FCE4EC"] # Dark to soft pink tones
cmap = colors.ListedColormap(pallet)
# Select columns for plotting (modify this to match the columns you are interested in)
To_Plot = ["Income", "Recency", "Age", "Total_Payment", "Is_Parent", 'Living_With']
# Plot the pairplot with the selected columns and using 'Living_With' for color hue
sns.pairplot(df[To_Plot], hue="Living_With", palette=pallet)
# Display the plot
plt.show()
Age와 Income 분포:
# Set custom style
sns.set(style="whitegrid", rc={"axes.facecolor": "#F0F0F0", "figure.facecolor": "#F0F0F0"}) # Light gray background
# Visualize Age distribution
plt.figure(figsize=(10, 6))
sns.histplot(df['Age'], bins=30, color='skyblue', kde=True, edgecolor='black')
plt.title('Age Distribution', fontsize=16, weight='bold')
plt.xlabel('Age', fontsize=12)
plt.ylabel('Frequency', fontsize=12)
plt.grid(True, linestyle='--', alpha=0.7)
plt.show()
# Visualize Income distribution
plt.figure(figsize=(10, 6))
sns.histplot(df['Income'], bins=30, color='lightcoral', kde=True, edgecolor='black')
plt.title('Income Distribution', fontsize=16, weight='bold')
plt.xlabel('Income', fontsize=12)
plt.ylabel('Frequency', fontsize=12)
plt.grid(True, linestyle='--', alpha=0.7)
plt.show()
그래프를 통해 Age와 Income 열에 아웃라이어 값이 존재하는 것을 확인할 수 있는데, 이를 수정할거예요~~
df = df[df['Income'] <= 600000]
print("The total number of data-points after removing wrong income:", len(df))
The total number of data-points after removing wrong income: 2215
df = df[df['Age'] <= 85]
print("The total number of data-points after removing age >85:", len(df))
The total number of data-points after removing age >85: 2212
2. 데이터 전처리
카테고리형 변수들을 분석에 사용하기 위해 Label Encoding을 사용하여 numerical data로 변환하기
# List of categorical variables
s = (df.dtypes == 'object')
object_cols = list(s[s].index)
print("Categorical variables in the dataset:", object_cols)
Categorical variables in the dataset: ['Education', 'Customer_Level', 'Living_With']
# Label Encoding for the categorical variables
LE=LabelEncoder()
for i in object_cols:
df[i]=df[[i]].apply(LE.fit_transform)
MinMaxScaler를 사용하여 데이터를 0과 1 사이로 정규화하기
df2 = df.copy()
scaler = MinMaxScaler()
df_scaled = scaler.fit(df2)
scaled_df = pd.DataFrame(scaler.transform(df2),columns= df2.columns)
scaled_df.head()
이제 데이터의 값이 numerical로 변환되었으므로, 클러스터링 작업을 시작할 수 있어요. 먼저 Elbow 방법을 사용하여 최적의 클러스터 수를 결정하겠습니다.
inertia = []
for i in range(1, 11):
kmeans = KMeans(n_clusters=i, random_state=42)
kmeans.fit(scaled_df) # df_scaled kullanılmalı
inertia.append(kmeans.inertia_)
# Finding optimal topic number by graph
plt.plot(range(1, 11), inertia)
plt.title('Elbow Method')
plt.xlabel('Number of clusters')
plt.ylabel('Inertia')
plt.show()
그래프에서 최적의 클러스터 수가 명확하게 보이지 않지만, 저는 5으로 설정하는 것이 적합하다고 생각했어요.
우리가 클러스터링 방법들의 성능을 Silhouette Score로 평가할거예요. 이 Silhouette Score은 각 데이터 포인트가 얼마나 잘 클러스터링되었는지를 측정하며, 값은 -1에서 1 사이로 나와요. 값이 1에 가까울수록 클러스터링이 잘 이루어졌다고 할 수 있어요.
- 1에 가까운 값: 데이터 포인트들이 같은 클러스터 내에서 서로 잘 묶여 있고, 다른 클러스터와는 명확하게 구분되는 경우
- 0에 가까운 값: 데이터 포인트들이 클러스터 내에서 비슷하지만, 다른 클러스터와의 경계가 모호해 구분이 어려운 경우
- -1에 가까운 값: 클러스터링이 잘못되어, 데이터 포인트들이 잘못된 클러스터에 배치된 경우
K-means
- K-Means는 데이터를 K개의 군집으로 나누는 비지도 학습 알고리즘입니다.
kmeans = KMeans(n_clusters=5, init='k-means++', random_state=50)
kmeans.fit(scaled_df)
cluster_labels = kmeans.labels_
df['Cluster'] = cluster_labels
from sklearn.metrics import silhouette_score
silhouette_avg = silhouette_score(scaled_df, cluster_labels)
print(f"Silhouette Score: {silhouette_avg}")
Silhouette Score: 0.21416380707085655
Agglomerative Clustering
- Agglomerative Clustering은 데이터를 계층적으로 병합하여 군집을 형성하는 군집화 알고리즘입니다.
from sklearn.cluster import AgglomerativeClustering
# Agglomerative Clustering
agglo = AgglomerativeClustering(n_clusters=5)
agglo_labels = agglo.fit_predict(scaled_df)
# Cluster labels
print("Agglomerative Clustering Labels:")
print(agglo_labels)
# Silhouette Score
silhouette_avg = silhouette_score(scaled_df, agglo_labels)
print(f"Silhouette Score for Agglomerative Clustering: {silhouette_avg}")
Agglomerative Clustering Labels:
[4 2 0 ... 4 3 1]
Silhouette Score for Agglomerative Clustering: 0.1880058080278573
- Agglomerative Clustering을 3개와 5개의 군집으로 시도했지만, 결과적으로 성능이 크게 향상되지 않았어요. 그래서 PCA로 차원 축소를 진행한 후 군집화를 다시 시도해 본 결과, 군집화 성능이 조금 더 나아졌어요. PCA로 차원 축소 후 얻어진 Silhouette Score가 이전보다 더 높은 값을 보여주었기 때문에, 이 방법이 더 효과적이라고 판단했어요.
PCA Reduction
PCA는 데이터의 차원 축소를 위한 기법이예요. 고차원 데이터를 시각화하거나 분석할 때, 차원을 줄여서 중요한 정보는 유지하면서, 불필요한 변수를 제거하는 데 사용됩니다.
- 차원 축소: 데이터를 2차원 또는 3차원으로 변환하여, 데이터를 쉽게 시각화하고 분석할 수 있어요.
- 중요한 정보 보존: PCA는 주성분을 찾아 데이터를 압축하면서도 중요한 특성을 유지해요.
- 노이즈 감소: 차원 축소는 데이터의 노이즈를 줄이고, 본질적인 패턴에 집중할 수 있도록 도와줘요.
from sklearn.decomposition import PCA
# Reduce the dimensions to 2 using PCA
pca = PCA(n_components=2)
pca.fit(scaled_df) # scaled_df: The scaled version of your data
PCA_df = pd.DataFrame(pca.transform(scaled_df), columns=["PC1", "PC2"])
from sklearn.cluster import AgglomerativeClustering
# Apply Agglomerative Clustering
AC = AgglomerativeClustering(n_clusters=5) # Set the number of clusters to 5
yhat_AC = AC.fit_predict(PCA_df) # Apply clustering to the PCA-reduced 2D data
# Add cluster labels to the 2D PCA DataFrame
PCA_df["Clusters"] = yhat_AC
# Add cluster labels to the original DataFrame
scaled_df["Clusters"] = yhat_AC
print(PCA_df.head()) # Display the PCA DataFrame with added cluster labels
print(scaled_df[["Clusters"]].head()) # Display the original DataFrame with added cluster labels
PC1 PC2 Clusters
0 2.686228 0.784727 3
1 -1.698752 0.231181 0
2 0.817617 0.310404 4
3 -0.610873 -0.420856 2
4 -0.520667 -0.332882 2
Clusters
0 3
1 0
2 4
3 2
4 2
# 2D scatter plot
plt.figure(figsize=(10, 8))
plt.scatter(PCA_df["PC1"], PCA_df["PC2"], c=cluster_labels, cmap='viridis') # cluster_labels indicate clusters for each observation
plt.xlabel('Principal Component 1')
plt.ylabel('Principal Component 2')
plt.title('2D PCA Projection')
plt.colorbar(label='Cluster')
plt.show()
# Calculate the Silhouette Score for the PCA-reduced data
sil_score = silhouette_score(PCA_df[["PC1", "PC2"]], PCA_df["Clusters"])
print("Silhouette Score for PCA-reduced data: ", sil_score)
Silhouette Score for PCA-reduced data: 0.8327815637518862
결과적으로, 먼저 K-Means 클러스터링을 적용한 결과 Silhouette Score는 0.214로 나타났습니다. 그 후, Agglomerative Clustering을 적용했을 때는 Silhouette Score가 0.188로 약간 더 낮은 값을 얻었습니다. 두 방법 모두 클러스터 간 구분이 명확하지 않다는 것을 시사했습니다.
그 후, PCA로 차원 축소를 적용한 후 Agglomerative Clustering을 다시 실행했더니, Silhouette Score는 0.832로 크게 향상되었습니다. 이는 PCA를 통해 데이터의 차원을 줄여 주요 특성만 남기고 불필요한 노이즈를 제거했기 때문에, 클러스터 간 구분이 명확해졌기 때문이예요. 이로 인해 PCA를 통한 차원 축소가 성능 향상에 큰 영향을 미쳤음을 알 수 있습니다.
'머신러닝' 카테고리의 다른 글
서울 자전거 대여 수요 이해하기: 머신러닝 접근법 [회귀분석] (4) | 2024.10.11 |
---|