📚 背景知識(読んでから問題へ)
Phase6で学んだ5グループは、どれも「やった方がいい」ことに見える。しかし限られた時間で全部を同じ熱量でやろうとすると、実際にはメダルへの寄与が小さい作業に時間を吸われがちである。「改善幅 ÷ 投入時間 = ROI」という単一の指標に翻訳することで、初めて「どこに時間を張るべきか」を機械的に決定できる。
| Phase6のグループ | 該当Day | 学んだこと(概要) |
|---|---|---|
| SOTA手法追跡・論文実装 | 126-130 | Label Smoothing・SAM・EMA・Mixup・Knowledge Distillation |
| カスタムアーキテクチャ設計 | 131-135 | Residual MLP・Gated MLP・Squeeze-and-Excite・Multi-Head Attention・Hybrid Block |
| 高度なアンサンブル | 136-140 | Pseudo Labeling・TTA(ノイズ摂動・Feature Masking)・Stacking基礎 |
| コンペ戦略 | 141-145 | 提出予算配分・探索と活用・チームマージ・メダル圏内距離・Gold3枚統合シミュレーション |
| 解法公開・フォーラム | 146-148 | 上位解法の読み方・write-upの信頼度判定(trust_score)・自分のwrite-up自動生成 |
🗂️ Phase6ログの構造化データ
| 変数名 | 型 | 内容 |
|---|---|---|
| phase6_log | list[dict] | {"theme": グループ名, "days": 該当Day, "avg_cv_gain": 平均CV改善幅(質的グループはNone), "hours": 平均投入時間} を5件 |
phase6_log = [ {"theme": "SOTA手法追跡", "days": "126-130", "avg_cv_gain": 0.0091, "hours": 14}, {"theme": "カスタムアーキテクチャ", "days": "131-135", "avg_cv_gain": 0.0043, "hours": 22}, {"theme": "高度なアンサンブル", "days": "136-140", "avg_cv_gain": 0.0158, "hours": 9}, {"theme": "コンペ戦略", "days": "141-145", "avg_cv_gain": 0.0067, "hours": 5}, {"theme": "解法公開・フォーラム", "days": "146-148", "avg_cv_gain": None, "hours": 12}, ]
🎯 問題
タスクA(理論・戦略)
以下2点をそれぞれ2〜3文で説明する。
タスクB(分析→コーディング)
phase6_logを受け取り、avg_cv_gainが数値のグループについてroi = avg_cv_gain / hoursを計算しROI降順にソートしたDataFrameと、avg_cv_gainがNone(質的なグループ)のDataFrameを分けて返す関数 compute_roi(phase6_log) -> tuple[pd.DataFrame, pd.DataFrame] を実装するtotal_hours=200とし、質的グループには1テーマあたりfloor_hours=15時間を最低保証として先に割り当て、残りをROIの大きさに比例して数値グループに配分する関数 allocate_budget(numeric_df, qualitative_df, total_hours=200, floor_hours=15) -> dict を実装するcompute_roiの出力がROI降順になっていることをassert文で確認するallocate_budgetが返す配分の合計がtotal_hours(誤差0.5時間以内)に一致することをassert文で確認する💡 ヒント
タスクAの1.は、投入時間が極端に小さいテーマ(例えば5時間しか使っていないのにたまたま改善幅が大きかったテーマ)のROIが不自然に高く出てしまう可能性を考えるとよい(分散の小さいサンプルほど極端な比率が出やすい、という一般的な注意点)。タスクBは、Day145の「Gold3枚達成プラン統合シミュレーション」やDay146の「write-up技術のROIランキング」の延長線上にあるが、今日は「技術1つずつ」ではなく「Phase6のグループ単位」でROIを計算する点が異なる。
compute_roi:avg_cv_gainがNoneかどうかを判定し、DataFrameを2つに分割する。ROIは列同士の演算で一括計算できるallocate_budget: まず質的グループの人数 ×floor_hoursをtotal_hoursから引いて「数値グループに配れる残り時間」を求める- 各数値グループの配分時間 = 残り時間 × (そのグループのROI ÷ 全数値グループのROI合計) という比例配分の式を使う
- 合計が
total_hoursにほぼ一致することの確認には、丸め誤差を許容するためabs(合計 - total_hours) < 0.5のような許容範囲を使う
import pandas as pd def compute_roi(phase6_log): df = pd.DataFrame(phase6_log) df["roi"] = df.apply( lambda r: (r["avg_cv_gain"] / r["hours"]) if r["avg_cv_gain"] is not None else None, axis=1, ) numeric = df[df["roi"].notna()].sort_values("roi", ascending=False).reset_index(drop=True) qualitative = df[df["roi"].isna()].reset_index(drop=True) return numeric, qualitative def allocate_budget(numeric_df, qualitative_df, total_hours=200, floor_hours=15): qual_total = floor_hours * len(qualitative_df) remaining = total_hours - qual_total weight_sum = numeric_df["roi"].sum() alloc = {} for _, row in numeric_df.iterrows(): alloc[row["theme"]] = round(remaining * row["roi"] / weight_sum, 1) for _, row in qualitative_df.iterrows(): alloc[row["theme"]] = float(floor_hours) return alloc # ... assert でROI降順・合計200hを確認 ...
✅ 模範解答
import pandas as pd phase6_log = [ {"theme": "SOTA手法追跡", "days": "126-130", "avg_cv_gain": 0.0091, "hours": 14}, {"theme": "カスタムアーキテクチャ", "days": "131-135", "avg_cv_gain": 0.0043, "hours": 22}, {"theme": "高度なアンサンブル", "days": "136-140", "avg_cv_gain": 0.0158, "hours": 9}, {"theme": "コンペ戦略", "days": "141-145", "avg_cv_gain": 0.0067, "hours": 5}, {"theme": "解法公開・フォーラム", "days": "146-148", "avg_cv_gain": None, "hours": 12}, ] def compute_roi(phase6_log): df = pd.DataFrame(phase6_log) df["roi"] = df.apply( lambda r: (r["avg_cv_gain"] / r["hours"]) if r["avg_cv_gain"] is not None else None, axis=1, ) numeric = df[df["roi"].notna()].sort_values("roi", ascending=False).reset_index(drop=True) qualitative = df[df["roi"].isna()].reset_index(drop=True) return numeric, qualitative def allocate_budget(numeric_df, qualitative_df, total_hours=200, floor_hours=15): qual_total = floor_hours * len(qualitative_df) remaining = total_hours - qual_total weight_sum = numeric_df["roi"].sum() alloc = {} for _, row in numeric_df.iterrows(): alloc[row["theme"]] = round(remaining * row["roi"] / weight_sum, 1) for _, row in qualitative_df.iterrows(): alloc[row["theme"]] = float(floor_hours) return alloc numeric_df, qualitative_df = compute_roi(phase6_log) alloc = allocate_budget(numeric_df, qualitative_df, total_hours=200, floor_hours=15) total = sum(alloc.values()) assert list(numeric_df["theme"]) == list(numeric_df.sort_values("roi", ascending=False)["theme"]), "ROI降順になっていません" assert abs(total - 200) < 0.5, f"配分合計が200時間から乖離しています: {total}" print("[OK] ROI降順ソート かつ 配分合計200h(誤差0.5h以内) を満たす") # ============ 実測結果(実際にPython 3で実行して確認済み) ============ # theme avg_cv_gain hours roi # 0 高度なアンサンブル 0.0158 9 0.001756 # 1 コンペ戦略 0.0067 5 0.001340 # 2 SOTA手法追跡 0.0091 14 0.000650 # 3 カスタムアーキテクチャ 0.0043 22 0.000195 # # theme avg_cv_gain hours # 0 解法公開・フォーラム NaN 12 # # 高度なアンサンブル 82.4h / コンペ戦略 62.9h / SOTA手法追跡 30.5h / # カスタムアーキテクチャ 9.2h / 解法公開・フォーラム 15.0h(フロア予算) # 合計: 200.0h # [OK] ROI降順ソート かつ 配分合計200h(誤差0.5h以内) を満たす
📊 結果の可視化
① Phase6ログの棚卸し
phase6_log に5グループの平均CV改善幅・投入時間を構造化して保持
② ROI計算・降順ランキング
compute_roi() が数値グループと質的グループを分け、ROI降順にソート
③ 90日・200時間の配分
allocate_budget() がフロア予算+ROI比例配分で次の時間の使い方を決定
| グループ | avg_cv_gain | hours | roi | 配分(次の90日) |
|---|---|---|---|---|
| 高度なアンサンブル | 0.0158 | 9 | 0.001756(1位) | 82.4h |
| コンペ戦略 | 0.0067 | 5 | 0.001340 | 62.9h |
| SOTA手法追跡 | 0.0091 | 14 | 0.000650 | 30.5h |
| 解法公開・フォーラム | None(質的) | 12 | 対象外 | 15.0h(フロア) |
| カスタムアーキテクチャ | 0.0043 | 22 | 0.000195(最下位) | 9.2h |
🪜 Step-by-Step 解説
1なぜ「グループ単位」でROIを計算するのか(Day146との違い)
phase6_log = [ {"theme": "SOTA手法追跡", "days": "126-130", ...}, ... ]
2なぜ質的なグループをROI計算から除外するのか
df["roi"] = df.apply( lambda r: (r["avg_cv_gain"] / r["hours"]) if r["avg_cv_gain"] is not None else None, axis=1, )
avg_cv_gainを無理やり0として計算すると、ROIも自動的に0になり、他の4グループと同じ土俵で比較されて「予算配分ゼロ」という結論に流れてしまう。Noneのまま扱い、ROI計算の対象から明示的に除外することで、「measurement対象外である」という事実そのものをコードの構造で表現できる。これはDay147で「red flagをtrust_scoreの4基準に含めず別枠にした」判断と同じ設計思想である。3なぜフロア予算を先に引いてから、残りを比例配分するのか
qual_total = floor_hours * len(qualitative_df) remaining = total_hours - qual_total weight_sum = numeric_df["roi"].sum() alloc[row["theme"]] = round(remaining * row["roi"] / weight_sum, 1)
total_hoursの合計が200時間を超えてしまい、assertが失敗する。「最低保証」と「ROI比例」という2種類の異なるロジックを、計算の順番で両立させている点がこのコードの設計上のポイントである。🧮 数学・統計の補足(文系向け)
roi = avg_cv_gain / hoursという式は、アルバイトの時給計算「もらえた金額 ÷ 働いた時間」と全く同じ構造をしている。「高度なアンサンブル」は9時間で0.0158点分のCV改善を得たので、いわば「1時間あたり0.00176点」を稼ぐ"時給"が高い作業だったと言える。一方「カスタムアーキテクチャ」は22時間かけて0.0043点しか改善しておらず、"時給"は約9分の1(0.000195点/時間)しかない。同じ「頑張った」でも、時給換算すると得意分野と不得意分野がはっきり分かれることが分かる。
remaining * row["roi"] / weight_sumという式は、「185時間分のピザを、ROIという"お腹の空き具合"に応じて切り分ける」計算だと考えると分かりやすい。4人がそれぞれのお腹の空き具合(ROI)を申告し、それぞれの空き具合を全員の合計で割った"割合"だけピザ(残り時間)を受け取る。空き具合が2倍の人は2倍の分量を受け取り、全員の受け取り分を合計すると必ずピザ全体(185時間)に一致する、という比例配分の基本的な考え方である。
🏆 Kaggleでの実践的な使い方
よく使われるコンペカテゴリ: ☑ 表形式データ(Tabular) / ☑ 自然言語処理(NLP) / ☑ 画像認識(CV) / ☑ 時系列(Time Series)
⏱️ コンペ中の時間配分を定量的に決める
感覚ではなく実測データに基づく判断
特徴量エンジニアリング・モデル選定・アンサンブル・write-upそれぞれの過去の改善幅と投入時間を記録しておけば、新しいコンペでも今日と同じROI計算で「今回はどこに時間を張るべきか」を機械的に決められる。
🤝 チームでの役割分担の根拠にする
得意分野の可視化
チームメンバーそれぞれの得意分野のROIを比較することで、「誰がアンサンブル担当、誰が特徴量エンジニアリング担当か」という役割分担を実測データに基づいて決められる。
🔍 「勉強のための勉強」の自己点検
学習目的とスコア目的のバランス
カスタムアーキテクチャの自作のように「勉強にはなるがROIは低い」活動と、アンサンブルのように「即座にスコアへ跳ね返るがROIが高い」活動を切り分けて記録することで、時間の使い方を意識的に選べるようになる。
⚠️ よくある誤解・ミス
| 誤解・ミス | なぜ起こるか | 正しい理解 |
|---|---|---|
| ROIが一番高い活動だけに全ての時間を使えばよいと考える | 「一番効率がいいものに全振りすべき」という単純な最適化発想 | 投入時間がゼロに近づくとその技術の再現性や汎用性の検証が不十分になる。ROI最大化と「様々な技術の引き出しを持つこと」はトレードオフであり、複数グループへ分散配分する設計が現実的である |
| CVスコアに効かない活動には時間を使う価値がないと考える | 「測れるものだけが価値」という数値至上主義 | 解法公開・フォーラムのように、短期的なCVには効かなくても中長期的な学習効率・コミュニティでの信頼という別軸の価値を持つ活動がある。フロア予算のような仕組みで意図的に確保する必要がある |
| 過去のROIが将来も同じ値で続くと思い込む | 一度計測した数値を固定的な真実として扱ってしまう | 今日のROIはあくまで過去5グループ・Day126-148の実測に基づく暫定値である。新しいコンペのドメインが変われば効果は変動しうるため、次のサイクルでも実測し直して更新する必要がある |
| 投入時間が短いテーマのROIをそのまま信頼してしまう | 分母が小さいほど比率が不安定になることを意識しない | 投入時間が5時間しかない「コンペ戦略」のROIは振れ幅が大きい可能性がある。投入時間そのものも記録し、極端に短い時間で計算されたROIには注意書きを添えるなどの工夫が必要である |
🚀 次のステップ
- 発展: 今日は「Phase6の5グループ」という粗い粒度でROIを計算したが、実際のコンペではさらに細かく「特徴量エンジニアリングのうちどのサブカテゴリが効くか」「アンサンブルのうちどの重み付け手法が効くか」という階層的なROI計算に発展させられる。
phase6_logのような構造化データを、より細かい粒度でも同じ形式で残しておく習慣が土台になる - 次回予告: Phase6の総復習が一区切りついたため、次回はPhase6の中でまだ深掘りしていない周辺テーマ(NLP系コンペへの応用、あるいは実在のPlayground Series/Tabularコンペへの実践投入を想定したシミュレーション)に取り組む予定。テーマ選定はDay150開始時に決定する
Phase 6 の学習マップ(総復習・Day149で一区切り)
roadmap.md記載のPhase6主要テーマ6項目はDay126-148で全て着手完了した。Day149の今日はそれらを1つの表に統合し、「次の90日、どこに時間を張るか」をROI計算と予算配分アルゴリズムという形で数値化した。ROI1位は高度なアンサンブル・最下位はカスタムアーキテクチャという結果を得た。
📝 自己評価(解いた後に記入)
自分の回答・気づき・メモ: