Day 103 — アンサンブル②(Stacking)— Level-1/Level-2・リーク防止

2026-07-24 青 / Phase 4 コーディング Level-0 / Level-1 / OOF特徴量

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

🔵
Day 102 では複数モデルの予測をそのまま平均する Averaging を学びました。今日はもう一歩進み、「予測をどう混ぜるか」自体を別のモデル(メタモデル)に学習させる Stacking に入ります。ベースモデル群をLevel-0、その予測を入力として最終判断を下すモデルをLevel-1と呼びます。
🚨
Stackingで最も起こりやすい事故は、Level-0モデルの予測としてそのモデルが学習に使った行そのものへの予測(in-sample予測)をメタモデルの特徴量にしてしまうことです。GradientBoostingなどは学習に使った行に対しては「ほぼ答えを覚えている」状態で予測でき、メタモデルはこの異常な自信を鵜呑みにしてしまいます。今日はこのリークを実際に発生させて、被害の大きさを数値で確認します。
用語直感的な意味
Level-0(ベースモデル)元の特徴量から予測を作るモデル群。今日はDay 102と同じLR・RF・GBの3モデル
Level-1(メタモデル)Level-0モデルたちの予測値そのものを新しい特徴量として受け取り、最終予測を作るモデル
OOF特徴量Level-0モデルが「学習に使っていない行」に対して出した予測。リークを防ぐための必須条件
in-sample予測(リーク源)Level-0モデルが「自分が学習した行」に対して出した予測。異常に高精度で、メタモデルを誤誘導する

🧩 Stackingの3つの原則

混ぜ方も学習する

🧠 固定の重みではなく、メタモデルが決める

Averagingの重み最適化の一般化

Day 102の重み最適化はw1,w2,w3という固定の線形結合でした。Stackingはこの混ぜ方自体を、正解ラベルを見ながらメタモデルに学習させる、より柔軟な枠組みです。

OOF特徴量が絶対条件

🔒 Level-0の出力は必ずOOF予測で

in-sample予測は即リーク

学習に使った行への予測をメタ特徴量にすると、モデルの「答えを覚えている」だけの自信をメタモデルが信頼してしまう。Day 102と同じ5-Fold OOF方式を必ず使う。

CVスコアを鵜呑みにしない

⚖️ 独立したholdoutでギャップを検証

見かけのスコアと実力を分離する

CV/OOFスコアが高くても、真に未知のholdoutデータで検証しない限りリークの有無は分からない。両者のギャップが今日の判断材料になる。

🎯 問題

Day 102 と同じChurn(銀行の顧客離反予測)データを使います。今日は追加で、学習に一切使わない「本番相当」の独立したholdoutデータも用意し、CV上のスコア(見かけ)と真の性能(実力)を比較します。

使用データChurn予測(Day102と同一データ生成関数)+ 独立holdout
import numpy as np
import pandas as pd

def make_churn_df(seed, n):
    rng_state = np.random.get_state()
    np.random.seed(seed)
    age = np.random.normal(40, 12, n).clip(18, 80)
    income = np.random.lognormal(10.5, 0.4, n)
    tenure_months = np.random.exponential(24, n).clip(0, 120)
    num_products = np.random.randint(1, 5, n)
    is_active = np.random.binomial(1, 0.6, n)

    logit = (
        -0.05 * age + 0.00003 * income - 0.03 * tenure_months - 1.2 * is_active
        + 2.2 * ((age > 55) & (num_products == 1)).astype(float)
        + 2.0 * ((tenure_months < 6) & (num_products >= 3)).astype(float)
        + np.random.normal(0, 0.5, n)
    )
    prob = 1 / (1 + np.exp(-logit))
    churn = np.random.binomial(1, prob)
    df = pd.DataFrame({'Age': age, 'Income': income, 'TenureMonths': tenure_months,
                        'NumProducts': num_products, 'IsActive': is_active, 'Churn': churn})
    np.random.set_state(rng_state)
    return df

