mc0117 commited on
Commit
bab510c
·
1 Parent(s): 1a3ab59

fixed ttl logic

Browse files
Files changed (2) hide show
  1. app.py +227 -55
  2. requirements.txt +1 -0
app.py CHANGED
@@ -13,24 +13,33 @@ import seaborn as sns
13
 
14
  from simulator import InningOutcomeDistribution, simulate_game
15
 
16
- _DATA_DIR = "/data" if os.path.isdir("/data") else "data"
17
- DATA_PATH = os.path.join(_DATA_DIR, "full_2025_season.parquet")
 
18
  _dist_cache: dict = {}
19
 
20
 
21
  def _ensure_data():
22
  if not os.path.exists(DATA_PATH):
23
  from huggingface_hub import hf_hub_download
 
24
  os.makedirs(_DATA_DIR, exist_ok=True)
25
- print(f"Downloading dataset to {DATA_PATH}...")
26
- hf_hub_download(
27
- repo_id="mc0117/mlb-models-storage",
28
- filename="full_2025_season.parquet",
29
- repo_type="dataset",
30
- local_dir=_DATA_DIR,
31
- local_dir_use_symlinks=False,
32
- token=os.environ.get("HF_TOKEN"),
33
- )
 
 
 
 
 
 
 
34
 
35
 
36
  def _get_dist(team: str) -> InningOutcomeDistribution:
@@ -143,65 +152,127 @@ def simulate_json(home_team: str, away_team: str, n_simulations: int) -> dict:
143
  }
144
 
145
 
146
- # MLB Stats API uses "AZ" for Arizona; everything else matches our abbreviations.
147
- _MLB_API_TO_ABBR = {"AZ": "ARI"}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
148
 
149
- _MLB_SCHEDULE_URL = "https://statsapi.mlb.com/api/v1/schedule?sportId=1&gameType=R&date={date}"
150
 
 
 
151
 
152
- # ── Endpoint 3: simulate_today ───────────────────────────────────────────────
153
- # Fetches today's MLB schedule and simulates every game.
154
 
155
- def simulate_today(n_simulations: int = 10000) -> dict:
156
- """
157
- Returns:
158
- {
159
- "date": str,
160
- "n_games": int,
161
- "games": [
162
- {
163
- "game_pk": int,
164
- "game_status": str, // "Preview" | "Live" | "Final"
165
- "home_team": str,
166
- "away_team": str,
167
- "home_win_pct": float,
168
- "away_win_pct": float,
169
- "home_avg_score": float,
170
- "away_avg_score": float,
171
- ... // full simulate_json payload per game
172
- },
173
- ...
174
- ]
175
- }
176
- """
177
- today = date.today().strftime("%Y-%m-%d")
178
- url = _MLB_SCHEDULE_URL.format(date=today)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  try:
181
  with urllib.request.urlopen(url, timeout=10) as resp:
182
  data = json.loads(resp.read())
183
  except Exception as e:
184
- return {"error": f"Failed to fetch MLB schedule: {e}", "date": today, "n_games": 0, "games": []}
185
 
186
  raw_games = data.get("dates", [{}])[0].get("games", []) if data.get("dates") else []
187
-
188
  _ensure_data()
189
 
190
  games_out = []
191
  for game in raw_games:
192
- home_abbr = game["teams"]["home"]["team"].get("abbreviation", "")
193
- away_abbr = game["teams"]["away"]["team"].get("abbreviation", "")
194
- home_abbr = _MLB_API_TO_ABBR.get(home_abbr, home_abbr)
195
- away_abbr = _MLB_API_TO_ABBR.get(away_abbr, away_abbr)
196
  status = game.get("status", {}).get("abstractGameState", "Unknown")
197
 
198
  if home_abbr not in MLB_TEAMS or away_abbr not in MLB_TEAMS:
199
  games_out.append({
200
- "game_pk": game.get("gamePk"),
201
  "game_status": status,
202
- "home_team": home_abbr,
203
- "away_team": away_abbr,
204
- "error": "Team abbreviation not found in simulation data",
205
  })
206
  continue
207
 
@@ -210,7 +281,94 @@ def simulate_today(n_simulations: int = 10000) -> dict:
210
  result["game_status"] = status
211
  games_out.append(result)
212
 
213
- return {"date": today, "n_games": len(games_out), "games": games_out}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
 
216
  # ── Gradio app ────────────────────────────────────────────────────────────────
@@ -245,12 +403,26 @@ with gr.Blocks(title="game-sim-v0 | MLB Game Simulator") as demo:
245
  api_name="simulate_json")
246
 
247
  with gr.Tab("Today's Games"):
248
- gr.Markdown("Fetches today's MLB schedule and simulates every game.")
249
  t_n = gr.Slider(minimum=1000, maximum=50000, step=1000, value=10000, label="Simulations per game")
250
- t_btn = gr.Button("Simulate Today's Games")
 
 
251
  t_out = gr.JSON(label="Results")
252
- t_btn.click(fn=simulate_today, inputs=[t_n], outputs=t_out,
253
- api_name="simulate_today")
 
 
 
 
 
 
 
 
 
 
 
 
254
 
255
  gr.Examples(
256
  examples=[["NYY", "LAD", 10000], ["BOS", "HOU", 5000], ["ATL", "PHI", 10000]],
 
13
 
14
  from simulator import InningOutcomeDistribution, simulate_game
15
 
16
+ _DATA_DIR = "/data" if os.path.isdir("/data") else "data"
17
+ _HF_REPO_ID = "mc0117/mlb-models-storage"
18
+ DATA_PATH = os.path.join(_DATA_DIR, "full_2026_season.parquet")
19
  _dist_cache: dict = {}
20
 
21
 
22
  def _ensure_data():
23
  if not os.path.exists(DATA_PATH):
24
  from huggingface_hub import hf_hub_download
25
+ from huggingface_hub.utils import EntryNotFoundError
26
  os.makedirs(_DATA_DIR, exist_ok=True)
27
+ filename = os.path.basename(DATA_PATH)
28
+ print(f"Downloading {filename} from HF Hub...")
29
+ try:
30
+ hf_hub_download(
31
+ repo_id=_HF_REPO_ID,
32
+ filename=filename,
33
+ repo_type="dataset",
34
+ local_dir=_DATA_DIR,
35
+ local_dir_use_symlinks=False,
36
+ token=os.environ.get("HF_TOKEN"),
37
+ )
38
+ except EntryNotFoundError:
39
+ raise RuntimeError(
40
+ f"{filename} not found on HF Hub. "
41
+ "Call update_season_data() to fetch and upload it first."
42
+ )
43
 
44
 
45
  def _get_dist(team: str) -> InningOutcomeDistribution:
 
152
  }
153
 
154
 
155
+ # MLB Stats API sometimes returns full names instead of abbreviations.
156
+ _TEAM_NAME_TO_ABBR = {
157
+ "Arizona Diamondbacks": "ARI", "Atlanta Braves": "ATL", "Baltimore Orioles": "BAL",
158
+ "Boston Red Sox": "BOS", "Chicago Cubs": "CHC", "Chicago White Sox": "CWS",
159
+ "Cincinnati Reds": "CIN", "Cleveland Guardians": "CLE","Colorado Rockies": "COL",
160
+ "Detroit Tigers": "DET", "Houston Astros": "HOU", "Kansas City Royals": "KC",
161
+ "Los Angeles Angels": "LAA", "Los Angeles Dodgers": "LAD","Miami Marlins": "MIA",
162
+ "Milwaukee Brewers": "MIL", "Minnesota Twins": "MIN", "New York Mets": "NYM",
163
+ "New York Yankees": "NYY", "Oakland Athletics": "OAK", "Athletics": "OAK",
164
+ "Philadelphia Phillies": "PHI","Pittsburgh Pirates": "PIT", "San Diego Padres": "SD",
165
+ "Seattle Mariners": "SEA", "San Francisco Giants": "SF","St. Louis Cardinals": "STL",
166
+ "Tampa Bay Rays": "TB", "Texas Rangers": "TEX", "Toronto Blue Jays": "TOR",
167
+ "Washington Nationals": "WSH",
168
+ # abbreviation-level overrides (API returns "AZ" for Arizona)
169
+ "AZ": "ARI",
170
+ }
171
 
172
+ _MLB_SCHEDULE_URL = "https://statsapi.mlb.com/api/v1/schedule?sportId=1&gameType=R&date={date}"
173
 
174
+ _CACHE_TTL = timedelta(hours=6)
175
+ _mem_cache: dict = {} # (date_str, n_sims) -> {"cached_at": datetime, "result": dict}
176
 
 
 
177
 
