Day 170 — 2026-10-03 | Playground Series 実戦投入⑤ — 最終提出の2本をどう選ぶか。公開LB最良は CV 最良より当たりやすかった(公開20%)が、公開が小さいと事前固定に負ける。いちばん損が小さいのは「CV最良+公開最良」の2枠だった

2026-10-03 金 / Phase 6(月替わりテーマ2: Playground Series 実戦投入) 戦略→分析 HistGB · 16ワールド · 960 fit

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

公開LBと非公開LB。 Kaggle のコンペでは、提出した予想を test の一部(公開、Playground ではおよそ2割、本シリーズの仮定)で採点して順位表(公開LB)に出し、最終順位は残りの大部分(非公開)で決まる。最終順位に使う提出は2本まで選べる(記憶による。今日は公式ルールを開いていない)。公開LBで1位でも、非公開で順位が落ちることを「シェイクアップ」と呼ぶ。

なぜ起きるか(直感)。 公開LBは、行数の少ない小さなテストにすぎない。小さなテストで測った点数は、実力と運(たまたま簡単な行が多かった)の合計になる。上位が僅差だと、運の幅が実力の差より大きくなり、順位が入れ替わる。

今日の3つの選び方(方針)。

方針中身想定される言い分
公開LB最良公開点数が最も良い候補を選ぶ「本物の未知データで測った唯一の点数」
CV最良train の5分割の点数(OOF)が最も良い候補を選ぶ「公開LBは小さくて運が大きい。大きなCVを信じよ」(Kaggle で広く言われる原則)
事前固定決定8 の形(既定値の HistGB に k近傍を0.1混ぜた1本)から動かない「選ぶこと自体が損を生むなら選ばない」

さらに、2枠(CV最良 + 公開最良の2本を出し、非公開の良い方が最終順位に使われる)と、ハイブリッド(公開上位3本の中からCV最良/CV上位3本の中から公開最良)を比べる。

用語整理:

用語意味
候補同じ train から作った提出候補。広い13本=HistGB の設定違い8本+特徴量違い3本+決定8の混合2本。狭い10本=設定違いと混合だけ(特徴量違いを除く)
真の点数候補が学習に一度も使っていない別の6万行(truth)での RMSLE。最終順位の「期待値」の代わり
損(regT)選んだ候補の真の点数 − 候補中の真の最良(×10⁻⁴、小さいほど良い)
損(regP)選んだ候補の非公開点数 − 候補中の非公開の最良。順位表に実際に出る損
当たり(hitT)損が 0.5×10⁻⁴ 以内(ほぼ真の最良)だった割合
公開20%test 2万行のうち無作為に4,000行を公開、残り16,000行を非公開(Day163 の公開LBの大きさを写した)。5%=1,000行、50%=10,000行も測る

縮小シミュレータは Day163〜166 と同じ構造(目的変数は Keytel et al. 2005, J Sports Sci 23(3):289–297 の心拍数からの消費カロリー式 × 運動時間)。ただし train 3万・元15k・test 2万・truth 6万で、ワールド0〜15 は Day164〜166 のワールドとは別のデータ(乱数の系列を3000+ワールドに変えた)。数値は仮定の上の実測。

データ・候補の構造(スキーマ)

列型意味・作り方使うモデル
Sex, Age, Height, Weightint / float性別・年齢・身長・体重(元データ 1.5万行から合成、Day163〜 と同じ)全候補(raw7 も含む)
Duration, Heart_Rate, Body_Tempfloat運動時間・心拍数・体温全候補
Dur_x_HRfloatDuration × Heart_Rate(決定6)HistGB(noDurHR を除く)
Keytel_kcalfloatKeytel 2005 の式 × Duration(決定6)HistGB(noKeytel を除く)
log_Durfloatlog(Duration)k近傍のみ
Calories(目的変数)floatlog1p 空間で学習、RMSLE で評価—
train / test / truth3万 / 2万 / 6万行test を公開/非公開に再分割(5/20/50%)。truth は候補が一度も見ない別の6万行(真の点数)—

❓ 問題

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

  1. 公開20%(4,000行)のとき、「公開LB最良」と「CV最良」はどちらが真の最良に近いか。理由も添える
  2. 公開LBで1位の候補は、非公開では13本中何位くらいか(一様ランダムなら平均7位)
  3. 「CV最良+公開最良」の2枠は、単独の選び方より損が小さくなるか。小さくなる場合、それは「選び方が賢いから」か「非公開の良い方が採られるという採点規則のおかげ」か
  4. 公開が5%(1,000行)まで小さくなったとき、公開LB最良は事前固定よりまだ良いか

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

  1. 候補13本(HistGB 11本 + k近傍 + 混合2本)の OOF(分割seed 0 の5分割)と、test・truth の予測(5モデル平均)を16ワールドで作る(day170.py)
  2. 公開/非公開を500回無作為に再分割し、6方針の損を出す(analyze170.py)
  3. 補助: CV と真の点数のずれが候補ごとに偏るか、公開LBの「候補間の差」の揺れ、主要な対の paired SE(diag170.py)

タスクC(決定)

CV 設計書に「決定12: 最終提出の2本の選び方」を足す。採らなかった案と、前提が変わったときの判断も書く。

💡 ヒント

ヒント1(方向性)

  • 公開LBの絶対点数の揺れ(約0.003)と、候補どうしの差の揺れは別物。同じ4,000行で全候補を測るので、行の難しさは共通で、差を取ると相殺される
  • CV と真の点数のずれが「全候補に同じだけ」なら順位に影響しない。候補ごとに違うとき(偏り)に順位を壊す

ヒント2(アプローチ)

  • 公開/非公開の再分割は、候補ごとの行ごとの二乗誤差(20,000行)を保存しておき、無作為な行の集合の平均を行列積でまとめて出す
  • 真の点数は、再分割とは別の6万行で測る。公開/非公開と真が同じ行を共有しない

ヒント3(コード骨格)

E = np.stack([sle(y_test, ev[j, :N_TEST]) for j in ids])            # (候補, 2万行)
M = np.zeros((N_TEST, R))
for r in range(R): M[rng.choice(N_TEST, n_pub, replace=False), r] = 1   # 公開に入る行
U = np.sqrt((E @ M) / n_pub)                                         # 公開点数 (候補, R)
P = np.sqrt((E.sum(1, keepdims=True) - E @ M) / (N_TEST - n_pub))    # 非公開点数
pub = U.argmin(0); cv = C.argmin()                                   # 公開最良 / CV最良

✅ 模範解答

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

