Day 168 — 2026-09-30 | Playground Series 実戦投入(9/30 のバックフィル分・独立1本)— 「R2 の SE は足りているか」。15個の fold 差を独立とみなす今の SE は、24ワールドで真の効果からのずれに対して 0.6 倍しか幅が無く(95%区間の被覆 0.82)、Nadeau & Bengio の補正(2.18倍)は逆に広すぎた(被覆 0.99・採用できた真の改善 22件中 2件)。実測が要求した拡大率は 1.2〜2.0 倍で、補正式の 2.18 の手前にある

2026-09-30(バックフィル) 金 / Phase 6(月替わりテーマ2: Playground Series 実戦投入) 理論→分析 HistGB · 24ワールド · 1,440 fit

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

R2 とは(Day164 で決めた採否ルール)。 分割seed を3通り変えて 5分割 CV を3回やり、候補と基準の差を fold ごとに出す(3 × 5 = 15個の fold 差)。その平均を「CV の差」、15個の SD を √15 で割ったものを SE(標準誤差)とし、差 < −2SE なら採用する。

この SE の暗黙の前提は「15個の差が互いに独立」。 実際は独立でない。5分割 CV では、ある fold の学習データ(全体の 80%)が他の fold の学習データと 75% 重なる。3回繰り返すと、同じ行が何度も検証に使われる。重なっていれば15個は「15個の別々の証拠」でなく、少数の証拠の言い換えになり、SE は小さく出すぎる。

Nadeau & Bengio(2003)の補正。 繰り返しの学習/検証分割で得た J 個の差の平均の分散は、独立なら σ²/J だが、学習データの重なりを考えると σ²(1/J + n_検証/n_学習) になる、という補正。今回は J = 15、n_検証/n_学習 = 6,000/24,000 = 0.25 なので、SE は √(1/15 + 0.25) / √(1/15) = 2.18 倍になる。(原典は今日開いていない。式は記憶による。係数の挙動を実測で確かめるのが今日の目的。)

正解の取り方。 各ワールドで「CV の差」が真の効果からどれだけずれたかを見る。真の効果 = 同じワールドの test 6万行で、15 個の fold モデル1つ1つについて候補 − 基準の RMSLE を取り、平均したもの(CV の差と同じ「1モデルの学習」を測っているので、Day166 の「5モデル平均で改善が縮む」問題を持ち込まない)。ずれ err = CV の差 − 真の効果。SE が正しければ、err/SE の SD は 1、95% 区間(±1.96SE)が真の効果を含む割合は約 0.95 になる。

データの列(縮小シミュレータ。Day163〜167 と同じ。乱数のシードだけワールドごとに変える):

列型意味基準7列候補で足す列
Sex0/1性別○
Age整数年齢(20〜79)○
Height / Weight整数身長 cm・体重 kg○
Duration整数運動時間 分(1〜30)○
Heart_Rate整数心拍数(67〜128)○
Body_Temp小数体温○
Dur_x_HR数値Duration × Heart_Rate候補A(Day164 で効いた。真の効果は負=良くなる)
BMI数値Weight / (Height/100)²候補B(Weight と Height から木が作れる列)
noise数値標準正規乱数候補C(陰性対照。Day164 では真に悪化)
Calories整数目的変数(消費カロリー)。学習では log1p をかけるyy

評価指標 RMSLE。 log1p(1を足して対数)してからの RMSE。小さいほど良い。「差」は 候補 − 基準 で、負なら候補が良い。以下、差は ×10⁻⁴ で書く(CV の点数 0.16 の 0.06% が 1 に当たる)。数値は仮定の上の実測(実物の S5E5 ではない)。

今日の問い:

  1. 15個の fold 差が独立だとみなす SE(以下 naive)は、真の効果からのずれ err に対して足りているか
  2. Nadeau & Bengio 補正(以下 NB、2.18倍)は足りるか、広すぎるか
  3. その SE で「差 < −2SE なら採用」としたとき、採用の成績はどう変わるか

