LESSON

演習:離反予測モデルを構築しよう

田中VPoE:「さあ、ここまで学んだモデルを実際に構築して比較してみよう。NetShop の離反予測データに対して、ロジスティック回帰、ランダムフォレスト、勾配ブースティングの3つのモデルを構築し、最適なモデルを選定してくれ。」

あなた:「データ準備から前処理、モデル構築、比較まで一通りやるんですね。」

田中VPoE:「そうだ。Step 2 で学んだ前処理パイプラインも組み合わせて、再現可能な形で構築してほしい。」

ミッション概要

NetShop の顧客離反予測のためのモデルを複数構築し、比較・選定します。前処理からモデル構築・評価までの一連のパイプラインを実装してください。


Mission 1: データ準備と前処理パイプラインの構築

リアルに近い離反予測データを生成し、前処理パイプラインを構築してください。

要件

  1. 5000件の顧客データを生成(離反率8%)
  2. 数値特徴量とカテゴリカル特徴量を含める
  3. 学習/検証/テストの3分割を行う
  4. ColumnTransformer で前処理パイプラインを構築する
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder, OrdinalEncoder
from sklearn.impute import SimpleImputer

np.random.seed(42)
# ここにデータ生成と前処理パイプラインのコードを書く
解答例
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.pipeline import Pipeline
from sklearn.compose import ColumnTransformer
from sklearn.preprocessing import StandardScaler, OneHotEncoder, OrdinalEncoder
from sklearn.impute import SimpleImputer

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]),
    'payment_method': 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)

# 特徴量生成
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.loc[np.random.random(n) < 0.03, 'avg_amount'] = np.nan
df.loc[np.random.random(n) < 0.02, 'page_views'] = np.nan

print(f"データ件数: {len(df)}, 離反率: {df['is_churned'].mean():.1%}")

# 特徴量とラベル分離
numeric_features = ['purchase_count', 'avg_amount', 'days_inactive', 'page_views',
                    'support_tickets', 'membership_months', 'purchase_per_view',
                    'support_per_purchase', 'log_amount']
onehot_features = ['device_type', 'payment_method']
ordinal_features = ['membership_rank']

feature_cols = numeric_features + onehot_features + ordinal_features
X = df[feature_cols]
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)

print(f"学習: {len(X_train)}, 検証: {len(X_val)}, テスト: {len(X_test)}")

# 前処理パイプライン
preprocessor = ColumnTransformer([
    ('num', Pipeline([
        ('imputer', SimpleImputer(strategy='median')),
        ('scaler', StandardScaler()),
    ]), numeric_features),
    ('onehot', Pipeline([
        ('imputer', SimpleImputer(strategy='most_frequent')),
        ('encoder', OneHotEncoder(drop='first', sparse_output=False)),
    ]), onehot_features),
    ('ordinal', Pipeline([
        ('imputer', SimpleImputer(strategy='most_frequent')),
        ('encoder', OrdinalEncoder(categories=[['ブロンズ', 'シルバー', 'ゴールド']])),
    ]), ordinal_features),
])

Mission 2: 3つのモデルを構築し比較する

ロジスティック回帰、ランダムフォレスト、LightGBM の3モデルを構築し、交差検証で比較してください。

要件

  1. 各モデルを Pipeline に組み込む
  2. 5-fold 交差検証で F1スコアを計算する
  3. 学習時間も記録する
  4. 結果を表形式で比較する
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
import lightgbm as lgb
from sklearn.model_selection import cross_val_score
import time

# ここにモデル構築と比較のコードを書く
解答例
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
import lightgbm as lgb_module
from sklearn.model_selection import cross_val_score
from sklearn.metrics import f1_score, classification_report
import time

models = {
    'ロジスティック回帰': Pipeline([
        ('preprocessor', preprocessor),
        ('model', LogisticRegression(max_iter=1000, random_state=42, C=1.0)),
    ]),
    'ランダムフォレスト': Pipeline([
        ('preprocessor', preprocessor),
        ('model', RandomForestClassifier(n_estimators=200, max_depth=10, random_state=42, n_jobs=-1)),
    ]),
    'LightGBM': Pipeline([
        ('preprocessor', preprocessor),
        ('model', lgb_module.LGBMClassifier(n_estimators=200, max_depth=6, learning_rate=0.1, random_state=42, verbose=-1)),
    ]),
}

results = []
for name, pipeline in models.items():
    start = time.time()
    scores = cross_val_score(pipeline, X_train, y_train, cv=5, scoring='f1')
    elapsed = time.time() - start

    # テストデータでの評価
    pipeline.fit(X_train, y_train)
    y_pred_test = pipeline.predict(X_test)
    test_f1 = f1_score(y_test, y_pred_test)

    results.append({
        'モデル': name,
        'CV F1(平均)': scores.mean(),
        'CV F1(標準偏差)': scores.std(),
        'テスト F1': test_f1,
        '学習時間(秒)': elapsed,
    })

    print(f"\n{'='*50}")
    print(f"{name}")
    print(f"{'='*50}")
    print(f"CV F1: {scores.mean():.3f} (+/- {scores.std():.3f})")
    print(f"テスト F1: {test_f1:.3f}")
    print(f"学習時間: {elapsed:.2f}秒")

# 比較表
comparison_df = pd.DataFrame(results)
print(f"\n{'='*60}")
print("モデル比較結果")
print(comparison_df.to_string(index=False))

Mission 3: 最適モデルの選定と詳細分析

最も精度が高いモデルを選び、詳細な分析を行ってください。

要件

  1. 最適モデルの混同行列を表示する
  2. 特徴量重要度を可視化する
  3. ROC曲線を描画する
  4. モデル選定の理由を3つ以上述べる
from sklearn.metrics import (
    confusion_matrix, ConfusionMatrixDisplay,
    roc_curve, roc_auc_score
)
import matplotlib.pyplot as plt

# ここに詳細分析のコードを書く
解答例
from sklearn.metrics import (
    confusion_matrix, ConfusionMatrixDisplay,
    roc_curve, roc_auc_score, classification_report
)
import matplotlib.pyplot as plt

# 最良モデル(LightGBM)を選定
best_pipeline = models['LightGBM']
best_pipeline.fit(X_train, y_train)

y_pred = best_pipeline.predict(X_test)
y_prob = best_pipeline.predict_proba(X_test)[:, 1]

# 1. 分類レポート
print("=== 分類レポート ===")
print(classification_report(y_test, y_pred, target_names=['継続', '離反']))

# 2. 混同行列
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
ConfusionMatrixDisplay.from_predictions(
    y_test, y_pred, display_labels=['継続', '離反'], ax=axes[0]
)
axes[0].set_title('混同行列')

# 3. ROC曲線
fpr, tpr, thresholds = roc_curve(y_test, y_prob)
auc = roc_auc_score(y_test, y_prob)
axes[1].plot(fpr, tpr, label=f'LightGBM (AUC = {auc:.3f})')
axes[1].plot([0, 1], [0, 1], 'k--')
axes[1].set_xlabel('偽陽性率')
axes[1].set_ylabel('真陽性率')
axes[1].set_title('ROC曲線')
axes[1].legend()
axes[1].grid(True)
plt.tight_layout()
plt.show()

# 4. 特徴量重要度
lgbm_model = best_pipeline.named_steps['model']
feature_names_out = best_pipeline.named_steps['preprocessor'].get_feature_names_out()
importance = pd.Series(lgbm_model.feature_importances_, index=feature_names_out)
importance = importance.sort_values(ascending=True)

plt.figure(figsize=(10, 8))
importance.tail(15).plot(kind='barh', color='steelblue')
plt.title('特徴量重要度 TOP 15(LightGBM)')
plt.xlabel('重要度')
plt.tight_layout()
plt.show()

# 5. 選定理由
print("\n=== モデル選定理由 ===")
print("1. F1スコアが3モデル中最も高い")
print("2. 学習速度が速く、定期的な再学習に適している")
print("3. 特徴量重要度が取得でき、ビジネスへの説明が可能")
print("4. 不均衡データへの対応パラメータが豊富")
print(f"5. AUC = {auc:.3f} と高い識別力を持つ")

達成度チェック

  • リアルに近い離反予測データを生成した
  • 前処理パイプラインを ColumnTransformer で構築した
  • 3つのモデルを構築し、交差検証で比較した
  • 最適モデルの混同行列と ROC 曲線を描画した
  • 特徴量重要度を可視化した
  • モデル選定の理由を論理的に述べた

推定所要時間: 90分