rairo commited on
Commit
80e3608
·
verified ·
1 Parent(s): d9795cd

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +250 -990
src/streamlit_app.py CHANGED
@@ -1,1031 +1,291 @@
1
  import streamlit as st
2
  import pandas as pd
3
  import numpy as np
4
- import plotly.express as px
5
- import plotly.graph_objects as go
6
- from plotly.subplots import make_subplots
7
  import requests
8
- from bs4 import BeautifulSoup # New import
9
- import re # New import for regex
10
- import time # For rate limiting
11
- from datetime import datetime
12
- import json
13
  import os
 
14
 
15
-
16
- # -----------------------------------------------------------------------
17
-
18
- # Page configuration
19
- st.set_page_config(
20
- page_title="NBA Analytics Hub",
21
- page_icon="🏀",
22
- layout="wide",
23
- initial_sidebar_state="expanded"
24
- )
25
-
26
- # Custom CSS
27
  st.markdown("""
28
  <style>
29
- .main-header {
30
- font-size: 3rem;
31
- font-weight: bold;
32
- text-align: center;
33
- color: #1f77b4;
34
- margin-bottom: 2rem;
35
- }
36
- .section-header {
37
- font-size: 1.5rem;
38
- font-weight: bold;
39
- color: #2e8b57;
40
- margin: 1rem 0;
41
- }
42
- .metric-card {
43
- background-color: #f0f2f6;
44
- padding: 1rem;
45
- border-radius: 10px;
46
- margin: 0.5rem 0;
47
- }
48
  </style>
49
  """, unsafe_allow_html=True)
50
 
51
- # Initialize session state
52
- if 'chat_history' not in st.session_state:
53
- st.session_state.chat_history = []
54
-
55
- # Perplexity API configuration
56
- PERPLEXITY_API_KEY = os.getenv("PERPLEXITY_API_KEY")
57
- PERPLEXITY_API_URL = "https://api.perplexity.ai/chat/completions"
58
-
59
- # Base URL for Basketball-Reference
60
- BBR_BASE_URL = "https://www.basketball-reference.com"
61
-
62
- # Hardcoded Team Name to BBR Abbreviation mapping
63
- # This is more reliable than scraping for team abbreviations.
64
- TEAM_NAME_TO_BBR_ABBR = {
65
- "Atlanta Hawks": "ATL", "Boston Celtics": "BOS", "Brooklyn Nets": "BRK",
66
- "Charlotte Hornets": "CHO", "Chicago Bulls": "CHI", "Cleveland Cavaliers": "CLE",
67
- "Dallas Mavericks": "DAL", "Denver Nuggets": "DEN", "Detroit Pistons": "DET",
68
- "Golden State Warriors": "GSW", "Houston Rockets": "HOU", "Indiana Pacers": "IND",
69
- "Los Angeles Clippers": "LAC", "Los Angeles Lakers": "LAL", "Memphis Grizzlies": "MEM",
70
- "Miami Heat": "MIA", "Milwaukee Bucks": "MIL", "Minnesota Timberwolves": "MIN",
71
- "New Orleans Pelicans": "NOP", "New York Knicks": "NYK", "Oklahoma City Thunder": "OKC",
72
- "Orlando Magic": "ORL", "Philadelphia 76ers": "PHI", "Phoenix Suns": "PHO",
73
- "Portland Trail Blazers": "POR", "Sacramento Kings": "SAC", "San Antonio Spurs": "SAS",
74
- "Toronto Raptors": "TOR", "Utah Jazz": "UTA", "Washington Wizards": "WAS"
75
- }
76
-
77
- # Mapping for season year in BBR URLs (e.g., 2023-24 -> 2024)
78
- BBR_SEASON_URL_MAP = {
79
- "2023-24": "2024", "2022-23": "2023", "2021-22": "2022",
80
- "2020-21": "2021", "2019-20": "2020", "2018-19": "2019",
81
- "2017-18": "2018", "2016-17": "2017", "2015-16": "2016",
82
- "2014-15": "2015", "2013-14": "2014", "2012-13": "2013",
83
- "2011-12": "2012", "2010-11": "2011", "2009-10": "2010"
84
- }
85
-
86
-
87
- # ---------- Perplexity API Functions ----------
88
- def get_perplexity_response(api_key, prompt, system_message="You are a helpful NBA analyst AI.", max_tokens=500, temperature=0.2):
89
- """
90
- Queries the Perplexity AI API with a given prompt and system message.
91
- """
92
- if not api_key:
93
- st.error("Perplexity API Key is not set. Please configure it as an environment variable (PERPLEXITY_API_KEY).")
94
- return None
95
-
96
- headers = {
97
- 'Authorization': f'Bearer {api_key}',
98
- 'Content-Type': 'application/json'
99
- }
100
- payload = {
101
- 'model': 'sonar-pro', # keep 'sonar-pro'
102
- 'messages': [
103
- {'role': 'system', 'content': system_message},
104
- {'role': 'user', 'content': prompt}
105
- ],
106
- "max_tokens": max_tokens,
107
- "temperature": temperature
108
- }
109
- try:
110
- with st.spinner("Querying Perplexity AI..."):
111
- response = requests.post(PERPLEXITY_API_URL, headers=headers, json=payload, timeout=45)
112
- response.raise_for_status() # Raise HTTPError for bad responses (4xx or 5xx)
113
- data = response.json()
114
- return data.get('choices', [{}])[0].get('message', {}).get('content', '')
115
- except requests.exceptions.RequestException as e:
116
- error_message = f"Error communicating with Perplexity API: {e}"
117
- if e.response is not None:
118
- try:
119
- error_detail = e.response.json().get("error", {}).get("message", e.response.text)
120
- error_message = f"Perplexity API error: {error_detail}"
121
- except ValueError: # If response is not valid JSON
122
- error_message = f"Perplexity API error: {e.response.status_code} - {e.response.reason}"
123
- st.error(error_message)
124
- return None
125
- except Exception as e:
126
- st.error(f"An unexpected error occurred with Perplexity API: {e}")
127
- return None
128
-
129
- # ---------- Basketball-Reference Data Fetching Functions ----------
130
-
131
- @st.cache_data(ttl=3600)
132
- def get_all_players_bbr():
133
- """
134
- Scrapes a list of active players from Basketball-Reference's 2024 per-game stats page.
135
- Note: This will not get ALL historical players, only those listed on this specific page.
136
- For a comprehensive list, a more extensive scrape of player index pages (A-Z) would be needed.
137
- """
138
- players_list = []
139
- url = f"{BBR_BASE_URL}/leagues/NBA_2024_per_game.html" # Using 2024 for current season
140
- try:
141
- response = requests.get(url, timeout=10)
142
- response.raise_for_status()
143
- soup = BeautifulSoup(response.content, 'lxml')
144
- table = soup.find('table', {'id': 'per_game_stats'})
145
- if table:
146
- for row in table.find_all('tr')[1:]: # Skip header row
147
- player_name_tag = row.find('a')
148
- if player_name_tag:
149
- player_name = player_name_tag.get_text()
150
- # BBR player ID is part of the href (e.g., /players/j/jamesle01.html)
151
- player_bbr_id = player_name_tag['href'].split('/')[-1].replace('.html', '')
152
- players_list.append({'full_name': player_name, 'id': player_bbr_id})
153
- st.success(f"Loaded {len(players_list)} players from Basketball-Reference.")
154
- else:
155
- st.warning(f"Could not find player stats table on {url}")
156
- except requests.exceptions.RequestException as e:
157
- st.error(f"Error fetching player list from Basketball-Reference: {e}")
158
- except Exception as e:
159
- st.error(f"An unexpected error occurred while parsing player list: {e}")
160
- return players_list
161
-
162
  @st.cache_data(ttl=3600)
163
- def get_all_teams_bbr():
164
- """
165
- Returns a list of NBA teams using a hardcoded mapping.
166
- """
167
- teams_list = []
168
- for full_name, abbr in TEAM_NAME_TO_BBR_ABBR.items():
169
- teams_list.append({'full_name': full_name, 'id': abbr}) # Using abbr as ID for consistency
170
- return teams_list
171
-
172
- @st.cache_data(ttl=300)
173
- def get_player_stats_bbr(player_name, season="2023-24"):
174
- """
175
- Scrapes player career stats for a given player from Basketball-Reference.
176
- Then filters for the specified season.
177
- Returns a DataFrame.
178
- """
179
- # Step 1: Find the player's BBR URL by searching
180
- search_url = f"{BBR_BASE_URL}/search/search.fcgi?search={player_name.replace(' ', '+')}"
181
- player_url = None
182
  try:
183
- search_response = requests.get(search_url, timeout=10)
184
- search_response.raise_for_status()
185
- search_soup = BeautifulSoup(search_response.content, 'lxml')
186
- # Look for a link to the player's page in the search results
187
- # This assumes the first search result is the correct player
188
- player_link_div = search_soup.find('div', {'class': 'search-item-name'})
189
- if player_link_div:
190
- player_link = player_link_div.find('a')
191
- if player_link and player_link['href'].startswith('/players/'):
192
- player_url = f"{BBR_BASE_URL}{player_link['href']}"
193
- if not player_url:
194
- st.warning(f"Could not find Basketball-Reference page for {player_name}.")
195
- return pd.DataFrame()
196
- except requests.exceptions.RequestException as e:
197
- st.error(f"Error searching for player {player_name} on Basketball-Reference: {e}")
198
- return pd.DataFrame()
199
  except Exception as e:
200
- st.error(f"An unexpected error occurred during player search for {player_name}: {e}")
201
  return pd.DataFrame()
202
 
203
- # Step 2: Scrape the player's page for career stats
204
- try:
205
- response = requests.get(player_url, timeout=10)
206
- response.raise_for_status()
207
- soup = BeautifulSoup(response.content, 'lxml')
208
 
209
- # Basketball-Reference often hides tables in comments.
210
- # Find the comment containing the 'per_game' table
211
- comment = soup.find(string=lambda text: isinstance(text, str) and 'id="per_game"' in text)
212
- if comment:
213
- soup_from_comment = BeautifulSoup(comment, 'lxml')
214
- table = soup_from_comment.find('table', {'id': 'per_game'})
215
- else:
216
- table = soup.find('table', {'id': 'per_game'}) # Try direct find if not in comment
217
-
218
- if table:
219
- df = pd.read_html(str(table))[0]
220
- # Clean up column names (remove special characters, make consistent)
221
- # Handle multi-index columns if present (e.g., 'Shooting' -> 'FG', 'FGA')
222
- if isinstance(df.columns, pd.MultiIndex):
223
- df.columns = ['_'.join(col).strip() for col in df.columns.values]
224
- else:
225
- df.columns = [col.strip() for col in df.columns.values]
226
-
227
- # Standardize column names to match original app's expectations
228
- df = df.rename(columns={
229
- 'Season': 'SEASON_ID_BBR', # Keep original BBR season for filtering
230
- 'Age': 'AGE', 'Tm': 'TEAM_ABBREVIATION', 'Lg': 'LEAGUE_ID', 'Pos': 'POSITION',
231
- 'G': 'GP', 'GS': 'GS', 'MP': 'MIN',
232
- 'FG': 'FGM', 'FGA': 'FGA', 'FG%': 'FG_PCT',
233
- '3P': 'FG3M', '3PA': 'FG3A', '3P%': 'FG3_PCT',
234
- '2P': 'FGM2', '2PA': 'FGA2', '2P%': 'FG2_PCT',
235
- 'eFG%': 'EFG_PCT', 'FT': 'FTM', 'FTA': 'FTA', 'FT%': 'FT_PCT',
236
- 'ORB': 'OREB', 'DRB': 'DREB', 'TRB': 'REB', 'AST': 'AST',
237
- 'STL': 'STL', 'BLK': 'BLK', 'TOV': 'TO', 'PF': 'PF', 'PTS': 'PTS'
238
- })
239
-
240
- # Filter for the specific season
241
- # BBR table's 'Season' column is like '2023-24', not just '2024' for the row.
242
- # So, we filter using the original `season` string.
243
- filtered_df = df[df['SEASON_ID_BBR'] == season].copy()
244
-
245
- if not filtered_df.empty:
246
- # Add PLAYER_NAME and SEASON_ID for consistency with original code
247
- filtered_df['PLAYER_NAME'] = player_name
248
- filtered_df['SEASON_ID'] = season # Keep original season format
249
- return filtered_df
250
- else:
251
- st.info(f"No stats found for {player_name} in season {season} on Basketball-Reference.")
252
- return pd.DataFrame()
253
- else:
254
- st.warning(f"Could not find 'per_game' table for {player_name} on Basketball-Reference.")
255
- return pd.DataFrame()
256
- except requests.exceptions.RequestException as e:
257
- st.error(f"Error fetching player stats for {player_name} from Basketball-Reference: {e}")
258
- return pd.DataFrame()
259
- except Exception as e:
260
- st.error(f"An unexpected error occurred while parsing player stats for {player_name}: {e}")
261
- return pd.DataFrame()
262
 
263
  @st.cache_data(ttl=300)
264
- def get_team_stats_bbr(team_name, season="2023-24"):
265
- """
266
- Scrapes team stats for a given team and season from Basketball-Reference.
267
- Returns a DataFrame.
268
- """
269
- team_abbr = TEAM_NAME_TO_BBR_ABBR.get(team_name)
270
- if not team_abbr:
271
- st.error(f"Could not find abbreviation for team: {team_name}")
272
  return pd.DataFrame()
 
 
 
 
 
 
273
 
274
- bbr_season_year = BBR_SEASON_URL_MAP.get(season)
275
- if not bbr_season_year:
276
- st.warning(f"Invalid season format for Basketball-Reference: {season}")
277
- return pd.DataFrame()
278
-
279
- url = f"{BBR_BASE_URL}/teams/{team_abbr}/{bbr_season_year}.html"
280
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
281
  try:
282
- response = requests.get(url, timeout=10)
283
- response.raise_for_status()
284
- soup = BeautifulSoup(response.content, 'lxml')
285
-
286
- # Team stats are usually in a table with id 'team_and_opponent' or similar
287
- comment = soup.find(string=lambda text: isinstance(text, str) and 'id="team_and_opponent"' in text)
288
- if comment:
289
- soup_from_comment = BeautifulSoup(comment, 'lxml')
290
- table = soup_from_comment.find('table', {'id': 'team_and_opponent'})
291
- else:
292
- table = soup.find('table', {'id': 'team_and_opponent'}) # Try direct find
293
-
294
- if table:
295
- df = pd.read_html(str(table))[0]
296
- # Clean up column names
297
- if isinstance(df.columns, pd.MultiIndex):
298
- df.columns = ['_'.join(col).strip() for col in df.columns.values]
299
- else:
300
- df.columns = [col.strip() for col in df.columns.values]
301
-
302
- # Standardize column names
303
- df = df.rename(columns={
304
- 'G': 'GP', 'MP': 'MIN', 'FG': 'FGM', 'FGA': 'FGA', 'FG%': 'FG_PCT',
305
- '3P': 'FG3M', '3PA': 'FG3A', '3P%': 'FG3_PCT', 'FT': 'FTM', 'FTA': 'FTA', 'FT%': 'FT_PCT',
306
- 'TRB': 'REB', 'AST': 'AST', 'STL': 'STL', 'BLK': 'BLK', 'TOV': 'TO', 'PF': 'PF', 'PTS': 'PTS'
307
- })
308
-
309
- if not df.empty:
310
- # The 'team_and_opponent' table has two main rows: 'Team' and 'Opponent'.
311
- # We want the 'Team' row.
312
- team_stats_row = df[df['Rk'] == 'Team'].copy()
313
- if team_stats_row.empty:
314
- # Fallback: if 'Rk' isn't 'Team', try the first row (common for overall team stats)
315
- team_stats_row = df.iloc[[0]].copy()
316
-
317
- if not team_stats_row.empty:
318
- team_stats_row['TEAM_NAME'] = team_name
319
- team_stats_row['SEASON'] = season
320
- return team_stats_row
321
- else:
322
- st.info(f"Could not extract team stats row for {team_name} in season {season}.")
323
- return pd.DataFrame()
324
- else:
325
- st.info(f"No stats found for team {team_name} in season {season} on Basketball-Reference.")
326
- return pd.DataFrame()
327
- else:
328
- st.warning(f"Could not find team stats table for {team_name} on Basketball-Reference.")
329
- return pd.DataFrame()
330
- except requests.exceptions.RequestException as e:
331
- st.error(f"Error fetching team stats for {team_name} from Basketball-Reference: {e}")
332
- return pd.DataFrame()
333
  except Exception as e:
334
- st.error(f"An unexpected error occurred while parsing team stats for {team_name}: {e}")
335
- return pd.DataFrame()
336
 
337
- # Redefine the main data fetching functions to use Basketball-Reference versions
338
- @st.cache_data(ttl=3600)
339
- def get_all_players():
340
- """Get all NBA players (from BBR)."""
341
- return get_all_players_bbr()
342
-
343
- @st.cache_data(ttl=3600)
344
- def get_all_teams():
345
- """Get all NBA teams (from BBR)."""
346
- return get_all_teams_bbr()
347
-
348
- @st.cache_data(ttl=300)
349
- def get_player_stats(player_name, season="2023-24"):
350
- """Get player stats (from BBR)."""
351
- return get_player_stats_bbr(player_name, season)
352
-
353
- @st.cache_data(ttl=300)
354
- def get_team_stats(team_name, season="2023-24"):
355
- """Get team stats (from BBR)."""
356
- return get_team_stats_bbr(team_name, season)
357
-
358
-
359
- def create_comparison_chart(data, players_names, metric):
360
- """Create comparison chart for players"""
361
- fig = go.Figure()
362
-
363
- for i, player in enumerate(players_names):
364
- if player in data['PLAYER_NAME'].values:
365
- player_data = data[data['PLAYER_NAME'] == player]
366
- fig.add_trace(go.Scatter(
367
- x=player_data['SEASON_ID'],
368
- y=player_data[metric],
369
- mode='lines+markers',
370
- name=player,
371
- line=dict(width=3)
372
- ))
373
-
374
- fig.update_layout(
375
- title=f"{metric} Comparison",
376
- xaxis_title="Season",
377
- yaxis_title=metric,
378
- hovermode='x unified',
379
- height=500
380
- )
381
-
382
- return fig
383
-
384
- def create_radar_chart(player_stats, categories):
385
- """Create radar chart for player comparison"""
386
- fig = go.Figure()
387
-
388
- for player_name, stats in player_stats.items():
389
- # Ensure all categories are present, default to 0 if not
390
- r_values = [stats.get(cat, 0) for cat in categories]
391
-
392
- fig.add_trace(go.Scatterpolar(
393
- r=r_values,
394
- theta=categories,
395
- fill='toself',
396
- name=player_name,
397
- opacity=0.7
398
- ))
399
-
400
- fig.update_layout(
401
- polar=dict(
402
- radialaxis=dict(
403
- visible=True,
404
- # The range should be adjusted based on the scaled data (0-100)
405
- range=[0, 100]
406
- )),
407
- showlegend=True,
408
- title="Player Comparison Radar Chart"
409
- )
410
-
411
- return fig
412
-
413
- # Main app
414
  def main():
415
- st.markdown('<h1 class="main-header">🏀 NBA Analytics Hub</h1>', unsafe_allow_html=True)
416
-
417
- # Sidebar navigation
418
  st.sidebar.title("Navigation")
419
- page = st.sidebar.selectbox(
420
- "Choose Analysis Type",
421
- [
422
- "Player vs Player Comparison",
423
- "Team vs Team Analysis",
424
- "NBA Awards Predictor",
425
- "AI Chat & Insights",
426
- "Young Player Projections",
427
- "Similar Players Finder",
428
- "Roster Builder"
429
- ]
430
- )
431
-
432
- if page == "Player vs Player Comparison":
433
- player_comparison_page()
434
- elif page == "Team vs Team Analysis":
435
- team_comparison_page()
436
- elif page == "NBA Awards Predictor":
437
- awards_predictor_page()
438
- elif page == "AI Chat & Insights":
439
- ai_chat_page()
440
- elif page == "Young Player Projections":
441
- young_player_projections_page()
442
- elif page == "Similar Players Finder":
443
- similar_players_page()
444
- elif page == "Roster Builder":
445
- roster_builder_page()
446
-
447
- def player_comparison_page():
448
  st.markdown('<h2 class="section-header">Player vs Player Comparison</h2>', unsafe_allow_html=True)
449
-
450
- # Get all players
451
- all_players = get_all_players() # This now calls get_all_players_bbr()
452
- player_names = [player['full_name'] for player in all_players]
453
-
454
- col1, col2 = st.columns(2)
455
-
456
- with col1:
457
- selected_players = st.multiselect(
458
- "Select Players to Compare (up to 4)",
459
- player_names,
460
- max_selections=4
461
- )
462
-
463
- with col2:
464
- seasons = st.multiselect(
465
- "Select Seasons",
466
- list(BBR_SEASON_URL_MAP.keys()), # Use keys from the BBR season map
467
- default=["2023-24"]
468
- )
469
-
470
- # Add a button to trigger the comparison
471
- if st.button("Run Player Comparison"):
472
- if not selected_players:
473
- st.warning("Please select at least one player to compare.")
474
- return
475
-
476
- # Fetch and display stats
477
- stats_tabs = st.tabs(["Basic Stats", "Advanced Stats", "Visualizations"])
478
-
479
- with stats_tabs[0]:
480
- st.subheader("Basic Statistics")
481
- basic_stats_data = []
482
-
483
- for player_name in selected_players: # Iterate by name directly
484
- for season in seasons:
485
- stats_df = get_player_stats(player_name, season) # Pass name and season
486
- if not stats_df.empty:
487
- # BBR returns one row per season, so no need to mean()
488
- # Ensure numeric columns are actually numeric
489
- for col in ['GP', 'MIN', 'PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', 'FT_PCT', 'FG3_PCT']:
490
- if col in stats_df.columns:
491
- stats_df[col] = pd.to_numeric(stats_df[col], errors='coerce')
492
- basic_stats_data.append(stats_df.iloc[0].to_dict()) # Take the first (and only) row
493
-
494
- if basic_stats_data:
495
- comparison_df = pd.DataFrame(basic_stats_data)
496
- basic_cols = ['PLAYER_NAME', 'SEASON_ID', 'GP', 'MIN', 'PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', 'FT_PCT', 'FG3_PCT']
497
- display_cols = [col for col in basic_cols if col in comparison_df.columns]
498
- st.dataframe(comparison_df[display_cols].round(2), use_container_width=True)
499
- else:
500
- st.info("No data available for the selected players and seasons.")
501
-
502
- with stats_tabs[1]:
503
- st.subheader("Advanced Statistics")
504
- if basic_stats_data:
505
- advanced_df = pd.DataFrame(basic_stats_data).copy()
506
- # Ensure numeric columns for calculations
507
- for col in ['PTS', 'FGA', 'FTA']:
508
- if col in advanced_df.columns:
509
- advanced_df[col] = pd.to_numeric(advanced_df[col], errors='coerce')
510
-
511
- # Calculate TS% (True Shooting Percentage)
512
- if all(col in advanced_df.columns for col in ['PTS', 'FGA', 'FTA']):
513
- advanced_df['TS_PCT'] = advanced_df.apply(
514
- lambda row: row['PTS'] / (2 * (row['FGA'] + 0.44 * row['FTA'])) if (row['FGA'] + 0.44 * row['FTA']) != 0 else 0,
515
- axis=1
516
- )
517
-
518
- advanced_cols = ['PLAYER_NAME', 'SEASON_ID', 'PTS', 'REB', 'AST', 'FG_PCT', 'TS_PCT'] if 'TS_PCT' in advanced_df.columns else ['PLAYER_NAME', 'SEASON_ID', 'PTS', 'REB', 'AST', 'FG_PCT']
519
- display_cols = [col for col in advanced_cols if col in advanced_df.columns]
520
- st.dataframe(advanced_df[display_cols].round(3), use_container_width=True)
521
- else:
522
- st.info("No data available for advanced statistics.")
523
-
524
- with stats_tabs[2]:
525
- st.subheader("Player Comparison Charts")
526
-
527
- if basic_stats_data:
528
- comparison_df = pd.DataFrame(basic_stats_data)
529
- metrics = ['PTS', 'REB', 'AST', 'FG_PCT']
530
- available_metrics = [m for m in metrics if m in comparison_df.columns]
531
-
532
- if available_metrics:
533
- selected_metric = st.selectbox("Select Metric to Visualize", available_metrics)
534
-
535
- if selected_metric:
536
- # Bar chart comparison (for average over selected seasons if multiple seasons selected)
537
- # Or for each season if only one player selected
538
- if len(selected_players) == 1 and len(seasons) > 1:
539
- # Show trend over seasons for one player
540
- fig = px.line(
541
- comparison_df[comparison_df['PLAYER_NAME'] == selected_players[0]],
542
- x='SEASON_ID',
543
- y=selected_metric,
544
- title=f"{selected_players[0]} - {selected_metric} Trend",
545
- markers=True
546
- )
547
- else:
548
- # Average over selected seasons for multiple players for bar chart
549
- avg_comparison_df = comparison_df.groupby('PLAYER_NAME')[available_metrics].mean().reset_index()
550
- fig = px.bar(
551
- avg_comparison_df,
552
- x='PLAYER_NAME',
553
- y=selected_metric,
554
- title=f"Average {selected_metric} Comparison (Selected Seasons)",
555
- color='PLAYER_NAME'
556
- )
557
- st.plotly_chart(fig, use_container_width=True)
558
-
559
- # Radar chart for multi-metric comparison
560
- radar_metrics_for_chart = ['PTS', 'REB', 'AST', 'STL', 'BLK']
561
- radar_metrics_for_chart = [m for m in radar_metrics_for_chart if m in comparison_df.columns]
562
-
563
- if len(radar_metrics_for_chart) >= 3:
564
- radar_data = {}
565
- # Use the averaged data for radar chart if multiple seasons
566
- if len(seasons) > 1:
567
- radar_source_df = comparison_df.groupby('PLAYER_NAME')[radar_metrics_for_chart].mean().reset_index()
568
- else:
569
- radar_source_df = comparison_df.copy()
570
-
571
- scaled_radar_df = radar_source_df.copy()
572
-
573
- # Simple min-max scaling for radar chart visualization (0-100)
574
- for col in radar_metrics_for_chart:
575
- min_val = scaled_radar_df[col].min()
576
- max_val = scaled_radar_df[col].max()
577
- if max_val > min_val:
578
- scaled_radar_df[col] = ((scaled_radar_df[col] - min_val) / (max_val - min_val)) * 100
579
- else:
580
- scaled_radar_df[col] = 0 # Default if all values are the same
581
-
582
- for _, row in scaled_radar_df.iterrows():
583
- radar_data[row['PLAYER_NAME']] = {
584
- metric: row[metric] for metric in radar_metrics_for_chart
585
- }
586
-
587
- if radar_data:
588
- radar_fig = create_radar_chart(radar_data, radar_metrics_for_chart)
589
- st.plotly_chart(radar_fig, use_container_width=True)
590
- else:
591
- st.info("Could not generate radar chart data.")
592
- else:
593
- st.info("Select at least 3 common metrics for a radar chart (e.g., PTS, REB, AST, STL, BLK).")
594
- else:
595
- st.info("No common metrics available for visualization.")
596
- else:
597
- st.info("No data available for visualizations.")
598
-
599
-
600
- def team_comparison_page():
601
  st.markdown('<h2 class="section-header">Team vs Team Analysis</h2>', unsafe_allow_html=True)
602
-
603
- all_teams = get_all_teams() # This now calls get_all_teams_bbr()
604
- team_names = [team['full_name'] for team in all_teams]
605
-
606
- col1, col2 = st.columns(2)
607
-
608
- with col1:
609
- selected_teams = st.multiselect(
610
- "Select Teams to Compare",
611
- team_names,
612
- max_selections=4
613
- )
614
-
615
- with col2:
616
- seasons = st.multiselect(
617
- "Select Seasons",
618
- list(BBR_SEASON_URL_MAP.keys()), # Use keys from the BBR season map
619
- default=["2023-24"]
620
- )
621
-
622
- # Add a button to trigger the comparison
623
- if st.button("Run Team Comparison"):
624
- if not selected_teams:
625
- st.warning("Please select at least one team to compare.")
626
- return
627
-
628
- team_stats_data = []
629
-
630
- for team_name in selected_teams:
631
- for season in seasons:
632
- stats_df = get_team_stats(team_name, season) # Pass name and season
633
- if not stats_df.empty:
634
- # Ensure numeric columns are actually numeric
635
- for col in ['PTS', 'REB', 'AST', 'FG_PCT', 'FG3_PCT', 'FT_PCT']:
636
- if col in stats_df.columns:
637
- stats_df[col] = pd.to_numeric(stats_df[col], errors='coerce')
638
- team_stats_data.append(stats_df.iloc[0].to_dict()) # Take the first (and only) row
639
-
640
- if team_stats_data:
641
- team_df = pd.DataFrame(team_stats_data)
642
-
643
- # Display team comparison
644
- st.subheader("Team Statistics Comparison")
645
- team_cols = ['TEAM_NAME', 'SEASON', 'PTS', 'REB', 'AST', 'FG_PCT', 'FG3_PCT', 'FT_PCT']
646
- display_cols = [col for col in team_cols if col in team_df.columns]
647
- st.dataframe(team_df[display_cols].round(2), use_container_width=True)
648
-
649
- # Visualization
650
- st.subheader("Team Performance Visualization")
651
- metric_options = ['PTS', 'REB', 'AST', 'FG_PCT']
652
- available_metrics = [m for m in metric_options if m in team_df.columns]
653
-
654
- if available_metrics:
655
- selected_metric = st.selectbox("Select Metric", available_metrics)
656
-
657
- fig = px.bar(
658
- team_df,
659
- x='TEAM_NAME',
660
- y=selected_metric,
661
- color='SEASON',
662
- title=f"Team {selected_metric} Comparison",
663
- barmode='group'
664
- )
665
- st.plotly_chart(fig, use_container_width=True)
666
- else:
667
- st.info("No common metrics available for visualization.")
668
- else:
669
- st.info("No data available for the selected teams and seasons.")
670
-
671
-
672
- def awards_predictor_page():
673
  st.markdown('<h2 class="section-header">NBA Awards Predictor</h2>', unsafe_allow_html=True)
674
-
675
- award_type = st.selectbox(
676
- "Select Award Type",
677
- ["MVP", "Defensive Player of the Year", "Rookie of the Year", "6th Man of the Year", "All-NBA First Team"]
678
- )
679
-
680
- st.subheader(f"{award_type} Prediction Criteria")
681
-
682
- # Define criteria for different awards
683
  criteria = {}
684
- if award_type == "MVP":
685
- criteria = {
686
- "Points Per Game": st.slider("Minimum PPG", 15.0, 35.0, 25.0),
687
- "Team Wins": st.slider("Minimum Team Wins", 35, 70, 50),
688
- "Player Efficiency Rating": st.slider("Minimum PER", 15.0, 35.0, 25.0),
689
- "Win Shares": st.slider("Minimum Win Shares", 5.0, 20.0, 10.0)
690
- }
691
- elif award_type == "Defensive Player of the Year":
692
- criteria = {
693
- "Blocks Per Game": st.slider("Minimum BPG", 0.0, 4.0, 1.5),
694
- "Steals Per Game": st.slider("Minimum SPG", 0.0, 3.0, 1.0),
695
- "Defensive Rating": st.slider("Maximum Defensive Rating", 90.0, 120.0, 105.0),
696
- "Team Defensive Ranking": st.slider("Maximum Team Def Rank", 1, 30, 10)
697
- }
698
- else: # Default for Rookie of the Year, 6th Man, All-NBA
699
- criteria = {
700
- "Points Per Game": st.slider("Minimum PPG", 10.0, 30.0, 15.0),
701
- "Games Played": st.slider("Minimum Games", 50, 82, 65),
702
- "Shooting Efficiency": st.slider("Minimum FG%", 0.35, 0.65, 0.45)
703
- }
704
 
705
  if st.button("Generate Predictions"):
706
- prompt = f"""
707
- Based on the following criteria for {award_type}, analyze current NBA players and provide predictions:
708
-
709
- Criteria: {criteria}
710
-
711
- Please provide:
712
- 1. Top 5 candidates with their stats
713
- 2. Analysis of why each candidate fits the criteria
714
- 3. Your prediction for the winner with reasoning
715
-
716
- Focus on current 2023-24 season performance and recent trends.
717
- """
718
-
719
- prediction = get_perplexity_response(PERPLEXITY_API_KEY, prompt, max_tokens=800, system_message="You are an NBA awards prediction expert AI.")
720
- if prediction:
721
- st.markdown("### AI Prediction Analysis")
722
- st.write(prediction)
723
-
724
- def ai_chat_page():
725
- st.markdown('<h2 class="section-header">AI NBA Chat & Insights</h2>', unsafe_allow_html=True)
726
-
727
- # Chat interface
728
- st.subheader("Ask AI About NBA Stats and Insights")
729
-
730
- # Display chat history
731
- for message in st.session_state.chat_history:
732
- with st.chat_message(message["role"]):
733
- st.write(message["content"])
734
-
735
- # Chat input
736
- if prompt := st.chat_input("Ask about NBA players, teams, stats, or strategies..."):
737
- # Add user message to chat history
738
- st.session_state.chat_history.append({"role": "user", "content": prompt})
739
-
740
- # Display user message
741
- with st.chat_message("user"):
742
- st.write(prompt)
743
-
744
- # Generate AI response
745
  with st.chat_message("assistant"):
746
- # Enhance prompt with NBA context
747
- enhanced_prompt = f"""
748
- As an NBA expert analyst, please answer this question about basketball:
749
-
750
- {prompt}
751
-
752
- Please provide detailed analysis with current stats, trends, and insights when relevant.
753
- If specific player or team stats are mentioned, include recent performance data.
754
- """
755
-
756
- response = get_perplexity_response(PERPLEXITY_API_KEY, enhanced_prompt, max_tokens=700, system_message="You are an NBA expert analyst AI.")
757
- if response:
758
- st.write(response)
759
- # Add assistant response to chat history
760
- st.session_state.chat_history.append({"role": "assistant", "content": response})
761
- else:
762
- st.session_state.chat_history.append({"role": "assistant", "content": "Sorry, I couldn't get a response from the AI."})
763
-
764
-
765
- # Quick action buttons
766
- st.subheader("Quick Insights")
767
- col1, col2, col3 = st.columns(3)
768
-
769
- with col1:
770
- if st.button("🏆 Championship Contenders"):
771
- prompt = "Analyze the current NBA championship contenders for 2024. Who are the top 5 teams and why?"
772
- response = get_perplexity_response(PERPLEXITY_API_KEY, prompt, system_message="You are an NBA expert analyst AI.")
773
- if response:
774
- st.write(response)
775
-
776
- with col2:
777
- if st.button("⭐ Rising Stars"):
778
- prompt = "Who are the most promising young NBA players to watch in 2024? Focus on players 23 and under."
779
- response = get_perplexity_response(PERPLEXITY_API_KEY, prompt, system_message="You are an NBA expert analyst AI.")
780
- if response:
781
- st.write(response)
782
-
783
- with col3:
784
- if st.button("📊 Trade Analysis"):
785
- prompt = "What are some potential NBA trades that could happen this season? Analyze team needs and available players."
786
- response = get_perplexity_response(PERPLEXITY_API_KEY, prompt, system_message="You are an NBA expert analyst AI.")
787
- if response:
788
- st.write(response)
789
-
790
- def young_player_projections_page():
791
  st.markdown('<h2 class="section-header">Young Player Projections</h2>', unsafe_allow_html=True)
