nexusbert commited on
Commit
847abec
verified
1 Parent(s): cc88923

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +224 -3
app.py CHANGED
@@ -68,6 +68,177 @@ def get_team_news(team: str, sport: str = "football"):
68
  headlines = [h.text for h in soup.select("h3")[:5]]
69
  return " ".join(headlines)
70
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
71
  @app.get("/soccer-predictions")
72
  def soccer_predictions():
73
  from datetime import timedelta
@@ -110,6 +281,8 @@ def soccer_predictions():
110
 
111
  for match in matches:
112
  home, away = match["homeTeam"]["name"], match["awayTeam"]["name"]
 
 
113
 
114
  home_news = get_team_news(home, "football")
115
  away_news = get_team_news(away, "football")
@@ -134,24 +307,50 @@ def soccer_predictions():
134
  emb_away = similarity_model.encode(away + " " + away_news, convert_to_tensor=True)
135
  similarity = util.pytorch_cos_sim(emb_home, emb_away).item()
136
 
 
 
 
 
 
 
 
 
 
 
 
137
  context = f"""
138
  Match: {home} vs {away}.
139
  Home news: {home_news}.
140
  Away news: {away_news}.
141
- Semantic similarity: {similarity}.
 
 
 
 
 
142
  """
143
  reasoning = reasoning_model(
144
- f"Predict 1X2 and Over/Under 2.5 goals for this soccer match. Context: {context}",
145
  max_length=256
146
  )[0]['generated_text']
147
 
148
  predictions.append({
149
  "league": league,
150
  "match": f"{home} vs {away}",
 
 
 
 
 
151
  "reason": reasoning,
 
152
  "news_summary": {
153
  home: home_sent,
154
  away: away_sent
 
 
 
 
155
  }
156
  })
157
 
@@ -281,6 +480,27 @@ def nba_predictions():
281
  "error": f"NBA API error: {str(e)}"
282
  }
283
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  for g in games:
285
  home, away = g["home_team"]["full_name"], g["visitor_team"]["full_name"]
286
 
@@ -309,13 +529,14 @@ def nba_predictions():
309
  Away news: {away_news}.
310
  """
311
  reasoning = reasoning_model(
312
- f"Predict winner and Over/Under points for this NBA game. Context: {context}",
313
  max_length=256
314
  )[0]['generated_text']
315
 
316
  predictions.append({
317
  "match": f"{home} vs {away}",
318
  "reason": reasoning,
 
319
  "news_summary": {
320
  home: home_sent,
321
  away: away_sent
 
68
  headlines = [h.text for h in soup.select("h3")[:5]]
69
  return " ".join(headlines)
70
 
71
+ # --- Helpers: NBA preseason scraping (very lightweight best-effort) ---
72
+ def _extract_pairs_from_texts(texts):
73
+ pairs = []
74
+ for t in texts:
75
+ s = t.strip()
76
+ if not s:
77
+ continue
78
+ lowered = s.lower()
79
+ if " vs " in lowered:
80
+ sep = " vs "
81
+ elif " v " in lowered:
82
+ sep = " v "
83
+ elif " at " in lowered:
84
+ sep = " at "
85
+ else:
86
+ continue
87
+ parts = s.split(sep, 1)
88
+ if len(parts) == 2:
89
+ home = parts[0].strip()
90
+ away = parts[1].strip()
91
+ if home and away:
92
+ pairs.append({"home": home, "away": away})
93
+ return pairs
94
+
95
+ def scrape_nba_preseason_pairs_for_dates(dates):
96
+ found = []
97
+ seen = set()
98
+ headers = {"User-Agent": "Mozilla/5.0"}
99
+ for d in dates:
100
+ q = f"NBA preseason {d}"
101
+ url = f"https://www.google.com/search?q={q}"
102
+ try:
103
+ r = requests.get(url, headers=headers, timeout=20)
104
+ soup = BeautifulSoup(r.text, "html.parser")
105
+ h3s = [h.text for h in soup.select("h3")[:20]]
106
+ pairs = _extract_pairs_from_texts(h3s)
107
+ for p in pairs:
108
+ key = (p["home"].lower(), p["away"].lower())
109
+ if key not in seen:
110
+ seen.add(key)
111
+ found.append(p)
112
+ except Exception:
113
+ continue
114
+ return found
115
+
116
+ # --- Verdict helpers ---
117
+ def build_soccer_verdict(home, away, agg):
118
+ oneXtwo = agg.get("oneXtwo", {})
119
+ over_under = agg.get("over_under", {})
120
+ pick_1x2 = max(oneXtwo, key=oneXtwo.get) if oneXtwo else "X"
121
+ conf_1x2 = oneXtwo.get(pick_1x2, 0.0)
122
+ ou_25 = over_under.get("2.5", {"over": 0.0, "under": 0.0})
123
+ pick_ou = "Over 2.5" if ou_25.get("over", 0.0) >= ou_25.get("under", 0.0) else "Under 2.5"
124
+ conf_ou = max(ou_25.get("over", 0.0), ou_25.get("under", 0.0))
125
+ return f"{home} vs {away}: {pick_1x2} ({conf_1x2:.2f}), {pick_ou} ({conf_ou:.2f})"
126
+
127
+ # --- Helpers: Football-Data last-5 and simple Poisson model ---
128
+ def football_headers():
129
+ return {"X-Auth-Token": FOOTBALL_API_KEY}
130
+
131
+ def fetch_last5_for_team(team_id: int):
132
+ try:
133
+ url = f"https://api.football-data.org/v4/teams/{team_id}/matches?status=FINISHED&limit=5"
134
+ r = requests.get(url, headers=football_headers(), timeout=20)
135
+ r.raise_for_status()
136
+ data = r.json()
137
+ return data.get("matches", [])
138
+ except Exception:
139
+ return []
140
+
141
+ def summarize_form(team_id: int, team_name: str):
142
+ matches = fetch_last5_for_team(team_id)
143
+ goals_for = 0
144
+ goals_against = 0
145
+ wins = 0
146
+ draws = 0
147
+ losses = 0
148
+ for m in matches:
149
+ ht = m.get("homeTeam", {})
150
+ at = m.get("awayTeam", {})
151
+ score = m.get("score", {}).get("fullTime", {})
152
+ hg = score.get("home") if score.get("home") is not None else 0
153
+ ag = score.get("away") if score.get("away") is not None else 0
154
+ is_home = (ht.get("id") == team_id)
155
+ gf = hg if is_home else ag
156
+ ga = ag if is_home else hg
157
+ goals_for += gf
158
+ goals_against += ga
159
+ if gf > ga:
160
+ wins += 1
161
+ elif gf == ga:
162
+ draws += 1
163
+ else:
164
+ losses += 1
165
+ games = max(1, len(matches))
166
+ avg_for = goals_for / games
167
+ avg_against = goals_against / games
168
+ return {
169
+ "team_id": team_id,
170
+ "team": team_name,
171
+ "matches_considered": len(matches),
172
+ "wins": wins,
173
+ "draws": draws,
174
+ "losses": losses,
175
+ "avg_goals_for": avg_for,
176
+ "avg_goals_against": avg_against,
177
+ }
178
+
179
+ def poisson_pmf(lmbda: float, k: int):
180
+ if lmbda <= 0:
181
+ lmbda = 0.0001
182
+ # simple Poisson PMF: e^-位 位^k / k!
183
+ from math import exp
184
+ # factorial up to 10 safely
185
+ fact = 1
186
+ for i in range(2, k + 1):
187
+ fact *= i
188
+ return (exp(-lmbda) * (lmbda ** k)) / fact
189
+
190
+ def match_score_prob(lambda_home: float, lambda_away: float, max_goals: int = 10):
191
+ matrix = []
192
+ for h in range(0, max_goals + 1):
193
+ row = []
194
+ for a in range(0, max_goals + 1):
195
+ row.append(poisson_pmf(lambda_home, h) * poisson_pmf(lambda_away, a))
196
+ matrix.append(row)
197
+ return matrix
198
+
199
+ def aggregate_outcomes(prob_matrix, thresholds=(0.5, 1.5, 2.5, 3.5)):
200
+ max_g = len(prob_matrix) - 1
201
+ p_home = 0.0
202
+ p_draw = 0.0
203
+ p_away = 0.0
204
+ ou = {t: {"over": 0.0, "under": 0.0} for t in thresholds}
205
+ total_prob = 0.0
206
+ for h in range(max_g + 1):
207
+ for a in range(max_g + 1):
208
+ p = prob_matrix[h][a]
209
+ total_prob += p
210
+ if h > a:
211
+ p_home += p
212
+ elif h == a:
213
+ p_draw += p
214
+ else:
215
+ p_away += p
216
+ s = h + a
217
+ for t in thresholds:
218
+ if s > t:
219
+ ou[t]["over"] += p
220
+ else:
221
+ ou[t]["under"] += p
222
+ # double chance
223
+ dc_1x = p_home + p_draw
224
+ dc_x2 = p_draw + p_away
225
+ # normalize tiny drift
226
+ if abs(total_prob - 1.0) > 1e-6:
227
+ scale = 1.0 / total_prob
228
+ p_home *= scale
229
+ p_draw *= scale
230
+ p_away *= scale
231
+ for t in thresholds:
232
+ ou[t]["over"] *= scale
233
+ ou[t]["under"] *= scale
234
+ dc_1x *= scale
235
+ dc_x2 *= scale
236
+ return {
237
+ "oneXtwo": {"1": p_home, "X": p_draw, "2": p_away},
238
+ "double_chance": {"1X": dc_1x, "X2": dc_x2},
239
+ "over_under": {str(t): ou[t] for t in thresholds}
240
+ }
241
+
242
  @app.get("/soccer-predictions")
243
  def soccer_predictions():
244
  from datetime import timedelta
 
281
 
282
  for match in matches:
283
  home, away = match["homeTeam"]["name"], match["awayTeam"]["name"]
284
+ home_id = match["homeTeam"].get("id")
285
+ away_id = match["awayTeam"].get("id")
286
 
287
  home_news = get_team_news(home, "football")
288
  away_news = get_team_news(away, "football")
 
307
  emb_away = similarity_model.encode(away + " " + away_news, convert_to_tensor=True)
308
  similarity = util.pytorch_cos_sim(emb_home, emb_away).item()
309
 
310
+ # Form summaries (last-5)
311
+ form_home = summarize_form(home_id, home) if home_id else None
312
+ form_away = summarize_form(away_id, away) if away_id else None
313
+
314
+ # Simple Poisson lambdas from form with home advantage factor
315
+ home_lambda = (form_home["avg_goals_for"] if form_home else 1.2) * 1.1
316
+ away_lambda = (form_away["avg_goals_for"] if form_away else 1.1) * 0.95
317
+ prob_matrix = match_score_prob(home_lambda, away_lambda)
318
+ agg = aggregate_outcomes(prob_matrix)
319
+
320
+ # LLM reasoning
321
  context = f"""
322
  Match: {home} vs {away}.
323
  Home news: {home_news}.
324
  Away news: {away_news}.
325
+ Similarity: {similarity:.3f}.
326
+ Home form (avg GF/GA): {(form_home['avg_goals_for'] if form_home else 'n/a')}/{(form_home['avg_goals_against'] if form_home else 'n/a')}.
327
+ Away form (avg GF/GA): {(form_away['avg_goals_for'] if form_away else 'n/a')}/{(form_away['avg_goals_against'] if form_away else 'n/a')}.
328
+ Model 1X2: {agg['oneXtwo']}.
329
+ O/U ladder: {agg['over_under']}.
330
+ Double chance: {agg['double_chance']}.
331
  """
332
  reasoning = reasoning_model(
333
+ f"Provide a concise betting-style verdict (1X2, O/U, double chance) with rationale given the context. Context: {context}",
334
  max_length=256
335
  )[0]['generated_text']
336
 
337
  predictions.append({
338
  "league": league,
339
  "match": f"{home} vs {away}",
340
+ "structured": {
341
+ "oneXtwo": agg["oneXtwo"],
342
+ "double_chance": agg["double_chance"],
343
+ "over_under": agg["over_under"],
344
+ },
345
  "reason": reasoning,
346
+ "verdict": build_soccer_verdict(home, away, agg),
347
  "news_summary": {
348
  home: home_sent,
349
  away: away_sent
350
+ },
351
+ "form": {
352
+ home: form_home,
353
+ away: form_away
354
  }
355
  })
356
 
 
480
  "error": f"NBA API error: {str(e)}"
481
  }
482
 
483
+ if not games:
484
+ # Try lightweight Google scraping for preseason fixtures
485
+ scraped_pairs = scrape_nba_preseason_pairs_for_dates(dates)
486
+ debug_info.append({
487
+ "note": "No NBA API games; attempted preseason scrape",
488
+ "dates_queried": dates,
489
+ "scraped_pairs": scraped_pairs
490
+ })
491
+ if not scraped_pairs:
492
+ return {
493
+ "date_range": f"{date_from} to {date_to}",
494
+ "predictions": [],
495
+ "debug_info": debug_info,
496
+ "total_predictions": 0
497
+ }
498
+ # Build stub games from scraped data
499
+ games = [{
500
+ "home_team": {"full_name": p["home"]},
501
+ "visitor_team": {"full_name": p["away"]}
502
+ } for p in scraped_pairs]
503
+
504
  for g in games:
505
  home, away = g["home_team"]["full_name"], g["visitor_team"]["full_name"]
506
 
 
529
  Away news: {away_news}.
530
  """
531
  reasoning = reasoning_model(
532
+ f"Provide a concise NBA verdict: likely winner and Over/Under guidance. Context: {context}",
533
  max_length=256
534
  )[0]['generated_text']
535
 
536
  predictions.append({
537
  "match": f"{home} vs {away}",
538
  "reason": reasoning,
539
+ "verdict": f"{home} vs {away}: {reasoning[:140]}...",
540
  "news_summary": {
541
  home: home_sent,
542
  away: away_sent