#予想の模範実測
1CV最良の方が近い(公開4,000行の運は大きいと考えるため)外れ。 公開20%(広い13本)の損は 公開LB最良 2.11、CV最良 2.80(×10⁻⁴)。世界ペアの差は −0.69±0.45(1.5SE、小さな差)で、少なくとも「CVの方が良い」は言えない。公開5%では逆転して +0.50±0.42、公開50%では −1.30±0.50(2.6SE)で公開の方が良い
2ほぼ一様ランダム(5〜7位)外れ。 公開20%で平均 4.1位(広い)/2.9位(狭い10本)。公開と非公開の順位相関は 0.53/0.63。公開5%では 5.3位(広い)まで悪化
3単独より小さい。主因は2本のうち非公開の良い方が採られる採点規則当たり。公開20%(広い)の損 1.67 は 公開最良 2.11 より −0.44±0.19(2.3SE)、CV最良 2.80 より小さい。公開5%では −1.29±0.24(5.4SE)と差が広がり、公開50%では −0.00±0.16 と差が消える。「CV最良+公開最良」の方が「事前固定+公開最良」(1.89)より 0.22 小さいが、この差の SE は測っていない
4公開LB最良は事前固定に負ける当たり。公開5%で 公開LB最良 − 事前固定 = +0.29±0.19(1.5SE、符号は悪化側だが小さな差)。公開20%では −0.89±0.23(3.9SE)で公開LB最良が良い

「CV を信じよ」が今回の設計で当たらなかった理由は、タスクBの補助解析(補助(4))にある。 CV と真の点数のずれが候補ごとに偏っていた(leaf63 は default より +5.73±0.98、leaf15 は −4.51±0.73、×10⁻⁴)。真の最良は 16世界中 14世界で leaf63 だったのに、CV が選んだのは 1世界だけ(lr05_it600+knn.1 が8世界、default+knn.1 が4世界)。

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

day170.py(候補を作る。ワールド番号を引数に、8並行でも1並列でも出力は同じ):

"""Day170: 最終提出の選び方。候補12本を作り、OOF(CV)・test(公開/非公開)・truth(大きな別holdout)を保存する。
使い方: OMP_NUM_THREADS=1 python day170.py <world>   -> raw170_w<world>.npz
"""
import sys, time
import numpy as np, pandas as pd
from sklearn.ensemble import HistGradientBoostingRegressor
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, N_TRUTH = 15000, 30000, 20000, 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-166 と同じ
    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-166 と同じ
    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
    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
    d["log_Dur"] = np.log(d.Duration)
    return d


RAW = ["Sex", "Age", "Height", "Weight", "Duration", "Heart_Rate", "Body_Temp"]
BASE = RAW + ["Dur_x_HR", "Keytel_kcal"]                    # 決定6
D = dict(learning_rate=0.1, max_iter=300, max_leaf_nodes=31, min_samples_leaf=20)   # 決定7の既定値
# (名前, 列, パラメータ上書き)  先頭11本が HistGB。pool「狭い」= 0〜7(同じ列で設定だけ違う)、「広い」= 0〜11
CANDS = [("default", BASE, {}),
         ("leaf15", BASE, dict(max_leaf_nodes=15)),
         ("leaf63", BASE, dict(max_leaf_nodes=63)),
         ("lr05_it600", BASE, dict(learning_rate=0.05, max_iter=600)),
         ("msl50", BASE, dict(min_samples_leaf=50)),
         ("msl5", BASE, dict(min_samples_leaf=5)),
         ("l2_1", BASE, dict(l2_regularization=1.0)),
         ("it150", BASE, dict(max_iter=150)),
         ("noKeytel", [c for c in BASE if c != "Keytel_kcal"], {}),
         ("noDurHR", [c for c in BASE if c != "Dur_x_HR"], {}),
         ("raw7", RAW, {})]
KNN = ["log_Dur", "Heart_Rate", "Weight", "Age", "Sex"]

rng = np.random.default_rng(3000 + W)
orig = make_original(N_ORIG, rng)
pool = synth_from(orig, N_TRAIN + N_TEST + N_TRUTH, rng)
orig, pool = add_feats(orig), add_feats(pool)
train = pool.iloc[:N_TRAIN].reset_index(drop=True)
evalset = pool.iloc[N_TRAIN:].reset_index(drop=True)       # 先頭 N_TEST = test、残り = truth
names = [c[0] for c in CANDS] + ["knn"]
oof = np.zeros((len(names), N_TRAIN)); ev = np.zeros((len(names), N_TEST + N_TRUTH))
for k, (tr, va) in enumerate(KFold(5, shuffle=True, random_state=0).split(train)):
    tr_df = pd.concat([train.iloc[tr], orig], ignore_index=True)
    y = np.log1p(tr_df.Calories.values)
    for j, (nm, cols, over) in enumerate(CANDS):
        m = HistGradientBoostingRegressor(early_stopping=False, random_state=0, **{**D, **over}).fit(tr_df[cols], y)
        oof[j, va] = m.predict(train.iloc[va][cols]); ev[j] += m.predict(evalset[cols]) / 5
    sc = StandardScaler().fit(tr_df[KNN]); m = KNeighborsRegressor(n_neighbors=15, algorithm="kd_tree").fit(sc.transform(tr_df[KNN]), y)
    oof[-1, va] = m.predict(sc.transform(train.iloc[va][KNN])); ev[-1] += m.predict(sc.transform(evalset[KNN])) / 5
    print(f"world {W} fold {k} {time.time()-T0:.0f}s", flush=True)
# 決定8: default に knn を 0.10 で混ぜた候補、lr05_it600 に混ぜた候補を足す(log 空間)
for base in (0, 3):
    oof = np.vstack([oof, 0.9 * oof[base] + 0.1 * oof[-1]]); ev = np.vstack([ev, 0.9 * ev[base] + 0.1 * ev[-1]])
    names.append(f"{names[base]}+knn.1")
np.savez_compressed(f"raw170_w{W}.npz", oof=oof, ev=ev.astype(np.float32), ytr=train.Calories.values,
                    yev=evalset.Calories.values, names=np.array(names))
print(f"world {W} wall {time.time()-T0:.0f}s")

analyze170.py(6方針の損):

"""Day170 集計: raw170_w*.npz から、最終提出の選び方 6方針を公開/非公開の再分割500回で比べる。使い方: python analyze170.py"""
import glob
import numpy as np, pandas as pd
from scipy.stats import spearmanr

pd.set_option("display.width", 250)
N_TEST, R = 20000, 500
files = sorted(glob.glob("raw170_w*.npz"))


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


rows, noise, pool_rows, rank_rows = [], [], [], []
for f in files:
    w = int(f.split("_w")[1].split(".")[0])
    z = np.load(f); oof, ev, ytr, yev, names = z["oof"], z["ev"].astype(np.float64), z["ytr"], z["yev"], list(z["names"])
    keep = [i for i, n in enumerate(names) if n != "knn"]                       # knn 単独は候補に入れない
    narrow = [i for i in keep if names[i] in ("default", "leaf15", "leaf63", "lr05_it600", "msl50", "msl5", "l2_1", "it150",
                                              "default+knn.1", "lr05_it600+knn.1")]
    E_te = np.stack([sle(yev[:N_TEST], ev[j, :N_TEST]) for j in range(len(names))])          # (K, 20000) test の行ごと二乗誤差
    T = np.array([np.sqrt(sle(yev[N_TEST:], ev[j, N_TEST:]).mean()) for j in range(len(names))])   # 真の点数(別の6万行)
    C = np.array([np.sqrt(sle(ytr, oof[j]).mean()) for j in range(len(names))])                    # CV 点数
    fixed = names.index("default+knn.1")                                                           # 決定8 の形(事前に決めた1本)
    rng = np.random.default_rng(w)
    for pool_name, ids in [("広い13本", keep), ("狭い10本", narrow)]:
        ids = np.array(ids)
        pool_rows.append(dict(world=w, pool=pool_name, K=len(ids), 真の最良_最悪差e4=(T[ids].max() - T[ids].min()) * 1e4,
                              真のSD_e4=T[ids].std(ddof=1) * 1e4, CV_真の差SD_e4=(C[ids] - T[ids]).std(ddof=1) * 1e4,
                              CVと真の順位相関=spearmanr(C[ids], T[ids])[0]))
        for frac in (0.05, 0.20, 0.50):
            n_pub = int(N_TEST * frac)
            M = np.zeros((N_TEST, R))
            for r in range(R): M[rng.choice(N_TEST, n_pub, replace=False), r] = 1
            S_pub = E_te[ids] @ M; U = np.sqrt(S_pub / n_pub)                                   # (K, R) 公開点数
            S_pri = E_te[ids].sum(1, keepdims=True) - S_pub; P = np.sqrt(S_pri / (N_TEST - n_pub))  # (K, R) 非公開点数
            Tk, Ck = T[ids], C[ids]
            ibest_T = Tk.min(); ibest_P = P.min(0)
            fx = int(np.where(ids == fixed)[0][0])
            pub = U.argmin(0); cv = int(Ck.argmin())
            cv3 = np.argsort(Ck)[:3]; pub3 = np.argsort(U, axis=0)[:3]
            pol = {}
            pol["公開LB最良"] = pub
            pol["CV最良"] = np.full(R, cv)
            pol["事前固定(決定8)"] = np.full(R, fx)
            pol["公開上位3→CV最良"] = np.array([pub3[:, r][Ck[pub3[:, r]].argmin()] for r in range(R)])
            pol["CV上位3→公開最良"] = np.array([cv3[U[cv3, r].argmin()] for r in range(R)])
            for nm, ch in pol.items():
                rows.append(dict(world=w, pool=pool_name, frac=frac, policy=nm, regT=(Tk[ch] - ibest_T) * 1e4,
                                 regP=(P[ch, np.arange(R)] - ibest_P) * 1e4, hitT=(Tk[ch] - ibest_T < 0.5e-4).astype(float),
                                 worse_fixed=(Tk[ch] > Tk[fx] + 0.5e-4).astype(float)))
            # 2枠(CV最良 + 公開最良): 非公開の良い方が採用される。真の点数は採用された方
            two = np.stack([np.full(R, cv), pub])
            Ptwo = np.stack([P[two[0], np.arange(R)], P[two[1], np.arange(R)]]); pick = Ptwo.argmin(0)
            chosen = two[pick, np.arange(R)]
            rows.append(dict(world=w, pool=pool_name, frac=frac, policy="2枠: CV最良+公開最良", regT=(Tk[chosen] - ibest_T) * 1e4,
                             regP=(Ptwo.min(0) - ibest_P) * 1e4, hitT=(Tk[chosen] - ibest_T < 0.5e-4).astype(float),
                             worse_fixed=(Tk[chosen] > Tk[fx] + 0.5e-4).astype(float)))
            two2 = np.stack([np.full(R, fx), pub]); Ptwo2 = np.stack([P[two2[0], np.arange(R)], P[two2[1], np.arange(R)]]); pick2 = Ptwo2.argmin(0)
            ch2 = two2[pick2, np.arange(R)]
            rows.append(dict(world=w, pool=pool_name, frac=frac, policy="2枠: 事前固定+公開最良", regT=(Tk[ch2] - ibest_T) * 1e4,
                             regP=(Ptwo2.min(0) - ibest_P) * 1e4, hitT=(Tk[ch2] - ibest_T < 0.5e-4).astype(float),
                             worse_fixed=(Tk[ch2] > Tk[fx] + 0.5e-4).astype(float)))
            rows.append(dict(world=w, pool=pool_name, frac=frac, policy="(参考)一様ランダム", regT=(Tk - ibest_T).mean() * 1e4,
                             regP=(P.mean(0) - ibest_P).mean() * 1e4, hitT=(Tk - ibest_T < 0.5e-4).mean(),
                             worse_fixed=(Tk > Tk[fx] + 0.5e-4).mean()))
            # 公開点数と真の点数のずれ(候補×分割の SD)、公開順位と非公開順位の相関(シェイクアップ)
            noise.append(dict(world=w, pool=pool_name, frac=frac, 公開_真の差SD_e4=(U - Tk[:, None]).std(ddof=1) * 1e4,
                              非公開_真の差SD_e4=(P - Tk[:, None]).std(ddof=1) * 1e4,
                              公開と非公開の順位相関=np.mean([spearmanr(U[:, r], P[:, r])[0] for r in range(0, R, 10)]),
                              公開最良の非公開順位=np.mean([(P[:, r] < P[pub[r], r]).sum() + 1 for r in range(R)])))

R_ = pd.DataFrame([{k: (float(np.mean(v)) if k in ("regT","regP","hitT","worse_fixed") else v) for k, v in r.items()} for r in rows]); N_ = pd.DataFrame(noise); P_ = pd.DataFrame(pool_rows)
print(f"worlds={len(files)}  再分割 R={R}  test={N_TEST}行  公開割合 5/20/50%")
print("\n=== 表1 候補プールの広がり(×1e-4、世界ごと) ===")
print(P_.sort_values(["pool","world"]).round(2).to_string(index=False))
print("\n=== 表2 公開LB の揺れ(公開点数−真の点数のSD、公開と非公開の順位相関、公開最良が非公開で何位か) 世界平均 ===")
print(N_.groupby(["pool", "frac"]).mean(numeric_only=True).drop(columns="world").round(3).to_string())
g = R_.groupby(["pool", "frac", "policy"])
T3 = g.agg(regT=("regT", "mean"), regP=("regP", "mean"), hitT=("hitT", "mean"), worse_fixed=("worse_fixed", "mean"))
# 世界ごとの平均 → 世界間 SE
wm = R_.groupby(["pool", "frac", "policy", "world"]).regT.mean().groupby(["pool", "frac", "policy"])
T3["regT_SE"] = wm.std(ddof=1) / np.sqrt(len(files))
print("\n=== 表3 選び方6+方針の損(×1e-4)。regT=真の最良との差、regP=非公開の最良との差、hitT=真の最良から0.5以内の割合、worse_fixed=事前固定より0.5超悪い割合 ===")
print(T3.round(3).to_string())
# 世界ごと regT(20%、広い)で対差
a = R_[(R_.frac == 0.20)].groupby(["pool", "policy", "world"]).regT.mean().unstack("policy")
print("\n=== 表4 公開20% 世界別 regT(×1e-4) ===")
print(a.round(2).to_string())
pd.DataFrame(R_).to_pickle("res170.pkl")

diag170.py(補助。CVのずれの偏り・公開LBの差の揺れ・paired SE):

"""Day170 補助: (1) CV−真の差は候補ごとに符号がそろうか(OOF は1モデル、提出は5モデル平均)
(2) 公開LBの「候補間の差」の揺れ(公開−真の絶対値のずれより小さいはず) (3) 主要な対の世界間 paired SE"""
import glob
import numpy as np, pandas as pd
N_TEST = 20000
def sle(y, lp): return (np.log1p(np.clip(np.expm1(lp), 1, None)) - np.log1p(y)) ** 2
files = sorted(glob.glob("raw170_w*.npz"), key=lambda f: int(f.split("_w")[1].split(".")[0]))
bias, dnoise = [], {0.05: [], 0.2: [], 0.5: []}
for f in files:
    w = int(f.split("_w")[1].split(".")[0]); z = np.load(f)
    oof, ev, ytr, yev, names = z["oof"], z["ev"].astype(np.float64), z["ytr"], z["yev"], list(z["names"])
    T = np.array([np.sqrt(sle(yev[N_TEST:], ev[j, N_TEST:]).mean()) for j in range(len(names))])
    C = np.array([np.sqrt(sle(ytr, oof[j]).mean()) for j in range(len(names))])
    bias.append(pd.Series((C - T) * 1e4, index=names, name=w))
    E = np.stack([sle(yev[:N_TEST], ev[j, :N_TEST]) for j in range(len(names))]); rng = np.random.default_rng(w)
    d0 = names.index("default")
    for fr in dnoise:
        n = int(N_TEST * fr); out = []
        for r in range(200):
            m = np.zeros(N_TEST, bool); m[rng.choice(N_TEST, n, replace=False)] = True
            U = np.sqrt(E[:, m].mean(1)); out.append((U - U[d0]) - (T - T[d0]))
        dnoise[fr].append(np.array(out)[:, [i for i in range(len(names)) if i != d0 and names[i] != "knn"]].std() * 1e4)
B = pd.concat(bias, axis=1)
print("=== (1) CV−真(×1e-4)候補ごと: 16ワールドの平均 / SD / 符号が正の割合 ===")
print(pd.DataFrame({"mean": B.mean(1), "sd": B.std(1, ddof=1), "正の割合": (B > 0).mean(1)}).round(2).to_string())
print("\n=== (2) 公開LB上の「default との差」の揺れ SD(×1e-4、16ワールド平均)===")
for fr, v in dnoise.items(): print(f"公開 {int(fr*100)}%: {np.mean(v):.2f}")
R = pd.read_pickle("res170.pkl"); R = R[R.pool == "広い13本"]
print("\n=== (3) 世界ごとの paired 差(regT, ×1e-4, 平均±SE, n=16)===")
for fr in (0.05, 0.2, 0.5):
    a = R[R.frac == fr].groupby(["policy", "world"]).regT.mean().unstack("policy")
    for x, y in [("公開LB最良", "CV最良"), ("2枠: CV最良+公開最良", "公開LB最良"), ("公開LB最良", "事前固定(決定8)"), ("CV最良", "事前固定(決定8)")]:
        d = a[x] - a[y]; print(f"公開{int(fr*100):>2}% {x} − {y}: {d.mean():+.2f} ± {d.std(ddof=1)/np.sqrt(len(d)):.2f}")

print("\n=== (4) 候補ごとの『CV−真』の default からのずれ(×1e-4, 世界ペア, 平均±SE)と、CV最良/真の最良に選ばれた回数(広い13本, 16世界)===")
B2 = B.drop(index="knn"); D = B2.sub(B2.loc["default"], axis=1)
cnt_cv, cnt_T = pd.Series(0, index=B2.index), pd.Series(0, index=B2.index)
for f in files:
    z = np.load(f); oof, ev, ytr, yev, names = z["oof"], z["ev"].astype(np.float64), z["ytr"], z["yev"], list(z["names"])
    ids = [i for i, n in enumerate(names) if n != "knn"]
    T = np.array([np.sqrt(sle(yev[N_TEST:], ev[j, N_TEST:]).mean()) for j in ids]); C = np.array([np.sqrt(sle(ytr, oof[j]).mean()) for j in ids])
    cnt_cv[names[ids[C.argmin()]]] += 1; cnt_T[names[ids[T.argmin()]]] += 1
print(pd.DataFrame({"CV-真(default比)平均": D.mean(axis=1), "SE": D.std(axis=1, ddof=1) / 4, "CV最良回数": cnt_cv, "真の最良回数": cnt_T}).round(2).to_string())

実行結果(実測)

# ============ 実行環境: macOS (M1, メモリ8GB)・Python 3.13・scikit-learn 1.9.1・numpy 2.5.3・pandas 3.0.6・scipy 1.18.1(新規 uv venv)
#              16ワールド(8並行×2回、OMP_NUM_THREADS=1)。壁時計 324〜327秒(前半8)/194〜198秒(後半8)。1ワールドを単独で回すと 77秒。fit 数 = 12モデル × 5 fold = 60 / ワールド、計 960
#              ワールド15 を別プロセスで回し直し、oof・ev が完全一致(最大差 0.0)。前半と後半の壁時計の差の原因は未調査(他の負荷の差と思われるが確かめていない)
# 表1(候補プールの広がり)は 16ワールド × 2プール = 32行。要約(世界平均):
#   広い13本: 真の最良−最悪 10.32、候補間の真のSD 3.19、CV−真のSD(候補間)3.27、CVと真の順位相関 平均 0.41(範囲 0.00〜0.86)  ×10⁻⁴
#   狭い10本: 真の最良−最悪 10.27、候補間の真のSD 3.38、CV−真のSD(候補間)3.35、CVと真の順位相関 平均 0.33(範囲 −0.20〜0.81)
=== 表2 公開LB の揺れ(公開点数−真の点数のSD、公開と非公開の順位相関、公開最良が非公開で何位か) 世界平均 ===
            公開_真の差SD_e4  非公開_真の差SD_e4  公開と非公開の順位相関  公開最良の非公開順位
