벌써 어느새,,, 머신러닝 모델 종류들과 딥러닝 역전파에대해서 배울 수 있었다. 특별히 이번주는 팀프로젝트도 진행해서 엄청 빨리 지나갔다.
1차 프로젝트는 자동차 관련 자유 주제였는데 2차 프로젝트는 가입 고객 이탈 예측하는 모델을 개발하는것이었다. 가입 고객 이탈 예측 데이터를 찾기에는 아무래도 공개해주는 기업이 많이 없고 종류도 많이 없기때문에 어떤 데이터로 개발을 진행할지에대한 고민이 많았다.
Kaggel 데이터셋 중 사용할 수 있는 데이터들 목록을 추렸고 Churn 예측 연구에서 가장 많이 쓰는 표준 데이터셋은 Telco Customer Churm 이었다. 다른 팀에서 무조건 겹칠 주제로 생각이 되어서 제외시켰다. Spotify 주제로 결정을 했으나 이것또한 다른 팀에서 이미 한다고 해서 다른 주제로 변경하였다.
형욱님이 발견하신 Bank Customer Churn Dataset 데이터도 깔끔하고 칼럼이 좀 부족하긴 했으나 데이터 개수는 10000개정도 되어서 해당 데이터셋으로 진행하기로 결정했다.
이번 팀프로젝트는 각자 개발을 하고 이야기해보기로 결정해서 데이터 EDA부터 성능평가까지 모두 진행해볼 수 있었다.
해당 데이터는 결측치를 가진 칼럼이 없었고 이탈 여부가 0:1 -> 8:2 정도로 비대칭 데이터셋이었다.


📌 데이터 EDA
왼쪽 상관계수 그래프를 통해 칼럼간의 연관성이 낮다는 사실을 인지했다. 중요한 칼럼들을 가지고 새로운 칼럼을 만들기 위해 모델에 중요도 높은 칼럼들을 출력해서 사용했는데
오른쪽 이미지처럼 이선님께서 EDA 작업에서 진행하셨던 그래프인데 이탈 여부에 가장 관련 높은 칼럼들을 시각화하기 위해 그리신 SHAP 그래프가 한눈에 보기 너무 좋아서 시각화의 중요성을 한 번 더 느낄 수 있었다.


📌 데이터 전처리
1️⃣ 제일 첫번째로 모델 예측할 때 의미없는 칼럼인 '식별자 ID' 칼럼을 제거했다.
df = df.drop(columns=['customer_id'], errors='ignore')
2️⃣ 두번째로 문자열 데이터를 숫자형으로 변환해주기 위해 'gender'와 'country'를 OneHotEncoder를 해주었다.
'gender' ➡️ 'gender_Female', 'gender_Male'
'country' ➡️ 'country_France', 'country_Germany', 'country_Spain'
def one_hot_encode(df, cols):
ohe = OneHotEncoder(sparse_output=False, drop='first')
encoded = ohe.fit_transform(df[cols])
encoded_df = pd.DataFrame(encoded,
columns=ohe.get_feature_names_out(cols),
index=df.index)
return pd.concat([df.drop(columns=cols), encoded_df], axis=1)
new_df = one_hot_encode(new_df, ['country', 'gender'])

3️⃣ 0 값을 많이 가지고 있었던 balance... Sacler
balance는 고객이 은행 계좌에 보유하고 있는 금액인데,,, 0이 엄청 많은 칼럼이었지만 제거하기에는 결과값에 영향을 미치는 칼럼이었다.

