Day 172 — 2026-10-05 | Playground Series 実戦投入⑦ — LightGBM の bagging・列抽出で model seed の揺れが入っても、補正CV(決定13)の偏り消しは効いた(生 CV の偏り最大 +12.30 → 補正後 +0.09、×10⁻⁴)。ただし「選ぶ力」は上がらず、今回の候補では乱数なしの既定値を常に出す方が損が小さかった(0.33 対 0.75)

2026-10-05 金 / Phase 6(月替わりテーマ2: Playground Series 実戦投入) 理論→分析(実験) LightGBM · 16ワールド · seed 3 · 2,480 fit · 318+1,275秒

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

Day171 のおさらい。 fold モデル5本の平均で提出するとき、二乗誤差では次の式が行ごとに必ず成り立つ。

5モデルの平均の二乗誤差 = 1モデルずつの二乗誤差の平均 − 5モデルの予測のばらつき(分散)

右の「ばらつき」は正解ラベル無しで test 行から測れる(A_test)。だから CV(OOF、1モデルの予測)を 補正CV = √(CV² − A_test) に直せば「提出の形」で候補を比べられる。Day171 は HistGB(乱数なし)だけで、真の最良に当たった世界が 1/16 → 8/16、損 2.34 → 0.92 に下がった。

今日の疑問。 LightGBM で bagging_fraction(行の抜き出し)や feature_fraction(列の抜き出し)を使うと、同じデータでも乱数(model seed)を変えるだけで予測が変わる。すると fold 間の分散には2種類が混ざる。

混ざるもの意味例え
データの揺れfold ごとに学習データが違うことによるばらつき5人の射手が別々の練習場で練習した差
seed の揺れ同じデータでも乱数で変わるばらつき同じ射手でも毎回の手ぶれ

恒等式そのものは「5本の予測が何でも」成り立つので、補正の式は壊れないはず。ただ、(1) OOF は1モデルなので seed の揺れを全部かぶり、提出は5本平均なので 1/5 しかかぶらない、(2) seed を変えるたびに補正CV の値が動き、選ぶ候補が入れ替わるかもしれない。今日はこの2つを測る。

用語整理(Day171 と同じものは省略):

用語意味
model seed 組 s0/s1/s2候補ごとに random_state = 1000×s + fold番号 で学習した3組。乱数なしの候補は3組とも同じ値
avg33組の seed を fold ごとに平均した提出(15モデル)。OOF も3 seed 平均
seed の割合fold 間分散 A_test のうち seed の揺れが占める割合(seed 間分散 σ²_seed × 0.8 ÷ A_test。0.8 は5本の母分散の係数 4/5)
世界ペア差同じワールドで基準候補 lgb_det(乱数なし LightGBM)との差を取り、16ワールドで平均と SE を出したもの
損(regT)選んだ候補の T5 − 候補中の T5 最良(×10⁻⁴、Day170・171 と同じ)

縮小シミュレータは Day163〜171 と同じ(train 3万・元1.5万・test 2万・truth 6万、乱数 3000+ワールド、16ワールド)。数値は仮定の上の実測。

データ・保存物の構造(スキーマ)

名前形・型意味・作り方使う場所
Sex … Body_Temp, Dur_x_HR, Keytel_kcalint / float(9列)Day163〜171 と同じ合成(乱数 3000+ワールド)。決定6 の9列全候補
oof(11, 3, 30000) float64(候補, model seed, 行)。分割seed 0 の5分割の OOF(1モデルの予測)→ CCV・補正CV
evk(11, 3, 5, 80000) float32(候補, seed, fold, 行)。平均しない予測。先頭2万行=test、残り6万行=truthT1・T5・A_test・σ²_seed
A_test(候補,)test 行で fold 間の予測の分散(ラベル不要)補正CV = √(C² − A_test)
σ²_seed(候補,)test 行で fold ごとに seed 間の分散(ddof=1)を取り平均(ラベル不要)seed の割合・3 seed 平均の得 2σ²/15
Calories(目的変数)floatlog1p 空間で学習、RMSLE で評価—

評価指標(RMSLE)と、fold 間分散の中身

RMSLE は log1p 空間の RMSE。二乗誤差なので「5本平均の誤差² = 1本ずつの誤差²の平均 − fold 間分散」が成り立つ。今日はその fold 間分散が、乱数ありの LightGBM でどれだけ seed の揺れで占められるかを測った。

fold 間分散のうち model seed の揺れの割合(s0、16ワールド平均)橙=seed の揺れ/青=データの揺れ。lgb_det・HistGB は 0(乱数なし)lr05_it600_b70.38bag080.46leaf63_b70.52ff050.53bag050.57leaf15_b70.59bag05_ff050.64bag03_ff050.73et0.95

❓ 問題

タスクA(理論)— コードを書く前に

  1. 恒等式 mean_k (f_k − y)² = (f̄ − y)² + mean_k (f_k − f̄)² は、f_k に seed の揺れが入っても成り立つか。成り立つなら、補正CV の「平均の偏り」は消えるはずか
  2. 3 seed 平均(avg3)にしたときの真の MSE の得を、ラベル無しで予測する式を書け(seed 間分散 σ²_seed を使う)
  3. seed の揺れがあると、補正CV の「選ぶ力」に何が起きると予想するか

タスクB(予想)

  1. fold 間分散のうち seed の揺れの割合は、bagging 0.8 でどれくらい、extra_trees でどれくらいか
  2. 生の CV の偏り(lgb_det との差)は最大どれくらいで、補正後はどこまで消えるか
  3. 補正CV最良と生の CV最良・公開LB最良(公開20%)の損の差は、Day171(−1.42・−1.18)と比べてどうなるか

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

  1. Day171 と同じ16ワールドで、HistGB 既定値(Day171 との一致確認用)・lgb_det・乱数ありの LightGBM 9本を、model seed 3通り × 5 fold で作り、fold 別・seed 別の予測を平均せずに保存する(day172.py)
  2. 恒等式の確認、seed の割合、補正後の偏り、seed 組を変えたときの揺れ、方針の損、3 seed 平均の得を出す(analyze172.py)

タスクD(決定)

決定13 を「乱数ありのモデル」に広げてよいかを決め、必要なら新しい決定を足す。

💡 ヒント

