📚 背景知識(読んでから問題へ)
| キーワード | 直感的な説明 |
|---|---|
| Public LB / Private LB | 期間中に見える暫定順位表(Public)と、終了時に公開される最終順位表(Private)。異なるデータサンプルで計算される |
| シェイクアップ(shake-up) | Public LBとPrivate LBのスコアのズレによって、最終順位が期間中の順位から変動する現象 |
| カットオフライン(cutoff score) | メダル圏内(今日はGold)に入るために必要な最低スコアのライン。これも変動する |
| 逆算(back-calculation) | 「目標とする到達確率」から出発し、それを満たすために必要な改善幅を求める考え方。Day141-143の「積み上げ型」に対し今日は「目標から逆に必要量を求める」 |
🎯 問題
タスクA(戦略・記述)
コンペ終了まで残り12日、現在のPublic LBスコアは0.815、Goldカットオフラインはおおよそ0.825あたりと見立てているが確信はない。以下3点をそれぞれ2〜3文で説明する。
タスクB(実装・シミュレーション)
| 項目 | 値 |
|---|---|
| 残り日数・現在のPublic LB | D_REMAINING=12 / PUBLIC_LB_NOW=0.815 |
| 自然な逓減改善曲線 | CV_GAIN_BASE=0.0018 / CV_DECAY=0.88 |
| Goldカットオフの推定値 | GOLD_CUTOFF_PUBLIC_EST=0.825 |
| シェイクアップ幅(自分・カットオフ双方に作用) | SHAKEUP_STD=0.006 |
| カットオフラインの上昇 | CUTOFF_DRIFT_MEAN=0.003 / CUTOFF_DRIFT_STD=0.004 |
| 目標到達確率・反復回数 | TARGET_PROB=0.70 / N_SIM=20000 |
import numpy as np N_SIM = 20000 D_REMAINING = 12 PUBLIC_LB_NOW = 0.815 CV_GAIN_BASE = 0.0018 CV_DECAY = 0.88 GOLD_CUTOFF_PUBLIC_EST = 0.825 SHAKEUP_STD = 0.006 CUTOFF_DRIFT_MEAN = 0.003 CUTOFF_DRIFT_STD = 0.004 TARGET_PROB = 0.70
gain(base, decay, start, length)(Day142/143と同じ)で自然な改善幅planned_gainを計算するnaive_gap = GOLD_CUTOFF_PUBLIC_EST - PUBLIC_LB_NOW - planned_gainを計算するdeltaを引数に取るsimulate_p_medal(delta, rng)を実装。own/cutoffともに独立なシェイクアップノイズを加えP(own_private > cutoff_private)を返すP(medal)を計算する。delta=0でのP(medal)がnaive_gapの符号から想像される値とどれだけ違うかに注目するP(medal)>=TARGET_PROBを満たす最小のrequired_deltaを求め、naive_gapと比較する💡 ヒント
タスクAの1.は、自分のスコアとカットオフラインの両方が同じ大きさの不確実性を持って独立に動くという点に気づけるかがポイント。2.はDay141-143の逓減する改善曲線から、残り日数が減るほど自然に稼げるplanned_gainが小さくなることを考える。タスクBは、「自分のスコア」と「カットオフライン」という2つの確率変数の差を扱う点が新しい。どちらも独立にシェイクアップの影響を受けるため、2つの不確実性の分散が合成されて、素朴な点差からは想像しにくいほど確率が動く。
simulate_p_medalの中でown_privateとcutoff_privateはどちらもN_SIM件のnumpy配列として一括生成し、(own_private > cutoff_private).mean()で比率を出す- 二分探索は「deltaが大きいほどP(medal)は単調に増加する」性質を利用し、
p < TARGET_PROBならlo=mid、そうでなければhi=midと区間を狭める - 比較を安定させるため、パラメータを変えて評価するたびに
rng = np.random.default_rng(0)と同じシードから作り直すと、乱数のブレでなくパラメータの違いが見やすくなる(Day143と同じ工夫) - 感度分析はSHAKEUP_STD・D_REMAININGそれぞれ他の設定を固定し、二分探索を繰り返せばよい
def gain(base, decay, start, length): if length <= 0: return 0.0 i = np.arange(start, start + length) return np.sum(base * (decay ** i)) def simulate_p_medal(delta, rng, n_sim=N_SIM, shakeup_std=SHAKEUP_STD, cutoff_drift_mean=CUTOFF_DRIFT_MEAN, cutoff_drift_std=CUTOFF_DRIFT_STD): planned_gain = gain(CV_GAIN_BASE, CV_DECAY, 0, D_REMAINING) own_base = PUBLIC_LB_NOW + planned_gain + delta own_private = own_base + rng.normal(0, shakeup_std, n_sim) cutoff_private = (GOLD_CUTOFF_PUBLIC_EST + rng.normal(cutoff_drift_mean, cutoff_drift_std, n_sim) + rng.normal(0, shakeup_std, n_sim)) return (own_private > cutoff_private).mean() # 二分探索でrequired_deltaを求める lo, hi = 0.0, 0.05 for _ in range(40): mid = (lo + hi) / 2 rng = np.random.default_rng(0) p = simulate_p_medal(mid, rng) if p < TARGET_PROB: lo = mid else: hi = mid required_delta = hi
✅ 模範解答
for delta in np.round(np.arange(0.000, 0.021, 0.001), 3): rng = np.random.default_rng(0) p = simulate_p_medal(delta, rng) print(f"delta={delta:.3f} P(medal)={p:.3f}") # ============ 実測結果 ============ # planned_gain(natural, 12days)=0.01176 # naive_gap = -0.00176 (素朴な点差だけ見ると「自然な伸びだけで届く」ように見える) # # delta=0.000 P(medal)=0.445 ← naive_gapは負(=届いて見える)なのにP(medal)は半分以下 # delta=0.003 P(medal)=0.575 # delta=0.006 P(medal)=0.700 ← ここでTARGET_PROB=0.70に到達 # delta=0.010 P(medal)=0.828 # delta=0.015 P(medal)=0.929 # delta=0.020 P(medal)=0.977 # # required_delta = 0.00601 (このdeltaでP(medal)=0.7000) # naive_gap = -0.00176 → required_delta / naive_gap = -3.406倍(符号も大きさも一致しない)
📊 結果の可視化
naive_gap(素朴な点差)と required_delta(不確実性を考慮した必要上乗せ幅)の符号の違い:
| 指標 | 値 | 意味 |
|---|---|---|
| planned_gain | 0.01176 | 残り12日で自然に稼げる改善幅 |
| naive_gap | −0.00176 | 不確実性を無視した素朴な点差(負=届いて見える) |
| P(medal) at delta=0 | 0.445 | 実際には半分を下回る |
| required_delta | +0.00601 | TARGET_PROB=0.70を満たす必要な上乗せ改善 |
🪜 Step-by-Step 解説
1なぜdelta=0でもnaive_gapの符号から想像できないほどP(medal)が低いのか
# naive_gap = -0.00176 (点差の上では届いているように見える)
# delta=0.000 P(medal)=0.445
own_privateとcutoff_privateはそれぞれ独立にNormal(0, SHAKEUP_STD)のノイズを持つため、両者の差の標準偏差は単純なSHAKEUP_STDではなく、sqrt(2*SHAKEUP_STD**2 + CUTOFF_DRIFT_STD**2) ≈ 0.0094という、naive_gapの絶対値(0.00176)よりずっと大きな広がりになる。さらにCUTOFF_DRIFT_MEAN=0.003(他の参加者も伸びる分、カットオフラインが平均的に上がる効果)が上乗せされ、実質的な期待差はnaive_gap - CUTOFF_DRIFT_MEAN ≈ -0.0048とさらに不利な方向にずれる。点差が広がりに対して小さすぎるほど、確率はコイントス(50%)に近づく。2なぜ残り日数が少ないほどrequired_deltaが大きくなるのか
# D_REMAINING= 4 planned_gain=0.00600 required_delta=0.01177
# D_REMAINING=20 planned_gain=0.01384 required_delta=0.00394
3なぜSHAKEUP_STDが大きいコンペほどrequired_deltaが大きくなるのか
# SHAKEUP_STD=0.002 required_delta=0.00379
# SHAKEUP_STD=0.014 required_delta=0.01153
🔁 追加検証(SHAKEUP_STD・D_REMAININGを変化させた場合)
それぞれ他の設定を固定し、SHAKEUP_STDとD_REMAININGを個別に変化させてrequired_deltaを求めた。
# --- SHAKEUP_STDを変えた場合 ---
# SHAKEUP_STD=0.002 required_delta=0.00379
# SHAKEUP_STD=0.004 required_delta=0.00481
# SHAKEUP_STD=0.006 required_delta=0.00601 ← 今日のメイン設定
# SHAKEUP_STD=0.008 required_delta=0.00731
# SHAKEUP_STD=0.010 required_delta=0.00873
# SHAKEUP_STD=0.012 required_delta=0.01011
# SHAKEUP_STD=0.014 required_delta=0.01153
# --- D_REMAINING(残り日数)を変えた場合 ---
# D_REMAINING= 4 planned_gain=0.00600 naive_gap=0.00400 required_delta=0.01177
# D_REMAINING= 6 planned_gain=0.00803 naive_gap=0.00197 required_delta=0.00974
# D_REMAINING= 8 planned_gain=0.00961 naive_gap=0.00039 required_delta=0.00817
# D_REMAINING=10 planned_gain=0.01082 naive_gap=-0.00082 required_delta=0.00695
# D_REMAINING=12 planned_gain=0.01176 naive_gap=-0.00176 required_delta=0.00601 ← 今日のメイン設定
# D_REMAINING=16 planned_gain=0.01306 naive_gap=-0.00306 required_delta=0.00472
# D_REMAINING=20 planned_gain=0.01384 naive_gap=-0.00384 required_delta=0.00394
🧮 数学・統計の補足(文系向け)
自分の矢が的の中心から平均0.5cmだけ的に近い場所に飛ぶとします。しかし風でブレて実際にはプラスマイナス3cmくらい散らばります。一方、的自体もわずかに揺れる台の上にあり、平均的な位置はほぼ動かないものの、これもプラスマイナス2cmくらい散らばるとします。「自分の矢の平均位置」と「的の平均位置」の差は0.5cmしかありませんが、矢のブレと的のブレの両方が重なった結果、実際に矢が的の内側に入るかどうかは、単純に0.5cmという数字だけからは想像できないほど五分五分に近づきます。今日のシミュレーションの
own_privateとcutoff_privateは、まさにこの「両方が独立に揺れる」構造を再現したものです。
2つの独立な確率変数の差の分散は、それぞれの分散の和になります(標準偏差は単純な足し算ではなく、2乗和のルート):
combined_std = np.sqrt(2 * SHAKEUP_STD**2 + CUTOFF_DRIFT_STD**2) # = sqrt(2 * 0.006**2 + 0.004**2) = sqrt(0.000088) ≈ 0.00938「点差(naive_gap)は0.00176しかないのに、広がり(combined_std)は0.0094もある」という状態は、点差が広がりの5分の1程度しかないことを意味します。1つの変数のバラつきを表す標準偏差に対し、2つの変数の差を考えるときはそれぞれのバラつきが両方効いてくる、という点が今日の実測のポイントです。
🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☑ 表形式データ(Tabular) / ☑ 自然言語処理(NLP) / ☑ 画像認識(CV) / ☑ 時系列(Time Series)
🚫 順位だけでメダルを確信しない
点差がプラスに見えても油断しない
今日の実測が示す通り、素朴な点差がわずかにプラス方向でも実際の到達確率は五分五分を大きく下回りうる。過去の類似コンペのシェイクアップ幅(Meta Kaggleデータなど)を事前に把握しておく。
📅 残り日数と必要な一手をセットで見積もる
定期的にrequired_deltaを逆算し直す
残り日数が少なくなるほど自然な伸びで補える割合は下がり、必要な上乗せ幅は増える。「あと何日で何点必要か」を定期的に見直し、Day142の探索やDay143のマージへの切り替えを早める。
👀 カットオフラインの動きも観測する
自分だけでなく相手側の伸びも見る
自分のスコアだけでなく、他の参加者のPublic LBの伸びのペースもLB履歴やDiscussionで定点観測し、CUTOFF_DRIFT_MEANに相当する上昇ペースを主観的にでも見積もる。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| Public LBで現在カットオフラインより上にいれば安心だと思う | 見た目の順位・点差をそのまま信じてしまう | 今日の実測ではnaive_gapが負(=点差の上では届いて見える)でも、P(medal)=44.5%と半分を下回った。シェイクアップとカットオフの変動、両方を考慮しないと過信になる |
| 残り日数が少なくなるほど、必要な上乗せ幅は小さくなると思う(もう十分伸ばしたから) | 「これまでの進捗」に安心してしまう直感 | 実測では逆に、残り日数が少ないほど自然な改善(planned_gain)が小さくなる分、必要な上乗せ幅(required_delta)は増えた(D=4で0.01177 vs D=20で0.00394) |
| シェイクアップは自分のスコアだけに影響すると思いがち | 「自分が主役」という視点で不確実性を捉えてしまう | 実際にはカットオフライン側にも同じ大きさの不確実性がある。両方の不確実性が独立に重なるため、実測のrequired_deltaは点差の大きさだけでは説明できない値になった |
| 必要な改善幅(required_delta)を満たせば確実にGoldに届くと思う | 「目標を満たした=保証された」という誤解 | required_deltaはTARGET_PROB=0.70を満たす値であり、それでも30%の確率で外れる。これは「必要な投資」であって「保証」ではない |
🚀 次のステップ
- 発展: 今日は自分のシェイクアップノイズとカットオフラインのシェイクアップノイズを独立とみなしたが、実際には同じテストデータの分割から生まれるズレである以上、両者にはある程度の正の相関がある可能性がある。
rng.multivariate_normalなどで相関を持たせたノイズに拡張し、相関がrequired_deltaをどう変えるか試してみましょう。CUTOFF_DRIFT_MEAN(他の参加者の伸びペース)を0〜0.010の範囲で変化させた場合の感度も確認する価値があります - 次回予告: Phase6テーマ4「コンペ戦略・Gold 3枚達成プラン」の5日目・最終日に入ります。次回はDay141〜144で扱った4つの意思決定(提出予算配分・探索と活用・チームマージ・メダル圏内までの距離の見積もり)を統合し、一つの「Gold 3枚達成プラン」として総括する予定です
Phase 6 の学習マップ(全20テーマ)
Day141で最終提出の選び方、Day142で探索/活用トレードオフ、Day143でチームマージ、Day144でメダル圏内までの距離の見積もりを実測。次回、5日間の総括に進む。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: