Project/2022 빅콘테스트

[2022 빅콘테스트] 앱 사용성 데이터를 통한 대출신청 예측분석 - 고객 군집화

문세희 2023. 2. 20. 14:08

이번 공모전에서 내가 주도적으로 맡은 부분은 바로 고객 군집화 부분이다.

먼저 앱을 통하여 대출신청을 받으려는 사용자들을 군집별로 나누기 위해 먼저 log_data 테이블을 살펴보았다.

log_data의 event 변수는 사용자들의 활동을 로그로 기록한 변수이며, 기록된 최종 활동이 고객의 특성을 예측하는 데에 사용될 수 있는 변수로 판단되어 이를 우선 Label encoding으로 변환하였다. 

핀다 앱 실행화면을 참조한 event 프로세스맵
행동 특정 및 label encoding

event 변수의 데이터 고유값이 생각보다 다양하였기 때문에 직접 앱을 실행해서 어떤 화면이 어떤 event로 기록되는 지 살펴보았고, 다음과 같은 프로세스맵을 만들어낼 수 있었다.

빨간색 블럭으로 표시한 부분이 최종 행동일 것이라는 가설 하에 5개의 event를 제외한 나머지 event들은 삭제해 주었다.

또한 5가지 event의 갯수가 불균형했기 때문에, 마찬가지로 random undersampling을 이용하여 데이터 갯수를 균형으로 맞추어 주었다.

df_merge['event'].value_counts()
print('event 데이터 불균형 정도', '0 : 1 : 3 : 4 : 2 =', np.round(569309/(569309 + 332357 + 163235 + 742 + 434), decimals= 4), ':', np.round(332357/(569309 + 332357 + 163235 + 742 + 434), decimals= 4), ':', np.round(163235/(569309 + 332357 + 163235 + 742 + 434), decimals= 4), ':', np.round(742/(569309 + 332357 + 163235 + 742 + 434), decimals= 4), ':', np.round(434/(569309 + 332357 + 163235 + 742 + 434), decimals= 4))

# RandomUnderSampling 과정으로 target data의 비율을 1:1로 맞추기
df_merge_undersampling = df_merge.sample(frac = 1)

fraud_df = df_merge.loc[df_merge['event'] == 2] # 제일 적은 값
non_fraud_df_1 = df_merge.loc[df_merge['event'] == 0][:434]
non_fraud_df_2 = df_merge.loc[df_merge['event'] == 1][:434]
non_fraud_df_3 = df_merge.loc[df_merge['event'] == 3][:434]
non_fraud_df_4 = df_merge.loc[df_merge['event'] == 4][:434]

normal_distributed_df = pd.concat([fraud_df, non_fraud_df_1, non_fraud_df_2, non_fraud_df_3, non_fraud_df_4])

df_undersampling = normal_distributed_df.sample(frac = 1, random_state = 42)

df_undersampling.head()

random undersampling 결과

label encoding을 통해 변수를 연속형으로 바꾸어 주었기 때문에 standard scaler을 통해 scailing해 주었다.

# StandardScaler - 표준화하기 
from sklearn.preprocessing import StandardScaler 

scaler = StandardScaler()             # 표준화를 하기 위한 StandardScaler를 scaler변수에 넣는다
scaler.fit(df_selected)                    # numerical data만 뽑았던 df_num dataset에 해당 scaler를 적용시킨다.
df_s = scaler.transform(df_selected)       # StandardScaler가 적용된 df_num을 해당 scalar대로 변환한다.
df_s = pd.DataFrame(df_s, index = df_selected.index, columns=df_selected.columns) # 변환된 데이터셋은 수치화 되어있으므로 데이터프레임화 시켜준다.
df_s
# 2차원 평면에 시각화하기 위한 pca 차원 축소 (선택한 data들의 분산치가 가장 큰 두 개의 축을 찾아 좀 더 유의미한 분석을 할 수 있기 때문에 pca로 진행한다.)
from sklearn.decomposition import PCA # sklearn 라이브러리의 PCA를 import한다

pca = PCA(n_components = 2)           # 2차원으로 시각화를 진행할 것이므로 2개로 설정한다.
pca.fit(df_s)             
df_p = pca.transform(df_s)
df_p = pd.DataFrame(df_p, columns = ['PC1','PC2']) #PCA진행 한 두 개의 값을 column으로 데이터프레임화 시킨다.
df_p

또한 2차원으로 시각화하기 위해서 pca 차원축소를 진행해 주었다.

 

사용하는 모델을 결정하기 위해서 명목형 변수와 수치형 변수가 섞여 있었으므로 k-prototype 모델을 활용하여 군집화를 진행하려 했으나, 많지 않은 데이터 갯수에도 불구하고 매우 학습시간이 길어진다는 단점이 존재하였다. 따라서 k-means 모델을 활용하기로 결정하였다.

 

