포스트

[이제와서 시작하는 Python 마스터하기 #19] 머신러닝 기초: scikit-learn으로 시작하는 AI

[이제와서 시작하는 Python 마스터하기 #19] 머신러닝 기초: scikit-learn으로 시작하는 AI

💼 실무 예시: 카카오뱅크 같은 AI 기반 금융 서비스 개발하기

머신러닝을 배우기 전에, 실제 한국 기업들이 어떻게 Python 머신러닝을 활용하고 있는지 살펴보겠습니다.

카카오뱅크/토스 스타일의 AI 금융 서비스 시나리오를 통해 scikit-learn의 실무 활용법을 이해해보겠습니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
# AI 금융 서비스 머신러닝 활용 사례
"""
카카오뱅크/토스가 머신러닝으로 하는 일:

1. 신용 평가 모델
   - 고객 신용도 예측 (로지스틱 회귀, 랜덤 포레스트)
   - 대출 승인/거부 자동화
   - 금리 책정 알고리즘

2. 이상 거래 탐지
   - 사기 거래 실시간 탐지 (SVM, 이상치 탐지)
   - 카드 도용 방지
   - 이상 로그인 패턴 감지

3. 맞춤형 상품 추천
   - 협업 필터링으로 금융상품 추천
   - 고객 세그먼테이션 (K-means 클러스터링)
   - 생활 패턴 기반 서비스 제안

4. 리스크 관리
   - 포트폴리오 위험도 계산
   - 시장 변동성 예측
   - 연체 예측 모델

5. 자연어 처리
   - 챗봇 자동응답 (한국어 NLP)
   - 고객 문의 자동 분류
   - 감정 분석으로 고객 만족도 측정

실제 사용 기업:
- 카카오뱅크: 신용평가, 사기탐지, 챗봇
- 토스: 가계부 자동분류, 투자 추천
- 네이버파이낸셜: 신용대출 심사, 보험 추천
- 신한은행: 디지털 트윈, 로보어드바이저
- KB국민은행: 리브 스마트 뱅킹, AI 상담
"""

# 간단한 신용평가 모델 예시
import pandas as pd
import numpy as np
from sklearn.ensemble import RandomForestClassifier

# 샘플 고객 데이터 (개인정보 비식별화)
sample_customers = {
    'age': [25, 35, 45, 30, 50],
    'income': [3000, 5000, 7000, 4000, 8000],  # 만원 단위
    'credit_history': [2, 5, 10, 3, 15],  # 년
    'loan_amount': [1000, 3000, 5000, 2000, 6000],  # 만원
    'employment_type': [1, 2, 2, 1, 2],  # 1: 정규직, 2: 공무원
    'default_risk': [0, 0, 0, 1, 0]  # 0: 정상, 1: 위험
}

print("AI 금융 서비스 머신러닝 시나리오:")
print("- 신용평가 자동화로 대출 심사 효율성 향상")
print("- 실시간 사기 탐지로 금융 보안 강화")
print("- 개인 맞춤 금융상품 추천으로 고객 만족도 증대")
print("- 한국어 NLP로 고객 서비스 자동화")

이제 이런 실제 AI 서비스를 만들 수 있는 머신러닝 기술을 배워보겠습니다.

🤖 머신러닝의 세계로의 첫 걸음

안녕하세요! Python 마스터리 시리즈의 19번째 포스트에 오신 것을 환영합니다. 오늘은 머신러닝의 기초부터 실전 구현까지, scikit-learn을 활용해 AI 모델을 만들어보겠습니다. 복잡해 보이는 머신러닝을 쉽고 체계적으로 배워보겠습니다.

📋 목차

  1. 머신러닝 기초 개념
  2. scikit-learn 환경 설정
  3. 지도학습: 분류와 회귀
  4. 비지도학습: 클러스터링
  5. 데이터 전처리와 특성 엔지니어링
  6. 모델 평가와 검증
  7. 실전 머신러닝 프로젝트

머신러닝 기초 개념

머신러닝이란?

머신러닝은 데이터로부터 패턴을 학습하여 예측하거나 의사결정을 하는 알고리즘입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
# 머신러닝의 기본 프로세스
"""
1. 데이터 수집 → 2. 전처리 → 3. 모델 선택 → 4. 학습 → 5. 평가 → 6. 예측
"""

# 실생활 예시
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_squared_error, r2_score

# 간단한 예시: 집 크기로 가격 예측
print("=== 머신러닝 개념 이해하기 ===")

# 가상의 주택 데이터
np.random.seed(42)
house_sizes = np.random.uniform(50, 200, 100)  # 집 크기 (평방미터)
# 집 크기가 클수록 가격이 높아지는 관계에 노이즈 추가
house_prices = house_sizes * 5 + np.random.normal(0, 50, 100) + 100

# 데이터 시각화
plt.figure(figsize=(10, 6))
plt.scatter(house_sizes, house_prices, alpha=0.6)
plt.xlabel('집 크기 (m²)')
plt.ylabel('가격 (만원)')
plt.title('집 크기와 가격의 관계')
plt.grid(True, alpha=0.3)
plt.show()

print(f"데이터 포인트 수: {len(house_sizes)}")
print(f"평균 집 크기: {house_sizes.mean():.2f}")
print(f"평균 가격: {house_prices.mean():.2f}만원")

머신러닝의 주요 유형

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# 머신러닝 유형 설명
machine_learning_types = {
    "지도학습 (Supervised Learning)": {
        "정의": "라벨이 있는 데이터로 학습",
        "종류": ["분류 (Classification)", "회귀 (Regression)"],
        "예시": "스팸 메일 분류, 주가 예측",
        "데이터": "입력 + 정답"
    },
    "비지도학습 (Unsupervised Learning)": {
        "정의": "라벨이 없는 데이터에서 패턴 발견",
        "종류": ["클러스터링", "차원 축소", "연관 규칙"],
        "예시": "고객 세분화, 추천 시스템",
        "데이터": "입력만"
    },
    "강화학습 (Reinforcement Learning)": {
        "정의": "환경과의 상호작용을 통한 학습",
        "종류": ["Q-learning", "정책 기반 학습"],
        "예시": "게임 AI, 자율주행",
        "데이터": "상태, 행동, 보상"
    }
}

for ml_type, info in machine_learning_types.items():
    print(f"\n=== {ml_type} ===")
    for key, value in info.items():
        print(f"{key}: {value}")

# 머신러닝 워크플로우
print("\n=== 머신러닝 워크플로우 ===")
workflow_steps = [
    "1. 문제 정의 및 목표 설정",
    "2. 데이터 수집 및 탐색",
    "3. 데이터 전처리 및 특성 엔지니어링",
    "4. 모델 선택 및 훈련",
    "5. 모델 평가 및 검증",
    "6. 하이퍼파라미터 튜닝",
    "7. 최종 모델 배포"
]

for step in workflow_steps:
    print(step)

[!TIP] 머신러닝은 마법이 아니에요!

머신러닝이 만능은 아닙니다. 데이터가 엉망이면 아무리 좋은 모델도 소용없습니다.

  • 데이터 품질이 80%: 좋은 데이터가 좋은 모델보다 중요합니다.
  • 단순한 모델부터: 로지스틱 회귀나 의사결정 트리처럼 간단한 모델로 시작하세요.
  • 과적합 주의: 훈련 데이터에서만 잘 동작하고 실제로는 안 되는 경우가 많습니다!

scikit-learn 환경 설정

필수 라이브러리 설치

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 필요한 라이브러리 임포트
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn import datasets
from sklearn.model_selection import train_test_split, cross_val_score, GridSearchCV
from sklearn.preprocessing import StandardScaler, LabelEncoder, OneHotEncoder
from sklearn.linear_model import LinearRegression, LogisticRegression
from sklearn.tree import DecisionTreeClassifier, DecisionTreeRegressor
from sklearn.ensemble import RandomForestClassifier, RandomForestRegressor
from sklearn.svm import SVC, SVR
from sklearn.naive_bayes import GaussianNB
from sklearn.neighbors import KNeighborsClassifier
from sklearn.cluster import KMeans, DBSCAN
from sklearn.decomposition import PCA
from sklearn.metrics import (
    accuracy_score, precision_score, recall_score, f1_score,
    confusion_matrix, classification_report,
    mean_squared_error, mean_absolute_error, r2_score
)
import warnings
warnings.filterwarnings('ignore')

print("scikit-learn 환경 설정 완료!")
print(f"사용 중인 scikit-learn 버전: {sklearn.__version__}")

# 한글 폰트 설정
plt.rcParams['font.family'] = 'AppleGothic' if 'AppleGothic' in plt.rcParams['font.family'] else 'DejaVu Sans'
plt.rcParams['axes.unicode_minus'] = False

scikit-learn 기본 구조

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
# scikit-learn의 일관된 API 구조
class MLModelTemplate:
    """scikit-learn 모델의 표준 구조"""

    def __init__(self, **params):
        """모델 초기화 및 하이퍼파라미터 설정"""
        pass

    def fit(self, X, y):
        """훈련 데이터로 모델 학습"""
        pass

    def predict(self, X):
        """새로운 데이터에 대한 예측"""
        pass

    def score(self, X, y):
        """모델 성능 평가"""
        pass

# 실제 사용 예시
from sklearn.datasets import load_iris
from sklearn.ensemble import RandomForestClassifier

# 1. 데이터 로드
iris = load_iris()
X, y = iris.data, iris.target

print("=== Iris 데이터셋 정보 ===")
print(f"특성 수: {X.shape[1]}")
print(f"샘플 수: {X.shape[0]}")
print(f"클래스 수: {len(np.unique(y))}")
print(f"특성 이름: {iris.feature_names}")
print(f"클래스 이름: {iris.target_names}")

# 2. 데이터 분할
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# 3. 모델 생성 및 학습
model = RandomForestClassifier(n_estimators=100, random_state=42)
model.fit(X_train, y_train)

# 4. 예측 및 평가
y_pred = model.predict(X_test)
accuracy = model.score(X_test, y_test)

print(f"\n모델 정확도: {accuracy:.3f}")
print(f"예측 결과: {y_pred}")
print(f"실제 값: {y_test}")

지도학습: 분류와 회귀

분류 (Classification)

분류는 데이터를 미리 정의된 카테고리로 분류하는 작업입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# 이진 분류 예제: 유방암 진단
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.metrics import classification_report, confusion_matrix

# 데이터 로드
cancer = load_breast_cancer()
X, y = cancer.data, cancer.target

print("=== 유방암 진단 분류 문제 ===")
print(f"특성 수: {X.shape[1]}")
print(f"샘플 수: {X.shape[0]}")
print(f"양성 비율: {(y == 1).mean():.3f}")
print(f"음성 비율: {(y == 0).mean():.3f}")

# 데이터 분할
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# 특성 정규화
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)

# 여러 분류 모델 비교
classifiers = {
    'Logistic Regression': LogisticRegression(random_state=42),
    'Random Forest': RandomForestClassifier(n_estimators=100, random_state=42),
    'SVM': SVC(random_state=42),
    'Naive Bayes': GaussianNB()
}

results = {}

for name, clf in classifiers.items():
    # 모델 학습
    if name == 'SVM':
        clf.fit(X_train_scaled, y_train)
        y_pred = clf.predict(X_test_scaled)
    else:
        clf.fit(X_train, y_train)
        y_pred = clf.predict(X_test)

    # 성능 평가
    accuracy = accuracy_score(y_test, y_pred)
    precision = precision_score(y_test, y_pred)
    recall = recall_score(y_test, y_pred)
    f1 = f1_score(y_test, y_pred)

    results[name] = {
        'accuracy': accuracy,
        'precision': precision,
        'recall': recall,
        'f1': f1
    }

# 결과 출력
print("\n=== 분류 모델 성능 비교 ===")
results_df = pd.DataFrame(results).T
print(results_df.round(4))

# 최고 성능 모델 선택
best_model_name = results_df['accuracy'].idxmax()
best_model = classifiers[best_model_name]

print(f"\n최고 성능 모델: {best_model_name}")
print(f"정확도: {results[best_model_name]['accuracy']:.4f}")

# 혼동 행렬 시각화
if best_model_name == 'SVM':
    y_pred_best = best_model.predict(X_test_scaled)
else:
    y_pred_best = best_model.predict(X_test)

plt.figure(figsize=(8, 6))
cm = confusion_matrix(y_test, y_pred_best)
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
            xticklabels=['악성', '양성'], yticklabels=['악성', '양성'])
