"""予想パターン(プロファイル)定義。""" from __future__ import annotations from dataclasses import dataclass @dataclass(frozen=True) class PredictionProfile: key: str label: str description: str lookback_runs: int pass_rate: float enable_stage1_5: bool enable_stage2: bool enable_stage3: bool rank_mode: str forecast_pipeline: str # legacy | base | ev stage3_min_prob: float stage3_odds_cap: float stage3_aggressive_min_prob: float stage3_aggressive_odds_cap: float # 画面/API の「このパターンの予想方法」と同一文言(public/api/all_race_compare_profiles.php の PROFILE_META と揃える) _DESC_P1 = """【だれ向け】 はじめてこの画面を見る方・「普通に強そうな馬」を順当に並べたい方向けの、いわゆる標準モードです。 【ざっくり流れ】 ① 各馬について、データベースにある「過去のレース結果」から特徴量(近走の着順・勝率・同じ芝や距離での成績・上がり3Fの速さ・斤量など)を作ります。 ② 学習済みの機械学習モデル(LightGBM)が、その特徴から「このメンバー・この条件ならどれだけ力がありそうか」をスコア化します(取込バッチで指定したモデルファイルに依存)。 ③ 第1段階で「明らかに足りない馬」を間引きます。見る過去走は直近10走まで、通過率は約50%(厳しめと中庸の中間くらい)です。 ④ 第1.5段階(ON)…ラップや調教の変化など、細かい材料で少しだけ上げ下げします。 ⑤ 第2段階(ON)…コース適性・展開・騎手などのファクターで加減点し、阪神・中山などコース構造のナレッジもごく小さく反映します。 ⑥ 第3段階(ON)…単勝オッズを見て「妙味」「期待値」の情報を足し、最終的な判断材料にします。 ⑦ 最後に「堅実順位(実力・安定寄りの並び)」で並べ直し、1位を◎本命、2位を〇対抗、3位を▲単穴、4位を△連下とします。 【押さえておくこと】 的中率や回収率はパターンや時期で変わります。印は「その時点の機械的な並び」であり、真の勝率を保証するものではありません。""" _DESC_P2 = """【だれ向け】 「直近の状態・勢い」を最重視したい方向けです。長期の古い成績より、今のコンディションに寄せます。 【ざっくり流れ】 ① 見る過去走は直近3走に絞ります(数字が変わりやすく、波のある馬を拾いやすい設定です)。 ② 第1段階の通過率は約62%と、他パターンより多めに馬を残します(足切りを甘めにして近走情報を活かすイメージ)。 ③ 第1.5段階(ON)…細かい上下調整は入ります。 ④ 第2段階(OFF)…コース・展開・競馬場ナレッジによる大きな再評価は行いません。その分、モデルが見た「近走ベースのスコア」に寄った結果になります。 ⑤ 第3段階(ON)…単勝オッズによる妙味・期待値は反映します(オッズの見方は予想1よりややタイトめのパラメータです)。 ⑥ 印は「堅実順位」で◎〇▲△を付けます。 【押さえておくこと】 近走だけに寄るため、一発変わりやすい馬が上位に来やすい反面、コース向き・不利補正は弱めです。""" _DESC_P3 = """【だれ向け】 「たまたまの一発」より、長い期間の成績でブレを抑えたい方・妙味(オッズとのバランス)も意識したい方向けです。 【ざっくり流れ】 ① 見る過去走は直近20走までと長めに取り、馬のベース能力や安定度を拾いに行きます。 ② 第1段階の通過率は約38%と厳しめです(力のなさそうな馬を早めに落とすイメージ)。 ③ 第1.5段階(ON)・第2段階(ON)・第3段階(ON)はすべて入ります。コースや展開の加減点、競馬場ナレッジ、オッズも使います。 ④ 第3段階では、単勝オッズの扱いを予想1よりタイトめにしており、「割に合いそうな馬」を意識しやすい設定です。 ⑤ 印の付け方が他と違い、「堅実順位」ではなく「妙味順位(オッズと合わせた妙味寄りの並び)」で◎〇▲△を決めます。そのため人気馬だけが並ぶとは限らず、印の並びが予想1と違って見えることがあります。 【押さえておくこと】 妙味重視は回収の伸びを狙う一方、1着的中率は下がりやすい傾向があり得ます。""" _DESC_P4 = """【だれ向け】 実用というより「極端な条件にしたとき、成績がどう変わるか」を比較するための検証用に近いパターンです。いつもこれだけを見る用途を想定していません。 【ざっくり流れ】 ① 見る過去走は直近2走のみ。第1段階の通過率は約78%と非常に甘く、多くの馬が残ります。 ② 第1.5段階(OFF)…ラップ・調教寄りの細かい救済・減点はしません。 ③ 第2段階(OFF)…コース・展開・競馬場ナレッジの加減点もしません。 ④ 第3段階(ON)…オッズの許容範囲は広めで、単勝の妙味・期待値は強く効かせやすい設定です。 ⑤ 印は「堅実順位」で◎〇▲△を付けますが、土台の情報が「超短期2走+補正ほぼなし」なので、結果はノイズが大きく出やすいです。 【押さえておくこと】 他パターンとの差分を見るための軸です。安定した予想として使うより、「この極端設定との乖離」を読む用途向きです。""" _DESC_P5 = """【だれ向け】 1着を当てる率よりも、長期的な回収や高配当を意識したい方向けです(穴馬が◎に上がりやすい設定です)。 【ざっくり流れ】 ① 見る過去走は直近8走。第1段階の通過率は約35%と厳しめで、そこそこ力のありそうな馬に絞り込みます。 ② 第1.5段階(ON)・第2段階(ON)・第3段階(ON)はすべて入ります。 ③ 第3段階では単勝オッズの上限を広く取り、人気薄でも期待値が立つ馬を拾いに行きやすいパラメータです。 ④ 印は「高回収寄りの順位(rank_value_aggressive)」で決めます。オッズが効きやすく、◎が人気の中心から外れやすいので、画面上の的中率は下がりやすい一方、配当面の振れを狙う設計です。 【押さえておくこと】 「当たらないが一度当たると大きい」タイプの振る舞いになりやすいです。資金配分は慎重にしてください。""" _DESC_BASE = """【ベース予想】 オッズ・期待値を入れる前の予想です。公開画面の◎〇▲△の根拠になります。 【流れ】 ① 直近10走・通過率50%で第1段階スクリーニング ② LightGBM モデルが特徴からベーススコアを算出 ③ 第1.5段階は OFF(モデル+第2段階を優先) ④ 第2段階 ON … コース適性・展開・騎手・競馬場ナレッジ ⑤ 全出走馬に堅実順位を付け、上位4頭を ◎〇▲△ とする ⑥ 各馬に勝率見立て(モデル生出力ベース。合計100%に按分しない) ⑦ 単勝オッズは使わない 【切】 順位が下だから切るのではない。見立てとして勝ち筋がない馬のみ 0%(切)とする。""" _DESC_EV = """【期待値予想】 ベース予想に、単勝オッズと期待値の層を載せたものです。公開の「予想結果」はこの期待値予想を表示します。 【流れ】 ① ベース予想と同一パイプラインで ◎〇▲△・全馬順位・勝率見立てを確定(オッズはまだ使わない) ② 各馬の勝率見立て × 確定単勝オッズ → 期待払戻率(expected_roi) ③ ◎の付け替えはしない。期待値は「買う価値」の参考指標 【パラメータ】 min_prob=0.03(期待値計算時に差し引く最低勝率)、odds_cap=80(単勝上限倍率)。""" PROFILES: list[PredictionProfile] = [ PredictionProfile( key="p1_pundit", label="予想1(予想家パターン)", description=_DESC_P1, lookback_runs=10, pass_rate=0.50, enable_stage1_5=True, enable_stage2=True, enable_stage3=True, rank_mode="accuracy", forecast_pipeline="legacy", stage3_min_prob=0.02, stage3_odds_cap=80.0, stage3_aggressive_min_prob=0.00, stage3_aggressive_odds_cap=300.0, ), PredictionProfile( key="p2_recent", label="予想2(直近重視)", description=_DESC_P2, lookback_runs=3, pass_rate=0.62, enable_stage1_5=True, enable_stage2=False, enable_stage3=True, rank_mode="accuracy", forecast_pipeline="legacy", stage3_min_prob=0.04, stage3_odds_cap=60.0, stage3_aggressive_min_prob=0.01, stage3_aggressive_odds_cap=160.0, ), PredictionProfile( key="p3_long", label="予想3(長期安定)", description=_DESC_P3, lookback_runs=20, pass_rate=0.38, enable_stage1_5=True, enable_stage2=True, enable_stage3=True, rank_mode="value", forecast_pipeline="legacy", stage3_min_prob=0.05, stage3_odds_cap=45.0, stage3_aggressive_min_prob=0.00, stage3_aggressive_odds_cap=120.0, ), PredictionProfile( key="p4_overfit", label="予想4(過学習寄せ検証)", description=_DESC_P4, lookback_runs=2, pass_rate=0.78, enable_stage1_5=False, enable_stage2=False, enable_stage3=True, rank_mode="accuracy", forecast_pipeline="legacy", stage3_min_prob=0.00, stage3_odds_cap=200.0, stage3_aggressive_min_prob=0.00, stage3_aggressive_odds_cap=700.0, ), PredictionProfile( key="p5_value", label="予想5(高回収狙い)", description=_DESC_P5, lookback_runs=8, pass_rate=0.35, enable_stage1_5=True, enable_stage2=True, enable_stage3=True, rank_mode="aggressive", forecast_pipeline="legacy", stage3_min_prob=0.00, stage3_odds_cap=180.0, stage3_aggressive_min_prob=0.00, stage3_aggressive_odds_cap=1200.0, ), PredictionProfile( key="base_forecast", label="ベース予想", description=_DESC_BASE, lookback_runs=10, pass_rate=0.50, enable_stage1_5=False, enable_stage2=True, enable_stage3=False, rank_mode="accuracy", forecast_pipeline="base", stage3_min_prob=0.03, stage3_odds_cap=80.0, stage3_aggressive_min_prob=0.00, stage3_aggressive_odds_cap=300.0, ), PredictionProfile( key="ev_forecast", label="期待値予想", description=_DESC_EV, lookback_runs=10, pass_rate=0.50, enable_stage1_5=False, enable_stage2=True, enable_stage3=True, rank_mode="accuracy", forecast_pipeline="ev", stage3_min_prob=0.03, stage3_odds_cap=80.0, stage3_aggressive_min_prob=0.00, stage3_aggressive_odds_cap=300.0, ), ] _PROFILE_ALIASES: dict[str, str] = { "p6_latest": "ev_forecast", } def get_profiles(selected: str | None = None) -> list[PredictionProfile]: if not selected or selected == "all": return PROFILES key = _PROFILE_ALIASES.get(selected, selected) items = [p for p in PROFILES if p.key == key] if not items: raise ValueError(f"unknown profile: {selected}") return items