FEATURES = ['Age', 'Income', 'TenureMonths', 'NumProducts', 'IsActive']

df_train = make_churn_df(seed=42, n=2000)     # Day102と同じ学習データ
df_holdout = make_churn_df(seed=999, n=1000)   # 学習に一切使わない「本番相当」の未知データ

X, y = df_train[FEATURES].values, df_train['Churn'].values
X_hold, y_hold = df_holdout[FEATURES].values, df_holdout['Churn'].values
カラム名意味
Age顧客の年齢float
Income年収(円)float
TenureMonths契約継続月数float
NumProducts契約中の商品数(1〜4)int
IsActiveアクティブ顧客フラグ(1=活発)int
Churn離反フラグ(1=離反、目的変数)int

タスク

1
【理論】StackingとAveraging(Day 102)の違いを「固定の混ぜ方」vs「学習される混ぜ方」の観点で説明する。Level-0の予測に「in-sample予測」を使うとなぜリークになるのかを説明する
2
【実装】3モデル(LR・RF・GB)について、5-Fold CVで①学習用データへのOOF予測、②X_holdへの予測(Foldモデルの平均)を同時に作る関数を実装する
3
【実装】OOF予測を特徴量としてLogisticRegressionをメタモデルとして学習する。Level-0とは別のKFold分割でメタモデル自身のOOF AUCを計算し、全データで学習した最終メタモデルをX_holdに適用して真のholdout AUCを計算する
4
【実装】「リークあり版」を実装する。3モデルを学習データ全体で学習し、in-sample予測をメタ特徴量にしてメタモデルを学習する。同じくCV AUCと真のholdout AUCの両方を計算する
5
【分析/戦略】正しいStacking・リークあり版・Day 102のAveraging手法について、「CV/OOF AUC」と「真のholdout AUC」の差(ギャップ)を比較する表を作り、リークがどれくらいスコアを歪めるかを定量的に説明する

🔀 Stackingの構造 — 正しい経路 vs リーク経路

同じ3モデルからスタートしても、Level-0の予測の作り方(OOF vs in-sample)によって結果が天と地ほど変わることを図解する。

Level-0: 3モデルを学習(LogisticRegression / RandomForest / GradientBoosting) LogisticRegression RandomForest GradientBoosting ↓ 予測の作り方が分岐点 ↓ ① OOF予測(5-Fold、未学習データへの予測) get_oof_and_test_avg() / Day102と同方式 ② in-sample予測(学習した行への予測) model.fit(X,y).predict_proba(X) ← 危険 Level-1: メタモデル(OOF特徴量で学習) Level-1: メタモデル(in-sample特徴量で学習) CV AUC = 0.7493(正直なスコア) CV AUC = 0.9281(見かけの高スコア) ↓ 真のholdoutで検証 ↓ ↓ 真のholdoutで検証 ↓ holdout AUC = 0.7924 ✔ CVとほぼ一致 → 信頼できる holdout AUC = 0.7693 ✘ CVから急落 → 単独RFにも劣る

📊 AUC比較(実行結果)

実際にコードを実行して得た値(乱数シード固定・環境により多少前後する)。バーの軸は0.70始まり(切り捨て軸)。「真のholdout AUC」が唯一の本当の実力の物差し。

LogisticRegression(単独)
0.7745
holdout AUC
RandomForest(単独)
0.7825
holdout AUC
GradientBoosting(単独)
0.7828
holdout AUC
Simple Average(Day102)
0.7922
holdout AUC
Rank Average(Day102)
0.7937
holdout AUC・全手法中トップ
重み最適化(Day102)
0.7917
holdout AUC
Stacking(正しい/OOF特徴量)
0.7924
holdout AUC・Averagingと同水準
Stacking(リークあり)
0.7693
単独LR(0.7745)にも劣る

CV/OOFスコアと真のholdout AUCのギャップ

Stacking(正しい/OOF特徴量)
CV 0.7493 → holdout 0.7924
ギャップ −0.0431(誤差の範囲)
Stacking(リークあり)
CV 0.9281 → holdout 0.7693
ギャップ +0.1588(危険な過大評価)

