Day 084 — Playground Series 活用法 — 月次コンペでBronzeメダルを取る戦略

2026-07-04 水 / Phase 3 戦略 Playground Series / LightGBM / OOF / ROC AUC / Bronzeメダル

📚 背景知識(読んでから問題へ)

🏅
Day 083 でTitanicのアンサンブルが完成しました。次の目標はKaggle Playground Series(PS)という月次コンペでメダルを獲得することです。

Playground Series とは?

Kaggle が毎月開催する「練習用コンペ」です。実際の業務データではなく、合成データ(Synthetic Data)を使用するため、初心者でもメダルを狙いやすい特徴があります。

項目 詳細
開催頻度毎月(Season 5 = 2025〜2026年)
データ種類合成データ(実データから生成 → 分布が安定)
Bronzeメダル上位40%(初心者でも狙える!)
Silverメダル上位20%
Goldメダル上位1%(Expert〜Master層の目標)
評価指標例AUC, RMSE, F1スコア, Accuracy
💡
なぜ Playground Series がおすすめか?
合成データは実データより分布が綺麗で欠損も少なく、CV≈LBの精度が高い(作った評価が本番でもほぼ同じスコアになる)。Discussion で上位ノートブックが共有されるため学習材料も豊富です。

Bronze メダルを取る3ステップ戦略

① EDA + ベースライン提出(1時間以内)
② 上位ノートブックの特徴量を取り込む
③ アンサンブルでスコア底上げ

🗂️ データスキーマ — Bank Customer Churn

今月のPlayground Series: 顧客離脱予測(評価指標: ROC AUC)

カラム名 説明 使い方
idint提出用顧客ID識別子のみ(学習に使わない)
CustomerIdint顧客番号識別子のみ(学習に使わない)
Surnamestr苗字通常は使わない(ただしレアケースで有効な場合も)
CreditScoreint信用スコア 300〜850数値特徴量として使用
Geographystr国(France/Germany/Spain)OneHot or Label Encoding
Genderstr性別Label Encoding (Male=0/Female=1)
Ageint年齢重要な特徴量(高年齢→離脱傾向)
Tenureint在籍年数 0〜10数値特徴量として使用
Balancefloat口座残高BalanceSalaryRatio など派生特徴量が有効
NumOfProductsint保有商品数 1〜4カテゴリとして扱うことも有効
HasCrCardintクレカ保有 0/1バイナリ特徴量
IsActiveMemberintアクティブ会員 0/1非アクティブは離脱の予兆
EstimatedSalaryfloat推定年収Balance との比率が有効
Exitedint離脱フラグ(目的変数)0=継続, 1=離脱

🎯 問題

⚠️
このコンペの評価指標は ROC AUC です。予測クラス(0/1)ではなく、生存確率(0〜1の実数)を提出してください。

要件

1
ROC AUC を最大化する特徴量エンジニアリングを3つ以上考案する
数値変数の比率・ゼロフラグ・交互作用などを活用
2
最適なモデル選択の根拠を説明する
なぜ LightGBM が AUC 評価のコンペで強いのかを理解する
3
CV と LB の乖離を防ぐ手法を3つ挙げる
4
LightGBM + Stratified K-Fold CV(k=5)の実装
OOF予測でCV AUC を計算 → submission.csv 生成
5
Bronzeメダル獲得のスコア目標を設定する

📊 AUCスコア目標(Bank Churn PS 参考値)

参考: Playground Series のChurn系コンペでの典型的なスコア分布

ランダムベースライン
AUC 0.500
0.500
基準
LR ベースライン
≈ 0.760
≈ 0.760
参加のみ
LightGBM 基本設定
≈ 0.840
≈ 0.840
Bronze圏
LGB + 特徴量工学
≈ 0.880
≈ 0.880
Silver圏
LGB + Optuna + Stacking
≈ 0.910
≈ 0.910
Gold圏
🥉
今日の目標: CV AUC ≈ 0.875 を達成してBronzeメダル圏内に入る。特徴量エンジニアリングを丁寧に行うだけで LR の 0.760 から 0.840〜0.880 まで引き上げられます。

💡 ヒント

ヒント 1 方向性

Playground Series の合成データは「元の実データ」の分布を模倣して生成されます。実データで有効だった特徴量エンジニアリングは PS でも有効なことが多いです。

Churn 予測では以下のパターンが特に効きやすい:

  • 残高ゼロフラグ(Balance == 0 は解約の前兆)
  • 高年齢 × 非アクティブの組み合わせ
  • 保有商品数 == 1 かつ非アクティブ
ヒント 2 アプローチ

ROC AUC は分類の「閾値非依存」の指標です。予測確率の精度が高いほど AUC が上がります。LightGBM は predict_proba の確率精度が高いため AUC 評価のコンペで強いモデルです。

early_stopping を使うと最適な n_estimators を自動で見つけられます。最初は n_estimators=1000 と大きく設定しておけばOKです。

ヒント 3 コード骨格
import lightgbm as lgb
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import roc_auc_score

cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
oof_preds  = np.zeros(len(X))
test_preds = np.zeros(len(X_test))

for fold, (tr_idx, val_idx) in enumerate(cv.split(X, y)):
    X_tr, X_val = X.iloc[tr_idx], X.iloc[val_idx]
    y_tr, y_val = y.iloc[tr_idx], y.iloc[val_idx]

    model = lgb.LGBMClassifier(n_estimators=1000, learning_rate=0.05)
    model.fit(X_tr, y_tr,
              eval_set=[(X_val, y_val)],
              callbacks=[lgb.early_stopping(50, verbose=False)])

    oof_preds[val_idx] = model.predict_proba(X_val)[:, 1]
    test_preds += model.predict_proba(X_test)[:, 1] / 5

print(f"CV AUC: {roc_auc_score(y, oof_preds):.5f}")

模範解答

import pandas as pd
import numpy as np
import lightgbm as lgb
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import roc_auc_score
from sklearn.preprocessing import LabelEncoder

# ── 1. データ読み込み ──────────────────────────────────────
train = pd.read_csv('train.csv')
test  = pd.read_csv('test.csv')

# ── 2. 特徴量エンジニアリング ─────────────────────────────
def engineer_features(df):
    df = df.copy()

    # 残高比率: 財務状況の相対的な評価
    df['BalanceSalaryRatio'] = df['Balance'] / (df['EstimatedSalary'] + 1)

    # 残高ゼロフラグ(0残高は解約の前兆かもしれない)
    df['ZeroBalance'] = (df['Balance'] == 0).astype(int)

    # 年齢 × アクティブ度(高年齢かつ非アクティブが離脱しやすい)
    df['AgeActivity'] = df['Age'] * df['IsActiveMember']

    # 信用スコアのバケット化(低/中/高/優)
    df['CreditBucket'] = pd.cut(df['CreditScore'],
                                bins=[0, 450, 600, 750, 1000],
                                labels=[0, 1, 2, 3]).astype(int)

    # 商品数 × アクティブ度
    df['ProductActivity'] = df['NumOfProducts'] * df['IsActiveMember']

    return df

train = engineer_features(train)
test  = engineer_features(test)

# ── 3. カテゴリ変数エンコーディング ──────────────────────
cat_cols = ['Geography', 'Gender']
for col in cat_cols:
    le = LabelEncoder()
    train[col] = le.fit_transform(train[col])
    test[col]  = le.transform(test[col])

# ── 4. 特徴量選択 ─────────────────────────────────────────
drop_cols = ['id', 'CustomerId', 'Surname', 'Exited']
feature_cols = [c for c in train.columns if c not in drop_cols]

X      = train[feature_cols]
y      = train['Exited']
X_test = test[feature_cols]