ヒント1(方向性)

  • 恒等式は f_k が「何であっても」成り立つ代数の式。seed の揺れは f_k の中身を変えるだけ
  • ただし「平均の偏りが消える」ことと「1回ごとの値がぶれない」ことは別の話

ヒント2(アプローチ)

  • 5 fold 平均の中の seed の揺れは σ²_seed/5、3 seed × 5 fold 平均なら σ²_seed/15。差が得になる
  • 選ぶ力を測るには、seed 組だけを変えて同じワールドで比べる(世界内 SD)
  • 「乱数なしの基準を常に出す」も方針の1つとして並べる。 選ぶこと自体が損を生む場合がある

ヒント3(コード骨格)

evk = np.zeros((n_cand, 3, 5, N_TEST + N_TRUTH), np.float32)       # (候補, seed, fold, 行)
evk[j, si, k] = lgb.LGBMRegressor(**params, random_state=1000*s + k).fit(X, y).predict(XE)
sig_s = evk[:, :, :, :N_TEST].var(axis=1, ddof=1).mean(axis=(1, 2))   # seed 間分散(ラベル無し)
A_te  = evk[:, s, :, :N_TEST].var(axis=1).mean(1)                      # fold 間分散(seed 組 s)
seed_share = 0.8 * sig_s / A_te
gain_avg3_pred = 2 * sig_s / 15                                          # 1/5 − 1/15

✅ 模範解答

タスクA(理論の模範)

  1. 成り立つ。 恒等式は5つの数とその平均についての代数の式で、数がどう作られたかを問わない。だから T1² − T5² = A(truth 行)は seed ありでも正確に成り立ち、test 行で測った A_test が truth の A と同じ分布なら、補正CV の平均の偏りは消える
  2. 得 = 2σ²_seed / 15。 5 fold 平均の中に残る seed の分散は σ²_seed/5、3 seed × 5 fold では σ²_seed/15。差は σ²_seed × (1/5 − 1/15) = 2σ²_seed/15(MSE 単位)。σ²_seed は test 行で seed を変えた予測の分散なのでラベル不要
  3. 選ぶ力は落ちる。 OOF(1モデル)は seed の揺れをまるごとかぶり、補正はその平均を引くだけで「今回の seed の当たり外れ」は消せない。だから補正CV は偏りは無くても、1回ごとの値がぶれ、候補の差が小さいと入れ替わる

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

#予想の模範実測
1bagging 0.8 で2〜3割、extra_trees で5割予想より大きい。 bagging 0.8 で 0.46、列抽出 0.5 で 0.53、bagging 0.3+列0.5 で 0.73、extra_trees は 0.95(fold 間分散のほぼ全部が seed の揺れ)
2生の偏りは最大 +10 前後、補正で ±1 以内に消える当たり。 生の CV の偏り(lgb_det との差)は最大 bag03_ff05 +12.30±0.95、補正後 +0.09±0.93。補正後の最大は bag05 +1.27±0.77(1.6SE)。11候補とも 2SE 未満
3Day171 と同じく補正CV が大きく勝つ外れ。 補正CV最良 − 生の CV最良は s0 −0.18±0.25、s1 +0.49±0.86、s2 +0.07±0.28(差なし)。公開LB最良には公開20%で −1.35±0.29(s0)と勝つ。ただし乱数なしの lgb_det を常に出す方針(0.24〜0.58)がどの選び方よりも損が小さかった

3が外れた理由(今日いちばん大事なこと): 今日の候補では、乱数ありの9本はどれも真の点数で lgb_det より悪かった(T5 の差 +1.11〜+15.25、avg3 にしても bag08 が −0.08 で並ぶだけ)。Day171 は複雑な設定(leaf63)が本当に良く、CV がそれを過小評価していたので、補正が「選ぶ力」に直結した。今日は本当に良い候補が無いので、偏りを消しても選べる上積みが無く、残るのは選ぶときのノイズだけだった。

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

day172.py(Day171 の day171.py から、候補を LightGBM に替え、model seed の次元を足した。HistGB 既定値は Day171 との一致確認のために残した):

"""Day172: Day171 と同じワールドで、LightGBM の bagging・列抽出(model seed の揺れがある候補)を
model seed 3通り × 5 fold で作り、fold 別の予測を平均せずに保存する。
使い方: OMP_NUM_THREADS=1 python day172.py <world>   -> raw172_w<world>.npz
"""
import sys, time, hashlib
import numpy as np, pandas as pd
import lightgbm as lgb
from sklearn.ensemble import HistGradientBoostingRegressor
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
SEEDS = (0, 1, 2)


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-171 と同じ
    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-171 と同じ
    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
LG = dict(objective="regression", learning_rate=0.1, n_estimators=300, num_leaves=31, min_child_samples=20,
          n_jobs=1, verbose=-1)                              # 決定7の既定値を LightGBM の名前に写したもの
B7 = dict(subsample=0.7, subsample_freq=1, colsample_bytree=0.7)
CANDS = [("lgb_det", {}),                                    # 乱数なし(bagging も列抽出もなし)
         ("bag08", dict(subsample=0.8, subsample_freq=1)),
         ("bag05", dict(subsample=0.5, subsample_freq=1)),
         ("ff05", dict(colsample_bytree=0.5)),
         ("bag05_ff05", dict(subsample=0.5, subsample_freq=1, colsample_bytree=0.5)),
         ("bag03_ff05", dict(subsample=0.3, subsample_freq=1, colsample_bytree=0.5)),
         ("leaf63_b7", dict(num_leaves=63, **B7)),
         ("leaf15_b7", dict(num_leaves=15, **B7)),
         ("lr05_it600_b7", dict(learning_rate=0.05, n_estimators=600, **B7)),
         ("et", dict(extra_trees=True))]
names = ["hgb_default"] + [c[0] for c in CANDS]

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
XE = evalset[BASE].values
oof = np.zeros((len(names), len(SEEDS), N_TRAIN))
evk = np.zeros((len(names), len(SEEDS), 5, N_TEST + N_TRUTH), np.float32)   # (候補, model seed, fold, 行)
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)
    X, y, XV = tr_df[BASE].values, np.log1p(tr_df.Calories.values), train.iloc[va][BASE].values
    m = HistGradientBoostingRegressor(early_stopping=False, random_state=0, learning_rate=0.1, max_iter=300,
                                      max_leaf_nodes=31, min_samples_leaf=20).fit(tr_df[BASE], y)
    p_va, p_ev = m.predict(train.iloc[va][BASE]), m.predict(evalset[BASE])
    for si in range(len(SEEDS)): oof[0, si, va] = p_va; evk[0, si, k] = p_ev                 # 乱数なし: 3 seed とも同じ値
    for j, (nm, over) in enumerate(CANDS, start=1):
        for si, s in enumerate(SEEDS):
            m = lgb.LGBMRegressor(**{**LG, **over}, random_state=1000 * s + k).fit(X, y)
            oof[j, si, va] = m.predict(XV); evk[j, si, k] = m.predict(XE)
    print(f"world {W} fold {k} {time.time()-T0:.0f}s", flush=True)
assert names[0] == "hgb_default" and names.index("lgb_det") == 1          # 決定9 追記: 名前と行の対応を表明
h = hashlib.sha256(oof[names.index("lgb_det"), 0].tobytes()).hexdigest()[:16]
np.savez_compressed(f"raw172_w{W}.npz", oof=oof, evk=evk, ytr=train.Calories.values,
                    yev=evalset.Calories.values, names=np.array(names))
print(f"world {W} wall {time.time()-T0:.0f}s sha_lgbdet_s0 {h}")

analyze172.py(恒等式・seed の割合・偏り・揺れ・方針の損・選んだ候補):

"""Day172 集計: raw172_w*.npz(候補11 × model seed 3 × fold 5 の予測)から
(1) 恒等式の確認(A_test・A_truth・T1−T5)を model seed ありの候補で
(2) fold 間分散のうち model seed の揺れが占める割合
(3) 補正CV(決定13)の偏りが seed ありでも消えるか(lgb_det との差、世界ペア)
(4) 方針の損(公開 5%/20%、再分割500回)を seed 組ごと・3 seed 平均で
(5) seed 組を変えたとき、補正CV の選ぶ候補と真の最良がどれだけ入れ替わるか
使い方: python analyze172.py
"""
import glob
import numpy as np, pandas as pd

pd.set_option("display.width", 250)
N_TEST, R = 20000, 500
files = sorted(glob.glob("raw172_w*.npz"), key=lambda f: int(f.split("_w")[1].split(".")[0]))


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


dec, pol, picks = [], [], []
for f in files:
    w = int(f.split("_w")[1].split(".")[0])
    z = np.load(f); OOF, EVK, ytr, yev, names = z["oof"], z["evk"].astype(np.float64), z["ytr"], z["yev"], list(z["names"])
    assert names[1] == "lgb_det"
    yte, ytru = yev[:N_TEST], yev[N_TEST:]
    sig_s = EVK[:, :, :, :N_TEST].var(axis=1, ddof=1).mean(axis=(1, 2))        # seed 間分散(fold・test 行で平均)
    settings = {f"s{s}": (OOF[:, s], EVK[:, s]) for s in range(3)}
    settings["avg3"] = (OOF.mean(1), EVK.mean(1))                               # 3 seed 平均(提出は 15 モデル)
    for st, (oof, evk) in settings.items():
        ev5 = evk.mean(1)
        C_mse = np.array([sle(ytr, oof[j]).mean() for j in range(len(names))])
        T5_mse = np.array([sle(ytru, ev5[j, N_TEST:]).mean() for j in range(len(names))])
        T1_mse = np.array([np.mean([sle(ytru, evk[j, k, N_TEST:]).mean() for k in range(5)]) for j in range(len(names))])
        A_te = evk[:, :, :N_TEST].var(axis=1).mean(1); A_tru = evk[:, :, N_TEST:].var(axis=1).mean(1)
        C, T5, T1, Ca = np.sqrt(C_mse), np.sqrt(T5_mse), np.sqrt(T1_mse), np.sqrt(C_mse - A_te)
        for j, n in enumerate(names):
            dec.append(dict(world=w, st=st, cand=n, C=C[j], T5=T5[j], Ca=Ca[j], C_T5=(C[j] - T5[j]) * 1e4,
                            T1_T5=(T1[j] - T5[j]) * 1e4, Cadj_T5=(Ca[j] - T5[j]) * 1e4, A_te=A_te[j] * 1e4,
                            A_tru=A_tru[j] * 1e4, gap_mse=(T1_mse[j] - T5_mse[j]) * 1e4,
                            seed_share=0.8 * sig_s[j] / A_te[j] if st != "avg3" else 0.8 * sig_s[j] / 3 / A_te[j]))
        best = T5.min(); picks.append(dict(world=w, st=st, true=names[T5.argmin()], cv=names[C.argmin()], cadj=names[Ca.argmin()]))
        E_te = np.stack([sle(yte, ev5[j, :N_TEST]) for j in range(len(names))])
        rng = np.random.default_rng(w)
        for frac in (0.05, 0.20):
            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 @ M; U = np.sqrt(S_pub / n_pub)
            P = np.sqrt((E_te.sum(1, keepdims=True) - S_pub) / (N_TEST - n_pub))
            pub = U.argmin(0); cv = int(C.argmin()); cva = int(Ca.argmin())
            def two(a):
                t = np.stack([np.full(R, a), pub]); pp = np.stack([P[t[0], np.arange(R)], P[t[1], np.arange(R)]])
                return t[pp.argmin(0), np.arange(R)]
            for nm, ch in {"CV最良": np.full(R, cv), "補正CV最良": np.full(R, cva), "公開LB最良": pub,
                           "2枠: 補正CV最良+公開最良": two(cva), "一様ランダム": None}.items():
                reg = (T5.mean() - best) * 1e4 if ch is None else float(((T5[ch] - best) * 1e4).mean())
                pol.append(dict(world=w, st=st, frac=frac, policy=nm, regT=reg))

