Day 166 — Playground Series 実戦投入④ — 性質の違うモデルを混ぜる。log_Dur は Ridge に −0.047 と桁違いに効いたが、Ridge の重みは 0。OOF で見えた混ぜる改善 −0.00028 は、提出する形(5モデル平均)では −0.00006 に縮んだ

2026-10-02 金 / Phase 6(月替わりテーマ2: Playground Series 実戦投入) コーディング→分析 HistGB・Ridge・k近傍 × 4ワールド × 分割seed 4通り(400 fit)

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

アンサンブル(混ぜる)の考え方。 同じくらい上手で、間違え方が違う予想屋を2人雇い、平均を取ると、1人より当たる。逆に、2人が同じ間違え方をするなら平均を取っても変わらない。混ぜて得をする条件は「個々の精度」より「間違え方の違い(残差の相関の低さ)」である。Kaggle の上位解法で GBDT・NN・線形モデルを混ぜるのはこのため。

今日の3人の予想屋:

モデル得意・不得意今日使う特徴量
HistGB(既定値、決定7)曲がった関係も拾える。割り算・掛け算の形は木を何本も重ねて近似する決定6の9列
Ridge(線形回帰+縮小)「足し算の形」しか表せない。目的変数が rate × Duration のような掛け算なら、対数を取ると足し算になる(log(a×b)=log a+log b)9列、+ log_Dur、+ log 4列
k近傍(15近傍の平均)似た行の平均を取る。木と違い、特徴量の「距離」で決まるlog_Dur・Heart_Rate・Weight・Age・Sex(標準化)

Day164 からの持ち越し。 Day164 で log_Dur は木のモデルでは差がちょうど0だった(単調変換は木の分割点を変えない)。ただしその後ろに「線形モデルでは効くはず」という予想を置いたまま測っていない。今日測る。

今日の問い:

  1. log_Dur は Ridge に効くか。効くなら、Ridge は HistGB に追いつくか
  2. 3モデルを混ぜると HistGB 単独より良くなるか。重みを OOF(Out-Of-Fold: 分割で学習に使わなかった行の予測)で決めたとき、見えた改善と本当の改善はどれだけずれるか
  3. 混ぜる重みは分割を変えても安定するか

用語整理:

用語意味
OOF5分割で、各行を「その行を学習に使っていない」モデルで予測した値。これで train 全体の点数が出る
新分割R2分割seed 1〜3 の5分割×3回、15個の fold 差から平均と SE を出したもの(Day164 から。重みは分割seed 0 で決めたものを固定して使う)
真の効果test 6万行で、分割seed 1〜3 の予測を平均したものどうしの RMSLE の差(行で対にしてデルタ法で SE、Day164・165 と同じ)
楽観真の差 − 見えた差(×10⁻⁴)。正なら見かけの改善の方が大きい
提出する形5分割で作った5モデルの test 予測の平均(Day163 の設計)。OOF の予測は1モデルずつの予測で、提出する形とは違う

縮小シミュレータは Day163〜165 と同じ(目的変数は Keytel et al. 2005, J Sports Sci 23(3):289–297 の心拍数からの消費カロリー式 × 運動時間)。ワールド0〜3 は Day164 のワールド0〜3 と同じ乱数のデータ(元15k・train 3万・test 6万)。数値は仮定の上の実測。

❓ 問題

タスクA(予想)— コードを書く前に紙に書く

  1. log_Dur を足すと Ridge の RMSLE はどれだけ下がるか。log 4列(log_Dur・log_HR・log_W・log_Keytel)まで足した Ridge は HistGB に勝てるか
  2. 3モデルを OOF の重みで混ぜたとき、HistGB 単独に対する改善は、Day164 の特徴量1本(−0.00051)・Day165 のチューニング(−0.0002〜−0.0006)と比べて大きいか小さいか
  3. 重みは Ridge と k近傍のどちらに多く付くか
  4. OOF の重みで見えた改善は、test で見た本当の改善より大きいか。大きいなら原因は「重みを同じデータで決めたこと」だけか

タスクB(コーディング)

  1. Day165 のシミュレータで、HistGB・Ridge(3種の特徴量)・k近傍の OOF と test 予測を、分割seed 0〜3 で作る(day166.py)
  2. 表1(単独の点数)、表2(log 特徴量の効果)、表3(混ぜる改善の見かけと本当)、表4(重みの安定性)、表5(行を分けて重みの楽観を測る)を出す(analyze166.py)
  3. 補助: 3モデルの残差相関、1モデルで予測した test と 5モデル平均の test の差(diag166.py)

タスクC(決定)

CV 設計書に「決定8: モデルを混ぜるか、どう混ぜるか」を足す。採らなかった案と、前提が変わったときの判断も書く。

💡 ヒント

ヒント1(方向性)

  • 混ぜて得をするかは「個々の点数」と「間違え方の似かた」の両方で決まる。点数の悪いモデルでも、間違え方が違えば混ぜる価値がある
  • 見えた改善の「甘さ」には少なくとも2つの出どころがある。重みを決めたデータで測ること、そして測っている予測の形と提出する予測の形が違うこと

ヒント2(アプローチ)

  • 重みは非負・合計1に制限し、0.05 刻みの全探索で RMSLE を最小にする(モデル3つなら231通り。Breiman 1996 の stacked regressions も非負制約を置く)
  • 行を半分に割って、半分で重みを決め、もう半分で測れば、「同じデータで決めた楽観」だけが分かる
  • test を fold ごとに1モデルで予測した値を保存しておくと、「1モデル」と「5モデル平均」を比べられる

ヒント3(コード骨格)

コードを開く(7 行)
oof = np.zeros((4, n_models, N_TRAIN)); tp = np.zeros((4, n_models, N_TEST))
for s in range(4):                                   # 分割seed 0〜3
    for k, (tr, va) in enumerate(KFold(5, shuffle=True, random_state=s).split(train)):
        for j, name in enumerate(MODELS):
            pv, pt = fit_predict(name, tr_df, y_log, [train.iloc[va], test])
            oof[s, j, va] = pv; tp[s, j] += pt / 5   # tp = 5モデル平均
w = best_w(y, oof[0, ids], grid(3))                  # 重みは分割seed 0 の OOF だけで決める

✅ 模範解答

タスクA(予想の模範と、実測との照合)

#予想の模範実測
1log_Dur で 0.03〜0.05 下がる。log 4列でさらに下がるが HistGB には届かない当たり。ridge_raw 0.2295 → +log_Dur 0.1798 → +log 4列 0.1655(ワールド0 test)。log_Dur 1列で −0.0497〜−0.0531(4ワールド、zT −55〜−57)、4列で −0.0640〜−0.0668。HistGB は 0.1555 で、4列の Ridge でも +0.0100 負ける
20.0001〜0.0005 程度で、特徴量1本と同じ桁かそれ以下当たり。ただし「見える」改善と「本当の」改善で割れた。OOF の最適重み(hgb 0.85〜0.9・knn 0.10〜0.15)で見えた改善は −0.00022〜−0.00034、test の本当の改善は −0.00021〜+0.00010(4ワールドの平均 −0.00006)。ワールド1 では本当は悪化(+0.97×10⁻⁴、zT +1.28)
3Ridge の方が多く付く(log で線形化したので)外れ。 Ridge の重みは 20/20 回の重み決定すべてで 0。k近傍が 0.10〜0.15。Ridge は HistGB と残差の相関が 0.934〜0.939 と高く、k近傍は 0.885〜0.889 と低い。点数の悪い Ridge は「HistGB と同じ間違え方をする悪い版」だった
4大きい。原因は同じデータで重みを決めたこと半分当たり。 見かけ −0.00028 に対し本当は −0.00006(見かけの方が大きい)。ただし重みだけの楽観は 4ワールド平均 +0.29×10⁻⁴ しかない(表5)。大きいのは、OOF が測る「1モデル」と提出する「5モデル平均」の差で、混ぜる改善が 1モデルでは −2.4〜−4.3 のものが 5モデル平均では −0.4〜−2.3(×10⁻⁴、約 2×10⁻⁴ 縮む)になる

タスクB(完全なコード)

day166.py(モデル作成。ワールド番号を引数に、4並行で各 67〜69秒):

コードを開く(112 行)
"""Day166: Playground S5E5 縮小シミュレータで「性質の違うモデルを混ぜる」を測る。
モデル3つ(HistGB / Ridge+log特徴量 / k近傍)を、分割seed 0〜3 の5分割で OOF 予測し、
(1) Ridge に log_Dur を足すと効くか (2) 混ぜる重みを OOF で決めたときの楽観 (3) 重みの安定性 を数える。
使い方: OMP_NUM_THREADS=2 python day166.py <world>   -> raw_w<world>.npz
"""
import sys, time
import numpy as np, pandas as pd
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.linear_model import Ridge
from sklearn.neighbors import KNeighborsRegressor
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import KFold

T0 = time.time()
W = int(sys.argv[1])
N_ORIG, N_TRAIN, N_TEST = 15000, 30000, 60000


def keytel_rate(sex, hr, w, age):
    m = (-55.0969 + 0.6309 * hr + 0.1988 * w + 0.2017 * age) / 4.184
    f = (-20.4022 + 0.4472 * hr - 0.1263 * w + 0.074 * age) / 4.184
    return np.where(sex == 1, m, f)


def make_original(n, rng):  # Day163-165 と同じ
    sex = rng.integers(0, 2, n); age = rng.integers(20, 80, n)
    h = np.where(sex == 1, rng.normal(175, 7, n), rng.normal(162, 6.5, n)).round()
    w = (rng.normal(24.5, 2.5, n) * (h / 100) ** 2).round()
    dur = rng.integers(1, 31, n).astype(float)
    hr = np.clip(80 + 1.2 * dur + rng.normal(0, 5, n), 67, 128).round()
    temp = (37 + 0.35 * dur ** 0.5 + rng.normal(0, 0.2, n)).round(1)
    rate = np.clip(keytel_rate(sex, hr, w, age), 0.3, None)
    cal = np.clip((rate * dur * np.exp(rng.normal(0, 0.05, n))).round(), 1, None)
    return pd.DataFrame(dict(Sex=sex, Age=age, Height=h, Weight=w, Duration=dur,
                             Heart_Rate=hr, Body_Temp=temp, Calories=cal))


def synth_from(orig, n, rng):  # Day163-165 と同じ
    s = orig.sample(n, replace=True, random_state=int(rng.integers(1 << 30))).reset_index(drop=True)
    s["Age"] = np.clip((s.Age + rng.normal(0, 1.5, n)).round(), 20, 79)
    s["Height"] = (s.Height + rng.normal(0, 2, n)).round()
    s["Weight"] = (s.Weight + rng.normal(0, 2, n)).round()
    s["Duration"] = np.clip((s.Duration + rng.normal(0, 1.0, n)).round(), 1, 30)
    s["Heart_Rate"] = (s.Heart_Rate + rng.normal(0, 2, n)).round()
    s["Body_Temp"] = (s.Body_Temp + rng.normal(0, 0.1, n)).round(1)
    s["Calories"] = np.clip((s.Calories * np.exp(rng.normal(0, 0.03, n))).round(), 1, None)
    return s


def add_feats(d):
    d = d.copy()
    d["Dur_x_HR"] = d.Duration * d.Heart_Rate                       # 決定6
    d["Keytel_kcal"] = np.clip(keytel_rate(d.Sex.values, d.Heart_Rate.values, d.Weight.values, d.Age.values),
                               0.3, None) * d.Duration              # 決定6
    d["log_Dur"] = np.log(d.Duration)
    d["log_HR"] = np.log(d.Heart_Rate)
    d["log_W"] = np.log(d.Weight)
    d["log_Keytel"] = np.log(d.Keytel_kcal)
    return d


BASE = ["Sex", "Age", "Height", "Weight", "Duration", "Heart_Rate", "Body_Temp", "Dur_x_HR", "Keytel_kcal"]
SETS = {"hgb": BASE,
        "ridge_raw": BASE,
        "ridge_logDur": BASE + ["log_Dur"],                          # 1列だけ足す(Day164 の問いを線形モデルで)
        "ridge_log": BASE + ["log_Dur", "log_HR", "log_W", "log_Keytel"],
        "knn": ["log_Dur", "Heart_Rate", "Weight", "Age", "Sex"]}
MODELS = list(SETS)


def make(name):
    if name == "hgb":  # 決定7の既定値、モデル側の乱数なし
        return HistGradientBoostingRegressor(early_stopping=False, random_state=0, learning_rate=0.1,
                                             max_iter=300, max_leaf_nodes=31, min_samples_leaf=20)
    if name.startswith("ridge"):
        return Ridge(alpha=1.0)
    return KNeighborsRegressor(n_neighbors=15, algorithm="kd_tree")


def fit_predict(name, tr_df, ytr_log, preds_dfs):
    cols = SETS[name]
    if name == "hgb":
        m = make(name).fit(tr_df[cols], ytr_log)
        return [m.predict(d[cols]) for d in preds_dfs]
    sc = StandardScaler().fit(tr_df[cols])
    m = make(name).fit(sc.transform(tr_df[cols]), ytr_log)
    return [m.predict(sc.transform(d[cols])) for d in preds_dfs]


rng = np.random.default_rng(2000 + W)        # Day164/165 のワールド W と同じデータ
orig = make_original(N_ORIG, rng)
pool = synth_from(orig, N_TRAIN + N_TEST, rng)
orig, pool = add_feats(orig), add_feats(pool)
train, test = pool.iloc[:N_TRAIN].reset_index(drop=True), pool.iloc[N_TRAIN:].reset_index(drop=True)

oof = np.zeros((4, len(MODELS), N_TRAIN))      # [分割seed, モデル, 行]  すべて log1p 空間
tp = np.zeros((4, len(MODELS), N_TEST))
tpf = np.zeros((5, len(MODELS), N_TEST), dtype=np.float32)   # 分割seed1 の fold ごとの test 予測(1モデルずつ)
for s in range(4):
    for k, (tr, va) in enumerate(KFold(5, shuffle=True, random_state=s).split(train)):
        tr_df = pd.concat([train.iloc[tr], orig], ignore_index=True)
        y = np.log1p(tr_df.Calories.values)
        for j, name in enumerate(MODELS):
            pv, pt = fit_predict(name, tr_df, y, [train.iloc[va], test])
            oof[s, j, va] = pv
            tp[s, j] += pt / 5
            if s == 1:
                tpf[k, j] = pt
    print(f"world {W} split {s} done {time.time()-T0:.0f}s", flush=True)
np.savez_compressed(f"raw_w{W}.npz", oof=oof, tp=tp, tpf=tpf, ytr=train.Calories.values, yt=test.Calories.values,
                    models=np.array(MODELS))
print(f"world {W} wall {time.time()-T0:.0f}s")

analyze166.py(集計):

コードを開く(110 行)
"""Day166 集計: raw_w*.npz を読み、表1〜5 を出す。使い方: python analyze166.py"""
import glob, itertools
import numpy as np, pandas as pd
from sklearn.model_selection import KFold

pd.set_option("display.width", 250)
files = sorted(glob.glob("raw_w*.npz"))
M = ["hgb", "ridge_raw", "ridge_logDur", "ridge_log", "knn"]
ix = {m: i for i, m in enumerate(M)}


def sle(y, lp):
    return (np.log1p(np.clip(np.expm1(lp), 1, None)) - np.log1p(y)) ** 2


def rmsle(y, lp):
    return np.sqrt(sle(y, lp).mean())


def folds(y, lp, s):
    return np.array([np.sqrt(sle(y[va], lp[va]).mean()) for _, va in KFold(5, shuffle=True, random_state=s).split(y)])


def grid(k, step=0.05):
    n = int(round(1 / step))
    return [np.array(c) * step for c in itertools.product(range(n + 1), repeat=k) if sum(c) == n]


def best_w(y, P, G):  # P: (k, rows)
    sc = [rmsle(y, np.tensordot(w, P, axes=1)) for w in G]
    return G[int(np.argmin(sc))]


def compare(y, yt, oof, tp, a, b):
    """a が b より良いか(a-b、負=a が良い)。新分割R2(分割seed1〜3の15 fold差)と test の真の効果"""
    d15 = np.concatenate([folds(y, oof[s, a], s) - folds(y, oof[s, b], s) for s in (1, 2, 3)])
    dF, seF = d15.mean(), d15.std(ddof=1) / np.sqrt(15)
    pa, pb = tp[1:, a].mean(0), tp[1:, b].mean(0)
    ea, eb = sle(yt, pa), sle(yt, pb)
    dT = np.sqrt(ea.mean()) - np.sqrt(eb.mean())
    seT = (ea - eb).std(ddof=1) / np.sqrt(len(yt)) / (2 * np.sqrt(eb.mean()))
    return dF, seF, dT, seT


