Day 171 — 2026-10-04 | Playground Series 実戦投入⑥(月内のまとめ)— CV設計書を1枚にし、いちばん弱い所を測り直した。CV の偏りは「1モデル対5モデル平均」で説明でき、ラベル無しで直せた(補正CVの損 0.92 対 CV 2.34、×10⁻⁴)。ついでに Day170 の混合候補1本の作り間違いを見つけた

2026-10-04 金 / Phase 6(月替わりテーマ2: Playground Series 実戦投入) 戦略→コーディング HistGB · 16ワールド · 960 fit · 508秒

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

今月やってきたこと。 S5E5(消費カロリー予測、RMSLE)を写した縮小シミュレータで、Kaggle のコンペを1本通すときの判断を12個決めてきた(決定1〜12)。どの決定にも「採らなかった案」と「前提が変わったら」が付いているが、証拠の強さはばらばらだ。5世界で1.6SE しかないもの、記憶だけで公式ルールを確かめていないもの、仮説のまま次へ進んだものが混ざっている。

設計書を1枚にする意味。 決定を日ごとのファイルに散らしておくと、(1) 決定どうしの矛盾に気づかない、(2) 弱い証拠の上に次の決定を積んでいることに気づかない。1枚に並べると両方が見える。今日はまずそれをやり、いちばん弱い所を1つ選んで測り直す。

今日測り直すもの: CV の偏り(Day170)。 Day170 で、CV(OOF)と真の点数のずれが候補ごとに違った(leaf63 は default より CV が +5.73 悪く見え、leaf15 は −4.51 良く見えた、×10⁻⁴)。そのせいで真の最良(16世界中14世界で leaf63)を CV はほとんど選べなかった。仮説は「OOF は1モデルの予測、提出は5つの fold モデルの平均。複雑な設定ほど平均で得をするのに、CV はその得を数えていない」。

平均の得はラベル無しで測れる(今日の鍵)。 二乗誤差では、次の式が行ごとに必ず成り立つ(アンサンブルの「あいまいさ分解」。Krogh & Vedelsby 1995, NIPS 7 として知られる。今日は原典を開いていない=記憶による)。

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

右の「ばらつき」には正解ラベルが要らない。test の行(ラベル無し)で fold モデルどうしの予測のばらつきを測れば、平均でどれだけ得するかが分かる。 RMSLE は log1p 空間の RMSE なので、log1p で学習した予測にそのまま使える。

用語整理:

用語意味
CCV の点数(OOF の RMSLE。各行を学習に使っていない1モデルが予測)
T1真の点数(truth 6万行)を、fold モデル1本ずつで測って平均したもの
T5真の点数を、5モデル平均(提出の形)で測ったもの
A_testtest 2万行で、5つの fold モデルの予測の分散を行ごとに取り、平均したもの(ラベル不要)
補正CVsqrt(C² − A_test)。CV を「5モデル平均で提出したときの点数」に直したもの
損(regT)選んだ候補の T5 − 候補中の T5 の最良(×10⁻⁴、小さいほど良い。Day170 と同じ)
証拠の印◎=対の差が 3SE 超、2つ以上の条件で同じ向き / ○=測ったが 2〜3SE か条件1つ / △=2SE 未満・比較していない / ✕=記憶・仮説だけ

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

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

名前形・型意味・作り方使う場所
Sex … Body_Tempint / float(7列)元データ 1.5万行から合成(Day163〜170 と同じ、乱数 3000+ワールド)全候補
Dur_x_HR, Keytel_kcalfloat決定6 で採用した2列HistGB(noDurHR・noKeytel・raw7 は抜く)
oof(12, 30000) float64分割seed 0 の5分割の OOF(1モデルの予測)→ CCV・補正CV
evk(12, 5, 80000) float32平均しない fold 別予測。先頭2万行=test、残り6万行=truthT1・T5・A_test
A_test(候補,)test 行で fold 間の予測の分散を取り平均(ラベル不要)補正CV = √(C² − A_test)
Calories(目的変数)floatlog1p 空間で学習、RMSLE で評価—

評価指標と今日の恒等式

RMSLE² = log1p 空間の平均2乗誤差。平均の2乗誤差 = 1モデルずつの平均 − fold 間の分散leaf63 の例(16ワールド平均、MSE 単位 ×10⁻⁴)。分散は正解ラベル無しで測れる1モデルずつの平均(T1²)5モデル平均(T5²、提出)分散分散: truth 行 5.71 / test 行(ラベル無し)5.68 / T1²−T5² 5.71 → 補正CV = √(CV² − 分散)

❓ 問題

タスクA(設計書)— コードを書く前に

  1. 決定1〜12 を1枚の表にし、各決定に証拠の印(◎○△✕)を付ける。印の根拠(何SE・何ワールド)も1行で書く
  2. 決定どうしで矛盾しているものを探す(ヒント: 公開LBの使い方)
  3. 印が最も弱く、かつ他の決定がその上に乗っているものを1つ選ぶ

タスクB(予想)

  1. CV の偏り(候補間で SD 3.3×10⁻⁴)のうち、何割が「1モデル対5モデル平均」(T1 − T5)で説明できるか
  2. ラベル無しの補正 sqrt(C² − A_test) で CV最良を選ぶと、損は Day170 の CV最良(2.80)・公開LB最良(2.11)と比べてどうなるか
  3. 候補数 K を 3→13 に増やすと、公開LB最良の損はどう増えるか

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

  1. Day170 と同じ16ワールドを作り直し、5つの fold モデルの予測を平均せずに保存する(day171.py)。Day170 の5モデル平均と一致するかを確かめる
  2. 候補ごとに C − T5 を (C − T1) + (T1 − T5) に分け、A_test で補正した CV で方針を比べ直す。K を 3/5/8/13 に変えた損も出す(analyze171.py)

タスクD(決定)

設計書を更新する(決定12 の改訂、決定13 の追加、決定5 の扱い)。S5E5 実データで利用者が行う作業を手順として切り出す。

💡 ヒント

ヒント1(方向性)

  • 決定5(Day163)と決定12(Day170)を並べて読む。公開LBを「壊れ確認だけ」にするのか「2本目を選ぶ」のに使うのか
  • 偏りの原因を確かめるには、同じ候補の「1モデルの真の点数」が要る。Day170 は5モデル平均しか保存していなかった

ヒント2(アプローチ)

  • 恒等式: mean_k (f_k − y)² = (f̄ − y)² + mean_k (f_k − f̄)²。右の第2項が fold 間分散。行ごとに成り立つので、平均しても成り立つ
  • 作り直したデータが Day170 と一致するかを、候補ごとに最大差で見る。一致しない候補があれば、どちらかの作り方が違う

ヒント3(コード骨格)

evk = np.zeros((n_cand, 5, N_TEST + N_TRUTH), np.float32)          # (候補, fold, 行)
evk[j, k] = model.predict(evalset[cols])                           # 平均せずに入れる
A_te = evk[:, :, :N_TEST].var(axis=1).mean(1)                      # test 行の fold 間分散(ラベル無し)
C_adj = np.sqrt(C_mse - A_te)                                      # 提出の形に直した CV
T1_mse = np.mean([sle(y, evk[j, k, N_TEST:]).mean() for k in range(5)])   # 1モデルの真の点数

✅ 模範解答

タスクA(設計書の模範)

S5E5 用 CV設計書(2026-10-04 版)。 「印」は上の用語整理の基準。「乗っている決定」は、その決定が崩れると見直しが要る決定。

#決定採った案印印の根拠乗っている決定
1分割ランダム KFold 5分割(shuffle・seed固定)△層別 KFold・GroupKFold と比べていない。敵対的 AUC 0.5004 だけ測った(Day163)全部
2目的変数log1p で学習、expm1 で戻す◎private −0.0049、5seed で 5/5 同じ向き(Day163)13(log 空間の恒等式)
3元データ学習 fold にだけ足す△private −0.00024±0.00015(1.6SE、Day163)6〜8
4予測のクリップ下限1—論理(NaN 防止)。測る対象ではない—
5公開LBの使い方判断は CV、公開LBは壊れ確認だけ✕(撤回)決定12 と矛盾(下)—
6特徴量の採否1つずつ足し、R2 で −2SE◎28件中 R2 の判定が真と一致26、偽採用1・見逃し1(Day164)7・8・11
7ハイパーパラメータ既定値を trial 0、Optuna 60回、新分割 R2 で採否○2ワールドで採否6/6が真と一致。ただし SE の個数差と分割の新しさは未分離(Day165)12
8混ぜるHistGB に knn を 0.10 固定○4ワールド中、真に採用に足りたのは2(Day166)12・13
9再現性起動時に OOF の sha256 を比べる。seed は対にする○max_features=1.0 で 6/6 完全一致(Day167)。ただし今日、sha256 では捕まらない作り間違いが見つかった(下)全部
10SE の使い分け採否は naive −2SE、報告は NB 補正○24ワールドで naive の被覆 0.82(Day168)。NB 原典は未読6・7・11
11−2SE の読み方「効く列か、効果±1.7以内の列」○108件中 11件採用(10.2%、Wilson [5.8,17.3]、Day169)6
12最終2本CV最良+公開LB最良△ → 改訂CV最良の損は Day170 で 2.80。原因が仮説のまま、他の決定の結論(提出の選び方)がその上に乗る—

矛盾(タスクA-2): 決定5(Day163)は「公開LBは壊れ確認だけ」、決定12(Day170)は「2本目は公開LB最良」。Day170 の測定(公開20%で公開LB最良が単独で 2.11、CV最良 2.80)を根拠に12が5を上書きしたが、5を撤回した記録がどこにも無かった。今日、5を撤回と明記する。

選んだ弱点(タスクA-3): 決定12 の「CV の偏り」。印が△で、原因は仮説、しかも提出の選び方という最後の決定がその上に乗る。原因が分かれば、補正の方法まで決まる可能性がある(決定12 を ✕ から外せる)。

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

#予想の模範実測
1大半(7〜8割)が T1 − T5 で説明できるおおむね当たり。 leaf63 の偏り +5.73 は T1−T5 で +5.48、leaf15 −4.51 は −4.01、it150 −3.84 は −4.03(default との差、×10⁻⁴)。候補間 SD は 3.31 → 補正後 2.25。ただし残り 2.2 のうち、特徴量を抜いた候補(noKeytel −2.35±1.11、raw7 −1.81±1.06)には平均の得では説明できないずれが残った
2補正CV最良は CV最良より良く、公開LB最良と同程度予想より良い。 補正CV最良の損 0.92 は、CV最良 2.34 より −1.42±0.33(4.3SE)、公開20%の公開LB最良 2.10 より −1.18±0.33(3.6SE)。真の最良に当たった世界は 1/16 → 8/16
3K に比例して増える増えたが比例ではない。 K=3/5/8/13 で 0.92/1.27/1.67/2.15。一様ランダムに対する比は 0.35→0.45 とゆっくり悪化。補正CV最良は 0.70/0.93/1.04/0.92 でほぼ平ら

途中で見つかったこと(予想外): 作り直したデータを Day170 と比べると、12本は最大差 3.8×10⁻⁷(float32 の丸め)で一致したのに、混合候補 lr05_it600+knn.1 だけ最大 0.087 ずれた。Day170 のコードは混合を2本続けて足すとき oof[-1](最後の行)を knn のつもりで使ったが、1本目を足した後の最後の行は default+knn.1 だった。Day170 の lr05_it600+knn.1 は、実際には 0.9×lr05 + 0.09×default + 0.01×knn だった(この式で作り直すと最大差 4.7×10⁻⁷ で一致、16ワールドとも)。

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

day171.py(Day170 の day170.py から3か所だけ変えた。変更は fold 別に保存することと、混合を作らないこと):

"""Day171: Day170 と同じワールド・同じ候補12本を作り直し、5つの fold モデルの予測を平均せずに1本ずつ保存する。
使い方: OMP_NUM_THREADS=1 python day171.py <world>   -> raw171_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-170 と同じ
    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-170 と同じ
    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の既定値
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)); evk = np.zeros((len(names), 5, N_TEST + N_TRUTH), np.float32)   # (候補, 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)
    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]); evk[j, k] = m.predict(evalset[cols])
    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])); evk[-1, k] = m.predict(sc.transform(evalset[KNN]))
    print(f"world {W} fold {k} {time.time()-T0:.0f}s", flush=True)
# 混合は後で(fold ごとに 0.9*base + 0.1*knn)。ここでは12本の fold 別予測だけ保存
np.savez_compressed(f"raw171_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")

analyze171.py(分解・補正・方針の比べ直し・候補数 K):

"""Day171 集計: raw171_w*.npz(fold 別予測)から
(B) CV の偏りが「1モデル対5モデル平均」で説明できるか(分解: C − T5 = (C − T1) + (T1 − T5))
(B') ラベル無しで測れる補正 A_test(test 行での fold 間の予測の分散)で CV を直す: C_adj = sqrt(C_mse − A_test)
(C) 補正した CV で最終提出の方針を比べ直す(公開 5/20/50%、再分割 500 回、Day170 と同じ乱数)
(D) 候補数 K を 3/5/8/13 に変えたときの方針の損
使い方: python analyze171.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("raw171_w*.npz"), key=lambda f: int(f.split("_w")[1].split(".")[0]))
OLD = "../d170/raw170_w{}.npz"   # Day170 の出力(再現確認用)


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


dec, pol_rows, k_rows, repro = [], [], [], []
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"])
    # 混合(決定8の形)を fold ごとに作る。knn の行番号を先に固定しておく(Day170 の作り間違いの再発防止)
    kn = names.index("knn")
    for base in ("default", "lr05_it600"):
        b = names.index(base)
        oof = np.vstack([oof, 0.9 * oof[b] + 0.1 * oof[kn]]); evk = np.concatenate([evk, (0.9 * evk[b] + 0.1 * evk[kn])[None]])
        names.append(f"{base}+knn.1")
    ev5 = evk.mean(1)                                                          # 提出の形(5モデル平均)
    old = np.load(OLD.format(w)); repro.append(np.abs(ev5 - old["ev"].astype(np.float64)).max())
    ids = [i for i, n in enumerate(names) if n != "knn"]
    yte, ytru = yev[:N_TEST], yev[N_TEST:]
    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_tru = evk[:, :, N_TEST:].var(axis=1).mean(1)                            # truth 行の fold 間分散(恒等式の項)
    A_te = evk[:, :, :N_TEST].var(axis=1).mean(1)                             # test 行の fold 間分散(実戦で測れる)
    C_adj_mse = C_mse - A_te
    C, T5, T1, Ca = np.sqrt(C_mse), np.sqrt(T5_mse), np.sqrt(T1_mse), np.sqrt(C_adj_mse)
    for j in ids:
        dec.append(dict(world=w, cand=names[j], C_T5=(C[j] - T5[j]) * 1e4, C_T1=(C[j] - T1[j]) * 1e4, T1_T5=(T1[j] - T5[j]) * 1e4,
                        Cadj_T5=(Ca[j] - T5[j]) * 1e4, A_tru=A_tru[j] * 1e4, gap_mse=(T1_mse[j] - T5_mse[j]) * 1e4, A_te=A_te[j] * 1e4,
                        T5=T5[j], C=C[j], Ca=Ca[j]))
    E_te = np.stack([sle(yte, ev5[j, :N_TEST]) for j in range(len(names))])
    fixed = names.index("default+knn.1")
    rng = np.random.default_rng(w)                                            # Day170 と同じ乱数(広い13本の再分割)
    idsA = np.array(ids)
    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[idsA] @ M; U = np.sqrt(S_pub / n_pub)
        P = np.sqrt((E_te[idsA].sum(1, keepdims=True) - S_pub) / (N_TEST - n_pub))
        Tk, Ck, Cak = T5[idsA], C[idsA], Ca[idsA]; best = Tk.min()
        fx = int(np.where(idsA == fixed)[0][0]); pub = U.argmin(0); cv = int(Ck.argmin()); cva = int(Cak.argmin())
        def two(a):
            t = np.stack([np.full(R, a) if np.isscalar(a) else 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)]
        pol = {"CV最良": np.full(R, cv), "補正CV最良": np.full(R, cva), "公開LB最良": pub, "事前固定(決定8)": np.full(R, fx),
               "2枠: CV最良+公開最良": two(cv), "2枠: 補正CV最良+公開最良": two(cva)}
        for nm, ch in pol.items():
            pol_rows.append(dict(world=w, frac=frac, policy=nm, regT=float(((Tk[ch] - best) * 1e4).mean())))
        if frac == 0.20:                                                      # (D) 候補数 K
            krng = np.random.default_rng(10_000 + w)
            for K in (3, 5, 8, 13):
                for s in range(300):
                    sub = np.sort(krng.choice(len(idsA), K, replace=False)) if K < len(idsA) else np.arange(len(idsA))
                    r = int(krng.integers(R)); b = Tk[sub].min()
                    p_ = sub[U[sub, r].argmin()]; c_ = sub[Ck[sub].argmin()]; a_ = sub[Cak[sub].argmin()]
                    def pick2(x): return x if P[x, r] <= P[p_, r] else p_
                    k_rows.append(dict(world=w, K=K, 公開LB最良=(Tk[p_] - b) * 1e4, CV最良=(Tk[c_] - b) * 1e4, 補正CV最良=(Tk[a_] - b) * 1e4,
                                       **{"2枠(CV+公開)": (Tk[pick2(c_)] - b) * 1e4, "2枠(補正CV+公開)": (Tk[pick2(a_)] - b) * 1e4},
                                       一様ランダム=(Tk[sub].mean() - b) * 1e4))

print(f"worlds={len(files)}  Day170 との再現(5モデル平均の最大差): max {max(repro):.2e}")
D = pd.DataFrame(dec)
print("\n=== 表1 分解(×1e-4、16ワールド平均) ===")
print(D.groupby("cand").mean(numeric_only=True).drop(columns=["world", "T5", "C", "Ca"]).round(2).to_string())
print("\n=== 表2 default との差(世界ペア、平均±SE、×1e-4) ===")
rows = []
for col in ("C_T5", "C_T1", "T1_T5", "Cadj_T5"):
    X = D.pivot(index="world", columns="cand", values=col); X = X.sub(X["default"], axis=0)
    for c in X.columns: rows.append(dict(cand=c, col=col, m=X[c].mean(), se=X[c].std(ddof=1) / np.sqrt(len(X))))
print(pd.DataFrame(rows).pivot(index="cand", columns="col", values=["m", "se"]).round(2).to_string())
for col in ("C_T5", "C_T1", "Cadj_T5"):
    print(f"候補間SD of {col}: {D.groupby('world')[col].std(ddof=1).mean():.2f}")
sp = []
for w, g in D.groupby("world"):
    sp.append(dict(world=w, rho_C=spearmanr(g.C, g.T5)[0], rho_Ca=spearmanr(g.Ca, g.T5)[0],
                   hit_C=g.cand.iloc[g.C.argmin()] == g.cand.iloc[g.T5.argmin()], hit_Ca=g.cand.iloc[g.Ca.argmin()] == g.cand.iloc[g.T5.argmin()],
                   best=g.cand.iloc[g.T5.argmin()], cabest=g.cand.iloc[g.Ca.argmin()]))
SP = pd.DataFrame(sp)
print("\n=== 表2b CV と真の順位相関(世界平均)・真の最良に当たった世界数 ===")
print(f"rho(C,T5) {SP.rho_C.mean():.3f}  rho(Cadj,T5) {SP.rho_Ca.mean():.3f}  hit C {SP.hit_C.sum()}/{len(SP)}  hit Cadj {SP.hit_Ca.sum()}/{len(SP)}")
print("Cadj最良に選ばれた候補:", SP.cabest.value_counts().to_dict(), " 真の最良:", SP.best.value_counts().to_dict())
print("\n=== 表3 方針の損(regT, ×1e-4, 16世界平均)と主要な対の paired 差 ===")
PR = pd.DataFrame(pol_rows)
print(PR.groupby(["frac", "policy"]).regT.agg(["mean", lambda s: s.std(ddof=1) / np.sqrt(len(s))]).round(3).to_string())
for fr in (0.05, 0.2, 0.5):
    a = PR[PR.frac == fr].pivot(index="world", columns="policy", values="regT")
    for x, y in [("補正CV最良", "CV最良"), ("補正CV最良", "公開LB最良"), ("補正CV最良", "事前固定(決定8)"),
                 ("2枠: 補正CV最良+公開最良", "2枠: CV最良+公開最良"), ("2枠: 補正CV最良+公開最良", "補正CV最良"), ("公開LB最良", "CV最良")]:
        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 候補数 K(公開20%、各世界300回の無作為部分集合、×1e-4) ===")
