|
|
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 |
|
|
import re |
|
|
import plotly.express as px |
|
|
import plotly.graph_objects as go |
|
|
|
|
|
|
|
|
|
|
|
try: |
|
|
from BRScraper import nba |
|
|
BRSCRAPER_AVAILABLE = True |
|
|
except ImportError: |
|
|
BRSCRAPER_AVAILABLE = False |
|
|
st.error("BRScraper not found. Please install with: `pip install BRScraper`") |
|
|
|
|
|
|
|
|
st.set_page_config( |
|
|
page_title="NBA Analytics Hub", |
|
|
page_icon="π", |
|
|
layout="wide", |
|
|
initial_sidebar_state="expanded" |
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
if 'chat_history' not in st.session_state: |
|
|
st.session_state.chat_history = [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@st.cache_data(ttl=3600) |
|
|
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") |
|
|
tbl_html = "" |
|
|
|
|
|
if table_id: |
|
|
|
|
|
tbl = soup.find("table", {"id": table_id}) |
|
|
if tbl: |
|
|
tbl_html = str(tbl) |
|
|
else: |
|
|
|
|
|
comment_pattern = re.compile(r'<!--.*?<table[^>]*id="%s"[^>]*>.*?</table>.*?-->' % table_id, re.DOTALL) |
|
|
comment_match = comment_pattern.search(html) |
|
|
if comment_match: |
|
|
|
|
|
comment_content = comment_match.group(0) |
|
|
|
|
|
comment_content = comment_content.replace('<!--', '').replace('-->', '') |
|
|
|
|
|
comment_soup = BeautifulSoup(comment_content, 'lxml') |
|
|
tbl = comment_soup.find('table', {'id': table_id}) |
|
|
if tbl: |
|
|
tbl_html = str(tbl) |
|
|
else: |
|
|
|
|
|
first = soup.find("table") |
|
|
if first: |
|
|
tbl_html = str(first) |
|
|
|
|
|
if not tbl_html: |
|
|
return pd.DataFrame() |
|
|
|
|
|
try: |
|
|
|
|
|
return pd.read_html(tbl_html)[0] |
|
|
except ValueError: |
|
|
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 |
|
|
|
|
|
|
|
|
team_name = str(team_name).strip().replace('*', '') |
|
|
|
|
|
|
|
|
team_mapping = { |
|
|
'NOP': 'NO', |
|
|
'PHX': 'PHO', |
|
|
'BRK': 'BKN', |
|
|
'CHA': 'CHO', |
|
|
'UTA': 'UTH' |
|
|
} |
|
|
|
|
|
return team_mapping.get(team_name, team_name) |
|
|
|
|
|
@st.cache_data(ttl=300) |
|
|
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() |
|
|
|
|
|
|
|
|
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 df.empty: |
|
|
soup = BeautifulSoup(html, "lxml") |
|
|
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() |
|
|
|
|
|
|
|
|
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] |
|
|
|
|
|
|
|
|
df.columns = [str(col).strip() for col in df.columns] |
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
if team_col != 'Team': |
|
|
df = df.rename(columns={team_col: 'Team'}) |
|
|
|
|
|
|
|
|
df = df[df["Team"].astype(str) != "Team"].copy() |
|
|
df = df[df["Team"].notna()].copy() |
|
|
|
|
|
|
|
|
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' |
|
|
} |
|
|
|
|
|
|
|
|
for old_col, new_col in column_mapping.items(): |
|
|
if old_col in df.columns: |
|
|
df = df.rename(columns={old_col: new_col}) |
|
|
|
|
|
|
|
|
if 'Team' in df.columns: |
|
|
df['Team'] = df['Team'].apply(clean_team_name) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
latest_season_end_year = current_year |
|
|
if current_month >= 7: |
|
|
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) |
|
|
|
|
|
@st.cache_data(ttl=3600) |
|
|
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: |
|
|
|
|
|
|
|
|
latest_season_end_year = int(get_available_seasons(1)[0].split('β')[1]) |
|
|
|
|
|
|
|
|
|
|
|
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.") |
|
|
|
|
|
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_players = [ |
|
|
'LeBron James', 'Stephen Curry', 'Kevin Durant', 'Giannis Antetokounmpo', |
|
|
'Nikola Jokic', 'Joel Embiid', 'Jayson Tatum', 'Luka Doncic' |
|
|
] |
|
|
return pd.DataFrame({'name': fallback_players}) |
|
|
|
|
|
@st.cache_data(ttl=300) |
|
|
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() |
|
|
|
|
|
row['Season'] = season_str |
|
|
all_rows.append(row) |
|
|
except Exception as e: |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
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}) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
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", |
|
|
"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 "" |
|
|
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
|
|
|
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 []) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
seasons = get_available_seasons() |
|
|
selected_season_str = st.selectbox("Select Season", seasons, index=0) |
|
|
year_for_team_stats = int(selected_season_str.split('β')[1]) |
|
|
|
|
|
|
|
|
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 |
|
|
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', |
|
|
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"): |
|
|
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() |