극단값(outlier)과 편향된 분포(skewness)가 매우 큰 변수이기 때문에,
평균/최대값에 민감한 MinMaxScaler나 StandardScaler(log1p 포함)보다, 중앙값(median)과 IQR을 기준으로 하는 RobustScaler가 훨씬 안정적일 것으로 판단했다.
4️⃣ 다른 수치형 칼럼들은 StandardScaler 진행했다.
numeric_cols = ["tenure", "credit_score", "estimated_salary", "products_number"]
df[numeric_cols] = StandardScaler().fit_transform(df[numeric_cols])
5️⃣ Age 칼럼의 경우, 연령대별 특징이 존재하기때문에 group을 나눠 LabelEncoder를 해주었다.
그리고 기존 Age 칼럼은 제거해줌. 연령대를 더 세부적으로 나눠보기도 했는데 이것보다 세부적이면 오히려 성능 저하가 발견되었다.
age_encoder = LabelEncoder()
age_bins = [0, 30, 40, 50, 60, 100]
age_labels = ['20s', '30s', '40s', '50s', '60+']
age_group_series = pd.cut(
df['age'],
bins=age_bins,
labels=age_labels
)
df['age_group'] = age_encoder.fit_transform(age_group_series)
📌 비교 모델 선택 과정
분류 선정 모델의 종류는 다양하게 많지만 Logistic Regression ➡️ RandomForest ➡️ XGBoost ➡️ LightGBM 순으로 모델을 선정했다. Logistic 모델로 기본 베이스 성능 평가 점수를 확인했고, Churn 데이터는 0/1 이진분류가 가능했기때문에 트리 형태인 RandomForest 모델도 선정하게 되었다.
부트스트랩(Boostrap) 중복 허용하여 데이터를 무작위로 샘플링하고 집계(Aggregation) 여러 모델의 예측을 합쳐 최종 예측을 만드는 것.
RandomForest 모델은 Bagging(Bootstrap Aggregating) 기반의 앙상블 모델로 여러 모델을 서로 다르게 학습시킨 뒤, 그 모델들의 예측을 평균 또는 투표해서 더 안정적인 결과를 만드는 앙상블 방법이다.
Boosting 모델은 여러 개의 약한 트리(weak learners)를 순차적으로 학습시키면서 각 모델이 이전 모델의 오차(error)를 보완하도록 만들어지는 Gradient Boosting 기반의 고성능 앙상블 기법.
마지막으로 불균형 데이터의 처리 능력을 높이기 위해 Boosting 계열 모델 중 XGBoost와 LightGBM을 진행하기로 했는데,
LightGBM은 가장 손실 감소가 큰 노드를 타고타고 깊게 확장하는 방식이라 좀 더 세부적인 모델 예측이 나올 수 있다. 그대신 XGBoost에비해 과적합이 날 가능성도 높아진다. 이러한 차이점으로 XGBoost와 LightGBM는 모두 비교 모델에 선정하게되었다.
CatBoost 모델은 칼럼이 많은 데이터도 아니고 직접 One-hot 데이터 전처리는 진행하고 싶었기때문에 제외시켰다.


📌 하이퍼파리미터 튜닝
하이퍼파라미터 튜닝은 RandomSearchCV를 사용했다. 여러 파라미터를 조정하다보니 과적합이 일어나 성능 저하가 일어났고,
결국 최소 params만 가지고 진행했다. (n_estimators, man_depth, learning_rate,,,)
📌 다시 데이터 전처리에서 칼럼 추가
아무래도 칼럼이 적어 모델 성능을 더 높이기 위해서 칼럼 추가가 필요해보였다. 찾아보니 결과에 영향을 많이 주는 중요한 칼럼들끼리 곱해서 새로운 칼럼을 만들어 가중치를 더 주는걸로 작업을 한다고 되어있었다.
LogisticRegression 모델에서는 model.coef_[0]으로, 다른 모델들은 model.feature_importances_로 모델 예측에 중요하게 영향을 미치는 칼럼들을 추출하였다.
def get_top_n_features(feature_names, importances, n=5):
# 절대값 변환
abs_imp = np.abs(importances)
# DataFrame으로 정리
df_imp = pd.DataFrame({
"feature": feature_names,
"importance": abs_imp
})
# 중요도 내림차순 정렬
df_sorted = df_imp.sort_values("importance", ascending=False)
# 상위 N개 feature 이름 리턴
return df_sorted.head(n)["feature"].tolist()
###########################################################################
if name == "LogisticRegression":
feature_importances = get_top_n_features(X_train.columns, model.coef_[0])
print(f"{name} FEATURE IMPORTANCES : \n{feature_importances}")
else:
feature_importances = get_top_n_features(X_train.columns, model.feature_importances_)
print(f"{name} FEATURE IMPORTANCES : \n{feature_importances}")

