Day 109 — Phase 4 総復習 — Tabularコンペで上位10%(Silverメダル)を目指す

2026-07-30 青 / Phase 4 戦略 Phase 4 完走回 / GBDT × Optuna × Stacking × SHAP

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

🔵
Phase 4 の最終回です。ここまでの19日間で XGBoost・LightGBM・CatBoost・Optuna・高度な特徴量工学・NestedCV・アンサンブル(Averaging/Stacking/Blending)・不均衡対策・SHAP解釈 を学びました。今日はこれらを1本のパイプラインに統合し、Phase 3 で作った「Bronze圏内(上位40%)」の型を、Silver圏内(上位10%) に届く型へ引き上げます。

料理でいうと「基本の一皿」から「コンテスト用のコース料理」への格上げです。Bronzeは「丁寧に基本を守れば届く」領域ですが、Silverは モデルの多様性・検証の厳密さ・自分の予測を疑う力 が要求されます。

Bronze → Silver で何が変わるか

項目Phase 3(Bronze・上位40%)Phase 4(Silver・上位10%)
モデルLightGBM 単体LightGBM + XGBoost(+ CatBoost)の 多様性
ハイパラデフォルト値 or 手動調整Optuna で自動探索(Day 096-097)
検証Stratified/K-Fold のみNestedCV でチューニングと評価を分離(Day 101)
特徴量基本の変換・単純TargetEncodingAggregation・Rolling/Lag・LeaveOneOut TE(Day 098-100)
最終予測単一モデルの平均Stacking/Blending で複数モデルを合成(Day 102-104)
頑健性外れ値対策・不均衡対策(Day 105-106)
解釈特徴量重要度のみSHAP でグローバル/ローカル解釈、リーク・バグの発見(Day 107-108)
💡
なぜ「多様性」が鍵なのか: LightGBM を3個スタッキングしても、同じアルゴリズムの誤差は似た形で間違えるため、平均しても誤差はあまり減りません。LightGBM・XGBoost・CatBoostのように育て方(木の育て方・分割基準)が違うモデルを混ぜると、それぞれの誤差パターンが独立に近くなり、平均したときの分散低減効果が大きくなります。

🗺️ Phase 4 の19日間マップ

Dayテーマ
090Gradient Boostingとは何か
091-092XGBoost(実装・パラメータ)
093-094LightGBM(実装・DART/GOSS)
095CatBoost(Ordered Boosting)
096-097Optuna(基礎・高度な使い方)
098-100Aggregation・Rolling/Lag・高度なTargetEncoding
101NestedCV
102-104アンサンブル(Averaging・Stacking・Blending)
105-106外れ値対策・不均衡データ対策
107-108特徴量選択の高度手法・SHAP値の解釈
109Phase 4 総復習(本日)

📐 評価指標: RMSE を視覚的に理解する

RMSE = √( mean( (y_true − y_pred)² ) )

誤差を2乗してから平均し、最後にルートを取って元の単位に戻した「誤差の代表的な大きさ」

なぜ2乗するのか? 5件の予測を例に、「そのままの誤差」と「2乗した誤差」を比べてみます。

5件のサンプル: |誤差|(青) vs 誤差²(赤・正規化) A (誤差=2) 2 4 B (誤差=4) 4 16 C (誤差=1) 1 1 D (誤差=1) 1 1 E (誤差=10・外れ値) 10 100 Eの誤差はDの10倍だが、2乗誤差は100倍。RMSEは「たまに出る大外れ」に敏感な指標
🎯
実務的な意味: RMSEは「平均的にどれくらいズレるか」を測るが、内部で2乗しているため 1件の大外れが全体のスコアを大きく引き下げる。だからこそPhase 4の「外れ値対策(Huber Loss・量子回帰・Day 105)」が効いてくる。RMSEを最小化するコンペでは、平均的な精度だけでなく「大外れをどれだけ減らせるか」が上位進出の鍵になる。

🎯 問題

Kaggle Expert(Bronze 2枚 or Silver 1枚は達成済み)として、Playground Series 相当の 回帰コンペ(評価指標 RMSE)に参加し、Silver圏内(上位10%)を目指す設定です。

データスキーマ

カラム説明
idint行を一意に識別するID
num_01num_18float数値特徴量18個(一部に外れ値・軽い欠損を含む)
cat_region, cat_plan, cat_device, cat_channel, cat_segmentcategory(低カーディナリティ, 4〜10種)属性系カテゴリ変数
cat_user_hashcategory(高カーディナリティ, 約500種)ユーザー系列を示す疑似ハッシュ
cat_item_hashcategory(高カーディナリティ, 約350種)アイテム系列を示す疑似ハッシュ
targetfloat(train のみ)予測対象の連続値。評価指標は RMSE

