Trifecta-Lab / src /prediction_model.py
Brettapps's picture
Upload folder using huggingface_hub (part 21)
e23172f verified
Raw History Blame Contribute Delete
4.53 kB
"""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)