pool  frac                                                    
広い13本 0.05       67.655         3.856        0.339       5.298
      0.20       31.575         8.018        0.531       4.127
      0.50       15.832        15.833        0.616       3.726
狭い10本 0.05       68.983         3.856        0.406       3.631
      0.20       31.437         7.966        0.631       2.869
      0.50       15.680        15.683        0.712       2.750

=== 表3 選び方6+方針の損(×1e-4)。regT=真の最良との差、regP=非公開の最良との差、hitT=真の最良から0.5以内の割合、worse_fixed=事前固定より0.5超悪い割合 ===
                           regT   regP   hitT  worse_fixed  regT_SE
pool  frac policy                                                  
広い13本 0.05 (参考)一様ランダム     4.771  4.939  0.091        0.524    0.302
           2枠: CV最良+公開最良  1.998  1.751  0.366        0.112    0.260
           2枠: 事前固定+公開最良  2.273  2.083  0.258        0.084    0.202
           CV上位3→公開最良     2.968  3.047  0.064        0.216    0.331
           CV最良           2.795  2.832  0.125        0.125    0.451
           事前固定(決定8)      2.998  3.441  0.000        0.000    0.286
           公開LB最良         3.292  3.546  0.308        0.376    0.206
           公開上位3→CV最良     2.988  3.217  0.152        0.237    0.278
      0.20 (参考)一様ランダム     4.771  5.101  0.091        0.524    0.302
           2枠: CV最良+公開最良  1.667  1.563  0.452        0.085    0.234
           2枠: 事前固定+公開最良  1.891  1.839  0.373        0.070    0.222
           CV上位3→公開最良     2.716  2.940  0.082        0.174    0.280
           CV最良           2.795  2.997  0.125        0.125    0.451
           事前固定(決定8)      2.998  3.601  0.000        0.000    0.286
           公開LB最良         2.107  2.704  0.450        0.213    0.186
           公開上位3→CV最良     2.475  2.862  0.189        0.147    0.259
      0.50 (参考)一様ランダム     4.771  5.620  0.091        0.524    0.302
           2枠: CV最良+公開最良  1.493  1.696  0.504        0.070    0.277
           2枠: 事前固定+公開最良  1.664  1.886  0.450        0.066    0.279
           CV上位3→公開最良     2.605  3.309  0.098        0.149    0.273
           CV最良           2.795  3.496  0.125        0.125    0.451
           事前固定(決定8)      2.998  4.111  0.000        0.000    0.286
           公開LB最良         1.495  2.602  0.560        0.133    0.240
           公開上位3→CV最良     2.334  3.126  0.203        0.120    0.299
狭い10本 0.05 (参考)一様ランダム     4.524  4.682  0.112        0.469    0.277
           2枠: CV最良+公開最良  1.641  1.317  0.434        0.063    0.217
           2枠: 事前固定+公開最良  1.825  1.678  0.350        0.029    0.178
           CV上位3→公開最良     2.314  2.417  0.193        0.093    0.297
           CV最良           2.552  2.231  0.125        0.125    0.289
           事前固定(決定8)      2.996  3.270  0.000        0.000    0.287
           公開LB最良         2.341  2.656  0.410        0.205    0.137
           公開上位3→CV最良     2.622  2.746  0.157        0.148    0.264
      0.20 (参考)一様ランダム     4.524  4.795  0.112        0.469    0.277
           2枠: CV最良+公開最良  1.411  1.221  0.505        0.053    0.233
           2枠: 事前固定+公開最良  1.545  1.415  0.443        0.032    0.211
           CV上位3→公開最良     2.091  2.393  0.226        0.051    0.291
           CV最良           2.552  2.345  0.125        0.125    0.289
           事前固定(決定8)      2.996  3.373  0.000        0.000    0.287
           公開LB最良         1.355  1.896  0.532        0.083    0.134
           公開上位3→CV最良     2.381  2.614  0.176        0.096    0.265
      0.50 (参考)一様ランダム     4.524  5.153  0.112        0.469    0.277
           2枠: CV最良+公開最良  1.224  1.244  0.570        0.052    0.265
           2枠: 事前固定+公開最良  1.302  1.363  0.540        0.026    0.262
           CV上位3→公開最良     1.922  2.625  0.278        0.012    0.329
           CV最良           2.552  2.709  0.125        0.125    0.289
           事前固定(決定8)      2.996  3.731  0.000        0.000    0.287
           公開LB最良         0.881  1.858  0.677        0.036    0.173
           公開上位3→CV最良     2.305  2.870  0.197        0.081    0.290

=== 表4 公開20% 世界別 regT(×1e-4) ===
policy       (参考)一様ランダム  2枠: CV最良+公開最良  2枠: 事前固定+公開最良  CV上位3→公開最良  CV最良  事前固定(決定8)  公開LB最良  公開上位3→CV最良
pool  world                                                                                           
広い13本 0            4.43           1.37           1.15        2.78  2.79       1.50    1.70        1.92
      1            3.76           1.60           1.64        1.30  1.75       1.65    1.99        1.76
      2            5.22           2.19           1.61        4.08  8.30       2.74    2.25        2.32
      3            3.99           2.52           2.52        2.72  3.29       3.29    2.60        2.37
      4            5.17           1.44           1.68        3.28  3.01       4.33    2.26        3.48
      5            4.98           2.69           2.67        3.30  3.24       2.90    2.88        3.31
      6            4.54           1.41           1.32        3.59  2.92       3.10    1.63        3.02
      7            3.86           1.77           1.53        2.05  2.85       2.55    1.70        2.26
      8            6.95           0.00           2.37        1.36  0.00       5.13    2.99        0.79
      9            5.70           1.32           1.32        2.60  3.21       3.21    1.52        2.92
      10           6.39           3.37           3.37        4.01  3.37       3.37    2.75        3.70
      11           2.66           0.46           1.07        0.64  0.46       1.53    1.13        0.62
      12           5.69           0.86           1.09        3.55  2.61       4.05    1.39        3.59
      13           5.74           2.72           3.61        3.78  3.18       4.24    2.74        3.65
      14           4.56           2.41           2.77        3.43  2.69       3.32    3.40        2.94
      15           2.69           0.55           0.55        1.00  1.06       1.06    0.79        0.93