用語整理:

用語意味
fold 差ある fold の検証 6,000 行で測った RMSLE の「候補 − 基準」。同じ分割・同じ fold 同士で対にする
SE naive15個の fold 差の SD ÷ √15
SE NB15個の fold 差の SD × √(1/15 + 0.25)
被覆率「CV の差 ± 1.96 SE」が真の効果を含んだワールドの割合。SE が正しければ 0.95
z の SDerr ÷ SE の標準偏差。1 より大きいと SE が小さすぎ、1 より小さいと大きすぎ

❓ 問題

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

  1. naive SE の被覆率は 0.95 より低いか、高いか。z の SD はいくつか
  2. NB 補正(2.18倍)を掛けた SE の z の SD は 1 に近づくか。広すぎる側に外れるか、足りない側に外れるか
  3. 「差 < −2SE で採用」で、真に効く候補 Dur_x_HR を何割採用できるか。NB で何割に減るか
  4. 陰性対照 noise と BMI の偽採用は何件出るか(予想の根拠も書く)

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

  1. Day167 の make_original・synth_from を再利用し、one_world(w) を書く(シード 3000+w。基準・候補3つ・分割seed 1/2/3・5分割・log1p・元データは学習foldだけ・下限1クリップ・HistGB(early_stopping=False, random_state=0))。戻り値は fold 差の平均 d_cv・SD sd_fold・真の効果 d_true
  2. 24ワールドを6プロセスに分けて回し、CSV にまとめる(OMP_NUM_THREADS=1)
  3. report で表A(真の効果・CV の差・err の SD・SE)、表B(z の SD・被覆率)、表C(採否の成績)、表D(実測が要求した拡大率)を出す

タスクC(決定・戦略)

表A〜D から、S5E5 の CV 設計書の決定6(採否は R2 で −2SE)を見直すか決め、「決定10」を足す。「採った案/採らなかった案と理由/前提が変わったら」を付ける。

💡 ヒント

ヒント1(方向性)

  • SE の正しさは「SE の値を見る」のでなく、「SE で割ったずれ(z)の広がりが1か」で測る。ずれには正解(真の効果)が要る
  • 真の効果を持つ候補と、持たない(むしろ悪い)候補の両方を入れると、SE がどちらでも足りるかが見える

ヒント2(アプローチ)

  • fold 差を15個作るとき、基準と候補は同じ分割・同じ fold で対にする
  • 真の効果は「fold モデル1つ1つの test 差」の平均。5モデルを平均した予測の差にしない(縮むため)
  • 並列プロセスを使うと1プロセスあたりの時間が約3.5倍になる。1ワールドの単独実測を先に取る

ヒント3(コード骨格)

fd = np.concatenate([res[s][c][0] - res[s]["base"][0] for s in SPLITS])   # 15 個の fold 差
td = np.concatenate([res[s][c][1] - res[s]["base"][1] for s in SPLITS])   # 15 個の 1モデル test 差
d_cv, d_true = fd.mean(), td.mean()
se_naive = fd.std(ddof=1) / np.sqrt(15)
se_nb    = fd.std(ddof=1) * np.sqrt(1/15 + 0.25)
z = (d_cv - d_true) / se       # この SD が 1 なら SE は正しい

✅ 模範解答

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

#予想の模範実測
1重なりで SE は小さく出る。被覆率は 0.95 を下回り、z の SD は 1.3〜2当たり。被覆率 0.82(72件)、z の SD 1.67。候補別では Dur_x_HR が最も悪く被覆 0.71・z の SD 2.13
2補正は 1 に近づくが、σ²/J の 4.75 倍(2.18²)は重なり 75% の最悪ケース向けで、広すぎる側に外れる当たり。z の SD 0.77、被覆 0.99。足りない側ではなく広すぎる側。候補別の z の SD は Dur_x_HR 0.98(ほぼ1)、noise 0.73、BMI 0.56
3naive でも半分程度、NB では2割以下Dur_x_HR 24件のうち naive で 11件、NB で 2件採用。真に効く22件に対し naive 10件・NB 2件(見逃し 12件・20件)
4noise と BMI は真に悪いので −2SE には届かず、偽採用は00件・0件。ただしこの結果は SE の良し悪しの証拠にならない(下の注意)

注意(予想4): noise(真の効果 +6.38〜+10.33)と BMI(+0.04〜+4.45)は、どのワールドでも真に悪い。差が −2SE まで下がるにはずれが SE の数倍必要で、naive でも起きなかった。偽採用の頻度を測るなら「真の効果がちょうど0の候補」が要る。今日の3候補にはそれが無く、偽採用の測定はできていない。表Cの「偽採用 1件」は Dur_x_HR が効かないワールド(真の効果 +1.19 と +0.08)で起きたもの。

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

day168.py(world・report の2モード):

"""Day168: R2(3回繰返し5分割=15 fold差)のSEは足りているか。Nadeau & Bengio(2003)補正を、複数ワールドで真の効果と照合する。
使い方:
  OMP_NUM_THREADS=1 python day168.py world <w0> <w1> <out.csv>   # ワールド w0..w1-1 を測って CSV に書く
  python day168.py report out1.csv out2.csv ...                   # 表を出す
"""
import sys, time
import numpy as np, pandas as pd
from sklearn.ensemble import HistGradientBoostingRegressor
from sklearn.model_selection import KFold

N_ORIG, N_TRAIN, N_TEST = 15000, 30000, 60000
BASE = ["Sex", "Age", "Height", "Weight", "Duration", "Heart_Rate", "Body_Temp"]
CANDS = {"Dur_x_HR": BASE + ["Dur_x_HR"], "BMI": BASE + ["BMI"], "noise": BASE + ["noise"]}
PARAMS = dict(learning_rate=0.1, max_iter=200, max_leaf_nodes=31, min_samples_leaf=20, l2_regularization=1e-4)
SPLITS = (1, 2, 3)                      # R2: 分割seed 3通り × 5分割 = 15 fold
K = 5


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〜167 と同じ
    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〜167 と同じ
    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(d, rng):
    d = d.copy(); d["Dur_x_HR"] = d.Duration * d.Heart_Rate
    d["BMI"] = d.Weight / (d.Height / 100) ** 2; d["noise"] = rng.normal(size=len(d)); return d


rmsle = lambda y, p: float(np.sqrt(((np.log1p(p) - np.log1p(y)) ** 2).mean()))


def one_world(w):
    rng = np.random.default_rng(3000 + w)
    orig = make_original(N_ORIG, rng)
    pool = synth_from(orig, N_TRAIN + N_TEST, rng)
    orig, pool = add(orig, rng), add(pool, rng)
    train, test = pool.iloc[:N_TRAIN].reset_index(drop=True), pool.iloc[N_TRAIN:].reset_index(drop=True)
    yt = test.Calories.values

    def run(cols, split_seed):  # CV設計書どおり。戻り値: 各 fold の検証 RMSLE と、各 fold モデルの test RMSLE
        va_s, te_s = [], []
        for tr, va in KFold(K, shuffle=True, random_state=split_seed).split(train):
            d = pd.concat([train.iloc[tr], orig], ignore_index=True)
            m = HistGradientBoostingRegressor(early_stopping=False, random_state=0, **PARAMS)
            m.fit(d[cols], np.log1p(d.Calories))
            pv = np.clip(np.expm1(m.predict(train.iloc[va][cols])), 1, None)
            va_s.append(rmsle(train.Calories.values[va], pv))
            te_s.append(rmsle(yt, np.clip(np.expm1(m.predict(test[cols])), 1, None)))
        return np.array(va_s), np.array(te_s)

    res = {s: {"base": run(BASE, s), **{c: run(cols, s) for c, cols in CANDS.items()}} for s in SPLITS}
    out = []
    for c in CANDS:
        fd = np.concatenate([res[s][c][0] - res[s]["base"][0] for s in SPLITS])   # 15 個の fold 差
        td = np.concatenate([res[s][c][1] - res[s]["base"][1] for s in SPLITS])   # 15 個の「1モデルの test 差」
        out.append(dict(world=w, cand=c, d_cv=fd.mean(), sd_fold=fd.std(ddof=1), d_true=td.mean()))
    return out


