Day 089 — Phase 3 総復習 — Kaggleコンペで Bronze 圏内を目指す

2026-07-09 水 / Phase 3 分析 実戦パイプライン / CV / TargetEncoding / LightGBM / 提出

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

🏁
Phase 3 の最終回です。EDA・欠損値処理・特徴量工学・TargetEncoding・データリーク・CV・提出戦略・コンペ選定を、1本の「提出パイプライン」に統合します。

これは料理でいう「練習した包丁さばき・下ごしらえ・火加減を1皿のフルコースにまとめる」段階。個々の技術を知っていることと、締切までに動くパイプラインを組んで提出できることは別のスキルです。

💡
Bronze の正体: Bronze メダルは「天才的なモデル」ではなく、丁寧なパイプラインを最後まで走り切る力で取れます。今日は新理論はほぼ登場しません。バラバラの知識を1つの再現可能なスクリプトに束ねます。

🧭 Bronze を取る「型」(9ステップ)

上位陣も初心者も、最初のサブミッションは同じ型から始まる。

1 データ理解 shape/dtype/分布 2 EDA 変数×target 3 前処理 欠損/外れ値 4 特徴量工学 変換/エンコード 5 リーク防止 fold内fit 6 CV設計 Stratified KFold 7 モデル RF/LightGBM 8 提出 submission.csv 9 改善ループ CVとLBの乖離を見て回す → Trust your CV

🗄️ データ仕様(架空の二値分類コンペ)

項目内容
train.csv10,000 行 × 特徴量 12 個 + target(0/1)
test.csv5,000 行 × 特徴量 12 個(target なし)
数値特徴量8 個(一部に欠損あり)
カテゴリ特徴量4 個(うち 1 個は高カーディナリティ)
評価指標ROC-AUC
提出形式id,targettarget は 0〜1 の確率)
目標Bronze 圏内 = 上位 40%

🎯 問題

⚠️
あなたは Kaggle Contributor。Playground Series 相当の二値分類コンペで Bronze 圏内(上位40%)を目指し、以下を分析・実装します。

タスク1: EDA チェックリストの実行(分析)

1
「最初の30分でやるべき EDA」を7項目 列挙
各項目で「何を見て、何を判断するか」を述べる

タスク2: 一気通貫パイプラインの実装(コーディング)

1
欠損値処理(数値=中央値 / カテゴリ=最頻値・新カテゴリ)
2
高カーディナリティ変数への fold 内 TargetEncoding(リーク防止)
3
Stratified K-Fold (5-fold) で OOF 予測と test 予測を作る
4
LightGBM で学習し OOF AUC を表示
5
submission.csv を出力

タスク3: CV と LB の乖離診断(戦略)

📉
提出したら CV AUC = 0.842 / Public LB = 0.798 だった。この乖離をどう解釈し、次に何をするか。3つの仮説と対処を述べる。

📊 評価指標 ROC-AUC を可視化で理解する

AUC =「ランダムに選んだ陽性1件陰性1件で、陽性のほうに高いスコアを付けられる確率」。

AUC の値と「並べ替え能力」

AUC = 0.5(コイン投げ)
無意味
50%
AUC = 0.70(弱い)
やや使える
70%
AUC = 0.80(実戦水準)
良好
80%
AUC = 0.842(このCV)
◎ Bronze圏
84.2%
📐
AUC は「絶対値が合っているか」ではなく「順位(並び順)が合っているか」を測る。だから提出は確率 0〜1 のまま出せばよく、閾値を決める必要がない

💡 ヒント

ヒント1方向性

新しい手法は不要。「fold の外に情報を漏らさない」ことだけを最優先に、Phase 3 の各ピースを順番につなぐ。TargetEncoding とスケーリングは必ず fold 内の train でのみ fit する。