train: 50,000行 / test: 20,000行 / 提出形式: id,target

タスク

1
ギャップ分析(戦略): 上の対応表を参考に、このコンペで追加すべき5つの打ち手を優先度順に並べ、それぞれ「なぜスコアが伸びるのか」を一言で説明する
2
NestedCVの設計(コーディング): Optuna用の内側CV(3-Fold)と、OOF生成・最終評価用の外側CV(5-Fold)を分離する
3
2モデル学習(コーディング): Optunaでチューニングした LightGBM と、手堅いXGBoostで、それぞれ OOF予測とtest予測を作る
4
Stacking(コーディング): 2つのOOF予測を特徴量として Ridge回帰でLevel-2 Stackingを行い、submission.csv を出力する
5
SHAP解釈(コーディング): 最終のLightGBMモデルにSHAPを適用し、上位3特徴量を確認する
6
提出戦略(戦略): CV RMSE=0.352 / Public LB=0.361のとき、Bronze期の「乖離0.04〜0.05は許容してCVを信じる」からどう判断基準を変えるべきかを、SHAPで確認すべきチェック項目とあわせて3点述べる

💡 ヒント

ヒント1方向性

新しいアルゴリズムを覚える回ではありません。「モデルの多様性」「検証の厳密さ」「自分の予測を疑う目(SHAP)」の3つを、既存のBronzeパイプラインに足し算していく回です。Optunaの探索に使うCVと、最終評価・Stacking用のOOFを作るCVは、役割を分けて考えます。

ヒント2アプローチ
  • Optuna: optuna.create_study(direction='minimize') で、目的関数の中では軽量な3-Foldなど専用のCVでRMSEを評価する(外側5-Foldとは別物)
  • 外側5-Fold: KFold(n_splits=5, shuffle=True, random_state=42) で LightGBM・XGBoost それぞれの OOF・test予測を作る
  • Stacking: oof_matrix = np.column_stack([oof_lgb, oof_xgb])Ridge() で学習し、test_matrix に適用
  • SHAP: shap.TreeExplainer(model).shap_values(X)np.abs(shap_values).mean(axis=0) で特徴量ごとの平均インパクトを見る
ヒント3コード骨格
import optuna
import numpy as np
from sklearn.model_selection import KFold
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_squared_error
import lightgbm as lgb
import xgboost as xgb

# --- Optuna: 内側CV専用(チューニングのみ) ---
def objective(trial):
    params = {
        'num_leaves': trial.suggest_int('num_leaves', 15, 63),
        'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.1, log=True),
        'min_child_samples': trial.suggest_int('min_child_samples', 5, 50),
    }
    kf_inner = KFold(n_splits=3, shuffle=True, random_state=0)  # ★外側とは別のCV
    scores = []
    for tr_idx, va_idx in kf_inner.split(X):
        model = lgb.LGBMRegressor(**params, n_estimators=500, random_state=42)
        model.fit(X.iloc[tr_idx], y.iloc[tr_idx])
        pred = model.predict(X.iloc[va_idx])
        scores.append(mean_squared_error(y.iloc[va_idx], pred, squared=False))
    return np.mean(scores)

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

# --- 外側5-Fold: OOF生成・最終評価用(チューニングには使わない) ---
kf_outer = KFold(n_splits=5, shuffle=True, random_state=42)
oof_lgb = np.zeros(len(X))
# ... LightGBM・XGBoostそれぞれで同様にOOFを作る ...

# --- Level-2 Stacking ---
oof_matrix = np.column_stack([oof_lgb, oof_xgb])
meta_model = Ridge(alpha=1.0)
meta_model.fit(oof_matrix, y)

模範解答

タスク1: Silverに向けた5つの打ち手(優先度順)

優先度打ち手なぜスコアが伸びるか
1モデルの多様性を増やす(LGBM+XGB、余力があればCatBoost)異なる木の育て方の誤差を平均すると分散が下がる
2NestedCVでチューニングの過大評価を防ぐ同じCVでチューニングと評価をすると、パラメータがそのCV分割に過適合しスコアが甘くなる
3高度な特徴量工学(Aggregation・LeaveOneOut TE)GBDTは特徴量の質に強く依存する。良い特徴量1つはパラメータ調整10回分の価値がある
4Stacking(単純平均より一段上)Ridgeがモデルごとの信頼度に応じた重みを自動学習し、単純平均より誤差を減らせる
5SHAPで予測の健全性を検証重要度が高い特徴に想定外のリークやノイズが混ざっていないかを早期発見できる

タスク2〜5: 統合パイプライン実装

import numpy as np
import pandas as pd
import optuna
from sklearn.model_selection import KFold
from sklearn.linear_model import Ridge
from sklearn.metrics import mean_squared_error
import lightgbm as lgb
import xgboost as xgb
import shap

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

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

cat_cols = X.select_dtypes(exclude='number').columns.tolist()
for c in cat_cols:
    X[c] = X[c].astype('category')
    X_test[c] = X_test[c].astype('category')

# ====== 1. Optuna(内側CV=3-Fold・チューニング専用) ======
def objective(trial):
    params = {
        'num_leaves': trial.suggest_int('num_leaves', 15, 63),
        'learning_rate': trial.suggest_float('learning_rate', 0.01, 0.1, log=True),
        'min_child_samples': trial.suggest_int('min_child_samples', 5, 50),
        'subsample': trial.suggest_float('subsample', 0.6, 1.0),
        'colsample_bytree': trial.suggest_float('colsample_bytree', 0.6, 1.0),
    }
    kf_inner = KFold(n_splits=3, shuffle=True, random_state=0)
    scores = []
    for tr_idx, va_idx in kf_inner.split(X):
        model = lgb.LGBMRegressor(**params, n_estimators=500, random_state=42)
        model.fit(X.iloc[tr_idx], y.iloc[tr_idx])
        pred = model.predict(X.iloc[va_idx])
        scores.append(mean_squared_error(y.iloc[va_idx], pred, squared=False))
    return np.mean(scores)

study = optuna.create_study(direction='minimize')
study.optimize(objective, n_trials=30)
best_params = study.best_params
print("Best params:", best_params)

# ====== 2. 外側5-Fold: LightGBM・XGBoost で OOF + test 予測 ======
kf_outer = KFold(n_splits=5, shuffle=True, random_state=42)
oof_lgb, oof_xgb = np.zeros(len(X)), np.zeros(len(X))
test_lgb, test_xgb = np.zeros(len(X_test)), np.zeros(len(X_test))
final_lgb_model = None

for fold, (tr_idx, va_idx) in enumerate(kf_outer.split(X)):
    X_tr, X_va = X.iloc[tr_idx], X.iloc[va_idx]
    y_tr, y_va = y.iloc[tr_idx], y.iloc[va_idx]

    # --- LightGBM(Optunaのベストパラメータを使用) ---
    model_lgb = lgb.LGBMRegressor(**best_params, n_estimators=1000, random_state=42)
    model_lgb.fit(
        X_tr, y_tr, eval_set=[(X_va, y_va)],
        callbacks=[lgb.early_stopping(50), lgb.log_evaluation(0)],
    )
    oof_lgb[va_idx] = model_lgb.predict(X_va)
    test_lgb += model_lgb.predict(X_test) / kf_outer.n_splits
    final_lgb_model = model_lgb  # SHAP用に最後のfoldモデルを保持

    # --- XGBoost(手堅い固定パラメータ) ---
    model_xgb = xgb.XGBRegressor(
        max_depth=6, learning_rate=0.03, n_estimators=1000,
        subsample=0.8, colsample_bytree=0.8,
        enable_categorical=True, random_state=42,
    )
    model_xgb.fit(X_tr, y_tr, eval_set=[(X_va, y_va)], verbose=False)
    oof_xgb[va_idx] = model_xgb.predict(X_va)
    test_xgb += model_xgb.predict(X_test) / kf_outer.n_splits

    print(f"fold{fold} LGBM RMSE={mean_squared_error(y_va, oof_lgb[va_idx], squared=False):.4f} "
          f"XGB RMSE={mean_squared_error(y_va, oof_xgb[va_idx], squared=False):.4f}")

print(f"\nLightGBM OOF RMSE: {mean_squared_error(y, oof_lgb, squared=False):.4f}")
print(f"XGBoost  OOF RMSE: {mean_squared_error(y, oof_xgb, squared=False):.4f}")

# ====== 3. Level-2 Stacking(Ridge回帰) ======
oof_matrix  = np.column_stack([oof_lgb, oof_xgb])
test_matrix = np.column_stack([test_lgb, test_xgb])

meta_model = Ridge(alpha=1.0)
meta_model.fit(oof_matrix, y)
oof_stack  = meta_model.predict(oof_matrix)
test_stack = meta_model.predict(test_matrix)

print(f"\n=== Stacking OOF RMSE: {mean_squared_error(y, oof_stack, squared=False):.4f} ===")
print("Meta weights (LGBM, XGB):", meta_model.coef_)

# ====== 4. SHAP で最終モデル(LightGBM)を解釈 ======
explainer = shap.TreeExplainer(final_lgb_model)
shap_values = explainer.shap_values(X_va)
importance = pd.Series(np.abs(shap_values).mean(axis=0), index=X.columns)
print("\nSHAP 上位3特徴量:")
print(importance.sort_values(ascending=False).head(3))

# ====== 5. 提出ファイル作成 ======
submission = pd.DataFrame({'id': test['id'], 'target': test_stack})
submission.to_csv('submission.csv', index=False)
print(submission.head())

タスク6: CV=0.352 / LB=0.361 の判断とSHAPチェック

#判断・チェック項目Bronze期との違い
1乖離の許容幅を狭めるBronze期は0.04〜0.05を許容していたが、Silverは上位10%内の僅差が順位を左右するため、乖離の原因(分布ズレ・fold設計)を必ず特定してから提出を続ける
2SHAPの上位特徴量にリーク源がないか確認重要度が異常に高い1特徴(例: IDと相関する列、集計時にtargetが混入した列)がないかをSHAP summary plotで毎回確認する
3Adversarial Validationで分布ズレを定量化train/testを見分ける分類器のAUCが0.5から大きく離れていないかをSilver狙いでは必須チェックにする
🧠
結論: Silverを狙う段階では「CVを信じる」だけでなく、「なぜそのCVスコアなのかをSHAPと分布比較で説明できる」ところまで踏み込む。乖離が説明できないまま提出を重ねると、Private LBでのシェイクダウンリスクが上がる。

🧩 アンサンブルの効果(参考シミュレーション値)

架空のコンペ設定における典型的な傾向を示す参考値。RMSEは小さいほど良い指標なので、バーが短いほど高性能を意味する。

LightGBM 単体(Optunaチューニング済)
0.365
OOF RMSE(小さいほど良い)
XGBoost 単体(手堅い固定パラメータ)
0.360
OOF RMSE(小さいほど良い)
単純平均(Averaging)
0.356
両モデルの平均
Ridge Stacking(本日の実装)
0.352
最も良いスコア

単体モデル → 単純平均 → Stackingの順に誤差が小さくなる。誤差パターンが異なる2モデルを組み合わせるほど、分散が下がりRMSEが改善する典型例。

🔍 SHAP解釈: 上位3特徴量(参考シミュレーション値)

mean(|SHAP値|) 正規化比較(上位3特徴量) num_07 0.184 cat_plan_te 0.125 num_03 0.083 num_07(Aggregation特徴量)が最上位。cat_plan_teはTargetEncoding済みのプラン種別
⚠️
上位特徴量が確認できたら、Day 108の要領で SHAP占有率 も併せてチェックする。特定の1特徴が全サンプルで異常に支配的なら、Aggregation計算時にtargetが混入していないか(リーク)を疑う。

🪜 Step-by-Step 解説

1Optunaの内側CVを外側CVと分離する(NestedCV)

kf_inner = KFold(n_splits=3, shuffle=True, random_state=0)   # Optuna専用
kf_outer = KFold(n_splits=5, shuffle=True, random_state=42)  # OOF・評価専用
🔧
なぜこうするか: 同じ分割でチューニングと最終評価を両方行うと、パラメータがその分割に「都合よく」フィットし、CVスコアが実力より良く見えます(Day 101 NestedCVの実戦適用)。random_stateも分けることで、内側と外側の分割が完全に独立します。

2LightGBMとXGBoostで別々にOOFを作る

oof_lgb[va_idx] = model_lgb.predict(X_va)
oof_xgb[va_idx] = model_xgb.predict(X_va)
🌳
なぜこうするか: 2つのモデルの誤差パターンが異なるほど、後段のStackingで誤差が相殺されます。同じ木構造ベースでも、LightGBMは葉ごと成長・XGBoostは階層ごと成長と育ち方が違うため、十分な多様性が生まれます。

3Ridge回帰でLevel-2 Stacking

meta_model = Ridge(alpha=1.0)
meta_model.fit(oof_matrix, y)
なぜこうするか: OOF予測はすでに「学習に使っていないデータへの予測」なので、これをそのまま特徴量にしてRidgeを学習しても外側CVでのリークにはなりません。Ridgeを使うのは、2つのOOFが強く相関していてもL2正則化で重みが暴れるのを防げるためです。