if __name__ == "__main__":
    if sys.argv[1] == "world":
        w0, w1, path = int(sys.argv[2]), int(sys.argv[3]), sys.argv[4]
        rows, T0 = [], time.time()
        for w in range(w0, w1):
            rows += one_world(w); pd.DataFrame(rows).to_csv(path, index=False)
            print(f"world {w} done {time.time()-T0:.0f}s", flush=True)

    if sys.argv[1] == "report":
        df = pd.concat([pd.read_csv(p) for p in sys.argv[2:]], ignore_index=True)
        J, ratio = 15, N_TRAIN / K / (N_TRAIN - N_TRAIN / K)        # 検証 6000 / 学習 24000 = 0.25
        df["se_naive"] = df.sd_fold / np.sqrt(J)                     # R2 のいまの SE(15 個の fold 差が独立だとみなす)
        df["se_nb"] = df.sd_fold * np.sqrt(1 / J + ratio)            # Nadeau & Bengio(2003) 補正
        df["err"] = df.d_cv - df.d_true                              # CV の差が真の効果からどれだけずれたか
        e = lambda x: f"{x*1e4:8.2f}"
        print("worlds:", df.world.nunique(), " 行:", len(df), " 補正係数 sqrt(1+J*ratio) =", f"{np.sqrt(1+J*ratio):.2f}")
        print("\n=== 表A 候補ごと(×1e-4): 真の効果・CVの差・SE・ずれの実測SD ===")
        for c, g in df.groupby("cand"):
            print(f"{c:9s} true平均{e(g.d_true.mean())} cv平均{e(g.d_cv.mean())} | ずれ err の SD(実測){e(g.err.std(ddof=1))} 平均{e(g.err.mean())}"
                  f" | SE naive 中央{e(g.se_naive.median())} NB 中央{e(g.se_nb.median())}")
        print("\n=== 表B SE は足りているか: z = err/SE の SD(1 が理想)と 95%被覆率 ===")
        for c, g in list(df.groupby("cand")) + [("全体", df)]:
            zn, zb = g.err / g.se_naive, g.err / g.se_nb
            print(f"{c:9s} n={len(g):2d} SD(z): naive {zn.std(ddof=1):5.2f}  NB {zb.std(ddof=1):5.2f} | 被覆: naive {(zn.abs()<1.96).mean():.2f}  NB {(zb.abs()<1.96).mean():.2f}")
        print("\n=== 表C 採否ルール「差 < -2SE で採用」の成績(真の効果<0 を採用すべきとする)===")
        for name in ("se_naive", "se_nb"):
            adopt = df.d_cv < -2 * df[name]; good = df.d_true < 0
            print(f"{name:9s} 採用{int(adopt.sum()):2d}件 正しい採用{int((adopt&good).sum()):2d} 偽採用{int((adopt&~good).sum()):2d} 見逃し{int((~adopt&good).sum()):2d} /  真に効く{int(good.sum())}件, 効かない{int((~good).sum())}件")
            for c, g in df.groupby("cand"):
                a = g.d_cv < -2 * g[name]
                print(f"    {c:9s} 採用 {int(a.sum()):2d}/{len(g)}  (真に効く {int((g.d_true<0).sum())})")
        print("\n=== 表D 必要な拡大率(実測)と符号 ===")
        for c, g in df.groupby("cand"):
            print(f"{c:9s} 実測のずれSD ÷ naive SE 中央 = {g.err.std(ddof=1)/g.se_naive.median():4.2f}倍 | ÷ NB SE 中央 = {g.err.std(ddof=1)/g.se_nb.median():4.2f}倍"
                  f" | CV差の符号が真と逆 {int((np.sign(g.d_cv)!=np.sign(g.d_true)).sum())}/{len(g)} | 真の効果の範囲 {e(g.d_true.min())} ~ {e(g.d_true.max())}")
        g = df[df.cand == "Dur_x_HR"]
        print("Dur_x_HR: 真の効果<0 の", int((g.d_true<0).sum()), "ワールドで、CV差/SE_naive の中央", f"{(g.d_cv/g.se_naive).median():.2f}", " CV差/SE_NB の中央", f"{(g.d_cv/g.se_nb).median():.2f}")
        print("Dur_x_HR: sd_fold 中央", e(g.sd_fold.median()), " 範囲", e(g.sd_fold.min()), "~", e(g.sd_fold.max()))