ヒント2アプローチ
  • CV: StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
  • TE: 各 fold で train から category→target平均 を計算し valid/test に適用。Smoothing で安定化
  • OOF 配列 oof = np.zeros(len(train)) に valid 予測を格納 → 最後に全体 AUC
  • test 予測は各 fold の平均(test_pred / n_splits
ヒント3コード骨格
import numpy as np, pandas as pd
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import roc_auc_score
import lightgbm as lgb

skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
oof = np.zeros(len(train))
test_pred = np.zeros(len(test))

for tr_idx, va_idx in skf.split(X, y):
    X_tr, X_va = X.iloc[tr_idx].copy(), X.iloc[va_idx].copy()
    y_tr, y_va = y.iloc[tr_idx], y.iloc[va_idx]
    # ★ここで TargetEncoding を X_tr から fit → X_va, test に transform
    model = lgb.LGBMClassifier(...)
    model.fit(X_tr, y_tr)
    oof[va_idx] = model.predict_proba(X_va)[:, 1]
    test_pred += model.predict_proba(test_X)[:, 1] / skf.n_splits

print("OOF AUC:", roc_auc_score(y, oof))

模範解答

タスク1: 最初の30分でやる EDA チェックリスト(7項目)

#見ること何を判断するか
1train.shape / test.shapeデータ量・train と test のサイズ比
2target.value_counts(normalize=True)クラス不均衡の有無(→ Stratified CV の要否)
3df.isna().mean()欠損率の高い列・欠損パターン
4df.describe() と歪度外れ値・対数変換すべき列
5df.nunique()高カーディナリティ変数の特定(→ TE候補)
6数値×target 相関 / カテゴリ×target 平均効きそうな特徴量の見当
7train と test の分布比較分布ズレ(covariate shift)・リークの兆候

タスク2: 一気通貫パイプライン

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

# 0. データ読み込み
train = pd.read_csv('train.csv')
test  = pd.read_csv('test.csv')
target_col, id_col = 'target', 'id'
y = train[target_col]
train_X = train.drop(columns=[target_col, id_col])
test_X  = test.drop(columns=[id_col])

num_cols  = train_X.select_dtypes(include='number').columns.tolist()
cat_cols  = train_X.select_dtypes(exclude='number').columns.tolist()
high_card = [c for c in cat_cols if train_X[c].nunique() > 20]
low_card  = [c for c in cat_cols if c not in high_card]

# 1. 欠損値処理(単純代入はリーク影響が小さいので全体でOK)
for c in num_cols:
    med = train_X[c].median()
    train_X[c] = train_X[c].fillna(med)
    test_X[c]  = test_X[c].fillna(med)
for c in cat_cols:
    train_X[c] = train_X[c].fillna('__MISSING__').astype(str)
    test_X[c]  = test_X[c].fillna('__MISSING__').astype(str)
for c in low_card:
    train_X[c] = train_X[c].astype('category')
    test_X[c]  = test_X[c].astype('category')

# 2. fold 内 TargetEncoding(Smoothing 付き・リーク防止)
def target_encode(tr, va, te, col, y_tr, m=10):
    prior = y_tr.mean()
    stats = y_tr.groupby(tr[col]).agg(['mean', 'count'])
    smooth = (stats['mean']*stats['count'] + prior*m) / (stats['count'] + m)
    return (tr[col].map(smooth).fillna(prior),
            va[col].map(smooth).fillna(prior),
            te[col].map(smooth).fillna(prior))

# 3-5. Stratified K-Fold で OOF + test 予測
skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42)
oof       = np.zeros(len(train_X))
test_pred = np.zeros(len(test_X))
params = dict(n_estimators=1000, learning_rate=0.03, num_leaves=31,
              subsample=0.8, colsample_bytree=0.8, random_state=42, n_jobs=-1)

for fold, (tr_idx, va_idx) in enumerate(skf.split(train_X, y)):
    X_tr, X_va, X_te = train_X.iloc[tr_idx].copy(), train_X.iloc[va_idx].copy(), test_X.copy()
    y_tr, y_va = y.iloc[tr_idx], y.iloc[va_idx]
    # ★ TargetEncoding は fold 内 train でのみ fit
    for c in high_card:
        e_tr, e_va, e_te = target_encode(X_tr, X_va, X_te, c, y_tr)
        X_tr[c+'_te'], X_va[c+'_te'], X_te[c+'_te'] = e_tr, e_va, e_te
    X_tr, X_va, X_te = (d.drop(columns=high_card) for d in (X_tr, X_va, X_te))

    model = lgb.LGBMClassifier(**params)
    model.fit(X_tr, y_tr, eval_set=[(X_va, y_va)], eval_metric='auc',
              callbacks=[lgb.early_stopping(50), lgb.log_evaluation(0)])
    oof[va_idx] = model.predict_proba(X_va)[:, 1]
    test_pred  += model.predict_proba(X_te)[:, 1] / skf.n_splits
    print(f"fold{fold} AUC = {roc_auc_score(y_va, oof[va_idx]):.4f}")

print(f"\n=== OOF AUC = {roc_auc_score(y, oof):.4f} ===")

# 6. 提出ファイル
submission = pd.DataFrame({id_col: test[id_col], target_col: test_pred})
submission.to_csv('submission.csv', index=False)

タスク3: CV=0.842 / LB=0.798 の乖離診断