# ── 5. LightGBM + Stratified K-Fold CV ──────────────────
cv          = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
oof_preds   = np.zeros(len(X))
test_preds  = np.zeros(len(X_test))

lgb_params = {
    'n_estimators':       1000,
    'learning_rate':      0.05,
    'num_leaves':         31,
    'max_depth':          -1,
    'min_child_samples':  20,
    'subsample':          0.8,
    'colsample_bytree':   0.8,
    'reg_alpha':          0.1,
    'reg_lambda':         1.0,
    'random_state':       42,
    'n_jobs':             -1,
    'verbose':            -1,
}

for fold, (tr_idx, val_idx) in enumerate(cv.split(X, y)):
    X_tr, X_val = X.iloc[tr_idx], X.iloc[val_idx]
    y_tr, y_val = y.iloc[tr_idx], y.iloc[val_idx]

    model = lgb.LGBMClassifier(**lgb_params)
    model.fit(
        X_tr, y_tr,
        eval_set=[(X_val, y_val)],
        callbacks=[lgb.early_stopping(50, verbose=False),
                   lgb.log_evaluation(-1)],
    )

    oof_preds[val_idx] = model.predict_proba(X_val)[:, 1]
    test_preds += model.predict_proba(X_test)[:, 1] / 5

    fold_auc = roc_auc_score(y_val, oof_preds[val_idx])
    print(f"  Fold {fold+1}: AUC = {fold_auc:.5f}  (best iter: {model.best_iteration_})")

cv_auc = roc_auc_score(y, oof_preds)
print(f"\nCV AUC: {cv_auc:.5f}")
print(f"Bronze 目標: AUC ≈ 0.875 (上位40%)")
print(f"Silver 目標: AUC ≈ 0.890 (上位20%)")

# ── 6. 提出ファイル生成 ───────────────────────────────────
submission = pd.DataFrame({
    'id':     test['id'],
    'Exited': test_preds,  # 確率を提出(クラスではない!)
})
submission.to_csv('submission.csv', index=False)
print("\nsubmission.csv を生成しました!")

🪜 Step-by-Step 解説

1 特徴量エンジニアリング:比率と交互作用

# 残高比率: 財務状況の相対的な評価
df['BalanceSalaryRatio'] = df['Balance'] / (df['EstimatedSalary'] + 1)
# 年齢 × アクティブ度(高年齢かつ非アクティブが離脱しやすい)
df['AgeActivity'] = df['Age'] * df['IsActiveMember']

「残高÷給与」という比率は、残高そのものより顧客の財務状況を表します。絶対値より比率・割合が特徴量として強いのはKaggleの鉄則です。

💡
特徴量エンジニアリングのアイデア源: コンペのDiscussionで「EDA」や「feature importance」を含む投稿を探す。公開ノートブックのコードを読んで、自分のコードに取り込む(改良しながら)のが最速です。

2 LightGBM を選ぶ理由

モデルAUC得意?理由
LightGBM非常に得意leaf-wise 成長で高精度な確率を出せる
XGBoost得意depth-wise だが正則化が強力
CatBoost得意カテゴリ変数が多いデータで強い
ロジスティック回帰普通線形モデルなので非線形な関係を捉えにくい

3 OOF(Out-of-Fold)予測の仕組み

oof_preds[val_idx] = model.predict_proba(X_val)[:, 1]

OOFとは「その折で検証に使ったデータへの予測をつなぎ合わせたもの」です。全データについて「学習に使っていないときの予測」を集めるため、情報リークのない正確なCV評価ができます。

Fold 1 の様子: 20%をvalidationとして「学習なし状態」で予測

Fold1: VAL Fold2: TR Fold3: TR Fold4: TR Fold5: TR ← OOF予測を書き込む

4 テスト予測を5つのモデルで平均

test_preds += model.predict_proba(X_test)[:, 1] / 5