実行結果(実測)

# ============ 実行環境: macOS (M1, メモリ8GB)・Python 3.13.12・scikit-learn 1.9.1・numpy 2.5.3・pandas 3.0.6(新規 uv venv)
# world モード: 24ワールドを 6プロセス × 4ワールドに分け、OMP_NUM_THREADS=1 で並列実行(壁時計 857秒。単独 1ワールドは 119秒)
# report モード(同一の CSV 6本から。出力は何度回しても同じ)
worlds: 24  行: 72  補正係数 sqrt(1+J*ratio) = 2.18

=== 表A 候補ごと(×1e-4): 真の効果・CVの差・SE・ずれの実測SD ===
BMI       true平均    2.13 cv平均    1.75 | ずれ err の SD(実測)    1.60 平均   -0.39 | SE naive 中央    1.29 NB 中央    2.82
Dur_x_HR  true平均   -2.07 cv平均   -2.59 | ずれ err の SD(実測)    2.74 平均   -0.51 | SE naive 中央    1.36 NB 中央    2.95
noise     true平均    8.51 cv平均    8.41 | ずれ err の SD(実測)    1.68 平均   -0.11 | SE naive 中央    1.14 NB 中央    2.49

=== 表B SE は足りているか: z = err/SE の SD(1 が理想)と 95%被覆率 ===
BMI       n=24 SD(z): naive  1.22  NB  0.56 | 被覆: naive 0.96  NB 1.00
Dur_x_HR  n=24 SD(z): naive  2.13  NB  0.98 | 被覆: naive 0.71  NB 0.96
noise     n=24 SD(z): naive  1.58  NB  0.73 | 被覆: naive 0.79  NB 1.00
全体        n=72 SD(z): naive  1.67  NB  0.77 | 被覆: naive 0.82  NB 0.99

=== 表C 採否ルール「差 < -2SE で採用」の成績(真の効果<0 を採用すべきとする)===
se_naive  採用11件 正しい採用10 偽採用 1 見逃し12 /  真に効く22件, 効かない50件
    BMI       採用  0/24  (真に効く 0)
    Dur_x_HR  採用 11/24  (真に効く 22)
    noise     採用  0/24  (真に効く 0)
se_nb     採用 2件 正しい採用 2 偽採用 0 見逃し20 /  真に効く22件, 効かない50件
    BMI       採用  0/24  (真に効く 0)
    Dur_x_HR  採用  2/24  (真に効く 22)
    noise     採用  0/24  (真に効く 0)