D = pd.DataFrame(dec); PR = pd.DataFrame(pol); PK = pd.DataFrame(picks)
print(f"worlds={len(files)}")
print("\n=== 表1 恒等式と seed の割合(s0、16世界平均、×1e-4。seed_share は fold 間分散のうち model seed の割合) ===")
print(D[D.st == "s0"].groupby("cand", sort=False)[["C_T5", "T1_T5", "Cadj_T5", "A_tru", "gap_mse", "A_te", "seed_share"]].mean().round(2).to_string())
print("\n=== 表2 lgb_det との差(世界ペア n=16、seed 組 s0/s1/s2 を世界内で平均してから、平均±SE、×1e-4) ===")
rows = []
for col in ("C_T5", "T1_T5", "Cadj_T5"):
    X = D[D.st != "avg3"].groupby(["world", "cand"])[col].mean().unstack(); X = X.sub(X["lgb_det"], axis=0)
    for c in X.columns: rows.append(dict(cand=c, col=col, v=f"{X[c].mean():+.2f}±{X[c].std(ddof=1)/np.sqrt(len(X)):.2f}"))
print(pd.DataFrame(rows).pivot(index="cand", columns="col", values="v").to_string())
for st in ("s0", "avg3"):
    print(f"候補間SD({st}): C_T5 {D[D.st==st].groupby('world').C_T5.std(ddof=1).mean():.2f}  Cadj_T5 {D[D.st==st].groupby('world').Cadj_T5.std(ddof=1).mean():.2f}")
# seed 組だけを変えたときの Cadj_T5 の揺れ(世界内 SD)と、候補間の真の差の大きさ
sd_seed = D[D.st.isin(["s0", "s1", "s2"])].groupby(["world", "cand"]).Cadj_T5.std(ddof=1).groupby("cand").mean()
sd_T5 = D[D.st.isin(["s0", "s1", "s2"])].groupby(["world", "cand"]).T5.std(ddof=1).groupby("cand").mean() * 1e4
print("\n=== 表3 seed 組を変えたときの揺れ(世界内 SD の世界平均、×1e-4) ===")
print(pd.DataFrame(dict(Cadj_T5_seedSD=sd_seed, T5_seedSD=sd_T5)).round(2).to_string())
print("\n=== 表4 方針の損(regT, ×1e-4, 16世界平均±SE) ===")
g = PR.groupby(["frac", "st", "policy"]).regT.agg(lambda s: f"{s.mean():.2f}±{s.std(ddof=1)/np.sqrt(len(s)):.2f}").unstack("st")
print(g.to_string())
for fr in (0.05, 0.2):
    for st in ("s0", "s1", "s2", "avg3"):
        a = PR[(PR.frac == fr) & (PR.st == st)].pivot(index="world", columns="policy", values="regT")
        out = []
        for x, y in [("補正CV最良", "CV最良"), ("補正CV最良", "公開LB最良"), ("2枠: 補正CV最良+公開最良", "補正CV最良")]:
            d = a[x] - a[y]; out.append(f"{x}−{y} {d.mean():+.2f}±{d.std(ddof=1)/np.sqrt(len(d)):.2f}")
        print(f"公開{int(fr*100):>2}% {st}: " + " | ".join(out))
print("\n=== 表5 選ばれた候補と真の最良(16世界) ===")
for st in ("s0", "s1", "s2", "avg3"):
    p = PK[PK.st == st]
    print(f"{st}: hit CV {int((p.cv==p.true).sum())}/16  hit 補正CV {int((p.cadj==p.true).sum())}/16  真の最良 {p.true.value_counts().to_dict()}  補正CV最良 {p.cadj.value_counts().to_dict()}")
q = PK[PK.st.isin(["s0", "s1", "s2"])].groupby("world").agg(n_true=("true", "nunique"), n_cadj=("cadj", "nunique"))
print("seed 組 3つで、真の最良が変わった世界:", int((q.n_true > 1).sum()), "/16   補正CV最良が変わった世界:", int((q.n_cadj > 1).sum()), "/16")
# seed 平均の得(同じ候補で s0 単独 → avg3)
g5 = D.pivot_table(index=["world", "cand"], columns="st", values="T5")
d = (g5["avg3"] - g5[["s0", "s1", "s2"]].mean(1)) * 1e4
print("\n=== 表6 3 seed 平均にした真の得(T5_avg3 − T5_単独 の平均、×1e-4、世界平均±SE) ===")
print(d.groupby("cand").agg(lambda s: f"{s.mean():+.2f}±{s.std(ddof=1)/np.sqrt(len(s)):.2f}").to_string())
D.to_pickle("dec172.pkl"); PR.to_pickle("pol172.pkl"); PK.to_pickle("pk172.pkl")

追加の集計(3 seed 平均の得の予測と、採用に余裕を求める「ガード」)は analyze172.py が保存した dec172.pkl と raw172_w*.npz から作った:

"""Day172 追加集計: (1) 3 seed 平均の得をラベル無しで予測(2σ²_seed/15)して実測と比べる
(2) 補正CV最良を採るのに lgb_det より m×1e-4 以上の余裕を求める「ガード」の損
使い方: python extra172.py(analyze172.py の後)
"""
import glob
import numpy as np, pandas as pd

N = 20000


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


rows = []
for f in sorted(glob.glob("raw172_w*.npz")):
    z = np.load(f); E = z["evk"].astype(np.float64); names = list(z["names"]); ytru = z["yev"][N:]
    for j, n in enumerate(names):
        if n in ("hgb_default", "lgb_det"): continue
        sig = E[j, :, :, :N].var(axis=0, ddof=1).mean()                  # test 行の seed 間分散(ラベル無し)
        t1 = np.mean([sle(ytru, E[j, s].mean(0)[N:]).mean() for s in range(3)])
        t3 = sle(ytru, E[j].mean((0, 1))[N:]).mean()
        rows.append(dict(cand=n, pred=2 * sig / 15 * 1e4, actual=(t1 - t3) * 1e4))
print(pd.DataFrame(rows).groupby("cand").mean().round(3).to_string())

D = pd.read_pickle("dec172.pkl"); out = []
for st in ["s0", "s1", "s2", "avg3"]:
    for m in [0, 0.5, 1, 2, 3, "常にlgb_det"]:
        r = []
        for w, h in D[D.st == st].groupby("world"):
            h = h.set_index("cand"); b = h.Ca.idxmin()
            pick = "lgb_det" if m == "常にlgb_det" else (b if h.Ca[b] < h.Ca["lgb_det"] - m * 1e-4 else "lgb_det")
            r.append((h.T5[pick] - h.T5.min()) * 1e4)
        out.append(dict(st=st, m=m, v=f"{np.mean(r):.2f}±{np.std(r, ddof=1) / 4:.2f}"))
print(pd.DataFrame(out).pivot(index="m", columns="st", values="v").to_string())

実行結果(実測)

# ============ 実行環境: macOS (M1, メモリ8GB)・Python 3.13.12・scikit-learn 1.9.1・numpy 2.5.3・pandas 3.0.6・scipy 1.18.1・lightgbm 4.7.0(今日の新しい uv venv)
#   LightGBM の macOS 用 wheel は libomp を同梱していないため、scikit-learn 同梱の libomp を DYLD_LIBRARY_PATH で読ませた(brew は使っていない)
#   fit 数 = 16ワールド × 5 fold × (HistGB 1 + LightGBM 10本 × seed 3) = 2,480。OMP_NUM_THREADS=1・n_jobs=1
#   壁時計: ワールド0 単独 318秒、ワールド1〜15 を4並行で 1,275秒(1ワールド 282〜355秒)
# 再現確認: hgb_default は Day171 の default と 16ワールドとも完全一致(fold 別予測・OOF とも最大差 0.0)
#           lgb_det(乱数なし)は seed 0 と seed 2 で完全一致(最大差 0.0)。乱数ありの9本は seed で最大 0.22〜0.49 変わる(log1p 空間、ワールド0)

worlds=16

=== 表1 恒等式と seed の割合(s0、16世界平均、×1e-4。seed_share は fold 間分散のうち model seed の割合) ===
                C_T5  T1_T5  Cadj_T5  A_tru  gap_mse  A_te  seed_share
cand                                                                  
hgb_default    11.08  12.73    -1.67   3.99     3.99  3.99        0.00
lgb_det        10.83  12.61    -1.80   3.95     3.95  3.96        0.00
bag08          12.76  14.23    -1.61   4.47     4.47  4.51        0.46
bag05          19.18  18.68     0.47   5.89     5.89  5.90        0.57
ff05           10.95  12.97    -2.06   4.08     4.08  4.09        0.53
bag05_ff05     17.17  19.70    -2.49   6.23     6.23  6.21        0.64
bag03_ff05     23.63  25.23    -1.59   8.02     8.02  8.01        0.73
leaf63_b7      21.97  22.73    -0.78   7.17     7.17  7.17        0.52
leaf15_b7      10.60  11.72    -1.18   3.70     3.70  3.71        0.59
lr05_it600_b7  10.11  11.40    -1.24   3.58     3.58  3.56        0.38
et             19.80  21.51    -1.73   6.81     6.81  6.81        0.95

=== 表2 lgb_det との差(世界ペア n=16、seed 組 s0/s1/s2 を世界内で平均してから、平均±SE、×1e-4) ===
col                   C_T5     Cadj_T5        T1_T5
cand                                               
bag03_ff05     +12.30±0.95  +0.09±0.93  +12.16±0.08
bag05           +7.29±0.76  +1.27±0.77   +6.03±0.07
bag05_ff05      +7.20±0.76  +0.51±0.73   +6.71±0.07
bag08           +1.93±0.80  +0.25±0.78   +1.65±0.06
et              +9.42±1.58  +0.35±1.48   +9.06±0.18
ff05            +0.09±0.69  +0.04±0.67   +0.05±0.08
hgb_default     +0.25±0.79  +0.14±0.75   +0.12±0.07
leaf15_b7       -0.89±0.85  +0.45±0.82   -1.38±0.07
leaf63_b7      +10.72±1.05  +0.99±1.02   +9.73±0.09
lgb_det         +0.00±0.00  +0.00±0.00   +0.00±0.00
lr05_it600_b7   -1.36±0.80  +0.04±0.79   -1.36±0.06
候補間SD(s0): C_T5 5.88  Cadj_T5 2.76
候補間SD(avg3): C_T5 3.58  Cadj_T5 2.43

=== 表3 seed 組を変えたときの揺れ(世界内 SD の世界平均、×1e-4) ===
               Cadj_T5_seedSD  T5_seedSD
cand                                    
bag03_ff05               2.28       0.84
bag05                    2.31       0.82
bag05_ff05               2.06       0.98
bag08                    1.32       0.46
et                       1.98       1.11
ff05                     1.67       0.63
hgb_default              0.00       0.00
leaf15_b7                1.36       0.49
leaf63_b7                2.11       0.66
lgb_det                  0.00       0.00
lr05_it600_b7            1.47       0.53

=== 表4 方針の損(regT, ×1e-4, 16世界平均±SE) ===
st                         avg3         s0         s1         s2
frac policy                                                     
0.05 2枠: 補正CV最良+公開最良  1.27±0.32  0.82±0.21  1.02±0.29  0.68±0.22
     CV最良             2.65±0.87  0.93±0.32  1.00±0.38  0.52±0.21
     一様ランダム           5.32±0.17  6.56±0.18  6.74±0.22  6.27±0.22
     公開LB最良           3.55±0.21  4.47±0.22  4.49±0.23  4.17±0.23
     補正CV最良           1.95±0.88  0.75±0.22  1.49±0.74  0.58±0.21
0.20 2枠: 補正CV最良+公開最良  0.86±0.19  0.64±0.18  0.73±0.19  0.63±0.24
     CV最良             2.65±0.87  0.93±0.32  1.00±0.38  0.52±0.21
     一様ランダム           5.32±0.17  6.56±0.18  6.74±0.22  6.27±0.22
     公開LB最良           1.99±0.24  2.10±0.26  2.01±0.28  2.03±0.28
     補正CV最良           1.95±0.88  0.75±0.22  1.49±0.74  0.58±0.21