KR = pd.DataFrame(k_rows)
print(KR.groupby("K").mean(numeric_only=True).drop(columns="world").round(2).to_string())
print((KR.groupby(["K", "world"]).mean(numeric_only=True).groupby("K").std(ddof=1) / np.sqrt(len(files))).round(2).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(Day170 と同じ uv venv)
#              16ワールド(4並行、OMP_NUM_THREADS=1)。壁時計 508秒。1ワールド 109〜129秒。fit 数 = 12モデル × 5 fold × 16 = 960
# 再現確認: 基本12本(HistGB 11本 + knn)は Day170 の5モデル平均と最大差 3.8e-07(float32 の丸め)、OOF は完全一致(差 0.0)
#           混合 lr05_it600+knn.1 だけ最大差 8.7e-02 → Day170 の値は 0.9*lr05 + 0.1*(default+knn.1) と一致(最大差 4.7e-07、16ワールド)
# このファイルに載せたコードを抜き出して回し直し、下の数字と一致することを確かめた。表2〜4 は出力を読みやすく並べ替えてある(数字は出力のまま)

=== 表1 分解(×1e-4、16ワールド平均): A_tru=truth行の fold 間分散、gap_mse=T1_mse−T5_mse、A_te=test行の分散(3つが一致すれば恒等式どおり) ===
                   C_T5  C_T1  T1_T5  Cadj_T5  A_tru  gap_mse  A_te
default           11.08 -1.65  12.73    -1.67   3.99     3.99  3.99
default+knn.1      9.28 -1.44  10.72    -1.46   3.36     3.36  3.36
it150              7.24 -1.46   8.70    -1.47   2.73     2.73  2.73
l2_1              11.48 -0.99  12.48    -0.97   3.91     3.91  3.90
leaf15             6.56 -2.15   8.71    -2.16   2.74     2.74  2.74
leaf63            16.80 -1.40  18.20    -1.34   5.71     5.71  5.68
lr05_it600         9.39 -1.68  11.07    -1.72   3.47     3.47  3.48
lr05_it600+knn.1   7.85 -1.53   9.37    -1.56   2.93     2.93  2.94
msl5              12.42 -2.11  14.53    -2.16   4.56     4.56  4.57
msl50              8.98 -2.10  11.08    -2.14   3.48     3.48  3.49
noDurHR           10.10 -2.39  12.49    -2.41   3.91     3.91  3.91
noKeytel          10.56 -4.00  14.55    -4.06   4.58     4.57  4.58
raw7              10.61 -3.46  14.06    -3.53   4.43     4.42  4.44

=== 表2 default との差(世界ペア、平均±SE、×1e-4) ===
                  C_T5 (Day170の偏り)   C_T1 (1モデル同士)   T1_T5 (平均の得)   Cadj_T5 (補正後)
default+knn.1      -1.80 ± 0.23          0.21 ± 0.22         -2.01 ± 0.02        0.21 ± 0.22
it150              -3.84 ± 0.54          0.19 ± 0.53         -4.03 ± 0.04        0.20 ± 0.53
l2_1                0.41 ± 0.53          0.66 ± 0.52         -0.25 ± 0.05        0.70 ± 0.55
leaf15             -4.51 ± 0.73         -0.50 ± 0.73         -4.01 ± 0.07       -0.49 ± 0.74
leaf63              5.73 ± 0.98          0.25 ± 0.91          5.48 ± 0.09        0.33 ± 0.94
lr05_it600         -1.69 ± 0.49         -0.03 ± 0.48         -1.66 ± 0.04       -0.05 ± 0.49
lr05_it600+knn.1   -3.23 ± 0.55          0.12 ± 0.53         -3.36 ± 0.05        0.11 ± 0.54
msl5                1.34 ± 0.68         -0.46 ± 0.67          1.81 ± 0.10       -0.49 ± 0.68
msl50              -2.09 ± 0.62         -0.45 ± 0.61         -1.64 ± 0.06       -0.47 ± 0.62
noDurHR            -0.98 ± 0.46         -0.74 ± 0.45         -0.24 ± 0.06       -0.74 ± 0.45
noKeytel           -0.52 ± 1.15         -2.35 ± 1.11          1.83 ± 0.09       -2.39 ± 1.11
raw7               -0.47 ± 1.10         -1.81 ± 1.06          1.34 ± 0.10       -1.86 ± 1.07
候補間SD of C_T5: 3.31   C_T1: 2.21   Cadj_T5: 2.25

=== 表2b CV と真の順位相関(世界平均)・真の最良に当たった世界数 ===
rho(C,T5) 0.428  rho(Cadj,T5) 0.701  hit C 1/16  hit Cadj 8/16
Cadj最良に選ばれた候補: leaf63 7, msl5 7, noDurHR 1, default+knn.1 1   真の最良: leaf63 14, msl5 1, noDurHR 1

=== 表3 方針の損(regT, ×1e-4, 16世界平均 ± SE)。lr05_it600+knn.1 は正しい混合に直した ===
                       公開5%          公開20%         公開50%
補正CV最良             0.92 ± 0.29     0.92 ± 0.29     0.92 ± 0.29
2枠: 補正CV最良+公開最良  0.92 ± 0.25     0.88 ± 0.24     0.86 ± 0.25
2枠: CV最良+公開最良    1.82 ± 0.24     1.58 ± 0.24     1.44 ± 0.28
公開LB最良             3.26 ± 0.21     2.10 ± 0.19     1.50 ± 0.24
CV最良                2.34 ± 0.29     2.34 ± 0.29     2.34 ± 0.29
事前固定(決定8)         3.00 ± 0.29     3.00 ± 0.29     3.00 ± 0.29
paired(世界ペア, n=16):
公開 5% 補正CV最良 − CV最良: -1.42 ± 0.33   補正CV最良 − 公開LB最良: -2.33 ± 0.35   補正CV最良 − 事前固定: -2.08 ± 0.38
公開20% 補正CV最良 − CV最良: -1.42 ± 0.33   補正CV最良 − 公開LB最良: -1.18 ± 0.33   2枠(補正) − 2枠(CV): -0.69 ± 0.23
公開50% 補正CV最良 − CV最良: -1.42 ± 0.33   補正CV最良 − 公開LB最良: -0.58 ± 0.35   2枠(補正) − 2枠(CV): -0.58 ± 0.22
2枠(補正) − 補正CV最良 単独:  公開5% -0.00 ± 0.09 / 20% -0.04 ± 0.14 / 50% -0.06 ± 0.20
公開LB最良 − CV最良(混合を直した後): 公開5% +0.92 ± 0.27 / 20% -0.24 ± 0.28 / 50% -0.84 ± 0.33
  (Day170 の作り間違いのまま: +0.50 ± 0.42 / -0.69 ± 0.45 / -1.30 ± 0.50)

=== 表4 候補数 K(公開20%、各世界300回の無作為部分集合、×1e-4、平均・世界間SE) ===
    公開LB最良     CV最良        補正CV最良     2枠(CV+公開)   2枠(補正CV+公開)  一様ランダム
K
3   0.92±0.07   1.32±0.20    0.70±0.16    0.55±0.06     0.37±0.05      2.61±0.13
5   1.27±0.10   1.58±0.23    0.93±0.20    0.80±0.11     0.52±0.07      3.32±0.18
8   1.67±0.12   1.93±0.23    1.04±0.23    1.16±0.15     0.67±0.13      4.00±0.23
13  2.15±0.20   2.34±0.29    0.92±0.29    1.60±0.24     0.89±0.24      4.75±0.30
一様ランダムに対する比: 公開LB最良 0.35 / 0.38 / 0.42 / 0.45、補正CV最良 0.27 / 0.28 / 0.26 / 0.19

読み方: 表1 の A_tru・gap_mse・A_te が小数2桁まで一致している。1つ目と2つ目の一致は恒等式(クリップの影響は無視できる大きさ)、2つ目と3つ目の一致は「ラベル無しの test 行で測った平均の得が、ラベル付きの truth 行の得と同じ」ことを示す。表2 は、Day170 の偏り(C_T5)が「平均の得」(T1_T5、SE 0.02〜0.10 と非常に精密)でほぼ打ち消されることを示す。

タスクD(決定)

(1) 決定13(新規): CV は「提出の形」に直してから比べる。

#決定採った案採らなかった案と理由前提が変わったら
13CV の点数の直し方候補ごとに test 行で5つの fold モデルの予測の分散(log1p 空間)を測り、補正CV = sqrt(CV² − A_test) で比べる。報告にも補正後の値を使う(a) 生の CV(真の最良に 1/16 世界しか当たらない、損 2.34)。(b) 提出を1モデルにして CV と形をそろえる(提出が平均の得 4〜6×10⁻⁴ を捨てる)。(c) 入れ子 CV で5モデル平均の OOF を作る(fit 数が5倍、今日は比べていない)。(d) truth 相当のラベル付きホールドアウトを切る(train が減る)指標が二乗誤差系でない(AUC・MAE など)→ 恒等式が成り立たないので使えない。提出が fold 平均でない(全データ再学習1本)→ 補正は不要、ただし全データ1本の得は別に測る。test の分布が train と違う(敵対的 AUC 0.55 超)→ A_test と A_truth がずれるはずなので測り直す

(2) 決定12(改訂): 最終2本 = 補正CV最良 + 公開LB最良。 損は 0.88(公開20%)で、改訂前(1.58)より −0.69±0.23 小さい。ただし 2本目(公開LB最良)を足した得は −0.04±0.14 で測れないほど小さい。2本目は「補正 CV の前提(決定13 の右欄)が崩れたときの保険」として残す。同じ候補になったら公開LBの2位を出す(Day170 と同じ)。

(3) 決定5: 撤回。 決定12 が公開LBを2本目の選択に使う以上、「壊れ確認だけ」とは両立しない。撤回の理由に、Day170 の比較(公開20%で公開LB最良 − CV最良 −0.69±0.45)は作り間違いの候補を含んでおり、直すと −0.24±0.28(差は測れない)だったことも書く。公開LBが CV(生)に勝つのは公開50%のとき(−0.84±0.33)だけで、公開5%では負ける(+0.92±0.27)。

(4) 決定9(追記): sha256 の再現テストは「同じ間違いを毎回同じに作る」ことしか保証しない。 候補の名前と作り方を対応させる表明(例: assert np.allclose(mix, 0.9*base + 0.1*knn))を、混合を作った直後に置く。

(5) 利用者の作業(S5E5 実データ)。 Kaggle のログインとルール同意が要るので、ここでは手順だけ書く(私は実行していない)。

手順何を確かめるか
1. S5E5 のルールページで、最終提出の本数と「良い方が採られるか」を読む決定12 の前提(記憶による)
2. データページで test 行数と公開の割合を読む決定12・13 の公開割合(今日は 5/20/50% の仮定)
3. train と test で敵対的 AUC を測る決定1・13 の「前提が変わったら」(0.55 超なら見直し)
4. 候補を作るとき、fold モデルの test 予測を平均前に保存する決定13 の A_test を測るため
5. 提出前に 補正CV の順位と公開LB の順位を並べて記録する実データでの補正の当たり外れ(今日の数字は縮小シミュレータ)

📊 結果の可視化

CV−真 の default との差(×10⁻⁴、16ワールド、世界ペア平均)赤=生の CV(Day170 の偏り)/緑=補正CV。右=CV が実際より悪く見えるleaf63+5.73 → +0.33msl5+1.34 → -0.49l2_1+0.41 → +0.70noKeytel-0.52 → -2.39raw7-0.47 → -1.86noDurHR-0.98 → -0.74lr05_it600-1.69 → -0.05default+knn.1-1.80 → +0.21msl50-2.09 → -0.47lr05+knn.1-3.23 → +0.11it150-3.84 → +0.20leaf15-4.51 → -0.49
方針別の損(×10⁻⁴、短いほど良い。実測・16ワールド・13本)上=公開5% / 中(濃)=公開20% / 下=公開50%補正CV最良0.92 (5%)0.92 (20%)0.92 (50%)2枠: 補正CV+公開0.92 (5%)0.88 (20%)0.86 (50%)2枠: CV+公開(Day170)1.82 (5%)1.58 (20%)1.44 (50%)公開LB最良3.26 (5%)2.10 (20%)1.50 (50%)CV最良(生)2.34 (5%)2.34 (20%)2.34 (50%)事前固定(決定8)3.00 (5%)3.00 (20%)3.00 (50%)
候補数 K と損(公開20%、×10⁻⁴)0123K=3K=5K=8K=13公開LB最良 2.15CV最良(生) 2.34補正CV最良 0.922枠: 補正CV+公開 0.89K は13までしか測れていない(候補が13本)。一様ランダムは 2.61→4.75

上: CV の偏りは fold 間分散(平均の得)を引くとほぼ消える(noKeytel・raw7 を除く)。中: 補正CV最良は公開割合によらず損 0.92 で、生の CV最良(2.34)・公開LB最良(公開20%で 2.10)より小さい。下: 候補数が増えると公開LB最良の損は増えるが、補正CV最良はほぼ平ら。

🪜 Step-by-Step 解説

Step 1: 設計書を1枚にしたら、矛盾が1つ見つかった

決定5(公開LBは壊れ確認だけ)と決定12(2本目は公開LB最良)。日ごとのファイルではどちらも「その日の正解」で、並べるまで気づかない。印を付けると、決定12 は△の上に乗っていた(CV の偏りの原因が仮説)。

Step 2: 弱点を測るために、平均する前の予測を保存する

Day170 は5モデル平均しか保存していなかったので、1モデルの真の点数(T1)が出せなかった。同じ乱数で作り直し、fold 別に保存した(1ワールド約120秒、4並行で計508秒)。

Step 3: 作り直したら、Day170 と1本だけ合わなかった

12本は 3.8×10⁻⁷(float32 の丸め)で一致、OOF は完全一致。混合 lr05_it600+knn.1 だけ 0.087 ずれた。Day170 のコードでは、1本目の混合を vstack で足した後に oof[-1] を knn のつもりで使っていた。「最後の行」は、行を足すたびに別のものを指す。 名前ではなく行番号で参照したことが原因。Day170 の sha256 の再現テスト(決定9)では捕まらない(毎回同じ間違いを再現する)。

Step 4: CV の偏りを2つに分ける(表2)

C − T5 = (C − T1) + (T1 − T5)。前半は「1モデル同士の CV と真のずれ」、後半は「平均の得」。leaf63 の +5.73 は、前半 +0.25±0.91、後半 +5.48±0.09。偏りのほぼ全部が平均の得だった。 複雑な設定(葉が多い・葉の最小行数が小さい)ほど fold モデルどうしの予測がばらつき、平均で大きく得をする。

Step 5: 平均の得をラベル無しで測る(表1)

恒等式により、平均の得(MSE 単位)は fold 間分散そのもの。test 行(ラベル無し)で測った A_te と truth 行の A_tru は全候補で小数2桁まで一致(leaf63 5.68 対 5.71)。train と test が同じ分布なら、実戦で必ず測れる量になる。

Step 6: 補正した CV で方針を比べ直す(表3)

補正CV最良の損 0.92。生の CV最良 2.34 より −1.42±0.33、公開20%の公開LB最良 2.10 より −1.18±0.33。真の最良に当たった世界 1/16 → 8/16。外した8世界の多くは msl5(7世界で選ばれ、真の最良だったのは1世界)。

Step 7: 残ったずれ(表2 の右)

補正後も候補間 SD は 2.25 残る。大半は世界ごとの雑音(SE 0.5〜0.9 の幅)だが、特徴量を抜いた候補(noKeytel −2.39±1.11、raw7 −1.86±1.07)は補正後も CV が良く見える側にずれた(約2SE)。原因は未調査(元データ1.5万行を足して学習することとの関係を疑っているが、測っていない)。

Step 8: 候補数 K(表4)

公開LB最良の損は K=3 で 0.92、13 で 2.15。一様ランダムも 2.61→4.75 と増えるので、比で見ると 0.35→0.45 とゆっくり悪化。補正CV最良は K によらずほぼ平ら(0.70〜1.04)。K は 13 までしか測れない(候補が13本しか無い)ので、数百本の世界は外挿になる。

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

平均すると得をする量は、ばらつきそのもの。 5人に同じ的を狙って矢を射させる。1人ずつの外れ方の平均(2乗)は、「5人の矢の真ん中」の外れ方(2乗)に、「5人の矢がどれだけ散らばっているか(分散)」を足したものに必ず等しい。真ん中で提出するなら、散らばりの分だけ得をする。そして散らばりは、的(正解)を見なくても矢どうしを見るだけで測れる。今日の補正は、この「矢どうしの散らばり」を test の行で測っただけ。

なぜ RMSLE でも使えるか。 RMSLE は「log(1+予測) と log(1+正解) の差の2乗平均の平方根」。log1p で学習したモデルの予測は最初から log 空間にあるので、その空間での2乗誤差に恒等式がそのまま使える。ただし下限1のクリップ(決定4)が入るとわずかに崩れる。今日は gap_mse と A_tru の差が最大 0.01×10⁻⁴ で無視できた。

「√(CV² − A)」の意味。 CV²(平均2乗誤差)から散らばりを引くと「5人の真ん中で射たときの平均2乗誤差」の推定になる。最後に平方根で RMSLE に戻す。

🏆 Kaggleでの実践的な使い方

  • fold モデルで test を平均して提出するなら、CV の比較は平均の得を引いてから。 葉の数・学習率・木の本数など「ばらつきを変える設定」の比較で、生の CV は単純な設定をひいきする(今日 leaf15 が −4.51 良く見えた)。必要なのは、各 fold の test 予測を平均する前に保存することだけ
  • AUC・MAE のコンペでは使えない。 恒等式は二乗誤差のもの。分類の logloss・AUC では別の議論が要る(今日は扱っていない)
  • 「公開LBを信じるか CV を信じるか」の前に、CV を提出の形に直す。 直した CV は、公開5〜50%のどの割合でも公開LB最良より損が小さかった(−2.33/−1.18/−0.58)
  • 再現できることと、正しいことは別。 同じ乱数で作り直して照合したから、作り間違いが見つかった。照合を「乱数の確認」だけにせず、候補の名前と式を表明(assert)で結ぶ
  • 設計書は定期的に1枚に並べる。 日ごとに決めると、前の決定を黙って上書きする(今日の決定5)

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
CV が良い設定は提出でも良いCV と提出が同じものを測っていると思うOOF は1モデル、提出は5モデル平均。差(平均の得)は候補ごとに 2.7〜5.7×10⁻⁴(MSE 単位)と違い、leaf63 を CV は15/16世界で外した
平均の得を測るには正解ラベルが要る「得」は正解との比較だと思う二乗誤差なら、予測どうしの分散そのもの。test 行で測れ、truth 行の値と小数2桁まで一致
補正すれば CV のずれは消える偏りの原因を1つだと思う平均の得で説明できる部分は消えたが、特徴量を抜いた候補には約2SE のずれが残った
sha256 が一致したので結果は正しい再現性を正しさと混同する毎回同じ間違いを作れば sha256 も一致する。Day170 の混合1本は作り方が名前と違っていた
arr[-1] で最後に足した行を指す1回目は正しく動く行を足すたびに「最後」が変わる。名前から行番号を引く(names.index("knn"))
公開LB最良は CV最良より当たる(Day170)1回の測定の結論を固定する作り間違いを直すと公開20%で −0.24±0.28 で差は測れない。公開5%では CV が勝つ(+0.92±0.27)

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

  • シミュレータ1つ、モデル族1つ: HistGB(乱数なし)と knn だけ。bagging・列抽出のある LightGBM、NN では fold 間分散に model seed の揺れが混ざる(決定9)。今日の補正がそのまま効くかは未測定
  • 残ったずれの原因: noKeytel・raw7 の約2SE のずれは原因を調べていない。16ワールドで約2SE なので、偶然の可能性も残る
  • 候補数 K: 13本までしか作っていない。数百本(実際のコンペ)は外挿。部分集合は無作為で、人が「良さそうなもの」を選んで足す偏りは入っていない
  • 補正の SE: A_test 自体の揺れ(2万行での推定誤差)は測っていない。test 行が少ない(数千行)コンペでは揺れが大きくなるはず
  • Day170 の他の結論の見直し: 混合候補を直した後の数字は表3 の6方針だけ出し直した。Day170 のハイブリッド方針・狭い10本・hitT・regP は出し直していない
  • 原典: Krogh & Vedelsby 1995 の恒等式は記憶による(式は今日の表1 で数値的に確かめた)。Kaggle のルール(最終2本・良い方採用)・S5E5 の公開割合は今日も確認していない(利用者の作業、タスクD(5))
  • 入れ子 CV との比較: 「5モデル平均の OOF を直接作る」方法(fit 数5倍)と補正CVの比較はしていない
  • 持ち越し(今月中には扱わない): Day165 の SE 個数差と分割の新しさの分離、Day162 の選択バイアス仮説、真の効果ちょうど0の候補での偽採用率、Nadeau & Bengio 原典の確認

🚀 次のステップ

  • 発展: 列抽出(max_features=0.5)を入れた HistGB で、fold 間分散に model seed の揺れが混ざっても補正CV が効くかを測る(決定9 と決定13 の接点)
  • 発展: noKeytel・raw7 の残ったずれが、元データを学習 fold に足すこと(決定3)と関係するかを、元データ無しで同じ分解をして確かめる
  • 次回予告: Day172 — Playground Series 実戦投入⑦(月の残りは 10/25 まで。今日の決定13 を前提に、LightGBM の bagging ありで補正CV を測り直す。その後、月替わりテーマ3「画像(CV)系コンペへの応用」は 10/25 から)

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

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