4SHAPで最終モデルの妥当性を確認

importance = pd.Series(np.abs(shap_values).mean(axis=0), index=X.columns)
🔍
なぜこうするか: 特徴量重要度だけでは「効いている」ことは分かっても「なぜ・どちらの方向に効くか」は分かりません。SHAPの絶対値平均でグローバルな重要度を確認し、想定外の特徴が上位に来ていないかをレビューします。

5最終提出は「説明できるCV」を選ぶ

🧭
なぜこうするか: CVとLBの数値だけで判断するのではなく、なぜそのスコアなのかをSHAPと分布比較で言語化できる提出を最終候補に選びます。これがBronze期の「CVを信じる」から一段進んだSilver期の考え方です。

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

🧾
Ridge回帰のStackingでの役割: Ridge回帰は「予測値どうしの重み付け平均を学習するが、重みが極端になりすぎないようブレーキをかける」手法です。最終予測 = w1×LightGBMの予測 + w2×XGBoostの予測(+ 切片)。2つのOOF予測が似すぎている(相関が高い)場合に単純な最小二乗法だと係数が不安定になりやすく、Ridgeは係数の二乗和にペナルティを与えることでこれを抑えます。
📝
NestedCVの直感: 「テストで良い点を取るための勉強法を、そのテストの過去問だけを見て決めてはいけない」。Optunaのパラメータ探索を外側CVと同じ分割で行うと、「このfold分割に強いパラメータ」を選んでしまい、外側CVのスコアが本当の実力より甘く出ます。内側と外側で分割を変えることで、パラメータ探索の視点と最終評価の視点を独立させます。

🏆 Kaggleでの実践的な使い方

よく使われるコンペカテゴリ: ☑ 表形式データ(Tabular) / ☐ 自然言語処理(NLP) / ☐ 画像認識(CV) / ☑ 時系列(Time Series)

土台

🧱 モデルを増やす

Phase 5でNNを追加

ニューラルネットワーク(PyTorch)をStackingに追加すると、GBDT系だけでは拾えない非線形パターンをさらに補える。

拡張

🧬 特徴量の型を変える

マルチモーダルへ

NLP特徴量・画像特徴量をTabular特徴量と混ぜるマルチモーダルコンペで、今日の骨格がそのまま使える。

深化

🪜 Stackingを多層化

Level-3へ

Level-2をLightGBMにする、Level-3を追加するなど、上位陣は3層構成のStackingも使う。

Silverメダル圏内に入るための実務チェック

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
同系統のGBDTを3つスタッキングすれば精度が上がる「モデルを増やせば良い」という思い込み誤差パターンが似ていると平均しても分散はあまり減らない。系統の違うモデルを混ぜる方が効果的
Optunaのチューニングと最終CVを同じ分割で行う実装を簡単にしたいパラメータがそのfold分割に過適合し、CVスコアが甘くなる(NestedCVで回避)
StackingのRidgeを外側CVなしで一発学習するOOFなら安全だと過信ベースモデルが少数(2〜3個)なら実務上は大きな問題になりにくいが、モデル数が増えるほどRidge自体もCVで検証すべき
SHAPは重要度ランキングを見るだけで終わる「重要度=正しい」と思い込む上位特徴量が本当に意味のある特徴か、リークやノイズでないかを毎回疑う
Public LBが良くなるまでStackingの重みを手動調整するLBが正義だと思うMeta modelの重みはOOFに基づいて自動学習させ、LB直接最適化(LB probing)は避ける

🚀 次のステップ

  • 発展: このパイプラインに CatBoost を3本目のベースモデルとして追加し、Stackingの重みがどう変化するか確認する。また optunan_trials を増やし、探索の安定性を比較する
  • 次回予告: Phase 5(紫帯)突入 — 「ディープラーニング基礎(ニューラルネットワークの直感)」(Day 110)。GBDTが苦手とする画像・テキスト・複雑な非線形パターンに、なぜニューラルネットワークが強いのかを文系向けに解き明かす

Kaggleランクの道のり

上位40-100%(圏外)
Bronze 上位40%
Silver 10%

Phase 3でBronze圏内(上位40%)の型を習得し、Phase 4でモデル多様性・NestedCV・Stacking・SHAPを積み上げてSilver圏内(上位10%)を狙えるところまで到達。Phase 5ではさらにGold(上位1%前後)を目指す武器を増やしていく。

🎉 Phase 4 完走! GBDT・Optuna・高度な特徴量工学・アンサンブル・SHAP解釈まで、Kaggle Expert〜Silver到達に必要な武器がそろいました。

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

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