792
-
793
- # Player selection
794
- all_players = get_all_players()
795
- player_names = [player['full_name'] for player in all_players]
796
-
797
- selected_player = st.selectbox("Select Young Player (or enter manually)", [""] + player_names)
798
-
799
- if not selected_player:
800
- manual_player = st.text_input("Enter Player Name Manually")
801
- if manual_player:
802
- selected_player = manual_player
803
-
804
- if selected_player:
805
- col1, col2 = st.columns(2)
806
-
807
- with col1:
808
- current_age = st.number_input("Current Age", min_value=18, max_value=25, value=21)
809
- years_in_league = st.number_input("Years in NBA", min_value=0, max_value=7, value=2)
810
-
811
- with col2:
812
- current_ppg = st.number_input("Current PPG", min_value=0.0, max_value=40.0, value=15.0)
813
- current_rpg = st.number_input("Current RPG", min_value=0.0, max_value=20.0, value=5.0)
814
- current_apg = st.number_input("Current APG", min_value=0.0, max_value=15.0, value=3.0)
815
-
816
- if st.button("Generate AI Projection"):
817
- prompt = f"""
818
- Analyze and project the future potential of NBA player {selected_player}:
819
-
820
- Current Stats:
821
- - Age: {current_age}
822
- - Years in NBA: {years_in_league}
823
- - PPG: {current_ppg}
824
- - RPG: {current_rpg}
825
- - APG: {current_apg}
826
-
827
- Please provide:
828
- 1. 3-year projection of their stats
829
- 2. Peak potential analysis
830
- 3. Areas for improvement
831
- 4. Comparison to similar players at the same age
832
- 5. Career trajectory prediction
833
-
834
- Base your analysis on historical player development patterns and current NBA trends.
835
- """
836
-
837
- projection = get_perplexity_response(PERPLEXITY_API_KEY, prompt, max_tokens=800, system_message="You are an NBA player projection expert AI.")
838
- if projection:
839
- st.markdown("### AI Player Projection")
840
- st.write(projection)
841
-
842
- # Create a simple projection visualization
843
- years = [f"Year {i+1}" for i in range(5)]
844
- projected_ppg = [current_ppg * (1 + 0.1 * i) for i in range(5)] # Simple growth model
845
-
846
- fig = go.Figure()
847
- fig.add_trace(go.Scatter(
848
- x=years,
849
- y=projected_ppg,
850
- mode='lines+markers',
851
- name='Projected PPG',
852
- line=dict(width=3, color='blue')
853
- ))
854
-
855
- fig.update_layout(
856
- title=f"{selected_player} - PPG Projection",
857
- xaxis_title="Years",
858
- yaxis_title="Points Per Game",
859
- height=400
860
- )
861
-
862
- st.plotly_chart(fig, use_container_width=True)
863
-
864
- def similar_players_page():
865
- st.markdown('<h2 class="section-header">Find Similar Players</h2>', unsafe_allow_html=True)
866
-
867
- all_players = get_all_players()
868
- player_names = [player['full_name'] for player in all_players]
869
-
870
- target_player = st.selectbox("Select Target Player", player_names)
871
-
872
- similarity_criteria = st.multiselect(
873
- "Select Similarity Criteria",
874
- ["Position", "Height/Weight", "Playing Style", "Statistical Profile", "Age/Experience"],
875
- default=["Playing Style", "Statistical Profile"]
876
- )
877
-
878
- if target_player and similarity_criteria:
879
- if st.button("Find Similar Players"):
880
- prompt = f"""
881
- Find NBA players similar to {target_player} based on the following criteria:
882
- {', '.join(similarity_criteria)}
883
-
884
- Please provide:
885
- 1. Top 5 most similar current NBA players
886
- 2. Top 3 historical comparisons
887
- 3. Explanation of similarities for each player
888
- 4. Key differences that distinguish them
889
- 5. Playing style analysis
890
-
891
- Focus on both statistical similarities and playing style/role similarities.
892
- """
893
-
894
- similar_players = get_perplexity_response(PERPLEXITY_API_KEY, prompt, max_tokens=800, system_message="You are an NBA player similarity expert AI.")
895
- if similar_players:
896
- st.markdown("### Similar Players Analysis")
897
- st.write(similar_players)
898
-
899
- # Alternative: Manual similarity finder
900
- st.subheader("Manual Player Comparison Tool")
901
-
902
- col1, col2 = st.columns(2)
903
-
904
- with col1:
905
- player1 = st.selectbox("Player 1", player_names, key="sim1")
906
-
907
- with col2:
908
- player2 = st.selectbox("Player 2", player_names, key="sim2")
909
-
910
- if player1 and player2 and player1 != player2:
911
- if st.button("Compare Players"):
912
- prompt = f"""
913
- Compare {player1} and {player2} in detail:
914
-
915
- Please analyze:
916
- 1. Statistical comparison (current season)
917
- 2. Playing style similarities and differences
918
- 3. Strengths and weaknesses of each
919
- 4. Team impact and role
920
- 5. Overall similarity score (1-10)
921
-
922
- Provide a comprehensive comparison with specific examples.
923
- """
924
-
925
- comparison = get_perplexity_response(PERPLEXITY_API_KEY, prompt, max_tokens=700, system_message="You are an NBA player comparison expert AI.")
926
- if comparison:
927
- st.markdown("### Player Comparison Analysis")
928
- st.write(comparison)
929
-
930
- def roster_builder_page():
931
  st.markdown('<h2 class="section-header">NBA Roster Builder</h2>', unsafe_allow_html=True)
932
-
933
- st.subheader("Build Your Ideal NBA Roster")
934
-
935
- # Roster building parameters
936
- col1, col2 = st.columns(2)
937
-
938
- with col1:
939
- salary_cap = st.number_input("Salary Cap (Millions)", min_value=100, max_value=200, value=136)
940
- team_strategy = st.selectbox(
941
- "Team Strategy",
942
- ["Championship Contender", "Young Core Development", "Balanced Veteran Mix", "Small Ball", "Defense First"]
943
- )
944
-
945
- with col2:
946
- key_positions = st.multiselect(
947
- "Priority Positions",
948
- ["Point Guard", "Shooting Guard", "Small Forward", "Power Forward", "Center"],
949
- default=["Point Guard", "Center"]
950
- )
951
-
952
- # Player budget allocation
953
  st.subheader("Budget Allocation")
954
- position_budgets = {}
955
-
956
- positions = ["PG", "SG", "SF", "PF", "C"]
957
  cols = st.columns(5)
958
-
959
- total_allocated = 0
960
- for i, pos in enumerate(positions):
961
- with cols[i]:
962
- budget = st.number_input(f"{pos} Budget ($M)", min_value=0, max_value=50, value=20, key=f"budget_{pos}")
963
- position_budgets[pos] = budget
964
- total_allocated += budget
965
-
966
- st.write(f"Total Allocated: ${total_allocated}M / ${salary_cap}M")
967
-
968
- if total_allocated > salary_cap:
969
- st.error("Budget exceeds salary cap!")
970
-
971
- # Generate roster suggestions
972
- if st.button("Generate Roster Suggestions"):
973
- if total_allocated <= salary_cap:
974
- prompt = f"""
975
- Build an NBA roster with the following constraints:
976
-
977
- - Salary Cap: ${salary_cap} million
978
- - Team Strategy: {team_strategy}
979
- - Priority Positions: {', '.join(key_positions)}
980
- - Position Budgets: {position_budgets}
981
-
982
- Please provide:
983
- 1. Starting lineup with specific player recommendations
984
- 2. Key bench players (6th man, backup center, etc.)
985
- 3. Total estimated salary breakdown
986
- 4. Rationale for each major signing
987
- 5. How this roster fits the chosen strategy
988
- 6. Potential weaknesses and how to address them
989
-
990
- Focus on realistic player availability and current market values.
991
- """
992
-
993
- roster_suggestions = get_perplexity_response(PERPLEXITY_API_KEY, prompt, max_tokens=900, system_message="You are an NBA roster building expert AI.")
994
- if roster_suggestions:
995
- st.markdown("### AI Roster Recommendations")
996
- st.write(roster_suggestions)
997
  else:
998
- st.warning("Please adjust your budget to be within the salary cap before generating suggestions.")
999
-
1000
- # Trade scenario analyzer
1001
- st.subheader("Trade Scenario Analyzer")
1002
-
1003
- trade_team1 = st.text_input("Team 1 Trading:")
1004
- trade_team2 = st.text_input("Team 2 Trading:")
1005
-
1006
- if trade_team1 and trade_team2:
1007
- if st.button("Analyze Trade"):
1008
- prompt = f"""
1009
- Analyze this potential NBA trade:
1010
-
1011
- Team 1 trades: {trade_team1}
1012
- Team 2 trades: {trade_team2}
1013
-
1014
- Please evaluate:
1015
- 1. Fair value assessment
1016
- 2. How this trade helps each team
1017
- 3. Salary cap implications
1018
- 4. Impact on team chemistry and performance
1019
- 5. Likelihood of this trade happening
1020
- 6. Alternative trade suggestions
1021
-
1022
- Consider current team needs and player contracts.
1023
- """
1024
-
1025
- trade_analysis = get_perplexity_response(PERPLEXITY_API_KEY, prompt, max_tokens=700, system_message="You are an NBA trade analysis expert AI.")
1026
- if trade_analysis:
1027
- st.markdown("### Trade Analysis")
1028
- st.write(trade_analysis)
1029
 
1030
  if __name__ == "__main__":
1031
  main()
 
1
  import streamlit as st
2
  import pandas as pd
3
  import numpy as np
 
 
 
4
  import requests
 
 
 
 
 
5
  import os
6
+ from datetime import datetime
7
 