バーの長さはギャップの絶対値(|CV/OOF AUC − 真のholdout AUC|)を、リークあり版=100%として正規化したもの。正しい版のギャップが負(holdoutの方が高い)なのは、seed=999で生成したholdoutデータがたまたま分離しやすかったというサンプルの偶然であり、CVが構造的に性能を過小評価しているわけではない。注目すべきは符号と大きさ: 正しい版は±0.04程度の誤差レベルに収まるのに対し、リークあり版は+0.16という桁違いの過大評価が、常に「見かけが良く出る」方向にだけ発生している点。

💡 ヒント

ヒント1方向性

Stackingは「Level-0モデルの予測値」を新しい特徴量とみなして、その上にもう1段モデルを乗せる操作。だからこそ「Level-0モデルの予測値がどう作られたか」が命取りになる。学習に使った行への予測(in-sample予測)と、学習に使っていない行への予測(OOF予測)とでは、同じモデル・同じ行でも数値の性質が全く異なることを念頭に、Day 102のget_oof_probaをどう拡張すれば「OOF予測」と「未知データへの予測」を両方作れるかを考える。

ヒント2アプローチ
  • Level-0の関数は、Day 102のget_oof_probaを拡張し、Foldごとに学習したモデルでX_test(holdout)も予測して、Kモデル分の予測を平均して返すようにする
  • Level-1(メタモデル)の学習には、Level-0のOOF生成に使ったKFoldとは別のrandom_stateのKFoldを使うと、Level-0とLevel-1のFold境界が偶然一致することを避けられる
  • リークあり版は、model.fit(X, y) のあとに同じXに対してpredict_probaすればin-sample予測になる(X_holdではなくX自身への予測であることに注意)
  • 「CV/OOF AUC」と「真のholdout AUC」の差が大きいほど、そのCVスコアは信用できない、という物差しで評価する
ヒント3コード骨格
from sklearn.base import clone
from sklearn.model_selection import KFold
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import roc_auc_score

def get_oof_and_test_avg(model, X, y, X_test, n_splits=5, random_state=42):
    kf = KFold(n_splits=n_splits, shuffle=True, random_state=random_state)
    oof = np.zeros(len(X))
    test_preds = np.zeros((n_splits, len(X_test)))
    for i, (tr_idx, val_idx) in enumerate(kf.split(X)):
        m = clone(model)
        m.fit(X[tr_idx], y[___])
        oof[val_idx] = m.predict_proba(X[val_idx])[:, 1]
        test_preds[i] = m.predict_proba(___)[:, 1]   # ← holdoutへの予測もこのFoldモデルで作る
    return oof, test_preds.mean(axis=___)

# タスク3: 正しいStacking
X_meta_oof = np.column_stack([oof_preds[n] for n in model_names])
X_meta_test = np.column_stack([test_preds[n] for n in model_names])

kf2 = KFold(n_splits=5, shuffle=True, random_state=7)  # Level-0とは別のCV
meta_oof = np.zeros(len(X))
for tr_idx, val_idx in kf2.split(X_meta_oof):
    m = clone(LogisticRegression())
    m.fit(X_meta_oof[___], y[tr_idx])
    meta_oof[val_idx] = m.predict_proba(X_meta_oof[val_idx])[:, 1]

meta_final = LogisticRegression().fit(X_meta_oof, y)
stack_holdout_pred = meta_final.predict_proba(___)[:, 1]  # ← X_meta_test を渡す

# タスク4: リークあり版
oof_leak = {}
for name, model in models.items():
    m = clone(model).fit(X, y)
    oof_leak[name] = m.predict_proba(___)[:, 1]   # ← X自身への予測(in-sample)

模範解答

import numpy as np
import pandas as pd
from sklearn.base import clone
from sklearn.model_selection import KFold
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import roc_auc_score

# ====== データ準備(省略: 問題文のmake_churn_dfをそのまま使用) ======
df_train = make_churn_df(seed=42, n=2000)
df_holdout = make_churn_df(seed=999, n=1000)
X, y = df_train[FEATURES].values, df_train['Churn'].values
X_hold, y_hold = df_holdout[FEATURES].values, df_holdout['Churn'].values

models = {
    'LogisticRegression': make_pipeline(StandardScaler(), LogisticRegression(max_iter=1000)),
    'RandomForest': RandomForestClassifier(n_estimators=200, max_depth=5, random_state=42),
    'GradientBoosting': GradientBoostingClassifier(n_estimators=150, max_depth=3, random_state=42),
}
model_names = list(models.keys())

# ====== タスク2: Level-0 の OOF予測 + holdout予測(Fold平均)を同時生成 ======
def get_oof_and_test_avg(model, X, y, X_test, n_splits=5, random_state=42):
    """OOF予測(train用)と、Kモデル平均によるholdout予測を同時に作る"""
    kf = KFold(n_splits=n_splits, shuffle=True, random_state=random_state)
    oof = np.zeros(len(X))
    test_preds = np.zeros((n_splits, len(X_test)))
    for i, (tr_idx, val_idx) in enumerate(kf.split(X)):
        m = clone(model)
        m.fit(X[tr_idx], y[tr_idx])
        oof[val_idx] = m.predict_proba(X[val_idx])[:, 1]
        test_preds[i] = m.predict_proba(X_test)[:, 1]
    return oof, test_preds.mean(axis=0)

oof_preds, test_preds = {}, {}
for name, model in models.items():
    oof, test_avg = get_oof_and_test_avg(model, X, y, X_hold)
    oof_preds[name] = oof
    test_preds[name] = test_avg
    print(f"{name:20s}: OOF AUC = {roc_auc_score(y, oof):.4f}  |  holdout AUC = {roc_auc_score(y_hold, test_avg):.4f}")

X_meta_oof = np.column_stack([oof_preds[n] for n in model_names])
X_meta_test = np.column_stack([test_preds[n] for n in model_names])

# ====== タスク3: 正しいStacking(Level-1もCVで評価) ======
meta = LogisticRegression()
kf2 = KFold(n_splits=5, shuffle=True, random_state=7)  # Level-0とは独立のCV分割
meta_oof = np.zeros(len(X))
for tr_idx, val_idx in kf2.split(X_meta_oof):
    m = clone(meta)
    m.fit(X_meta_oof[tr_idx], y[tr_idx])
    meta_oof[val_idx] = m.predict_proba(X_meta_oof[val_idx])[:, 1]
stack_cv_auc = roc_auc_score(y, meta_oof)

meta_final = clone(meta).fit(X_meta_oof, y)
stack_holdout_pred = meta_final.predict_proba(X_meta_test)[:, 1]
stack_holdout_auc = roc_auc_score(y_hold, stack_holdout_pred)

print(f"\n正しいStacking: CV AUC = {stack_cv_auc:.4f} | 真のholdout AUC = {stack_holdout_auc:.4f}")
print(f"メタモデル係数 (LR, RF, GB) = {np.round(meta_final.coef_[0], 3)}")

# ====== タスク4: リークあり版(in-sample予測をメタ特徴量にする) ======
oof_leak, test_leak = {}, {}
for name, model in models.items():
    m = clone(model).fit(X, y)               # train全体で学習
    oof_leak[name] = m.predict_proba(X)[:, 1]      # 自分が学習した行への予測(in-sample)
    test_leak[name] = m.predict_proba(X_hold)[:, 1]
    print(f"{name:20s}: in-sample AUC = {roc_auc_score(y, oof_leak[name]):.4f}  (学習データへの予測なので過大)")

X_meta_leak = np.column_stack([oof_leak[n] for n in model_names])
X_meta_test_leak = np.column_stack([test_leak[n] for n in model_names])

meta_leak_oof = np.zeros(len(X))
for tr_idx, val_idx in kf2.split(X_meta_leak):
    m = clone(meta)
    m.fit(X_meta_leak[tr_idx], y[tr_idx])
    meta_leak_oof[val_idx] = m.predict_proba(X_meta_leak[val_idx])[:, 1]
leak_cv_auc = roc_auc_score(y, meta_leak_oof)

meta_leak_final = clone(meta).fit(X_meta_leak, y)
leak_holdout_pred = meta_leak_final.predict_proba(X_meta_test_leak)[:, 1]
leak_holdout_auc = roc_auc_score(y_hold, leak_holdout_pred)

print(f"\nリークあり版Stacking: CV AUC = {leak_cv_auc:.4f} | 真のholdout AUC = {leak_holdout_auc:.4f}")

出力イメージ(実際に実行して確認した値。乱数・環境により多少前後する):

LogisticRegression  : OOF AUC = 0.7305  |  holdout AUC = 0.7745
RandomForest        : OOF AUC = 0.7480  |  holdout AUC = 0.7825
GradientBoosting    : OOF AUC = 0.7433  |  holdout AUC = 0.7828

正しいStacking: CV AUC = 0.7493 | 真のholdout AUC = 0.7924
メタモデル係数 (LR, RF, GB) = [2.555 2.42  1.355]

LogisticRegression  : in-sample AUC = 0.7398  (学習データへの予測なので過大)
RandomForest        : in-sample AUC = 0.8419  (学習データへの予測なので過大)
GradientBoosting    : in-sample AUC = 0.9251  (学習データへの予測なので過大)   ← 答えをほぼ覚えている

リークあり版Stacking: CV AUC = 0.9281 | 真のholdout AUC = 0.7693  ← 単独RandomForest(0.7825)にも負ける
🧠
理論問題(タスク1): Averagingはw1×LR + w2×RF + w3×GBという固定の重みで予測を混ぜる手法でした。Stackingはこの混ぜ方自体を、正解ラベルyを見ながらメタモデルに学習させる手法です。メタモデルが線形なら重み最適化とほぼ同じ発想ですが、非線形なメタモデルを使えば「LRとRFの予測が食い違うときはどちらを信じるべきか」のような条件付きの判断も学習できる点で、より柔軟です。
🧠
リークの核心(タスク5): GradientBoostingのin-sample AUC = 0.9251という異常な高さは、モデルが「まだ見ぬデータへの予測能力」ではなく「学習に使った行の答えをほぼ覚えている」ことを意味します。メタモデルはこの「異常な自信」を「このモデルは信頼できる」という学習データとして受け取り、in-sample特徴量で学習したCV AUC 0.9281という見かけ上ほぼ完璧なスコアを叩き出します。しかし真のholdoutデータではGradientBoostingはその行の答えを知らないため通常の実力(holdout AUC 0.7828相当)しか出せず、メタモデルが学習した「過剰な信頼」が仇となって、リークあり版の真のholdout AUCは0.7693まで低下します。これは単独のRandomForest(0.7825)にすら負ける結果で、「リークのあるStackingは何もしないより悪い」ことを如実に示しています。

🪜 Step-by-Step 解説

1Level-0を「OOF予測」と「holdout予測(Fold平均)」の両方を返す形に拡張する

def get_oof_and_test_avg(model, X, y, X_test, n_splits=5, random_state=42):
    kf = KFold(n_splits=n_splits, shuffle=True, random_state=random_state)
    oof = np.zeros(len(X))
    test_preds = np.zeros((n_splits, len(X_test)))
    for i, (tr_idx, val_idx) in enumerate(kf.split(X)):
        m = clone(model)
        m.fit(X[tr_idx], y[tr_idx])
        oof[val_idx] = m.predict_proba(X[val_idx])[:, 1]
        test_preds[i] = m.predict_proba(X_test)[:, 1]
    return oof, test_preds.mean(axis=0)
🔑
なぜこうするか: Day 102のget_oof_probaはtrain内のOOF予測しか作っていませんでした。Stackingでは学習データ用の特徴量(OOF予測)に加えて、真のholdout用の特徴量も必要です。「Foldごとに学習した5個のモデルの予測を平均する」方式を採用しているのは、OOF予測と同じ「複数モデルの合議」という性質をholdout側にも揃えるためです。

