rairo commited on
Commit
e6027d1
·
verified ·
1 Parent(s): 28b1285

Update src/streamlit_app.py

Browse files
Files changed (1) hide show
  1. src/streamlit_app.py +95 -54
src/streamlit_app.py CHANGED
@@ -8,14 +8,14 @@ from datetime import datetime
8
  import plotly.express as px
9
  import plotly.graph_objects as go
10
 
11
- # Import BRScraper - Fixed import
12
  try:
13
- from basketball_reference_scraper import players, teams, seasons
14
  BRSCRAPER_AVAILABLE = True
15
  except ImportError:
16
  BRSCRAPER_AVAILABLE = False
17
  # Display error message if BRScraper is not found
18
- st.error("BRScraper not found. Please install with: pip install basketball-reference-scraper")
19
 
20
  # Page configuration
21
  st.set_page_config(
@@ -62,7 +62,7 @@ def get_available_seasons(num_seasons=6):
62
  end_year = latest_season_end_year - i
63
  start_year = end_year - 1
64
  # Use en-dash for consistency with BBR format
65
- seasons_list.append(f"{start_year}–{str(end_year)[-2:]}")
66
  return sorted(seasons_list, reverse=True) # Sort to show most recent first
67
 
68
  @st.cache_data(ttl=3600)
@@ -79,11 +79,11 @@ def get_player_index_brscraper():
79
  # Example: '2024–25' -> 2025
80
  latest_season_end_year = int(get_available_seasons(1)[0].split('–')[1])
81
 
82
- # Use seasons.get_stats to get a list of players for the latest season
83
- # BRScraper's get_stats returns a 'PLAYER' column
84
- df = seasons.get_stats(latest_season_end_year, data_format='per_game', playoffs=False)
85
 
86
- if df.empty or 'PLAYER' not in df.columns:
87
  st.warning(f"BRScraper could not fetch player list for {latest_season_end_year}. Falling back to common players.")
88
  # Fallback to a hardcoded list of common players for demo
89
  common_players = [
@@ -95,7 +95,7 @@ def get_player_index_brscraper():
95
  ]
96
  return pd.DataFrame({'name': common_players})
97
 
98
- player_names = df['PLAYER'].unique().tolist()
99
  return pd.DataFrame({'name': player_names})
100
 
101
  except Exception as e:
@@ -117,22 +117,23 @@ def get_player_career_stats_brscraper(player_name):
117
  return pd.DataFrame()
118
 
119
  try:
120
- # BRScraper's players.get_stats returns a DataFrame with career stats
121
- df = players.get_stats(player_name, stat_type='PER_GAME')
122
 
123
  if df.empty:
124
  return pd.DataFrame()
125
 
126
  # Standardize column names from BRScraper output to app's expected format
 
127
  column_mapping = {
128
- 'SEASON': 'Season', # BRScraper returns 'SEASON'
129
  'G': 'GP', 'GS': 'GS', 'MP': 'MIN',
130
- 'FG_PCT': 'FG_PCT', 'FG3_PCT': 'FG3_PCT', 'FT_PCT': 'FT_PCT',
131
  'TRB': 'REB', 'AST': 'AST', 'STL': 'STL', 'BLK': 'BLK', 'TOV': 'TO',
132
  'PF': 'PF', 'PTS': 'PTS',
133
- 'AGE': 'AGE', 'TEAM': 'TEAM_ABBREVIATION', 'POS': 'POSITION',
134
- 'FG': 'FGM', 'FGA': 'FGA', 'FG3': 'FG3M', 'FG3A': 'FG3A',
135
- 'FG2': 'FGM2', 'FG2A': 'FGA2', 'FG2_PCT': 'FG2_PCT', 'EFG_PCT': 'EFG_PCT',
136
  'FT': 'FTM', 'FTA': 'FTA', 'ORB': 'OREB', 'DRB': 'DREB'
137
  }
138
 
@@ -146,8 +147,8 @@ def get_player_career_stats_brscraper(player_name):
146
  df['Season'] = df['Season'].astype(str).str.replace('-', '–')
147
 
148
  # Convert numeric columns
149
- # Exclude 'Season', 'TEAM_ABBREVIATION', 'POSITION' as they are strings
150
- non_numeric_cols = {'Season', 'TEAM_ABBREVIATION', 'POSITION'}
151
  for col in df.columns:
152
  if col not in non_numeric_cols:
153
  df[col] = pd.to_numeric(df[col], errors="coerce")
@@ -164,51 +165,62 @@ def get_player_career_stats_brscraper(player_name):
164
  @st.cache_data(ttl=300)
165
  def get_team_season_stats_brscraper(year):
166
  """
167
- Uses BRScraper to get team stats for a given season year.
168
  Applies column renaming and numeric conversion.
169
  """
170
  if not BRSCRAPER_AVAILABLE:
171
  return pd.DataFrame()
172
 
173
  try:
174
- # BRScraper's teams.get_team_stats returns a DataFrame with team stats for the year
175
- df = teams.get_team_stats(year, data_format='per_game')
 
176
 
177
  if df.empty:
178
  return pd.DataFrame()
179
 
 
 
 
 
 
 
 
 
 
180
  # Standardize column names from BRScraper output
 
181
  column_mapping = {
182
  'G': 'GP', 'MP': 'MIN',
183
- 'FG_PCT': 'FG_PCT', 'FG3_PCT': 'FG3_PCT', 'FT_PCT': 'FT_PCT',
184
  'TRB': 'REB', 'AST': 'AST', 'STL': 'STL', 'BLK': 'BLK', 'TOV': 'TO',
185
  'PF': 'PF', 'PTS': 'PTS',
186
- 'RK': 'RANK', 'W': 'WINS', 'L': 'LOSSES', 'W_L_PCT': 'WIN_LOSS_PCT', # BRScraper uses W_L_PCT
187
- 'FG': 'FGM', 'FGA': 'FGA', 'FG3': 'FG3M', 'FG3A': 'FG3A',
188
- 'FG2': 'FGM2', 'FG2A': 'FGA2', 'FG2_PCT': 'FG2_PCT', 'EFG_PCT': 'EFG_PCT',
189
  'FT': 'FTM', 'FTA': 'FTA', 'ORB': 'OREB', 'DRB': 'DREB',
190
- 'TEAM': 'Team' # BRScraper returns 'TEAM'
191
  }
192
 
193
  for old_col, new_col in column_mapping.items():
194
- if old_col in df.columns:
195
- df = df.rename(columns={old_col: new_col})
196
 
197
  # Convert numeric columns
198
  # Exclude 'Team' and 'RANK' as they are strings/identifiers
199
  non_numeric_cols = {"Team", "RANK"}
200
- for col in df.columns:
201
  if col not in non_numeric_cols:
202
- df[col] = pd.to_numeric(df[col], errors="coerce")
203
 
204
  # Ensure 'Team' column is present and clean it (remove asterisks)
205
- if 'Team' in df.columns:
206
- df['Team'] = df['Team'].astype(str).str.replace('*', '', regex=False).str.strip()
207
  else:
208
  st.warning(f"Could not find 'Team' column in BRScraper output for year {year}.")
209
  return pd.DataFrame()
210
 
211
- return df
212
 
213
  except Exception as e:
214
  st.error(f"Error fetching team stats for {year} with BRScraper: {e}")
@@ -305,8 +317,10 @@ def create_radar_chart(player_stats, categories):
305
  # Main App Structure
306
  # —————————————————————————————————————————————————————————————————————————————
307
  def main():
 
 
308
  if not BRSCRAPER_AVAILABLE:
309
- st.warning("⚠️ BRScraper is not installed. Some features may be limited. Install with: `pip install basketball-reference-scraper`")
310
 
311
  st.markdown('<h1 class="main-header">🏀 NBA Analytics Hub (BBR Edition)</h1>', unsafe_allow_html=True)
312
  st.sidebar.title("Navigation")
@@ -333,7 +347,7 @@ def player_vs_player():
333
  st.markdown('<h2 class="section-header">Player vs Player Comparison</h2>', unsafe_allow_html=True)
334
 
335
  if not BRSCRAPER_AVAILABLE:
336
- st.error("BRScraper is required for this feature. Please install basketball-reference-scraper.")
337
  return
338
 
339
  idx = get_player_index_brscraper()
@@ -349,7 +363,7 @@ def player_vs_player():
349
  return
350
 
351
  stats_tabs = st.tabs(["Basic Stats", "Advanced Stats", "Visualizations"])
352
- all_player_season_data = []
353
  players_with_no_data = []
354
 
355
  with st.spinner("Fetching player data..."):
@@ -357,6 +371,8 @@ def player_vs_player():
357
  df_player_career = get_player_career_stats_brscraper(player_name)
358
 
359
  if not df_player_career.empty:
 
 
360
  filtered_df = df_player_career[df_player_career['Season'].isin(selected_seasons)].copy()
361
 
362
  if not filtered_df.empty:
@@ -366,17 +382,21 @@ def player_vs_player():
366
  else:
367
  players_with_no_data.append(player_name)
368
 
 
369
  if players_with_no_data:
370
- st.info(f"No data found for the selected seasons ({', '.join(selected_seasons)}) for: {', '.join(players_with_no_data)}.")
371
 
372
  if not all_player_season_data:
373
- st.error("No data available for any of the selected players and seasons to display.")
374
  return
375
 
 
376
  comparison_df_raw = pd.concat(all_player_season_data, ignore_index=True)
377
 
378
  with stats_tabs[0]:
379
  st.subheader("Basic Statistics")
 
 
380
  if len(selected_seasons) > 1:
381
  basic_display_df = comparison_df_raw.groupby('Player').mean(numeric_only=True).reset_index()
382
  basic_cols = ['Player', 'GP', 'MIN', 'PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', 'FT_PCT', 'FG3_PCT']
@@ -393,6 +413,7 @@ def player_vs_player():
393
  advanced_df = comparison_df_raw.copy()
394
 
395
  # Calculate TS% (True Shooting Percentage)
 
396
  advanced_df['FGA'] = pd.to_numeric(advanced_df['FGA'], errors='coerce').fillna(0)
397
  advanced_df['FTA'] = pd.to_numeric(advanced_df['FTA'], errors='coerce').fillna(0)
398
  advanced_df['PTS'] = pd.to_numeric(advanced_df['PTS'], errors='coerce').fillna(0)
@@ -418,14 +439,16 @@ def player_vs_player():
418
  st.subheader("Player Comparison Charts")
419
 
420
  if not comparison_df_raw.empty:
421
- metrics = ['PTS', 'REB', 'AST', 'FG_PCT', 'FG3_PCT', 'FT_PCT', 'STL', 'BLK']
422
  available_metrics = [m for m in metrics if m in comparison_df_raw.columns]
423
 
424
  if available_metrics:
425
  selected_metric = st.selectbox("Select Metric to Visualize", available_metrics)
426
 
427
  if selected_metric:
 
428
  if len(selected_players) == 1 and len(selected_seasons) > 1:
 
429
  player_trend_df = comparison_df_raw[comparison_df_raw['Player'] == selected_players[0]].sort_values(by='Season')
430
  fig = px.line(
431
  player_trend_df,
@@ -435,6 +458,7 @@ def player_vs_player():
435
  markers=True
436
  )
437
  else:
 
438
  avg_comparison_df = comparison_df_raw.groupby('Player')[available_metrics].mean(numeric_only=True).reset_index()
439
  fig = px.bar(
440
  avg_comparison_df,
@@ -445,12 +469,13 @@ def player_vs_player():
445
  )
446
  st.plotly_chart(fig, use_container_width=True)
447
 
448
- # Radar chart
449
  radar_metrics_for_chart = ['PTS', 'REB', 'AST', 'STL', 'BLK']
450
  radar_metrics_for_chart = [m for m in radar_metrics_for_chart if m in comparison_df_raw.columns]
451
 
452
  if len(radar_metrics_for_chart) >= 3:
453
  radar_data = {}
 
454
  if len(selected_seasons) > 1:
455
  radar_source_df = comparison_df_raw.groupby('Player')[radar_metrics_for_chart].mean(numeric_only=True).reset_index()
456
  else:
@@ -458,13 +483,14 @@ def player_vs_player():
458
 
459
  scaled_radar_df = radar_source_df.copy()
460
 
 
461
  for col in radar_metrics_for_chart:
462
  min_val = scaled_radar_df[col].min()
463
  max_val = scaled_radar_df[col].max()
464
  if max_val > min_val:
465
  scaled_radar_df[col] = ((scaled_radar_df[col] - min_val) / (max_val - min_val)) * 100
466
  else:
467
- scaled_radar_df[col] = 0
468
 
469
  for _, row in scaled_radar_df.iterrows():
470
  radar_data[row['Player']] = {
@@ -474,6 +500,15 @@ def player_vs_player():
474
  if radar_data:
475
  radar_fig = create_radar_chart(radar_data, radar_metrics_for_chart)
476
  st.plotly_chart(radar_fig, use_container_width=True)
 
 
 
 
 
 
 
 
 
477
 
478
  def team_vs_team():
479
  st.markdown('<h2 class="section-header">Team vs Team Analysis</h2>', unsafe_allow_html=True)
@@ -488,15 +523,15 @@ def team_vs_team():
488
  return
489
 
490
  selected_season_str = st.selectbox("Select Season", available_seasons, index=0)
491
- # Correctly extract the full end year (e.g., "2024–25" -> 2025) for BRScraper
492
  year_for_team_stats = int(selected_season_str.split('–')[1])
493
 
494
- tm_df = get_team_season_stats_brscraper(year_for_team_stats)
495
  if tm_df.empty:
496
- st.info(f"No team data available for the {selected_season_str} season.")
497
  return
498
 
499
- teams = tm_df['Team'].unique().tolist()
500
  selected_teams = st.multiselect("Select Teams (up to 4)", teams, max_selections=4)
501
 
502
  if st.button("Run Comparison"):
@@ -509,45 +544,51 @@ def team_vs_team():
509
 
510
  with st.spinner("Fetching team data..."):
511
  for t in selected_teams:
512
- df = tm_df[tm_df.Team == t].copy()
513
  if not df.empty:
514
- df['Season'] = selected_season_str
515
- stats.append(df.iloc[0].to_dict())
 
 
516
  else:
517
  teams_with_no_data.append(t)
518
 
519
  if teams_with_no_data:
520
- st.info(f"No data found for: {', '.join(teams_with_no_data)}")
521
 
522
  if not stats:
523
- st.error("No data available for the selected teams.")
524
  return
525
 
526
  comp = pd.DataFrame(stats)
527
- for col in ['PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', 'FG3_PCT', 'FT_PCT']:
 
528
  if col in comp.columns:
529
  comp[col] = pd.to_numeric(comp[col], errors='coerce')
530
 
531
  st.subheader("Team Statistics Comparison")
532
- cols = ['Team', 'Season', 'PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', 'FG3_PCT', 'FT_PCT']
533
  display_cols = [col for col in cols if col in comp.columns]
534
  st.dataframe(comp[display_cols].round(2), use_container_width=True)
535
 
536
  st.subheader("Team Performance Visualization")
537
- metric_options = ['PTS', 'REB', 'AST', 'FG_PCT', 'FG3_PCT', 'FT_PCT']
538
  available_metrics = [m for m in metric_options if m in comp.columns]
539
 
540
  if available_metrics:
541
  selected_metric = st.selectbox("Select Metric", available_metrics)
 
542
  fig = px.bar(
543
  comp,
544
  x='Team',
545
  y=selected_metric,
546
- color='Team',
547
  title=f"Team {selected_metric} Comparison ({selected_season_str} Season)",
548
  barmode='group'
549
  )
550
  st.plotly_chart(fig, use_container_width=True)
 
 
551
 
552
  def awards_predictor():
553
  st.markdown('<h2 class="section-header">NBA Awards Predictor</h2>', unsafe_allow_html=True)
 
8
  import plotly.express as px
9
  import plotly.graph_objects as go
10
 
11
+ # Import BRScraper
12
  try:
13
+ from BRScraper import nba
14
  BRSCRAPER_AVAILABLE = True
15
  except ImportError:
16
  BRSCRAPER_AVAILABLE = False
17
  # Display error message if BRScraper is not found
18
+ st.error("BRScraper not found. Please install with: `pip install BRScraper`")
19
 
20
  # Page configuration
21
  st.set_page_config(
 
62
  end_year = latest_season_end_year - i
63
  start_year = end_year - 1
64
  # Use en-dash for consistency with BBR format
65
+ seasons_list.append(f"{start_year}–{end_year}")
66
  return sorted(seasons_list, reverse=True) # Sort to show most recent first
67
 
68
  @st.cache_data(ttl=3600)
 
79
  # Example: '2024–25' -> 2025
80
  latest_season_end_year = int(get_available_seasons(1)[0].split('–')[1])
81
 
82
+ # Use nba.get_stats to get a list of players for the latest season
83
+ # BRScraper's get_stats returns a 'Player' column
84
+ df = nba.get_stats(latest_season_end_year, info='per_game', rename=False)
85
 
86
+ if df.empty or 'Player' not in df.columns:
87
  st.warning(f"BRScraper could not fetch player list for {latest_season_end_year}. Falling back to common players.")
88
  # Fallback to a hardcoded list of common players for demo
89
  common_players = [
 
95
  ]
96
  return pd.DataFrame({'name': common_players})
97
 
98
+ player_names = df['Player'].unique().tolist()
99
  return pd.DataFrame({'name': player_names})
100
 
101
  except Exception as e:
 
117
  return pd.DataFrame()
118
 
119
  try:
120
+ # BRScraper's nba.get_player_stats returns a DataFrame with career stats
121
+ df = nba.get_player_stats(player_name)
122
 
123
  if df.empty:
124
  return pd.DataFrame()
125
 
126
  # Standardize column names from BRScraper output to app's expected format
127
+ # BRScraper's nba.get_player_stats uses column names like 'FG%', 'TRB', 'Tm', 'Age'
128
  column_mapping = {
129
+ 'Season': 'Season', # BRScraper returns 'Season'
130
  'G': 'GP', 'GS': 'GS', 'MP': 'MIN',
131
+ 'FG%': 'FG_PCT', '3P%': 'FG3_PCT', 'FT%': 'FT_PCT',
132
  'TRB': 'REB', 'AST': 'AST', 'STL': 'STL', 'BLK': 'BLK', 'TOV': 'TO',
133
  'PF': 'PF', 'PTS': 'PTS',
134
+ 'Age': 'AGE', 'Tm': 'TEAM_ABBREVIATION', 'Lg': 'LEAGUE_ID', 'Pos': 'POSITION',
135
+ 'FG': 'FGM', 'FGA': 'FGA', '3P': 'FG3M', '3PA': 'FG3A',
136
+ '2P': 'FGM2', '2PA': 'FGA2', '2P%': 'FG2_PCT', 'eFG%': 'EFG_PCT',
137
  'FT': 'FTM', 'FTA': 'FTA', 'ORB': 'OREB', 'DRB': 'DREB'
138
  }
139
 
 
147
  df['Season'] = df['Season'].astype(str).str.replace('-', '–')
148
 
149
  # Convert numeric columns
150
+ # Exclude 'Season', 'TEAM_ABBREVIATION', 'LEAGUE_ID', 'POSITION' as they are strings
151
+ non_numeric_cols = {'Season', 'TEAM_ABBREVIATION', 'LEAGUE_ID', 'POSITION'}
152
  for col in df.columns:
153
  if col not in non_numeric_cols:
154
  df[col] = pd.to_numeric(df[col], errors="coerce")
 
165
  @st.cache_data(ttl=300)
166
  def get_team_season_stats_brscraper(year):
167
  """
168
+ Uses BRScraper to get per-game team stats for a given season year.
169
  Applies column renaming and numeric conversion.
170
  """
171
  if not BRSCRAPER_AVAILABLE:
172
  return pd.DataFrame()
173
 
174
  try:
175
+ # BRScraper's nba.get_stats returns a DataFrame with team stats for the year
176
+ # This function returns both player and team stats, so we need to filter for teams
177
+ df = nba.get_stats(year, info='per_game', rename=False)
178
 
179
  if df.empty:
180
  return pd.DataFrame()
181
 
182
+ # Filter for team rows. Team rows typically have 'Player' as NaN.
183
+ # Also, remove any rows that are just headers (e.g., 'Rk' == 'Rk')
184
+ team_df = df[df['Player'].isna()].copy()
185
+ team_df = team_df[team_df['Rk'].astype(str).str.lower() != 'rk'].copy() # Remove header rows if they slipped through
186
+
187
+ if team_df.empty:
188
+ st.warning(f"Could not reliably identify team rows for year {year}. Returning empty DataFrame.")
189
+ return pd.DataFrame()
190
+
191
  # Standardize column names from BRScraper output
192
+ # BRScraper's nba.get_stats for teams uses 'Tm', 'W/L%', etc.
193
  column_mapping = {
194
  'G': 'GP', 'MP': 'MIN',
195
+ 'FG%': 'FG_PCT', '3P%': 'FG3_PCT', 'FT%': 'FT_PCT',
196
  'TRB': 'REB', 'AST': 'AST', 'STL': 'STL', 'BLK': 'BLK', 'TOV': 'TO',
197
  'PF': 'PF', 'PTS': 'PTS',
198
+ 'Rk': 'RANK', 'W': 'WINS', 'L': 'LOSSES', 'W/L%': 'WIN_LOSS_PCT',
199
+ 'FG': 'FGM', 'FGA': 'FGA', '3P': 'FG3M', '3PA': 'FG3A',
200
+ '2P': 'FGM2', '2PA': 'FGA2', '2P%': 'FG2_PCT', 'eFG%': 'EFG_PCT',
201
  'FT': 'FTM', 'FTA': 'FTA', 'ORB': 'OREB', 'DRB': 'DREB',
202
+ 'Tm': 'Team' # BRScraper returns 'Tm' for team abbreviation
203
  }
204
 
205
  for old_col, new_col in column_mapping.items():
206
+ if old_col in team_df.columns:
207
+ team_df = team_df.rename(columns={old_col: new_col})
208
 
209
  # Convert numeric columns
210
  # Exclude 'Team' and 'RANK' as they are strings/identifiers
211
  non_numeric_cols = {"Team", "RANK"}
212
+ for col in team_df.columns:
213
  if col not in non_numeric_cols:
214
+ team_df[col] = pd.to_numeric(team_df[col], errors="coerce")
215
 
216
  # Ensure 'Team' column is present and clean it (remove asterisks)
217
+ if 'Team' in team_df.columns:
218
+ team_df['Team'] = team_df['Team'].astype(str).str.replace('*', '', regex=False).str.strip()
219
  else:
220
  st.warning(f"Could not find 'Team' column in BRScraper output for year {year}.")
221
  return pd.DataFrame()
222
 
223
+ return team_df
224
 
225
  except Exception as e:
226
  st.error(f"Error fetching team stats for {year} with BRScraper: {e}")
 
317
  # Main App Structure
318
  # —————————————————————————————————————————————————————————————————————————————
319
  def main():
320
+ # Check if BRScraper is available at the start of main
321
+ # This flag is set globally based on the import attempt
322
  if not BRSCRAPER_AVAILABLE:
323
+ st.warning("⚠️ BRScraper is not installed. Some features may be limited. Install with: `pip install BRScraper`")
324
 
325
  st.markdown('<h1 class="main-header">🏀 NBA Analytics Hub (BBR Edition)</h1>', unsafe_allow_html=True)
326
  st.sidebar.title("Navigation")
 
347
  st.markdown('<h2 class="section-header">Player vs Player Comparison</h2>', unsafe_allow_html=True)
348
 
349
  if not BRSCRAPER_AVAILABLE:
350
+ st.error("BRScraper is required for this feature. Please install BRScraper.")
351
  return
352
 
353
  idx = get_player_index_brscraper()
 
363
  return
364
 
365
  stats_tabs = st.tabs(["Basic Stats", "Advanced Stats", "Visualizations"])
366
+ all_player_season_data = [] # To store individual season rows for each player
367
  players_with_no_data = []
368
 
369
  with st.spinner("Fetching player data..."):
 
371
  df_player_career = get_player_career_stats_brscraper(player_name)
372
 
373
  if not df_player_career.empty:
374
+ # Filter for selected seasons. The player_season_stats function
375
+ # already ensures the 'Season' column uses en-dashes.
376
  filtered_df = df_player_career[df_player_career['Season'].isin(selected_seasons)].copy()
377
 
378
  if not filtered_df.empty:
 
382
  else:
383
  players_with_no_data.append(player_name)
384
 
385
+ # Report on players with no data for selected seasons
386
  if players_with_no_data:
387
+ st.info(f"No data found for the selected seasons ({', '.join(selected_seasons)}) for: {', '.join(players_with_no_data)}. This might be because the season hasn't started or data is not yet available, or the player name was not found by BRScraper.")
388
 
389
  if not all_player_season_data:
390
+ st.error("No data available for any of the selected players and seasons to display. Please adjust your selections.")
391
  return
392
 
393
+ # Concatenate all collected season data into one DataFrame
394
  comparison_df_raw = pd.concat(all_player_season_data, ignore_index=True)
395
 
396
  with stats_tabs[0]:
397
  st.subheader("Basic Statistics")
398
+ # Group by player and average for the basic stats table if multiple seasons are selected
399
+ # Otherwise, show individual season stats if only one season is selected
400
  if len(selected_seasons) > 1:
401
  basic_display_df = comparison_df_raw.groupby('Player').mean(numeric_only=True).reset_index()
402
  basic_cols = ['Player', 'GP', 'MIN', 'PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', 'FT_PCT', 'FG3_PCT']
 
413
  advanced_df = comparison_df_raw.copy()
414
 
415
  # Calculate TS% (True Shooting Percentage)
416
+ # Ensure FGA and FTA are numeric and not zero to avoid division by zero
417
  advanced_df['FGA'] = pd.to_numeric(advanced_df['FGA'], errors='coerce').fillna(0)
418
  advanced_df['FTA'] = pd.to_numeric(advanced_df['FTA'], errors='coerce').fillna(0)
419
  advanced_df['PTS'] = pd.to_numeric(advanced_df['PTS'], errors='coerce').fillna(0)
 
439
  st.subheader("Player Comparison Charts")
440
 
441
  if not comparison_df_raw.empty:
442
+ metrics = ['PTS', 'REB', 'AST', 'FG_PCT', '3P_PCT', 'FT_PCT', 'STL', 'BLK']
443
  available_metrics = [m for m in metrics if m in comparison_df_raw.columns]
444
 
445
  if available_metrics:
446
  selected_metric = st.selectbox("Select Metric to Visualize", available_metrics)
447
 
448
  if selected_metric:
449
+ # Determine if we are showing a trend for one player or comparison for multiple
450
  if len(selected_players) == 1 and len(selected_seasons) > 1:
451
+ # Show trend over seasons for one player
452
  player_trend_df = comparison_df_raw[comparison_df_raw['Player'] == selected_players[0]].sort_values(by='Season')
453
  fig = px.line(
454
  player_trend_df,
 
458
  markers=True
459
  )
460
  else:
461
+ # Average over selected seasons for multiple players for bar chart
462
  avg_comparison_df = comparison_df_raw.groupby('Player')[available_metrics].mean(numeric_only=True).reset_index()
463
  fig = px.bar(
464
  avg_comparison_df,
 
469
  )
470
  st.plotly_chart(fig, use_container_width=True)
471
 
472
+ # Radar chart for multi-metric comparison
473
  radar_metrics_for_chart = ['PTS', 'REB', 'AST', 'STL', 'BLK']
474
  radar_metrics_for_chart = [m for m in radar_metrics_for_chart if m in comparison_df_raw.columns]
475
 
476
  if len(radar_metrics_for_chart) >= 3:
477
  radar_data = {}
478
+ # Use the averaged data for radar chart if multiple seasons
479
  if len(selected_seasons) > 1:
480
  radar_source_df = comparison_df_raw.groupby('Player')[radar_metrics_for_chart].mean(numeric_only=True).reset_index()
481
  else:
 
483
 
484
  scaled_radar_df = radar_source_df.copy()
485
 
486
+ # Simple min-max scaling for radar chart visualization (0-100)
487
  for col in radar_metrics_for_chart:
488
  min_val = scaled_radar_df[col].min()
489
  max_val = scaled_radar_df[col].max()
490
  if max_val > min_val:
491
  scaled_radar_df[col] = ((scaled_radar_df[col] - min_val) / (max_val - min_val)) * 100
492
  else:
493
+ scaled_radar_df[col] = 0 # Default if all values are the same
494
 
495
  for _, row in scaled_radar_df.iterrows():
496
  radar_data[row['Player']] = {
 
500
  if radar_data:
501
  radar_fig = create_radar_chart(radar_data, radar_metrics_for_chart)
502
  st.plotly_chart(radar_fig, use_container_width=True)
503
+ else:
504
+ st.info("Could not generate radar chart data.")
505
+ else:
506
+ st.info("Select at least 3 common metrics for a radar chart (e.g., PTS, REB, AST, STL, BLK).")
507
+ else:
508
+ st.info("No common metrics available for visualization.")
509
+ else:
510
+ st.info("No data available for visualizations.")
511
+
512
 
513
  def team_vs_team():
514
  st.markdown('<h2 class="section-header">Team vs Team Analysis</h2>', unsafe_allow_html=True)
 
523
  return
524
 
525
  selected_season_str = st.selectbox("Select Season", available_seasons, index=0)
526
+ # Extract the end year from the season string (e.g., "2024–25" -> 2025)
527
  year_for_team_stats = int(selected_season_str.split('–')[1])
528
 
529
+ tm_df = get_team_season_stats_brscraper(year_for_team_stats) # Use BRScraper for team stats
530
  if tm_df.empty:
531
+ 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 BRScraper could not fetch it.")
532
  return
533
 
534
+ teams = tm_df['Team'].unique().tolist() # Use 'Team' column from BRScraper output
535
  selected_teams = st.multiselect("Select Teams (up to 4)", teams, max_selections=4)
536
 
537
  if st.button("Run Comparison"):
 
544
 
545
  with st.spinner("Fetching team data..."):
546
  for t in selected_teams:
547
+ df = tm_df[tm_df.Team == t].copy() # Filter by 'Team' column
548
  if not df.empty:
549
+ # For team stats, we usually get one row per team per season from team_per_game
550
+ # So, no need for .mean() here, just take the row.
551
+ df['Season'] = selected_season_str # Add 'Season' column
552
+ stats.append(df.iloc[0].to_dict()) # Convert the single row to dict
553
  else:
554
  teams_with_no_data.append(t)
555
 
556
  if teams_with_no_data:
557
+ 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.")
558
 
559
  if not stats:
560
+ st.error("No data available for the selected teams to display. Please adjust your selections.")
561
  return
562
 
563
  comp = pd.DataFrame(stats)
564
+ # Ensure numeric columns are actually numeric for display and calculations
565
+ for col in ['PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', '3P_PCT', 'FT_PCT']:
566
  if col in comp.columns:
567
  comp[col] = pd.to_numeric(comp[col], errors='coerce')
568
 
569
  st.subheader("Team Statistics Comparison")
570
+ cols = ['Team', 'Season', 'PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', '3P_PCT', 'FT_PCT']
571
  display_cols = [col for col in cols if col in comp.columns]
572
  st.dataframe(comp[display_cols].round(2), use_container_width=True)
573
 
574
  st.subheader("Team Performance Visualization")
575
+ metric_options = ['PTS', 'REB', 'AST', 'FG_PCT', '3P_PCT', 'FT_PCT']
576
  available_metrics = [m for m in metric_options if m in comp.columns]
577
 
578
  if available_metrics:
579
  selected_metric = st.selectbox("Select Metric", available_metrics)
580
+
581
  fig = px.bar(
582
  comp,
583
  x='Team',
584
  y=selected_metric,
585
+ color='Team', # Color by team for clarity
586
  title=f"Team {selected_metric} Comparison ({selected_season_str} Season)",
587
  barmode='group'
588
  )
589
  st.plotly_chart(fig, use_container_width=True)
590
+ else:
591
+ st.info("No common metrics available for visualization.")
592
 
593
  def awards_predictor():
594
  st.markdown('<h2 class="section-header">NBA Awards Predictor</h2>', unsafe_allow_html=True)