Update src/streamlit_app.py
Browse files- src/streamlit_app.py +63 -77
src/streamlit_app.py
CHANGED
|
@@ -71,65 +71,56 @@ def get_player_index_brscraper():
|
|
| 71 |
return pd.DataFrame({'name': common})
|
| 72 |
|
| 73 |
@st.cache_data(ttl=300)
|
| 74 |
-
def get_player_career_stats_brscraper(player_name):
|
| 75 |
"""
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
applies mapping and numeric conversion.
|
| 79 |
"""
|
| 80 |
if not BRSCRAPER_AVAILABLE:
|
| 81 |
return pd.DataFrame()
|
| 82 |
-
|
| 83 |
-
try:
|
| 84 |
-
df = nba.get_player_stats(player_name)
|
| 85 |
-
if df.empty:
|
| 86 |
-
return df
|
| 87 |
-
|
| 88 |
-
# 1) Try to autodetect a season column
|
| 89 |
-
season_col = None
|
| 90 |
-
for col in df.columns:
|
| 91 |
-
low = col.lower().strip()
|
| 92 |
-
if 'season' in low or 'year' in low:
|
| 93 |
-
season_col = col
|
| 94 |
-
break
|
| 95 |
-
|
| 96 |
-
# 2) Fallback: if nothing detected, just take the first column
|
| 97 |
-
if season_col is None:
|
| 98 |
-
season_col = df.columns[0]
|
| 99 |
-
|
| 100 |
-
# 3) Rename whatever that column is to exactly 'Season'
|
| 101 |
-
df = df.rename(columns={season_col: 'Season'})
|
| 102 |
-
|
| 103 |
-
# 4) Standard column mapping
|
| 104 |
-
mapping = {
|
| 105 |
-
'G':'GP','GS':'GS','MP':'MIN',
|
| 106 |
-
'FG%':'FG_PCT','3P%':'FG3_PCT','FT%':'FT_PCT',
|
| 107 |
-
'TRB':'REB','AST':'AST','STL':'STL','BLK':'BLK','TOV':'TO',
|
| 108 |
-
'PF':'PF','PTS':'PTS','Age':'AGE','Tm':'TEAM_ABBREVIATION',
|
| 109 |
-
'Lg':'LEAGUE_ID','Pos':'POSITION',
|
| 110 |
-
'FG':'FGM','FGA':'FGA','3P':'FG3M','3PA':'FG3A',
|
| 111 |
-
'2P':'FGM2','2PA':'FGA2','2P%':'FG2_PCT','eFG%':'EFG_PCT',
|
| 112 |
-
'FT':'FTM','FTA':'FTA','ORB':'OREB','DRB':'DREB'
|
| 113 |
-
}
|
| 114 |
-
df = df.rename(columns={o:n for o,n in mapping.items() if o in df.columns})
|
| 115 |
-
|
| 116 |
-
# 5) Clean and format the Season column
|
| 117 |
-
df['Season'] = df['Season'].astype(str).str.replace('-', 'β').str.strip()
|
| 118 |
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
|
| 125 |
-
|
| 126 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
return pd.DataFrame()
|
| 133 |
|
| 134 |
@st.cache_data(ttl=300)
|
| 135 |
def get_team_season_stats_brscraper(year):
|
|
@@ -243,19 +234,19 @@ def player_vs_player():
|
|
| 243 |
seasons = get_available_seasons()
|
| 244 |
selected_seasons = st.multiselect("Select Seasons", seasons, default=[seasons[0]] if seasons else [])
|
| 245 |
|
| 246 |
-
# βββ DEBUG PREVIEW ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 247 |
if selected_players:
|
| 248 |
-
st.markdown("### π§ͺ Raw Data Preview
|
| 249 |
for player in selected_players:
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
st.write("
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
st.
|
| 257 |
st.markdown("---")
|
| 258 |
-
#
|
| 259 |
|
| 260 |
if st.button("Run Comparison"):
|
| 261 |
if not selected_players:
|
|
@@ -263,20 +254,16 @@ def player_vs_player():
|
|
| 263 |
return
|
| 264 |
|
| 265 |
all_data, no_data = [], []
|
| 266 |
-
with st.spinner("Fetching
|
| 267 |
for p in selected_players:
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
all_data.append(filt)
|
| 274 |
-
else:
|
| 275 |
-
no_data.append(p)
|
| 276 |
else:
|
| 277 |
no_data.append(p)
|
| 278 |
-
|
| 279 |
-
st.error(f"Error fetching stats for {p}: {e}")
|
| 280 |
no_data.append(p)
|
| 281 |
|
| 282 |
if no_data:
|
|
@@ -286,7 +273,7 @@ def player_vs_player():
|
|
| 286 |
return
|
| 287 |
|
| 288 |
comp_df = pd.concat(all_data, ignore_index=True)
|
| 289 |
-
tabs = st.tabs(["Basic Stats","Advanced Stats","Visualizations"])
|
| 290 |
|
| 291 |
with tabs[0]:
|
| 292 |
st.subheader("Basic Statistics")
|
|
@@ -310,10 +297,10 @@ def player_vs_player():
|
|
| 310 |
)
|
| 311 |
if len(selected_seasons) > 1:
|
| 312 |
df_adv = df_a.groupby('Player').mean(numeric_only=True).reset_index()
|
| 313 |
-
cols = ['Player',
|
| 314 |
else:
|
| 315 |
df_adv = df_a.copy()
|
| 316 |
-
cols = ['Player',
|
| 317 |
st.dataframe(df_adv[[c for c in cols if c in df_adv.columns]].round(3), use_container_width=True)
|
| 318 |
|
| 319 |
with tabs[2]:
|
|
@@ -323,8 +310,7 @@ def player_vs_player():
|
|
| 323 |
m = st.selectbox("Select Metric", metrics)
|
| 324 |
if len(selected_players) == 1 and len(selected_seasons) > 1:
|
| 325 |
trend = comp_df[comp_df['Player'] == selected_players[0]].sort_values('Season')
|
| 326 |
-
fig = px.line(trend, x='Season', y=m, markers=True,
|
| 327 |
-
title=f"{selected_players[0]} β {m} Trend")
|
| 328 |
else:
|
| 329 |
avg = comp_df.groupby('Player')[metrics].mean(numeric_only=True).reset_index()
|
| 330 |
fig = px.bar(avg, x='Player', y=m, title=f"Average {m} Comparison")
|
|
|
|
| 71 |
return pd.DataFrame({'name': common})
|
| 72 |
|
| 73 |
@st.cache_data(ttl=300)
|
| 74 |
+
def get_player_career_stats_brscraper(player_name, seasons_to_check=10):
|
| 75 |
"""
|
| 76 |
+
Build a player's career stats by fetching per-game data season-by-season
|
| 77 |
+
via nba.get_stats, instead of nba.get_player_stats (which was returning empty).
|
|
|
|
| 78 |
"""
|
| 79 |
if not BRSCRAPER_AVAILABLE:
|
| 80 |
return pd.DataFrame()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
+
all_rows = []
|
| 83 |
+
seasons = get_available_seasons(seasons_to_check)
|
| 84 |
+
for season_str in seasons:
|
| 85 |
+
end_year = int(season_str.split('β')[1])
|
| 86 |
+
try:
|
| 87 |
+
df_season = nba.get_stats(end_year, info='per_game', playoffs=False, rename=False)
|
| 88 |
+
if 'Player' in df_season.columns:
|
| 89 |
+
row = df_season[df_season['Player'] == player_name]
|
| 90 |
+
if not row.empty:
|
| 91 |
+
row = row.copy()
|
| 92 |
+
# normalize the season label
|
| 93 |
+
row['Season'] = season_str
|
| 94 |
+
all_rows.append(row)
|
| 95 |
+
except Exception as e:
|
| 96 |
+
# skip any failures
|
| 97 |
+
st.warning(f"Could not fetch {season_str} for {player_name}: {e}")
|
| 98 |
+
|
| 99 |
+
if not all_rows:
|
| 100 |
+
return pd.DataFrame()
|
| 101 |
|
| 102 |
+
df = pd.concat(all_rows, ignore_index=True)
|
| 103 |
+
|
| 104 |
+
# Standardize columns
|
| 105 |
+
mapping = {
|
| 106 |
+
'G':'GP','GS':'GS','MP':'MIN',
|
| 107 |
+
'FG%':'FG_PCT','3P%':'FG3_PCT','FT%':'FT_PCT',
|
| 108 |
+
'TRB':'REB','AST':'AST','STL':'STL','BLK':'BLK','TOV':'TO',
|
| 109 |
+
'PF':'PF','PTS':'PTS','ORB':'OREB','DRB':'DREB',
|
| 110 |
+
'FG':'FGM','FGA':'FGA','3P':'FG3M','3PA':'FG3A',
|
| 111 |
+
'2P':'FGM2','2PA':'FGA2','2P%':'FG2_PCT','eFG%':'EFG_PCT',
|
| 112 |
+
'FT':'FTM','FTA':'FTA'
|
| 113 |
+
}
|
| 114 |
+
df = df.rename(columns={o:n for o,n in mapping.items() if o in df.columns})
|
| 115 |
|
| 116 |
+
# Convert numeric columns
|
| 117 |
+
non_num = {'Season','Player','Tm','Lg','Pos'}
|
| 118 |
+
for col in df.columns:
|
| 119 |
+
if col not in non_num:
|
| 120 |
+
df[col] = pd.to_numeric(df[col], errors='coerce')
|
| 121 |
|
| 122 |
+
df['Player'] = player_name
|
| 123 |
+
return df
|
|
|
|
| 124 |
|
| 125 |
@st.cache_data(ttl=300)
|
| 126 |
def get_team_season_stats_brscraper(year):
|
|
|
|
| 234 |
seasons = get_available_seasons()
|
| 235 |
selected_seasons = st.multiselect("Select Seasons", seasons, default=[seasons[0]] if seasons else [])
|
| 236 |
|
| 237 |
+
# βββ RAW DEBUG PREVIEW ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 238 |
if selected_players:
|
| 239 |
+
st.markdown("### π§ͺ Raw Seasonal Data Preview")
|
| 240 |
for player in selected_players:
|
| 241 |
+
st.subheader(player)
|
| 242 |
+
raw = get_player_career_stats_brscraper(player, seasons_to_check=6)
|
| 243 |
+
if raw.empty:
|
| 244 |
+
st.write("β No rows returnedβplayer may be named differently or data unavailable.")
|
| 245 |
+
else:
|
| 246 |
+
st.write("Columns:", raw.columns.tolist())
|
| 247 |
+
st.dataframe(raw, use_container_width=True)
|
| 248 |
st.markdown("---")
|
| 249 |
+
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 250 |
|
| 251 |
if st.button("Run Comparison"):
|
| 252 |
if not selected_players:
|
|
|
|
| 254 |
return
|
| 255 |
|
| 256 |
all_data, no_data = [], []
|
| 257 |
+
with st.spinner("Fetching filtered data..."):
|
| 258 |
for p in selected_players:
|
| 259 |
+
df = get_player_career_stats_brscraper(p, seasons_to_check=6)
|
| 260 |
+
if not df.empty:
|
| 261 |
+
filt = df[df['Season'].isin(selected_seasons)]
|
| 262 |
+
if not filt.empty:
|
| 263 |
+
all_data.append(filt)
|
|
|
|
|
|
|
|
|
|
| 264 |
else:
|
| 265 |
no_data.append(p)
|
| 266 |
+
else:
|
|
|
|
| 267 |
no_data.append(p)
|
| 268 |
|
| 269 |
if no_data:
|
|
|
|
| 273 |
return
|
| 274 |
|
| 275 |
comp_df = pd.concat(all_data, ignore_index=True)
|
| 276 |
+
tabs = st.tabs(["Basic Stats", "Advanced Stats", "Visualizations"])
|
| 277 |
|
| 278 |
with tabs[0]:
|
| 279 |
st.subheader("Basic Statistics")
|
|
|
|
| 297 |
)
|
| 298 |
if len(selected_seasons) > 1:
|
| 299 |
df_adv = df_a.groupby('Player').mean(numeric_only=True).reset_index()
|
| 300 |
+
cols = ['Player','PTS','REB','AST','FG_PCT','TS_PCT']
|
| 301 |
else:
|
| 302 |
df_adv = df_a.copy()
|
| 303 |
+
cols = ['Player','Season','PTS','REB','AST','FG_PCT','TS_PCT']
|
| 304 |
st.dataframe(df_adv[[c for c in cols if c in df_adv.columns]].round(3), use_container_width=True)
|
| 305 |
|
| 306 |
with tabs[2]:
|
|
|
|
| 310 |
m = st.selectbox("Select Metric", metrics)
|
| 311 |
if len(selected_players) == 1 and len(selected_seasons) > 1:
|
| 312 |
trend = comp_df[comp_df['Player'] == selected_players[0]].sort_values('Season')
|
| 313 |
+
fig = px.line(trend, x='Season', y=m, markers=True, title=f"{selected_players[0]} β {m} Trend")
|
|
|
|
| 314 |
else:
|
| 315 |
avg = comp_df.groupby('Player')[metrics].mean(numeric_only=True).reset_index()
|
| 316 |
fig = px.bar(avg, x='Player', y=m, title=f"Average {m} Comparison")
|