Spaces:
Configuration error
Configuration error
Download trifecta_bro/model/probability.py from Brettapps/Trifecta-Lab: direct link, hf CLI and curl.
- Browser
- Download file 4.99 kB
-
https://huggingface.co/spaces/Brettapps/Trifecta-Lab/resolve/main/trifecta_bro/model/probability.py
- Command line
-
hf download hf://spaces/Brettapps/Trifecta-Lab/trifecta_bro/model/probability.py
-
curl -L -o probability.py https://huggingface.co/spaces/Brettapps/Trifecta-Lab/resolve/main/trifecta_bro/model/probability.py
4.99 kB
| """Probability, trifecta generation, and confidence. | |
| Deterministic / statistical only — no LLM involvement. Probabilities are model | |
| estimates (softmax over scores), explicitly NOT bookmaker odds, and are labelled | |
| as such in all output. | |
| """ | |
| from __future__ import annotations | |
| import math | |
| from collections import OrderedDict | |
| from typing import Optional | |
| def softmax_probabilities(scores: list[float], temperature: float = 1.0) -> list[float]: | |
| if not scores: | |
| return [] | |
| mx = max(scores) | |
| exps = [math.exp((s - mx) / temperature) for s in scores] | |
| total = sum(exps) or 1.0 | |
| return [e / total for e in exps] | |
| def joint_prob(first: int, second: int, third: int, | |
| runner_probs: dict[int, float], order: list[int]) -> float: | |
| """Probability that `order` (a permutation of first/second/third) lands, | |
| under a simple first-order model: P(1st) * P(2nd | not 1st) * P(3rd | not 1,2).""" | |
| nums = [first, second, third] | |
| perm = list(order) | |
| p = 1.0 | |
| excluded = set() | |
| for pos, pick in enumerate(perm): | |
| avail = [n for n in nums if n not in excluded] | |
| # P(pick | available) proportional to its standalone prob among available | |
| avail_p = {n: runner_probs.get(n, 0.0) for n in avail} | |
| s = sum(avail_p.values()) or 1.0 | |
| p *= avail_p.get(pick, 0.0) / s | |
| excluded.add(pick) | |
| return p | |
| def generate_trifecta(ranked: list[dict], weights_cfg: dict) -> dict: | |
| """ranked: list of {number, score, win_est, place_est} sorted desc by score. | |
| Returns primary, top3, alternatives (savers), and probabilities.""" | |
| if len(ranked) < 3: | |
| return {"skippable": True, "reason": "fewer than 3 active runners"} | |
| # standalone per-runner win probabilities (softmax over scores) | |
| nums = [r["number"] for r in ranked] | |
| scores = [r["score"] for r in ranked] | |
| probs = softmax_probabilities(scores, temperature=12.0) | |
| runner_probs = {n: p for n, p in zip(nums, probs)} | |
| top3 = [ranked[0]["number"], ranked[1]["number"], ranked[2]["number"]] | |
| primary = list(top3) | |
| # enumerate saver permutations of the top-K (K=4) and score them | |
| k = min(4, len(ranked)) | |
| pool = [r["number"] for r in ranked[:k]] | |
| from itertools import permutations | |
| candidates = [] | |
| for order in permutations(pool): | |
| if order == tuple(primary): | |
| continue | |
| pr = joint_prob(primary[0], primary[1], primary[2], runner_probs, order) | |
| candidates.append((list(order), pr)) | |
| candidates.sort(key=lambda x: x[1], reverse=True) | |
| primary_prob = joint_prob(primary[0], primary[1], primary[2], runner_probs, tuple(primary)) | |
| max_ratio = weights_cfg.get("min_saver_prob_ratio", 0.18) | |
| max_savers = weights_cfg.get("max_savers", 6) | |
| savers = [c for c in candidates if c[1] >= primary_prob * max_ratio][:max_savers] | |
| return { | |
| "primary": primary, | |
| "top3": top3, | |
| "alternatives": [c[0] for c in savers], | |
| "primary_prob": round(primary_prob, 4), | |
| "saver_probs": [round(c[1], 4) for c in savers], | |
| "runner_probs": {str(k): round(v, 4) for k, v in runner_probs.items()}, | |
| } | |
| def compute_confidence(ranked: list[dict], race_completeness: float, | |
| pace: dict, weights_cfg: dict) -> dict: | |
| if len(ranked) < 3: | |
| return {"category": "AVOID", "score": 0, | |
| "reason": "Insufficient active runners for a trifecta."} | |
| conf = weights_cfg | |
| leader = ranked[0]["score"] | |
| if leader < weights_cfg.get("min_leader_score_for_prediction", 30.0): | |
| return {"category": "AVOID", "score": int(leader), | |
| "reason": "Leader score below threshold — data too weak to predict."} | |
| sep = ranked[0]["score"] - ranked[1]["score"] | |
| # competitive runners within `competitive_band` of leader | |
| band = conf.get("competitive_band", 8.0) | |
| competitive = sum(1 for r in ranked if leader - r["score"] <= band) | |
| if (sep >= conf.get("high_separation", 18.0) | |
| and race_completeness >= conf.get("high_min_completeness", 0.85) | |
| and competitive <= conf.get("max_competitive_runners_for_high", 3)): | |
| cat, score = "HIGH", min(100, 70 + int(sep) + int(race_completeness * 15)) | |
| elif (sep >= conf.get("medium_separation", 9.0) | |
| and race_completeness >= conf.get("medium_min_completeness", 0.6) | |
| and competitive <= conf.get("max_competitive_runners_for_medium", 6)): | |
| cat, score = "MEDIUM", min(75, 50 + int(sep) + int(race_completeness * 10)) | |
| elif sep < 4.0 or race_completeness < 0.35: | |
| cat, score = "AVOID", min(35, int(leader)) | |
| else: | |
| cat, score = "LOW", min(50, 30 + int(sep)) | |
| score = max(0, min(100, score)) | |
| return { | |
| "category": cat, | |
| "score": score, | |
| "leader_score": round(leader, 1), | |
| "separation": round(sep, 1), | |
| "competitive_runners": competitive, | |
| "data_completeness": round(race_completeness, 2), | |
| "pace": pace.get("label"), | |
| } | |