본문 바로가기
Sprint_DA01/스프린트 미션

Mission 13 - 포르투갈 은행의 마케팅 데이터 분석

by Toddler_AD 2024. 10. 24.

1. Situation

이번 미션에서는 포르투갈 은행의 마케팅 데이터를 분석해볼 예정입니다. 이 실습에서는 결정 트리와 앙상블 기법을 사용하여 분류 모델을 구축하고, 마케팅 캠페인의 효율성을 높이는 전략을 도출해 보겠습니다.

 

미션 배경

여러분은 지금부터 포르투갈 은행의 마케팅 담당자입니다. 데이터는 2008년부터 2010년까지의 은행 마케팅 캠페인 데이터를 포함하고 있습니다. 여러분의 목표는,

  • 이 데이터를 통해 고객이 정기 예금을 가입할 가능성을 예측하고,
  • 이를 통해 마케팅 캠페인의 효율성을 높이는 것입니다.

마케팅 담당자로서 정기 예금과 관련이 있는 요소들을 파악해보고, 고객의 행동을 이해해보세요. 어떤 상황에서 어떤 고객들이 정기 예금을 가입할까요?

 

 

 

2. Task

이번 미션의 최종 목표는,

  • 가장 정확한 분류 모델을 개발하여 고객이 정기 예금을 가입할지 여부를 예측하고,
  • 그 모델을 통해 도출한 인사이트를 바탕으로 비즈니스 전략을 제시하는 것입니다.

 

 

 

 

3.  Action

1. 범주형 변수 시각화

 

2. 범주형 변수 - Unknown 값 정제

# 직업군에 따른 교육 수준 대체 로직
job_education_map = {
    'admin.': 'university.degree',
    'blue-collar': 'basic.9y',
    'entrepreneur': 'university.degree',
    'housemaid': 'basic.4y',
    'management': 'university.degree',
    'retired': 'university.degree',
    'self-employed': 'university.degree',
    'services': 'high.school',
    'student': 'university.degree',
    'technician': 'professional.course',
    'unemployed': 'high.school'
}

bank_df['education'] = bank_df.apply(
    lambda row: job_education_map[row['job']]
                if row['education'] == 'unknown' and row['job'] in job_education_map
                else row['education'],
    axis=1
)
  • job_education_map 딕셔너리를 사용하여 각 직업군에 일반적으로 요구되는 교육 수준을 미리 정의.
  • education 값이 unknown이고 job이 매핑에 존재하는 경우 해당 직업군에 맞는 교육 수준을 대체로 사용.
  • 만약 job이 job_education_map에 없거나 education이 이미 설정된 경우 기존 값을 유지.

 

def impute_marital(row):
    if row['marital'] == 'unknown':
        if row['age'] > 30:
            return 'married'
        else:
            return 'single'
    return row['marital']
  • marital이 unknown이면 연령을 기반으로 결측값을 추정.
    • age > 30이면 married로 추정하고,
    • age <= 30이면 single로 설정하여 젊은 층을 결혼하지 않은 상태로 추정.
  • marital이 unknown이 아닌 경우 기존 값을 그대로 유지.

 

def impute_loan(row):
    if row['loan'] == 'unknown':
        if row['job'] in ['blue-collar', 'services']:
            return 'yes'
        else:
            return 'no'
    return row['loan']
  • loan 값이 unknown인 경우 특정 직업군(blue-collar, services)은 대출 가능성이 높다고 보고 yes로 대체.
  • 그렇지 않은 직업군의 경우 기본값으로 no를 대체.

 

def impute_housing(row):
    if row['housing'] == 'unknown':
        if row['job'] in ['blue-collar', 'services'] or row['education'] == 'basic.9y':
            return 'yes'
        else:
            return 'no'
    return row['housing']
  • housing 값이 unknown인 경우 직업이 blue-collar 또는 services인 경우 yes로 대체.
  • education이 basic.9y인 경우, 경제적 상황을 고려하여 주택 대출이 필요할 가능성이 높다고 추정하여 yes로 대체.

 