=== (1) CV−真(×1e-4)候補ごと: 16ワールドの平均 / SD / 符号が正の割合 ===
                   mean     sd  正の割合
default           11.08  15.00  0.75
leaf15             6.56  14.44  0.69
leaf63            16.80  16.49  0.81
lr05_it600         9.39  15.15  0.69
msl50              8.98  14.87  0.75
msl5              12.42  15.45  0.81
l2_1              11.48  13.92  0.81
it150              7.24  14.67  0.69
noKeytel          10.56  14.03  0.75
noDurHR           10.10  14.38  0.75
raw7              10.61  13.47  0.81
knn               11.79  11.80  0.88
default+knn.1      9.28  14.54  0.75
lr05_it600+knn.1   8.80  15.08  0.69

=== (2) 公開LB上の「default との差」の揺れ SD(×1e-4、16ワールド平均)===
公開 5%: 8.22
公開 20%: 3.97
公開 50%: 2.36

=== (3) 世界ごとの paired 差(regT, ×1e-4, 平均±SE, n=16)===
公開 5% 公開LB最良 − CV最良: +0.50 ± 0.42
公開 5% 2枠: CV最良+公開最良 − 公開LB最良: -1.29 ± 0.24
公開 5% 公開LB最良 − 事前固定(決定8): +0.29 ± 0.19
公開 5% CV最良 − 事前固定(決定8): -0.20 ± 0.52
公開20% 公開LB最良 − CV最良: -0.69 ± 0.45
公開20% 2枠: CV最良+公開最良 − 公開LB最良: -0.44 ± 0.19
公開20% 公開LB最良 − 事前固定(決定8): -0.89 ± 0.23
公開20% CV最良 − 事前固定(決定8): -0.20 ± 0.52
公開50% 公開LB最良 − CV最良: -1.30 ± 0.50
公開50% 2枠: CV最良+公開最良 − 公開LB最良: -0.00 ± 0.16
公開50% 公開LB最良 − 事前固定(決定8): -1.50 ± 0.32
公開50% CV最良 − 事前固定(決定8): -0.20 ± 0.52
=== (4) 候補ごとの『CV−真』の default からのずれ(×1e-4, 世界ペア, 平均±SE)と、CV最良/真の最良に選ばれた回数(広い13本, 16世界)===
                  CV-真(default比)平均    SE  CV最良回数  真の最良回数
default                       0.00  0.00       0       0
leaf15                       -4.51  0.73       0       0
leaf63                        5.73  0.98       1      14
lr05_it600                   -1.69  0.49       0       0
msl50                        -2.09  0.62       0       0
msl5                          1.34  0.68       1       1
l2_1                          0.41  0.53       0       0
it150                        -3.84  0.54       0       0
noKeytel                     -0.52  1.15       1       0
noDurHR                      -0.98  0.46       1       1
raw7                         -0.47  1.10       0       0
default+knn.1                -1.80  0.23       4       0
lr05_it600+knn.1             -2.28  0.45       8       0

読み方: 損の単位は ×10⁻⁴。regT は真の最良との差(候補選びで失った量)、regP は非公開の最良との差(順位表に出る損)、hitT は真の最良から 0.5以内に入った割合。regT_SE は16世界の平均の SE。表3の「(参考)一様ランダム」は13本からランダムに1本選んだときの平均。「2枠」の regT は「非公開が良い方」の真の点数、regP は2本のうち良い方の非公開点数。「事前固定」は常に決定8の1本(worse_fixed は「選んだ候補が事前固定より真の点数で 0.5×10⁻⁴ 超悪い割合」)で、真の最良に 0.5 以内に入った回は 16世界×500回の中で 0 回(hitT 0.000)。

タスクC(決定)

(1) 手順: 最終提出は2枠。1本目は CV 最良、2本目は公開LB最良。どちらも決定6〜8 の形(5分割の平均で提出)で作る。公開の大きさでは切り替えない(公開5%でも2枠の損 2.00 は単独方針より小さい)。ただし CV 最良の単独の信頼は低い(偏り、補助(4))ので、2本目の公開LB最良が保険になる。

案判定理由(今日の数字、広い13本・公開20%)
公開LB最良の1本だけを出す採らない損 2.11 で、2枠 1.67 より −0.44±0.19 悪い。公開が5%だと 3.29 で事前固定(3.00)にも負ける(+0.29±0.19)
CV最良の1本だけを出す採らない損 2.80 で事前固定 3.00 とほぼ同じ(−0.20±0.52、差は測れない)。真の最良(14/16世界で leaf63)を 15/16 世界で外した(偏り、補助(4))
事前固定(決定8)の1本だけを出す採らない損 3.00、hitT 0.000。ただし最悪を避ける力は最強(worse_fixed 定義上 0)。候補プールに既定値より 0.0003 良い設定が常にある環境では、動かない方が損
ハイブリッド(公開上位3→CV最良/CV上位3→公開最良)採らない2.48/2.72 で、単独の公開LB最良(2.11)を超えない。CV を挟むと CV の偏りが入る
2枠: CV最良 + 公開最良採る損 1.67、hitT 0.452、事前固定より 0.5超悪いのは 8.5% の世界・分割だけ(worse_fixed)。公開5%(2.00)・50%(1.49)でも単独方針より小さいか同等
2枠: 事前固定 + 公開最良次点損 1.89(公開20%)。CV最良に偏りがある今回は、CV最良の代わりに事前固定でも +0.22 しか悪くならない(SE 未測定)。測り方を直す前の保険として妥当
公開の割合で方針を切り替える採らない公開5%(2枠 2.00 対 公開最良 3.29)・20%(1.67 対 2.11)・50%(1.49 対 1.50)のどれでも2枠が最小か同等。切り替える理由が無い

注意(今日の比較の混ざり): 2枠の損は「非公開の良い方が採られる」という採点規則の効果を含む(損 regP は 1.56、単独の公開最良は 2.70)。規則から得られる分と、2本の組み合わせが賢い分は分けていない。「公開最良」は CV の偏りと関係なく選ぶので、2枠の2本目としては CV に依存しない保険になる、というのが今日の説明の仮説である。

(2) CV 設計書に書き足す「決定12: 最終提出の2本の選び方」(S5E5 用)

#決定採った案採らなかった案と理由前提が変わったら
12最終提出の2本候補は決定6〜8 の設定違いに限る。2本=CV最良(OOF の1番)+公開LB最良。同じ候補なら公開LBの2位を出す。公開の割合(5〜50%)では切り替えない公開最良だけ(損 2.11)、CV最良だけ(2.80、真の最良を外す偏り)、事前固定だけ(3.00)、ハイブリッド(2.48/2.72)、一様ランダム(4.77)提出の形(5モデル平均)でCVを測り直せたら、CV の偏りが消えるかを先に測る(下の次のステップ)。消えれば CV最良 の単独方針を見直す。候補が13本より大幅に多い(数百本)なら公開最良は運の当たりが増えるので見直す(未測定)。公開が約50%に近いなら公開LB最良の単独で2枠と同等(−0.00±0.16)

📊 結果の可視化

方針別の損(×10⁻⁴、短いほど良い。実測・16ワールド・広い13本)濃い=公開20% / 薄い上=公開5% / 薄い下=公開50%2枠: CV最良+公開最良2.00 (5%)1.67 (20%)1.49 (50%)公開LB最良3.29 (5%)2.11 (20%)1.50 (50%)2枠: 事前固定+公開最良2.27 (5%)1.89 (20%)1.66 (50%)公開上位3→CV最良2.99 (5%)2.48 (20%)2.33 (50%)CV上位3→公開最良2.97 (5%)2.72 (20%)2.60 (50%)CV最良2.79 (5%)2.79 (20%)2.79 (50%)事前固定(決定8)3.00 (5%)3.00 (20%)3.00 (50%)一様ランダム(参考)4.77 (5%)4.77 (20%)4.77 (50%)
CV−真 の default との差(×10⁻⁴、世界ペア平均±SE、16ワールド)右=CV が実際より悪く見える(複雑な設定)。右端の数字は「真の最良に選ばれた世界数 / CV最良に選ばれた世界数」leaf63+5.73 ± 0.98 真14 / CV1msl5+1.34 ± 0.68 真1 / CV1l2_1+0.41 ± 0.53 真0 / CV0default+0.00 ± 0.00 真0 / CV0raw7-0.47 ± 1.10 真0 / CV0noKeytel-0.52 ± 1.15 真0 / CV1noDurHR-0.98 ± 0.46 真1 / CV1lr05_it600-1.69 ± 0.49 真0 / CV0default+knn.1-1.80 ± 0.23 真0 / CV4msl50-2.09 ± 0.62 真0 / CV0lr05+knn.1-2.28 ± 0.45 真0 / CV8it150-3.84 ± 0.54 真0 / CV0leaf15-4.51 ± 0.73 真0 / CV0

上: 方針別の損。2枠(CV最良+公開最良)は公開5/20/50%のどれでも最小か同等。下: CV の偏り。複雑な設定(leaf63)は真の最良に14世界で選ばれるが、CV最良には1世界しか選ばれない。

🪜 Step-by-Step 解説

Step 1: 候補13本を決める

同じ決定6の9列の HistGB(決定7の既定値)に対して、設定違い7本(leaf15・leaf63・lr05_it600・msl50・msl5・l2_1・it150)、特徴量違い3本(noKeytel・noDurHR・raw7=元7列)、決定8の形 2本(default+knn.1・lr05_it600+knn.1)。HistGB 11本と混合2本で13本。k近傍単独は混合の材料にだけ使い、候補には入れない。候補の集め方は人間が数十本試す普通の状況を写したつもりで、実際のコンペ(数百本の提出)とは違う(扱わなかった範囲)。

Step 2: 3種類の点数を保存する

各候補について、(a) CV:分割seed 0 の5分割の OOF(train 3万行、各行を学習に使っていないモデルが予測)、(b) test 2万行の予測(5モデル平均)、(c) truth 6万行の予測(5モデル平均)。(b)(c) は決定6〜8の「提出する形」と同じ。(a) は1モデルの予測で、(b)(c) は5モデル平均。この違いが今日の主な発見につながる。

Step 3: 公開/非公開を無作為に再分割する

test 2万行から公開4,000行を無作為に取り、残りを非公開にする。これを500回繰り返し、各回で6方針が選んだ候補の真の点数・非公開点数を記録する。ワールドごとにデータ(train・test・truth)が違うので、世界間の差を SE にする(16世界)。再分割は同じ test 2万行からの抽出で、test の行そのものが違う場合の揺れは含まない(ワールド間の揺れに含まれる)。

Step 4: 公開LBは絶対点数は揺れるが、候補間の差はそれほど揺れない(表2、補助(2))

公開20%で、公開点数 − 真の点数の SD は 31.6×10⁻⁴(0.0032。Day163 の公開LB の揺れ 0.00313 と同じ桁)。ところが「default との差」の SD は 3.97×10⁻⁴(公開5% 8.22、50% 2.36)。同じ行で全候補を測ると、行の難しさが差で相殺される。今回の候補間の真の SD(3.19)と同じ桁なので、公開LB最良は「運の当たり」だけでなく実力の情報も持つ。これがタスクA-1・2が外れた理由。

Step 5: CV は候補ごとに偏っていた(補助(1)(4))

CV−真の差(C−T)の候補間 SD は 3.27×10⁻⁴ と、真の候補間 SD(3.19)と同じ大きさ。しかもこのずれは候補で系統的に違い、leaf63 が default より +5.73±0.98、leaf15 が −4.51±0.73、it150 が −3.84±0.54(×10⁻⁴、16世界の世界ペア)。複雑な設定(leaf63・msl5)ほど CV が実際より悪く見え、単純な設定ほど良く見える。Day166 の「OOF は1モデル、提出は5モデル平均で縮み方が違う」が、設定を変えても出る可能性が高い(因果は未検証。今日は1モデルの test 予測を保存していない)。

Step 6: 6方針の損(表3)

公開20%(広い13本)で損は、2枠 1.67 < 公開最良 2.11 < 公開上位3→CV最良 2.48 < CV上位3→公開最良 2.72 < CV最良 2.80 < 事前固定 3.00 < 一様ランダム 4.77(×10⁻⁴)。「真の最良の 0.5以内に入る割合」は 2枠 45%、公開最良 45%、CV最良 12.5%、事前固定 0%。

Step 7: 公開の大きさで順位が入れ替わる(表3、paired)

公開5%(1,000行)では 公開最良 3.29 が事前固定 3.00 より悪くなる(+0.29±0.19)。公開50%(10,000行)では公開最良 1.49 が最良の単独方針になり、2枠との差は −0.00±0.16。公開LBの価値は、その行数で決まる。

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

