Kwon's 데이터분석기

Daicon 경진대회 - 학습플랫폼 이용자 구독 갱신 예측 모델링 본문

카테고리 없음

Daicon 경진대회 - 학습플랫폼 이용자 구독 갱신 예측 모델링

DataKwon 2023. 12. 24. 15:24

Daicon 경진대회 - 학습플랫폼 이용자 구독 갱신 예측에 대한 모델링을 수행해보았다.

 

EDA 및 분석을 실시한 결과는

1. train_data와 test_data에서의 분포는 preferred_difficulty_level을 제외하고 차이가 없다.

2. average_time_per_learning_session은 오른쪽으로 꼬리가 긴 양수 왜도 형태를 띈다.

3. train_data의 boxplot결과에서는 이상치가 존재하는 것 처럼 보였지만 train_data에서 test_data의

   최대 최소값에 벗어나는 값이 존재하는지 확인해보니 없기 때문에 이상치가 따로 존재하지 않는 것 같다.

4. 커뮤니티 참여도가 낮을수록 총 학습 코스 수가 적다.

5. 커뮤니티 참여도가 낮을수록 각 학습 세션에 소요된 평균 시간 도 낮다.

6. 선호 난이도가 높을수록 완료한 총 코스 수가 낮다.

7. 선호 난이도가 높을수록 각 학습 세션에 소요된 평균 시간이 낮다.

8. 완료한 총 코스 수는 구독 유형이 Premium인 사람이 Basic인 사람보다 높다.

9. 각 학습 세션에 소요된 평균 시간은 Premium인 사람이 Basic인 사람보다 높다.

10. target변수에 대해서 데이터 불균형이 존재한다.

이러한 결론이 내려졌기 떄문에 모델링을 위한 데이터 전처리는 이에 맞게 실행을 한다.

 

먼저 기초적인 모듈을 들고와준 후 데이터를 들고와준다.

# 기초 모듈
import pandas as pd
import numpy as np
import warnings
warnings.filterwarnings('ignore')

train_data = pd.read_csv('./dataset/subscription/train.csv')
test_data = pd.read_csv('./dataset/subscription/test.csv')
result_data = pd.read_csv('./dataset/subscription/sample_submission.csv')

 

먼저 데이터에서 모델링을 할 때 필요없는 데이터인 user_id를 지워준 후 변수 정보를 찾아본다.

train_data = train_data.drop('user_id', axis = 1)
test_data = test_data.drop('user_id', axis = 1)

print(train_data.info(), '\n')
print(test_data.info())

train, test 데이터 열 정보

 

그 다음 EDA결과 중 하나인 "10. target변수에 대해서 데이터 불균형이 존재한다." 가 있는데, 데이터 불균형이 존재하면  모델 학습을 할 때 정확도의 한계, 과적합 문제, 평가지표의 왜곡이 존재할 수 도 있기 때문에 데이터가 얼마나 불균형한지 확인을 해본다.

# 데이터를 target 기준으로 groupby로 묶어준 후 count해준다.
df1 = train_data.groupby('target').count()

# 파이차트 시각화
plt.pie(data = df1, x = 'subscription_duration',labels=df1.index, autopct = lambda x : '{:.1f}%'.format(x))
plt.legend()
plt.show()

target변수의 비율

종속변수인 target이 62대 38로 약간의 데이터 불균형이 존재하는 것으로 보인다.

 

데이터 불균형이 약간 존재하는 것을 확인을 했기 때문에, 종속변수인 target을 따로 값을 빼준 후 train_data에서 target을 삭제해준다.

# train_data의 target 변수를 target_y로 새로 만들어줌
target_y = train_data['target']
# target데이터 삭제
train_data.drop('target', axis=1, inplace = True)

이렇게 모델 학습을 위해서 종속변수인 target까지 처리를 해주었다.

 

또한 데이터의 총 개수를 확인해보니 10000개의 행인 비교적 적은 양의 데이터로 이루어 진 것 또한 알수 있다.

그래서 데이터의 양을 늘려주면서 종속변수의 데이터 불균형도 처리해줄 수 있도록 오버샘플링 방법 중 하나인 SMOTE 방식을 사용하여 데이터의 양도 늘려주며 데이터 불균형 문제도 해소할 수 있다.