8
+ # —————————————————————————————————————————————————————————————————————————————
9
+ # CSS (black & white theme)
 
 
 
 
 
 
 
 
 
 
10
  st.markdown("""
11
  <style>
12
+ .main-header {font-size:3rem; font-weight:bold; text-align:center; color:#000;}
13
+ .section-header {font-size:1.5rem; font-weight:bold; color:#333; margin:1rem 0;}
14
+ table.dataframe {width:100%;}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
15
  </style>
16
  """, unsafe_allow_html=True)
17
 
18
+ # —————————————————————————————————————————————————————————————————————————————
19
+ # Caching helpers
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  @st.cache_data(ttl=3600)
21
+ def fetch_table(url, idx=0):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  try:
23
+ resp = requests.get(url, timeout=20)
24
+ resp.raise_for_status()
25
+ dfs = pd.read_html(resp.text)
26
+ return dfs[idx]
 
 
 
 
 
 
 
 
 
 
 
 
27
  except Exception as e:
28
+ st.error(f"Failed to fetch {url}: {e}")
29
  return pd.DataFrame()
30
 
31
+ # —————————————————————————————————————————————————————————————————————————————
32
+ # Basketball-Reference scrapers
 
 
 
33
 
34
+ @st.cache_data(ttl=3600)
35
+ def get_player_index():
36
+ base = "https://www.basketball-reference.com/players/"
37
+ rows = []
38
+ for letter in map(chr, range(ord('a'), ord('z')+1)):
39
+ df = fetch_table(f"{base}{letter}/")
40
+ if df.empty: continue
41
+ for _, r in df.iterrows():
42
+ raw = r['Player']
43
+ href = raw.split('href="')[1].split('"')[0]
44
+ name = raw.split('>')[1].split('<')[0]
45
+ rows.append({'name': name, 'url': f"https://www.basketball-reference.com{href}"})
46
+ return pd.DataFrame(rows)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
 
48
  @st.cache_data(ttl=300)
49
+ def player_season_stats(bbr_url):
50
+ df = fetch_table(bbr_url, 0)
51
+ if 'Season' not in df.columns:
 
 
 
 
 
52
  return pd.DataFrame()
53
+ df = df[df['Season']!='Season']
54
+ df['Season'] = df['Season'].astype(str)
55
+ nonnum = ['Season','Tm','Lg','Pos']
56
+ for c in df.columns.difference(nonnum):
57
+ df[c] = pd.to_numeric(df[c], errors='coerce')
58
+ return df
59
 
60
+ @st.cache_data(ttl=300)
61
+ def team_per_game(year):
62
+ url = f"https://www.basketball-reference.com/leagues/NBA_{year}_per_game.html"
63
+ df = fetch_table(url)
64
+ if df.empty: return df
65
+ df = df[df['Player']!='Player']
66
+ df.rename(columns={'Team':'Tm'}, inplace=True)
67
+ for c in df.columns.difference(['Player','Pos','Tm']):
68
+ df[c] = pd.to_numeric(df[c], errors='coerce')
69
+ return df
70
+
71
+ # —————————————————————————————————————————————————————————————————————————————
72
+ # Perplexity integration (unchanged)
73
+ PERP_KEY = os.getenv("PERPLEXITY_API_KEY")
74
+ PERP_URL = "https://api.perplexity.ai/chat/completions"
75
+
76
+ def ask_perp(prompt, system="You are a helpful NBA analyst AI.", max_tokens=500, temp=0.2):
77
+ if not PERP_KEY:
78
+ st.error("Set PERPLEXITY_API_KEY env var.")
79
+ return ""
80
+ hdr = {'Authorization':f'Bearer {PERP_KEY}','Content-Type':'application/json'}
81
+ payload = {
82
+ "model":"sonar-pro",
83
+ "messages":[{"role":"system","content":system},{"role":"user","content":prompt}],
84
+ "max_tokens":max_tokens, "temperature":temp
85
+ }
86
  try:
87
+ r = requests.post(PERP_URL, json=payload, headers=hdr, timeout=45)
88
+ r.raise_for_status()
89
+ return r.json().get("choices", [{}])[0].get("message",{}).get("content","")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  except Exception as e:
91
+ st.error(f"Perplexity error: {e}")
92
+ return ""
93
 
94
+ # —————————————————————————————————————————————————————————————————————————————
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  def main():
96
+ st.markdown('<h1 class="main-header">🏀 NBA Analytics Hub (BBR Edition)</h1>', unsafe_allow_html=True)
 
 
97
  st.sidebar.title("Navigation")
98
+ page = st.sidebar.radio("", [
99
+ "Player vs Player Comparison", "Team vs Team Analysis",
100
+ "NBA Awards Predictor", "AI Chat & Insights",
101
+ "Young Player Projections", "Similar Players Finder",
102
+ "Roster Builder", "Trade Scenario Analyzer"
103
+ ])
104
+
105
+ if page == "Player vs Player Comparison": player_vs_player()
106
+ elif page == "Team vs Team Analysis": team_vs_team()
107
+ elif page == "NBA Awards Predictor": awards_predictor()
108
+ elif page == "AI Chat & Insights": ai_chat()
109
+ elif page == "Young Player Projections": young_projections()
110
+ elif page == "Similar Players Finder": similar_players()
111
+ elif page == "Roster Builder": roster_builder()
112
+ else: trade_analyzer()
113
+
114
+ # —————————————————————————————————————————————————————————————————————————————
115
+ def player_vs_player():
 
 
 
 
 
 
 
 
 
 
 
116
  st.markdown('<h2 class="section-header">Player vs Player Comparison</h2>', unsafe_allow_html=True)
117
+ idx = get_player_index()
118
+ names = idx['name'].tolist()
119
+ sel = st.multiselect("Select Players (up to 4)", names, max_selections=4)
120
+ seasons = st.multiselect("Select Seasons", ["2023–24","2022–23","2021–22","2020–21"], default=["2023–24"])
121
+
122
+ if st.button("Run Comparison"):
123
+ if not sel: return st.warning("Pick at least one player.")
124
+ stats = []
125
+ for p in sel:
126
+ url = idx.loc[idx.name==p,'url'].iat[0]
127
+ df = player_season_stats(url)
128
+ df['Season'] = df['Season'].str.replace('-','–')
129
+ df = df[df['Season'].isin(seasons)]
130
+ if df.empty: continue
131
+ avg = df.mean(numeric_only=True).to_frame().T
132
+ avg['Player'] = p
133
+ stats.append(avg)
134
+ if not stats: return st.info("No data.")
135
+ comp = pd.concat(stats, ignore_index=True)
136
+ cols = ['Player','PTS','TRB','AST','STL','BLK','FG%','3P%','FT%']
137
+ st.dataframe(comp[cols].round(2), use_container_width=True)
138
+
139
+ def team_vs_team():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
140
  st.markdown('<h2 class="section-header">Team vs Team Analysis</h2>', unsafe_allow_html=True)
141
+ year = st.selectbox("Season End Year", [2024,2023,2022,2021], index=0)
142
+ tm_df = team_per_game(year)
143
+ teams = tm_df['Tm'].unique().tolist()
144
+ sel = st.multiselect("Select Teams (up to 4)", teams, max_selections=4)
145
+
146
+ if st.button("Run Comparison"):
147
+ if not sel: return st.warning("Pick at least one team.")
148
+ stats = []
149
+ for t in sel:
150
+ df = tm_df[tm_df.Tm==t]
151
+ if df.empty: continue
152
+ avg = df.mean(numeric_only=True).to_frame().T
153
+ avg['Team'] = t
154
+ stats.append(avg)
155
+ if not stats: return st.info("No data.")
156
+ comp = pd.concat(stats, ignore_index=True)
157
+ cols = ['Team','PTS','TRB','AST','STL','BLK','FG%','3P%','FT%']
158
+ st.dataframe(comp[cols].round(2), use_container_width=True)
159
+
160
+ def awards_predictor():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
161
  st.markdown('<h2 class="section-header">NBA Awards Predictor</h2>', unsafe_allow_html=True)