# # 적절한 군집수 찾기
# Inertia(군집 내 거리제곱합의 합) value (적정 군집수)

ks = range(1,10)
inertias = []

for k in ks:
    model = KMeans(n_clusters=k)
    model.fit(df_p)
    inertias.append(model.inertia_)

# Plot ks vs inertias
plt.figure(figsize=(4, 4))

plt.plot(ks, inertias, '-o')
plt.xlabel('number of clusters, k')
plt.ylabel('inertia')
plt.xticks(ks)
plt.show()

# k의 갯수가 4에서 기울기가 완만하게 변하기 시작한다. (애매하긴 함) 고로 k의 갯수는 4가 적절하다고 볼 수 있다. +) 5개로도 해봄

k-means 클러스터링에는 k 값(군집 갯수) 을 임의로 설정하고 직접 값을 달리하면서 클러스터링 결과를 지켜보아야 한다는 단점이 존재한다. 이를 보완하기 위해 elbow 기법을 이용하였다.

k 값은 기울기가 완만하게 바뀌는 지점인 5로 결정하였다.

#kmeans
kmeans = KMeans(n_clusters=5, random_state=42)
kmeans.fit(df_p)
label = kmeans.labels_
label = pd.Series(label)
df_p['label'] = label.values
df_p

 

군집별로 다른 색을 적용하여 시각화한 결과.

시각화해본 결과로서는 서로 다른 5개 군집의 특성이 명확히 드러나지 않았다. 

따라서 명목형 변수에는 최빈값을, 연속형 변수에는 평균값을 통해 군집화된 고객의 특성을 정리하면서 각 군집의 특성을 파악하였다.

df_undersampling.groupby('kmeans_cluster').count()
#그룹4>1>2>3>0 (Standard, undersampling)
#그룹 1>3=4>2>1 (standard, pca, undersampling)

df_undersampling.groupby('kmeans_cluster').mean()
# 신용점수 다 엇비슷하지만 0>1>2>3>4 (Standard, undersampling)
# 신용점수 1>3>0>4>2 (standard, pca, undersampling)

## 클러스터별 income_type 최빈값: 이벤트
df_undersampling.groupby('kmeans_cluster')['event'].agg(**{
    'most_common_value':lambda x:x.mode()
}).reset_index()

## 클러스터별 income_type 최빈값: 성별
df_undersampling.groupby('kmeans_cluster')['gender'].agg(**{
    'most_common_value':lambda x:x.mode()
}).reset_index()
# 그룹 1: 여성, 나머지 : 남성

## 클러스터별 income_type 최빈값: 목적
df_undersampling.groupby('kmeans_cluster')['purpose'].agg(**{
    'most_common_value':lambda x:x.mode()
}).reset_index()

## 클러스터별 income_type 최빈값: 근로형태
df_undersampling.groupby('kmeans_cluster')['income_type'].agg(**{
    'most_common_value':lambda x:x.mode()
}).reset_index()
# 0, 1, 2, 4 = 직장가입자 (4대보험O) 3 = 개인사업자

## 클러스터별 income_type 최빈값: 그룹2 - 고용형태

df_undersampling.groupby('kmeans_cluster')['employment_type'].agg(**{
    'most_common_value':lambda x:x.mode()
}).reset_index()
# 3 - 정규직 1 - 개인사업자
# 0, 1, 4 = 정규직, 2, 3 = 개인사업자

## 클러스터별 income_type 최빈값: 그룹4 -  개인회생자
df_undersampling.groupby('kmeans_cluster')['personal_rehabilitation_yn'].agg(**{
    'most_common_value':lambda x:x.mode()
}).reset_index()

# 0, 1, 2, 4 = 개인회생x, 3 = 개인회생o

## 클러스터별 income_type 최빈값: 개인회생자 납입완료
df_undersampling.groupby('kmeans_cluster')['personal_rehabilitation_complete_yn'].agg(**{
    'most_common_value':lambda x:x.mode()
}).reset_index()

# 0, 1, 2, 4 = x, 3 = o (개인회생자이니까)

## 클러스터별 income_type 최빈값: 주거소유형태
df_undersampling.groupby('kmeans_cluster')['houseown_type'].agg(**{
    'most_common_value':lambda x:x.mode()
}).reset_index()

# 1, 2, 3 = 전월세, 0 = 배우자소유, 4 = 기타가족소유

컬럼의 최빈값, 평균값을 나타낸 엑셀 표

각 군집에서 유의미한 특성을 대변할 수 있는 셀에 색을 채워넣고, 이로 인해 군집의 특성을 결정할 수 있었다.

다음은 각 고객 군집에 대한 서비스 메시지를 고안한 것이다.