해당 결과를 가지고 4개의 칼럼을 뽑아 새로운 칼럼들을 생성했다.
칼럼을 추가하고 안하고의 Recall 성능은 크게 차이가 없었으나 Precision 성능이 올라가는 것을 확인했다.
# Interaction Feature
# Age × Active Member
df["age_x_active"] = df["age"] * df["active_member"]
# # Product × Active Member
df["products_x_active"] = df["products_number"] * df["active_member"]
# # Age × Product Number
df["age_x_products"] = df["age"] * df["products_number"]
# # estimated_salary × age
df["estimated_x_age"] = df["estimated_salary"] * df["age"]
# # estimated_salary × active_member
df["estimated_x_active"] = df["estimated_salary"] * df["active_member"]
https://github.com/SKNETWORKS-FAMILY-AICAMP/SKN21-2nd-1Team
📌 최종 목표 : Recall >= 0.7 and Precision >= 0.5
우리 팀의 모델의 성능은 실제 이탈 고객을 놓치지 않고 최대한 잡기 위해 Recall은 0.7~0.8 보다는 높은 성능을 보이도록 집중했고,
과도한 False Positive를 방지하기 위해 Precision은 0.5 이상을 목표로 진행했다.
predict_proba 0과 1이 될 확률을 볼 수 있는데 임계값 default인 중간값 0.5 기준으로 0.5보다 높으면 이탈 1, 낮으면 유지 0으로 결과값을 내보내게 된다.
임계값이 0.5에서 0.3으로 낮추면 확률이 0.35인 경우, 이탈이라고 볼 수 있는 것이다. 임계값을 0.2까지 낮추면서 Recall은 최대로 끌어올렸다. 여기서 Precision은 내려가게 되는데 Recall을 유지하면서 Precision을 올릴 수 있었던 것은 칼럼 추가하면서 0.5 선을 지킬 수 있었다.
팀원들과 모두 각자 성능 좋은 하나의 모델을 가져오기로 했는데 나는 Recall과 Precision 성능을 안정성 있게 가져갈 수 있었기에 XGBoost를 뽑았다.

📌 last, Streamlit 시각화
미리 학습시켜둔 모델을 pkl로 저장해두고, load_model 함수를 만들어 모델을 가져오도록 했다.
@st.cache_resource(show_spinner=False)
def load_model():
model_dir = project_root / "result"
name = "XGBoost"
model_path = model_dir / f"{name}.pkl"
if model_path.exists():
with open(model_path, "rb") as f:
model = pickle.load(f)
return name, model
return None, None
가져온 모델로 이탈률 예측 대시보드를 만들어 칼럼들의 값을 조정해서 이탈 예측 결과를 볼 수 있게 했다.
오른쪽은 나이가 많고 활성 고객 여부가 아니오 인 경우, 이탈 예측 결과가 74%인 것을 확인 할 수 있다.