162
+ award = st.selectbox("Select Award", ["MVP","Defensive Player of the Year","Rookie of the Year","6th Man of the Year","All-NBA First Team"])
163
+ st.subheader(f"{award} Criteria")
164
+ # (same sliders as before…)
 
 
 
 
 
 
165
  criteria = {}
166
+ if award=="MVP":
167
+ criteria = {
168
+ "PPG":st.slider("Min PPG",15.0,35.0,25.0),
169
+ "Wins":st.slider("Min Team Wins",35,70,50),
170
+ "PER":st.slider("Min PER",15.0,35.0,25.0),
171
+ "WS":st.slider("Min Win Shares",5.0,20.0,10.0)
172
+ }
173
+ elif award=="Defensive Player of the Year":
174
+ criteria = {
175
+ "BPG":st.slider("Min BPG",0.0,4.0,1.5),
176
+ "SPG":st.slider("Min SPG",0.0,3.0,1.0),
177
+ "DefRtgMax":st.slider("Max Def Rating",90.0,120.0,105.0),
178
+ "DefRankMax":st.slider("Max Team Def Rank",1,30,10)
179
+ }
180
+ else:
181
+ criteria = {
182
+ "PPG":st.slider("Min PPG",10.0,30.0,15.0),
183
+ "Games":st.slider("Min Games",50,82,65),
184
+ "FG%":st.slider("Min FG%",0.35,0.65,0.45)
185
+ }
186
 
187
  if st.button("Generate Predictions"):
188
+ p = f"Predict top 5 {award} based on {criteria}. Focus on 2023-24 season."
189
+ resp = ask_perp(p, system="You are an NBA awards expert AI.", max_tokens=800)
190
+ st.markdown("### Predictions")
191
+ st.write(resp)
192
+
193
+ def ai_chat():
194
+ st.markdown('<h2 class="section-header">AI Chat & Insights</h2>', unsafe_allow_html=True)
195
+ if 'history' not in st.session_state: st.session_state.history=[]
196
+ for msg in st.session_state.history:
197
+ with st.chat_message(msg["role"]):
198
+ st.write(msg["content"])
199
+ if prompt:=st.chat_input("Ask me anything about NBA…"):
200
+ st.session_state.history.append({"role":"user","content":prompt})
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  with st.chat_message("assistant"):
202
+ ans = ask_perp(prompt, system="You are an NBA expert analyst AI.", max_tokens=700)
203
+ st.write(ans)
204
+ st.session_state.history.append({"role":"assistant","content":ans})
205
+ st.subheader("Quick Actions")
206
+ c1,c2,c3 = st.columns(3)
207
+ if c1.button("🏆 Contenders"):
208
+ st.write(ask_perp("Top 5 championship contenders for 2024 and why?"))
209
+ if c2.button("⭐ Rising Stars"):
210
+ st.write(ask_perp("Most promising NBA players age ≤23 in 2024?"))
211
+ if c3.button("📊 Trades"):
212
+ st.write(ask_perp("Potential NBA trades this season with analysis."))
213
+
214
+ def young_projections():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  st.markdown('<h2 class="section-header">Young Player Projections</h2>', unsafe_allow_html=True)
216
+ all_p = get_player_index()['name'].tolist()
217
+ sp = st.selectbox("Select or enter player", [""]+all_p)
218
+ if not sp:
219
+ sp = st.text_input("Enter player name manually")
220
+ if sp:
221
+ age = st.number_input("Current Age",18,25,21)
222
+ yrs = st.number_input("Years in NBA",0,7,2)
223
+ ppg = st.number_input("PPG",0.0,40.0,15.0)
224
+ rpg = st.number_input("RPG",0.0,20.0,5.0)
225
+ apg = st.number_input("APG",0.0,15.0,3.0)
226
+ if st.button("Project"):
227
+ prompt = (
228
+ f"Project {sp}'s next 5-year stats based on Age={age}, "
229
+ f"Yrs={yrs}, PPG={ppg}, RPG={rpg}, APG={apg}."
230
+ )
231
+ out = ask_perp(prompt, system="You are an NBA projection expert AI.", max_tokens=800)
232
+ st.markdown("### Projection Analysis")
233
+ st.write(out)
234
+ yrs_lbl = [f"Year {i+1}" for i in range(5)]
235
+ vals = [ppg*(1+0.1*i) for i in range(5)]
236
+ st.line_chart({'PPG':vals}, x=yrs_lbl)
237
+
238
+ def similar_players():
239
+ st.markdown('<h2 class="section-header">Similar Players Finder</h2>', unsafe_allow_html=True)
240
+ all_p = get_player_index()['name'].tolist()
241
+ tp = st.selectbox("Target Player", all_p)
242
+ crit = st.multiselect("Criteria",["Position","Height/Weight","Style","Stats","Experience"],default=["Style","Stats"])
243
+ if tp and crit and st.button("Find Similar"):
244
+ prompt = f"Find top 5 current and top 3 historical similar to {tp} by {crit}."
245
+ st.write(ask_perp(prompt, system="You are a similarity expert AI.", max_tokens=800))
246
+ st.subheader("Manual Compare")
247
+ p1 = st.selectbox("Player 1", all_p, key="p1")
248
+ p2 = st.selectbox("Player 2", all_p, key="p2")
249
+ if p1 and p2 and p1!=p2 and st.button("Compare Players"):
250
+ prompt = f"Compare {p1} vs {p2} on stats, style, strengths/weaknesses, team impact."
251
+ st.write(ask_perp(prompt, system="You are a comparison expert AI.", max_tokens=700))
252
+
253
+ def roster_builder():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
254
  st.markdown('<h2 class="section-header">NBA Roster Builder</h2>', unsafe_allow_html=True)
255
+ cap = st.number_input("Salary Cap (M)",100,200,136)
256
+ strat = st.selectbox("Strategy",["Champ Contender","Young Development","Balanced","Small Ball","Defense First"])
257
+ pos = st.multiselect("Priority Positions",["PG","SG","SF","PF","C"],default=["PG","C"])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
258
  st.subheader("Budget Allocation")
 
 
 
259
  cols = st.columns(5)
260
+ alloc = {}
261
+ total = 0
262
+ for i,p in enumerate(["PG","SG","SF","PF","C"]):
263
+ val = cols[i].number_input(f"{p} Budget ($M)",0,50,20, key=f"b{p}")
264
+ alloc[p]=val; total+=val
265
+ st.write(f"Total: ${total}M / ${cap}M")
266
+ if total>cap: st.error("Over cap!")
267
+
268
+ if st.button("Generate Roster"):
269
+ if total<=cap:
270
+ prompt = (
271
+ f"Build roster with cap=${cap}M, strat={strat}, "
272
+ f"priority={pos}, budgets={alloc}."
273
+ )
274
+ st.markdown("### Suggestions")
275
+ st.write(ask_perp(prompt, system="You are a roster builder AI.", max_tokens=900))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
276
  else:
277
+ st.warning("Adjust budgets under cap.")
278
+
279
+ def trade_analyzer():
280
+ st.markdown('<h2 class="section-header">Trade Scenario Analyzer</h2>', unsafe_allow_html=True)
281
+ t1 = st.text_input("Team 1 trades")
282
+ t2 = st.text_input("Team 2 trades")
283
+ if t1 and t2 and st.button("Analyze Trade"):
284
+ prompt = (
285
+ f"Team1 trades: {t1}. Team2 trades: {t2}. "
286
+ "Assess fairness, cap, impact, chemistry, likelihood, alternatives."
287
+ )
288
+ st.write(ask_perp(prompt, system="You are a trade analysis AI.", max_tokens=700))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
289
 
290
  if __name__ == "__main__":
291
  main()