SMOTE방식에는 단점도 존재하는데, 소수 클래스의 데이터가 과하게 늘어나 소수 클래스에 과도하게 적합되고, 실제 데이터와의 일반화 성능이 저하될 가능성이 있으며, 이상치에 민감한 모델에서는 이러한 이상치가 부정적인 영향을 끼칠 수 있으며, SMOTE는 주로 이진 분류 문제에 사용되기 때문에 다중 클래스 문제에 대한 처리가 상대적으로 어려울 수 있는 문제점들이 있지만, EDA를 해봤을 때 문제가 되는 이상치가 존재하지 않았으며, 데이터 불균형이 크지 않기 때문에 소수 클래스에 과도하게 적합될 가능성이 적어보였으며, 우리가 풀어야하는 문제는 이진분류 문제이기 때문에 이러한 SMOTE 방식을 사용하기로 정하였다.

 

이러한 SMOTE 방식을 적용하기 전 데이터에 대한 전처리를 미리 수행하는 것이 좋다.

먼저 데이터 전처리 하기 전 payment_pattern과 preferred_difficulty_level은 수치형으로 나오지만 데이터의 내용은 범주형과 같기 때문에 이 둘을 object 타입으로 변환을 시켜준다.

# train_data에서 community_engagement_level과 payment_pattern을 object 범주형으로 변환
train_data['community_engagement_level'] = train_data['community_engagement_level'].astype('object')
train_data['payment_pattern'] = train_data['payment_pattern'].astype('object')

# test_data또한 변환
test_data['community_engagement_level'] = test_data['community_engagement_level'].astype('object')
test_data['payment_pattern'] = test_data['payment_pattern'].astype('object')

# 잘 변환되었는지 확인
print(train_data.info(), '\n')
print(test_data.info())

community_engagement_level과 payment_pattern이 변환이 잘 된 것을 확인할 수 있다.

 

그 후 수치형 변수와 범주형 변수를 따로 변환시키기 위해 열을 뽑아내주도록 한다.

# 범주형 변수 열 리스트
col_cat = train_data.select_dtypes(include='object').columns.tolist()
# 수치형 변수 열 리스트
col_num = train_data.select_dtypes(exclude='object').columns.tolist()

print(col_cat, '\n')
print(col_num)

범주형 변수와 수치형 변수의 열

데이터 타입이 범주형인지 수치형인지에 따라 잘 분리되어서 리스트에 입력 잘 되었는지 확인해보니 잘 된것을 알 수 있다.

 

이제 EDA결과 중 " 2. average_time_per_learning_session은 오른쪽으로 꼬리가 긴 양수 왜도 형태를 띈다."에 대해서 데이터를 로그변환을 시켜 데이터 분포의 대칭성을 향상시켜준다.

# average_time_per_learning_session을 로그변환 시켜 분포의 대칭성 향상
train_data['average_time_per_learning_session'] = np.log1p(train_data['average_time_per_learning_session'])
test_data['average_time_per_learning_session'] = np.log1p(test_data['average_time_per_learning_session'])

 

이제 수치형 데이터에 대해서 스케일링을 수행을 할 것이다. 스케일링에 대한 방법론의 대표적인 방법으로, Min-Max Scaler, Standard Scaler, Robust Scaler가 존재하는데, 그 중 우리는 Standard Scaler를 사용을 할 것이다.

이는 각 스케일링 방법에 대한 장단점이 존재하기 때문이다.

  • RobustScaler:
    • 장점: 이상치에 강건함.
    • 단점: 이상치가 없는 경우에는 다른 스케일러들과 성능이 비슷할 수 있음.
  • StandardScaler:
    • 장점: 대부분의 경우에 효과적.
    • 단점: 이상치에 민감할 수 있음.
  • MinMaxScaler:
    • 장점: 데이터를 [0, 1] 범위로 정규화하여 동일한 스케일을 가지게 함.
    • 단점: 이상치에 민감할 수 있음.

이러한 장단점을 확인하며 Standard Scaler가 이상치에 영향이 적다면 대부분의 경우 효과적이기 때문에 사용하기로 한 것이다. 

Standard Scaler를 train_data와 test_data에 적용을 시켜본다.