def impute_job(row):
    if row['job'] == 'unknown':
        if row['education'] in ['basic.9y', 'basic.4y'] and row['age'] > 40:
            return 'blue-collar'
        elif row['education'] in ['high.school', 'professional.course']:
            return 'technician'
        else:
            return 'services'
    return row['job']
  • job이 unknown인 경우, education이 basic.9y 또는 basic.4y이며 age > 40인 경우 blue-collar로 추정.
  • education이 high.school 또는 professional.course인 경우 technician으로, 그 외의 경우는 services로 대체.

 

def impute_education(row):
    if row['education'] == 'unknown':
        if row['job'] == 'student':
            return 'university.degree'
        elif row['job'] == 'blue-collar':
            return 'basic.9y'
        else:
            return 'high.school'
    return row['education']
  • job이 student인 경우 education을 university.degree로 설정하여 학업 중인 상태를 반영.
  • job이 blue-collar인 경우 basic.9y로 설정하며, 그 외의 경우 high.school로 대체.

 

def impute_default(row):
    if row['default'] == 'unknown':
        if row['loan'] == 'yes' or row['housing'] == 'yes':
            return 'yes'
        else:
            return 'no'
    return row['default']
  • default가 unknown이면 loan이나 housing 중 하나라도 yes로 설정된 경우 default를 yes로 대체하여 대출 상환의 위험성을 반영.
  • 그렇지 않으면 no로 대체.

 

print(bank_df.isin(['unknown']).sum())  # unknown이 대체되었는지 확인

 

 

3. 수치형 변수 시각화

 

4. 수치형 변수 - 상관관계

  • 경제적 변수들 (emp.var.rate, euribor3m, nr.employed, cons.price.idx)는 서로 높은 상관관계를 보이며, 전반적인 경제 상황을 반영하는 지표로 볼 수 있다.
  • 캠페인 관련 변수들 (previous, pdays, campaign)은 상호 관계가 약하거나 특정 조건에 따라 음의 상관관계를 보이지만, 경제 변수들과는 비교적 독립적이다.
  • 고객 개인 정보 변수 (age, duration)는 대부분의 변수와 상관관계가 낮아 캠페인과 독립적인 요소로 판단할 수 있다.

 

 

5. 결정트리와 앙상블 기법

  • 결정 트리 모델
from sklearn.pipeline import Pipeline

# 1. 결정 트리 모델

# 파이프라인 정의
pipeline = Pipeline(steps=[
    ('preprocessor', preprocessor),
    ('classifier', DecisionTreeClassifier(max_depth= 5, random_state= 42))
])
# 모델 학습
pipeline.fit(X_train, y_train)

# 예측
y_pred_dt = pipeline.predict(X_test)
y_pred_proba_dt = pipeline.predict_proba(X_test)  # 예측 확률

# 분류 모델 평가
accuracy, precision, recall, f1, roc_auc, confusion_mat, class_report = evaluate_class_model(y_test, y_pred_dt, y_pred_proba_dt)

# 결과 출력
print("Decision Tree Model Evaluation")
print("Accuracy:", accuracy)
print("Precision:", precision)
print("Recall:", recall)
print("F1 Score:", f1)
print("ROC AUC:", roc_auc)
print("Confusion Matrix:\n", confusion_mat)
print("Classification Report:\n", class_report)

Class 0 - 정기 예금에 가입하지 않은 고객 집단