plt.title(f'{best_model_name} - 혼동 행렬')
plt.ylabel('실제 값')
plt.xlabel('예측 값')
plt.show()

print(f"\n상세 분류 리포트:")
print(classification_report(y_test, y_pred_best,
                          target_names=['악성', '양성']))

다중 클래스 분류

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
# 다중 클래스 분류: 붓꽃 품종 분류
from sklearn.datasets import load_iris
from sklearn.tree import DecisionTreeClassifier
from sklearn.metrics import classification_report

# 데이터 로드
iris = load_iris()
X, y = iris.data, iris.target

print("=== 붓꽃 품종 분류 (다중 클래스) ===")

# 데이터 분할
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.3, random_state=42, stratify=y
)

# 의사결정 트리 모델
dt_model = DecisionTreeClassifier(random_state=42, max_depth=3)
dt_model.fit(X_train, y_train)

# 예측 및 평가
y_pred = dt_model.predict(X_test)
accuracy = accuracy_score(y_test, y_pred)

print(f"정확도: {accuracy:.4f}")
print("\n분류 리포트:")
print(classification_report(y_test, y_pred, target_names=iris.target_names))

# 특성 중요도 시각화
feature_importance = dt_model.feature_importances_
feature_names = iris.feature_names

plt.figure(figsize=(10, 6))
plt.barh(feature_names, feature_importance)
plt.title('특성 중요도')
plt.xlabel('중요도')
plt.show()

# 의사결정 트리 시각화 (텍스트 기반)
from sklearn.tree import export_text
tree_rules = export_text(dt_model, feature_names=feature_names)
print("\n의사결정 트리 규칙:")
print(tree_rules[:500] + "..." if len(tree_rules) > 500 else tree_rules)

회귀 (Regression)

회귀는 연속적인 수치 값을 예측하는 작업입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# 회귀 예제: 보스턴 주택 가격 예측
from sklearn.datasets import fetch_california_housing
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_squared_error, r2_score

# 캘리포니아 주택 데이터 (보스턴 데이터셋의 대안)
housing = fetch_california_housing()
X, y = housing.data, housing.target

print("=== 캘리포니아 주택 가격 예측 ===")
print(f"특성 수: {X.shape[1]}")
print(f"샘플 수: {X.shape[0]}")
print(f"평균 가격: ${y.mean():.2f} (단위: $100,000)")
print(f"가격 범위: ${y.min():.2f} ~ ${y.max():.2f}")
print(f"특성 이름: {housing.feature_names}")

# 데이터 분할
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42
)

# 회귀 모델들 비교
regressors = {
    'Linear Regression': LinearRegression(),
    'Random Forest': RandomForestRegressor(n_estimators=100, random_state=42),
    'Decision Tree': DecisionTreeRegressor(random_state=42)
}

regression_results = {}