178
+ def _cache_hf_path(today: str, n_simulations: int) -> str:
179
+ return f"simulate_today_cache/{today}_{n_simulations}.json"
180
+
181
+
182
+ def _load_cache(today: str, n_simulations: int) -> dict | None:
183
+ key = (today, n_simulations)
184
+ now = datetime.now(timezone.utc)
185
+
186
+ entry = _mem_cache.get(key)
187
+ if entry and now - entry["cached_at"] < _CACHE_TTL:
188
+ return entry["result"]
189
+
190
+ try:
191
+ from huggingface_hub import hf_hub_download
192
+ local_path = hf_hub_download(
193
+ repo_id=_HF_REPO_ID,
194
+ filename=_cache_hf_path(today, n_simulations),
195
+ repo_type="dataset",
196
+ token=os.environ.get("HF_TOKEN"),
197
+ force_download=True,
198
+ )
199
+ with open(local_path) as f:
200
+ stored = json.load(f)
201
+ cached_at = datetime.fromisoformat(stored["cached_at"])
202
+ if cached_at.tzinfo is None:
203
+ cached_at = cached_at.replace(tzinfo=timezone.utc)
204
+ if now - cached_at < _CACHE_TTL:
205
+ _mem_cache[key] = {"cached_at": cached_at, "result": stored["result"]}
206
+ return stored["result"]
207
+ except Exception:
208
+ pass
209
+
210
+ return None
211
+
212
+
213
+ def _save_cache(today: str, n_simulations: int, result: dict) -> None:
214
+ key = (today, n_simulations)
215
+ now = datetime.now(timezone.utc)
216
+ _mem_cache[key] = {"cached_at": now, "result": result}
217
+
218
+ payload = {"cached_at": now.isoformat(), "result": result}
219
+ tmp_path = None
220
+ try:
221
+ from huggingface_hub import upload_file
222
+ with tempfile.NamedTemporaryFile(mode="w", suffix=".json", delete=False) as f:
223
+ json.dump(payload, f)
224
+ tmp_path = f.name
225
+ upload_file(
226
+ path_or_fileobj=tmp_path,
227
+ path_in_repo=_cache_hf_path(today, n_simulations),
228
+ repo_id=_HF_REPO_ID,
229
+ repo_type="dataset",
230
+ token=os.environ.get("HF_TOKEN"),
231
+ )
232
+ except Exception as e:
233
+ print(f"Cache upload failed: {e}")
234
+ finally:
235
+ if tmp_path:
236
+ try:
237
+ os.unlink(tmp_path)
238
+ except Exception:
239
+ pass
240
+
241
 
242
+ # ── Endpoint 3 & 4: simulate_today / simulate_today_refresh ─────────────────
243
+ # simulate_today — returns cached result if within 6 h, else recomputes.
244
+ # simulate_today_refresh — always recomputes and overwrites the cache.
245
+
246
+ def _resolve_abbr(info: dict) -> str:
247
+ raw = info.get("abbreviation") or info.get("name", "")
248
+ return _TEAM_NAME_TO_ABBR.get(raw, raw)
249
+
250
+
251
+ def _compute_today(n_simulations: int) -> dict:
252
+ today = date.today().strftime("%Y-%m-%d")
253
+ url = _MLB_SCHEDULE_URL.format(date=today)
254
  try:
255
  with urllib.request.urlopen(url, timeout=10) as resp:
256
  data = json.loads(resp.read())
257
  except Exception as e:
258
+ return {"error": f"Failed to fetch MLB schedule: {e}", "date": today, "n_games": 0, "cached": False, "cached_at": None, "games": []}
259
 
260
  raw_games = data.get("dates", [{}])[0].get("games", []) if data.get("dates") else []
 
261
  _ensure_data()
262
 
263
  games_out = []
264
  for game in raw_games:
265
+ home_abbr = _resolve_abbr(game["teams"]["home"]["team"])
266
+ away_abbr = _resolve_abbr(game["teams"]["away"]["team"])
 
 
267
  status = game.get("status", {}).get("abstractGameState", "Unknown")
268
 
269
  if home_abbr not in MLB_TEAMS or away_abbr not in MLB_TEAMS:
270
  games_out.append({
271
+ "game_pk": game.get("gamePk"),
272
  "game_status": status,
273
+ "home_team": home_abbr,
274
+ "away_team": away_abbr,
275
+ "error": "Team abbreviation not found in simulation data",
276
  })
277
  continue
278
 
 
281
  result["game_status"] = status
282
  games_out.append(result)
283
 
