Download src/streamlit_app.py from rairo/NBA-Test: direct link, hf CLI and curl.
- Browser
- Download file 33.8 kB
-
https://huggingface.co/spaces/rairo/NBA-Test/resolve/main/src/streamlit_app.py
- Command line
-
hf download hf://spaces/rairo/NBA-Test/src/streamlit_app.py
-
curl -L -o streamlit_app.py https://huggingface.co/spaces/rairo/NBA-Test/resolve/main/src/streamlit_app.py
33.8 kB
| import streamlit as st | |
| import pandas as pd | |
| import numpy as np | |
| import requests | |
| import os | |
| from datetime import datetime | |
| from bs4 import BeautifulSoup, Comment # Re-added for custom BS scraping | |
| import re # Re-added for custom BS scraping (regex for comments) | |
| import plotly.express as px | |
| import plotly.graph_objects as go | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Import BRScraper | |
| try: | |
| from BRScraper import nba | |
| BRSCRAPER_AVAILABLE = True | |
| except ImportError: | |
| BRSCRAPER_AVAILABLE = False | |
| st.error("BRScraper not found. Please install with: `pip install BRScraper`") | |
| # Page config | |
| st.set_page_config( | |
| page_title="NBA Analytics Hub", | |
| page_icon="π", | |
| layout="wide", | |
| initial_sidebar_state="expanded" | |
| ) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # CSS (black & white theme) | |
| st.markdown(""" | |
| <style> | |
| .main-header {font-size:3rem; font-weight:bold; text-align:center; color:#000;} | |
| .section-header {font-size:1.5rem; font-weight:bold; color:#333; margin:1rem 0;} | |
| table.dataframe {width:100%;} | |
| </style> | |
| """, unsafe_allow_html=True) | |
| # Initialize chat history | |
| if 'chat_history' not in st.session_state: | |
| st.session_state.chat_history = [] | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Custom BeautifulSoup Data Fetching Utilities (for teams) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def fetch_html(url): | |
| """Fetch raw HTML for a URL (with error handling).""" | |
| try: | |
| resp = requests.get(url, timeout=20) | |
| resp.raise_for_status() | |
| return resp.text | |
| except requests.exceptions.RequestException as e: | |
| st.error(f"Failed to fetch {url}: {e}") | |
| return "" | |
| except Exception as e: | |
| st.error(f"An unexpected error occurred while fetching {url}: {e}") | |
| return "" | |
| def parse_table(html, table_id=None): | |
| """ | |
| Given raw HTML and optional table_id, locate that <table>, | |
| handling cases where it's commented out, then parse it with pandas.read_html. | |
| """ | |
| if not html: | |
| return pd.DataFrame() | |
| soup = BeautifulSoup(html, "lxml") # Using lxml for potentially faster parsing | |
| tbl_html = "" | |
| if table_id: | |
| # First, try to find the table directly | |
| tbl = soup.find("table", {"id": table_id}) | |
| if tbl: | |
| tbl_html = str(tbl) | |
| else: | |
| # If not found directly, search for it within HTML comments | |
| comment_pattern = re.compile(r'<!--.*?<table[^>]*id="%s"[^>]*>.*?</table>.*?-->' % table_id, re.DOTALL) | |
| comment_match = comment_pattern.search(html) | |
| if comment_match: | |
| # Extract the content of the comment | |
| comment_content = comment_match.group(0) | |
| # Remove the comment tags | |
| comment_content = comment_content.replace('<!--', '').replace('-->', '') | |
| # Parse the comment content as new HTML | |
| comment_soup = BeautifulSoup(comment_content, 'lxml') | |
| tbl = comment_soup.find('table', {'id': table_id}) | |
| if tbl: | |
| tbl_html = str(tbl) | |
| else: | |
| # fallback: first table on page (only if no table_id specified) | |
| first = soup.find("table") | |
| if first: | |
| tbl_html = str(first) | |
| if not tbl_html: | |
| return pd.DataFrame() | |
| try: | |
| # pd.read_html returns a list of DataFrames, we want the first one | |
| return pd.read_html(tbl_html)[0] | |
| except ValueError: # No tables found in the provided HTML string | |
| return pd.DataFrame() | |
| except Exception as e: | |
| st.error(f"Error parsing table with pandas: {e}") | |
| return pd.DataFrame() | |
| def clean_team_name(team_name): | |
| """ | |
| Clean and standardize team names from Basketball Reference. | |
| """ | |
| if pd.isna(team_name): | |
| return team_name | |
| # Remove any asterisks or other symbols | |
| team_name = str(team_name).strip().replace('*', '') | |
| # Handle special cases for team name variations (if needed, keep for consistency) | |
| team_mapping = { | |
| 'NOP': 'NO', # New Orleans Pelicans sometimes shown as NOP | |
| 'PHX': 'PHO', # Phoenix Suns sometimes shown as PHX | |
| 'BRK': 'BKN', # Brooklyn Nets sometimes shown as BRK | |
| 'CHA': 'CHO', # Charlotte sometimes inconsistent | |
| 'UTA': 'UTH' # Utah Jazz sometimes shown as UTA | |
| } | |
| return team_mapping.get(team_name, team_name) | |
| def get_team_stats_bs(year): | |
| """ | |
| Scrapes the leagueβs perβgame team stats table from: | |
| https://www.basketball-reference.com/leagues/NBA_{year}_per_game.html | |
| Returns cleaned DataFrame. | |
| """ | |
| url = f"https://www.basketball-reference.com/leagues/NBA_{year}_per_game.html" | |
| html = fetch_html(url) | |
| if not html: | |
| return pd.DataFrame() | |
| # Try multiple possible table IDs for team stats | |
| possible_table_ids = ["per_game-team", "per_game_team", "team-stats-per_game", "teams_per_game"] | |
| df = pd.DataFrame() | |
| for table_id in possible_table_ids: | |
| df = parse_table(html, table_id=table_id) | |
| if not df.empty: | |
| break | |
| # If no specific table found, try to find any table with team data | |
| if df.empty: | |
| soup = BeautifulSoup(html, "lxml") # Use lxml for consistency | |
| tables = soup.find_all("table") | |
| for table in tables: | |
| if table.find("th", string=lambda text: text and "team" in text.lower()): | |
| df = parse_table(str(table)) | |
| if not df.empty: | |
| break | |
| if df.empty: | |
| st.warning(f"Could not find team stats table for {year}") | |
| return pd.DataFrame() | |
| # Handle potential MultiIndex columns | |
| if isinstance(df.columns, pd.MultiIndex): | |
| df.columns = ['_'.join(str(col).strip() for col in cols if str(col).strip() and str(col).strip() != 'Unnamed: 0_level_0') | |
| for cols in df.columns.values] | |
| # Clean column names | |
| df.columns = [str(col).strip() for col in df.columns] | |
| # Find team column | |
| team_col = None | |
| for col in df.columns: | |
| if 'team' in col.lower() or col in ['Team', 'Tm']: | |
| team_col = col | |
| break | |
| if team_col is None: | |
| st.warning(f"Could not find team column in team stats. Available columns: {df.columns.tolist()}") | |
| return pd.DataFrame() | |
| # Rename team column to standard name | |
| if team_col != 'Team': | |
| df = df.rename(columns={team_col: 'Team'}) | |
| # Remove header rows | |
| df = df[df["Team"].astype(str) != "Team"].copy() | |
| df = df[df["Team"].notna()].copy() | |
| # Standardize column names | |
| column_mapping = { | |
| 'G': 'GP', 'MP': 'MIN', | |
| 'FG%': 'FG_PCT', '3P%': 'FG3_PCT', 'FT%': 'FT_PCT', | |
| 'TRB': 'REB', 'AST': 'AST', 'STL': 'STL', 'BLK': 'BLK', 'TOV': 'TO', | |
| 'PF': 'PF', 'PTS': 'PTS', | |
| 'Rk': 'RANK', 'W': 'WINS', 'L': 'LOSSES', 'W/L%': 'WIN_LOSS_PCT', | |
| 'FG': 'FGM', 'FGA': 'FGA', '3P': 'FG3M', '3PA': 'FG3A', | |
| '2P': 'FGM2', '2PA': 'FGA2', '2P%': 'FG2_PCT', 'eFG%': 'EFG_PCT', | |
| 'FT': 'FTM', 'FTA': 'FTA', 'ORB': 'OREB', 'DRB': 'DREB' | |
| } | |
| # Apply column mapping only for columns that exist | |
| for old_col, new_col in column_mapping.items(): | |
| if old_col in df.columns: | |
| df = df.rename(columns={old_col: new_col}) | |
| # Clean team names | |
| if 'Team' in df.columns: # Ensure 'Team' column exists before applying | |
| df['Team'] = df['Team'].apply(clean_team_name) | |
| # Convert numeric columns | |
| non_numeric_cols = {"Team", "RANK"} | |
| for col in df.columns: | |
| if col not in non_numeric_cols: | |
| df[col] = pd.to_numeric(df[col], errors="coerce") | |
| return df | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # BRScraper Data Fetching Utilities (for players) | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Helper for dynamic season generation | |
| def get_available_seasons(num_seasons=6): | |
| """Generates a list of recent NBA seasons in 'YYYYβYY' format.""" | |
| current_year = datetime.now().year | |
| current_month = datetime.now().month | |
| # Determine the latest season end year. | |
| latest_season_end_year = current_year | |
| if current_month >= 7: # NBA season typically ends in June | |
| latest_season_end_year += 1 | |
| seasons_list = [] | |
| for i in range(num_seasons): | |
| end_year = latest_season_end_year - i | |
| start_year = end_year - 1 | |
| seasons_list.append(f"{start_year}β{end_year}") | |
| return sorted(seasons_list, reverse=True) # Sort to show most recent first | |
| def get_player_index_brscraper(): | |
| """ | |
| Uses BRScraper to get a list of players from a recent season's stats. | |
| This serves as our player index for the multiselect. | |
| """ | |
| if not BRSCRAPER_AVAILABLE: | |
| return pd.DataFrame(columns=['name']) | |
| try: | |
| # Get the end year of the most recent season for BRScraper | |
| # Example: '2024β25' -> 2025 | |
| latest_season_end_year = int(get_available_seasons(1)[0].split('β')[1]) | |
| # Use nba.get_stats to get a list of players for the latest season | |
| # BRScraper's get_stats returns a 'Player' column | |
| df = nba.get_stats(latest_season_end_year, info='per_game', rename=False) | |
| if df.empty or 'Player' not in df.columns: | |
| st.warning(f"BRScraper could not fetch player list for {latest_season_end_year}. Falling back to common players.") | |
| # Fallback to a hardcoded list of common players for demo | |
| common_players = [ | |
| 'LeBron James', 'Stephen Curry', 'Kevin Durant', 'Giannis Antetokounmpo', | |
| 'Nikola Jokic', 'Joel Embiid', 'Jayson Tatum', 'Luka Doncic', | |
| 'Damian Lillard', 'Jimmy Butler', 'Kawhi Leonard', 'Paul George', | |
| 'Anthony Davis', 'Rudy Gobert', 'Donovan Mitchell', 'Trae Young', | |
| 'Devin Booker', 'Karl-Anthony Towns', 'Zion Williamson', 'Ja Morant' | |
| ] | |
| return pd.DataFrame({'name': common_players}) | |
| player_names = df['Player'].unique().tolist() | |
| return pd.DataFrame({'name': player_names}) | |
| except Exception as e: | |
| st.error(f"Error fetching player index with BRScraper: {e}. Falling back to common players.") | |
| # Fallback to hardcoded list | |
| fallback_players = [ | |
| 'LeBron James', 'Stephen Curry', 'Kevin Durant', 'Giannis Antetokounmpo', | |
| 'Nikola Jokic', 'Joel Embiid', 'Jayson Tatum', 'Luka Doncic' | |
| ] | |
| return pd.DataFrame({'name': fallback_players}) | |
| def get_player_career_stats_brscraper(player_name, seasons_to_check=10): | |
| """ | |
| Build a player's career stats by fetching per-game data season-by-season | |
| via nba.get_stats, instead of nba.get_player_stats (which was returning empty). | |
| """ | |
| if not BRSCRAPER_AVAILABLE: | |
| return pd.DataFrame() | |
| all_rows = [] | |
| seasons = get_available_seasons(seasons_to_check) | |
| for season_str in seasons: | |
| end_year = int(season_str.split('β')[1]) | |
| try: | |
| df_season = nba.get_stats(end_year, info='per_game', playoffs=False, rename=False) | |
| if 'Player' in df_season.columns: | |
| row = df_season[df_season['Player'] == player_name] | |
| if not row.empty: | |
| row = row.copy() | |
| # normalize the season label | |
| row['Season'] = season_str | |
| all_rows.append(row) | |
| except Exception as e: | |
| # skip any failures | |
| st.warning(f"Could not fetch {season_str} for {player_name}: {e}") | |
| if not all_rows: | |
| return pd.DataFrame() | |
| df = pd.concat(all_rows, ignore_index=True) | |
| # Standardize columns | |
| mapping = { | |
| 'G':'GP','GS':'GS','MP':'MIN', | |
| 'FG%':'FG_PCT','3P%':'FG3_PCT','FT%':'FT_PCT', | |
| 'TRB':'REB','AST':'AST','STL':'STL','BLK':'BLK','TOV':'TO', | |
| 'PF':'PF','PTS':'PTS','ORB':'OREB','DRB':'DREB', | |
| 'FG':'FGM','FGA':'FGA','3P':'FG3M','3PA':'FG3A', | |
| '2P':'FGM2','2PA':'FGA2','2P%':'FG2_PCT','eFG%':'EFG_PCT', | |
| 'FT':'FTM','FTA':'FTA' | |
| } | |
| df = df.rename(columns={o:n for o,n in mapping.items() if o in df.columns}) | |
| # Convert numeric columns | |
| non_num = {'Season','Player','Tm','Lg','Pos'} | |
| for col in df.columns: | |
| if col not in non_num: | |
| df[col] = pd.to_numeric(df[col], errors='coerce') | |
| df['Player'] = player_name | |
| return df | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Perplexity API | |
| PERP_KEY = os.getenv("PERPLEXITY_API_KEY") | |
| PERP_URL = "https://api.perplexity.ai/chat/completions" | |
| def ask_perp(prompt, system="You are a helpful NBA analyst AI.", max_tokens=500, temp=0.2): | |
| if not PERP_KEY: | |
| st.error("Set PERPLEXITY_API_KEY env var.") | |
| return "" | |
| hdr = {'Authorization':f'Bearer {PERP_KEY}','Content-Type':'application/json'} | |
| payload = { | |
| "model":"sonar-medium-online", # Using a commonly available online model | |
| "messages":[{"role":"system","content":system},{"role":"user","content":prompt}], | |
| "max_tokens":max_tokens, "temperature":temp | |
| } | |
| try: | |
| r = requests.post(PERP_URL, json=payload, headers=hdr, timeout=45) | |
| r.raise_for_status() | |
| return r.json().get("choices", [])[0].get("message", {}).get("content", "") | |
| except Exception as e: | |
| st.error(f"Perplexity API error: {e}") | |
| return "" | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Plotting functions | |
| def create_comparison_chart(data, players_names, metric): | |
| """Create comparison chart for players""" | |
| fig = go.Figure() | |
| for i, player in enumerate(players_names): | |
| if player in data['Player'].values: | |
| player_data = data[data['Player'] == player] | |
| fig.add_trace(go.Scatter( | |
| x=player_data['Season'], | |
| y=player_data[metric], | |
| mode='lines+markers', | |
| name=player, | |
| line=dict(width=3) | |
| )) | |
| fig.update_layout( | |
| title=f"{metric} Comparison", | |
| xaxis_title="Season", | |
| yaxis_title=metric, | |
| hovermode='x unified', | |
| height=500 | |
| ) | |
| return fig | |
| def create_radar_chart(player_stats, categories): | |
| """Create radar chart for player comparison""" | |
| fig = go.Figure() | |
| for player_name, stats in player_stats.items(): | |
| r_values = [stats.get(cat,0) for cat in categories] | |
| fig.add_trace(go.Scatterpolar( | |
| r=r_values, | |
| theta=categories, | |
| fill='toself', | |
| name=player_name, | |
| opacity=0.7 | |
| )) | |
| fig.update_layout( | |
| polar=dict(radialaxis=dict(visible=True, range=[0,100])), | |
| showlegend=True, | |
| title="Player Comparison Radar Chart" | |
| ) | |
| return fig | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Main structure | |
| def main(): | |
| if not BRSCRAPER_AVAILABLE: | |
| st.warning("β οΈ BRScraper is not installed. Install with `pip install BRScraper`") | |
| st.markdown('<h1 class="main-header">π NBA Analytics Hub (BBR Edition)</h1>', unsafe_allow_html=True) | |
| st.sidebar.title("Navigation") | |
| page = st.sidebar.radio("", [ | |
| "Player vs Player Comparison", "Team vs Team Analysis", | |
| "NBA Awards Predictor", "AI Chat & Insights", | |
| "Young Player Projections", "Similar Players Finder", | |
| "Roster Builder", "Trade Scenario Analyzer" | |
| ]) | |
| if page == "Player vs Player Comparison": | |
| player_vs_player() | |
| elif page == "Team vs Team Analysis": | |
| team_vs_team() | |
| elif page == "NBA Awards Predictor": | |
| awards_predictor() | |
| elif page == "AI Chat & Insights": | |
| ai_chat() | |
| elif page == "Young Player Projections": | |
| young_projections() | |
| elif page == "Similar Players Finder": | |
| similar_players() | |
| elif page == "Roster Builder": | |
| roster_builder() | |
| else: | |
| trade_analyzer() | |
| # ββββββββββββββββββββββββββββββββββββββββββββββ | |
| # Page implementations | |
| def player_vs_player(): | |
| st.markdown('<h2 class="section-header">Player vs Player Comparison</h2>', unsafe_allow_html=True) | |
| if not BRSCRAPER_AVAILABLE: | |
| st.error("BRScraper is required for this feature. Please install BRScraper.") | |
| return | |
| idx = get_player_index_brscraper() | |
| selected_players = st.multiselect("Select Players (up to 4)", idx['name'], max_selections=4) | |
| seasons = get_available_seasons() | |
| selected_seasons = st.multiselect("Select Seasons", seasons, default=[seasons[0]] if seasons else []) | |
| # βββ RAW DEBUG PREVIEW ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| # This section is for debugging and can be commented out or removed in production | |
| # if selected_players: | |
| # st.markdown("### π§ͺ Raw Seasonal Data Preview") | |
| # for player in selected_players: | |
| # st.subheader(player) | |
| # # Fetching for a few seasons to show raw structure | |
| # raw = get_player_career_stats_brscraper(player) | |
| # if raw.empty: | |
| # st.write("β No rows returnedβplayer may be named differently or data unavailable.") | |
| # else: | |
| # st.write("Columns:", raw.columns.tolist()) | |
| # st.dataframe(raw, use_container_width=True) | |
| # st.markdown("---") | |
| # ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if st.button("Run Comparison"): | |
| if not selected_players: | |
| st.warning("Please select at least one player.") | |
| return | |
| all_data, no_data = [], [] | |
| with st.spinner("Fetching filtered data..."): | |
| for p in selected_players: | |
| # get_player_career_stats_brscraper fetches all career stats, then we filter | |
| df = get_player_career_stats_brscraper(p) | |
| if not df.empty: | |
| filt = df[df['Season'].isin(selected_seasons)] | |
| if not filt.empty: | |
| all_data.append(filt) | |
| else: | |
| no_data.append(p) | |
| else: | |
| no_data.append(p) | |
| if no_data: | |
| st.info(f"No data found for the selected seasons ({', '.join(selected_seasons)}) for: {', '.join(no_data)}.") | |
| if not all_data: | |
| st.error("No data available for any of the selected players and seasons to display. Please adjust your selections.") | |
| return | |
| comp_df = pd.concat(all_data, ignore_index=True) | |
| tabs = st.tabs(["Basic Stats", "Advanced Stats", "Visualizations"]) | |
| with tabs[0]: | |
| st.subheader("Basic Statistics") | |
| if len(selected_seasons) > 1: | |
| basic_display_df = comp_df.groupby('Player').mean(numeric_only=True).reset_index() | |
| basic_cols = ['Player','GP','MIN','PTS','REB','AST','STL','BLK','FG_PCT','FT_PCT','FG3_PCT'] | |
| else: | |
| basic_display_df = comp_df.copy() | |
| basic_cols = ['Player','Season','GP','MIN','PTS','REB','AST','STL','BLK','FG_PCT','FT_PCT','FG3_PCT'] | |
| st.dataframe(basic_display_df[[c for c in basic_cols if c in basic_display_df.columns]].round(2), use_container_width=True) | |
| with tabs[1]: | |
| st.subheader("Advanced Statistics") | |
| if not comp_df.empty: | |
| advanced_df = comp_df.copy() | |
| advanced_df['FGA'] = pd.to_numeric(advanced_df.get('FGA', 0), errors='coerce').fillna(0) | |
| advanced_df['FTA'] = pd.to_numeric(advanced_df.get('FTA', 0), errors='coerce').fillna(0) | |
| advanced_df['PTS'] = pd.to_numeric(advanced_df.get('PTS', 0), errors='coerce').fillna(0) | |
| advanced_df['TS_PCT'] = advanced_df.apply( | |
| lambda r: r['PTS'] / (2 * (r['FGA'] + 0.44 * r['FTA'])) if (r['FGA'] + 0.44 * r['FTA']) else 0, | |
| axis=1 | |
| ) | |
| if len(selected_seasons) > 1: | |
| advanced_display_df = advanced_df.groupby('Player').mean(numeric_only=True).reset_index() | |
| advanced_cols = ['Player','PTS','REB','AST','FG_PCT','TS_PCT'] | |
| else: | |
| advanced_display_df = advanced_df.copy() | |
| advanced_cols = ['Player','Season','PTS','REB','AST','FG_PCT','TS_PCT'] | |
| st.dataframe(advanced_display_df[[c for c in advanced_cols if c in advanced_display_df.columns]].round(3), use_container_width=True) | |
| else: | |
| st.info("No data available for advanced statistics.") | |
| with tabs[2]: | |
| st.subheader("Visualizations") | |
| if not comp_df.empty: | |
| metrics = [m for m in ['PTS','REB','AST','FG_PCT','FG3_PCT','FT_PCT','STL','BLK'] if m in comp_df.columns] | |
| if metrics: | |
| selected_metric = st.selectbox("Select Metric to Visualize", metrics) | |
| if selected_metric: | |
| if len(selected_players) == 1 and len(selected_seasons) > 1: | |
| player_trend_df = comp_df[comp_df['Player'] == selected_players[0]].sort_values('Season') | |
| fig = px.line(player_trend_df, x='Season', y=selected_metric, markers=True, title=f"{selected_players[0]} β {selected_metric} Trend") | |
| else: | |
| avg_comparison_df = comp_df.groupby('Player')[metrics].mean(numeric_only=True).reset_index() | |
| fig = px.bar(avg_comparison_df, x='Player', y=selected_metric, title=f"Average {selected_metric} Comparison (Selected Seasons)") | |
| st.plotly_chart(fig, use_container_width=True) | |
| radar_metrics_for_chart = [c for c in ['PTS','REB','AST','STL','BLK'] if c in comp_df.columns] | |
| if len(radar_metrics_for_chart) >= 3: | |
| radar_source_df = (comp_df.groupby('Player')[radar_metrics_for_chart].mean(numeric_only=True).reset_index() | |
| if len(selected_seasons) > 1 else comp_df.copy()) | |
| scaled_radar_df = radar_source_df.copy() | |
| for c in radar_metrics_for_chart: | |
| mn, mx = scaled_radar_df[c].min(), scaled_radar_df[c].max() | |
| scaled_radar_df[c] = ((scaled_radar_df[c] - mn) / (mx - mn) * 100) if mx > mn else 0 | |
| radar_data = {r['Player']: {c: r[c] for c in radar_metrics_for_chart} for _, r in scaled_radar_df.iterrows()} | |
| st.plotly_chart(create_radar_chart(radar_data, radar_metrics_for_chart), use_container_width=True) | |
| else: | |
| st.info("Need β₯3 metrics for radar chart.") | |
| else: | |
| st.info("No metrics available.") | |
| else: | |
| st.info("No data available for visualizations.") | |
| def team_vs_team(): | |
| st.markdown('<h2 class="section-header">Team vs Team Analysis</h2>', unsafe_allow_html=True) | |
| # This page uses the custom BeautifulSoup scraper, so no BRSCRAPER_AVAILABLE check here. | |
| seasons = get_available_seasons() | |
| selected_season_str = st.selectbox("Select Season", seasons, index=0) | |
| year_for_team_stats = int(selected_season_str.split('β')[1]) | |
| # Use the custom BeautifulSoup scraper for team stats | |
| tm_df = get_team_stats_bs(year_for_team_stats) | |
| if tm_df.empty: | |
| st.info(f"No team data available for the {selected_season_str} season. This might be because the season hasn't started or data is not yet available, or the scraper encountered an issue.") | |
| return | |
| teams = tm_df['Team'].unique().tolist() | |
| selected_teams = st.multiselect("Select Teams (up to 4)", teams, max_selections=4) | |
| if st.button("Run Comparison"): | |
| if not selected_teams: | |
| st.warning("Please select at least one team.") | |
| return | |
| stats = [] | |
| teams_with_no_data = [] | |
| with st.spinner("Fetching team data..."): | |
| for t in selected_teams: | |
| df = tm_df[tm_df.Team == t].copy() | |
| if not df.empty: | |
| df_dict = df.iloc[0].to_dict() | |
| df_dict['Season'] = selected_season_str # Add season string for display | |
| stats.append(df_dict) | |
| else: | |
| teams_with_no_data.append(t) | |
| if teams_with_no_data: | |
| st.info(f"No data found for the selected season ({selected_season_str}) for: {', '.join(teams_with_no_data)}. This might be because the season hasn't started or data is not yet available.") | |
| if not stats: | |
| st.error("No data available for the selected teams to display. Please adjust your selections.") | |
| return | |
| comp = pd.DataFrame(stats) | |
| for col in ['PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', '3P_PCT', 'FT_PCT']: | |
| if col in comp.columns: | |
| comp[col] = pd.to_numeric(comp[col], errors='coerce') | |
| st.subheader("Team Statistics Comparison") | |
| cols = ['Team', 'Season', 'PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', '3P_PCT', 'FT_PCT'] | |
| display_cols = [col for col in cols if col in comp.columns] | |
| st.dataframe(comp[display_cols].round(2), use_container_width=True) | |
| st.subheader("Team Performance Visualization") | |
| metric_options = ['PTS', 'REB', 'AST', 'FG_PCT', '3P_PCT', 'FT_PCT'] | |
| available_metrics = [m for m in metric_options if m in comp.columns] | |
| if available_metrics: | |
| selected_metric = st.selectbox("Select Metric", available_metrics) | |
| fig = px.bar( | |
| comp, | |
| x='Team', | |
| y=selected_metric, | |
| color='Team', # Color by team for clarity | |
| title=f"Team {selected_metric} Comparison ({selected_season_str} Season)", | |
| barmode='group' | |
| ) | |
| st.plotly_chart(fig, use_container_width=True) | |
| else: | |
| st.info("No common metrics available for visualization.") | |
| def awards_predictor(): | |
| st.markdown('<h2 class="section-header">NBA Awards Predictor</h2>', unsafe_allow_html=True) | |
| award = st.selectbox("Select Award", ["MVP","Defensive Player of the Year","Rookie of the Year","6th Man of the Year","All-NBA First Team"]) | |
| st.subheader(f"{award} Criteria") | |
| if award=="MVP": | |
| crit = { | |
| "PPG":st.slider("Min PPG",15.0,35.0,25.0), | |
| "Wins":st.slider("Min Team Wins",35,70,50), | |
| "PER":st.slider("Min PER",15.0,35.0,25.0), | |
| "WS":st.slider("Min Win Shares",5.0,20.0,10.0) | |
| } | |
| elif award=="Defensive Player of the Year": | |
| crit = { | |
| "BPG":st.slider("Min BPG",0.0,4.0,1.5), | |
| "SPG":st.slider("Min SPG",0.0,3.0,1.0), | |
| "DefRtgMax":st.slider("Max Def Rating",90.0,120.0,105.0), | |
| "DefRankMax":st.slider("Max Team Def Rank",1,30,10) | |
| } | |
| else: | |
| crit = { | |
| "PPG":st.slider("Min PPG",10.0,30.0,15.0), | |
| "Games":st.slider("Min Games",50,82,65), | |
| "FG%":st.slider("Min FG%",0.35,0.65,0.45) | |
| } | |
| if st.button("Generate Predictions"): | |
| p = f"Predict top 5 {award} candidates based on {crit}. Focus on 2024-25 season." | |
| resp = ask_perp(p, system="You are an NBA awards expert AI.", max_tokens=800) | |
| st.markdown("### Predictions") | |
| st.write(resp) | |
| def ai_chat(): | |
| st.markdown('<h2 class="section-header">AI Chat & Insights</h2>', unsafe_allow_html=True) | |
| for msg in st.session_state.chat_history: | |
| with st.chat_message(msg["role"]): | |
| st.write(msg["content"]) | |
| if prompt := st.chat_input("Ask me anything about NBAβ¦"): | |
| st.session_state.chat_history.append({"role":"user","content":prompt}) | |
| with st.chat_message("user"): # Display user message immediately | |
| st.write(prompt) | |
| with st.chat_message("assistant"): | |
| ans = ask_perp(prompt) | |
| st.write(ans) | |
| st.session_state.chat_history.append({"role":"assistant","content":ans}) | |
| st.subheader("Quick Actions") | |
| c1, c2, c3 = st.columns(3) | |
| if c1.button("π Contenders"): | |
| resp = ask_perp("Analyze the current NBA championship contenders for 2025.") | |
| st.write(resp) | |
| if c2.button("β Rising Stars"): | |
| resp = ask_perp("Who are the most promising young NBA players to watch in 2025?") | |
| st.write(resp) | |
| if c3.button("π Trades"): | |
| resp = ask_perp("What are some potential NBA trades this season?") | |
| st.write(resp) | |
| def young_projections(): | |
| st.markdown('<h2 class="section-header">Young Player Projections</h2>', unsafe_allow_html=True) | |
| names = get_player_index_brscraper()['name'].tolist() | |
| sel = st.selectbox("Select or enter player", [""]+names) | |
| player = sel or st.text_input("Enter player name manually") | |
| if player: | |
| age = st.number_input("Current Age",18,25,21) | |
| yrs = st.number_input("Years in NBA",0,7,2) | |
| ppg = st.number_input("PPG",0.0,40.0,15.0) | |
| rpg = st.number_input("RPG",0.0,20.0,5.0) | |
| apg = st.number_input("APG",0.0,15.0,3.0) | |
| if st.button("Generate AI Projection"): | |
| prompt = ( | |
| f"Project {player}: Age={age}, Years={yrs}, PPG={ppg}, RPG={rpg}, APG={apg}." | |
| ) | |
| out = ask_perp(prompt, system="You are an NBA projection expert AI.", max_tokens=800) | |
| st.markdown("### Projection Analysis") | |
| st.write(out) | |
| years = [f"Year {i+1}" for i in range(5)] | |
| vals = [ppg*(1+0.1*i) for i in range(5)] | |
| fig = go.Figure() | |
| fig.add_trace(go.Scatter(x=years, y=vals, mode='lines+markers')) | |
| fig.update_layout(title=f"{player} β PPG Projection", xaxis_title="Year", yaxis_title="PPG") | |
| st.plotly_chart(fig, use_container_width=True) | |
| def similar_players(): | |
| st.markdown('<h2 class="section-header">Similar Players Finder</h2>', unsafe_allow_html=True) | |
| names = get_player_index_brscraper()['name'].tolist() | |
| tp = st.selectbox("Target Player", names) | |
| crit = st.multiselect("Criteria", ["Position","Height/Weight","Playing Style","Statistical Profile","Age/Experience"], | |
| default=["Playing Style","Statistical Profile"]) | |
| if tp and crit and st.button("Find Similar"): | |
| prompt = f"Find top 5 current and top 3 historical similar to {tp} based on {', '.join(crit)}." | |
| st.write(ask_perp(prompt, system="You are a similarity expert AI.")) | |
| st.subheader("Manual Compare") | |
| p1 = st.selectbox("Player 1", names, key="p1") | |
| p2 = st.selectbox("Player 2", names, key="p2") | |
| if p1 and p2 and p1!=p2 and st.button("Compare Players"): | |
| prompt = f"Compare {p1} vs {p2} in detail on stats, style, impact, etc." | |
| st.write(ask_perp(prompt, system="You are a comparison expert AI.")) | |
| def roster_builder(): | |
| st.markdown('<h2 class="section-header">NBA Roster Builder</h2>', unsafe_allow_html=True) | |
| cap = st.number_input("Salary Cap (Millions)",100,200,136) | |
| strat = st.selectbox("Strategy",["Championship Contender","Young Core Development","Balanced Veteran Mix","Small Ball","Defense First"]) | |
| pos = st.multiselect("Priority Positions",["Point Guard","Shooting Guard","Small Forward","Power Forward","Center"],default=["Point Guard","Center"]) | |
| st.subheader("Budget Allocation") | |
| cols = st.columns(5) | |
| alloc = {} | |
| total = 0 | |
| for i,p in enumerate(["PG","SG","SF","PF","C"]): | |
| val = cols[i].number_input(f"{p} Budget ($M)",0,50,20, key=f"b{p}") | |
| alloc[p] = val | |
| total += val | |
| st.write(f"Total Allocated: ${total}M / ${cap}M") | |
| if total > cap: | |
| st.error("Budget exceeds cap!") | |
| if st.button("Generate Roster Suggestions") and total <= cap: | |
| prompt = ( | |
| f"Build roster: Cap ${cap}M, Strategy {strat}, Positions {pos}, Budgets {alloc}." | |
| ) | |
| st.markdown("### AI Roster Recommendations") | |
| st.write(ask_perp(prompt, system="You are an NBA roster building expert AI.")) | |
| def trade_analyzer(): | |
| st.markdown('<h2 class="section-header">Trade Scenario Analyzer</h2>', unsafe_allow_html=True) | |
| t1 = st.text_input("Team 1 trades") | |
| t2 = st.text_input("Team 2 trades") | |
| if t1 and t2 and st.button("Analyze Trade"): | |
| prompt = f"Analyze trade: Team1 {t1}, Team2 {t2}." | |
| st.write(ask_perp(prompt, system="You are a trade analysis AI.")) | |
| if __name__ == "__main__": | |
| main() |