✨ The four Fs
FACTS (사실, 객관)
이번 프로젝트에서 캐글(Kaggle) 내 다양한 데이터셋을 탐색하며, 경진대회 방식의 데이터 처리 흐름과 활용성을 익힐 수 있었다. Bank Customer Churn Dataset으로 결정하고 Churn(이탈률)에대한 문제에 적합한 분류 모델을 선정하고 비교하는 과정에서 각 모델의 특성과 장단점을 분석해볼 수 있었다.
강사님께서 어느정도 개발 경험이 많아지면 데이터의 형태와 분포만 봐도 어떤 Encoder와 Scaler부터 어떤 모델을 써야할지 감이 온다고 하셨다.
첫 ML 개발 프로젝트이다보니까 감이라는건 있을 수 없었고,,, 어떤 칼럼에 어떤 Encoder, Scaler를 사용해야하는지? 해당 데이터에는 어떤 알고리즘이 적합한지? 논리적으로 접근해서 후보들을 생각하고 기준을 맞춰서 모델들을 선정했다.
EDA부터 Feature Engineering, 모델링, 성능평가까지 End-to-End 전체적인 과정을 개발한 경험은 ML 기본기를 탄탄하게 다질 수 있는 프로젝트였다.
FEELINGS (느낌, 주관)
처음에는 가장 쉬울 것이라 생각했던 EDA가 엄청 중요한 단계이고 시각화의 중요성에대해 알 수 있었다. 시각화를 통해 데이터의 분포, 이상치, 관계 구조를 이해할 수 있었다.
또한, 분류 모델이 워낙 많아 처음에는 복잡하고 혼란스러웠지만, 비교 기반으로 정리하다보니 각 모델이 어떤 상황에서 강점을 가지는 조금씩 체계적으로 구분할 수 있었다.
결과적으로 해당 모델을 통해 이탈 가능성 높은 고객들에게 각각 개인화된 서비스 제공이나 정보 제공을 통해서 실제로 어떻게 활용 될 수 있는지 고민해보면서 LLM 공부에대한 각오를 다질 수 있었다.
FINDINGS (배운 것)
성능평가에서도 이탈 방지의 경우엔 단순히 정확도 점수만 보는 것이 아니라 Recall과 Percision이 더 중요하게 세부적으로 조절해야되는 것을 배웠다. 내은님이랑 논의하는 과정에서 Threshold를 조정하면서 Precision-Recall 중심을 어디에 둘 것인지 조절할 수 있다는 것을 알 수 있었다. 수업시간에 배웠지만 무조건 데이터 전처리와 하이퍼파라미터 튜닝만 생각해서 Threshold는 아예 생각을 못했었다.
모델 성능은 단순 수치상 점수만 생각하면서 만드는게 아니라 운영 환경 기준에 맞게 튜닝해야된다는 것을 한 번 더 배울 수 있었다.
FUTURE (미래)
현재 진행 중인 캐글에서 S&P500 예측 모델을 개발하는 경진대회에서도 좋은 성과를 내고 싶다. 이번 Churn 프로젝트에 비해 훨씬,,,, 복잡한 금융 시계열 데이터를 다루지만 기본 구조부터 다시 생각하면서 도전하려고 한다.
시계열 데이터는 금융 뿐만 아니라 헬스 바이탈 신호 분석이나 사용자 활동량 분석에서도 많이 쓰이므로 중요하게 이번에 잘 공부해놔야겠다.
GitHub - SKNETWORKS-FAMILY-AICAMP/SKN21-2nd-1Team
Contribute to SKNETWORKS-FAMILY-AICAMP/SKN21-2nd-1Team development by creating an account on GitHub.
github.com
'AI > AI TECH' 카테고리의 다른 글
| [플레이데이터 SK네트웍스 Family AI 캠프 21기] 12월 1주차 회고 (0) | 2025.12.08 |
|---|---|
| [sklearn.metrics] 분류형 평가지표 자세히 분석하기 (0) | 2025.11.23 |
| [플레이데이터 SK네트웍스 Family AI 캠프 21기] 11월 1주차 회고 (0) | 2025.11.10 |
| 모델 성능을 높이기 위한 데이터 전처리 ✨ (0) | 2025.11.09 |
| [플레이데이터 SK네트웍스 Family AI 캠프 21기] 10월 4주차 회고 - 1차 단위프로젝트 (0) | 2025.10.26 |