from sklearn.preprocessing import StandardScaler
ss = StandardScaler() # Standard Scaler를 ss로 지정

# 수치형 데이터의 열을 저장한 col_num을 이용해 모든 수치형데이터의 열에 Standard Scaler수행
train_data[col_num] = ss.fit_transform(train_data[col_num])
test_data[col_num] = ss.fit_transform(test_data[col_num])

# 잘 적용되었는지 확인
train_data.head()

train_data의 수치형 데이터들이 StandardScaler 적용 되었는지 확인

이렇게 Standard Scaler가 잘 적용된 것을 확인을 할 수 있다.

 

이제 범주형 변수에 대해서 인코딩을 수행을 해야한다.

범주형 변수의 인코딩 방법 또한 여러가지 방법이 존재한다.

  1. 원핫 인코딩 (One-Hot Encoding):
    • 장점:
      • 범주 간의 순서나 계층을 나타내지 않으므로 명목형 변수에 적합합니다.
      • 모델에 범주형 정보를 제공하여 성능을 향상시킬 수 있습니다.
    • 단점:
      • 범주의 개수가 많은 경우, 특성의 차원이 증가하여 희소성이 증가하고 모델의 학습과 예측에 부담이 될 수 있습니다.
  2. 라벨 인코딩 (Label Encoding):
    • 장점:
      • 범주 간의 순서가 있는 경우에 활용 가능합니다.
      • 정수로 인코딩되어 연산이 빠르고 저장 공간을 절약합니다.
    • 단점:
      • 순서가 없는 명목형 변수에는 적합하지 않습니다.
      • 모델이 인코딩된 순서에 영향을 받을 수 있습니다.

이 외에도 많은 방법이 있지만 대표적으론 위 방법들이 존재한다. 원-핫 인코딩 방법은 순서나 계층이 없는 데이터에 대한 변환을 할 때 좋고, 라벨 인코딩은 순서가 있는 데이터에 대해서 수행을 하면 좋다는 것을 알 수 있다.

이러한 특성을 따라 순서가 존재하는 community_engagement_level, preferred_difficulty_level, subscription_type은 라벨 인코딩, 순서가 없는 payment_pattern은 원-핫 인코딩인 더미변수화를 시켜줄 것이다.

 

먼저 payment_pattern을 더미변수화를 시켜준다.

train_data = pd.get_dummies(train_data, columns = ['payment_pattern'])
test_data = pd.get_dummies(test_data, columns = ['payment_pattern'])
train_data.head()

payment_pattern 더미변수화 결과

payment_pattern에 대한 더미변수화가 잘 수행된 것 같다.

 

라벨 인코딩을 이제 수행해보도록 하는데, 라벨 인코딩은 1차원 배열에서만 작동하기 때문에 각각 하나씩 적용을 한다.

from sklearn.preprocessing import LabelEncoder
le = LabelEncoder

train_data['community_engagement_level'] = le.fit_transform(train_data['community_engagement_level'])
test_data['community_engagement_level'] = le.fit_transform(test_data['community_engagement_level'])

train_data['preferred_difficulty_level'] = le.fit_transform(train_data['preferred_difficulty_level'])
test_data['preferred_difficulty_level'] = le.fit_transform(test_data['preferred_difficulty_level'])

train_data['subscription_type'] = le.fit_transform(train_data['subscription_type'])
test_data['subscription_type'] = le.fit_transform(test_data['subscription_type'])

train_data.head()

라벨 인코딩 결과

community_engagement_level, preferred_difficulty_level, subscription_type의 라벨 인코딩이 잘 적용되었다.

 

이렇게 변수들에 대한 스케일링 및 인코딩이 완료되었기 때문에 이제 SMOTE를 수행을 할 것이다.

from imblearn.over_sampling import SMOTE #SMOTE 모듈
smote = SMOTE(sampling_strategy='auto') #auto로 지정해 데이터의 양을 1대1로 만들어 늘려준다.

# SMOTE 적용
x_resampled, y_resampled = smote.fit_resample(train_data, target_y)

# SMOTE 적용한 데이터 확인
x_resampled

SMOTE 적용 데이터

SMOTE 적용으로 10000개의 데이터가 12398개의 데이터로 증가한 것을 확인할 수 있다.

 

이제부터 예측모델을 만들 것이다. 많은 모델을 사용하고 서로 비교를 해 볼 것이다.

그 전에 데이터를 train, validation, test로 분리를 할 것이지만 validation은 데이터의 양이 12398개로 많지 않기 떄문에 분리하지 않고 train, test로만 데이터를 8대 2로 분리할 것이다.

from sklearn.model_selection import train_test_split

x_train, x_test, y_train, y_test = train_test_split(x_resampled, y_resampled, test_size = 0.2, random_state = 42)

random_state 시드를 42로 지정하고 test_sizefmf 0.2로 지정하여 train, test를 8대 2로 나눴으며, 데이터는 SMOTE를 수행한 x_resampled과 y_resampled를 사용하였다.

 

이제 사용할 모델을 모두 불러와본다.

from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.linear_model import LogisticRegression

dtc = DecisionTreeClassifier(random_state = 42) # 의사결정나무 분류기
rfc = RandomForestClassifier(random_state = 42) # 랜덤포레스트 분류기 
svc = SVC(random_state = 42)  #서포트벡터 머신 분류기
knc = KNeighborsClassifier(n_neighbors = 3) # 이웃수 3 / 최근접이웃 분류기
lr = LogisticRegression()  #로지스틱 회귀

dtc.fit(x_train, y_train)  # 의사결정나무 모델생성

rfc.fit(x_train, y_train)  # 랜덤포레스트 모델생성

svc.fit(x_train, y_train)  # 서포트벡터 머신 모델생성

knc.fit(x_train, y_train)  # 최근접 이웃 모델생성

lr.fit(x_train, y_train)  # 로지스틱 회귀 모델생성

이렇게 모델을 생성을 한다.

 

이제 모델에 대한 성능 점수를 계산을 한다. 위 경진대회에선 macro_f1의 점수를 기준으로 등수를 매겼다.

그렇지만 나는 macro_f1과 함께 accuracy_score까지 함께 확인해보도록 한다.

 

from sklearn.metrics import f1_score # f1_score 점수
from sklearn.metrics import accuracy_score # accuracy_score 점수

# 분리한 x_test로 모델별 예측값 생성
y_pred_dtc = dtc.predict(x_test)
y_pred_rfc = rfc.predict(x_test)
y_pred_svc = svc.predict(x_test)
y_pred_knc = knc.predict(x_test)
y_pred_lr = lr.predict(x_test)

# macro_f1 score 점수 계산
f1_score_dtc = f1_score(y_test, y_pred_dtc, average='macro')
f1_score_rfc = f1_score(y_test, y_pred_rfc, average='macro')
f1_score_svc = f1_score(y_test, y_pred_svc, average='macro')
f1_score_knc = f1_score(y_test, y_pred_knc, average='macro')
f1_score_lr = f1_score(y_test, y_pred_lr, average='macro')

# accuracy_score 점수 계산
accuracy_dtc = accuracy_score(y_test, y_pred_dtc)
accuracy_rfc = accuracy_score(y_test, y_pred_rfc)
accuracy_svc = accuracy_score(y_test, y_pred_svc)
accuracy_knc = accuracy_score(y_test, y_pred_knc)
accuracy_lr = accuracy_score(y_test, y_pred_lr)

# 점수 결과
print(f"DecisionTreeClassifier -- f1_score: {f1_score_dtc}, accuracy_score: {accuracy_dtc} \n")
print(f"RandomForestClassifier -- f1_score: {f1_score_rfc}, accuracy_score: {accuracy_rfc} \n")
print(f"SVC -- f1_score: {f1_score_svc}, accuracy_score: {accuracy_svc} \n")
print(f"KNeighborsClassifier -- f1_score: {f1_score_knc}, accuracy_score: {accuracy_knc} \n")
print(f"LogisticRegressor -- f1_score: {f1_score_lr}, accuracy_score: {accuracy_lr}")

각 모델 별 f1_score과 accuracy_score

f1_score하고 macro_f1의 점수는 이렇게 나왔다.

위 점수를 보니 모든 모델 중에서 macro_f1_score 점수가 가장 높은 모델은 RandomForestClassifier모델이고, accuracy_score 기준 또한 RandomForestClassifier가 점수가 높게 나온 것을 알 수 있다.

하지만 각 모델에서 점수가 높다고 무조건 좋은 것이 아닌 과대적합이나 과소적합에 대해서도 유의해야 한다.

이를 한번 확인해보기 위해 학습곡선을 그려보도록 한다.

import matplotlib.pyplot as plt
import seaborn as sns
from sklearn.model_selection import learning_curve

# 모델, 특성, 타겟 데이터를 사용하여 학습 곡선 생성 , cv=5로 교차검정 5번 수행
train_sizes, train_scores, test_scores = learning_curve(
    rfc, x_resampled, y_resampled, cv = 5, train_sizes=np.linspace(0.1, 1.0, 10))
    
# 훈련 세트와 테스트 세트의 성능 평균 및 표준 편차 계산
train_scores_mean = np.mean(train_scores, axis=1)
train_scores_std = np.std(train_scores, axis=1)
test_scores_mean = np.mean(test_scores, axis=1)
test_scores_std = np.std(test_scores, axis=1)
# 학습 곡선 그리기
plt.figure(figsize=(10, 6))
plt.title("Learning Curve")
plt.xlabel("Training examples")
plt.ylabel("Score")
plt.grid()

plt.fill_between(train_sizes, train_scores_mean - train_scores_std,
                 train_scores_mean + train_scores_std, alpha=0.1, color="r")
plt.fill_between(train_sizes, test_scores_mean - test_scores_std,
                 test_scores_mean + test_scores_std, alpha=0.1, color="g")
plt.plot(train_sizes, train_scores_mean, 'o-', color="r", label="Training score")
plt.plot(train_sizes, test_scores_mean, 'o-', color="g", label="Cross-validation score")

plt.legend(loc="best")
plt.show()

RandomForestClassifier 학습 곡선

점수가 가장 높았던 RandomForestClassifier 에 대해서 학습곡선을 그려서 학습이 잘되었는지, 과대적합인지, 과소적합인지를 확인을 해본 결과, Cross-Validation Score에 대해서는 점진적 상승을 일으키지만, Training Score가 1로 적합이 되어있는 형태인 과대적합 형태로 볼 수 있다.

이러한 과대적합은 모델이 훈련 데이터에 대해 너무 맞춰져서 새로운 데이터에 대한 일반화 성능이 떨어져서 생긴 것이다. 이러한 과대적합을 해결하기 위해서는 모델에 대한 단순화를 수행을 해야한다. 먼저 모델에 대한 하이퍼파라미터에 대해서 최적의 값을 찾아 과대적합이 해결되는지 확인을 하도록 한다. 우리가 고쳐야 할 하이퍼파라미터 종류는 아래와 같다.

 

랜덤포레스트 하이퍼파리미터의 종류

n_estimators:

  • 의사결정나무의 개수를 지정합니다. 많은 트리를 사용하면 모델이 더 강력해지지만, 계산 비용이 증가하게 됩니다. 기본값은 100입니다.

criterion:

  • 노드의 분할 기준을 지정합니다. "gini" 또는 "entropy" 중에서 선택할 수 있습니다. 기본값은 "gini"입니다.

max_depth:

  • 각 의사결정나무의 최대 깊이를 제한합니다. 더 깊은 트리는 더 복잡한 모델을 생성할 수 있지만, 계산 비용이 증가합니다.

min_samples_split:

  • 노드를 분할하기 위한 최소한의 샘플 수를 지정합니다. 이 수보다 작으면 더 이상 분할하지 않습니다.

min_samples_leaf:

  • 리프 노드가 가져야 하는 최소한의 샘플 수를 지정합니다.

이러한 하이퍼파라미터에 대해서 최적의 값을 찾은 후 다시 한번 모델에 대한 학습곡선을 그려서 판단해보도록 한다.

 

다음 글에서 다른 모델에 대한 학습곡선 그림과 각 모델에 대한 하이퍼퍼라미터 튜닝 및 그리드 서치를 통한 최적의 하이퍼파라미터 찾는 방법을 수행하며, 머신러닝 모델이 아닌 딥러닝 모델인 Tensorflow Dense를 사용하여 모델을 만드는 것을 수행해 볼 것이다.