for name, regressor in regressors.items():
    # 모델 학습
    regressor.fit(X_train, y_train)

    # 예측
    y_pred = regressor.predict(X_test)

    # 성능 평가
    mse = mean_squared_error(y_test, y_pred)
    rmse = np.sqrt(mse)
    mae = mean_absolute_error(y_test, y_pred)
    r2 = r2_score(y_test, y_pred)

    regression_results[name] = {
        'MSE': mse,
        'RMSE': rmse,
        'MAE': mae,
        '': r2
    }

# 결과 출력
print("\n=== 회귀 모델 성능 비교 ===")
regression_df = pd.DataFrame(regression_results).T
print(regression_df.round(4))

# 최고 성능 모델 (R² 기준)
best_regressor_name = regression_df[''].idxmax()
best_regressor = regressors[best_regressor_name]

print(f"\n최고 성능 모델: {best_regressor_name}")
print(f"R² 점수: {regression_results[best_regressor_name]['']:.4f}")

# 예측 vs 실제 값 시각화
y_pred_best = best_regressor.predict(X_test)

plt.figure(figsize=(12, 5))

plt.subplot(1, 2, 1)
plt.scatter(y_test, y_pred_best, alpha=0.6)
plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], 'r--', lw=2)
plt.xlabel('실제 가격')
plt.ylabel('예측 가격')
plt.title(f'{best_regressor_name}: 예측 vs 실제')

plt.subplot(1, 2, 2)
residuals = y_test - y_pred_best
plt.scatter(y_pred_best, residuals, alpha=0.6)
plt.axhline(y=0, color='r', linestyle='--')
plt.xlabel('예측 가격')
plt.ylabel('잔차 (실제 - 예측)')
plt.title('잔차 플롯')

plt.tight_layout()
plt.show()

# 특성 중요도 (Random Forest인 경우)
if best_regressor_name == 'Random Forest':
    feature_importance = best_regressor.feature_importances_

    plt.figure(figsize=(10, 6))
    plt.barh(housing.feature_names, feature_importance)
    plt.title('특성 중요도 (Random Forest)')
    plt.xlabel('중요도')
    plt.show()

[!WARNING] 훈련/테스트 데이터 분리는 필수!

절대 모든 데이터로 훈련하고 같은 데이터로 평가하면 안 됩니다!

  • 훈련 데이터: 모델이 패턴을 배우는 데 사용 (70-80%)
  • 테스트 데이터: 모델의 실제 성능을 평가 (20-30%)

train_test_split()을 꼭 사용해서 데이터를 나누c야 합니다. 안 그러면 모델이 답을 외우기만 하고 새로운 데이터에서는 전혀 작동하지 않을 수 있습니다!

비지도학습: 클러스터링

K-Means 클러스터링

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# K-Means 클러스터링 예제
from sklearn.cluster import KMeans
from sklearn.datasets import make_blobs

# 샘플 데이터 생성
X_blobs, y_true = make_blobs(n_samples=300, centers=4, cluster_std=0.60,
                           random_state=42)

print("=== K-Means 클러스터링 ===")
print(f"데이터 포인트 수: {X_blobs.shape[0]}")
print(f"특성 수: {X_blobs.shape[1]}")

# K-Means 모델
kmeans = KMeans(n_clusters=4, random_state=42, n_init=10)
y_pred = kmeans.fit_predict(X_blobs)

# 클러스터 중심점
centroids = kmeans.cluster_centers_

# 시각화
plt.figure(figsize=(15, 5))

# 1. 원본 데이터 (실제 레이블)
plt.subplot(1, 3, 1)
plt.scatter(X_blobs[:, 0], X_blobs[:, 1], c=y_true, cmap='viridis', alpha=0.7)
plt.title('실제 클러스터')
plt.xlabel('특성 1')
plt.ylabel('특성 2')

# 2. K-Means 결과
plt.subplot(1, 3, 2)
plt.scatter(X_blobs[:, 0], X_blobs[:, 1], c=y_pred, cmap='viridis', alpha=0.7)
plt.scatter(centroids[:, 0], centroids[:, 1], marker='x', s=300,
           linewidths=3, color='red', label='중심점')
plt.title('K-Means 클러스터링 결과')
plt.xlabel('특성 1')
plt.ylabel('특성 2')
plt.legend()

# 3. 최적 클러스터 수 찾기 (엘보우 방법)
inertias = []
K_range = range(1, 11)

for k in K_range:
    kmeans_temp = KMeans(n_clusters=k, random_state=42, n_init=10)
    kmeans_temp.fit(X_blobs)
    inertias.append(kmeans_temp.inertia_)

plt.subplot(1, 3, 3)
plt.plot(K_range, inertias, 'bo-')
plt.title('엘보우 방법 (최적 K 찾기)')
plt.xlabel('클러스터 수 (K)')
plt.ylabel('이너셔 (Inertia)')
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

print(f"K-Means 이너셔: {kmeans.inertia_:.2f}")

실제 데이터 클러스터링

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# 붓꽃 데이터로 클러스터링 (비지도학습)
from sklearn.datasets import load_iris
from sklearn.cluster import KMeans, DBSCAN
from sklearn.preprocessing import StandardScaler

# 데이터 로드 (레이블 없이 사용)
iris = load_iris()
X = iris.data  # 레이블(y) 사용하지 않음

print("=== 붓꽃 데이터 클러스터링 ===")

# 데이터 정규화
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)

# K-Means 클러스터링
kmeans = KMeans(n_clusters=3, random_state=42, n_init=10)
kmeans_labels = kmeans.fit_predict(X_scaled)

# DBSCAN 클러스터링
dbscan = DBSCAN(eps=0.5, min_samples=5)
dbscan_labels = dbscan.fit_predict(X_scaled)

# 결과 비교 시각화 (처음 두 특성만 사용)
plt.figure(figsize=(15, 5))

# 1. 실제 레이블
plt.subplot(1, 3, 1)
plt.scatter(X[:, 0], X[:, 1], c=iris.target, cmap='viridis', alpha=0.7)
plt.title('실제 붓꽃 품종')
plt.xlabel(iris.feature_names[0])
plt.ylabel(iris.feature_names[1])

# 2. K-Means 결과
plt.subplot(1, 3, 2)
plt.scatter(X[:, 0], X[:, 1], c=kmeans_labels, cmap='viridis', alpha=0.7)
plt.title('K-Means 클러스터링')
plt.xlabel(iris.feature_names[0])
plt.ylabel(iris.feature_names[1])

# 3. DBSCAN 결과
plt.subplot(1, 3, 3)
plt.scatter(X[:, 0], X[:, 1], c=dbscan_labels, cmap='viridis', alpha=0.7)
plt.title('DBSCAN 클러스터링')
plt.xlabel(iris.feature_names[0])
plt.ylabel(iris.feature_names[1])

plt.tight_layout()
plt.show()

# 클러스터링 성능 평가 (실제 레이블과 비교)
from sklearn.metrics import adjusted_rand_score, silhouette_score

# 조정된 랜드 지수 (실제 레이블 필요)
ari_kmeans = adjusted_rand_score(iris.target, kmeans_labels)
ari_dbscan = adjusted_rand_score(iris.target, dbscan_labels)

# 실루엣 점수 (실제 레이블 불필요)
sil_kmeans = silhouette_score(X_scaled, kmeans_labels)
sil_dbscan = silhouette_score(X_scaled, dbscan_labels) if len(set(dbscan_labels)) > 1 else -1

print("\n=== 클러스터링 성능 평가 ===")
print(f"K-Means ARI: {ari_kmeans:.3f}")
print(f"DBSCAN ARI: {ari_dbscan:.3f}")
print(f"K-Means 실루엣 점수: {sil_kmeans:.3f}")
print(f"DBSCAN 실루엣 점수: {sil_dbscan:.3f}")
print(f"DBSCAN 클러스터 수: {len(set(dbscan_labels)) - (1 if -1 in dbscan_labels else 0)}")

차원 축소 (PCA)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
# 주성분 분석 (PCA)
from sklearn.decomposition import PCA

