LESSON

演習:ML問題を設計しよう

田中VPoE:「さて、ここまで学んだ知識を使って、実際に NetShop の離反予測を ML 問題として設計してみよう。コードを書く前に、問題設定を明確にすることが成功の第一歩だ。」

あなた:「問題定義からデータ分割、評価指標の選定まで、一通りやってみます。」

田中VPoE:「良い心構えだ。実務では、ここの設計が甘いままモデル構築に突入して、後から手戻りになるケースが多い。しっかり取り組んでくれ。」

ミッション概要

NetShop 社の顧客離反予測プロジェクトの ML 問題設計を行います。ビジネス課題の理解から、問題の定式化、データ分割、評価指標の選定までを実践します。


Mission 1: ビジネス課題を ML 問題に変換する

NetShop 社のビジネス課題を整理し、ML 問題として定式化してください。

要件

  1. 以下のビジネス背景を読み、ML 問題として定義する
    • NetShop 社は EC サイトを運営
    • 月間アクティブユーザー 50,000 人
    • 直近の月次離反率は約 8%
    • 離反した顧客の再獲得コストは、維持コストの 5 倍
  2. 以下を明確に定義する
    • 離反の定義(何をもって「離反」とするか)
    • 予測対象期間
    • 予測のタイミングと頻度
    • 利用可能な特徴量の候補
    • 予測結果の活用方法
# ヒント: 問題定義のテンプレート
problem_definition = {
    "business_goal": "顧客離反を事前に予測し、防止施策を打つ",
    "ml_task": "???",  # 分類 or 回帰
    "target_variable": "???",  # 何を予測するか
    "prediction_window": "???",  # 何日先まで予測するか
    "features_available": ["???"],  # 使える特徴量
    "evaluation_metric": "???",  # 主要な評価指標
    "success_criteria": "???",  # 成功基準
}
解答例
problem_definition = {
    "business_goal": "顧客離反を事前に予測し、防止施策を打つ",
    "ml_task": "二値分類(Binary Classification)",
    "target_variable": "30日以内に離反するか(1: 離反, 0: 継続)",
    "churn_definition": "30日以上ログインおよび購入がない状態",
    "prediction_window": "30日先",
    "prediction_timing": "毎日バッチ実行",
    "features_available": [
        "過去90日の購入回数",
        "過去90日の平均購入額",
        "最終ログインからの経過日数",
        "最終購入からの経過日数",
        "サポート問い合わせ回数",
        "会員期間(月数)",
        "利用デバイス",
        "過去30日のページビュー数",
        "クーポン利用回数",
        "お気に入り登録数",
    ],
    "evaluation_metric": "F1スコア(再現率を重視)",
    "success_criteria": "F1スコア 0.75 以上、再現率 0.80 以上",
    "action_plan": "離反確率が閾値以上の顧客にクーポン配布・フォローメール送信",
}

print("ML問題定義:")
for key, value in problem_definition.items():
    print(f"  {key}: {value}")

Mission 2: データを適切に分割する

サンプルデータを使って、学習・検証・テストの3分割を行ってください。

要件

  1. 不均衡データ(離反率 8%)を生成する
  2. 層化分割(stratify)を使ってデータを分割する
  3. 各データセットの離反率が維持されていることを確認する
  4. 分割の妥当性を検証する
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split

# サンプルデータ生成
np.random.seed(42)
n_samples = 5000
churn_rate = 0.08

# ここにデータ分割のコードを書く
解答例
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split

np.random.seed(42)
n_samples = 5000
churn_rate = 0.08

# サンプルデータ生成
n_churned = int(n_samples * churn_rate)
n_active = n_samples - n_churned

y = np.array([0] * n_active + [1] * n_churned)
X = np.random.randn(n_samples, 10)  # 10個の特徴量

# シャッフル
shuffle_idx = np.random.permutation(n_samples)
X, y = X[shuffle_idx], y[shuffle_idx]