Class 1 - 정기 예금에 가입한 고객 집단

  1. Precision (정밀도): 모델이 예측한 긍정 클래스 중 실제로 맞은 비율.
    1. Class 0: 정밀도가 0.94로 매우 높아, Class 0으로 예측한 값의 94%가 실제로 맞았다.
    2. Class 1: 정밀도는 0.65로 상대적으로 낮아, Class 1로 예측한 값의 65%만이 실제로 맞았다.
  2. Recall (재현율): 실제 긍정 클래스 중에서 모델이 올바르게 예측한 비율.
    1. Class 0: 0.96으로 높은 재현율을 보여 Class 0에 대해 잘 탐지한다.
    2. Class 1: 0.54로 낮은 재현율을 보여 Class 1에 대한 탐지가 부족하다.
  3. F1-Score: 정밀도와 재현율의 조화 평균으로, 전체적인 균형을 보여준다.
    1. Class 0: F1-Score가 0.95로 매우 높아 Class 0 예측에서 우수한 성능을 보인다.
    2. Class 1: F1-Score는 0.59로 낮아 Class 1 예측에서 성능이 다소 떨어진다.
  4. Accuracy (정확도): 전체 데이터에서 모델이 올바르게 예측한 비율로 0.92 (92%)이다.
  5. Macro Average: 각 클래스의 지표를 단순 평균한 값으로 클래스 비율에 관계없이 모델 성능을 평가.
    1. F1-Score: 0.77로, 전체 클래스의 평균 성능이 중간 정도임을 보여준다.
  6. Weighted Average: 클래스 비율을 고려한 평균으로, 데이터의 클래스 불균형을 반영하여 성능을 평가.
    1. F1-Score: 0.91로 전체 데이터에서 전반적으로 우수한 성능을 보인다.

종합해석

모델은 Class 0에서 높은 성능을 보이지만, Class 1에 대해서는 성능이 상대적으로 떨어진다. 데이터가 Class 0에 더 집중되어 있어(총 8238개의 데이터 중 7303개가 Class 0) 전체 정확도는 92%로 높지만, Class 1에서 정밀도와 재현율이 낮기 때문에 이 클래스에서의 성능 향상을 위해 추가적인 조치(데이터 증강, 모델 개선 등)가 필요하다.

 

  • 랜덤포레스트 모델
# 2. 랜덤 포레스트 모델

# 파이프라인 정의 (랜덤 포레스트 모델 포함)
pipeline_rf = Pipeline(steps=[
    ('preprocessor', preprocessor),
    ('classifier', RandomForestClassifier(n_estimators= 100, max_depth= 5, random_state= 42))
])
# 모델 학습
pipeline_rf.fit(X_train, y_train)

# 예측
y_pred_rf = pipeline_rf.predict(X_test)
y_pred_proba_rf = pipeline_rf.predict_proba(X_test)  # 예측 확률

# 분류 모델 평가
accuracy, precision, recall, f1, roc_auc, confusion_mat, class_report = evaluate_class_model(y_test, y_pred_rf, y_pred_proba_rf)

# 결과 출력
print("Random Forest Model Evaluation")
print("Accuracy:", accuracy)
print("Precision:", precision)
print("Recall:", recall)
print("F1 Score:", f1)
print("ROC AUC:", roc_auc)
print("Confusion Matrix:\n", confusion_mat)
print("Classification Report:\n", class_report)

Class 0 - 정기 예금에 가입하지 않은 고객 집단

Class 1 - 정기 예금에 가입한 고객 집단

  1. Precision (정밀도): 모델이 특정 클래스를 예측할 때 맞춘 비율.
    1. Class 0: 0.91로 높아, Class 0으로 예측한 값 중 91%가 맞았다.
    2. Class 1: 0.77로, Class 1으로 예측한 값 중 77%가 맞았다.
  2. Recall (재현율): 실제 클래스 중에서 모델이 정확히 예측한 비율.
    1. Class 0: 0.99로 높아, Class 0의 거의 모든 항목을 잘 탐지.
    2. Class 1: 0.19로 매우 낮아, Class 1에 대해 모델이 제대로 탐지하지 못하고 있다.
  3. F1-Score: 정밀도와 재현율의 조화 평균.
    1. Class 0: F1-Score가 0.95로, Class 0에 대해 모델이 매우 우수한 성능을 보인다.
    2. Class 1: F1-Score가 0.30으로 낮아, Class 1 예측에서 성능이 많이 떨어진다.
  4. Accuracy (정확도): 전체 데이터에서 모델이 맞춘 비율로, 90%.
  5. Macro Average: 각 클래스의 지표를 단순 평균하여 계산한 값으로, 클래스 간의 성능 차이를 반영.
    1. F1-Score: 0.63으로, 두 클래스의 평균 성능이 비교적 낮음을 나타낸다.
  6. Weighted Average: 클래스 비율을 반영하여 계산한 평균으로, 데이터의 클래스 불균형을 고려.
    1. F1-Score: 0.87로, 전체 데이터에서 성능이 비교적 높음을 나타낸다.