公開 5% s0: 補正CV最良−CV最良 -0.18±0.25 | 補正CV最良−公開LB最良 -3.72±0.22 | 2枠: 補正CV最良+公開最良−補正CV最良 +0.07±0.17
公開 5% s1: 補正CV最良−CV最良 +0.49±0.86 | 補正CV最良−公開LB最良 -3.00±0.78 | 2枠: 補正CV最良+公開最良−補正CV最良 -0.47±0.53
公開 5% s2: 補正CV最良−CV最良 +0.07±0.28 | 補正CV最良−公開LB最良 -3.58±0.21 | 2枠: 補正CV最良+公開最良−補正CV最良 +0.10±0.13
公開 5% avg3: 補正CV最良−CV最良 -0.70±0.37 | 補正CV最良−公開LB最良 -1.60±0.87 | 2枠: 補正CV最良+公開最良−補正CV最良 -0.68±0.61
公開20% s0: 補正CV最良−CV最良 -0.18±0.25 | 補正CV最良−公開LB最良 -1.35±0.29 | 2枠: 補正CV最良+公開最良−補正CV最良 -0.11±0.16
公開20% s1: 補正CV最良−CV最良 +0.49±0.86 | 補正CV最良−公開LB最良 -0.52±0.83 | 2枠: 補正CV最良+公開最良−補正CV最良 -0.76±0.71
公開20% s2: 補正CV最良−CV最良 +0.07±0.28 | 補正CV最良−公開LB最良 -1.45±0.27 | 2枠: 補正CV最良+公開最良−補正CV最良 +0.05±0.20
公開20% avg3: 補正CV最良−CV最良 -0.70±0.37 | 補正CV最良−公開LB最良 -0.04±0.95 | 2枠: 補正CV最良+公開最良−補正CV最良 -1.09±0.80

=== 表5 選ばれた候補と真の最良(16世界) ===
s0: hit CV 5/16  hit 補正CV 5/16  真の最良 {np.str_('lgb_det'): 8, np.str_('hgb_default'): 6, np.str_('bag08'): 2}  補正CV最良 {np.str_('lgb_det'): 8, np.str_('bag08'): 5, np.str_('hgb_default'): 3}
s1: hit CV 6/16  hit 補正CV 3/16  真の最良 {np.str_('lgb_det'): 7, np.str_('hgb_default'): 6, np.str_('bag08'): 3}  補正CV最良 {np.str_('lgb_det'): 8, np.str_('bag08'): 4, np.str_('hgb_default'): 3, np.str_('et'): 1}
s2: hit CV 7/16  hit 補正CV 6/16  真の最良 {np.str_('hgb_default'): 7, np.str_('lgb_det'): 6, np.str_('bag08'): 3}  補正CV最良 {np.str_('lgb_det'): 7, np.str_('bag08'): 5, np.str_('hgb_default'): 4}
avg3: hit CV 4/16  hit 補正CV 5/16  真の最良 {np.str_('bag08'): 7, np.str_('hgb_default'): 5, np.str_('lgb_det'): 4}  補正CV最良 {np.str_('lgb_det'): 6, np.str_('bag08'): 5, np.str_('hgb_default'): 3, np.str_('et'): 2}
seed 組 3つで、真の最良が変わった世界: 5 /16   補正CV最良が変わった世界: 4 /16

=== 表6 3 seed 平均にした真の得(T5_avg3 − T5_単独 の平均、×1e-4、世界平均±SE) ===
cand
bag03_ff05       -3.02±0.02
bag05            -1.76±0.01
bag05_ff05       -2.08±0.01
bag08            -1.10±0.01
et               -3.40±0.04
ff05             -1.12±0.01
hgb_default      +0.00±0.00
leaf15_b7        -1.13±0.01
leaf63_b7        -1.96±0.01
lgb_det          +0.00±0.00
lr05_it600_b7    -0.71±0.00

=== 追加1 3 seed 平均の MSE の得(×1e-4): 予測 2σ²_seed/15(test 行、ラベル無し) vs 実測(truth 行) ===
                pred  actual
cand                        
bag03_ff05     0.969   0.950
bag05          0.556   0.550
bag05_ff05     0.661   0.654
bag08          0.342   0.343
et             1.072   1.069
ff05           0.364   0.350
leaf15_b7      0.365   0.356
leaf63_b7      0.621   0.614
lr05_it600_b7  0.227   0.222

=== 追加2 ガード: 補正CV最良が lgb_det より m×1e-4 以上良いときだけ採用、そうでなければ lgb_det(16世界平均±SE, ×1e-4) ===
st              avg3         s0         s1         s2
m                                                    
0          1.95±0.88  0.75±0.22  1.49±0.74  0.58±0.21
0.5        1.95±0.88  0.69±0.22  1.49±0.74  0.59±0.21
1          1.83±0.89  0.69±0.22  0.66±0.15  0.40±0.14
2          1.85±0.89  0.57±0.22  0.45±0.13  0.42±0.14
3          0.64±0.18  0.57±0.22  0.37±0.12  0.29±0.10
常にlgb_det  0.58±0.15  0.33±0.14  0.32±0.10  0.24±0.09

読み方:

  • 表1 の A_tru・gap_mse・A_te が全候補で小数2桁まで一致(et 6.81/6.81/6.81)。seed の揺れが fold 間分散の 38〜95% を占めても、恒等式もラベル無しの推定もそのまま使える
  • 表2 の右列(T1_T5=平均の得)は SE 0.06〜0.18 と非常に精密で、左列(生の偏り)をほぼ打ち消す。補正後(中央列)は11候補とも ±1.27 以内・2SE 未満
  • 表3: seed 組を変えるだけで補正CV − 真のずれは世界内 SD 1.3〜2.3 動く。真の点数(5本平均)の seed による揺れ 0.5〜1.1 の約2倍。OOF は1モデルなので seed の揺れを 1/5 に薄められない
  • 追加1: 3 seed 平均の得はラベル無しの予測と実測が 4% 以内で一致(et 1.072 対 1.069)。seed を増やすべきかは学習前の test 予測だけで判断できる
  • 追加2: 補正CV最良を採るのに「lgb_det より 3×10⁻⁴ 以上良いこと」を求めると、avg3 の損が 1.95 → 0.64、s1 が 1.49 → 0.37 に下がる。それでも「常に lgb_det」(0.24〜0.58)には届かない

外れ値2件の正体: avg3 で補正CV が et を選んだ2世界(損 11.76・9.72)と、s1 で et を選んだ1世界(12.45)が、補正CV の損の平均をほぼ決めている(et を候補から外すと avg3 は 0.62、s1 は 0.71)。ワールド4 の et は補正CV − 真が 4つの seed 設定すべてで −11.0〜−11.5(lgb_det は +1.7)。seed を変えても動かないので、seed の揺れではない。 lgb_det との相対的なずれの世界間 SD は、他の候補 3.0〜4.4 に対し et 6.08。分割の閾値をランダムに引く extra_trees は、ワールド(データ)ごとの CV の当たり外れが他と違う向きに出る。

