Spaces:
Configuration error
Configuration error
File size: 4,534 Bytes
e23172f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 | """Lightweight open-source trifecta prediction model."""
from __future__ import annotations
from dataclasses import dataclass, field
from typing import Any
@dataclass
class Runner:
number: int
name: str
jockey: str = ""
trainer: str = ""
weight: float | None = None
barrier: int | None = None
form: str = ""
last20Starts: str = ""
careerPrizeMoney: str = "$0"
scratched: bool = False
stats: dict[str, Any] = field(default_factory=dict)
@dataclass
class Race:
date: str
track: str
track_slug: str
race_number: str
race_name: str
distance: str
condition: str
weather: str
race_class: str
start_time: str
prize_money: str
number_of_runners: int
runners: list[Runner] = field(default_factory=list)
class TrifectaPredictor:
"""Open-source trifecta prediction model."""
def predict(self, race: Race) -> dict[str, Any]:
runners = [r for r in race.runners if not r.scratched]
if len(runners) < 3:
return {"error": "Insufficient runners"}
scored: list[dict[str, Any]] = []
for runner in runners:
scored.append({
"number": runner.number,
"name": runner.name,
"score": self._score_runner(runner),
"win_prob": self._win_probability(runner),
"place_prob": self._place_probability(runner),
})
scored.sort(key=lambda x: x["score"], reverse=True)
top = scored[:3]
primary = f"{top[0]['number']}-{top[1]['number']}-{top[2]['number']}"
secondary = None
value = None
if len(scored) > 3:
secondary = f"{top[0]['number']}-{top[2]['number']}-{scored[3]['number']}"
outsiders = [s for s in scored[3:] if s["score"] > 30]
if outsiders:
value = f"{scored[1]['number']}-{top[0]['number']}-{outsiders[0]['number']}"
else:
value = f"{scored[1]['number']}-{top[0]['number']}-{top[2]['number']}"
return {
"date": race.date,
"track": race.track,
"race_number": race.race_number,
"race_name": race.race_name,
"primary": primary,
"secondary": secondary,
"value": value,
"top3": top,
"confidence": "MEDIUM",
}
def _score_runner(self, runner: Runner) -> float:
score = 0.0
form = str(runner.form or runner.last20Starts or "")
recent = form[-5:] if len(form) > 5 else form
score += min((recent.count("1") * 8 + recent.count("2") * 4 + recent.count("3") * 4), 25)
overall = runner.stats.get("overall", {})
starts = overall.get("starts", 0) or 0
win_pct = overall.get("winPercent", 0) or 0
place_pct = overall.get("placePercent", 0) or 0
score += win_pct * 20
score += place_pct * 10
track_stats = runner.stats.get("track", {})
track_starts = track_stats.get("starts", 0) or 0
track_places = track_stats.get("places", 0) or 0
score += min((track_places / max(track_starts, 1)) * 10, 10)
dist_stats = runner.stats.get("distance", {})
dist_starts = dist_stats.get("starts", 0) or 0
dist_places = dist_stats.get("places", 0) or 0
score += min((dist_places / max(dist_starts, 1)) * 8, 8)
cond_stats = runner.stats.get("conditions", {})
for key, data in cond_stats.items():
c_starts = data.get("starts", 0) or 0
c_places = data.get("places", 0) or 0
score += min((c_places / max(c_starts, 1)) * 8, 8)
try:
barrier = int(runner.barrier) if runner.barrier else 5
score += max(0, 5 - abs(barrier - 5))
except Exception:
score += 3
try:
prize = float(str(runner.careerPrizeMoney).replace("$", "").replace(",", ""))
score += min(prize / 20000, 5)
except Exception:
pass
return min(round(score, 1), 100)
def _win_probability(self, runner: Runner) -> float:
overall = runner.stats.get("overall", {})
win_pct = overall.get("winPercent", 0) or 0
return min(max(round(win_pct * 100, 1), 0), 100)
def _place_probability(self, runner: Runner) -> float:
overall = runner.stats.get("overall", {})
place_pct = overall.get("placePercent", 0) or 0
return min(max(round(place_pct * 100, 1), 0), 100)
|