各Foldで学習した5つのモデルの予測を平均(アンサンブル)することで、単一モデルより安定した予測ができます。これをOOF + テスト平均と呼びます。

5 CV と LB の乖離を防ぐ3つの手法

#手法説明
Pipeline を使って CV する前処理を CV ループの中で行い情報リークを防ぐ
Stratified K-Fold を使う各 Fold で離脱率(正例比率)を均等に保つ
識別子(ID)を学習から除外するid/CustomerId は目的変数と無関係のため必ず除く

📅 Playground Series 参戦タイムライン(1ヶ月間)

Day 1-2(コンペ開始直後)
EDA + 簡単なベースライン提出
データ形状・型・欠損を確認。LR or LGB のデフォルト設定でまず1回提出してLBを確認する。
Day 3-7(改善フェーズ)
上位ノートブックの解法を読んで特徴量を改善
Discussion の上位投稿・公開ノートブックをチェック。有望な特徴量アイデアを自分のコードに組み込む。
Day 14-21(最適化フェーズ)
Optuna でハイパーパラメータ最適化
num_leaves, learning_rate, reg_alpha などを Optuna で自動探索。CV AUC を最大化する。
Day 25-最終日(仕上げ)
アンサンブル + 提出モデルの選定
LGB + XGB + CatBoost の OOF アンサンブル。提出は「CV高い」と「LB高い」2つを Bookmark しておく。
最終日(締め切り)
CV 重視の提出を優先して選択
PS は Public LB が shake-up しやすい。CV が高い提出が Private LB で逆転することが多い。

📐 数学・統計の補足(文系向け)— ROC AUC とは何か?

AUC(Area Under the Curve)は「ランダムな正例と負例を比べたとき、正例の方が高いスコアになる確率」です。

AUC = 0.500(完全ランダム)
コインと同じ
0.500
AUC = 0.700(まずまず)
70%で正しく順位付け
0.700
AUC = 0.875(Bronze目標)
87.5%で正しく順位付け
0.875
AUC = 0.900(優秀)
90%で正しく順位付け
0.900
AUC = 1.000(完璧→リーク疑い)
完全な分類(実運用では非現実的)
1.000
⚠️
AUC が 0.99 や 1.0 に近い場合はデータリークを強く疑うこと。ID や日付が目的変数と相関している可能性があります。

🏆 Kaggleでの実践的な使い方

状況戦略
Day 1 提出EDA + デフォルト LGB → LBの位置を確認
Discussion 活用「EDA」「Feature Engineering」タグの投稿を重点確認
スコア停滞時特徴量を削って過学習を疑う / CV と LB の乖離を確認
最終提出選択CV 重視 1 + LB 重視 1 の2つを Selected Submission に設定
メダル圏外の場合Discussion で上位解法を読んで次回に活かす(学習が最優先)

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
AUC ≈ 1.0 が出た → 良いモデルだ! データリークを見落とす ID やタイムスタンプが混入している可能性を必ず疑う
CV が良いのに LB スコアが悪い 過学習 or 分布の違い 特徴量の数を減らす・正則化を強める
predict で 0/1 を提出してしまった AUC は確率が必要と知らない AUC 評価は predict_proba()[:, 1] を提出する
Public LB を信じて提出を選ぶ LB の shake-up を考慮しない Private LB では CV スコアが強い提出が逆転しやすい
全データをシャッフルしてから時系列で CV 時系列データに不適切な CV 時系列コンペは time-based split を使う(PS では通常不要)

🚀 次のステップ

📈
発展: XGBoost + LightGBM + CatBoost の3モデルOOFアンサンブル(Level-2 Stacking)でスコアをさらに底上げする
次回予告: House Prices EDA — 回帰コンペでの特徴量工学を実践(RMSE最小化)

📝 自己評価(解いた後に記入)

自分の回答・実装メモ:

気づき・メモ: