LESSON

演習:モデルの評価と改善を繰り返そう

田中VPoE:「モデルを作って終わりじゃない。評価して、弱点を見つけて、改善する。このサイクルを回すことで精度が上がっていく。今回は、ベースラインモデルから出発して、段階的に改善していく流れを体験してもらう。」

あなた:「どこまで改善できるか、挑戦してみます。」

田中VPoE:「良い意気込みだ。ただし、改善の記録をしっかり残すこと。どの施策がどれだけ効いたかを把握するのが、次のプロジェクトにも活きる。」

ミッション概要

NetShop の離反予測モデルを段階的に改善し、改善プロセスを記録します。ベースラインから出発し、特徴量改善、パラメータ最適化、閾値調整を順に適用して、最終的な精度向上を目指します。


Mission 1: ベースラインモデルを構築し、弱点を特定する

シンプルなモデルをベースラインとして構築し、その弱点を分析してください。

要件

  1. ロジスティック回帰でベースラインモデルを構築する
  2. 交差検証で F1 スコア、適合率、再現率を評価する
  3. 混同行列を確認し、FP と FN の傾向を分析する
  4. 改善の方向性を検討する
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_validate, StratifiedKFold
from sklearn.metrics import confusion_matrix, classification_report

# Step 3 で作成したデータとパイプラインを使用
# ここにベースラインモデルのコードを書く
解答例
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import cross_validate, StratifiedKFold, train_test_split
from sklearn.metrics import confusion_matrix, classification_report, f1_score
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.compose import ColumnTransformer
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import OneHotEncoder, OrdinalEncoder

# データ生成(Step 3 と同様)
np.random.seed(42)
n = 5000
df = pd.DataFrame({
    'purchase_count': np.random.poisson(8, n),
    'avg_amount': np.random.lognormal(8, 0.8, n).astype(int),
    'days_inactive': np.random.exponential(25, n).astype(int),
    'page_views': np.random.poisson(30, n),
    'support_tickets': np.random.poisson(1.5, n),
    'membership_months': np.random.exponential(18, n).astype(int),
    'device_type': np.random.choice(['PC', 'スマホ', 'タブレット'], n, p=[0.3, 0.55, 0.15]),
    'membership_rank': np.random.choice(['ブロンズ', 'シルバー', 'ゴールド'], n, p=[0.5, 0.3, 0.2]),
})
churn_prob = 1 / (1 + np.exp(-(df['days_inactive'] - 35) / 12 + df['purchase_count'] * 0.08 - df['support_tickets'] * 0.15))
df['is_churned'] = (np.random.random(n) < churn_prob).astype(int)

# 基本特徴量のみでベースライン
basic_features = ['purchase_count', 'avg_amount', 'days_inactive', 'page_views', 'support_tickets']
X = df[basic_features]
y = df['is_churned']

X_temp, X_test, y_temp, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
X_train, X_val, y_train, y_val = train_test_split(X_temp, y_temp, test_size=0.25, random_state=42, stratify=y_temp)

# ベースライン: ロジスティック回帰
baseline = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('scaler', StandardScaler()),
    ('model', LogisticRegression(max_iter=1000, random_state=42)),
])

# 交差検証
cv = StratifiedKFold(5, shuffle=True, random_state=42)
cv_results = cross_validate(
    baseline, X_train, y_train, cv=cv,
    scoring=['f1', 'precision', 'recall'],
    return_train_score=True,
)

print("=== ベースライン評価 ===")
for metric in ['f1', 'precision', 'recall']:
    train_m = cv_results[f'train_{metric}'].mean()
    val_m = cv_results[f'test_{metric}'].mean()
    print(f"{metric}: 学習={train_m:.3f}, 検証={val_m:.3f}, 差={train_m-val_m:.3f}")

# テストデータでの評価
baseline.fit(X_train, y_train)
y_pred = baseline.predict(X_test)
print(f"\nテストF1: {f1_score(y_test, y_pred):.3f}")
print(f"\n{classification_report(y_test, y_pred, target_names=['継続', '離反'])}")

# 改善記録
improvement_log = [{'ステップ': 'ベースライン', 'F1': f1_score(y_test, y_pred)}]

Mission 2: 特徴量改善とモデル変更で精度を上げる

ベースラインから特徴量とモデルを改善して精度を向上させてください。

要件

  1. ドメイン知識に基づく特徴量を追加する
  2. LightGBM に変更する
  3. 交差検証で改善効果を確認する
  4. 改善幅を記録する
import lightgbm as lgb

# ここに特徴量改善とモデル変更のコードを書く
解答例
import lightgbm as lgb_module

# 特徴量追加
df['purchase_per_view'] = df['purchase_count'] / df['page_views'].replace(0, 1)
df['support_per_purchase'] = df['support_tickets'] / df['purchase_count'].replace(0, 1)
df['log_amount'] = np.log1p(df['avg_amount'])
df['is_long_inactive'] = (df['days_inactive'] > 30).astype(int)
df['purchase_amount_interaction'] = df['purchase_count'] * df['avg_amount']