=== 表D 必要な拡大率(実測)と符号 ===
BMI       実測のずれSD ÷ naive SE 中央 = 1.24倍 | ÷ NB SE 中央 = 0.57倍 | CV差の符号が真と逆 5/24 | 真の効果の範囲     0.04 ~     4.45
Dur_x_HR  実測のずれSD ÷ naive SE 中央 = 2.02倍 | ÷ NB SE 中央 = 0.93倍 | CV差の符号が真と逆 4/24 | 真の効果の範囲    -5.09 ~     1.19
noise     実測のずれSD ÷ naive SE 中央 = 1.47倍 | ÷ NB SE 中央 = 0.67倍 | CV差の符号が真と逆 0/24 | 真の効果の範囲     6.38 ~    10.33
Dur_x_HR: 真の効果<0 の 22 ワールドで、CV差/SE_naive の中央 -1.91  CV差/SE_NB の中央 -0.88
Dur_x_HR: sd_fold 中央     5.25  範囲     3.32 ~     8.66

読み方: 表A・D の単位は ×10⁻⁴。「ずれ err の SD(実測)」が、24ワールドを通した CV の差の本当の揺れ。SE の中央値がそれより小さければ、SE は足りていない。表Dの「÷ naive SE 中央」が、naive に掛ければ実測のずれと一致する拡大率。

タスクC(決定)

(1) 見立て: R2 の SE は足りない(実測 1.2〜2.0 倍の拡大が要る)。ただし NB の 2.18 倍は広すぎる。採否のルールは変えず、幅の報告にだけ NB を使う。

案判定理由(今日の数字)
採否は今のまま naive の −2SE採る(変えない)真に効く22件のうち10件を採用、偽採用は1件(Dur_x_HR が効かない2ワールドの一方)。見逃しは12件で、検出力が足りないのは R2 の15 fold の限界。名目の「2SE」は実際の被覆(0.82)から見て約 1.3SE 相当だが、採用の正しさは 10/11 だった
採否を NB の −2SE にする採らない採用が 2件に減り、見逃し 20/22。偽採用を減らす効果は 1件分(今日のデータ)だが、効く改善の 91% を捨てる
「改善幅の報告」に NB の SE を使う(差 ± 1.96×SE_NB を区間として書く)採る被覆 0.99(72件)。広すぎるが、「約±0.0006 は seed と分割で動く」と書く用途には、足りない SE より安全。naive の区間は 82% しか真を含まなかった
拡大率を実測から決めて 1.5 倍を掛ける採らない実測の拡大率は候補で 1.24(BMI)・1.47(noise)・2.02(Dur_x_HR)とばらつく。24ワールド・3候補から1つの定数を決めるのは過学習で、別の効果量の候補で当たる根拠が無い
分割seed の繰り返し数を増やして SE を縮める今回は測っていない分割seed が3通りの R2 で、各 fold の学習データの重なりは変わらない。J を 15 から増やしても、σ²(1/J + 0.25) の 0.25 の項は残る(式の読み。未実測)

(2) CV 設計書に書き足す「決定10: R2 の SE の読み方」(S5E5 用)

#決定採った案採らなかった案と理由前提が変わったら
10R2 の SE は採否用と報告用で分ける採否は決定6のまま(naive の −2SE)。報告用の幅は NB の SE(naive の 2.18 倍)で書く。「差が −2SE」を「効果がある確率 97.5%」と読まない(実被覆は 0.82)NB を採否にも使う(採用 2/22 に落ちる)。定数 1.5 倍(候補でばらつく)。偽採用の測定が無いまま採否を締める真の効果がちょうど0の候補(例: 既存列の複製や、同じ特徴量を別名で足す)で偽採用率を測ったら、−2SE の閾値を測り直す。ワールド数が 24 から増え、拡大率の候補別のばらつき(1.24〜2.02)が説明できたら定数を採る

🪜 Step-by-Step 解説

Step 1: 正解(真の効果)を先に決める

SE の良し悪しは SE だけを見ても分からない。そこで test 6万行を使い、同じワールドの fold モデル15個それぞれについて「候補 − 基準」を測って平均した。CV の差と同じ「1モデルを 24,000 + 15,000 行で学習した効果」を測っているので、Day166 の縮み(5モデル平均で改善が約半分になる)の影響を受けない。

Step 2: SE を2種類出す

同じ15個の fold 差から、naive(SD/√15)と NB(SD × √(1/15 + 0.25))を出した。違いは掛ける係数だけで、Dur_x_HR の SE の中央値は 1.36 と 2.95(×10⁻⁴)。

Step 3: ずれを SE で割る(表B)

err/SE の SD が 1 なら SE は正しい。naive は全体で 1.67(SE が約 0.6 倍しかない)、NB は 0.77。候補別では Dur_x_HR で naive 2.13・NB 0.98 と、NB がちょうど合う。noise・BMI では NB でも 0.73・0.56 と広すぎる。

Step 4: なぜ候補で変わるか

候補別の実測の拡大率は 1.24(BMI)・1.47(noise)・2.02(Dur_x_HR)。fold 差の SD は Dur_x_HR で中央値 5.25(範囲 3.32〜8.66)と大きい。ずれの大きさが fold 差のばらつきにだけ比例するなら拡大率は同じになるはずだが、そうならなかった。原因は分けていない。 考えられる候補は2つ: (a) Dur_x_HR は真の効果がワールドで −5.09〜+1.19 と大きく動く(効果が大きい候補ほど、学習データが変わると効き方が変わる)。(b) naive の SE が小さく出る程度が候補の性質で変わる。どちらも未検証。

Step 5: 採否の成績(表C)

Dur_x_HR は24ワールド中22で真に効く。naive の −2SE は 11件採用(正しい 10・偽 1)、NB の −2SE は 2件(正しい 2・偽 0)。naive の見逃し 12件は「CV の差/SE の中央値が −1.91」、つまり典型的な効き方でも −2SE にぎりぎり届かないという意味。R2 の15 fold で −2SE を超えられるのは、効果が平均より大きい約半分のワールドだけ。

Step 6: 決定の根拠

採否を NB に変えると、偽採用は 1件減るが正しい採用が 8件減る。S5E5 のように候補を1つずつ足す運用では、見逃し(効く列を捨てる)のコストと、偽採用(効かない列を入れる)のコストを比べる必要がある。今日のデータでは偽採用の効果の大きさが小さく(+0.08, +1.19)、見逃しの損失(真の効果 平均 −2.07)の方が大きい。この比較は1シミュレータ・1候補のみ。

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

SE は「同じことをやり直したらどれだけ揺れるか」の物差し。 15個の差の平均の揺れを、「15回の独立な実験」と見なせば SD/√15。しかし15個は、同じ3万行から取った重なる学習データで作った点なので、15回の実験ではなく、実際には数回分の情報しかない。体重計に15回乗っても、同じ朝の同じ体重を15回測っただけなら、「日による変動」は1回分しか分からない、という感覚。

補正の 2.18 倍はどこから来るか。 検証の割合が 1/5 のとき、学習データ同士の相関は約 0.8 とみなせる(重なり 75% に近い値)。相関がある平均の分散は、独立の分散より 1 + (J−1)ρ に近い倍率で大きくなり、その平方根が 2.18 前後になる。ρ をどうみなすかは仮定で、3回の繰り返しでは繰り返し間の相関も違うので、補正は近似になる。今日の結果(被覆 0.99・z の SD 0.77)は、その近似がやや保守的だったことを示す。

被覆率の読み方。 「±1.96SE が真の値を含む割合」が 0.95 になるのが正しい SE。0.82 なら、95%区間と呼んでいるものの実際の信頼度は約82%。72件での被覆率の誤差は約 ±0.05 なので、0.82 と 0.95 の差は偶然では説明しにくいが、0.99 と 0.95 の差は 72件では小さい。