print(f"全データ: {len(y)} 件, 離反率: {y.mean():.3f}")

# Step 1: テストデータを分離(20%)
X_temp, X_test, y_temp, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

# Step 2: 学習データと検証データに分割
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"\n--- データ分割結果 ---")
print(f"学習データ: {len(y_train)} 件 ({len(y_train)/len(y):.0%}), 離反率: {y_train.mean():.3f}")
print(f"検証データ: {len(y_val)} 件 ({len(y_val)/len(y):.0%}), 離反率: {y_val.mean():.3f}")
print(f"テストデータ: {len(y_test)} 件 ({len(y_test)/len(y):.0%}), 離反率: {y_test.mean():.3f}")

# 分割の妥当性検証
print(f"\n--- 妥当性検証 ---")
print(f"各セットの離反率の差が小さいことを確認:")
rates = [y_train.mean(), y_val.mean(), y_test.mean()]
print(f"  最大差: {max(rates) - min(rates):.4f}")
print(f"  → {'OK: 十分小さい' if max(rates) - min(rates) < 0.01 else 'NG: 偏りあり'}")

Mission 3: 評価指標を比較する

ダミーの予測結果を使って、各評価指標を計算し、指標ごとの特徴を理解してください。

要件

  1. 3つの異なるモデルの予測結果(ダミー)を作成する
    • モデルA: 高い適合率、低い再現率
    • モデルB: 低い適合率、高い再現率
    • モデルC: バランス型
  2. 各モデルについて、正解率・適合率・再現率・F1スコアを計算する
  3. NetShop の離反予測に最適なモデルを選び、理由を述べる
from sklearn.metrics import (
    accuracy_score, precision_score, recall_score, f1_score,
    confusion_matrix, classification_report
)

# ここに評価指標の比較コードを書く
解答例
import numpy as np
from sklearn.metrics import (
    accuracy_score, precision_score, recall_score, f1_score,
    classification_report
)

np.random.seed(42)

# 実際のラベル(離反率10%)
n = 1000
y_true = np.array([0]*900 + [1]*100)

# モデルA: 高適合率・低再現率(慎重に予測)
y_pred_A = np.zeros(n, dtype=int)
y_pred_A[900:930] = 1  # 30件だけ離反と予測、うち28件が正解

# モデルB: 低適合率・高再現率(積極的に予測)
y_pred_B = np.zeros(n, dtype=int)
y_pred_B[700:] = 1  # 300件を離反と予測、離反者はほぼ網羅

# モデルC: バランス型
y_pred_C = np.zeros(n, dtype=int)
y_pred_C[850:] = 1  # 150件を離反と予測

models = {
    "モデルA(高適合率型)": y_pred_A,
    "モデルB(高再現率型)": y_pred_B,
    "モデルC(バランス型)": y_pred_C,
}

print("=" * 60)
for name, y_pred in models.items():
    print(f"\n{name}")
    print("-" * 40)
    print(f"  正解率:  {accuracy_score(y_true, y_pred):.3f}")
    print(f"  適合率:  {precision_score(y_true, y_pred):.3f}")
    print(f"  再現率:  {recall_score(y_true, y_pred):.3f}")
    print(f"  F1スコア: {f1_score(y_true, y_pred):.3f}")

print("\n" + "=" * 60)
print("\n【結論】")
print("NetShop の離反予測では、離反者の見逃し(FN)が")
print("大きな損失につながるため、再現率を重視すべき。")
print("ただし適合率が極端に低いとクーポンコストが膨らむため、")
print("モデルC(バランス型)をベースに、閾値調整で")
print("再現率を高めるアプローチが推奨される。")

達成度チェック

  • ビジネス課題を ML 問題として定式化できた
  • 離反の定義、予測対象、利用特徴量を明確にした
  • データを3分割し、層化分割の効果を確認した
  • 複数の評価指標を計算し、特徴を理解した
  • NetShop に適した評価指標を選択し、理由を述べた

推定所要時間: 60分