タスクD(決定)

(1) 決定13 の範囲を広げる: 乱数ありのモデルでも補正CV で比べる(偏りについては◎)。 16ワールドで11候補とも補正後の偏りが 2SE 未満、生の偏り最大 +12.30 を +0.09 にした。恒等式(表1)も seed ありで一致。

(2) 決定14(新規): 補正CV で基準を乗り換えるには余裕を求める。

#決定採った案採らなかった案と理由前提が変わったら
14基準からの乗り換え乱数なしの基準(今は lgb_det か HistGB 既定値)より補正CV が 3×10⁻⁴ 以上良い候補だけを1本目に採る。届かなければ基準を1本目に出し、補正CV最良は2本目の候補にする。乱数ありの候補は seed を2組以上回し、seed による補正CV の世界内 SD(今日 1.3〜2.3)を報告に添える(a) 補正CV最良をそのまま採る(今日は外れ値3件で損 0.58〜1.95、et に引っ張られる)。(b) 常に基準を出す(今日は最小の損 0.24〜0.58 だが、本当に良い候補が来たとき拾えない。Day171 では事前固定の損 3.00 対 補正CV 0.92 で逆の結果)。(c) 余裕を世界ごとの SE から決める(ワールドは実戦で1つしかなく、その SE は測れない)。(d) seed を増やして OOF の揺れを減らす(avg3 でも et の外れは消えなかった。seed ではなくデータ側のずれだったため)本当に良い乱数ありの候補(基準より 3 以上良い)が多い問題 → 余裕 3 は良い候補を見逃す。今日はその場合を作っていないので、余裕の大きさは仮置き(印△)。基準と別の系統(extra_trees・NN など)が入る → 系統ごとのずれの SD を先に測る

(3) 3 seed 平均は「得の予測」が当たる。 2σ²_seed/15 で学習前に分かるので、seed 平均を足すかはこの予測と計算時間で決める(今日 bag08 で 0.34×10⁻⁴、3倍の時間)。

(4) 設計書の印の更新: 決定13 は○→◎(seed ありでも偏りが消えることを確認、2つ目の条件)。決定12(補正CV最良+公開LB最良)は、決定14 で1本目が基準になる場合があるので「1本目=決定14 の結果、2本目=公開LB最良(同じなら補正CV最良)」に読み替える。2本目の得(今日 公開20%で +0.05〜−1.09、どれも 2SE 未満)は測れていない。

📊 結果の可視化

CV−真 の lgb_det との差(×10⁻⁴、16ワールド、seed 3組を平均)赤=生の CV/緑=補正CV。右=CV が実際より悪く見えるbag03_ff05+12.30 → +0.09leaf63_b7+10.72 → +0.99et+9.42 → +0.35bag05+7.29 → +1.27bag05_ff05+7.20 → +0.51bag08+1.93 → +0.25hgb_default+0.25 → +0.14ff05+0.09 → +0.04leaf15_b7-0.89 → +0.45lr05_it600_b7-1.36 → +0.04
方針別の損(×10⁻⁴、短いほど良い。実測・16ワールド・11本)上から seed 組 s0 / s1 / s2 / avg3(3 seed 平均の提出)常に lgb_det(選ばない)0.33 (s0)0.32 (s1)0.24 (s2)0.58 (avg3)ガード(余裕3)0.57 (s0)0.37 (s1)0.29 (s2)0.64 (avg3)補正CV最良0.75 (s0)1.49 (s1)0.58 (s2)1.95 (avg3)CV最良(生)0.93 (s0)1.00 (s1)0.52 (s2)2.65 (avg3)公開LB最良(20%)2.10 (s0)2.01 (s1)2.03 (s2)1.99 (avg3)
3 seed 平均の MSE の得: ラベル無しの予測 2σ²/15(横)対 実測(縦)、×10⁻⁴000.40.40.80.81.21.2lr05_it600_b7bag08ff05leaf15_b7bag05leaf63_b7bag05_ff05bag03_ff05et点線=予測どおり。9候補とも4%以内(et 1.072 対 1.069)

上: seed の揺れがあっても、生の CV の偏り(最大 +12.30)は補正でほぼ消える。中: それでも選ぶ方針はどれも「常に lgb_det」(0.24〜0.58)に負けた。今日の候補に本当に良いものが無く、選ぶノイズだけが残ったため。下: 3 seed 平均の得はラベル無しで予測でき、実測と一致する。

🪜 Step-by-Step 解説

Step 1: 理論で「壊れないもの」と「壊れうるもの」を分ける

恒等式は代数なので壊れない。壊れうるのは「1回ごとの値のぶれ」。前者は表1で、後者は表3で測る、と先に決めておく。

Step 2: Day171 と同じワールドを作り、つながりを確かめる

HistGB 既定値を候補に残し、Day171 の fold 別予測と 16ワールドとも完全一致(0.0)を確認した。これで今日の LightGBM の数字を Day171 と同じ土俵で比べられる。lgb_det は seed を変えても完全一致し、「乱数なし」の対照として使える。

Step 3: seed の割合を測る(表1)

seed を3通り回すと、fold の中で seed だけを変えた分散 σ²_seed が出る。bag08 で fold 間分散の 46%、extra_trees で 95%。LightGBM の bagging を入れると、fold 間分散の半分前後は seed の揺れになる。

Step 4: 偏りが消えるかを見る(表2)

生の CV は乱数が多い候補ほど悪く見える(bag03_ff05 +12.30、et +9.42)。補正後は +0.09・+0.35。平均としては、決定13 は seed ありでも正しい。

Step 5: 1回ごとのぶれを見る(表3)

seed 組を変えるだけで補正CV − 真は 1.3〜2.3 動き、真の点数の揺れ(0.5〜1.1)の約2倍。理由は、OOF が1モデルで seed の揺れを薄められないから。

Step 6: 方針で比べる(表4・表5)

補正CV最良と生の CV最良は差なし。公開LB最良には公開20%で勝つ(s0 −1.35±0.29)。しかし lgb_det を常に出すと損 0.24〜0.58 で、どの選び方より小さい。真の最良は lgb_det・HistGB・bag08 の3つしか出てこない(表5)。