🏆 Kaggleでの実践的な使い方

  • CV の差に付ける SE は、fold 間の重なりを考えて読む。 5分割 CV の fold 差を独立とみなす SE は、今日の実測では 0.5〜0.8 倍に小さく出た。LB の1件の改善や CV の 0.0001 台の差を「有意」と決める前に、SE を2倍ほどに見積もった区間で見る
  • scikit-learn の公式ギャラリーにも補正 SE を使った比較の例がある(記憶による。今日は未確認)。 使うなら、補正が保守側に出ることを知っておく。採否に使うと、効いた改善を捨てる
  • 効果量が小さい特徴量を足し続ける運用では、見逃しを減らす側に倒す。 Playground Series のように CV と LB が近い大会では、見逃した改善より、入れた偽の特徴量の害が小さいことがある(今日の偽採用 2例の真の効果は +0.08 と +1.19)。ただし、これは今日のシミュレータでの話で、実物では確認していない

⚠️ よくある誤解・ミス

誤解・ミスなぜ起こるか正しい理解
fold を15個に増やせば SE が √15 分の1になる独立な標本の平均の公式fold の学習データは重なる。今日の実測で naive は被覆 0.82、z の SD 1.67
補正式を使えば正しい SE になる論文の式だから今日の NB は広すぎた(被覆 0.99、z の SD 0.77)。式の仮定(相関の大きさ)は実データで確かめる
−2SE で採用 = 95%の確信正規分布の 2σ を覚えているR2 の naive の −2SE は、実際の被覆から見て 1.3SE 相当。確信度は読み替えが要る
偽採用が0件だったので SE は十分陰性対照が採用されなかった今日の noise・BMI は真に悪く、そもそも −2SE に近づかない。真の効果が0の候補が無いと偽採用は測れない
1つの拡大率で全候補を直せる補正係数は1つの数字実測は候補で 1.24・1.47・2.02 とばらついた。3候補 24ワールドで1つに決めると過学習になる

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

  • 偽採用率の測定(最重要の未達): 真の効果がちょうど0の候補を入れていない。noise は真に悪化(+8.5)、BMI も悪化(+2.1)で、どちらも −2SE の近くに来ない。決定10 の「採否は変えない」は、偽採用を測らずに下した判断で、Dur_x_HR が効かない2ワールド(偽採用1件)だけが根拠
  • 世界の実物との比較: 1シミュレータ・HistGB・24ワールド。Nadeau & Bengio(2003, Machine Learning 52:239–281)の原典、Bouckaert & Frank の反復 CV の議論は今日は読んでいない(式は記憶)。補正式の J は独立な分割の数として導かれており、反復 k 分割 CV への当てはめは近似
  • Kaggle の実物(S5E5 の実データ): ログインとルール同意が必要なため使っていない(Day163 から同じ)
  • 繰り返し数・分割数を変えた場合: J = 15(3 × 5)のみ。10×10 や 3分割など、重なりが違う設計で拡大率がどう変わるかは未測定
  • 拡大率が候補で違う理由: (a) 効果量の大きさ、(b) fold 差の SD の大きさ、の2案を挙げたが分けていない。24ワールド × 3候補では分離できる自由度が足りない
  • fold 差を行単位で対にするデルタ法の SE(Day164)との比較: 今日は fold 単位のみ。同じワールドで2つの SE を並べれば、どちらが被覆に近いかが分かる(未測定)
  • R3(CV 差の SE を3回繰り返しで取る)との比較: 今日は R2 だけ

🚀 次のステップ

  • 発展: 真の効果がちょうど0の候補(例: Weight の複製列、Sex と Age の既存の組み合わせを木が自力で作れるだけの列)を足し、naive・NB・行対デルタ法の −2SE の偽採用率を数える。偽採用と見逃しのコストを揃えて、決定6・10の閾値を見直す
  • 次回予告: (この回は独立したバックフィル。state の次回予告「Day 167 — 最終提出の選び方」は変更しない。同予告の持ち越しのうち、「R2 への Nadeau & Bengio 補正」は今日で一部消化(偽採用率は未測定)。なお予告の「Day 167」は実際には Day169 以降の番号になる)

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

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