タスクCの表。決定6(特徴量)→ 7(チューニング)→ 8(混ぜる)→ 12(最終提出の2本)。Day167〜169(バックフィル分)の決定9〜11 は、再現・SE の補正・偽採用に関するもの。

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

「差を取ると揺れが小さくなる」のはなぜか。 小テストを2人の生徒に解かせると、問題が難しい回は両方の点が下がり、簡単な回は両方上がる。2人の点の「差」を取れば、問題の難しさは消える。公開LBは全候補が同じ4,000行を解くので、候補どうしの差は、絶対点数ほど揺れない。ただし候補どうしがほぼ同じ間違え方をする(Day166 の残差相関 0.93)ことが前提になる。

1モデルと5モデル平均で順位が変わる理由。 単純な設定は1回の学習でも安定して予測するが、複雑な設定は学習のたびに予想が大きく揺れる(分散が大きい)。5回分を平均すると揺れが打ち消されて複雑な設定の方が大きく良くなる。CV は1回分(1モデル)の点数なので、複雑な設定を実力より低く評価する。例え: 一発勝負では不安定な選手と、安定した選手がいる。5回の平均点で団体戦を戦うなら不安定な選手の方が有利になりうるが、1回の練習試合の記録では安定した選手が高く見える。

シェイクアップの量を順位の入れ替わりで測る。 公開と非公開の順位相関 0.53(1.0 なら完全に同じ順位、0 ならバラバラ)。公開最良が非公開で平均 4.1位に落ちる(13本中)。

🏆 Kaggleでの実践的な使い方

  • 公開LBを無視せず、公開の大きさで重みを変える。 「CV を信じて公開LBは無視せよ」は、CV が提出の形と一致しているときの話。今日のように CV と提出の形が違うと、公開LB(本物の未知データ)の方が候補を並べる力が上回った(公開20%)。一方で公開が1,000行なら事前固定に負ける
  • 最終2本は役割を分ける。 1本は自分の仮説(CV)に基づく本命、もう1本は公開LBが選んだ別の答え。同じ候補を2本出さない。2枠は片方が外れても良い方が採られるので、今回測った3つの公開割合では単独の公開最良以下の損(公開5%で −1.29±0.24、50%で差なし)
  • 候補が多いほど、公開LBの最良は運の当たりが混ざる。 今日は13本まで。数百本の提出から公開最良を選ぶと、運の分が増えるはず(未測定)
  • 選ぶ前に、CV が提出の形になっているかを確認する。 今日は OOF が1モデル、提出が5モデル平均で、設定が複雑なほど過小評価された。Day166 と同じ根が、候補選びにも出た
  • 「事前固定」は最悪を避ける。 決定8の1本は、既定値より 0.0003 良い候補がある環境では損を出し続けるが、悪い選び方(公開5%の公開最良)よりは損が小さい。選ぶ根拠が弱いときは動かない

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
CV最良を選べば公開LBより安全「公開LBは小さくて運」という経験則今回は公開20%でCV最良の損 2.80、公開最良 2.11。CV が候補ごとに偏る(leaf63 が +5.73、leaf15 が −4.51)と、安全でなくなる
公開LB最良は運で選んだだけ公開の絶対点数の揺れ(31.6)に目が行く同じ行で全候補を測るので、候補間の差の揺れは 3.97 まで小さい。公開20%なら非公開で平均 4.1位(ランダムは7位)
公開が小さくても公開最良が一番情報を持つ公開が最も新しいデータだから公開5%(1,000行)では候補間の差の揺れが 8.22 で、事前固定より悪い(+0.29)
2本出すなら上位2本を出せばよい「良い順に2本」上位2本は似た候補になりやすい。役割(CV本命+公開最良)で分ける方が、損 1.67(公開上位2本は未測定)
2枠の改善は選び方が賢いから数字が小さくなるので非公開の良い方が採られる採点規則を含む。規則分と組み合わせ分は今日は分けていない

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

  • ワールド数と1つの設計: 16ワールド、候補13本(広い)と10本(狭い)。候補の集め方(HistGB の設定を人が数十本試す)を1通りしか測っていない。本物のコンペ(数百〜数千提出、上位は別のモデル族)の候補分布とは違う。「公開最良の損」は候補数 K に依存するはずだが、K を変えた測定はしていない
  • CV の偏りの原因: 「複雑な設定は5モデル平均で得をするので、1モデルのCVは過小評価する」は仮説。1モデルの truth 予測を保存していないので、同じ候補の1モデルと5モデル平均の差は測っていない(次のステップ)
  • 事前固定の位置づけ: 決定8の形を常に出す方針は、今回は「調整していない基準」。実際には私が固定する1本を選ぶ時点で別の損が生じる(選ぶ前の情報が必要)
  • 公開LBの割合: 5/20/50% の3通りだけ。S5E5 の実際の公開割合・test 行数は未確認(Kaggle のデータページを開いていない)
  • 採点規則: 「最終2本、非公開の良い方が最終順位に使われる」は記憶による。公式ルールは今日開いていない
  • 公開LBの再分割: 同じ test 2万行の中での無作為抽出。test の行が違う場合の揺れはワールド間の揺れに含まれる(ワールド16件)
  • ハイブリッド方針: 上位3本のしきい値は3の1通りだけ。他の値は未測定
  • 名目の統計: 方針ごとの損の差は paired SE(16世界)で見たが、多数の比較のうち主要な4対だけを出した。多重比較の補正はしていない
  • 文献との照合: シェイクアップ・公開/非公開の分割に関する Kaggle 上の議論(フォーラム投稿)は今日は読んでいない。結論は測定による

🚀 次のステップ

  • 発展: 同じ候補の「1モデル」と「5モデル平均」の truth 点数を保存し、CV の偏り(leaf63 +5.73、leaf15 −4.51)が1モデルと5モデルの差で説明できるかを測る。説明できるなら、「提出の形のCV」(5モデル平均を作れる train の外の行で測る)に直した CV最良 が公開最良を超えるか、再度6方針を比べる
  • 発展: 候補数 K を 5/13/50 と変えて、公開最良の損の増え方を測る
  • 次回予告: Day171 — Playground Series 実戦投入⑥(月内のまとめ。決定6〜12 を1枚の「CV設計書」にまとめ、そのうち測った数字の弱いもの(偏り・K・公開割合)に印を付ける。S5E5 の実データへの当てはめは、Kaggle へのログインとルール同意が必要なため、利用者の作業として切り出す)。持ち越し: 縮み(約0.0002)の原因を学習データ量を変えた1モデルで分離、SE 個数差と分割の新しさの分離(Day165)、Day162 の選択バイアス仮説、真の効果ちょうど0の候補での偽採用率、Nadeau & Bengio 原典の確認

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

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