CoolFace
Apppublic

Airmanv/NFL3.1

sourceHugging Faceupdated 10mo agoView on Hugging Face
0likes
app.py581 linesDownload Raw Back to root
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