본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 7. · 42 Views
실전 프로젝트: 고객 이탈 예측 모델 구축
실제 기업에서 활용하는 고객 이탈 예측 모델을 처음부터 끝까지 구축해봅니다. 데이터 전처리부터 모델 학습, 평가, 그리고 실무 적용까지 머신러닝 프로젝트의 전체 흐름을 익힐 수 있습니다.
목차
- 비즈니스_문제_정의와_데이터_탐색
- 데이터_전처리와_피처_엔지니어링
- 학습_데이터와_테스트_데이터_분리
- 로지스틱_회귀_모델_학습
- 랜덤_포레스트로_성능_향상
- 모델_평가_지표_깊이_이해하기
- 임계값_조정으로_비즈니스_최적화
- 교차_검증으로_신뢰성_확보
- 하이퍼파라미터_튜닝
- 모델_저장과_실무_적용
1. 비즈니스 문제 정의와 데이터 탐색
김개발 씨는 이커머스 회사에 입사한 지 6개월 된 데이터 분석가입니다. 어느 날 마케팅 팀장이 급하게 찾아왔습니다.
"개발 씨, 최근 고객 이탈률이 심상치 않아요. 어떤 고객이 떠날지 미리 알 수 있을까요?" 김개발 씨의 첫 번째 머신러닝 프로젝트가 시작되었습니다.
고객 이탈 예측은 기존 고객이 서비스를 떠날 가능성을 사전에 파악하는 것입니다. 마치 의사가 환자의 증상을 보고 질병을 예측하는 것처럼, 데이터 과학자는 고객의 행동 패턴을 보고 이탈 여부를 예측합니다.
이를 통해 기업은 소중한 고객을 미리 케어하여 이탈을 방지할 수 있습니다.
다음 코드를 살펴봅시다.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
# 고객 데이터 로드
df = pd.read_csv('customer_churn.csv')
# 데이터 기본 정보 확인
print(f"총 고객 수: {len(df):,}명")
print(f"컬럼 수: {len(df.columns)}개")
print(f"이탈 고객 비율: {df['Churn'].mean()*100:.1f}%")
# 결측치 현황 파악
missing = df.isnull().sum()
print(f"\n결측치 현황:\n{missing[missing > 0]}")
김개발 씨는 마케팅 팀장의 요청을 받고 책상 앞에 앉았습니다. 머릿속이 복잡해졌습니다.
머신러닝이라고 하면 복잡한 수식과 알고리즘이 먼저 떠오르는데, 대체 어디서부터 시작해야 할까요? 선배 박시니어 씨가 커피 한 잔을 건네며 다가왔습니다.
"개발 씨, 머신러닝 프로젝트에서 가장 중요한 건 뭘까요? 최신 알고리즘?
복잡한 모델?" 김개발 씨가 고개를 갸우뚱하자, 박시니어 씨가 웃으며 말했습니다. "바로 문제 정의예요." 고객 이탈 예측이라는 문제를 생각해봅시다.
마치 병원에서 건강검진을 받는 것과 비슷합니다. 의사는 혈압, 혈당, 콜레스테롤 수치 등 다양한 지표를 보고 환자의 건강 상태를 판단합니다.
우리도 마찬가지입니다. 고객의 구매 빈도, 마지막 접속일, 불만 접수 횟수 같은 지표를 보고 이탈 여부를 예측하는 것입니다.
그렇다면 가장 먼저 해야 할 일은 무엇일까요? 바로 데이터 탐색입니다.
데이터를 처음 받았을 때 무작정 모델을 돌리는 것은 마치 지도 없이 낯선 도시를 여행하는 것과 같습니다. 먼저 어떤 데이터가 있는지, 결측치는 얼마나 있는지, 이탈 고객의 비율은 어떻게 되는지 전체적인 그림을 파악해야 합니다.
위 코드를 살펴보면, 먼저 pandas 라이브러리로 CSV 파일을 읽어옵니다. 그 다음 총 고객 수와 컬럼 수를 확인합니다.
여기서 중요한 것은 이탈 고객 비율입니다. 만약 이탈 고객이 전체의 5%에 불과하다면, 이는 불균형 데이터 문제를 야기할 수 있습니다.
결측치 현황도 반드시 확인해야 합니다. 실제 기업 데이터에는 생각보다 많은 결측치가 존재합니다.
고객이 정보를 입력하지 않았거나, 시스템 오류로 데이터가 누락되기도 합니다. 이런 결측치를 어떻게 처리하느냐에 따라 모델의 성능이 크게 달라질 수 있습니다.
김개발 씨는 데이터를 살펴보며 몇 가지 중요한 사실을 발견했습니다. 이탈 고객 비율이 약 20%였고, 일부 컬럼에 결측치가 존재했습니다.
이제 본격적인 데이터 전처리를 시작할 준비가 되었습니다.
실전 팁
💡 - 프로젝트 시작 전 비즈니스 담당자와 충분히 소통하여 문제를 명확히 정의하세요
- 데이터 탐색 단계에서 발견한 내용을 문서로 정리해두면 나중에 큰 도움이 됩니다
2. 데이터 전처리와 피처 엔지니어링
데이터 탐색을 마친 김개발 씨 앞에는 새로운 과제가 놓였습니다. 원본 데이터에는 결측치도 있고, 문자열 데이터도 섞여 있었습니다.
박시니어 씨가 말했습니다. "날것의 데이터를 그대로 모델에 넣으면 안 돼요.
요리로 치면 재료 손질 단계가 필요하죠."
데이터 전처리는 원본 데이터를 모델이 학습할 수 있는 형태로 변환하는 과정입니다. 마치 요리사가 재료를 씻고, 다듬고, 적당한 크기로 썰어야 맛있는 요리를 만들 수 있듯이, 데이터 과학자도 데이터를 정제하고 가공해야 좋은 모델을 만들 수 있습니다.
피처 엔지니어링은 여기서 한 발 더 나아가 새로운 특성을 만들어내는 창의적인 작업입니다.
다음 코드를 살펴봅시다.
from sklearn.preprocessing import LabelEncoder, StandardScaler
# 결측치 처리
df['TotalCharges'] = df['TotalCharges'].fillna(df['TotalCharges'].median())
# 범주형 변수 인코딩
le = LabelEncoder()
categorical_cols = ['Gender', 'Contract', 'PaymentMethod']
for col in categorical_cols:
df[col + '_encoded'] = le.fit_transform(df[col])
# 피처 엔지니어링: 월평균 요금 계산
df['AvgMonthlyCharge'] = df['TotalCharges'] / (df['Tenure'] + 1)
# 수치형 변수 정규화
scaler = StandardScaler()
numeric_cols = ['MonthlyCharges', 'TotalCharges', 'Tenure']
df[numeric_cols] = scaler.fit_transform(df[numeric_cols])
김개발 씨는 원본 데이터를 바라보며 한숨을 쉬었습니다. TotalCharges 컬럼에는 빈 값이 있고, Gender나 Contract 같은 컬럼에는 문자열이 들어있었습니다.
머신러닝 모델은 기본적으로 숫자만 이해할 수 있는데, 이 데이터를 어떻게 넣어야 할까요? 박시니어 씨가 화이트보드에 그림을 그리며 설명했습니다.
"전처리는 크게 세 단계로 나눌 수 있어요. 결측치 처리, 인코딩, 그리고 정규화입니다." 먼저 결측치 처리를 살펴봅시다.
결측치를 처리하는 방법은 여러 가지가 있습니다. 해당 행을 삭제하거나, 평균값이나 중앙값으로 채우거나, 더 복잡한 방법으로 예측하여 채울 수도 있습니다.
위 코드에서는 중앙값으로 채우는 방법을 사용했습니다. 중앙값은 평균값과 달리 극단적인 이상치에 영향을 덜 받기 때문에 더 안정적입니다.
다음은 인코딩입니다. 컴퓨터는 "남성", "여성" 같은 문자를 이해하지 못합니다.
이를 0과 1 같은 숫자로 변환해주어야 합니다. LabelEncoder는 각 범주를 고유한 숫자로 변환해줍니다.
다만 주의할 점이 있습니다. LabelEncoder는 순서가 있는 것처럼 숫자를 부여하기 때문에, 순서가 없는 범주형 변수에는 원핫 인코딩이 더 적합할 수 있습니다.
피처 엔지니어링은 데이터 과학의 예술이라고 불립니다. 기존 변수들을 조합하여 새로운 의미 있는 변수를 만들어내는 것입니다.
위 코드에서는 총 결제액을 이용 기간으로 나누어 월평균 요금을 계산했습니다. 이렇게 만든 새로운 피처가 이탈 예측에 중요한 역할을 할 수 있습니다.
마지막으로 정규화입니다. MonthlyCharges는 50에서 100 사이의 값을 가지고, Tenure는 0에서 72 사이의 값을 가질 수 있습니다.
이처럼 스케일이 다른 변수들을 그대로 사용하면, 스케일이 큰 변수가 모델에 과도한 영향을 미칠 수 있습니다. StandardScaler는 모든 변수를 평균 0, 표준편차 1로 변환하여 이 문제를 해결합니다.
김개발 씨는 전처리 코드를 실행하고 결과를 확인했습니다. 이제 모든 데이터가 숫자로 변환되었고, 스케일도 맞춰졌습니다.
드디어 모델을 학습할 준비가 된 것입니다.
실전 팁
💡 - 결측치 처리 방법은 데이터의 특성과 결측 비율에 따라 신중하게 선택하세요
- 피처 엔지니어링은 도메인 지식이 중요합니다. 비즈니스 담당자와 협업하면 좋은 아이디어를 얻을 수 있습니다
3. 학습 데이터와 테스트 데이터 분리
전처리를 마친 김개발 씨가 바로 모델 학습에 들어가려 하자, 박시니어 씨가 손을 들어 막았습니다. "잠깐, 데이터 분리부터 해야 해요.
시험 문제를 미리 보고 공부하면 실력을 제대로 평가할 수 없잖아요?" 김개발 씨는 고개를 끄덕이며 중요한 개념을 배우게 되었습니다.
학습/테스트 데이터 분리는 모델의 실제 성능을 공정하게 평가하기 위한 필수 과정입니다. 마치 학생이 공부할 때 사용하는 교재와 실제 시험 문제가 달라야 진정한 실력을 알 수 있듯이, 모델도 학습에 사용하지 않은 데이터로 평가해야 합니다.
이를 통해 모델이 새로운 데이터에도 잘 작동하는지, 즉 일반화 성능을 확인할 수 있습니다.
다음 코드를 살펴봅시다.
from sklearn.model_selection import train_test_split
# 피처와 타겟 분리
feature_cols = ['Tenure', 'MonthlyCharges', 'TotalCharges',
'Gender_encoded', 'Contract_encoded', 'AvgMonthlyCharge']
X = df[feature_cols]
y = df['Churn']
# 학습/테스트 데이터 분리 (80:20)
X_train, X_test, y_train, y_test = train_test_split(
X, y,
test_size=0.2,
random_state=42,
stratify=y # 이탈 비율 유지
)
print(f"학습 데이터: {len(X_train):,}개")
print(f"테스트 데이터: {len(X_test):,}개")
박시니어 씨가 비유를 들어 설명했습니다. "수능 시험을 준비한다고 생각해봐요.
만약 기출문제만 달달 외워서 같은 문제가 시험에 나온다면, 그게 진짜 실력일까요?" 김개발 씨는 바로 이해했습니다. 모델이 학습 데이터를 그대로 외워버리면, 새로운 고객 데이터가 들어왔을 때 제대로 예측하지 못할 수 있습니다.
이것을 과적합이라고 부릅니다. 이 문제를 해결하기 위해 데이터를 두 부분으로 나눕니다.
하나는 모델을 학습시키는 학습 데이터, 다른 하나는 모델의 성능을 평가하는 테스트 데이터입니다. 보통 80:20 또는 70:30의 비율로 나눕니다.
위 코드에서 주목할 부분이 있습니다. 바로 stratify=y 옵션입니다.
이 옵션이 왜 중요할까요? 우리의 데이터에서 이탈 고객은 약 20%입니다.
만약 무작위로 데이터를 나누면, 우연히 학습 데이터에는 이탈 고객이 15%만 있고 테스트 데이터에는 30%가 있을 수 있습니다. 이렇게 되면 공정한 평가가 어렵습니다.
stratify 옵션을 사용하면 학습 데이터와 테스트 데이터 모두에서 이탈 고객 비율이 20%로 유지됩니다. random_state=42는 재현성을 위한 것입니다.
같은 코드를 여러 번 실행해도 항상 같은 방식으로 데이터가 분리됩니다. 이렇게 하면 실험 결과를 다른 사람과 공유하거나, 나중에 다시 확인할 때 동일한 결과를 얻을 수 있습니다.
실무에서는 종종 검증 데이터를 추가로 분리하기도 합니다. 학습, 검증, 테스트로 3분할하여 검증 데이터로 하이퍼파라미터를 튜닝하고, 최종 성능은 테스트 데이터로 평가합니다.
하지만 데이터가 충분하지 않을 때는 교차 검증이라는 기법을 사용하기도 합니다. 김개발 씨는 데이터 분리를 마치고 각 데이터셋의 크기를 확인했습니다.
이제 진짜 모델을 학습할 시간입니다.
실전 팁
💡 - random_state는 팀 내에서 통일된 값을 사용하면 결과 재현이 쉬워집니다
- 데이터가 적을 때는 K-Fold 교차 검증을 고려해보세요
4. 로지스틱 회귀 모델 학습
드디어 모델 학습 단계에 도달한 김개발 씨는 흥분을 감추지 못했습니다. 어떤 알고리즘을 써야 할지 고민하던 중, 박시니어 씨가 조언했습니다.
"복잡한 것부터 시작하지 마세요. 가장 단순한 모델로 시작해서 기준점을 만드는 게 좋아요."
로지스틱 회귀는 이진 분류 문제에서 가장 먼저 시도해볼 수 있는 기본 알고리즘입니다. 이름에 회귀가 들어가지만 실제로는 분류 모델입니다.
마치 저울이 무게를 재듯이, 각 특성의 중요도에 가중치를 두어 결과를 예측합니다. 단순하지만 해석이 쉽고, 과적합 위험이 적어 첫 번째 모델로 적합합니다.
다음 코드를 살펴봅시다.
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, classification_report
# 로지스틱 회귀 모델 생성 및 학습
model = LogisticRegression(random_state=42, max_iter=1000)
model.fit(X_train, y_train)
# 예측 수행
y_pred = model.predict(X_test)
# 기본 정확도 확인
accuracy = accuracy_score(y_test, y_pred)
print(f"정확도: {accuracy:.4f}")
# 상세 분류 리포트
print("\n분류 리포트:")
print(classification_report(y_test, y_pred))
박시니어 씨가 화이트보드에 그래프를 그렸습니다. "로지스틱 회귀의 핵심은 시그모이드 함수예요.
이 함수는 어떤 값이든 0과 1 사이로 변환해줍니다." 쉽게 설명하면 이렇습니다. 여러 요인을 종합해서 점수를 매기고, 그 점수를 0부터 1 사이의 확률로 변환하는 것입니다.
예를 들어 이용 기간이 짧고, 월 요금이 높고, 불만 접수가 많은 고객은 높은 이탈 확률을 갖게 됩니다. 코드를 살펴보면, 모델 생성부터 예측까지 불과 몇 줄이면 됩니다.
fit 메서드로 학습하고, predict 메서드로 예측합니다. scikit-learn 라이브러리는 이렇게 일관된 인터페이스를 제공하기 때문에, 나중에 다른 알고리즘으로 바꿀 때도 코드 수정이 거의 필요 없습니다.
정확도가 80%로 나왔다고 가정해봅시다. 이게 좋은 결과일까요?
여기서 함정이 있습니다. 만약 이탈 고객이 20%라면, 모든 고객을 "이탈하지 않음"으로 예측해도 80% 정확도가 나옵니다.
따라서 정확도만으로는 부족합니다. classification_report가 출력하는 지표들을 살펴봅시다.
Precision은 이탈이라고 예측한 고객 중 실제 이탈 고객의 비율입니다. Recall은 실제 이탈 고객 중 우리가 찾아낸 비율입니다.
고객 이탈 예측에서는 Recall이 특히 중요합니다. 이탈할 고객을 놓치면 그 고객은 영원히 떠나버리기 때문입니다.
김개발 씨는 분류 리포트를 자세히 살펴보았습니다. 정확도는 나쁘지 않았지만, 이탈 고객에 대한 Recall이 조금 낮았습니다.
더 좋은 모델을 찾아야 할 것 같았습니다.
실전 팁
💡 - 첫 모델의 결과는 기준선으로 삼고, 이후 모델과 비교하는 용도로 활용하세요
- max_iter 값을 충분히 크게 설정해야 수렴 경고를 피할 수 있습니다
5. 랜덤 포레스트로 성능 향상
로지스틱 회귀의 한계를 느낀 김개발 씨에게 박시니어 씨가 새로운 알고리즘을 소개했습니다. "나무 한 그루보다 숲이 더 현명한 결정을 내릴 수 있어요.
랜덤 포레스트를 사용해보는 건 어때요?" 김개발 씨의 눈이 반짝였습니다.
랜덤 포레스트는 여러 개의 결정 트리를 만들어 다수결로 최종 결과를 결정하는 앙상블 알고리즘입니다. 마치 중요한 결정을 내릴 때 여러 전문가의 의견을 종합하는 것과 같습니다.
개별 트리는 실수할 수 있지만, 여러 트리의 의견을 모으면 더 정확하고 안정적인 예측이 가능합니다. 또한 어떤 특성이 중요한지 알려주는 피처 중요도도 제공합니다.
다음 코드를 살펴봅시다.
from sklearn.ensemble import RandomForestClassifier
# 랜덤 포레스트 모델 생성 및 학습
rf_model = RandomForestClassifier(
n_estimators=100, # 트리 100개 생성
max_depth=10, # 트리 최대 깊이
min_samples_split=5, # 분할 최소 샘플 수
random_state=42
)
rf_model.fit(X_train, y_train)
# 예측 및 성능 평가
y_pred_rf = rf_model.predict(X_test)
print(f"랜덤 포레스트 정확도: {accuracy_score(y_test, y_pred_rf):.4f}")
# 피처 중요도 확인
importance = pd.DataFrame({
'feature': feature_cols,
'importance': rf_model.feature_importances_
}).sort_values('importance', ascending=False)
print("\n피처 중요도:\n", importance)
박시니어 씨가 재미있는 비유를 들었습니다. "TV 퀴즈 프로그램에서 청중에게 물어보기를 사용하면 정답률이 높아지는 것 알죠?
랜덤 포레스트도 비슷한 원리예요." 결정 트리는 질문을 던지며 데이터를 분류합니다. "이용 기간이 6개월 미만인가요?", "월 요금이 70달러 이상인가요?" 이런 식으로 계속 질문하며 최종 결론에 도달합니다.
하지만 단일 결정 트리는 학습 데이터에 과적합되기 쉽습니다. 랜덤 포레스트는 이 문제를 해결합니다.
100개의 트리를 만들되, 각 트리는 조금씩 다른 데이터와 특성을 사용합니다. 마치 같은 문제를 100명의 전문가에게 물어보는 것과 같습니다.
그리고 최종 결과는 다수결로 결정합니다. 코드에서 n_estimators=100은 100개의 트리를 만든다는 의미입니다.
트리 수가 많을수록 성능이 좋아지지만, 학습 시간도 길어집니다. max_depth=10은 트리의 최대 깊이를 제한하여 과적합을 방지합니다.
랜덤 포레스트의 큰 장점 중 하나는 피처 중요도를 제공한다는 것입니다. 이탈 예측에 어떤 변수가 가장 큰 영향을 미치는지 알 수 있습니다.
예를 들어 이용 기간과 계약 유형이 가장 중요하다는 것을 발견했다면, 마케팅 팀은 신규 고객의 초기 경험과 장기 계약 유도에 집중할 수 있습니다. 김개발 씨는 랜덤 포레스트의 결과를 보고 감탄했습니다.
정확도가 로지스틱 회귀보다 높아졌을 뿐만 아니라, 이탈 고객에 대한 Recall도 개선되었습니다. 게다가 피처 중요도 덕분에 비즈니스 인사이트까지 얻을 수 있었습니다.
하지만 박시니어 씨가 조언했습니다. "아직 끝이 아니에요.
모델 성능을 제대로 평가하려면 더 많은 지표를 살펴봐야 해요."
실전 팁
💡 - n_estimators는 보통 100에서 500 사이로 설정하며, 더 많다고 항상 좋은 것은 아닙니다
- 피처 중요도 결과를 비즈니스 팀과 공유하면 액션 아이템 도출에 도움이 됩니다
6. 모델 평가 지표 깊이 이해하기
김개발 씨가 결과를 마케팅 팀장에게 보고하려 하자, 박시니어 씨가 잠시 멈추게 했습니다. "정확도가 85%라고만 말하면 안 돼요.
비즈니스 관점에서 중요한 질문은 따로 있거든요. 우리가 얼마나 많은 이탈 고객을 잡아낼 수 있느냐가 핵심이에요."
분류 모델의 성능을 평가하는 지표는 다양합니다. 정확도만으로는 불균형 데이터에서 모델의 진짜 성능을 알 수 없습니다.
Precision은 예측의 정밀함을, Recall은 실제 양성 케이스를 얼마나 찾아내는지를, F1 Score는 둘의 조화 평균을 나타냅니다. 특히 ROC-AUC는 모델의 전반적인 분류 능력을 하나의 숫자로 보여주는 강력한 지표입니다.
다음 코드를 살펴봅시다.
from sklearn.metrics import confusion_matrix, roc_auc_score, roc_curve
import matplotlib.pyplot as plt
# 혼동 행렬
cm = confusion_matrix(y_test, y_pred_rf)
print("혼동 행렬:")
print(f" TN: {cm[0,0]} FP: {cm[0,1]}")
print(f" FN: {cm[1,0]} TP: {cm[1,1]}")
# 이탈 확률 예측
y_proba = rf_model.predict_proba(X_test)[:, 1]
# ROC-AUC 점수
auc_score = roc_auc_score(y_test, y_proba)
print(f"\nROC-AUC 점수: {auc_score:.4f}")
# ROC 커브 시각화
fpr, tpr, thresholds = roc_curve(y_test, y_proba)
plt.plot(fpr, tpr, label=f'AUC = {auc_score:.3f}')
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.legend()
박시니어 씨가 화이트보드에 2x2 표를 그렸습니다. "이걸 혼동 행렬이라고 해요.
모델의 예측과 실제 결과를 비교하는 표죠." 혼동 행렬에는 네 가지 경우가 있습니다. **TN(True Negative)**은 이탈하지 않는다고 예측했고 실제로 이탈하지 않은 경우입니다.
**TP(True Positive)**는 이탈한다고 예측했고 실제로 이탈한 경우입니다. 여기까지는 모델이 맞춘 경우입니다.
문제는 틀린 경우입니다. **FP(False Positive)**는 이탈한다고 예측했지만 실제로는 이탈하지 않은 경우입니다.
이 고객에게 불필요한 할인 쿠폰을 보내는 비용이 발생합니다. **FN(False Negative)**은 이탈하지 않는다고 예측했지만 실제로 이탈한 경우입니다.
이 고객은 아무런 케어도 받지 못하고 떠나버립니다. 비즈니스 관점에서 어떤 오류가 더 치명적일까요?
대부분의 경우 FN이 더 위험합니다. 신규 고객을 확보하는 비용이 기존 고객을 유지하는 비용의 5배에서 25배에 달하기 때문입니다.
ROC-AUC는 모델의 전반적인 분류 능력을 0에서 1 사이의 값으로 나타냅니다. 0.5면 동전 던지기 수준이고, 1에 가까울수록 완벽한 분류입니다.
보통 0.7 이상이면 괜찮고, 0.8 이상이면 좋은 모델로 평가합니다. predict_proba 메서드는 단순히 이탈/비이탈을 예측하는 것이 아니라, 이탈 확률을 0에서 1 사이의 값으로 제공합니다.
이 확률을 활용하면 고객을 이탈 위험도에 따라 등급을 나눌 수 있습니다. 예를 들어 80% 이상은 긴급 케어, 50~80%는 주의 관찰, 50% 미만은 일반 관리로 분류할 수 있습니다.
김개발 씨는 이제 단순한 정확도를 넘어 모델의 진짜 성능을 이해할 수 있게 되었습니다.
실전 팁
💡 - 비즈니스 목적에 따라 Precision과 Recall 중 어느 것이 더 중요한지 결정하세요
- 예측 확률을 활용하면 고객 세그먼트별 차별화된 마케팅 전략을 수립할 수 있습니다
7. 임계값 조정으로 비즈니스 최적화
마케팅 팀장이 김개발 씨의 보고를 듣고 질문했습니다. "이탈 확률이 몇 퍼센트 이상이면 케어 대상으로 봐야 하나요?" 김개발 씨는 당연히 50%라고 대답하려다가, 박시니어 씨의 눈빛을 보고 멈췄습니다.
거기에도 비즈니스 의사결정이 필요했습니다.
분류 모델은 기본적으로 50%를 임계값으로 사용합니다. 확률이 50%를 넘으면 이탈로, 그렇지 않으면 잔류로 예측하는 것입니다.
하지만 이 임계값을 조정하면 Precision과 Recall의 균형을 비즈니스 목적에 맞게 조절할 수 있습니다. 마치 그물망의 크기를 조절하는 것과 같습니다.
그물이 촘촘하면 작은 물고기도 잡지만, 원하지 않는 것도 많이 잡히게 됩니다.
다음 코드를 살펴봅시다.
import numpy as np
# 다양한 임계값에 따른 성능 비교
thresholds = [0.3, 0.4, 0.5, 0.6, 0.7]
print("임계값별 성능 비교:")
print("-" * 50)
for threshold in thresholds:
y_pred_custom = (y_proba >= threshold).astype(int)
tn, fp, fn, tp = confusion_matrix(y_test, y_pred_custom).ravel()
precision = tp / (tp + fp) if (tp + fp) > 0 else 0
recall = tp / (tp + fn) if (tp + fn) > 0 else 0
print(f"임계값 {threshold}: Precision={precision:.3f}, "
f"Recall={recall:.3f}, 케어 대상={tp+fp}명")
# 비즈니스 최적 임계값 선택 (비용 기반)
# 예: 이탈 고객 1명 놓치는 비용 = 케어 비용의 5배
cost_ratio = 5
best_threshold = thresholds[0] # 실제로는 비용 함수로 계산
박시니어 씨가 흥미로운 질문을 던졌습니다. "김개발 씨, 암 검진에서 임계값을 어떻게 설정할 것 같아요?" 김개발 씨가 잠시 생각했습니다.
"음... 암은 놓치면 안 되니까, 임계값을 낮춰서 조금이라도 의심되면 추가 검사를 받게 하는 게 좋을 것 같아요." 박시니어 씨가 고개를 끄덕였습니다.
"바로 그거예요. 고객 이탈도 마찬가지입니다.
이탈 고객을 놓치는 비용이 크다면, 임계값을 낮추는 게 합리적이에요." 기본 임계값 0.5를 0.3으로 낮추면 어떻게 될까요? 이탈 확률이 30%만 넘어도 케어 대상이 됩니다.
이렇게 하면 Recall이 높아집니다. 더 많은 이탈 고객을 찾아낼 수 있습니다.
하지만 대가가 있습니다. 실제로는 이탈하지 않을 고객에게도 케어 비용이 발생합니다.
즉, Precision이 낮아집니다. 반대로 임계값을 0.7로 높이면 어떻게 될까요?
정말 확실한 이탈 고객만 케어 대상이 됩니다. Precision은 높아지지만, 많은 이탈 고객을 놓치게 됩니다.
위 코드는 다양한 임계값에서 Precision, Recall, 그리고 케어 대상 수를 보여줍니다. 마케팅 팀장은 이 표를 보고 의사결정을 내릴 수 있습니다.
케어 예산이 한정되어 있다면 임계값을 높여 정밀하게 타겟팅하고, 예산이 넉넉하다면 임계값을 낮춰 더 많은 고객을 케어할 수 있습니다. 이것이 바로 데이터 과학과 비즈니스가 만나는 지점입니다.
모델은 확률을 제공하고, 그 확률을 어떻게 활용할지는 비즈니스 의사결정의 영역입니다.
실전 팁
💡 - 임계값 결정 시 FN과 FP의 비용을 정량화하면 더 객관적인 의사결정이 가능합니다
- 시간에 따라 최적 임계값이 변할 수 있으므로 주기적으로 재검토하세요
8. 교차 검증으로 신뢰성 확보
김개발 씨의 모델이 좋은 성능을 보였지만, 박시니어 씨가 한 가지 우려를 표했습니다. "이 결과가 우연히 좋게 나온 건 아닐까요?
데이터를 다르게 나눴으면 결과가 달라졌을 수도 있어요." 김개발 씨는 그 가능성을 생각해보지 못했습니다.
교차 검증은 데이터를 여러 번 다르게 나누어 모델을 평가함으로써 결과의 신뢰성을 높이는 기법입니다. 마치 시험을 한 번만 보는 것보다 여러 번 보고 평균을 내는 것이 실력을 더 정확히 반영하는 것과 같습니다.
가장 흔히 사용되는 K-Fold 교차 검증은 데이터를 K개의 부분으로 나누어 K번 학습과 평가를 반복합니다.
다음 코드를 살펴봅시다.
from sklearn.model_selection import cross_val_score, StratifiedKFold
# 5-Fold 교차 검증 설정
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
# 교차 검증 수행
cv_scores = cross_val_score(
rf_model, X, y,
cv=cv,
scoring='roc_auc'
)
print("5-Fold 교차 검증 결과:")
print(f"각 Fold AUC: {cv_scores}")
print(f"평균 AUC: {cv_scores.mean():.4f}")
print(f"표준편차: {cv_scores.std():.4f}")
# 신뢰 구간 계산 (95%)
confidence_interval = 1.96 * cv_scores.std()
print(f"\n95% 신뢰 구간: {cv_scores.mean():.4f} ± {confidence_interval:.4f}")
박시니어 씨가 주사위를 꺼내며 설명했습니다. "주사위를 한 번 던져서 6이 나왔다고 이 주사위가 항상 6만 나오는 주사위라고 할 수 있을까요?" 당연히 아닙니다.
여러 번 던져봐야 진짜 성능을 알 수 있습니다. 모델 평가도 마찬가지입니다.
K-Fold 교차 검증의 원리는 간단합니다. 전체 데이터를 K개의 동일한 크기로 나눕니다.
예를 들어 5-Fold라면 5개로 나눕니다. 첫 번째 실험에서는 1번 조각을 테스트용으로, 나머지 2~5번을 학습용으로 사용합니다.
두 번째 실험에서는 2번 조각을 테스트용으로, 나머지를 학습용으로 사용합니다. 이렇게 5번 반복합니다.
이렇게 하면 모든 데이터가 한 번씩은 테스트 데이터로 사용됩니다. 그리고 5개의 성능 점수를 얻을 수 있습니다.
이 점수들의 평균이 모델의 기대 성능이고, 표준편차가 성능의 안정성을 나타냅니다. 위 코드의 결과를 해석해봅시다.
만약 각 Fold의 AUC가 0.82, 0.85, 0.83, 0.84, 0.81이라면, 평균 AUC는 약 0.83이고 표준편차는 0.015 정도입니다. 표준편차가 작으면 모델이 안정적이라는 의미입니다.
StratifiedKFold는 각 Fold에서 이탈 고객 비율이 동일하게 유지되도록 합니다. 일반 KFold를 사용하면 어떤 Fold는 이탈 고객이 많고 어떤 Fold는 적을 수 있어 공정한 비교가 어렵습니다.
95% 신뢰 구간을 계산하면 "이 모델의 실제 성능은 95%의 확률로 이 범위 안에 있다"고 말할 수 있습니다. 이것은 경영진에게 결과를 보고할 때 매우 유용합니다.
실전 팁
💡 - 데이터가 적을 때는 K를 크게 (10 정도), 데이터가 많을 때는 K를 작게 (3~5) 설정하세요
- 표준편차가 너무 크면 모델이 불안정하다는 신호이므로 원인을 분석해보세요
9. 하이퍼파라미터 튜닝
김개발 씨는 랜덤 포레스트에서 n_estimators와 max_depth 값을 어떻게 정했는지 질문받았습니다. 사실 그냥 적당히 넣은 값이었습니다.
박시니어 씨가 웃으며 말했습니다. "그 값들을 체계적으로 찾는 방법이 있어요.
하이퍼파라미터 튜닝이라고 합니다."
하이퍼파라미터는 모델이 학습하기 전에 사람이 미리 설정해야 하는 값입니다. 마치 오븐의 온도와 시간을 요리 전에 설정하는 것과 같습니다.
너무 낮은 온도에서는 덜 익고, 너무 높은 온도에서는 타버립니다. Grid Search는 여러 조합을 체계적으로 시도하여 최적의 하이퍼파라미터를 찾아줍니다.
다음 코드를 살펴봅시다.
from sklearn.model_selection import GridSearchCV
# 탐색할 하이퍼파라미터 그리드 정의
param_grid = {
'n_estimators': [50, 100, 200],
'max_depth': [5, 10, 15, None],
'min_samples_split': [2, 5, 10],
'min_samples_leaf': [1, 2, 4]
}
# Grid Search 수행
grid_search = GridSearchCV(
RandomForestClassifier(random_state=42),
param_grid,
cv=5,
scoring='roc_auc',
n_jobs=-1, # 모든 CPU 코어 사용
verbose=1
)
grid_search.fit(X_train, y_train)
# 최적 파라미터와 성능 출력
print(f"\n최적 파라미터: {grid_search.best_params_}")
print(f"최고 AUC 점수: {grid_search.best_score_:.4f}")
# 최적 모델로 테스트 데이터 예측
best_model = grid_search.best_estimator_
final_score = roc_auc_score(y_test, best_model.predict_proba(X_test)[:, 1])
print(f"테스트 AUC: {final_score:.4f}")
박시니어 씨가 레시피 책을 펼쳐 보였습니다. "케이크를 구울 때 온도, 시간, 재료 비율이 중요하죠?
머신러닝 모델도 비슷해요." n_estimators는 몇 개의 트리를 만들지, max_depth는 각 트리가 얼마나 깊이 질문할지, min_samples_split은 더 나누기 위해 최소 몇 개의 샘플이 필요한지를 결정합니다. 이 값들에 따라 모델의 성능이 크게 달라질 수 있습니다.
Grid Search는 가능한 모든 조합을 시도합니다. 위 코드에서는 3 x 4 x 3 x 3 = 108개의 조합을 탐색합니다.
각 조합에 대해 5-Fold 교차 검증을 수행하므로, 총 540번의 모델 학습이 이루어집니다. n_jobs=-1은 컴퓨터의 모든 CPU 코어를 사용하여 병렬 처리를 하라는 의미입니다.
이렇게 하면 탐색 시간을 크게 줄일 수 있습니다. Grid Search의 단점은 조합의 수가 기하급수적으로 늘어날 수 있다는 것입니다.
탐색 공간이 너무 크면 Random Search를 고려해볼 수 있습니다. Random Search는 모든 조합 대신 무작위로 일부만 탐색하지만, 의외로 좋은 결과를 낼 때가 많습니다.
최적 파라미터를 찾은 후에는 반드시 테스트 데이터로 최종 성능을 확인해야 합니다. Grid Search 과정에서 사용한 점수는 교차 검증 점수이지, 진짜 새로운 데이터에 대한 성능이 아닙니다.
김개발 씨는 Grid Search를 돌리고 최적 파라미터를 찾았습니다. 테스트 AUC가 기존보다 약간 향상되었습니다.
작은 개선이지만, 수천 명의 고객에게 적용되면 큰 차이를 만들 수 있습니다.
실전 팁
💡 - 처음에는 넓은 범위로 탐색하고, 점점 좁혀나가는 전략이 효과적입니다
- 탐색 시간이 너무 길다면 RandomizedSearchCV를 시도해보세요
10. 모델 저장과 실무 적용
마침내 최적의 모델을 완성한 김개발 씨에게 마지막 관문이 남아있었습니다. "이 모델을 매일 실행해서 이탈 위험 고객 목록을 마케팅 팀에 전달해야 해요." 박시니어 씨의 말에 김개발 씨는 프로덕션 환경에 대해 생각하기 시작했습니다.
학습된 모델을 저장하면 매번 다시 학습하지 않고 바로 예측에 활용할 수 있습니다. 마치 완성된 레시피를 저장해두고 필요할 때마다 꺼내 쓰는 것과 같습니다.
Python에서는 joblib이나 pickle을 사용하여 모델을 파일로 저장합니다. 실무에서는 저장된 모델을 불러와 새로운 고객 데이터에 적용하는 파이프라인을 구축합니다.
다음 코드를 살펴봅시다.
import joblib
from datetime import datetime
# 모델 저장
model_filename = f'churn_model_{datetime.now().strftime("%Y%m%d")}.pkl'
joblib.dump(best_model, model_filename)
print(f"모델 저장 완료: {model_filename}")
# 모델 불러오기 및 새 고객 예측
loaded_model = joblib.load(model_filename)
# 새로운 고객 데이터로 예측 (예시)
new_customers = pd.DataFrame({
'Tenure': [3, 24, 12],
'MonthlyCharges': [85.0, 45.0, 70.0],
'TotalCharges': [255.0, 1080.0, 840.0],
'Gender_encoded': [1, 0, 1],
'Contract_encoded': [0, 2, 1],
'AvgMonthlyCharge': [85.0, 45.0, 70.0]
})
# 이탈 확률 예측
churn_probabilities = loaded_model.predict_proba(new_customers)[:, 1]
print("\n신규 예측 결과:")
for i, prob in enumerate(churn_probabilities):
risk_level = "고위험" if prob > 0.6 else "주의" if prob > 0.3 else "안정"
print(f" 고객 {i+1}: 이탈 확률 {prob:.1%} ({risk_level})")
박시니어 씨가 서버실을 가리키며 말했습니다. "개발 씨 노트북에서만 돌아가면 안 돼요.
서버에서 매일 자동으로 실행되어야 합니다." 모델 저장의 중요성은 여기에 있습니다. 랜덤 포레스트 모델을 학습하는 데 수십 분이 걸릴 수 있습니다.
새 고객이 가입할 때마다 모델을 다시 학습하는 것은 비효율적입니다. 한 번 학습한 모델을 저장해두고, 예측할 때만 불러와서 사용하면 됩니다.
joblib은 scikit-learn 모델을 저장하는 데 최적화되어 있습니다. pickle보다 대용량 데이터를 효율적으로 처리합니다.
저장할 때 날짜를 파일명에 포함시키면 버전 관리가 쉬워집니다. 위 코드에서 새 고객 데이터를 예측하는 부분을 주목하세요.
predict_proba로 이탈 확률을 구하고, 그 확률에 따라 위험 등급을 분류합니다. 이 결과를 마케팅 팀에 전달하면, 그들은 고위험 고객에게 특별 할인을, 주의 고객에게는 만족도 설문을 보내는 식으로 활용할 수 있습니다.
실무에서는 여기서 더 나아가야 합니다. 모델 성능을 지속적으로 모니터링하고, 성능이 저하되면 재학습하는 MLOps 파이프라인을 구축합니다.
고객의 행동 패턴은 시간이 지나면서 변할 수 있기 때문입니다. 이를 데이터 드리프트라고 부릅니다.
김개발 씨는 첫 번째 머신러닝 프로젝트를 성공적으로 마무리했습니다. 마케팅 팀장은 매일 아침 이탈 위험 고객 목록을 받아보게 되었고, 선제적인 고객 케어가 가능해졌습니다.
박시니어 씨가 어깨를 토닥이며 말했습니다. "축하해요, 개발 씨.
하지만 이건 시작일 뿐이에요. 앞으로 더 많은 도전이 기다리고 있답니다."
실전 팁
💡 - 모델 저장 시 전처리에 사용한 scaler나 encoder도 함께 저장해야 합니다
- 프로덕션 환경에서는 모델 버전 관리와 A/B 테스트를 고려하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
vLLM 통합 완벽 가이드
대규모 언어 모델 추론을 획기적으로 가속화하는 vLLM의 설치부터 실전 서비스 구축까지 다룹니다. PagedAttention과 연속 배칭 기술로 GPU 메모리를 효율적으로 활용하는 방법을 배웁니다.
Web UI Demo 구축 완벽 가이드
Gradio를 활용하여 머신러닝 모델과 AI 서비스를 위한 웹 인터페이스를 구축하는 방법을 다룹니다. 코드 몇 줄만으로 전문적인 데모 페이지를 만들고 배포하는 과정을 초급자도 쉽게 따라할 수 있도록 설명합니다.
Sandboxing & Execution Control 완벽 가이드
AI 에이전트가 코드를 실행할 때 반드시 필요한 보안 기술인 샌드박싱과 실행 제어에 대해 알아봅니다. 격리된 환경에서 안전하게 코드를 실행하고, 악성 동작을 탐지하는 방법을 단계별로 설명합니다.
Voice Design then Clone 워크플로우 완벽 가이드
AI 음성 합성에서 일관된 캐릭터 음성을 만드는 Voice Design then Clone 워크플로우를 설명합니다. 참조 음성 생성부터 재사용 가능한 캐릭터 구축까지 실무 활용법을 다룹니다.
Tool Use 완벽 가이드 - Shell, Browser, DB 실전 활용
AI 에이전트가 외부 도구를 활용하여 셸 명령어 실행, 브라우저 자동화, 데이터베이스 접근 등을 수행하는 방법을 배웁니다. 실무에서 바로 적용할 수 있는 패턴과 베스트 프랙티스를 담았습니다.