2Level-1(メタモデル)はLevel-0とは独立のKFoldで評価する

kf2 = KFold(n_splits=5, shuffle=True, random_state=7)  # Level-0とは別のCV分割
meta_oof = np.zeros(len(X))
for tr_idx, val_idx in kf2.split(X_meta_oof):
    m = clone(meta)
    m.fit(X_meta_oof[tr_idx], y[tr_idx])
    meta_oof[val_idx] = m.predict_proba(X_meta_oof[val_idx])[:, 1]
🧪
なぜこうするか: X_meta_oofの各行はすでに「その行を学習に使っていないLevel-0モデル」による予測なのでリークはありませんが、メタモデル自身の性能を測るときも通常のモデルと同じくCVで評価するのが筋です。random_stateをLevel-0と変えているのは、2つのCV分割の境界が完全に一致してしまう偶然を避けるためです。

3リークあり版は「学習した本人への予測」を特徴量にしてしまう

m = clone(model).fit(X, y)
oof_leak[name] = m.predict_proba(X)[:, 1]   # 学習に使ったXそのものへの予測
🚨
なぜこうするか: これが今日のテーマの核心です。mX, y全体で学習されているため、m.predict_proba(X)は「自分が答えを知っている問題」への回答になります。特にGradientBoostingやRandomForestのような表現力の高いモデルは、この自己採点で非常に高いスコアを出せてしまいます(in-sample AUC = 0.9251)。この数値をメタモデルの入力に使うと、メタモデルは「この特徴量が高いときは正解」という本番では成立しない関係を学習してしまいます。

4「CV/OOFスコア」と「真のholdoutスコア」のギャップで評価する

print(f"リークあり版: CV={leak_cv_auc:.4f} vs holdout={leak_holdout_auc:.4f} (差={leak_cv_auc-leak_holdout_auc:+.4f})")
print(f"正しい版  : CV={stack_cv_auc:.4f} vs holdout={stack_holdout_auc:.4f} (差={stack_cv_auc-stack_holdout_auc:+.4f})")
🔑
なぜこうするか: 単独の数値だけを見ても「リークがあるかどうか」は分かりません。同じパイプラインの中で「自分で計算したCV/OOFスコア」と「本当に一度も使っていないholdoutデータでのスコア」を突き合わせ、その差を見ることで初めてリークの有無・大きさを定量的に判断できます。今日の結果ではリークあり版のギャップが+0.1588(大幅な過大評価)、正しい版のギャップが-0.0431(過大評価なし)でした。

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

📐
Stackingを直感で: Stackingのメタモデルを「複数の専門家の意見を聞いて最終判断を下す上司」に例えると分かりやすいです。3人の専門家(LR・RF・GB)がそれぞれ意見(予測確率)を出し、上司(メタモデル)はその3つの意見を材料に最終判断をします。ここで重要なのは、上司が聞いている「意見」が本物でなければならないということ。専門家が「事前に答えを教えてもらった上での意見」(in-sample予測)を言っていたら、上司はその意見を過大に信頼してしまい、本番(答えを知らない場面)ではその判断基準が全く役に立ちません。
📝
身近な例: 模擬試験の「自己採点」と「本番の採点」の違いに近い状況です。自分で自分の答案を採点すると驚くほど高得点になりますが、それは実力を表していません。第三者(=学習に使っていないFold)に採点してもらって初めて、本当の実力(OOFスコア)が分かります。今日のリークあり版は「自己採点の結果を、進路指導の先生(メタモデル)に本当の模試結果として報告してしまった」ようなものです。

数式を見たら

ŷ_meta = σ(β₀ + β₁p̂_LR + β₂p̂_RF + β₃p̂_GB)