종합 해석

모델은 Class 0에 대해 우수한 성능을 보이지만, Class 1에 대해 성능이 떨어진다. 특히 Class 1에서 재현율이 매우 낮아, 이 클래스의 데이터를 잘 탐지하지 못하는 문제가 있다. 데이터 불균형 문제를 해결하거나 Class 1 탐지를 강화하는 방향으로 모델을 개선할 필요가 있다.

 

  • 그라디언트 부스팅 모델
# 3. 그라디언트 부스팅 모델

# 파이프라인 정의 (그라디언트 부스팅 모델 포함)
pipeline_gb = Pipeline(steps=[
    ('preprocessor', preprocessor),
    ('classifier', GradientBoostingClassifier(n_estimators= 100, learning_rate= 0.1, max_depth= 3, random_state= 42))
])
# 모델 학습
pipeline_gb.fit(X_train, y_train)

# 예측
y_pred_gb = pipeline_gb.predict(X_test)
y_pred_proba_gb = pipeline_gb.predict_proba(X_test)  # 예측 확률

# 분류 모델 평가
accuracy, precision, recall, f1, roc_auc, confusion_mat, class_report = evaluate_class_model(y_test, y_pred_gb, y_pred_proba_gb)

# 결과 출력
print("Gradient Boosting Model Evaluation")
print("Accuracy:", accuracy)
print("Precision:", precision)
print("Recall:", recall)
print("F1 Score:", f1)
print("ROC AUC:", roc_auc)
print("Confusion Matrix:\n", confusion_mat)
print("Classification Report:\n", class_report)

Class 0 - 정기 예금에 가입하지 않은 고객 집단

Class 1 - 정기 예금에 가입한 고객 집단

  1. Precision (정밀도): 모델이 특정 클래스를 예측할 때 실제로 맞춘 비율.
    1. Class 0: 정밀도가 0.94로 높아, 모델이 Class 0으로 예측한 경우 중 94%가 실제로 맞았다.
    2. Class 1: 정밀도는 0.68로, Class 1로 예측한 경우 중 68%가 맞았다.
  2. Recall (재현율): 실제 클래스 중에서 모델이 정확히 예측한 비율.
    1. Class 0: 재현율이 0.97로 높아, Class 0의 대부분을 잘 예측했다.
    2. Class 1: 재현율이 0.53으로 낮아, Class 1 데이터를 제대로 탐지하지 못하는 경우가 많다.
  3. F1-Score: 정밀도와 재현율의 조화 평균. F1-Score는 두 지표 간의 균형을 나타내므로 모델 성능의 전체적인 수준을 보여준다.
    1. Class 0: F1-Score가 0.95로 매우 높아, Class 0에 대한 모델 성능이 우수함을 나타낸다.
    2. Class 1: F1-Score가 0.60으로 낮아, Class 1에 대한 모델 성능이 상대적으로 부족함을 보여준다.
  4. Accuracy (정확도): 전체 데이터 중에서 모델이 맞춘 비율로, 0.92 (92%).
  5. Macro Average: 각 클래스의 성능 지표를 단순 평균한 값으로, 클래스 비율에 상관없이 모델 성능을 평가.
    1. F1-Score: 0.78로, 각 클래스의 성능을 평균했을 때 비교적 중간 수준의 성능을 보여준다.
  6. Weighted Average: 각 클래스의 데이터 비율을 고려한 평균으로, 데이터 불균형을 반영하여 모델 성능을 평가
    1. F1-Score: 0.91로, 데이터 비율을 고려했을 때 모델이 전반적으로 우수한 성능을 보입니다.

종합 해석

모델은 Class 0에서 매우 높은 성능을 보이지만, Class 1에 대해 상대적으로 낮은 성능을 보인다. 특히 Class 1의 재현율이 낮아, 이 클래스에 대한 탐지가 부족할 수 있다. 이 경우, Class 1에 대한 성능을 개선하기 위해 데이터 증강이나 클래스 불균형 처리 등의 추가적인 작업이 필요하다.

 

 

  • 중요 변수 식별 특성
# 중요 변수 확인
# ColumnTransformer를 통해 변환된 특성 이름 가져오기
feature_names = (pipeline_rf.named_steps['preprocessor']
                 .transformers_[0][1]
                 .get_feature_names_out(numeric_features).tolist() +
                 pipeline_rf.named_steps['preprocessor']
                 .transformers_[1][1]
                 .get_feature_names_out(categorical_features).tolist())

# 중요도 데이터프레임 생성
importances = pd.DataFrame({'Feature': feature_names, 'Importance': pipeline_rf.named_steps['classifier'].feature_importances_})

# 중요도 출력
print("\n중요 변수로 식별한 특성 in Random Forest Model:")
print(importances.sort_values(by='Importance', ascending= False))

주요 변수 분석
1. duration (0.267059): 상담의 지속 시간은 캠페인 성공에 가장 큰 영향을 미치는 변수. 이는 고객이 상담 과정에서 얼마나 적극적으로 참여했는지를 나타낸다.
- 전략: 상담 시간을 늘리기 위해 상담원의 교육을 강화하고, 고객의 질문에 충분히 답변할 수 있는 방안을 마련. 고객과의 상호작용을 늘리기 위해 통화 후 후속 조치를 계획.

2. euribor3m (0.138411): 이자율은 금융 상품에 대한 고객의 결정에 중요한 요소. 고객의 경제적 상황에 따라 상품에 대한 반응이 달라질 수 있다.
- 전략: 이자율 변동을 모니터링하고, 고객에게 현재의 경제 상황에 맞는 금융 상품을 추천.

3. nr.employed (0.122257): 고용 인원 수는 고객의 소득 수준을 나타내며, 이는 금융 상품 구매 능력과 연관.
- 전략: 고용 상황에 따라 맞춤형 금융 상품을 제공하여 고객의 구매 가능성을 높인다.

4. poutcome_success (0.093107): 이전 캠페인의 성공 여부는 현재 캠페인의 성과에 큰 영향을 미친다.
- 전략: 이전 성공 사례를 강조한 마케팅 자료를 개발하고, 성공적인 고객을 대상으로 한 후속 캠페인을 진행.

5. pdays (0.079365): 이전 연락 시점으로부터의 경과일 수로, 고객의 관심과 반응이 시간에 따라 달라질 수 있다.
- 전략: 이전에 연락을 받았던 고객을 대상으로 적절한 시간 간격으로 후속 연락을 시도하고, 이들에게 맞춤형 예금 상품 제안.

 

상대적으로 낮은 중요도의 변수
1. age (0.013963): 고객의 연령대는 상대적으로 적은 영향을 미치는 것으로 나타났다.
- 전략: 연령대에 따라 다르게 접근하는 것보다는, 고객의 경제적 상황이나 이전 반응에 더 집중하는 것이 효과적일 수 있다.

2. housing_yes / housing_no (0.000273 / 0.000242): 주택 보유 여부는 중요도가 낮은 변수로 나타났다.
- 전략: 이 변수가 중요하지 않음을 고려하여 주택 관련 금융 상품의 마케팅에 대한 리소스를 줄이고, 다른 변수에 더 집중한다.

 

 

5. Result 

- 클래스간 데이터 불균형이 심하여,

- ROC 지표점수가 90% 이상 상회하는 모델성능을 보여주었음에도,

- 정기예금에 가입하지 않은 고객 집단인 클래스 0의 정밀도와 재현율이 95%를 상회할 만큼 높은 반면 ,