Step 7: 外れ値を調べる

補正CV の損を押し上げたのは et を選んだ3件。seed を変えても同じずれ(−11)が出るので、seed ではなくデータ側(ワールドごとの CV の当たり外れ)の問題。系統が違う候補は、ずれの世界間 SD が倍近い(6.08 対 3.0〜4.4)。

Step 8: ガードを試す(追加2)

「基準より 3 以上良いときだけ乗り換える」で外れ値3件が消え、損は 0.29〜0.64。余裕の大きさ 3 は今日のずれの SD(約3)に合わせた仮置き。本当に良い候補がある世界では損になりうるので、決定14 の印は△にした。

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

手ぶれは「平均の人数」で割れる。 射手の手ぶれ(seed の揺れ)の分散を σ² とすると、5人の矢の真ん中の手ぶれは σ²/5、15人なら σ²/15。手ぶれは的(正解)を見なくても、同じ射手に何度か射させて矢どうしの散らばりを見れば測れる。今日の「3 seed 平均の得の予測 2σ²/15」はこの引き算で、実測と 4% 以内で合った。

偏りが無い ≠ 当てになる。 体重計が平均では正しくても、乗るたびに ±2kg ぶれるなら、0.5kg の差は見分けられない。補正CV は「平均では正しい体重計」になったが、seed の手ぶれ(±1.3〜2.3)と、系統ごとのくせ(et で ±6)が残る。候補どうしの本当の差(今日は多くが 1 未満)より大きいと、比べるほど外れを引く。

だから余裕を求める。 「3 以上軽くなったら本当に痩せた」と決めておく考え方。余裕を大きくすると誤った乗り換えは減るが、本当の改善も見逃す。この交換条件は、本当に良い候補がどれくらいの頻度で来るかで決まる(今日は測っていない)。

🏆 Kaggleでの実践的な使い方

  • LightGBM の bagging・列抽出を比べるときも、生の OOF は乱数の多い設定を不当に悪く見せる。 今日 bag03_ff05 は +12.30×10⁻⁴ 悪く見えた。fold 別の test 予測を平均前に保存し、補正CV で比べる
  • seed 平均を足すか迷ったら、学習前に得を予測する。 seed を2つ回して test 予測の分散 σ² を取り、2σ²/15(3 seed の場合)を CV の改善幅と比べる。上位解法の「seed averaging」を、計算時間に見合うかで判断できる
  • 系統の違うモデル(extra_trees、NN など)の CV は、同じ系統どうしより当てにならない。 1本目を乗り換えるのは余裕があるときだけにし、迷う候補は2本目か混ぜる側に回す
  • 「何も選ばない」方針も比べる。 候補を増やすほど、本当の差が無い所で CV のノイズに選ばされる。今日は既定値を出すのが最良だった

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
乱数ありのモデルでは fold 間分散の補正が使えないseed の揺れは「データの差」ではないと思う恒等式は5本の予測が何でも成り立つ。今日も小数2桁で一致し、補正後の偏りは11候補とも 2SE 未満
偏りが消えたので補正CV最良を採れば良い偏り(平均のずれ)と、1回ごとのぶれを混同するseed で 1.3〜2.3、系統によって ±6 ぶれる。本当の差が小さいと外れを引く(今日は既定値を常に出す方が良かった)
seed を増やせば CV の外れは消える外れの原因を seed だと決めつけるet の外れは seed を変えても同じ −11 だった。データ側(ワールドごと)のずれは seed では減らない
seed 平均の得はやってみないと分からない得は正解との比較だと思う2σ²_seed/15 で test 行から予測でき、実測と 4% 以内で一致
LightGBM で random_state を固定すれば seed の問題は無い再現性と乱数の影響を混同する固定すれば毎回同じ値になるだけで、「その seed の当たり外れ」は残る。比べるなら seed を2組以上回して揺れを測る

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

  • 本当に良い乱数ありの候補: 今日の9本はどれも真に lgb_det より悪かった(avg3 の bag08 が −0.08 で並ぶだけ)。そのため「補正CV が乗り換えで得をする」場面を作れておらず、決定14 の余裕 3 は良い候補を見逃す側のコストを測っていない。次に測るべき最重要項目
  • ワールドの作りが LightGBM 向きでない可能性: シミュレータは Keytel の式+小さな雑音で、木の深さや bagging の正則化が効きにくい。実データ(S5E5)で乱数ありの設定が勝つかは分からない
  • 余裕の決め方: 3×10⁻⁴ は今日のずれの世界間 SD(約3)に合わせた仮置き。0/0.5/1/2/3 しか試しておらず、交差検証で決めたわけではない
  • 公開50%・候補数 K: 公開は 5%・20% だけ。K は11本固定(Day171 の K 依存は出し直していない)
  • et のずれの原因: 「分割閾値をランダムに引く系統はワールドごとのずれが大きい」は観察で、なぜかは調べていない
  • seed による OOF の揺れの分離: OOF を3 seed 平均にした avg3 でも外れは残ったが、「OOF の seed の揺れ」と「test 側の seed の揺れ」を別々に測ってはいない
  • 持ち越し(Day171 から): noKeytel・raw7 の補正後のずれ、補正CV と入れ子CV の比較、A_test の推定誤差、Day170 のハイブリッド方針・狭い10本の出し直し、SE 個数差と分割の新しさ(Day165)、Day162 の選択バイアス、真の効果ちょうど0の偽採用率、NB・Krogh & Vedelsby 原典(今日も記憶による。式は表1と追加1で数値的に確かめた)

🚀 次のステップ

  • 発展: 真に基準より良い乱数ありの候補(例: 雑音を増やしたワールドで bag05 が効くように作る)を混ぜ、決定14 の余裕 0〜5 で「見逃しの損」と「外れの損」の釣り合う点を測る
  • 発展: et のような別系統について、ワールドごとのずれの SD を系統別に測り、余裕を系統ごとに変えるべきかを調べる
  • 次回予告: Day173 — Playground Series 実戦投入⑧(決定14 の余裕を、本当に良い候補が含まれる候補群で測り直す)。月替わりテーマ3「画像(CV)系コンペへの応用」は 10/25 から

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

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