print("=== 주성분 분석 (PCA) ===")

# 원본 데이터는 4차원
print(f"원본 데이터 차원: {X.shape[1]}")

# PCA로 2차원으로 축소
pca = PCA(n_components=2)
X_pca = pca.fit_transform(X_scaled)

print(f"축소된 데이터 차원: {X_pca.shape[1]}")
print(f"설명된 분산 비율: {pca.explained_variance_ratio_}")
print(f"총 설명된 분산: {pca.explained_variance_ratio_.sum():.3f}")

# PCA 결과 시각화
plt.figure(figsize=(12, 5))

plt.subplot(1, 2, 1)
plt.scatter(X_pca[:, 0], X_pca[:, 1], c=iris.target, cmap='viridis', alpha=0.7)
plt.xlabel(f'PC1 ({pca.explained_variance_ratio_[0]:.2%} 분산)')
plt.ylabel(f'PC2 ({pca.explained_variance_ratio_[1]:.2%} 분산)')
plt.title('PCA 결과')
plt.colorbar(label='붓꽃 품종')

# 주성분 벡터 시각화
plt.subplot(1, 2, 2)
for i, (x, y) in enumerate(pca.components_.T):
    plt.arrow(0, 0, x, y, head_width=0.05, head_length=0.05, fc='red', ec='red')
    plt.text(x*1.2, y*1.2, iris.feature_names[i], fontsize=10)

plt.xlim(-1, 1)
plt.ylim(-1, 1)
plt.xlabel('PC1')
plt.ylabel('PC2')
plt.title('주성분 벡터')
plt.grid(True, alpha=0.3)

plt.tight_layout()
plt.show()

# 각 주성분의 기여도
print("\n=== 주성분별 특성 기여도 ===")
components_df = pd.DataFrame(
    pca.components_.T,
    columns=['PC1', 'PC2'],
    index=iris.feature_names
)
print(components_df.round(3))

데이터 전처리와 특성 엔지니어링

데이터 전처리

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
# 종합적인 데이터 전처리 예제
from sklearn.preprocessing import StandardScaler, MinMaxScaler, LabelEncoder, OneHotEncoder
from sklearn.impute import SimpleImputer

# 샘플 데이터 생성 (다양한 타입의 특성 포함)
np.random.seed(42)
sample_data = {
    'age': np.random.normal(35, 10, 1000).astype(int),
    'income': np.random.exponential(50000, 1000),
    'education': np.random.choice(['High School', 'Bachelor', 'Master', 'PhD'], 1000),
    'city': np.random.choice(['Seoul', 'Busan', 'Incheon', 'Daegu'], 1000),
    'experience': np.random.uniform(0, 20, 1000),
    'rating': np.random.uniform(1, 5, 1000)
}

# 일부 결측값 추가
sample_data['income'][::100] = np.nan
sample_data['rating'][::150] = np.nan

df = pd.DataFrame(sample_data)
print("=== 원본 데이터 ===")
print(df.head())
print(f"\n데이터 형태: {df.shape}")
print(f"결측값:\n{df.isnull().sum()}")

# 1. 결측값 처리
print("\n=== 1. 결측값 처리 ===")

# 수치형 변수: 평균으로 채우기
numeric_imputer = SimpleImputer(strategy='mean')
df[['income', 'rating']] = numeric_imputer.fit_transform(df[['income', 'rating']])

print("결측값 처리 후:")
print(df.isnull().sum())

# 2. 범주형 변수 인코딩
print("\n=== 2. 범주형 변수 인코딩 ===")

# Label Encoding (순서가 있는 경우)
education_mapping = {'High School': 1, 'Bachelor': 2, 'Master': 3, 'PhD': 4}
df['education_encoded'] = df['education'].map(education_mapping)

# One-Hot Encoding (순서가 없는 경우)
city_dummies = pd.get_dummies(df['city'], prefix='city')
df_processed = pd.concat([df, city_dummies], axis=1)

print("인코딩 후 컬럼:")
print(df_processed.columns.tolist())

# 3. 수치형 변수 스케일링
print("\n=== 3. 수치형 변수 스케일링 ===")

numeric_columns = ['age', 'income', 'experience', 'rating']

# StandardScaler (평균 0, 표준편차 1)
scaler_standard = StandardScaler()
df_standard = df_processed.copy()
df_standard[numeric_columns] = scaler_standard.fit_transform(df_processed[numeric_columns])

# MinMaxScaler (0-1 범위)
scaler_minmax = MinMaxScaler()
df_minmax = df_processed.copy()
df_minmax[numeric_columns] = scaler_minmax.fit_transform(df_processed[numeric_columns])

print("스케일링 전후 비교 (income):")
print(f"원본: 평균={df_processed['income'].mean():.2f}, 표준편차={df_processed['income'].std():.2f}")
print(f"Standard: 평균={df_standard['income'].mean():.2f}, 표준편차={df_standard['income'].std():.2f}")
print(f"MinMax: 최솟값={df_minmax['income'].min():.2f}, 최댓값={df_minmax['income'].max():.2f}")

# 4. 이상값 탐지 및 처리
print("\n=== 4. 이상값 탐지 및 처리 ===")

def detect_outliers_iqr(data, column):
    Q1 = data[column].quantile(0.25)
    Q3 = data[column].quantile(0.75)
    IQR = Q3 - Q1
    lower_bound = Q1 - 1.5 * IQR
    upper_bound = Q3 + 1.5 * IQR
    outliers = data[(data[column] < lower_bound) | (data[column] > upper_bound)]
    return outliers, lower_bound, upper_bound

outliers, lower, upper = detect_outliers_iqr(df_processed, 'income')
print(f"Income 이상값: {len(outliers)}")
print(f"정상 범위: {lower:.2f} ~ {upper:.2f}")

# 이상값 시각화
plt.figure(figsize=(12, 4))

plt.subplot(1, 3, 1)
plt.boxplot(df_processed['income'])
plt.title('Income 박스플롯')
plt.ylabel('Income')

plt.subplot(1, 3, 2)
plt.hist(df_processed['income'], bins=50, alpha=0.7)
plt.title('Income 분포')
plt.xlabel('Income')

plt.subplot(1, 3, 3)
plt.scatter(range(len(df_processed)), df_processed['income'], alpha=0.6)
plt.axhline(y=upper, color='r', linestyle='--', label='상한선')
plt.axhline(y=lower, color='r', linestyle='--', label='하한선')
plt.title('Income 산점도')
plt.xlabel('인덱스')
plt.ylabel('Income')
plt.legend()

plt.tight_layout()
plt.show()

특성 엔지니어링

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
# 고급 특성 엔지니어링
print("=== 특성 엔지니어링 ===")

# 1. 새로운 특성 생성
df_engineered = df_processed.copy()

# 파생 변수 생성
df_engineered['income_per_experience'] = df_engineered['income'] / (df_engineered['experience'] + 1)
df_engineered['age_group'] = pd.cut(df_engineered['age'],
                                  bins=[0, 25, 35, 50, 100],
                                  labels=['청년', '중년초', '중년후', '장년'])

# 상호작용 특성
df_engineered['income_x_rating'] = df_engineered['income'] * df_engineered['rating']

# 2. 다항식 특성
from sklearn.preprocessing import PolynomialFeatures

poly_features = PolynomialFeatures(degree=2, include_bias=False, interaction_only=True)
income_experience = df_engineered[['income', 'experience']].values
poly_result = poly_features.fit_transform(income_experience)

poly_feature_names = poly_features.get_feature_names_out(['income', 'experience'])
poly_df = pd.DataFrame(poly_result, columns=poly_feature_names)

print("다항식 특성:")
print(poly_df.head())

# 3. 특성 선택
from sklearn.feature_selection import SelectKBest, f_regression
from sklearn.feature_selection import RFE
from sklearn.ensemble import RandomForestRegressor

