LESSON

演習:離反予測モデルのスコアを改善しよう

「ここからが本番だ。ベースラインを超えるモデルを自分の手で作り上げてくれ。」

田中VPoEが目標値を示す。

「目標はAUC-ROC 0.85以上。特徴量エンジニアリングとチューニングの両面から攻めろ。改善のプロセスを記録することも忘れるな。」

ミッション概要

Telco Customer Churnデータセットで、AUC-ROC 0.85以上を達成する離反予測モデルを構築する。改善のプロセスを実験ログとして記録すること。


Mission 1: ベースラインの確立(20分)

ロジスティック回帰でベースラインを構築し、以下の指標を記録せよ。

  1. 前処理パイプラインの構築(Step 2の内容を実装)
  2. ロジスティック回帰(class_weight=‘balanced’)でベースライン
  3. AUC-ROC, PR-AUC, F1-Score, Recall を記録
  4. 混同行列を作成しビジネス観点で解釈
解答例
import pandas as pd
import numpy as np
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sklearn.metrics import roc_auc_score, average_precision_score, f1_score, recall_score, confusion_matrix

# データ読み込みと前処理
df = pd.read_csv('WA_Fn-UseC_-Telco-Customer-Churn.csv')
df['TotalCharges'] = pd.to_numeric(df['TotalCharges'], errors='coerce').fillna(0)
df = df.drop('customerID', axis=1)
df['Churn'] = (df['Churn'] == 'Yes').astype(int)

# エンコーディング
binary_cols = ['gender', 'Partner', 'Dependents', 'PhoneService', 'PaperlessBilling']
le = LabelEncoder()
for col in binary_cols:
    df[col] = le.fit_transform(df[col])

service_cols = ['MultipleLines', 'OnlineSecurity', 'OnlineBackup',
                'DeviceProtection', 'TechSupport', 'StreamingTV', 'StreamingMovies']
for col in service_cols:
    df[col] = df[col].replace({'No internet service': 'No', 'No phone service': 'No'})

multi_cols = service_cols + ['InternetService', 'Contract', 'PaymentMethod']
df = pd.get_dummies(df, columns=multi_cols, drop_first=True)

# 分割
X = df.drop('Churn', axis=1)
y = df['Churn']
X_train, X_temp, y_train, y_temp = train_test_split(X, y, test_size=0.4, random_state=42, stratify=y)
X_val, X_test, y_val, y_test = train_test_split(X_temp, y_temp, test_size=0.5, random_state=42, stratify=y_temp)

# スケーリング
numeric_cols = ['tenure', 'MonthlyCharges', 'TotalCharges']
scaler = StandardScaler()
X_train[numeric_cols] = scaler.fit_transform(X_train[numeric_cols])
X_val[numeric_cols] = scaler.transform(X_val[numeric_cols])
X_test[numeric_cols] = scaler.transform(X_test[numeric_cols])

# ベースラインモデル
baseline = LogisticRegression(max_iter=1000, class_weight='balanced', random_state=42)
baseline.fit(X_train, y_train)

y_proba = baseline.predict_proba(X_val)[:, 1]
y_pred = baseline.predict(X_val)

# 実験ログ
experiment_log = [{
    'Experiment': 'Baseline (LR)',
    'AUC-ROC': roc_auc_score(y_val, y_proba),
    'PR-AUC': average_precision_score(y_val, y_proba),
    'F1': f1_score(y_val, y_pred),
    'Recall': recall_score(y_val, y_pred),
}]
print(pd.DataFrame(experiment_log).to_string(index=False))
print(f"\n混同行列:\n{confusion_matrix(y_val, y_pred)}")

Mission 2: 特徴量エンジニアリング + モデル改善(40分)

以下のステップでモデルを段階的に改善せよ。各ステップでスコアを記録すること。

  1. Step A: ランダムフォレスト / XGBoost / LightGBM でモデル比較
  2. Step B: 特徴量エンジニアリング(最低5つの新特徴量を追加)
  3. Step C: Optunaで最良モデルのチューニング(50試行以上)
  4. 各ステップの実験ログをテーブルにまとめる
解答例
from sklearn.ensemble import RandomForestClassifier
from xgboost import XGBClassifier
from lightgbm import LGBMClassifier
import optuna

# Step A: モデル比較
models = {
    'Random Forest': RandomForestClassifier(n_estimators=100, class_weight='balanced', random_state=42),
    'XGBoost': XGBClassifier(n_estimators=100, scale_pos_weight=2.77, random_state=42, eval_metric='auc', use_label_encoder=False),
    'LightGBM': LGBMClassifier(n_estimators=100, is_unbalance=True, random_state=42, verbose=-1),
}

for name, model in models.items():
    model.fit(X_train, y_train)
    y_proba = model.predict_proba(X_val)[:, 1]
    y_pred = model.predict(X_val)
    experiment_log.append({
        'Experiment': f'Step A: {name}',
        'AUC-ROC': roc_auc_score(y_val, y_proba),
        'PR-AUC': average_precision_score(y_val, y_proba),
        'F1': f1_score(y_val, y_pred),
        'Recall': recall_score(y_val, y_pred),
    })

# Step B: 特徴量エンジニアリング(元データに対して実施後、再分割)
df_fe = pd.read_csv('WA_Fn-UseC_-Telco-Customer-Churn.csv')
df_fe['TotalCharges'] = pd.to_numeric(df_fe['TotalCharges'], errors='coerce').fillna(0)