284
+ core = {"date": today, "n_games": len(games_out), "games": games_out}
285
+ _save_cache(today, n_simulations, core)
286
+ return {**core, "cached": False, "cached_at": None}
287
+
288
+
289
+ def simulate_today(n_simulations: int = 10000) -> dict:
290
+ today = date.today().strftime("%Y-%m-%d")
291
+ cached = _load_cache(today, n_simulations)
292
+ if cached is not None:
293
+ entry = _mem_cache.get((today, n_simulations))
294
+ return {**cached, "cached": True, "cached_at": entry["cached_at"].isoformat() if entry else None}
295
+ return _compute_today(n_simulations)
296
+
297
+
298
+ def simulate_today_refresh(n_simulations: int = 10000) -> dict:
299
+ today = date.today().strftime("%Y-%m-%d")
300
+ key = (today, n_simulations)
301
+ _mem_cache.pop(key, None)
302
+ return _compute_today(n_simulations)
303
+
304
+
305
+ # ── Endpoint 4: update_season_data ──────────────────────────────────────────
306
+ # Pulls fresh Statcast data for the given season, saves parquet, uploads to HF.
307
+
308
+ def update_season_data(season_year: int = 2026) -> dict:
309
+ """
310
+ Fetches all Statcast data for `season_year` from Baseball Savant via
311
+ pybaseball, saves a trimmed parquet locally, and uploads it to HF Hub.
312
+ Clears the distribution cache so the next simulation uses fresh data.
313
+
314
+ Returns:
315
+ {
316
+ "status": "ok" | "error",
317
+ "season_year": int,
318
+ "rows": int,
319
+ "filename": str,
320
+ "message": str
321
+ }
322
+ """
323
+ try:
324
+ import pybaseball
325
+ pybaseball.cache.enable()
326
+ except ImportError:
327
+ return {"status": "error", "message": "pybaseball is not installed. Add it to requirements.txt."}
328
+
329
+ start_dt = f"{season_year}-03-01"
330
+ end_dt = date.today().strftime("%Y-%m-%d")
331
+ filename = f"full_{season_year}_season.parquet"
332
+ local_path = os.path.join(_DATA_DIR, filename)
333
+
334
+ print(f"Fetching Statcast data {start_dt} → {end_dt} (this takes a few minutes)...")
335
+ try:
336
+ df = pybaseball.statcast(start_dt=start_dt, end_dt=end_dt, verbose=True)
337
+ except Exception as e:
338
+ return {"status": "error", "message": f"pybaseball fetch failed: {e}"}
339
+
340
+ keep = ["events", "home_team", "away_team", "inning_topbot", "game_date"]
341
+ df = df[keep].copy()
342
+ df["home_team"] = df["home_team"].str.upper()
343
+ df["away_team"] = df["away_team"].str.upper()
344
+
345
+ os.makedirs(_DATA_DIR, exist_ok=True)
346
+ df.to_parquet(local_path, index=False)
347
+ print(f"Saved {len(df):,} rows to {local_path}")
348
+
349
+ try:
350
+ from huggingface_hub import upload_file
351
+ upload_file(
352
+ path_or_fileobj=local_path,
353
+ path_in_repo=filename,
354
+ repo_id=_HF_REPO_ID,
355
+ repo_type="dataset",
356
+ token=os.environ.get("HF_TOKEN"),
357
+ )
358
+ except Exception as e:
359
+ return {"status": "error", "message": f"HF Hub upload failed: {e}", "rows": len(df), "filename": filename}
360
+
361
+ global DATA_PATH
362
+ DATA_PATH = local_path
363
+ _dist_cache.clear()
364
+
365
+ return {
366
+ "status": "ok",
367
+ "season_year": season_year,
368
+ "rows": len(df),
369
+ "filename": filename,
370
+ "message": f"Uploaded {filename} ({len(df):,} rows) to HF Hub and refreshed dist cache.",
371
+ }
372
 
373
 
374
  # ── Gradio app ────────────────────────────────────────────────────────────────
 
403
  api_name="simulate_json")
404
 
405
  with gr.Tab("Today's Games"):
406
+ gr.Markdown("Fetches today's MLB schedule and simulates every game. Results cached 6 h in HF Hub.")
407
  t_n = gr.Slider(minimum=1000, maximum=50000, step=1000, value=10000, label="Simulations per game")
408
+ with gr.Row():
409
+ t_btn = gr.Button("Simulate Today's Games")
410
+ tr_btn = gr.Button("Force Refresh", variant="secondary")
411
  t_out = gr.JSON(label="Results")
412
+ t_btn.click(fn=simulate_today, inputs=[t_n], outputs=t_out, api_name="simulate_today")
413
+ tr_btn.click(fn=simulate_today_refresh, inputs=[t_n], outputs=t_out, api_name="simulate_today_refresh")
414
+
415
+ with gr.Tab("Update Season Data"):
416
+ gr.Markdown(
417
+ "Pulls fresh Statcast data from Baseball Savant via **pybaseball**, "
418
+ "saves a trimmed parquet, and uploads it to HF Hub. "
419
+ "⚠️ Takes several minutes for a full season."
420
+ )
421
+ u_year = gr.Number(value=2026, label="Season year", precision=0)
422
+ u_btn = gr.Button("Fetch & Upload")
423
+ u_out = gr.JSON(label="Status")
424
+ u_btn.click(fn=update_season_data, inputs=[u_year], outputs=u_out,
425
+ api_name="update_season_data")
426
 
427
  gr.Examples(
428
  examples=[["NYY", "LAD", 10000], ["BOS", "HOU", 5000], ["ATL", "PHI", 10000]],
requirements.txt CHANGED
@@ -3,3 +3,4 @@ pyarrow
3
  matplotlib
4
  seaborn
5
  huggingface_hub
 
 
3
  matplotlib
4
  seaborn
5
  huggingface_hub
6
+ pybaseball