「3モデルの予測確率p̂を入力として、ロジスティック回帰の係数βで重み付けし、シグモイド関数σで0〜1の確率に変換する」という意味。Day 102の重み付き平均w1p̂₁+w2p̂₂+w3p̂₃Σw=1, w≥0の制約あり)と似ているが、Stackingのβには非負制約も合計1の制約もなく、シグモイドを通すため確率空間ではなくロジット(対数オッズ)空間での線形結合になっている点がAveragingとの数学的な違い。

🏆 Kaggleでの実践的な使い方

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

  • 上位解法の定番構成: Otto Group Product Classification・Home Credit Default Riskなど多くのTabularコンペの上位解法で「Level-0に10〜20個の多様なモデル、Level-1〜2にLightGBM/LogisticRegressionのメタモデル」という多層Stackingが使われている
  • Level-2・Level-3への拡張: 今日はLevel-0→Level-1の2階建てだったが、大規模コンペではLevel-1の出力をさらに別のLevel-2メタモデルに入れる3階建て構成も珍しくない。層を増やすほどリーク管理とCV設計の複雑さが増すため、今日のような「CV/OOFスコア vs 真のholdoutスコア」の検証を必ず挟むのが定石
  • Stackingが効かない・悪化するケースの認識: 今日の結果ではholdout AUCでRank Average(0.7937)が最良、正しいStacking(0.7924)は僅差2位だった。ベースモデルの多様性が乏しい・データ量が少ない場面ではStackingの複雑さに見合う効果が出ないことも多く、「まずAveragingを試し、伸び代がありそうならStackingを試す」という順序が実務的
  • 本番運用でのholdout検証の重要性: 今日構築した「独立したholdoutセットでCV/OOFスコアと真の性能を突き合わせる」検証フローは、Kaggleに限らず実務のMLパイプラインでリークを検出する標準的な手法

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
Level-0モデルをtrain全体で学習し、そのpredict_proba(X)をそのままメタ特徴量にする「学習済みモデルがあるなら、それで予測すればいい」という直感学習に使った行への予測はin-sample予測でありリーク。必ずOOF方式(Day 102と同じ)でメタ特徴量を作る
Stackingは必ずAveragingより強いと思い込む「手法が複雑・高度=性能が良い」という思い込み今日の結果でもRank Average(holdout 0.7937)が正しいStacking(0.7924)よりわずかに良かった。ベースモデルの多様性やデータ量次第でAveragingの方が安定することも多い
CV/OOF AUCが高ければ「良いStackingができた」と判断するCVスコアは常に信頼できるという前提リークあり版はCV AUC 0.9281という非常に高い値を出したが、真のholdout AUCは0.7693で単独モデルにも劣った。CVスコアは「正しく作られていれば」信頼できるに過ぎない
holdoutを一切用意せず、CV/OOFスコアだけでStackingの効果を判断する「Kaggleでは公式のtrain/testしかない」という思い込み手元のtrainデータの一部を「検証専用のholdout」として切り出し、パイプライン全体(Level-0〜Level-1)の健全性を確認する習慣が重要
メタモデルにも複雑なモデル(GBDTなど)を使えば必ず良くなると思う「強いモデルを積めば積むほど強くなる」という発想メタモデルの入力はベースモデルの数だけ(今日は3列)しかなく、複雑なモデルは少ない特徴量に対して過学習しやすい。まずはLogisticRegressionのようなシンプルなメタモデルから試すのが定石

🚀 次のステップ

  • 発展: メタモデルをLogisticRegressionから浅いGradientBoostingClassifiermax_depth=2程度)に変えて、非線形なメタモデルがholdout AUCを改善するか、それとも3特徴量に対して過学習してholdout AUCを悪化させるかを確認してみる
  • 次回予告: Day 104「アンサンブル③(Blending)」 — hold-outベースのBlending。今日のStackingはKFold全体を使ってOOF予測を作る「重い」手法だったが、次回は1回のtrain/holdout分割だけで済む「軽量版」のBlendingを学び、StackingとBlendingのトレードオフ(精度 vs 実装の簡潔さ・計算コスト)を比較する

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

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