# 가상의 타겟 변수 생성 (income 기반)
y_target = df_engineered['income'] + np.random.normal(0, 10000, len(df_engineered))

# 수치형 특성만 선택
numeric_features = ['age', 'income', 'experience', 'rating', 'education_encoded']
X_numeric = df_engineered[numeric_features]

# 유니버셜 특성 선택 (F-검정)
selector_univariate = SelectKBest(score_func=f_regression, k=3)
X_selected_univariate = selector_univariate.fit_transform(X_numeric, y_target)

selected_features = X_numeric.columns[selector_univariate.get_support()]
print(f"\n유니버셜 선택된 특성: {list(selected_features)}")

# 순환 특성 제거 (RFE)
rf_model = RandomForestRegressor(n_estimators=50, random_state=42)
selector_rfe = RFE(estimator=rf_model, n_features_to_select=3)
X_selected_rfe = selector_rfe.fit_transform(X_numeric, y_target)

selected_features_rfe = X_numeric.columns[selector_rfe.get_support()]
print(f"RFE 선택된 특성: {list(selected_features_rfe)}")

# 특성 중요도 시각화
rf_model.fit(X_numeric, y_target)
feature_importance = rf_model.feature_importances_

plt.figure(figsize=(10, 6))
plt.barh(X_numeric.columns, feature_importance)
plt.title('Random Forest 특성 중요도')
plt.xlabel('중요도')
plt.show()

print(f"\n특성별 중요도:")
for feature, importance in zip(X_numeric.columns, feature_importance):
    print(f"{feature}: {importance:.3f}")

모델 평가와 검증

교차 검증

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
# 교차 검증과 모델 평가
from sklearn.model_selection import cross_val_score, StratifiedKFold, learning_curve
from sklearn.metrics import make_scorer

print("=== 모델 평가와 검증 ===")

# 분류 문제로 다시 붓꽃 데이터 사용
iris = load_iris()
X, y = iris.data, iris.target

# 여러 모델 준비
models = {
    'Logistic Regression': LogisticRegression(random_state=42, max_iter=1000),
    'Random Forest': RandomForestClassifier(n_estimators=100, random_state=42),
    'SVM': SVC(random_state=42),
    'Decision Tree': DecisionTreeClassifier(random_state=42)
}

# 1. 교차 검증 수행
print("=== 1. 교차 검증 결과 ===")
cv_results = {}

for name, model in models.items():
    # 5-fold 교차 검증
    cv_scores = cross_val_score(model, X, y, cv=5, scoring='accuracy')
    cv_results[name] = {
        'mean': cv_scores.mean(),
        'std': cv_scores.std(),
        'scores': cv_scores
    }
    print(f"{name}: {cv_scores.mean():.3f} (+/- {cv_scores.std() * 2:.3f})")

# 2. Stratified K-Fold (클래스 비율 유지)
print("\n=== 2. Stratified K-Fold 결과 ===")
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)

for name, model in models.items():
    cv_scores = cross_val_score(model, X, y, cv=skf, scoring='accuracy')
    print(f"{name}: {cv_scores.mean():.3f} (+/- {cv_scores.std() * 2:.3f})")

# 3. 학습 곡선
print("\n=== 3. 학습 곡선 분석 ===")

def plot_learning_curve(estimator, title, X, y, cv=None, n_jobs=None,
                        train_sizes=np.linspace(.1, 1.0, 5)):
    """학습 곡선 그리기"""
    train_sizes, train_scores, test_scores = learning_curve(
        estimator, X, y, cv=cv, n_jobs=n_jobs, train_sizes=train_sizes)

    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=(8, 6))
    plt.title(title)
    plt.xlabel("훈련 샘플 수")
    plt.ylabel("정확도")

    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="훈련 점수")
    plt.plot(train_sizes, test_scores_mean, 'o-', color="g", label="검증 점수")

    plt.legend(loc="best")
    plt.grid(True, alpha=0.3)
    return plt

# Random Forest 학습 곡선
plot_learning_curve(RandomForestClassifier(n_estimators=100, random_state=42),
                   "Random Forest 학습 곡선", X, y, cv=5)
plt.show()

# 4. 다양한 평가 지표
print("\n=== 4. 다양한 평가 지표 ===")

# 데이터 분할
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3,
                                                   random_state=42, stratify=y)

# Random Forest 모델 학습
rf_model = RandomForestClassifier(n_estimators=100, random_state=42)
rf_model.fit(X_train, y_train)
y_pred = rf_model.predict(X_test)

# 분류 리포트
print("분류 리포트:")
print(classification_report(y_test, y_pred, target_names=iris.target_names))

# 혼동 행렬
cm = confusion_matrix(y_test, y_pred)
plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
           xticklabels=iris.target_names, yticklabels=iris.target_names)
plt.title('혼동 행렬')
plt.xlabel('예측 값')
plt.ylabel('실제 값')
plt.show()

하이퍼파라미터 튜닝

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# 하이퍼파라미터 튜닝
from sklearn.model_selection import GridSearchCV, RandomizedSearchCV

print("=== 하이퍼파라미터 튜닝 ===")

# 1. Grid Search
print("=== 1. Grid Search ===")

# Random Forest 하이퍼파라미터 그리드
param_grid = {
    'n_estimators': [50, 100, 200],
    'max_depth': [3, 5, 7, None],
    'min_samples_split': [2, 5, 10],
    'min_samples_leaf': [1, 2, 4]
}

rf = RandomForestClassifier(random_state=42)

# Grid Search 수행
grid_search = GridSearchCV(estimator=rf, param_grid=param_grid,
                          cv=5, scoring='accuracy', n_jobs=-1, verbose=1)

grid_search.fit(X_train, y_train)

print(f"최적 파라미터: {grid_search.best_params_}")
print(f"최적 교차 검증 점수: {grid_search.best_score_:.3f}")

# 최적 모델로 테스트
best_rf = grid_search.best_estimator_
test_score = best_rf.score(X_test, y_test)
print(f"테스트 정확도: {test_score:.3f}")

# 2. Random Search (더 효율적)
print("\n=== 2. Random Search ===")

# 더 넓은 범위의 파라미터 분포
param_dist = {
    'n_estimators': range(50, 300, 10),
    'max_depth': [3, 5, 7, 10, None],
    'min_samples_split': range(2, 21),
    'min_samples_leaf': range(1, 11),
    'max_features': ['sqrt', 'log2', None]
}

random_search = RandomizedSearchCV(estimator=rf, param_distributions=param_dist,
                                 n_iter=100, cv=5, scoring='accuracy',
                                 n_jobs=-1, verbose=1, random_state=42)

random_search.fit(X_train, y_train)

print(f"최적 파라미터: {random_search.best_params_}")
print(f"최적 교차 검증 점수: {random_search.best_score_:.3f}")

# 3. 하이퍼파라미터 중요도 분석
print("\n=== 3. 하이퍼파라미터 중요도 분석 ===")

# Grid Search 결과에서 파라미터별 성능 분석
results_df = pd.DataFrame(grid_search.cv_results_)

# n_estimators의 영향
n_estimators_effect = results_df.groupby('param_n_estimators')['mean_test_score'].mean()
print("n_estimators별 평균 성능:")
print(n_estimators_effect)

# max_depth의 영향
max_depth_effect = results_df.groupby('param_max_depth')['mean_test_score'].mean()
print("\nmax_depth별 평균 성능:")
print(max_depth_effect)

# 시각화
plt.figure(figsize=(15, 5))

plt.subplot(1, 3, 1)
n_estimators_effect.plot(kind='bar')
plt.title('n_estimators 영향')
plt.xlabel('n_estimators')
plt.ylabel('평균 정확도')
plt.xticks(rotation=45)

plt.subplot(1, 3, 2)
max_depth_effect.plot(kind='bar')
plt.title('max_depth 영향')
plt.xlabel('max_depth')
plt.ylabel('평균 정확도')
plt.xticks(rotation=45)

plt.subplot(1, 3, 3)
top_10_results = results_df.nlargest(10, 'mean_test_score')
plt.scatter(range(len(top_10_results)), top_10_results['mean_test_score'])
plt.title('상위 10개 모델 성능')
plt.xlabel('모델 순위')
plt.ylabel('정확도')

plt.tight_layout()
plt.show()

실전 머신러닝 프로젝트

종합 프로젝트: 고객 이탈 예측

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
# 종합 머신러닝 프로젝트: 고객 이탈 예측
class CustomerChurnPredictor:
    def __init__(self):
        self.models = {}
        self.preprocessor = None
        self.best_model = None
        self.feature_names = None

    def generate_synthetic_data(self, n_samples=5000):
        """합성 고객 데이터 생성"""
        np.random.seed(42)

        data = {
            'customer_id': range(1, n_samples + 1),
            'age': np.random.normal(40, 12, n_samples).astype(int),
            'tenure_months': np.random.exponential(24, n_samples).astype(int),
            'monthly_charges': np.random.normal(65, 20, n_samples),
            'total_charges': np.random.exponential(1000, n_samples),
            'contract_type': np.random.choice(['Month-to-month', 'One year', 'Two year'],
                                            n_samples, p=[0.5, 0.3, 0.2]),
            'payment_method': np.random.choice(['Electronic check', 'Mailed check',
                                              'Bank transfer', 'Credit card'],
                                             n_samples, p=[0.3, 0.2, 0.25, 0.25]),
            'internet_service': np.random.choice(['DSL', 'Fiber optic', 'No'],
                                               n_samples, p=[0.4, 0.4, 0.2]),
            'online_security': np.random.choice(['Yes', 'No'], n_samples, p=[0.3, 0.7]),
            'tech_support': np.random.choice(['Yes', 'No'], n_samples, p=[0.3, 0.7]),
            'streaming_tv': np.random.choice(['Yes', 'No'], n_samples, p=[0.4, 0.6]),
            'paperless_billing': np.random.choice(['Yes', 'No'], n_samples, p=[0.6, 0.4]),
            'senior_citizen': np.random.choice([0, 1], n_samples, p=[0.85, 0.15]),
            'partner': np.random.choice(['Yes', 'No'], n_samples, p=[0.5, 0.5]),
            'dependents': np.random.choice(['Yes', 'No'], n_samples, p=[0.3, 0.7])
        }

        df = pd.DataFrame(data)

        # 이탈 확률 계산 (현실적인 관계 생성)
        churn_prob = (
            0.3 * (df['contract_type'] == 'Month-to-month').astype(int) +
            0.2 * (df['tenure_months'] < 12).astype(int) +
            0.15 * (df['monthly_charges'] > 80).astype(int) +
            0.1 * (df['senior_citizen'] == 1).astype(int) +
            0.1 * (df['tech_support'] == 'No').astype(int) +
            0.15 * (df['payment_method'] == 'Electronic check').astype(int)
        )

        # 노이즈 추가 및 이탈 여부 결정
        churn_prob += np.random.normal(0, 0.1, n_samples)
        df['churn'] = (churn_prob + np.random.uniform(0, 0.3, n_samples) > 0.5).astype(int)

        # 일부 결측값 추가
        df.loc[np.random.choice(df.index, 100), 'total_charges'] = np.nan

        return df

    def preprocess_data(self, df):
        """데이터 전처리"""
        print("=== 데이터 전처리 시작 ===")

        # 결측값 처리
        df['total_charges'].fillna(df['total_charges'].median(), inplace=True)

        # 나이 이상값 처리
        df['age'] = df['age'].clip(18, 80)
        df['tenure_months'] = df['tenure_months'].clip(0, 72)

        # 특성 엔지니어링
        df['avg_monthly_charges'] = df['total_charges'] / (df['tenure_months'] + 1)
        df['charges_per_year'] = df['monthly_charges'] * 12
        df['tenure_years'] = df['tenure_months'] / 12

        # 카테고리 변수 인코딩
        categorical_columns = ['contract_type', 'payment_method', 'internet_service',
                             'online_security', 'tech_support', 'streaming_tv',
                             'paperless_billing', 'partner', 'dependents']

        df_encoded = pd.get_dummies(df, columns=categorical_columns, drop_first=True)

        print(f"전처리 후 특성 수: {df_encoded.shape[1] - 2}")  # customer_id, churn 제외
        return df_encoded

    def train_models(self, X_train, X_test, y_train, y_test):
        """여러 모델 훈련 및 평가"""
        print("=== 모델 훈련 시작 ===")

        # 모델들 정의
        models = {
            'Logistic Regression': LogisticRegression(random_state=42, max_iter=1000),
            'Random Forest': RandomForestClassifier(n_estimators=100, random_state=42),
            'Decision Tree': DecisionTreeClassifier(random_state=42, max_depth=10),
            'SVM': SVC(random_state=42, probability=True),
            'Naive Bayes': GaussianNB()
        }

        results = {}

        for name, model in models.items():
            print(f"훈련 중: {name}")

            # 모델 훈련
            if name == 'SVM':
                # SVM은 정규화가 필요
                scaler = StandardScaler()
                X_train_scaled = scaler.fit_transform(X_train)
                X_test_scaled = scaler.transform(X_test)
                model.fit(X_train_scaled, y_train)
                y_pred = model.predict(X_test_scaled)
                y_prob = model.predict_proba(X_test_scaled)[:, 1]
            else:
                model.fit(X_train, y_train)
                y_pred = model.predict(X_test)
                y_prob = model.predict_proba(X_test)[:, 1]

            # 성능 평가
            accuracy = accuracy_score(y_test, y_pred)
            precision = precision_score(y_test, y_pred)
            recall = recall_score(y_test, y_pred)
            f1 = f1_score(y_test, y_pred)

            # 교차 검증
            cv_scores = cross_val_score(model, X_train, y_train, cv=5, scoring='accuracy')

            results[name] = {
                'model': model,
                'accuracy': accuracy,
                'precision': precision,
                'recall': recall,
                'f1': f1,
                'cv_mean': cv_scores.mean(),
                'cv_std': cv_scores.std(),
                'predictions': y_pred,
                'probabilities': y_prob
            }

        self.models = results
        return results

    def evaluate_models(self):
        """모델 평가 및 비교"""
        print("=== 모델 평가 결과 ===")

        # 결과 테이블 생성
        metrics = ['accuracy', 'precision', 'recall', 'f1', 'cv_mean']
        results_df = pd.DataFrame({
            name: [results[metric] for metric in metrics]
            for name, results in self.models.items()
        }, index=metrics)

        print(results_df.round(4))

        # 최고 성능 모델 선택 (F1 점수 기준)
        best_model_name = max(self.models.keys(),
                            key=lambda x: self.models[x]['f1'])
        self.best_model = self.models[best_model_name]['model']

        print(f"\n최고 성능 모델: {best_model_name}")
        print(f"F1 점수: {self.models[best_model_name]['f1']:.4f}")

        return results_df, best_model_name

    def create_comprehensive_visualizations(self, X_test, y_test):
        """종합적인 시각화"""
        plt.figure(figsize=(20, 15))

        # 1. 모델 성능 비교
        plt.subplot(3, 4, 1)
        metrics = ['accuracy', 'precision', 'recall', 'f1']
        model_names = list(self.models.keys())

        x = np.arange(len(model_names))
        width = 0.2

        for i, metric in enumerate(metrics):
            values = [self.models[name][metric] for name in model_names]
            plt.bar(x + i * width, values, width, label=metric, alpha=0.8)

        plt.xlabel('모델')
        plt.ylabel('점수')
        plt.title('모델 성능 비교')
        plt.xticks(x + width * 1.5, model_names, rotation=45)
        plt.legend()

        # 2. 교차 검증 점수 분포
        plt.subplot(3, 4, 2)
        cv_means = [self.models[name]['cv_mean'] for name in model_names]
        cv_stds = [self.models[name]['cv_std'] for name in model_names]

        plt.bar(model_names, cv_means, yerr=cv_stds, capsize=5, alpha=0.7)
        plt.title('교차 검증 성능')
        plt.ylabel('정확도')
        plt.xticks(rotation=45)

        # 3. ROC 곡선
        plt.subplot(3, 4, 3)
        from sklearn.metrics import roc_curve, auc

        for name in model_names:
            y_prob = self.models[name]['probabilities']
            fpr, tpr, _ = roc_curve(y_test, y_prob)
            roc_auc = auc(fpr, tpr)
            plt.plot(fpr, tpr, label=f'{name} (AUC = {roc_auc:.2f})')

        plt.plot([0, 1], [0, 1], 'k--', alpha=0.5)
        plt.xlabel('False Positive Rate')
        plt.ylabel('True Positive Rate')
        plt.title('ROC 곡선')
        plt.legend()

        # 4. 혼동 행렬 (최고 성능 모델)
        plt.subplot(3, 4, 4)
        best_model_name = max(self.models.keys(),
                            key=lambda x: self.models[x]['f1'])
        y_pred_best = self.models[best_model_name]['predictions']

        cm = confusion_matrix(y_test, y_pred_best)
        sns.heatmap(cm, annot=True, fmt='d', cmap='Blues')
        plt.title(f'{best_model_name} 혼동 행렬')
        plt.xlabel('예측')
        plt.ylabel('실제')

        # 5-8. 특성 중요도 (Random Forest)
        if 'Random Forest' in self.models:
            rf_model = self.models['Random Forest']['model']
            feature_importance = rf_model.feature_importances_

            # 상위 특성들만 표시
            top_indices = np.argsort(feature_importance)[-10:]
            top_features = [self.feature_names[i] for i in top_indices]
            top_importance = feature_importance[top_indices]

            plt.subplot(3, 4, 5)
            plt.barh(top_features, top_importance)
            plt.title('상위 10개 특성 중요도')
            plt.xlabel('중요도')

        # 9. 예측 확률 분포
        plt.subplot(3, 4, 6)
        best_probs = self.models[best_model_name]['probabilities']

        plt.hist(best_probs[y_test == 0], bins=30, alpha=0.7, label='유지', density=True)
        plt.hist(best_probs[y_test == 1], bins=30, alpha=0.7, label='이탈', density=True)
        plt.xlabel('이탈 확률')
        plt.ylabel('밀도')
        plt.title('예측 확률 분포')
        plt.legend()

        plt.tight_layout()
        plt.show()

    def run_full_pipeline(self):
        """전체 파이프라인 실행"""
        print("=== 고객 이탈 예측 프로젝트 시작 ===")

        # 1. 데이터 생성
        print("1. 데이터 생성 중...")
        df = self.generate_synthetic_data(5000)
        print(f"생성된 데이터: {df.shape}")
        print(f"이탈율: {df['churn'].mean():.2%}")

        # 2. 데이터 전처리
        print("\n2. 데이터 전처리 중...")
        df_processed = self.preprocess_data(df)

        # 3. 특성과 타겟 분리
        feature_columns = [col for col in df_processed.columns
                          if col not in ['customer_id', 'churn']]
        X = df_processed[feature_columns]
        y = df_processed['churn']
        self.feature_names = feature_columns

        print(f"최종 특성 수: {len(feature_columns)}")

        # 4. 데이터 분할
        X_train, X_test, y_train, y_test = train_test_split(
            X, y, test_size=0.2, random_state=42, stratify=y
        )

        print(f"훈련 데이터: {X_train.shape[0]}")
        print(f"테스트 데이터: {X_test.shape[0]}")

        # 5. 모델 훈련
        print("\n3. 모델 훈련 중...")
        self.train_models(X_train, X_test, y_train, y_test)

        # 6. 모델 평가
        print("\n4. 모델 평가 중...")
        results_df, best_model_name = self.evaluate_models()

        # 7. 시각화
        print("\n5. 결과 시각화 중...")
        self.create_comprehensive_visualizations(X_test, y_test)

        print("\n=== 프로젝트 완료 ===")
        return {
            'data': df_processed,
            'results': results_df,
            'best_model': best_model_name,
            'X_test': X_test,
            'y_test': y_test
        }

# 프로젝트 실행
predictor = CustomerChurnPredictor()
project_results = predictor.run_full_pipeline()

# 비즈니스 인사이트 도출
print("\n=== 비즈니스 인사이트 ===")
print("1. 고객 이탈 예측 모델을 통해 사전에 이탈 위험 고객을 식별할 수 있습니다.")
print("2. 특성 중요도 분석을 통해 이탈에 가장 큰 영향을 미치는 요인을 파악했습니다.")
print("3. 예측 모델을 활용해 맞춤형 고객 유지 전략을 수립할 수 있습니다.")
print("4. 정기적인 모델 재훈련을 통해 예측 성능을 지속적으로 개선할 수 있습니다.")
graph TD
    A[문제 정의] --> B[데이터 수집]
    B --> C[탐색적 데이터 분석]
    C --> D[데이터 전처리]
    D --> E[특성 엔지니어링]
    E --> F[모델 선택]
    F --> G[모델 훈련]
    G --> H[모델 평가]
    H --> I{성능 만족?}
    I -->|No| J[하이퍼파라미터 튜닝]
    J --> G
    I -->|Yes| K[최종 모델 선택]
    K --> L[모델 배포]
    L --> M[모니터링 및 유지보수]

    subgraph "평가 지표"
        N[분류: 정확도, 정밀도, 재현율, F1]
        O[회귀: MSE, RMSE, MAE, R²]
        P[클러스터링: 실루엣, ARI]
    end

    H --> N
    H --> O
    H --> P

    style A fill:#e1f5fe
    style L fill:#f3e5f5
    style M fill:#fff3e0

🎯 핵심 포인트 정리

머신러닝 기초 개념

  • 지도학습: 분류(Classification), 회귀(Regression)
  • 비지도학습: 클러스터링, 차원 축소
  • 강화학습: 환경과의 상호작용을 통한 학습

scikit-learn 핵심 API

  • 일관된 인터페이스: fit(), predict(), score()
  • 데이터 전처리: StandardScaler, LabelEncoder, OneHotEncoder
  • 모델 선택: train_test_split, cross_val_score, GridSearchCV

모델 평가 방법

  • 분류 지표: 정확도, 정밀도, 재현율, F1 점수
  • 회귀 지표: MSE, RMSE, MAE, R² 점수
  • 교차 검증: K-Fold, Stratified K-Fold

실전 머신러닝 프로세스

  1. 문제 정의: 비즈니스 목표와 기술적 목표 설정
  2. 데이터 준비: 수집, 정제, 전처리, 특성 엔지니어링
  3. 모델 개발: 선택, 훈련, 평가, 튜닝
  4. 배포와 모니터링: 실운영 환경 적용 및 성능 추적

모범 사례

  • 데이터 품질: 결측값, 이상값, 중복값 처리
  • 특성 선택: 도메인 지식과 통계적 방법 결합
  • 모델 선택: 문제 유형과 데이터 특성에 맞는 알고리즘
  • 검증 전략: 교차 검증과 홀드아웃 테스트

🔗 시리즈 네비게이션

기초편 (1-8)

  1. Python 마스터리 #1 - 개발환경 설정과 기초 문법
  2. Python 마스터리 #2 - 변수와 데이터 타입 완전 정복
  3. Python 마스터리 #3 - 조건문과 반복문으로 제어하기
  4. Python 마스터리 #4 - 함수와 람다로 코드 재사용하기
  5. Python 마스터리 #5 - 리스트, 튜플, 딕셔너리 완전 활용
  6. Python 마스터리 #6 - 문자열 처리와 정규표현식
  7. Python 마스터리 #7 - 파일 입출력과 예외 처리
  8. Python 마스터리 #8 - 클래스와 객체지향 프로그래밍

중급편 (9-16)

  1. Python 마스터리 #9 - 모듈과 패키지로 코드 구조화하기
  2. Python 마스터리 #10 - 데코레이터와 컨텍스트 매니저
  3. Python 마스터리 #11 - 비동기 프로그래밍 async/await
  4. Python 마스터리 #12 - 제너레이터와 이터레이터 활용법
  5. Python 마스터리 #13 - 타입 힌팅과 정적 분석
  6. Python 마스터리 #14 - 테스팅과 디버깅 마스터하기
  7. Python 마스터리 #15 - 성능 최적화와 프로파일링
  8. Python 마스터리 #16 - 메타클래스와 고급 객체지향

실전편 (17-20)

  1. Python 마스터리 #17 - 웹 개발: Flask/Django 시작하기
  2. Python 마스터리 #18 - 데이터 분석: Pandas와 NumPy로 데이터 다루기
  3. Python 마스터리 #19 - 머신러닝 기초: scikit-learn 활용 (현재 글)
  4. Python 마스터리 #20 - 실전 프로젝트와 모범 사례

⚠️ 초보자들이 자주 하는 실수

머신러닝을 처음 배울 때 자주 발생하는 실수들을 정리했습니다. 이런 실수들을 미리 알고 피해가세요!

1. 데이터 분할 전에 전처리하기

1
2
3
4
5
6
7
8
9
10
11
# ❌ 잘못된 예: 전체 데이터에 스케일링 후 분할
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X)  # 전체 데이터로 학습
X_train, X_test, y_train, y_test = train_test_split(X_scaled, y)

# ✅ 올바른 예: 분할 후 훈련 데이터로만 스케일링
X_train, X_test, y_train, y_test = train_test_split(X, y)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)  # 훈련 데이터로만 학습
X_test_scaled = scaler.transform(X_test)  # 테스트 데이터는 변환만

2. 테스트 데이터로 모델 선택하기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# ❌ 잘못된 예: 테스트 데이터로 모델 성능 비교
models = [LogisticRegression(), RandomForestClassifier(), SVM()]
best_score = 0
best_model = None
for model in models:
    model.fit(X_train, y_train)
    score = model.score(X_test, y_test)  # 테스트 데이터 사용!
    if score > best_score:
        best_model = model

# ✅ 올바른 예: 검증 데이터 또는 교차 검증 사용
from sklearn.model_selection import cross_val_score
best_score = 0
best_model = None
for model in models:
    scores = cross_val_score(model, X_train, y_train, cv=5)
    avg_score = scores.mean()
    if avg_score > best_score:
        best_model = model

3. 불균형 데이터셋에서 정확도만 보기

1
2
3
4
5
6
7
8
9
10
11
12
# ❌ 잘못된 예: 불균형 데이터에서 정확도만 확인
# 사기 거래: 1%, 정상 거래: 99%
accuracy = accuracy_score(y_test, y_pred)  # 99% 나올 수 있음
print(f"정확도: {accuracy}")  # 높아도 의미 없음

# ✅ 올바른 예: 여러 지표 함께 확인
from sklearn.metrics import classification_report, confusion_matrix
print("분류 리포트:")
print(classification_report(y_test, y_pred))
print("혼동 행렬:")
print(confusion_matrix(y_test, y_pred))
# Precision, Recall, F1-score 확인

4. 하이퍼파라미터 튜닝 시 과적합

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# ❌ 잘못된 예: 테스트 데이터로 하이퍼파라미터 튜닝
param_grid = {'n_estimators': [50, 100, 200], 'max_depth': [3, 5, 10]}
best_params = None
best_score = 0
for params in param_grid:
    model = RandomForestClassifier(**params)
    model.fit(X_train, y_train)
    score = model.score(X_test, y_test)  # 테스트 데이터 사용!
    if score > best_score:
        best_params = params

# ✅ 올바른 예: GridSearchCV 사용
from sklearn.model_selection import GridSearchCV
param_grid = {'n_estimators': [50, 100, 200], 'max_depth': [3, 5, 10]}
grid_search = GridSearchCV(RandomForestClassifier(), param_grid, cv=5)
grid_search.fit(X_train, y_train)
best_model = grid_search.best_estimator_

5. 범주형 변수를 숫자로 잘못 인코딩

1
2
3
4
5
6
7
8
# ❌ 잘못된 예: 순서 없는 범주를 숫자로 변환
# 색상: 빨강=1, 파랑=2, 노랑=3 (순서 의미 없음)
df['color'] = df['color'].map({'red': 1, 'blue': 2, 'yellow': 3})

# ✅ 올바른 예: 원핫 인코딩 사용
from sklearn.preprocessing import LabelEncoder, OneHotEncoder
# 또는 pandas get_dummies
df = pd.get_dummies(df, columns=['color'], prefix='color')

6. 피처 스케일링 불일치

1
2
3
4
5
6
7
8
9
10
11
# ❌ 잘못된 예: 일부 피처만 스케일링
scaler = StandardScaler()
X_train_scaled = X_train.copy()
X_train_scaled['age'] = scaler.fit_transform(X_train[['age']])
# income은 스케일링 안 함 (0-100만원 vs 나이 0-100세)

# ✅ 올바른 예: 모든 수치형 피처 스케일링
numerical_cols = ['age', 'income', 'credit_score']
scaler = StandardScaler()
X_train_scaled = X_train.copy()
X_train_scaled[numerical_cols] = scaler.fit_transform(X_train[numerical_cols])

7. 랜덤 시드 설정 안 하기

1
2
3
4
5
6
7
8
9
10
11
# ❌ 잘못된 예: 재현 불가능한 결과
X_train, X_test, y_train, y_test = train_test_split(X, y)
model = RandomForestClassifier()
model.fit(X_train, y_train)
# 매번 다른 결과

# ✅ 올바른 예: 재현 가능한 실험
X_train, X_test, y_train, y_test = train_test_split(X, y, random_state=42)
model = RandomForestClassifier(random_state=42)
model.fit(X_train, y_train)
# 항상 같은 결과

8. 모델 해석 없이 결과만 보기

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# ❌ 잘못된 예: 블랙박스 모델 그대로 사용
model = RandomForestClassifier()
model.fit(X_train, y_train)
predictions = model.predict(X_test)
# 왜 이런 예측을 했는지 모름

# ✅ 올바른 예: 피처 중요도 및 해석 추가
model = RandomForestClassifier()
model.fit(X_train, y_train)

# 피처 중요도 확인
feature_importance = pd.DataFrame({
    'feature': X.columns,
    'importance': model.feature_importances_
}).sort_values('importance', ascending=False)

print("피처 중요도:")
print(feature_importance)

# SHAP나 LIME을 사용한 해석
# import shap
# explainer = shap.TreeExplainer(model)
# shap_values = explainer.shap_values(X_test)

이런 실수들을 피하면 더 신뢰할 수 있고 실용적인 머신러닝 모델을 만들 수 있습니다!


다음 포스트에서는 실전 프로젝트와 모범 사례에 대해 알아보겠습니다. 지금까지 배운 모든 내용을 활용해 완성도 높은 Python 프로젝트를 구축하는 방법을 배워보세요!

Happy Coding! 🐍✨

이 기사는 저작권자의 CC BY 4.0 라이센스를 따릅니다.