enhanced_features = basic_features + [
    'purchase_per_view', 'support_per_purchase', 'log_amount',
    'is_long_inactive', 'purchase_amount_interaction', 'membership_months',
]

X_enhanced = df[enhanced_features]
X_temp2, X_test2, y_temp2, y_test2 = train_test_split(X_enhanced, y, test_size=0.2, random_state=42, stratify=y)
X_train2, X_val2, y_train2, y_val2 = train_test_split(X_temp2, y_temp2, test_size=0.25, random_state=42, stratify=y_temp2)

# LightGBM で構築
enhanced_pipeline = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('model', lgb_module.LGBMClassifier(n_estimators=200, max_depth=6, learning_rate=0.1, random_state=42, verbose=-1)),
])

cv_results2 = cross_validate(
    enhanced_pipeline, X_train2, y_train2, cv=cv,
    scoring=['f1', 'precision', 'recall'],
    return_train_score=True,
)

print("=== 改善モデル評価 ===")
for metric in ['f1', 'precision', 'recall']:
    train_m = cv_results2[f'train_{metric}'].mean()
    val_m = cv_results2[f'test_{metric}'].mean()
    print(f"{metric}: 学習={train_m:.3f}, 検証={val_m:.3f}")

enhanced_pipeline.fit(X_train2, y_train2)
y_pred2 = enhanced_pipeline.predict(X_test2)
f1_enhanced = f1_score(y_test2, y_pred2)
print(f"\nテストF1: {f1_enhanced:.3f}")

improvement_log.append({'ステップ': '特徴量改善+LightGBM', 'F1': f1_enhanced})
print(f"改善幅: +{f1_enhanced - improvement_log[0]['F1']:.3f}")

Mission 3: ハイパーパラメータ最適化と閾値調整で仕上げる

Optuna でパラメータを最適化し、閾値調整で最終的なモデルを仕上げてください。

要件

  1. Optuna で LightGBM のパラメータを最適化する(20〜50 trials)
  2. 最適パラメータでモデルを再構築する
  3. 閾値を調整して F1 スコアを最大化する
  4. 改善の全体履歴をまとめる
import optuna

# ここにハイパーパラメータ最適化と閾値調整のコードを書く
解答例
import optuna
from sklearn.metrics import precision_score, recall_score

optuna.logging.set_verbosity(optuna.logging.WARNING)

def objective(trial):
    params = {
        'n_estimators': trial.suggest_int('n_estimators', 100, 500),
        'max_depth': trial.suggest_int('max_depth', 3, 12),
        'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.2, log=True),
        'num_leaves': trial.suggest_int('num_leaves', 10, 80),
        'subsample': trial.suggest_float('subsample', 0.6, 1.0),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.6, 1.0),
        'min_child_samples': trial.suggest_int('min_child_samples', 5, 30),
    }

    model = Pipeline([
        ('imputer', SimpleImputer(strategy='median')),
        ('model', lgb_module.LGBMClassifier(**params, random_state=42, verbose=-1)),
    ])

    scores = cross_val_score(model, X_train2, y_train2, cv=cv, scoring='f1')
    return scores.mean()

study = optuna.create_study(direction='maximize')
study.optimize(objective, n_trials=30)

print(f"最適パラメータ: {study.best_params}")
print(f"最適CV F1: {study.best_value:.3f}")

# 最適パラメータでモデル構築
best_model = Pipeline([
    ('imputer', SimpleImputer(strategy='median')),
    ('model', lgb_module.LGBMClassifier(**study.best_params, random_state=42, verbose=-1)),
])
best_model.fit(X_train2, y_train2)

# 閾値最適化
y_prob = best_model.predict_proba(X_test2)[:, 1]

best_f1 = 0
best_threshold = 0.5
for t in np.arange(0.1, 0.8, 0.02):
    y_pred_t = (y_prob >= t).astype(int)
    f1_t = f1_score(y_test2, y_pred_t)
    if f1_t > best_f1:
        best_f1 = f1_t
        best_threshold = t

print(f"\n最適閾値: {best_threshold:.2f}")
print(f"最終F1スコア: {best_f1:.3f}")

y_final = (y_prob >= best_threshold).astype(int)
print(f"\n{classification_report(y_test2, y_final, target_names=['継続', '離反'])}")

improvement_log.append({'ステップ': 'Optuna最適化+閾値調整', 'F1': best_f1})

# 改善履歴の表示
print("\n=== 改善履歴 ===")
log_df = pd.DataFrame(improvement_log)
for _, row in log_df.iterrows():
    print(f"{row['ステップ']:25s}: F1 = {row['F1']:.3f}")
print(f"\n総改善幅: +{improvement_log[-1]['F1'] - improvement_log[0]['F1']:.3f}")

達成度チェック

  • ベースラインモデルを構築し、弱点を特定した
  • 特徴量の追加とモデル変更で精度が向上した
  • Optuna でハイパーパラメータを最適化した
  • 閾値調整で F1 スコアを改善した
  • 改善の全体履歴を記録した
  • 各改善ステップの効果を定量的に把握した

推定所要時間: 90分