G3, G2 = grid(3), grid(2)
t1, t2, t3, t4, t5 = [], [], [], [], []
for f in files:
    w = int(f[-5])
    z = np.load(f); oof, tp, y, yt = z["oof"], z["tp"], z["ytr"], z["yt"]
    # 表1 単独モデルの点数
    for m in M:
        t1.append(dict(world=w, model=m, oof_s0=rmsle(y, oof[0, ix[m]]),
                       test=rmsle(yt, tp[1:, ix[m]].mean(0))))
    # 表2 線形モデルへの log 特徴量
    for a, b in [("ridge_logDur", "ridge_raw"), ("ridge_log", "ridge_raw"), ("ridge_log", "ridge_logDur")]:
        dF, seF, dT, seT = compare(y, yt, oof, tp, ix[a], ix[b])
        t2.append(dict(world=w, pair=f"{a} - {b}", dF=dF * 1e4, zF=dF / seF, dT=dT * 1e4, zT=dT / seT))
    # 表3 混ぜる: 分割seed0 の OOF で重みを決め、新分割R2 と test で測る
    for tag, names, G in [("hgb+ridge_log", ["hgb", "ridge_log"], G2),
                          ("hgb+ridge_log+knn", ["hgb", "ridge_log", "knn"], G3)]:
        ids = [ix[n] for n in names]
        wts = best_w(y, oof[0, ids], G)
        for label, wv in [("OOF最適", wts), ("等分", np.ones(len(ids)) / len(ids))]:
            blend_oof = np.tensordot(wv, oof[:, ids], axes=([0], [1]))  # (4, rows)
            blend_tp = np.tensordot(wv, tp[:, ids], axes=([0], [1]))
            oo, tt = oof.copy(), tp.copy()
            oo[:, 0] = blend_oof; tt[:, 0] = blend_tp                    # 0番目(hgb)を混ぜた予測で置換
            dS = rmsle(y, blend_oof[0]) - rmsle(y, oof[0, 0])
            fs0 = folds(y, blend_oof[0], 0) - folds(y, oof[0, 0], 0)
            seS = fs0.std(ddof=1) / np.sqrt(5)
            # 比較相手の hgb 単独は元の配列から
            d15 = np.concatenate([folds(y, blend_oof[s], s) - folds(y, oof[s, 0], s) for s in (1, 2, 3)])
            dF, seF = d15.mean(), d15.std(ddof=1) / np.sqrt(15)
            ea, eb = sle(yt, blend_tp[1:].mean(0)), sle(yt, tp[1:, 0].mean(0))
            dT = np.sqrt(ea.mean()) - np.sqrt(eb.mean())
            seT = (ea - eb).std(ddof=1) / np.sqrt(len(yt)) / (2 * np.sqrt(eb.mean()))
            t3.append(dict(world=w, mix=tag, weights=label + str(np.round(wv, 2).tolist()), dS=dS * 1e4, zS=dS / seS,
                           dF=dF * 1e4, zF=dF / seF, dT=dT * 1e4, zT=dT / seT,
                           opt_S=(dT - dS) * 1e4, opt_F=(dT - dF) * 1e4))
    # 表4 重みの安定性: 分割seed 0〜3 それぞれで決めた重み、test で決めた重み
    ids = [ix["hgb"], ix["ridge_log"], ix["knn"]]
    for s in range(4):
        t4.append(dict(world=w, source=f"OOF split{s}", **dict(zip(["hgb", "ridge_log", "knn"], best_w(y, oof[s, ids], G3)))))
    t4.append(dict(world=w, source="test(正解)", **dict(zip(["hgb", "ridge_log", "knn"], best_w(yt, tp[1:, ids].mean(0), G3)))))
    # 表5 重みを決める行と測る行を分ける: 行の半分で重みを決め、残り半分で測る(OOF split0、20回)
    rng = np.random.default_rng(w)
    gaps = []
    for _ in range(20):
        perm = rng.permutation(len(y)); A, B = perm[: len(y) // 2], perm[len(y) // 2:]
        P = oof[0, ids]
        wv = best_w(y[A], P[:, A], G3)
        gA = rmsle(y[A], np.tensordot(wv, P[:, A], axes=1)) - rmsle(y[A], P[0, A])
        gB = rmsle(y[B], np.tensordot(wv, P[:, B], axes=1)) - rmsle(y[B], P[0, B])
        gaps.append((gA, gB))
    gaps = np.array(gaps)
    t5.append(dict(world=w, 決めた半分=gaps[:, 0].mean() * 1e4, 測った半分=gaps[:, 1].mean() * 1e4,
                   楽観=(gaps[:, 1] - gaps[:, 0]).mean() * 1e4, 楽観SE=(gaps[:, 1] - gaps[:, 0]).std(ddof=1) / np.sqrt(20) * 1e4))

T1 = pd.DataFrame(t1).pivot(index="world", columns="model", values=["oof_s0", "test"])
print("=== 表1 単独モデルの RMSLE(oof_s0 = 分割seed0のOOF、test = 6万行)==="); print(T1.round(5).to_string())
print("\n=== 表2 log特徴量は線形モデルに効くか(差 ×1e-4、負 = 前者が良い)==="); print(pd.DataFrame(t2).round(2).to_string(index=False))
print("\n=== 表3 混ぜる: hgb 単独との差 ×1e-4(負 = 混ぜた方が良い)、opt = 真の差 − 見えた差 ==="); T3 = pd.DataFrame(t3); print(T3.round(2).to_string(index=False))
print("\n=== 表4 重みの安定性 ==="); print(pd.DataFrame(t4).round(2).to_string(index=False))
print("\n=== 表5 行を分けて重みを測る(hgb単独との差 ×1e-4、20回平均)==="); print(pd.DataFrame(t5).round(2).to_string(index=False))

T2, T4 = pd.DataFrame(t2), pd.DataFrame(t4)
assert (T2[T2.pair == "ridge_logDur - ridge_raw"].zT < -10).all(), "log_Dur 1列で Ridge は全ワールドで大きく良くなる"
assert (T1["test"]["ridge_log"] > T1["test"]["hgb"]).all(), "log 4列の Ridge でも HistGB には全ワールドで負ける"
assert (T4[T4.source.str.startswith("OOF")].ridge_log == 0).all(), "OOF で決めた重みで Ridge は 16/16 回すべて 0"
print("all asserts passed")

diag166.py(補助。残差相関と、1モデル対 5モデル平均):

コードを開く(36 行)
"""Day166 補助: (1) 3モデルの残差相関 (2) test 予測を 5モデル平均(1分割) と 15モデル平均(3分割) で作ったときの混ぜる効果"""
import glob
import numpy as np
M = ["hgb", "ridge_raw", "ridge_logDur", "ridge_log", "knn"]; ix = {m: i for i, m in enumerate(M)}
def sle(y, lp): return (np.log1p(np.clip(np.expm1(lp), 1, None)) - np.log1p(y)) ** 2
def rm(y, lp): return np.sqrt(sle(y, lp).mean())
print("world | 残差相関 hgb-ridge_log / hgb-knn / ridge_log-knn (OOF split0, log空間) | test RMSLE hgb: 1分割(5モデル平均) 3分割(15モデル平均) | 混ぜる(0.9/0/0.1)との差 ×1e-4: OOF split0, test 1分割, test 3分割")
for f in sorted(glob.glob("raw_w*.npz")):
    z = np.load(f); oof, tp, y, yt = z["oof"], z["tp"], z["ytr"], z["yt"]
    ly = np.log1p(y); lyt = np.log1p(yt)
    r = {m: oof[0, ix[m]] - ly for m in ["hgb", "ridge_log", "knn"]}
    c = lambda a, b: np.corrcoef(r[a], r[b])[0, 1]
    h1, h3 = rm(yt, tp[1, 0]), rm(yt, tp[1:, 0].mean(0))
    w = np.array([0.9, 0, 0.1]); ids = [0, 3, 4]
    bl = lambda P: np.tensordot(w, P[ids], axes=1)
    d_oof = rm(y, bl(oof[0])) - rm(y, oof[0, 0])
    d_t1 = rm(yt, bl(tp[1])) - rm(yt, tp[1, 0])
    d_t3 = rm(yt, bl(tp[1:].mean(0))) - rm(yt, tp[1:, 0].mean(0))
    print(f"{f[-5]} | {c('hgb','ridge_log'):.3f} / {c('hgb','knn'):.3f} / {c('ridge_log','knn'):.3f} | {h1:.5f} {h3:.5f} | {d_oof*1e4:+.2f} {d_t1*1e4:+.2f} {d_t3*1e4:+.2f}")

print("\n--- 1モデル(fold単独) と 5モデル平均 の test 差(分割seed1、混ぜる 0.9/0/0.1 − hgb、×1e-4)---")
for f in sorted(glob.glob("raw_w*.npz")):
    z = np.load(f); tpf, tp, yt = z["tpf"].astype(np.float64), z["tp"], z["yt"]
    w = np.array([0.9, 0, 0.1]); ids = [0, 3, 4]
    single = [rm(yt, np.tensordot(w, tpf[k][ids], axes=1)) - rm(yt, tpf[k, 0]) for k in range(5)]
    avg5 = rm(yt, np.tensordot(w, tp[1][ids], axes=1)) - rm(yt, tp[1, 0])
    h_single = np.mean([rm(yt, tpf[k, 0]) for k in range(5)]); h_avg = rm(yt, tp[1, 0])
    print(f"world {f[-5]}: 1モデル平均 {np.mean(single)*1e4:+.2f} (範囲 {min(single)*1e4:+.2f}〜{max(single)*1e4:+.2f}) | 5モデル平均 {avg5*1e4:+.2f} | hgb 単独 test RMSLE 1モデル {h_single:.5f} 5モデル平均 {h_avg:.5f}")

print("\n--- 固定重み 0.9/0/0.1 の本当の効果(test、分割seed1〜3 平均、hgb 単独との差)---")
for f in sorted(glob.glob("raw_w*.npz")):
    z = np.load(f); tp, yt = z["tp"], z["yt"]
    w = np.array([0.9, 0, 0.1]); ids = [0, 3, 4]
    ea, eb = sle(yt, np.tensordot(w, tp[1:][:, ids].mean(0), axes=1)), sle(yt, tp[1:, 0].mean(0))
    d = np.sqrt(ea.mean()) - np.sqrt(eb.mean()); se = (ea - eb).std(ddof=1) / np.sqrt(len(yt)) / (2 * np.sqrt(eb.mean()))
    print(f"world {f[-5]}: dT {d*1e4:+.2f}e-4  zT {d/se:+.2f}")

実行結果(実測)

# ============ 実行環境: macOS (M1, メモリ8GB)・Python 3.13.12・scikit-learn 1.9.1・numpy 2.5.3・pandas 3.0.6(新規 uv venv)
#              4ワールドを別プロセスで並行(各 OMP_NUM_THREADS=2)。壁時計 66〜69秒(初回は 84〜87秒)。fit 数 = 4分割seed × 5 fold × 5モデル = 100 / ワールド
#              2回回して oof・tp が完全一致(最大差 0.0)。2回目は fold ごとの test 予測 tpf を足して保存 ============
=== 表1 単独モデルの RMSLE(oof_s0 = 分割seed0のOOF、test = 6万行)===
        oof_s0                                               test                                          
model      hgb      knn ridge_log ridge_logDur ridge_raw      hgb      knn ridge_log ridge_logDur ridge_raw
world                                                                                                      
0      0.15830  0.17251   0.16713      0.18146   0.22884  0.15550  0.16922   0.16549      0.17983   0.22951
1      0.15773  0.17186   0.16730      0.18134   0.23287  0.15529  0.17050   0.16574      0.18115   0.23123
2      0.15943  0.17523   0.16882      0.18214   0.23426  0.15673  0.17047   0.16632      0.18002   0.23317
3      0.15780  0.17330   0.16804      0.18250   0.23375  0.15777  0.17354   0.16852      0.18198   0.23399

=== 表2 log特徴量は線形モデルに効くか(差 ×1e-4、負 = 前者が良い)===
 world                     pair      dF      zF      dT     zT
     0 ridge_logDur - ridge_raw -473.71  -78.81 -496.77 -55.53
     0    ridge_log - ridge_raw -617.03 -119.52 -640.16 -69.40
     0 ridge_log - ridge_logDur -143.33  -63.67 -143.39 -38.40
     1 ridge_logDur - ridge_raw -515.34  -67.92 -500.78 -55.47
     1    ridge_log - ridge_raw -655.60  -73.52 -654.92 -70.88
     1 ridge_log - ridge_logDur -140.26  -58.86 -154.13 -41.05
     2 ridge_logDur - ridge_raw -521.33  -60.22 -531.43 -57.14
     2    ridge_log - ridge_raw -654.60  -90.47 -668.47 -69.98
     2 ridge_log - ridge_logDur -133.27  -43.51 -137.04 -39.02
     3 ridge_logDur - ridge_raw -512.48  -50.14 -520.07 -55.50
     3    ridge_log - ridge_raw -657.15  -59.44 -654.70 -67.28
     3 ridge_log - ridge_logDur -144.66  -48.08 -134.63 -35.48

=== 表3 混ぜる: hgb 単独との差 ×1e-4(負 = 混ぜた方が良い)、opt = 真の差 − 見えた差 ===
 world               mix                weights    dS    zS    dF    zF    dT    zT  opt_S  opt_F
     0     hgb+ridge_log      OOF最適[0.95, 0.05] -0.35 -1.11 -0.62 -3.17  1.08  5.38   1.43   1.70
     0     hgb+ridge_log           等分[0.5, 0.5] 19.55  7.20 17.37 10.74 29.97 14.18  10.42  12.59
     0 hgb+ridge_log+knn   OOF最適[0.9, 0.0, 0.1] -2.96 -3.70 -3.46 -7.22 -1.24 -2.57   1.71   2.22
     0 hgb+ridge_log+knn   等分[0.33, 0.33, 0.33] 26.80  6.98 23.54 10.72 35.33 12.37   8.52  11.79
     1     hgb+ridge_log        OOF最適[1.0, 0.0]  0.00   NaN  0.00   NaN  0.00   NaN   0.00   0.00
     1     hgb+ridge_log           等分[0.5, 0.5] 23.58  9.21 21.72 10.70 31.36 13.94   7.78   9.64
     1 hgb+ridge_log+knn OOF最適[0.85, 0.0, 0.15] -3.37 -4.92 -3.48 -7.59  0.97  1.28   4.34   4.45
     1 hgb+ridge_log+knn   等分[0.33, 0.33, 0.33] 28.88 10.71 27.95 13.24 40.52 13.25  11.65  12.57
     2     hgb+ridge_log      OOF最適[0.95, 0.05] -0.34 -0.70 -0.34 -1.88  0.66  3.12   1.00   1.00
     2     hgb+ridge_log           等分[0.5, 0.5] 21.15  4.74 21.31 11.41 26.79 12.47   5.64   5.48
     2 hgb+ridge_log+knn   OOF最適[0.9, 0.0, 0.1] -2.22 -2.25 -1.88 -3.73 -2.09 -4.06   0.13  -0.21
     2 hgb+ridge_log+knn   等分[0.33, 0.33, 0.33] 32.67  5.73 33.64 12.30 32.30 11.04  -0.36  -1.33
     3     hgb+ridge_log      OOF最適[0.95, 0.05] -0.09 -0.37 -0.27 -1.10  0.83  3.63   0.92   1.09
     3     hgb+ridge_log           等分[0.5, 0.5] 24.42 10.59 23.37 10.71 30.52 13.13   6.10   7.15
     3 hgb+ridge_log+knn   OOF最適[0.9, 0.0, 0.1] -2.44 -3.48 -2.68 -6.65 -0.22 -0.42   2.22   2.46
     3 hgb+ridge_log+knn   等分[0.33, 0.33, 0.33] 33.99  9.83 32.95 13.58 42.05 13.44   8.06   9.09

=== 表4 重みの安定性 ===
 world     source  hgb  ridge_log  knn
     0 OOF split0 0.90        0.0 0.10
     0 OOF split1 0.85        0.0 0.15
     0 OOF split2 0.85        0.0 0.15
     0 OOF split3 0.85        0.0 0.15
     0   test(正解) 0.90        0.0 0.10
     1 OOF split0 0.85        0.0 0.15
     1 OOF split1 0.90        0.0 0.10
     1 OOF split2 0.85        0.0 0.15
     1 OOF split3 0.85        0.0 0.15
     1   test(正解) 0.95        0.0 0.05
     2 OOF split0 0.90        0.0 0.10
     2 OOF split1 0.90        0.0 0.10
     2 OOF split2 0.90        0.0 0.10
     2 OOF split3 0.90        0.0 0.10
     2   test(正解) 0.90        0.0 0.10
     3 OOF split0 0.90        0.0 0.10
     3 OOF split1 0.90        0.0 0.10
     3 OOF split2 0.90        0.0 0.10
     3 OOF split3 0.90        0.0 0.10
     3   test(正解) 0.95        0.0 0.05

=== 表5 行を分けて重みを測る(hgb単独との差 ×1e-4、20回平均)===
 world  決めた半分  測った半分    楽観  楽観SE
     0  -3.54  -2.19  1.35  0.69
     1  -3.26  -3.30 -0.04  0.54
     2  -2.21  -2.01  0.20  0.30
     3  -2.17  -2.54 -0.37  0.29
all asserts passed

world | 残差相関 hgb-ridge_log / hgb-knn / ridge_log-knn (OOF split0, log空間) | test RMSLE hgb: 1分割(5モデル平均) 3分割(15モデル平均) | 混ぜる(0.9/0/0.1)との差 ×1e-4: OOF split0, test 1分割, test 3分割
0 | 0.939 / 0.889 / 0.904 | 0.15560 0.15550 | -2.96 -1.37 -1.24
1 | 0.939 / 0.888 / 0.904 | 0.15542 0.15529 | -3.25 -0.43 -0.26
2 | 0.937 / 0.886 / 0.907 | 0.15689 0.15673 | -2.22 -2.31 -2.09
3 | 0.934 / 0.885 / 0.905 | 0.15787 0.15777 | -2.44 -0.44 -0.22

--- 1モデル(fold単独) と 5モデル平均 の test 差(分割seed1、混ぜる 0.9/0/0.1 − hgb、×1e-4)---
world 0: 1モデル平均 -3.50 (範囲 -4.17〜-2.87) | 5モデル平均 -1.37 | hgb 単独 test RMSLE 1モデル 0.15694 5モデル平均 0.15560
world 1: 1モデル平均 -2.39 (範囲 -2.62〜-2.02) | 5モデル平均 -0.43 | hgb 単独 test RMSLE 1モデル 0.15669 5モデル平均 0.15542
world 2: 1モデル平均 -4.30 (範囲 -4.57〜-4.09) | 5モデル平均 -2.31 | hgb 単独 test RMSLE 1モデル 0.15816 5モデル平均 0.15689
world 3: 1モデル平均 -2.62 (範囲 -3.18〜-1.86) | 5モデル平均 -0.44 | hgb 単独 test RMSLE 1モデル 0.15924 5モデル平均 0.15787

--- 固定重み 0.9/0/0.1 の本当の効果(test、分割seed1〜3 平均、hgb 単独との差)---
world 0: dT -1.24e-4  zT -2.57
world 1: dT -0.26e-4  zT -0.51
world 2: dT -2.09e-4  zT -4.06
world 3: dT -0.22e-4  zT -0.42

読み方: dS は分割seed 0 の OOF、dF は新分割R2、dT は test で見た「混ぜた − HistGB単独」(×10⁻⁴、負なら改善)。opt_S・opt_F は真の差 − 見えた差(正なら見かけの方が大きい)。「OOF最適」の重みは分割seed 0 の OOF だけで決めた。hgb+ridge_log のワールド1 は重みが hgb 1.0 になり、混ぜていない(差 0、z は 0÷0 で NaN)。

📊 結果の可視化

図1 log_Dur は線形モデルに桁違いに効く。それでも HistGB には届かない

test RMSLE が HistGB より悪い分(×10⁻⁴、ワールド0。短いほど良い)ridge_raw(9列)+740(test 0.2295)ridge_logDur(+log_Dur 1列)+243(test 0.1798)ridge_log(+log 4列)+99(test 0.1655)knn(15近傍)+137(test 0.1692)HistGB(既定値)+0(test 0.1555)

ridge_raw → +log_Dur で 497(×10⁻⁴、zT −55)縮み、log 4列でさらに 144。HistGB では log_Dur の差がちょうど0だった(Day164)。

図2 混ぜる改善は、測る「形」で大きさが変わる

0.9/0/0.1 で混ぜた改善(×10⁻⁴、下に長いほど改善)OOF(1モデル)test・1モデルtest・5モデル平均(提出の形)0-1-2-3-4-2.96-3.50-1.37W0-3.25-2.39-0.43W1-2.22-4.30-2.31W2-2.44-2.62-0.44W3

青(OOF)と紫(test の1モデル)は同じ大きさ。黄(提出の形、5モデル平均)だけ約 2×10⁻⁴ 短い。重みは 0.9/0/0.1 で固定、test は分割seed 1 の5モデル。

🧭 決定 — 混ぜ方と CV 設計書「決定8」

タスクC(決定)

(1) 手順: モデルは HistGB を土台にし、k近傍を重み 0.10 で固定して混ぜる。Ridge は混ぜない。重みは OOF で細かく決めず、丸めた値で固定する。混ぜる効果は「OOF の改善 − 0.0002」と見積もる。

案判定理由(今日の数字)
等分(1/3ずつ)で混ぜる採らない4ワールドで hgb 単独より +0.0032〜+0.0042 悪化(zT +11〜+13)。点数の悪いモデルを同じ重みで混ぜた
OOF 最適重み(hgb 0.85〜0.9, ridge 0, knn 0.10〜0.15)をそのまま使う採らない見かけ −0.00028 に対し本当は −0.00006。ワールド1 は 0.85/0/0.15 で本当は +0.97×10⁻⁴ 悪化。OOF の重みは test の最適(hgb 0.90〜0.95)より knn に 0.05〜0.10 多く振れた(表4、ワールド1・3)
Ridge を混ぜる採らない重みは 16/16 回すべて 0(OOF 4分割seed × 4ワールド)。log 4列で単独 0.1655 まで来ても HistGB と残差相関 0.934〜0.939 で、混ぜる情報が無い
k近傍を 0.10 固定で混ぜる採る0.9/0/0.1 の本当の差は 4ワールドとも負(−1.24・−0.26・−2.09・−0.22 ×10⁻⁴)。ただし小さく、zT(0.9/0/0.1 固定)は −2.57・−0.51・−4.06・−0.42 で、−2 を下回ったのは2ワールドだけ。費用は k近傍1回の学習で、悪化しない側に倒れた
HistGB 単独で出す次点混ぜる本当の改善は平均 −0.00006 で、公開 LB の揺れ(Day163 の実物換算 約 0.0009、未照合)の 1/10 以下。混ぜなくても失うものは測れない大きさ。k近傍を採ったのは「4/4 で悪化しなかった」という、4ワールドだけの根拠
stacking(メタモデルで重みを学習)・NN を混ぜる今回は採らない非線形に混ぜる自由度が増えると楽観が増える(Day165 の探索と同じ構造)。今日は重み2個の線形混合までしか測っていない

注意(今日の比較の混ざり): 表3の「新分割R2(dF)」は重みを決めた分割seed 0 と同じ train 3万行で測っているため、行に由来する楽観は残る。表5(行を半分に割る)で重みだけの楽観は 4ワールド平均 +0.29×10⁻⁴(SE 0.3〜0.7)と小さかったが、ワールド0 だけ +1.35±0.69 で 2SE に近い。

(2) CV 設計書に書き足す「決定8: モデルを混ぜるか」(S5E5 用)

#決定採った案採らなかった案と理由前提が変わったら
8モデルを混ぜるか、どう混ぜるかHistGB(決定6・7 の設定)の5分割平均に、k近傍(log_Dur・Heart_Rate・Weight・Age・Sex、15近傍)を重み 0.10 で混ぜる。Ridge は混ぜない。重みは丸めて固定。報告する改善幅は「OOF の改善 − 0.0002」(実測の縮み 1.96〜2.18×10⁻⁴、4ワールド)等分混合(+0.003 悪化)、OOF 最適重み(knn 過多でワールド1 は悪化)、Ridge を混ぜる(重み 0)、stacking・NN(楽観の自由度が増える)提出を5分割平均でなく1モデルにする場合は OOF の改善がそのまま近い(1モデルの test では −2.4〜−4.3 ×10⁻⁴)ので、OOF 最適重みを使ってよい。GBDT 側に bagging・列抽出などの乱数があるモデルなら、5モデル平均の縮みは別に測る。Ridge は線形モデルが特徴量の足し算で強い問題(目的変数が加法的に分解できる)では重みが付く可能性がある。この問題では木が Duration × Heart_Rate の形を拾えるため 0

🪜 Step-by-Step

Step 1: 4つの「特徴量の組」を用意する

HistGB と ridge_raw は同じ決定6の9列。ridge_logDur は 9列+log_Dur の1列だけ足す(Day164 の「log_Dur は効くか」を線形モデルで隔離して測るため)。ridge_log は 9列+log 4列。k近傍は距離で効くので、似た意味の列を重ねず5列に絞り、log_Dur を使う(Duration の間隔は短い所ほど意味が大きい)。k近傍の列の選び方は測っていない(扱わなかった範囲)。

Step 2: 学習は log1p 空間に統一する

目的変数 log1p(Calories) に3モデルとも回帰する。RMSLE がこの空間の二乗誤差の平均の平方根そのものなので、混ぜる計算(log 空間の重み付き平均)と評価が噛み合う。Ridge と k近傍は標準化してから学習した(Ridge の縮小の強さを列の単位で変えないため)。

Step 3: 予想を作るときの「形」を2通り保存する

各 fold で、(a) 検証用行の予測(OOF)、(b) test 6万行の予測を作る。(b) は fold ごとに足して5で割り、5モデル平均として保存する(Day163 からの提出の形)。さらに分割seed 1 だけは fold ごとの予測も保存した(tpf、1モデルの予測)。今日の主発見はこの2つの比較から出た。

Step 4: log_Dur は線形モデルでは巨大に効く(表2)

ridge_raw 0.2295 → ridge_logDur 0.1798(ワールド0 test)。1列で −0.0497。4ワールドとも zT は −55〜−57。HistGB では同じ列が差ちょうど0だった(Day164)ので、特徴量の価値はモデルの種類で決まり、Day164 の「log_Dur は無意味」は木に限った結論だった。log 4列で 0.1655 まで来るが、HistGB の 0.1555 には届かない。

Step 5: 混ぜる重みを決める(表4)

0.05 刻みで RMSLE を最小にする重みを全探索した。Ridge の重みは 16/16 回で 0。理由は残差相関(log 空間、分割seed 0 の OOF)で、hgb と ridge_log は 0.934〜0.939、hgb と knn は 0.885〜0.889。点数も ridge_log(OOF 0.167〜0.169)は knn(0.172〜0.175)より良いのに、knn の方が HistGB と違う間違え方をした。

Step 6: 見えた改善と本当の改善(表3)

OOF の重みで混ぜた3モデルは、OOF で hgb 単独に −2.22〜−3.37 ×10⁻⁴(zS −2.25〜−4.92)。同じ重みで test を測ると −2.09〜+0.97 ×10⁻⁴。平均すると見かけ −2.75、本当 −0.65。

Step 7: 楽観の出どころを2つに分ける(表5 と 診断)

  • 重みを同じデータで決めた楽観: 行を半分で割る実験(表5)で 4ワールド平均 +0.29 ×10⁻⁴。小さい
  • OOF が測る形と提出する形の違い: 同じ重み 0.9/0/0.1 で test を測ると、fold ごとの1モデルでは −2.39〜−4.30、5モデル平均では −0.43〜−2.31。縮み 1.96〜2.18 ×10⁻⁴ が4ワールドでほぼ一定。HistGB 単独の test RMSLE も、1モデル 0.1567〜0.1592 から 5モデル平均 0.1554〜0.1579 に下がる。5モデルを平均すると、k近傍を混ぜて取りたかった「分散を減らす効果」を HistGB 自身がすでに取ってしまう(仮説。分割seed 1 の5モデルから15モデルに増やしても混ぜる改善は −1.37→−1.24 などほぼ動かず、分散の残りは小さいことと整合する)
  • 採否の判定で何が起きたか: Day164・165 の R2(新分割の fold 差)は、OOF 同士の比較なので「1モデルの差」を測る。今日は R2 で zF −3〜−7 と全ワールド採用になるが、真の効果は zT −2 を下回ったのが 2/4 ワールドだけ。R2 が偽採用を出したのは、行の問題でなく測る対象の形が違うから

Step 8: 決定を設計書に足す

タスクCの表。決定6(特徴量)→ 決定7(チューニング)→ 決定8(混ぜる)の順で、S5E5 の手順は「特徴量を決める → 既定値を含めて探す → 5分割平均で提出する形を決める」になる。

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

なぜ対数を取ると線形モデルが効くのか。 消費カロリーは「1分あたりの消費量 × 運動時間」の掛け算に近い。掛け算は線グラフの形で足し算に直せない(時間が2倍で結果が2倍=比例)。対数を取ると log(掛け算)=log(1分あたり)+log(時間) と足し算になり、「足し算しか知らない」Ridge でも表せる。木のモデルは「この値より大きいか」で分けるだけなので、値を対数に引き直しても大小の順は変わらず、分け方は同じ(Day164 の差0)。

残差相関。 各モデルが「どれだけ・どちらに間違えたか」を並べて、2つのモデルで連動するか見る数。1に近いと「同じ間違え方」で、混ぜても平均が元と同じになる。0.93 は高く、0.89 はやや低い。Ridge の相関が高いのは、同じ特徴量(決定6の列)を使い、同じ「滑らかな関係」を学習するから、と考えられる(因果は未検証)。

1モデルと5モデル平均の違い。 5人の予想屋の平均は、1人の予想より誤差が小さい(ランダムな間違いが打ち消し合う)。HistGB は5分割でデータの見え方が少しずつ違う5モデルを作るので、平均を取るだけで1モデルより良くなる。k近傍を混ぜる効果の一部は同じ「ランダムな間違いの打ち消し」なので、平均した後には残りが少ない。例え: 運転が荒い友人1人の評価を直すのに別の友人の意見を足すと効くが、すでに5人の友人の意見を平均した後では足す効果が薄い。

非負の重みと合計1。 重みに負を許すと、「あるモデルの逆を足して間違いを打ち消す」ような不安定な混ぜ方が最適に見え、新しいデータで外れやすい(Breiman 1996, Machine Learning 24(1):49–64, stacked regressions が非負制約を勧める理由。書誌は記憶による、未照合)。

🏆 Kaggleでの実践的な使い方

  • 特徴量の価値はモデルごとに測る。 木で効かなかった特徴量(log_Dur)が NN や線形モデルでは桁違いに効く(今日 −0.05)。GBDT 用に作った特徴量セットを NN にそのまま渡すと損をする。逆に GBDT 向けの交互作用(掛け算)は NN が自力で学ぶことも多い
  • 混ぜる候補は「点数」でなく「残差相関」で選ぶ。 今日は点数の良い Ridge(0.1655)より悪い k近傍(0.169)の方が重みを得た。Kaggle の上位解法で GBDT と NN を混ぜるのは、点数が近く、間違え方が違うため(一般に言われる傾向。今日の測定は GBDT 対 k近傍・Ridge のみ)
  • 混ぜる重みは粗く固定する。 今日の OOF 重みは test の最適より knn に 0.05〜0.10 多く振れた(ワールド1・3)。0.05 刻みの全探索でも、分割seed 間で hgb 0.85〜0.90 に揺れた(表4)。重みの細かい違いは 1×10⁻⁴ 未満の差しか生まない
  • OOF の改善を、提出する形の改善と取り違えない。 5分割のモデルを平均して提出するなら、OOF で見えた混ぜる改善は約 0.0002 過大になる(今日の4ワールドで 1.96〜2.18×10⁻⁴)。Playground の「公開 LB で混ぜるか決める」でも、公開 LB の揺れ(約 0.0009、未照合)より改善が小さいので、どちらにしても決め手にならない
  • 混ぜる前に、同じ特徴量・同じ分割・同じ提出の形で測る。 今日のずれは「測る形」を揃えると消えた。Day164・165 の R2 が一致した場面(モデルが1つ)と、今日の R2 が偽採用を出した場面(混ぜる)の違いは、ここにある

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
log_Dur は無意味な特徴量だDay164 で差が0だった木に対してだけ。Ridge に1列足すと −0.0497(4ワールド zT −55〜−57)
点数の良いモデルほど混ぜる価値がある強いモデル同士を混ぜる直感Ridge(0.1655)は k近傍(0.169)より良いのに重み 0。決めるのは残差相関(0.934〜0.939 対 0.885〜0.889)
重みは OOF で細かく最適化すれば良い「最適」の言葉に引かれる分割seed ごとに 0.85〜0.90 で揺れ、test の最適より knn が 0.05〜0.10 多く出た(ワールド1・3)。丸めて固定する
OOF で見えた混ぜる改善がそのまま提出の改善になるOOF と提出が同じ予測だと思うOOF は1モデルの予測、提出は5モデル平均。混ぜる改善は −2.4〜−4.3 から −0.4〜−2.3(×10⁻⁴)に縮んだ
R2 が採用と言えば混ぜて良いDay164・165 で R2 が真と一致した今日の R2 は 4/4 採用、真の効果で採用に足りたのは 2/4。R2 は OOF(1モデル)の差を測るので、提出する形が違うと外れる
等分で混ぜておけば無難平均は安全に見える点数の悪いモデルが入ると +0.003 悪化(4ワールドとも zT +11〜+13)

🚧 扱わなかった範囲と理由

  • ワールド数: 4ワールド、混ぜる判定は 3モデル混合で4件。「縮み 0.0002」は 4件の 1.96〜2.18 から出した値で、データや提出の形が変われば変わる。言えるのは今日の4件の中身まで
  • 縮みの原因の特定: 「5モデル平均が分散を取ってしまう」は仮説である。分割seed 1 の5モデルと3分割の15モデルでの比較(−1.37→−1.24 など)は整合するだけで、1モデルでの訓練データ量の違い(OOF 側は train の 80%+元データ、提出側も同じ 80%+元データだが test の行は別)との分離はしていない。分けるには、学習に使う行の割合を変えた1モデルで同じ測定をする必要がある(次回候補)
  • モデルの種類: Ridge は alpha=1 固定、k近傍は k=15・5列固定で、チューニングしていない。k近傍の列選択(Day164 の log_Dur を入れた効果)は未測定。NN・LightGBM・CatBoost・stacking のメタモデルは測っていない(Day165 のチューニングの楽観を混ぜる側にも持ち込むことになるため)
  • 重み探索の粗さ: 0.05 刻みの全探索、非負・合計1。刻みを細かくした場合や、scipy.optimize の NNLS との差は未測定
  • log 空間で混ぜる前提: 予測を log1p 空間で重み付き平均した。元の空間(カロリーの値)で混ぜる場合との差は未測定。Day163〜165 の「5モデル平均」は元の空間の平均だったが、今日の truth は log 空間の平均で作った(差は小さいと仮定、未測定)
  • Nadeau & Bengio 補正・実データ(S5E5)・Day162 の選択バイアス仮説: Day164〜165 から持ち越し(実データは Kaggle のログインとルール同意が必要)
  • 文献との照合: Wolpert 1992(Neural Networks 5(2):241–259, stacked generalization)、Breiman 1996(上記)、Caruana et al. 2004(ICML, ensemble selection)は存在を記憶しているが、今日は原文を開いていない。今日の結論は文献でなく測定による

🚀 次のステップ

  • 発展: 「OOF が1モデル、提出が5モデル平均」のずれを、学習データ量を変えた1モデル(50%・80%)で測り、縮みが学習データ量で変わるかを見る。k近傍の列から log_Dur を外したときの混ぜる改善を測る
  • 次回予告: Day167 — Playground Series 実戦投入⑤(最終提出の選び方。決定6〜8 で作った提出の形を、公開 LB(約 20%)と private(約 80%)が割れる状況でどう2本選ぶか。Day163 の公開 LB の揺れとシミュレーションで、公開 LB 最良を選ぶ・CV 最良を選ぶ・混ぜた1本と単独1本を選ぶ、の3方針を shake-up の大きさで比べる)

Phase 6 の学習マップ

1-23
Phase6主要テーマ・総復習(Day126-149、完了)
月替わり1
NLP系コンペへの応用①〜⑬(Day150-162、完了)
月替わり2
Playground Series 実戦投入①〜④(Day163〜、〜2026-10-25)

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

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