- 정기예금에 가입한 고객집단인 클래스 1의 정밀도와 재현율은 60%의 오차범위 ± 10% 에서 표현될 만큼 차이가 심한 결과를 보여 주었다.

- 데이터 불균형을 위한 처리(예: 오버샘플링, 언더샘플링, 클래스 가중치 적용 등)를 통해 모델 성능을 개선해야 될 과제가 있음을 확인하게 되었다.

- 주요 변수 분석을 통한 마케팅 전략 제시

  • duration : 상담시간을 지속하기 위한 방안을 최우선순위로 수립해야 한다.
  • euribor3m : 이자율의 변동은 예금 가입 요소로써 민감에 작용하고 있음을 유의해야 한다.
  • nr.employed : 고객의 소득수준에 맞는 예금 상품이 준비되어 있어야 예금 유치에 성공할 수 있는 확률이 높아진다.
  • poutcome_success : 예금 상품에 대한 마케팅 성공 경험이 현재의 마케팅 성공을 위한 자신감 확립에 큰 경험이 된다.
  • pdays : 마케팅이 노출되는 시간 사이의 공백이 너무 길어서는 안된다. 고객이 은행의 마케팅을 잊어버리지 않을 만한 적절한 시간간격을 유지하는 것이 중요하다.

 

 

6. 부록

1. 추가 모델 적용

from sklearn.linear_model import LogisticRegression, RidgeClassifier
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis
from sklearn.naive_bayes import GaussianNB, MultinomialNB, BernoulliNB
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, AdaBoostClassifier, GradientBoostingClassifier, StackingClassifier, VotingClassifier
from sklearn.ensemble import BaggingClassifier, ExtraTreesClassifier
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
from sklearn.neural_network import MLPClassifier
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier
from catboost import CatBoostClassifier
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.compose import ColumnTransformer

# 분류 모델을 담은 딕셔너리
models = {
    "Logistic Regression": LogisticRegression(),
    "Ridge Classifier": RidgeClassifier(),
    "Linear Discriminant Analysis": LinearDiscriminantAnalysis(),
    "Gaussian Naive Bayes": GaussianNB(),
    "Multinomial Naive Bayes": MultinomialNB(),
    "Bernoulli Naive Bayes": BernoulliNB(),
    "Decision Tree": DecisionTreeClassifier(),
    "Random Forest": RandomForestClassifier(),
    "Extra Trees": ExtraTreesClassifier(),
    "AdaBoost": AdaBoostClassifier(),
    "Gradient Boosting": GradientBoostingClassifier(),
    "XGBoost": XGBClassifier(use_label_encoder= False, eval_metric= 'logloss'),
    "LightGBM": LGBMClassifier(force_row_wise= True),
    "CatBoost": CatBoostClassifier(silent= True),
    "SVC (Support Vector Machine)": SVC(probability= True),
    "K-Nearest Neighbors": KNeighborsClassifier(),
    "MLP Classifier": MLPClassifier(max_iter= 1000),
    "Bagging Classifier": BaggingClassifier(),

    # 스태킹 및 보팅 앙상블을 활용한 복합 모델


    "Voting Classifier": VotingClassifier(
        estimators=[
            ('lgb', LGBMClassifier(force_row_wise= True)),
            ('gb', GradientBoostingClassifier()),
            ('xgb', XGBClassifier(use_label_encoder= False, eval_metric= 'logloss'))
        ],
        voting='soft'
    ),

        "Stacking Classifier": StackingClassifier(
        estimators=[
            ('lgb', LGBMClassifier(force_row_wise= True)),
            ('gb', GradientBoostingClassifier()),
            ('xgb', XGBClassifier(use_label_encoder= False, eval_metric= 'logloss'))
        ],
        final_estimator= CatBoostClassifier(silent= True)
    )
}

 

 

 

 

 

2. 결과 평가

 

 

 

2. 혼동 행렬

 

 

 

3. Classification Report