# 新特徴量
df_fe['num_services'] = df_fe[['OnlineSecurity','OnlineBackup','DeviceProtection',
                                'TechSupport','StreamingTV','StreamingMovies']].apply(
    lambda row: (row=='Yes').sum(), axis=1)
df_fe['is_new_customer'] = (df_fe['tenure'] <= 6).astype(int)
df_fe['is_loyal'] = (df_fe['tenure'] >= 48).astype(int)
df_fe['charge_per_tenure'] = np.where(df_fe['tenure']>0, df_fe['MonthlyCharges']/df_fe['tenure'], df_fe['MonthlyCharges'])
df_fe['high_risk'] = ((df_fe['Contract']=='Month-to-month') & (df_fe['InternetService']=='Fiber optic') & (df_fe['tenure']<=12)).astype(int)
# ... 前処理と分割を実施 ...

# Step C: Optunaチューニング
def objective(trial):
    params = {
        'n_estimators': trial.suggest_int('n_estimators', 100, 500),
        'max_depth': trial.suggest_int('max_depth', 3, 10),
        'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.3, log=True),
        'num_leaves': trial.suggest_int('num_leaves', 20, 150),
        'subsample': trial.suggest_float('subsample', 0.6, 1.0),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.6, 1.0),
        'is_unbalance': True, 'random_state': 42, 'verbose': -1,
    }
    model = LGBMClassifier(**params)
    from sklearn.model_selection import cross_val_score, StratifiedKFold
    cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
    return cross_val_score(model, X_train_fe, y_train, cv=cv, scoring='roc_auc').mean()

study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=50)
print(f"Best AUC-ROC (CV): {study.best_value:.4f}")

# 実験ログ出力
print(pd.DataFrame(experiment_log).to_string(index=False))

Mission 3: 最終モデル評価と分析レポート(30分)

最良モデルをテストデータで評価し、以下を含む分析レポートを作成せよ。

  1. 最終モデルのテストデータでのスコア(AUC-ROC, PR-AUC, F1, Recall)
  2. ROC曲線とPR曲線のプロット
  3. 特徴量重要度のTop 10
  4. 過学習の有無の確認(訓練/テストのスコア差)
  5. 全実験の比較表と、改善に最も寄与した要因の考察
解答例
from sklearn.metrics import roc_curve, precision_recall_curve
import matplotlib.pyplot as plt

# 最終モデル
final_model = LGBMClassifier(**study.best_params, is_unbalance=True, random_state=42, verbose=-1)
final_model.fit(X_train_fe, y_train)

# テストデータ評価
y_test_proba = final_model.predict_proba(X_test_fe)[:, 1]
y_test_pred = final_model.predict(X_test_fe)

print("=== 最終モデル(テストデータ)===")
print(f"AUC-ROC: {roc_auc_score(y_test, y_test_proba):.4f}")
print(f"PR-AUC:  {average_precision_score(y_test, y_test_proba):.4f}")
print(f"F1:      {f1_score(y_test, y_test_pred):.4f}")
print(f"Recall:  {recall_score(y_test, y_test_pred):.4f}")

# ROC曲線
fpr, tpr, _ = roc_curve(y_test, y_test_proba)
plt.figure(figsize=(12, 5))
plt.subplot(1, 2, 1)
plt.plot(fpr, tpr, 'b-', label=f'AUC={roc_auc_score(y_test, y_test_proba):.3f}')
plt.plot([0,1], [0,1], 'k--')
plt.xlabel('FPR'); plt.ylabel('TPR'); plt.title('ROC Curve'); plt.legend()

# PR曲線
precision, recall, _ = precision_recall_curve(y_test, y_test_proba)
plt.subplot(1, 2, 2)
plt.plot(recall, precision, 'r-', label=f'PR-AUC={average_precision_score(y_test, y_test_proba):.3f}')
plt.xlabel('Recall'); plt.ylabel('Precision'); plt.title('PR Curve'); plt.legend()
plt.tight_layout()
plt.savefig('final_model_curves.png', dpi=150)

# 特徴量重要度
fi = pd.DataFrame({'feature': X_train_fe.columns, 'importance': final_model.feature_importances_})
fi = fi.sort_values('importance', ascending=False)
print(f"\nTop 10 特徴量:\n{fi.head(10).to_string(index=False)}")

# 過学習チェック
train_auc = roc_auc_score(y_train, final_model.predict_proba(X_train_fe)[:, 1])
print(f"\n訓練AUC: {train_auc:.4f}, テストAUC: {roc_auc_score(y_test, y_test_proba):.4f}")
print(f"差分: {train_auc - roc_auc_score(y_test, y_test_proba):.4f}")

# 全実験の比較表
print(f"\n=== 実験比較 ===")
print(pd.DataFrame(experiment_log).to_string(index=False))

達成度チェック

  • ベースラインモデル(AUC-ROC 0.82-0.84)を構築できた
  • 3つ以上のモデルを比較し最良モデルを選定した
  • 5つ以上の新特徴量を追加し効果を測定した
  • Optunaでハイパーパラメータを最適化した
  • 最終モデルのAUC-ROC 0.85以上を達成した
  • 過学習の有無を確認した
  • 実験ログを記録し改善プロセスを説明できる

推定所要時間: 90分