Airmanv/NFL3.1
0
1import streamlit as st2import pandas as pd3import numpy as np4import nfl_data_py as nfl5from sklearn.ensemble import RandomForestClassifier6from sklearn.preprocessing import StandardScaler7from sklearn.pipeline import Pipeline8from sklearn.model_selection import train_test_split9import joblib10import warnings11from math import radians, sin, cos, sqrt, atan212import requests13import datetime14import os15import time16 17# --- PAGE CONFIG ---18st.set_page_config(page_title="Kevin's NFL Forecast Tool v3.1", page_icon="๐")19 20# --- CENTER LOGO ---21logo_col1, logo_col2, logo_col3 = st.columns([1, 2, 1])22with logo_col2:23 st.image("Airmanv.png", width=220)24 25# --- SINGLE-LINE CENTERED TITLE ---26st.markdown(27 """28 <h1 style='text-align: center; color: white; font-size: 40px; margin-top: -10px;'>29 ๐ Kevin's NFL Forecast Tool v3.130 </h1>31 """,32 unsafe_allow_html=True33)34 35 36# --- API SETUP ---37ODDS_API_KEY = os.getenv("ODDS_API_KEY")38ODDS_API_URL = "https://api.the-odds-api.com/v4/sports/americanfootball_nfl/odds"39 40if not ODDS_API_KEY:41 st.error("โ ๏ธ API Key not found. Please set ODDS_API_KEY in Settings > Secrets.")42 43# --- SHARED CONSTANTS ---44TEAM_ABBR_MAP = {45 "ARI": "ARI", "ATL": "ATL", "BAL": "BAL", "BUF": "BUF", "CAR": "CAR", "CHI": "CHI",46 "CIN": "CIN", "CLE": "CLE", "DAL": "DAL", "DEN": "DEN", "DET": "DET", "GB": "GB",47 "HOU": "HOU", "IND": "IND", "JAX": "JAX", "KC": "KC", "LV": "LV", "LAC": "LAC",48 "LAR": "LAR", "MIA": "MIA", "MIN": "MIN", "NE": "NE", "NO": "NO", "NYG": "NYG",49 "NYJ": "NYJ", "PHI": "PHI", "PIT": "PIT", "SEA": "SEA", "SF": "SF", "TB": "TB",50 "TEN": "TEN", "WAS": "WAS",51}52 53ODDS_API_TEAM_NAMES = {54 "ARI": "Arizona Cardinals", "ATL": "Atlanta Falcons", "BAL": "Baltimore Ravens",55 "BUF": "Buffalo Bills", "CAR": "Carolina Panthers", "CHI": "Chicago Bears",56 "CIN": "Cincinnati Bengals", "CLE": "Cleveland Browns", "DAL": "Dallas Cowboys",57 "DEN": "Denver Broncos", "DET": "Detroit Lions", "GB": "Green Bay Packers",58 "HOU": "Houston Texans", "IND": "Indianapolis Colts", "JAX": "Jacksonville Jaguars",59 "KC": "Kansas City Chiefs", "LA": "Los Angeles Rams", "LAC": "Los Angeles Chargers",60 "LV": "Las Vegas Raiders", "MIA": "Miami Dolphins", "MIN": "Minnesota Vikings",61 "NE": "New England Patriots", "NO": "New Orleans Saints", "NYG": "New York Giants",62 "NYJ": "New York Jets", "PHI": "Philadelphia Eagles", "PIT": "Pittsburgh Steelers",63 "SEA": "Seattle Seahawks", "SF": "San Francisco 49ers", "TB": "Tampa Bay Buccaneers",64 "TEN": "Tennessee Titans", "WAS": "Washington Commanders",65}66 67stadiums = pd.DataFrame({68 "team_abbr": ["ARI","ATL","BAL","BUF","CAR","CHI","CIN","CLE","DAL","DEN","DET","GB","HOU","IND","JAX","KC","LV","LAC","LAR","MIA","MIN","NE","NO","NYG","NYJ","PHI","PIT","SEA","SF","TB","TEN","WAS"],69 "latitude": [33.5275,33.7550,39.2787,42.7737,35.2258,41.8625,39.0954,41.5061,32.7473,39.7439,42.3400,44.5013,29.6847,39.7601,30.3240,39.0490,36.0909,33.9535,34.0140,25.9580,44.9740,42.0909,29.9511,40.8128,40.8135,39.9008,40.4469,47.5952,37.4030,27.9759,36.1663,38.9078],70 "longitude": [-112.2626,-84.3915,-76.6227,-78.7868,-80.8528,-87.6167,-84.5160,-81.6995,-97.0945,-105.0201,-83.0458,-88.0622,-95.4107,-86.1637,-81.6377,-94.4839,-115.1833,-118.3391,-118.2879,-80.2389,-93.2577,-71.2643,-90.0812,-74.0743,-74.0743,-75.1675,-80.0158,-122.3316,-122.0829,-82.5034,-86.7713,-77.0074]71})72team_coords = stadiums.set_index("team_abbr")[["latitude", "longitude"]].to_dict("index")73 74_odds_cache = {}75 76# --- HELPER: HAVERSINE ---77def haversine(lat1, lon1, lat2, lon2):78 R = 3958.879 dlat = radians(lat2 - lat1)80 dlon = radians(lon2 - lon1)81 a = sin(dlat / 2)**2 + cos(radians(lat1)) * cos(radians(lat2)) * sin(dlon / 2)**282 return 2 * R * atan2(sqrt(a), sqrt(1 - a))83 84# --- ODDS FUNCTIONS (PORTED FROM LOCAL SCRIPT) ---85def fetch_oddsapi_draftkings_odds(home_team_abbr, away_team_abbr):86 """Fetch odds from The Odds API (DraftKings-style), similar to local script."""87 home_abbr = home_team_abbr.upper()88 away_abbr = away_team_abbr.upper()89 90 cache_key = ("ODDS_API_DK", home_abbr, away_abbr)91 if cache_key in _odds_cache:92 return _odds_cache[cache_key]93 94 home_full = ODDS_API_TEAM_NAMES.get(home_abbr)95 away_full = ODDS_API_TEAM_NAMES.get(away_abbr)96 if not home_full or not away_full:97 return None98 99 params = {100 "apiKey": ODDS_API_KEY,101 "regions": "us",102 "markets": "h2h,spreads,totals",103 "oddsFormat": "american",104 }105 106 try:107 resp = requests.get(ODDS_API_URL, params=params, timeout=10)108 if resp.status_code != 200:109 return None110 data = resp.json()111 except Exception:112 return None113 114 game_obj = None115 for g in data:116 if g.get("home_team") == home_full and g.get("away_team") == away_full:117 game_obj = g118 break119 120 if game_obj is None:121 return None122 123 bookmakers = game_obj.get("bookmakers", [])124 dk = None125 for b in bookmakers:126 title = (b.get("title") or "").lower()127 if b.get("key") == "draftkings" or title.startswith("draftkings"):128 dk = b129 break130 if dk is None and bookmakers:131 dk = bookmakers[0]132 133 spread_line = None134 over_under_line = None135 ml_home = None136 ml_away = None137 138 if dk:139 for m in dk.get("markets", []):140 mkey = m.get("key")141 outcomes = m.get("outcomes", [])142 if mkey == "spreads":143 for o in outcomes:144 if o.get("name") == home_full:145 try:146 spread_line = float(o.get("point"))147 except Exception:148 pass149 elif mkey == "totals":150 for o in outcomes:151 name = (o.get("name") or "").lower()152 if name.startswith("over"):153 try:154 over_under_line = float(o.get("point"))155 except Exception:156 pass157 elif mkey == "h2h":158 for o in outcomes:159 if o.get("name") == home_full:160 ml_home = o.get("price")161 elif o.get("name") == away_full:162 ml_away = o.get("price")163 164 result = {165 "spread_line": spread_line,166 "over_under_line": over_under_line,167 "ml_home": ml_home,168 "ml_away": ml_away,169 }170 _odds_cache[cache_key] = result171 return result172 173def fetch_combined_odds(home_team_abbr, away_team_abbr):174 odds = fetch_oddsapi_draftkings_odds(home_team_abbr, away_team_abbr)175 if odds is not None:176 return odds177 return None178 179# --- DATA LOAD FUNCTION ---180@st.cache_data181def load_nfl_data():182 today_dt = datetime.date.today()183 current_season = today_dt.year if today_dt.month >= 9 else today_dt.year - 1184 185 games = nfl.import_schedules([current_season])186 historical_games = nfl.import_schedules(range(2010, current_season + 1))187 188 189 date_col = "game_date" if "game_date" in historical_games.columns else "gameday"190 cols_to_keep = [191 "game_id", "season", "week", "home_team", "away_team",192 "home_score", "away_score", date_col193 ]194 for col in ["spread_line", "over_under_line", "over_under_line_close"]:195 if col in historical_games.columns:196 cols_to_keep.append(col)197 198 df = historical_games[cols_to_keep].copy()199 df = df.dropna(subset=["home_score", "away_score"])200 df["game_date"] = pd.to_datetime(df[date_col])201 df["home_win"] = (df["home_score"] > df["away_score"]).astype(int)202 df["point_diff"] = df["home_score"] - df["away_score"]203 204 if "spread_line" not in df.columns:205 df["spread_line"] = 0.0206 if "over_under_line" not in df.columns:207 df["over_under_line"] = df["point_diff"].abs().mean()208 209 # Rest days210 df = df.sort_values(["season", "week"])211 rest_records = []212 team_last_game = {}213 for _, row in df.iterrows():214 ht, at = row["home_team"], row["away_team"]215 gd = row["game_date"]216 home_rest = (gd - team_last_game[ht]).days if ht in team_last_game else 7217 away_rest = (gd - team_last_game[at]).days if at in team_last_game else 7218 team_last_game[ht] = gd219 team_last_game[at] = gd220 rest_records.append({221 "season": row["season"],222 "home_team": ht,223 "away_team": at,224 "home_rest_days": home_rest,225 "away_rest_days": away_rest226 })227 rest_df = pd.DataFrame(rest_records)228 df = pd.merge(df, rest_df, on=["season", "home_team", "away_team"], how="left")229 230 # Travel distance231 df["travel_distance"] = [232 haversine(233 team_coords[row["home_team"]]["latitude"],234 team_coords[row["home_team"]]["longitude"],235 team_coords[row["away_team"]]["latitude"],236 team_coords[row["away_team"]]["longitude"],237 )238 if row["home_team"] in team_coords and row["away_team"] in team_coords239 else np.nan240 for _, row in df.iterrows()241 ]242 243 # Cover target244 df["home_cover"] = ((df["home_score"] - df["away_score"]) > (-df["spread_line"])).astype(int)245 246 # Rolling team stats247 records = []248 for season in df["season"].unique():249 season_df = df[df["season"] == season].copy()250 team_stats = {}251 for _, row in season_df.iterrows():252 ht, at = row["home_team"], row["away_team"]253 hs, as_ = row["home_score"], row["away_score"]254 for t in [ht, at]:255 if t not in team_stats:256 team_stats[t] = {"wins": 0, "games": 0, "point_diff": 0}257 team_stats[ht]["games"] += 1258 team_stats[at]["games"] += 1259 team_stats[ht]["point_diff"] += (hs - as_)260 team_stats[at]["point_diff"] += (as_ - hs)261 if hs > as_:262 team_stats[ht]["wins"] += 1263 else:264 team_stats[at]["wins"] += 1265 records.append({266 "season": season,267 "home_team": ht,268 "away_team": at,269 "home_win_pct": team_stats[ht]["wins"] / team_stats[ht]["games"],270 "away_win_pct": team_stats[at]["wins"] / team_stats[at]["games"],271 "home_point_diff": team_stats[ht]["point_diff"],272 "away_point_diff": team_stats[at]["point_diff"]273 })274 275 df_features = pd.DataFrame(records)276 df = pd.merge(df, df_features, on=["season", "home_team", "away_team"], how="left")277 278 return games, df, current_season279 280with st.spinner("Downloading NFL Data..."):281 games, df, current_season = load_nfl_data()282 283# --- MODEL TRAINING - MATCH LOCAL SCRIPT (TRAIN/TEST SPLIT) ---284@st.cache_resource285def load_models(df):286 features = [287 "home_win_pct", "away_win_pct",288 "home_point_diff", "away_point_diff",289 "home_rest_days", "away_rest_days",290 "travel_distance", "spread_line", "over_under_line"291 ]292 293 df_model = df.dropna(subset=features + ["home_win", "home_cover"])294 X = df_model[features]295 y_win = df_model["home_win"]296 y_cover = df_model["home_cover"]297 298 X_train, X_test, y_train, y_test = train_test_split(299 X, y_win, test_size=0.25, random_state=42300 )301 Xc_train, Xc_test, yc_train, yc_test = train_test_split(302 X, y_cover, test_size=0.25, random_state=42303 )304 305 pipe_win = Pipeline([306 ("scaler", StandardScaler()),307 ("rf", RandomForestClassifier(n_estimators=300, random_state=42, n_jobs=-1))308 ])309 pipe_cover = Pipeline([310 ("scaler", StandardScaler()),311 ("rf", RandomForestClassifier(n_estimators=300, random_state=42, n_jobs=-1))312 ])313 314 pipe_win.fit(X_train, y_train)315 pipe_cover.fit(Xc_train, yc_train)316 317 win_acc = pipe_win.score(X_test, y_test)318 cover_acc = pipe_cover.score(Xc_test, yc_test)319 320 return pipe_win, pipe_cover, win_acc, cover_acc321 322with st.spinner("Training models..."):323 model_win, model_cover, win_acc, cover_acc = load_models(df)324 325# --- FEATURE COMPUTATION FOR A MATCHUP ---326def compute_matchup_features(home_team, away_team):327 home_team = home_team.upper()328 away_team = away_team.upper()329 330 mask = (games["home_team"] == home_team) & (games["away_team"] == away_team)331 if not mask.any():332 mask_hist = (df["home_team"] == home_team) & (df["away_team"] == away_team)333 if mask_hist.any():334 game_row = df[mask_hist].iloc[-1]335 else:336 raise ValueError(f"No game found for {away_team} at {home_team}.")337 else:338 game_row = games[mask].iloc[-1]339 340 season = int(game_row["season"])341 d_col = "game_date" if "game_date" in game_row.index else "gameday"342 game_date = pd.to_datetime(game_row[d_col])343 344 past_games = df[(df["season"] == season) & (df["game_date"] < game_date)].copy()345 past_games = past_games.sort_values("game_date")346 347 team_stats = {}348 349 def init_team(t):350 if t not in team_stats:351 team_stats[t] = {"wins": 0, "games": 0, "point_diff": 0, "last_game": None}352 353 for _, row in past_games.iterrows():354 ht, at = row["home_team"], row["away_team"]355 hs, as_ = row["home_score"], row["away_score"]356 gd = row["game_date"]357 for t in (ht, at):358 init_team(t)359 team_stats[t]["games"] += 1360 team_stats[t]["last_game"] = gd361 team_stats[ht]["point_diff"] += (hs - as_)362 team_stats[at]["point_diff"] += (as_ - hs)363 if hs > as_:364 team_stats[ht]["wins"] += 1365 else:366 team_stats[at]["wins"] += 1367 368 def derive_features(team):369 if team not in team_stats or team_stats[team]["games"] == 0:370 return {"win_pct": 0.0, "point_diff": 0.0, "rest_days": 7.0}371 s = team_stats[team]372 win_pct = s["wins"] / s["games"]373 point_diff = s["point_diff"]374 rest_days = 7.0 if s["last_game"] is None else float(375 (game_date.normalize() - s["last_game"].normalize()).days376 )377 return {"win_pct": win_pct, "point_diff": point_diff, "rest_days": rest_days}378 379 hs = derive_features(home_team)380 as_ = derive_features(away_team)381 382 td = 0.0383 if home_team in team_coords and away_team in team_coords:384 td = haversine(385 team_coords[home_team]["latitude"], team_coords[home_team]["longitude"],386 team_coords[away_team]["latitude"], team_coords[away_team]["longitude"]387 )388 389 return {390 "home_win_pct": hs["win_pct"], "away_win_pct": as_["win_pct"],391 "home_point_diff": hs["point_diff"], "away_point_diff": as_["point_diff"],392 "home_rest_days": hs["rest_days"], "away_rest_days": as_["rest_days"],393 "travel_distance": td394 }395 396def get_single_game_stats(ht, at):397 try:398 f = compute_matchup_features(ht, at)399 except Exception:400 return None401 return f402 403# ------------------------404# UI LAYOUT405# ------------------------406tab1, tab2 = st.tabs(["Single Game Prediction", "Upcoming Week Dump"])407 408# TAB 1 - SINGLE GAME PREDICTION409with tab1:410 st.subheader("Predict Single Game")411 412 col1, col2 = st.columns(2)413 with col1:414 home_team = st.selectbox("Home Team", sorted(TEAM_ABBR_MAP.keys()))415 with col2:416 away_team = st.selectbox("Away Team", sorted(TEAM_ABBR_MAP.keys()), index=1)417 418 if st.button("Analyze Matchup"):419 with st.spinner("Crunching numbers..."):420 stats = get_single_game_stats(home_team, away_team)421 if not stats:422 st.error("No schedule or historical game found for this matchup.")423 else:424 s_col1, s_col2 = st.columns(2)425 s_col1.info(426 f"**HOME ({home_team})**\n\n"427 f"Win%: {stats['home_win_pct']:.3f}\n\n"428 f"Diff: {stats['home_point_diff']:.1f}\n\n"429 f"Rest: {stats['home_rest_days']:.1f}"430 )431 s_col2.info(432 f"**AWAY ({away_team})**\n\n"433 f"Win%: {stats['away_win_pct']:.3f}\n\n"434 f"Diff: {stats['away_point_diff']:.1f}\n\n"435 f"Rest: {stats['away_rest_days']:.1f}"436 )437 438 odds = fetch_combined_odds(home_team, away_team)439 sp_val = odds.get("spread_line") if odds and odds.get("spread_line") is not None else 0.0440 ou_val = odds.get("over_under_line") if odds and odds.get("over_under_line") is not None else 45.0441 442 st.write("---")443 st.subheader("Betting Lines")444 c1, c2 = st.columns(2)445 spread_input = c1.number_input("Spread (Home)", value=float(sp_val))446 total_input = c2.number_input("Total (O/U)", value=float(ou_val))447 448 row = pd.DataFrame([stats])449 row["spread_line"] = spread_input450 row["over_under_line"] = total_input451 452 win_prob = model_win.predict_proba(row)[0][1]453 cover_prob = model_cover.predict_proba(row)[0][1]454 455 st.success(f"**Win Probability:** {win_prob:.1%}")456 st.success(f"**Cover Probability:** {cover_prob:.1%}")457 458# TAB 2 - UPCOMING WEEK DUMP WITH TERMINAL STYLE LOGS459with tab2:460 st.subheader("Upcoming Week Predictions")461 462 log_box = st.empty()463 log_lines = []464 465 def log(line, delay=0.35):466 log_lines.append(line)467 log_box.text("\n".join(log_lines))468 time.sleep(delay)469 470 if st.button("Generate Report"):471 log("Environment check passed.")472 log("Downloading NFL schedule and historical data...")473 log("Calculating rest days...")474 log("Adding travel distances...")475 log("Building historical team records for training...")476 log("Training Random Forest models...")477 log(f"Models trained. Win Acc: {win_acc:.2f}, Cover Acc: {cover_acc:.2f}")478 log("")479 log("--- NFL 2.8 Menu ---")480 log("1. Single Game Prediction")481 log("2. Batch Prediction (Excel)")482 log("3. Dump Upcoming Week Data (Excel)")483 log("Q. Quit")484 log("Select: 3")485 log("")486 log("--- Option 3: Upcoming Week Data Dump & Prediction ---")487 488 d_col = "game_date" if "game_date" in games.columns else "gameday"489 games[d_col] = pd.to_datetime(games[d_col])490 upcoming = games[games[d_col].dt.date >= datetime.date.today()].copy().sort_values(d_col)491 492 if upcoming.empty:493 st.warning("No upcoming games found.")494 log("No upcoming games found.")495 else:496 next_week = upcoming["week"].min()497 week_games = upcoming[upcoming["week"] == next_week]498 log(f"Processing {len(week_games)} games for Week {next_week}...")499 500 dump_data = []501 progress_bar = st.progress(0)502 503 for i, (idx, row_g) in enumerate(week_games.iterrows()):504 ht = row_g["home_team"]505 at = row_g["away_team"]506 507 try:508 feats = compute_matchup_features(ht, at)509 except Exception:510 feats = {511 "home_win_pct": np.nan, "away_win_pct": np.nan,512 "home_point_diff": np.nan, "away_point_diff": np.nan,513 "home_rest_days": np.nan, "away_rest_days": np.nan,514 "travel_distance": np.nan515 }516 517 odds = fetch_combined_odds(ht, at) or {}518 sp_val = odds.get("spread_line") if odds.get("spread_line") is not None else 0.0519 ou_val = odds.get("over_under_line") if odds.get("over_under_line") is not None else 45.0520 521 input_row = pd.DataFrame([{522 "home_win_pct": feats["home_win_pct"],523 "away_win_pct": feats["away_win_pct"],524 "home_point_diff": feats["home_point_diff"],525 "away_point_diff": feats["away_point_diff"],526 "home_rest_days": feats["home_rest_days"],527 "away_rest_days": feats["away_rest_days"],528 "travel_distance": feats["travel_distance"],529 "spread_line": sp_val,530 "over_under_line": ou_val531 }]).fillna(0.0)532 533 win_prob = model_win.predict_proba(input_row)[0][1] * 100534 cover_prob = model_cover.predict_proba(input_row)[0][1] * 100535 536 dump_data.append({537 "Season": current_season,538 "Week": next_week,539 "Date": str(row_g[d_col].date()),540 "Home": ht,541 "Away": at,542 "Home Win Pct": feats["home_win_pct"],543 "Away Win Pct": feats["away_win_pct"],544 "Home Pt Diff": feats["home_point_diff"],545 "Away Pt Diff": feats["away_point_diff"],546 "Home Rest Days": feats["home_rest_days"],547 "Away Rest Days": feats["away_rest_days"],548 "Travel Distance": feats["travel_distance"],549 "Spread (Home)": odds.get("spread_line"),550 "Total (O/U)": odds.get("over_under_line"),551 "Home ML": odds.get("ml_home"),552 "Away ML": odds.get("ml_away"),553 "Model Win %": round(win_prob, 2),554 "Model Cover %": round(cover_prob, 2),555 })556 557 progress_bar.progress((i + 1) / len(week_games))558 559 result_df = pd.DataFrame(dump_data)560 st.write("### Upcoming Week Data Dump")561 st.dataframe(result_df)562 563 from io import BytesIO564 excel_buffer = BytesIO()565 result_df.to_excel(excel_buffer, index=False, sheet_name=f"Week_{next_week}")566 excel_buffer.seek(0)567 st.download_button(568 label=f"Download Week {next_week} Data Dump (Excel)",569 data=excel_buffer,570 file_name=f"NFL_Week_{next_week}_Data_Dump.xlsx",571 mime="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",572 )573 574 csv_data = result_df.to_csv(index=False).encode("utf-8")575 st.download_button(576 label="Download Predictions CSV",577 data=csv_data,578 file_name="nfl_predictions.csv",579 mime="text/csv",580 )581 