#仮説見分け方対処
1リーク(CVが甘い) TE を fold 外で fit していないか。OOF が異常に高くないか fold 内 fit を徹底。時系列なら時間分割 CV へ
2train/test の分布ズレ 各列の分布比較。Adversarial Validation の AUC が 0.5 から離れる ズレる特徴量を除外/重要度低下。CV を test 分布に寄せる
3Public LB が小さくノイズ Public LB の対象行数を確認(例: test の 20%) LB を過信せず CV を信じる。最終提出は CV ベース
🎯
結論: 0.04 程度の乖離は Playground では珍しくない。まず Adversarial Validation で分布ズレを確認し、問題なければ CV を信頼して過度な LB probing を避ける。「CV を信じて安定提出」が最も再現性が高い。

🪜 Step-by-Step 解説

1目的変数を最初に見る

不均衡(例: 陽性 5%)なら Stratified K-Fold 必須。fold ごとにクラス比率が崩れると CV スコアが暴れる。AUC は不均衡に比較的頑健だが、CV 設計は必ず層化する。

2欠損値は「単純代入」から始める

🩹
中央値・最頻値の単純代入は統計量が target に依存しないため、fold 外で計算してもリーク影響が小さい。まず動くパイプラインを作り、後で高度な代入に差し替える。「動くものを最速で」が総復習の主旨。

3TargetEncoding は fold 内で fit(最重要)

リーク vs 正解要注意
# ❌ リーク: 全 train で category→target 平均を計算してから CV
# ✅ 正しい: fold の train 部分だけで計算し、valid と test に適用

Day 077「データリーク」の実戦適用。Smoothingm=10)は出現回数が少ないカテゴリの平均を全体平均に寄せ、過学習を防ぐ。

4OOF で「正直な」スコアを測る

out-of-fold 予測は「学習に使っていないデータへの予測」を全 train 分そろえたもの。これで計算した AUC が「LB に最も近い自分だけの評価指標」。パラメータ変更の良し悪しは常に OOF で判断する。

5test 予測は fold 平均

各 fold のモデルは少しずつ違うデータで学習しているため、5 モデルの予測を平均すると分散が下がり安定する。軽いアンサンブル(bagging 効果)でもある。

6CV と LB が食い違ったら CV を信じる

🏆
Kaggle 格言 「Trust your CV」。Public LB は test の一部でしか計算されず、そこに過剰適合すると Private LB で shake down する。CV 設計が正しい限り、CV を最終判断の軸にする。

🧮 数学・統計の補足(文系向け)

AUC を直感で理解する

AUC = 1.0 → 完璧に並べられる
AUC = 0.5 → コイン投げと同じ(無意味)
AUC = 0.8 → 8割の確率で 陽性 > 陰性 に並べられる

Smoothing の式の意味

補正平均 = (カテゴリ平均 × 件数 + 全体平均 × m) / (件数 + m)

「件数が多いカテゴリは自分の平均を信頼、少ないカテゴリは全体平均に寄せる」加重平均m は全体平均を何件分の重みとして混ぜるか。件数が m を大きく超えるとほぼ自分の平均、少ないと全体平均に近づく。

🏆 Kaggleでの実践的な使い方

この総復習パイプラインが「土台」になる

今日組んだ枠組みは Phase 4 以降でそのまま拡張される。OOF と test_pred の枠組みは変わらず、中身を差し替えるだけでスコアが伸びる。

拡張ポイントPhase 4 で学ぶこと
モデルを差し替えXGBoost / CatBoost / 複数モデル
ハイパラ最適化Optuna で params を自動探索
特徴量を増やすAggregation / Lag / 高度な TargetEncoding
アンサンブル複数モデルの OOF を Stacking

Bronze 圏内に入るための実務チェック

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
全 train で TE してから CV「前処理は先にまとめて」の習慣fold 内 fit しないと OOF が甘くなりリーク
LB が上がるまでパラメータを回すLB が正義だと思うLB probing は Private で崩れる。CV を信じる
test 予測を 1 fold だけで作る実装が楽5 fold 平均のほうが安定して伸びる
AUC なのに 0/1 に丸めて提出分類=ラベル出力だと思うAUC は確率(順位)で提出。丸めるとスコア低下
不均衡データで通常の KFoldStratified を忘れるfold 間でクラス比が崩れ CV が暴れる

🚀 次のステップ

  • 発展: パイプラインに Adversarial Validation(train と test を見分ける分類器で分布ズレ検出)を追加し、CV と LB の乖離を実際に診断する
  • 次回予告: Phase 4(青帯)突入 — 「Gradient Boosting とは何か」(Day 090)。決定木を「順番に弱点を補強していく」Boosting の直感を文系向けに解き明かす
🎉
Phase 3 完走! EDA から実戦パイプラインまで、Kaggle Contributor に必要な土台が完成しました。次はいよいよ GBDT の Phase 4 へ。

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

自分の回答・気づき・メモ: