Update src/streamlit_app.py
Browse files- src/streamlit_app.py +428 -90
src/streamlit_app.py
CHANGED
|
@@ -4,6 +4,34 @@ import numpy as np
|
|
| 4 |
import requests
|
| 5 |
import os
|
| 6 |
from datetime import datetime
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
|
| 8 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 9 |
# CSS (black & white theme)
|
|
@@ -15,11 +43,14 @@ st.markdown("""
|
|
| 15 |
</style>
|
| 16 |
""", unsafe_allow_html=True)
|
| 17 |
|
| 18 |
-
#
|
| 19 |
-
|
| 20 |
-
|
| 21 |
|
| 22 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
|
|
|
|
|
|
| 23 |
@st.cache_data(ttl=3600)
|
| 24 |
def fetch_html(url):
|
| 25 |
"""Fetch raw HTML for a URL (with error handling)."""
|
|
@@ -27,31 +58,57 @@ def fetch_html(url):
|
|
| 27 |
resp = requests.get(url, timeout=20)
|
| 28 |
resp.raise_for_status()
|
| 29 |
return resp.text
|
| 30 |
-
except
|
| 31 |
st.error(f"Failed to fetch {url}: {e}")
|
| 32 |
return ""
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
def parse_table(html, table_id=None):
|
| 35 |
"""
|
| 36 |
-
Given raw HTML and optional table_id, locate that <table>
|
| 37 |
-
then parse it with pandas.read_html.
|
| 38 |
"""
|
| 39 |
-
soup = BeautifulSoup(html, "
|
|
|
|
|
|
|
| 40 |
if table_id:
|
|
|
|
| 41 |
tbl = soup.find("table", {"id": table_id})
|
| 42 |
-
if
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
else:
|
| 46 |
-
# fallback: first table on page
|
| 47 |
first = soup.find("table")
|
| 48 |
-
if
|
| 49 |
-
|
| 50 |
-
|
|
|
|
|
|
|
| 51 |
|
| 52 |
try:
|
|
|
|
| 53 |
return pd.read_html(tbl_html)[0]
|
| 54 |
-
except ValueError:
|
|
|
|
|
|
|
|
|
|
| 55 |
return pd.DataFrame()
|
| 56 |
|
| 57 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -67,13 +124,16 @@ def get_player_index():
|
|
| 67 |
for letter in map(chr, range(ord('a'), ord('z')+1)):
|
| 68 |
url = f"{base}{letter}/"
|
| 69 |
html = fetch_html(url)
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
continue
|
| 73 |
|
| 74 |
-
|
| 75 |
-
soup = BeautifulSoup(html, "html.parser")
|
| 76 |
-
for row in soup.select("table#players tbody tr"):
|
| 77 |
th = row.find("th", {"data-stat": "player"})
|
| 78 |
if not th:
|
| 79 |
continue
|
|
@@ -95,18 +155,36 @@ def player_season_stats(bbr_url):
|
|
| 95 |
Returns cleaned DataFrame.
|
| 96 |
"""
|
| 97 |
html = fetch_html(bbr_url)
|
|
|
|
|
|
|
|
|
|
| 98 |
df = parse_table(html, table_id="per_game")
|
| 99 |
if df.empty or "Season" not in df.columns:
|
| 100 |
return pd.DataFrame()
|
| 101 |
|
| 102 |
-
# drop repeated header rows
|
| 103 |
df = df[df["Season"] != "Season"].copy()
|
| 104 |
-
df["Season"] = df["Season"].astype(str)
|
| 105 |
-
|
| 106 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
for col in df.columns:
|
| 108 |
-
if col not in
|
| 109 |
df[col] = pd.to_numeric(df[col], errors="coerce")
|
|
|
|
| 110 |
return df
|
| 111 |
|
| 112 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
@@ -119,23 +197,39 @@ def team_per_game(year):
|
|
| 119 |
"""
|
| 120 |
url = f"https://www.basketball-reference.com/leagues/NBA_{year}_per_game.html"
|
| 121 |
html = fetch_html(url)
|
| 122 |
-
|
|
|
|
|
|
|
|
|
|
| 123 |
if df.empty or "Team" not in df.columns:
|
| 124 |
return pd.DataFrame()
|
| 125 |
|
| 126 |
# drop repeated headers & rename
|
| 127 |
df = df[df["Team"] != "Team"].copy()
|
| 128 |
df.rename(columns={"Team": "Tm"}, inplace=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
# coerce numeric columns
|
| 130 |
-
|
| 131 |
for col in df.columns:
|
| 132 |
-
if col not in
|
| 133 |
df[col] = pd.to_numeric(df[col], errors="coerce")
|
| 134 |
|
| 135 |
return df
|
| 136 |
|
| 137 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 138 |
-
# Perplexity integration
|
| 139 |
PERP_KEY = os.getenv("PERPLEXITY_API_KEY")
|
| 140 |
PERP_URL = "https://api.perplexity.ai/chat/completions"
|
| 141 |
|
|
@@ -145,7 +239,7 @@ def ask_perp(prompt, system="You are a helpful NBA analyst AI.", max_tokens=500,
|
|
| 145 |
return ""
|
| 146 |
hdr = {'Authorization':f'Bearer {PERP_KEY}','Content-Type':'application/json'}
|
| 147 |
payload = {
|
| 148 |
-
"model":"sonar-
|
| 149 |
"messages":[{"role":"system","content":system},{"role":"user","content":prompt}],
|
| 150 |
"max_tokens":max_tokens, "temperature":temp
|
| 151 |
}
|
|
@@ -153,10 +247,44 @@ def ask_perp(prompt, system="You are a helpful NBA analyst AI.", max_tokens=500,
|
|
| 153 |
r = requests.post(PERP_URL, json=payload, headers=hdr, timeout=45)
|
| 154 |
r.raise_for_status()
|
| 155 |
return r.json().get("choices", [{}])[0].get("message",{}).get("content","")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 156 |
except Exception as e:
|
| 157 |
-
st.error(f"
|
| 158 |
return ""
|
| 159 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 160 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 161 |
def main():
|
| 162 |
st.markdown('<h1 class="main-header">π NBA Analytics Hub (BBR Edition)</h1>', unsafe_allow_html=True)
|
|
@@ -177,51 +305,236 @@ def main():
|
|
| 177 |
elif page == "Roster Builder": roster_builder()
|
| 178 |
else: trade_analyzer()
|
| 179 |
|
|
|
|
|
|
|
| 180 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 181 |
def player_vs_player():
|
| 182 |
st.markdown('<h2 class="section-header">Player vs Player Comparison</h2>', unsafe_allow_html=True)
|
| 183 |
idx = get_player_index()
|
| 184 |
names = idx['name'].tolist()
|
| 185 |
-
|
| 186 |
-
|
|
|
|
|
|
|
| 187 |
|
| 188 |
if st.button("Run Comparison"):
|
| 189 |
-
if not
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 204 |
|
| 205 |
def team_vs_team():
|
| 206 |
st.markdown('<h2 class="section-header">Team vs Team Analysis</h2>', unsafe_allow_html=True)
|
| 207 |
-
|
| 208 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 209 |
teams = tm_df['Tm'].unique().tolist()
|
| 210 |
-
|
| 211 |
|
| 212 |
if st.button("Run Comparison"):
|
| 213 |
-
if not
|
|
|
|
|
|
|
|
|
|
| 214 |
stats = []
|
| 215 |
-
for t in
|
| 216 |
-
df = tm_df[tm_df.Tm==t]
|
| 217 |
-
if df.empty:
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
|
| 224 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 225 |
|
| 226 |
def awards_predictor():
|
| 227 |
st.markdown('<h2 class="section-header">NBA Awards Predictor</h2>', unsafe_allow_html=True)
|
|
@@ -258,28 +571,37 @@ def awards_predictor():
|
|
| 258 |
|
| 259 |
def ai_chat():
|
| 260 |
st.markdown('<h2 class="section-header">AI Chat & Insights</h2>', unsafe_allow_html=True)
|
| 261 |
-
if '
|
| 262 |
-
for msg in st.session_state.
|
| 263 |
with st.chat_message(msg["role"]):
|
| 264 |
st.write(msg["content"])
|
| 265 |
if prompt:=st.chat_input("Ask me anything about NBAβ¦"):
|
| 266 |
-
st.session_state.
|
|
|
|
|
|
|
| 267 |
with st.chat_message("assistant"):
|
| 268 |
ans = ask_perp(prompt, system="You are an NBA expert analyst AI.", max_tokens=700)
|
| 269 |
st.write(ans)
|
| 270 |
-
st.session_state.
|
| 271 |
st.subheader("Quick Actions")
|
| 272 |
c1,c2,c3 = st.columns(3)
|
| 273 |
if c1.button("π Contenders"):
|
| 274 |
-
|
|
|
|
|
|
|
| 275 |
if c2.button("β Rising Stars"):
|
| 276 |
-
|
|
|
|
|
|
|
| 277 |
if c3.button("π Trades"):
|
| 278 |
-
|
|
|
|
|
|
|
| 279 |
|
| 280 |
def young_projections():
|
| 281 |
st.markdown('<h2 class="section-header">Young Player Projections</h2>', unsafe_allow_html=True)
|
| 282 |
-
|
|
|
|
| 283 |
sp = st.selectbox("Select or enter player", [""]+all_p)
|
| 284 |
if not sp:
|
| 285 |
sp = st.text_input("Enter player name manually")
|
|
@@ -299,28 +621,32 @@ def young_projections():
|
|
| 299 |
st.write(out)
|
| 300 |
yrs_lbl = [f"Year {i+1}" for i in range(5)]
|
| 301 |
vals = [ppg*(1+0.1*i) for i in range(5)]
|
| 302 |
-
|
|
|
|
|
|
|
|
|
|
| 303 |
|
| 304 |
def similar_players():
|
| 305 |
st.markdown('<h2 class="section-header">Similar Players Finder</h2>', unsafe_allow_html=True)
|
| 306 |
-
|
|
|
|
| 307 |
tp = st.selectbox("Target Player", all_p)
|
| 308 |
-
crit = st.multiselect("Criteria",["Position","Height/Weight","Style","
|
| 309 |
if tp and crit and st.button("Find Similar"):
|
| 310 |
-
prompt = f"Find top 5 current and top 3 historical similar to {tp} by {crit}."
|
| 311 |
st.write(ask_perp(prompt, system="You are a similarity expert AI.", max_tokens=800))
|
| 312 |
st.subheader("Manual Compare")
|
| 313 |
p1 = st.selectbox("Player 1", all_p, key="p1")
|
| 314 |
p2 = st.selectbox("Player 2", all_p, key="p2")
|
| 315 |
if p1 and p2 and p1!=p2 and st.button("Compare Players"):
|
| 316 |
-
prompt = f"Compare {p1} vs {p2} on
|
| 317 |
st.write(ask_perp(prompt, system="You are a comparison expert AI.", max_tokens=700))
|
| 318 |
|
| 319 |
def roster_builder():
|
| 320 |
st.markdown('<h2 class="section-header">NBA Roster Builder</h2>', unsafe_allow_html=True)
|
| 321 |
-
cap = st.number_input("Salary Cap (
|
| 322 |
-
strat = st.selectbox("Strategy",["
|
| 323 |
-
pos = st.multiselect("Priority Positions",["
|
| 324 |
st.subheader("Budget Allocation")
|
| 325 |
cols = st.columns(5)
|
| 326 |
alloc = {}
|
|
@@ -328,28 +654,40 @@ def roster_builder():
|
|
| 328 |
for i,p in enumerate(["PG","SG","SF","PF","C"]):
|
| 329 |
val = cols[i].number_input(f"{p} Budget ($M)",0,50,20, key=f"b{p}")
|
| 330 |
alloc[p]=val; total+=val
|
| 331 |
-
st.write(f"Total: ${total}M / ${cap}M")
|
| 332 |
-
if total>cap: st.error("
|
| 333 |
|
| 334 |
-
if st.button("Generate Roster"):
|
| 335 |
if total<=cap:
|
| 336 |
prompt = (
|
| 337 |
-
f"Build roster with
|
| 338 |
-
f"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 339 |
)
|
| 340 |
-
st.markdown("###
|
| 341 |
-
st.write(ask_perp(prompt, system="You are
|
| 342 |
else:
|
| 343 |
-
st.warning("
|
| 344 |
|
| 345 |
def trade_analyzer():
|
| 346 |
st.markdown('<h2 class="section-header">Trade Scenario Analyzer</h2>', unsafe_allow_html=True)
|
| 347 |
-
t1 = st.text_input("Team 1 trades")
|
| 348 |
-
t2 = st.text_input("Team 2 trades")
|
| 349 |
if t1 and t2 and st.button("Analyze Trade"):
|
| 350 |
prompt = (
|
| 351 |
-
f"
|
| 352 |
-
"
|
|
|
|
|
|
|
|
|
|
| 353 |
)
|
| 354 |
st.write(ask_perp(prompt, system="You are a trade analysis AI.", max_tokens=700))
|
| 355 |
|
|
|
|
| 4 |
import requests
|
| 5 |
import os
|
| 6 |
from datetime import datetime
|
| 7 |
+
from bs4 import BeautifulSoup
|
| 8 |
+
import re # New import for regex operations
|
| 9 |
+
|
| 10 |
+
# --- IMPORTANT: Addressing PermissionError in Containerized Environments ---
|
| 11 |
+
# The error "PermissionError: [Errno 13] Permission denied: '/.streamlit'"
|
| 12 |
+
# occurs because Streamlit tries to write to a non-writable directory.
|
| 13 |
+
# To fix this in your Dockerfile or when running your Docker container,
|
| 14 |
+
# you should set one of the following environment variables:
|
| 15 |
+
#
|
| 16 |
+
# Option 1 (Recommended): Set the HOME environment variable to a writable path.
|
| 17 |
+
# In your Dockerfile: ENV HOME /tmp
|
| 18 |
+
# Or when running: docker run -e HOME=/tmp your_image_name
|
| 19 |
+
#
|
| 20 |
+
# Option 2: Disable Streamlit's usage statistics gathering.
|
| 21 |
+
# In your Dockerfile: ENV STREAMLIT_BROWSER_GATHER_USAGE_STATS=False
|
| 22 |
+
# Or when running: docker run -e STREAMLIT_BROWSER_GATHER_USAGE_STATS=False your_image_name
|
| 23 |
+
#
|
| 24 |
+
# Option 1 is generally more robust as it provides a writable home directory
|
| 25 |
+
# for any application that might need it.
|
| 26 |
+
# -----------------------------------------------------------------------
|
| 27 |
+
|
| 28 |
+
# Page configuration
|
| 29 |
+
st.set_page_config(
|
| 30 |
+
page_title="NBA Analytics Hub",
|
| 31 |
+
page_icon="π",
|
| 32 |
+
layout="wide",
|
| 33 |
+
initial_sidebar_state="expanded"
|
| 34 |
+
)
|
| 35 |
|
| 36 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 37 |
# CSS (black & white theme)
|
|
|
|
| 43 |
</style>
|
| 44 |
""", unsafe_allow_html=True)
|
| 45 |
|
| 46 |
+
# Initialize session state for chat history
|
| 47 |
+
if 'chat_history' not in st.session_state:
|
| 48 |
+
st.session_state.chat_history = []
|
| 49 |
|
| 50 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 51 |
+
# Basketball-Reference Data Fetching Utilities
|
| 52 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 53 |
+
|
| 54 |
@st.cache_data(ttl=3600)
|
| 55 |
def fetch_html(url):
|
| 56 |
"""Fetch raw HTML for a URL (with error handling)."""
|
|
|
|
| 58 |
resp = requests.get(url, timeout=20)
|
| 59 |
resp.raise_for_status()
|
| 60 |
return resp.text
|
| 61 |
+
except requests.exceptions.RequestException as e:
|
| 62 |
st.error(f"Failed to fetch {url}: {e}")
|
| 63 |
return ""
|
| 64 |
+
except Exception as e:
|
| 65 |
+
st.error(f"An unexpected error occurred while fetching {url}: {e}")
|
| 66 |
+
return ""
|
| 67 |
|
| 68 |
def parse_table(html, table_id=None):
|
| 69 |
"""
|
| 70 |
+
Given raw HTML and optional table_id, locate that <table>,
|
| 71 |
+
handling cases where it's commented out, then parse it with pandas.read_html.
|
| 72 |
"""
|
| 73 |
+
soup = BeautifulSoup(html, "lxml") # Using lxml for potentially faster parsing
|
| 74 |
+
tbl_html = ""
|
| 75 |
+
|
| 76 |
if table_id:
|
| 77 |
+
# First, try to find the table directly
|
| 78 |
tbl = soup.find("table", {"id": table_id})
|
| 79 |
+
if tbl:
|
| 80 |
+
tbl_html = str(tbl)
|
| 81 |
+
else:
|
| 82 |
+
# If not found directly, search for it within HTML comments
|
| 83 |
+
# Basketball-Reference often comments out tables
|
| 84 |
+
comment_pattern = re.compile(r'<!--.*?<table[^>]*id="%s"[^>]*>.*?</table>.*?-->' % table_id, re.DOTALL)
|
| 85 |
+
comment_match = comment_pattern.search(html)
|
| 86 |
+
if comment_match:
|
| 87 |
+
# Extract the content of the comment
|
| 88 |
+
comment_content = comment_match.group(0)
|
| 89 |
+
# Remove the comment tags
|
| 90 |
+
comment_content = comment_content.replace('<!--', '').replace('-->', '')
|
| 91 |
+
# Parse the comment content as new HTML
|
| 92 |
+
comment_soup = BeautifulSoup(comment_content, 'lxml')
|
| 93 |
+
tbl = comment_soup.find('table', {'id': table_id})
|
| 94 |
+
if tbl:
|
| 95 |
+
tbl_html = str(tbl)
|
| 96 |
else:
|
| 97 |
+
# fallback: first table on page (only if no table_id specified)
|
| 98 |
first = soup.find("table")
|
| 99 |
+
if first:
|
| 100 |
+
tbl_html = str(first)
|
| 101 |
+
|
| 102 |
+
if not tbl_html:
|
| 103 |
+
return pd.DataFrame()
|
| 104 |
|
| 105 |
try:
|
| 106 |
+
# pd.read_html returns a list of DataFrames, we want the first one
|
| 107 |
return pd.read_html(tbl_html)[0]
|
| 108 |
+
except ValueError: # No tables found in the provided HTML string
|
| 109 |
+
return pd.DataFrame()
|
| 110 |
+
except Exception as e:
|
| 111 |
+
st.error(f"Error parsing table with pandas: {e}")
|
| 112 |
return pd.DataFrame()
|
| 113 |
|
| 114 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 124 |
for letter in map(chr, range(ord('a'), ord('z')+1)):
|
| 125 |
url = f"{base}{letter}/"
|
| 126 |
html = fetch_html(url)
|
| 127 |
+
if not html:
|
| 128 |
+
continue
|
| 129 |
+
|
| 130 |
+
soup = BeautifulSoup(html, "lxml")
|
| 131 |
+
# The players table is usually directly available, not commented out.
|
| 132 |
+
table = soup.find("table", {"id": "players"})
|
| 133 |
+
if not table:
|
| 134 |
continue
|
| 135 |
|
| 136 |
+
for row in table.select("tbody tr"):
|
|
|
|
|
|
|
| 137 |
th = row.find("th", {"data-stat": "player"})
|
| 138 |
if not th:
|
| 139 |
continue
|
|
|
|
| 155 |
Returns cleaned DataFrame.
|
| 156 |
"""
|
| 157 |
html = fetch_html(bbr_url)
|
| 158 |
+
if not html:
|
| 159 |
+
return pd.DataFrame()
|
| 160 |
+
|
| 161 |
df = parse_table(html, table_id="per_game")
|
| 162 |
if df.empty or "Season" not in df.columns:
|
| 163 |
return pd.DataFrame()
|
| 164 |
|
| 165 |
+
# drop repeated header rows (e.g., rows where 'Season' is literally 'Season')
|
| 166 |
df = df[df["Season"] != "Season"].copy()
|
| 167 |
+
df["Season"] = df["Season"].astype(str) # Ensure season is string
|
| 168 |
+
|
| 169 |
+
# Standardize column names to match previous nba_api output expectations
|
| 170 |
+
df = df.rename(columns={
|
| 171 |
+
'G': 'GP', 'GS': 'GS', 'MP': 'MIN',
|
| 172 |
+
'FG%': 'FG_PCT', '3P%': 'FG3_PCT', 'FT%': 'FT_PCT',
|
| 173 |
+
'TRB': 'REB', 'AST': 'AST', 'STL': 'STL', 'BLK': 'BLK', 'TOV': 'TO',
|
| 174 |
+
'PF': 'PF', 'PTS': 'PTS',
|
| 175 |
+
'Age': 'AGE', 'Tm': 'TEAM_ABBREVIATION', 'Lg': 'LEAGUE_ID', 'Pos': 'POSITION',
|
| 176 |
+
'FG': 'FGM', 'FGA': 'FGA', '3P': 'FG3M', '3PA': 'FG3A',
|
| 177 |
+
'2P': 'FGM2', '2PA': 'FGA2', '2P%': 'FG2_PCT', 'eFG%': 'EFG_PCT',
|
| 178 |
+
'FT': 'FTM', 'FTA': 'FTA', 'ORB': 'OREB', 'DRB': 'DREB'
|
| 179 |
+
})
|
| 180 |
+
|
| 181 |
+
# Coerce all numeric columns
|
| 182 |
+
# Exclude columns that are definitely not numeric or are identifiers
|
| 183 |
+
non_numeric_cols = {'Season', 'TEAM_ABBREVIATION', 'LEAGUE_ID', 'POSITION', 'Player'}
|
| 184 |
for col in df.columns:
|
| 185 |
+
if col not in non_numeric_cols:
|
| 186 |
df[col] = pd.to_numeric(df[col], errors="coerce")
|
| 187 |
+
|
| 188 |
return df
|
| 189 |
|
| 190 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
|
|
| 197 |
"""
|
| 198 |
url = f"https://www.basketball-reference.com/leagues/NBA_{year}_per_game.html"
|
| 199 |
html = fetch_html(url)
|
| 200 |
+
if not html:
|
| 201 |
+
return pd.DataFrame()
|
| 202 |
+
|
| 203 |
+
df = parse_table(html, table_id="per_game-team") # Correct table ID for team stats
|
| 204 |
if df.empty or "Team" not in df.columns:
|
| 205 |
return pd.DataFrame()
|
| 206 |
|
| 207 |
# drop repeated headers & rename
|
| 208 |
df = df[df["Team"] != "Team"].copy()
|
| 209 |
df.rename(columns={"Team": "Tm"}, inplace=True)
|
| 210 |
+
|
| 211 |
+
# Standardize column names
|
| 212 |
+
df = df.rename(columns={
|
| 213 |
+
'G': 'GP', 'MP': 'MIN',
|
| 214 |
+
'FG%': 'FG_PCT', '3P%': 'FG3_PCT', 'FT%': 'FT_PCT',
|
| 215 |
+
'TRB': 'REB', 'AST': 'AST', 'STL': 'STL', 'BLK': 'BLK', 'TOV': 'TO',
|
| 216 |
+
'PF': 'PF', 'PTS': 'PTS',
|
| 217 |
+
'Rk': 'RANK', 'W': 'WINS', 'L': 'LOSSES', 'W/L%': 'WIN_LOSS_PCT',
|
| 218 |
+
'FG': 'FGM', 'FGA': 'FGA', '3P': 'FG3M', '3PA': 'FG3A',
|
| 219 |
+
'2P': 'FGM2', '2PA': 'FGA2', '2P%': 'FG2_PCT', 'eFG%': 'EFG_PCT',
|
| 220 |
+
'FT': 'FTM', 'FTA': 'FTA', 'ORB': 'OREB', 'DRB': 'DREB'
|
| 221 |
+
})
|
| 222 |
+
|
| 223 |
# coerce numeric columns
|
| 224 |
+
non_numeric_cols = {"Tm", "RANK"} # 'RANK' is usually numeric, but 'Tm' is not
|
| 225 |
for col in df.columns:
|
| 226 |
+
if col not in non_numeric_cols:
|
| 227 |
df[col] = pd.to_numeric(df[col], errors="coerce")
|
| 228 |
|
| 229 |
return df
|
| 230 |
|
| 231 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 232 |
+
# Perplexity integration
|
| 233 |
PERP_KEY = os.getenv("PERPLEXITY_API_KEY")
|
| 234 |
PERP_URL = "https://api.perplexity.ai/chat/completions"
|
| 235 |
|
|
|
|
| 239 |
return ""
|
| 240 |
hdr = {'Authorization':f'Bearer {PERP_KEY}','Content-Type':'application/json'}
|
| 241 |
payload = {
|
| 242 |
+
"model":"sonar-medium-online", # Changed to a commonly available online model
|
| 243 |
"messages":[{"role":"system","content":system},{"role":"user","content":prompt}],
|
| 244 |
"max_tokens":max_tokens, "temperature":temp
|
| 245 |
}
|
|
|
|
| 247 |
r = requests.post(PERP_URL, json=payload, headers=hdr, timeout=45)
|
| 248 |
r.raise_for_status()
|
| 249 |
return r.json().get("choices", [{}])[0].get("message",{}).get("content","")
|
| 250 |
+
except requests.exceptions.RequestException as e:
|
| 251 |
+
error_message = f"Error communicating with Perplexity API: {e}"
|
| 252 |
+
if r.response is not None:
|
| 253 |
+
try:
|
| 254 |
+
error_detail = r.response.json().get("error", {}).get("message", r.response.text)
|
| 255 |
+
error_message = f"Perplexity API error: {error_detail}"
|
| 256 |
+
except ValueError:
|
| 257 |
+
error_message = f"Perplexity API error: {r.response.status_code} - {r.response.reason}"
|
| 258 |
+
st.error(error_message)
|
| 259 |
+
return ""
|
| 260 |
except Exception as e:
|
| 261 |
+
st.error(f"An unexpected error occurred with Perplexity API: {e}")
|
| 262 |
return ""
|
| 263 |
|
| 264 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 265 |
+
# Helper for dynamic season generation
|
| 266 |
+
def get_available_seasons(num_seasons=6):
|
| 267 |
+
"""Generates a list of recent NBA seasons in 'YYYYβYY' format."""
|
| 268 |
+
current_year = datetime.now().year
|
| 269 |
+
current_month = datetime.now().month
|
| 270 |
+
|
| 271 |
+
# Determine the latest season end year.
|
| 272 |
+
# If it's before July (e.g., May 2025), the current season is 2024-25 (ends 2025).
|
| 273 |
+
# If it's July or later (e.g., July 2025), the 2024-25 season just finished,
|
| 274 |
+
# and the next season (2025-26) is considered the "current" one for future projections.
|
| 275 |
+
latest_season_end_year = current_year
|
| 276 |
+
if current_month >= 7: # NBA season typically ends in June
|
| 277 |
+
latest_season_end_year += 1
|
| 278 |
+
|
| 279 |
+
seasons_list = []
|
| 280 |
+
for i in range(num_seasons):
|
| 281 |
+
end_year = latest_season_end_year - i
|
| 282 |
+
start_year = end_year - 1
|
| 283 |
+
seasons_list.append(f"{start_year}β{end_year}")
|
| 284 |
+
return sorted(seasons_list, reverse=True) # Sort to show most recent first
|
| 285 |
+
|
| 286 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 287 |
+
# Main App Structure
|
| 288 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 289 |
def main():
|
| 290 |
st.markdown('<h1 class="main-header">π NBA Analytics Hub (BBR Edition)</h1>', unsafe_allow_html=True)
|
|
|
|
| 305 |
elif page == "Roster Builder": roster_builder()
|
| 306 |
else: trade_analyzer()
|
| 307 |
|
| 308 |
+
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 309 |
+
# Page Implementations
|
| 310 |
# βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 311 |
def player_vs_player():
|
| 312 |
st.markdown('<h2 class="section-header">Player vs Player Comparison</h2>', unsafe_allow_html=True)
|
| 313 |
idx = get_player_index()
|
| 314 |
names = idx['name'].tolist()
|
| 315 |
+
selected_players = st.multiselect("Select Players (up to 4)", names, max_selections=4)
|
| 316 |
+
|
| 317 |
+
available_seasons = get_available_seasons()
|
| 318 |
+
selected_seasons = st.multiselect("Select Seasons", available_seasons, default=[available_seasons[0]])
|
| 319 |
|
| 320 |
if st.button("Run Comparison"):
|
| 321 |
+
if not selected_players:
|
| 322 |
+
st.warning("Please select at least one player to compare.")
|
| 323 |
+
return
|
| 324 |
+
|
| 325 |
+
stats_tabs = st.tabs(["Basic Stats", "Advanced Stats", "Visualizations"])
|
| 326 |
+
all_player_season_data = [] # To store individual season rows for each player
|
| 327 |
+
|
| 328 |
+
with st.spinner("Fetching player data..."):
|
| 329 |
+
for player_name in selected_players:
|
| 330 |
+
player_url = idx.loc[idx.name == player_name, 'url'].iat[0]
|
| 331 |
+
df_player_career = player_season_stats(player_url)
|
| 332 |
+
|
| 333 |
+
if not df_player_career.empty:
|
| 334 |
+
# Filter for selected seasons and ensure 'Season' column is consistent
|
| 335 |
+
df_player_career['Season'] = df_player_career['Season'].str.replace('-', 'β')
|
| 336 |
+
filtered_df = df_player_career[df_player_career['Season'].isin(selected_seasons)].copy()
|
| 337 |
+
|
| 338 |
+
if not filtered_df.empty:
|
| 339 |
+
filtered_df['Player'] = player_name # Add player name for identification
|
| 340 |
+
all_player_season_data.append(filtered_df)
|
| 341 |
+
else:
|
| 342 |
+
st.info(f"No data found for {player_name} in selected seasons.")
|
| 343 |
+
else:
|
| 344 |
+
st.info(f"Could not fetch career stats for {player_name}.")
|
| 345 |
+
|
| 346 |
+
if not all_player_season_data:
|
| 347 |
+
st.info("No data available for the selected players and seasons.")
|
| 348 |
+
return
|
| 349 |
+
|
| 350 |
+
# Concatenate all collected season data into one DataFrame
|
| 351 |
+
comparison_df_raw = pd.concat(all_player_season_data, ignore_index=True)
|
| 352 |
+
|
| 353 |
+
with stats_tabs[0]:
|
| 354 |
+
st.subheader("Basic Statistics")
|
| 355 |
+
# Group by player and average for the basic stats table if multiple seasons are selected
|
| 356 |
+
# Otherwise, show individual season stats if only one season is selected
|
| 357 |
+
if len(selected_seasons) > 1:
|
| 358 |
+
basic_display_df = comparison_df_raw.groupby('Player').mean(numeric_only=True).reset_index()
|
| 359 |
+
basic_cols = ['Player', 'GP', 'MIN', 'PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', 'FT_PCT', 'FG3_PCT']
|
| 360 |
+
else:
|
| 361 |
+
basic_display_df = comparison_df_raw.copy()
|
| 362 |
+
basic_cols = ['Player', 'Season', 'GP', 'MIN', 'PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', 'FT_PCT', 'FG3_PCT']
|
| 363 |
+
|
| 364 |
+
display_cols = [col for col in basic_cols if col in basic_display_df.columns]
|
| 365 |
+
st.dataframe(basic_display_df[display_cols].round(2), use_container_width=True)
|
| 366 |
+
|
| 367 |
+
with stats_tabs[1]:
|
| 368 |
+
st.subheader("Advanced Statistics")
|
| 369 |
+
if not comparison_df_raw.empty:
|
| 370 |
+
advanced_df = comparison_df_raw.copy()
|
| 371 |
+
|
| 372 |
+
# Calculate TS% (True Shooting Percentage)
|
| 373 |
+
# Ensure FGA and FTA are numeric and not zero to avoid division by zero
|
| 374 |
+
advanced_df['FGA'] = pd.to_numeric(advanced_df['FGA'], errors='coerce').fillna(0)
|
| 375 |
+
advanced_df['FTA'] = pd.to_numeric(advanced_df['FTA'], errors='coerce').fillna(0)
|
| 376 |
+
advanced_df['PTS'] = pd.to_numeric(advanced_df['PTS'], errors='coerce').fillna(0)
|
| 377 |
+
|
| 378 |
+
advanced_df['TS_PCT'] = advanced_df.apply(
|
| 379 |
+
lambda row: row['PTS'] / (2 * (row['FGA'] + 0.44 * row['FTA'])) if (row['FGA'] + 0.44 * row['FTA']) != 0 else 0,
|
| 380 |
+
axis=1
|
| 381 |
+
)
|
| 382 |
+
|
| 383 |
+
if len(selected_seasons) > 1:
|
| 384 |
+
advanced_display_df = advanced_df.groupby('Player').mean(numeric_only=True).reset_index()
|
| 385 |
+
advanced_cols = ['Player', 'PTS', 'REB', 'AST', 'FG_PCT', 'TS_PCT']
|
| 386 |
+
else:
|
| 387 |
+
advanced_display_df = advanced_df.copy()
|
| 388 |
+
advanced_cols = ['Player', 'Season', 'PTS', 'REB', 'AST', 'FG_PCT', 'TS_PCT']
|
| 389 |
+
|
| 390 |
+
display_cols = [col for col in advanced_cols if col in advanced_display_df.columns]
|
| 391 |
+
st.dataframe(advanced_display_df[display_cols].round(3), use_container_width=True)
|
| 392 |
+
else:
|
| 393 |
+
st.info("No data available for advanced statistics.")
|
| 394 |
+
|
| 395 |
+
with stats_tabs[2]:
|
| 396 |
+
st.subheader("Player Comparison Charts")
|
| 397 |
+
|
| 398 |
+
if not comparison_df_raw.empty:
|
| 399 |
+
metrics = ['PTS', 'REB', 'AST', 'FG_PCT', '3P_PCT', 'FT_PCT', 'STL', 'BLK']
|
| 400 |
+
available_metrics = [m for m in metrics if m in comparison_df_raw.columns]
|
| 401 |
+
|
| 402 |
+
if available_metrics:
|
| 403 |
+
selected_metric = st.selectbox("Select Metric to Visualize", available_metrics)
|
| 404 |
+
|
| 405 |
+
if selected_metric:
|
| 406 |
+
# Determine if we are showing a trend for one player or comparison for multiple
|
| 407 |
+
if len(selected_players) == 1 and len(selected_seasons) > 1:
|
| 408 |
+
# Show trend over seasons for one player
|
| 409 |
+
player_trend_df = comparison_df_raw[comparison_df_raw['Player'] == selected_players[0]].sort_values(by='Season')
|
| 410 |
+
fig = px.line(
|
| 411 |
+
player_trend_df,
|
| 412 |
+
x='Season',
|
| 413 |
+
y=selected_metric,
|
| 414 |
+
title=f"{selected_players[0]} - {selected_metric} Trend",
|
| 415 |
+
markers=True
|
| 416 |
+
)
|
| 417 |
+
else:
|
| 418 |
+
# Average over selected seasons for multiple players for bar chart
|
| 419 |
+
avg_comparison_df = comparison_df_raw.groupby('Player')[available_metrics].mean().reset_index()
|
| 420 |
+
fig = px.bar(
|
| 421 |
+
avg_comparison_df,
|
| 422 |
+
x='Player',
|
| 423 |
+
y=selected_metric,
|
| 424 |
+
title=f"Average {selected_metric} Comparison (Selected Seasons)",
|
| 425 |
+
color='Player'
|
| 426 |
+
)
|
| 427 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 428 |
+
|
| 429 |
+
# Radar chart for multi-metric comparison
|
| 430 |
+
radar_metrics_for_chart = ['PTS', 'REB', 'AST', 'STL', 'BLK']
|
| 431 |
+
radar_metrics_for_chart = [m for m in radar_metrics_for_chart if m in comparison_df_raw.columns]
|
| 432 |
+
|
| 433 |
+
if len(radar_metrics_for_chart) >= 3:
|
| 434 |
+
radar_data = {}
|
| 435 |
+
# Use the averaged data for radar chart if multiple seasons
|
| 436 |
+
if len(selected_seasons) > 1:
|
| 437 |
+
radar_source_df = comparison_df_raw.groupby('Player')[radar_metrics_for_chart].mean(numeric_only=True).reset_index()
|
| 438 |
+
else:
|
| 439 |
+
radar_source_df = comparison_df_raw.copy()
|
| 440 |
+
|
| 441 |
+
scaled_radar_df = radar_source_df.copy()
|
| 442 |
+
|
| 443 |
+
# Simple min-max scaling for radar chart visualization (0-100)
|
| 444 |
+
for col in radar_metrics_for_chart:
|
| 445 |
+
min_val = scaled_radar_df[col].min()
|
| 446 |
+
max_val = scaled_radar_df[col].max()
|
| 447 |
+
if max_val > min_val:
|
| 448 |
+
scaled_radar_df[col] = ((scaled_radar_df[col] - min_val) / (max_val - min_val)) * 100
|
| 449 |
+
else:
|
| 450 |
+
scaled_radar_df[col] = 0 # Default if all values are the same
|
| 451 |
+
|
| 452 |
+
for _, row in scaled_radar_df.iterrows():
|
| 453 |
+
radar_data[row['Player']] = {
|
| 454 |
+
metric: row[metric] for metric in radar_metrics_for_chart
|
| 455 |
+
}
|
| 456 |
+
|
| 457 |
+
if radar_data:
|
| 458 |
+
radar_fig = create_radar_chart(radar_data, radar_metrics_for_chart)
|
| 459 |
+
st.plotly_chart(radar_fig, use_container_width=True)
|
| 460 |
+
else:
|
| 461 |
+
st.info("Could not generate radar chart data.")
|
| 462 |
+
else:
|
| 463 |
+
st.info("Select at least 3 common metrics for a radar chart (e.g., PTS, REB, AST, STL, BLK).")
|
| 464 |
+
else:
|
| 465 |
+
st.info("No common metrics available for visualization.")
|
| 466 |
+
else:
|
| 467 |
+
st.info("No data available for visualizations.")
|
| 468 |
+
|
| 469 |
|
| 470 |
def team_vs_team():
|
| 471 |
st.markdown('<h2 class="section-header">Team vs Team Analysis</h2>', unsafe_allow_html=True)
|
| 472 |
+
|
| 473 |
+
available_seasons = get_available_seasons()
|
| 474 |
+
selected_season_str = st.selectbox("Select Season", available_seasons, index=0)
|
| 475 |
+
|
| 476 |
+
# Extract the end year from the season string (e.g., "2024β25" -> 2025)
|
| 477 |
+
year_for_team_stats = int(selected_season_str.split('β')[1])
|
| 478 |
+
|
| 479 |
+
tm_df = team_per_game(year_for_team_stats)
|
| 480 |
+
if tm_df.empty:
|
| 481 |
+
st.info(f"No team data available for the {selected_season_str} season.")
|
| 482 |
+
return
|
| 483 |
+
|
| 484 |
teams = tm_df['Tm'].unique().tolist()
|
| 485 |
+
selected_teams = st.multiselect("Select Teams (up to 4)", teams, max_selections=4)
|
| 486 |
|
| 487 |
if st.button("Run Comparison"):
|
| 488 |
+
if not selected_teams:
|
| 489 |
+
st.warning("Please select at least one team.")
|
| 490 |
+
return
|
| 491 |
+
|
| 492 |
stats = []
|
| 493 |
+
for t in selected_teams:
|
| 494 |
+
df = tm_df[tm_df.Tm == t].copy() # Use .copy() to avoid SettingWithCopyWarning
|
| 495 |
+
if not df.empty:
|
| 496 |
+
# For team stats, we usually get one row per team per season from team_per_game
|
| 497 |
+
# So, no need for .mean() here, just take the row.
|
| 498 |
+
df['Team'] = t # Add 'Team' column for consistency
|
| 499 |
+
df['Season'] = selected_season_str # Add 'Season' column
|
| 500 |
+
stats.append(df.iloc[0].to_dict()) # Convert the single row to dict
|
| 501 |
+
else:
|
| 502 |
+
st.info(f"No data found for team {t} in {selected_season_str}.")
|
| 503 |
+
|
| 504 |
+
if not stats:
|
| 505 |
+
st.info("No data available for the selected teams.")
|
| 506 |
+
return
|
| 507 |
+
|
| 508 |
+
comp = pd.DataFrame(stats)
|
| 509 |
+
# Ensure numeric columns are actually numeric for display and calculations
|
| 510 |
+
for col in ['PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', '3P_PCT', 'FT_PCT']:
|
| 511 |
+
if col in comp.columns:
|
| 512 |
+
comp[col] = pd.to_numeric(comp[col], errors='coerce')
|
| 513 |
+
|
| 514 |
+
st.subheader("Team Statistics Comparison")
|
| 515 |
+
cols = ['Team', 'Season', 'PTS', 'REB', 'AST', 'STL', 'BLK', 'FG_PCT', '3P_PCT', 'FT_PCT']
|
| 516 |
+
display_cols = [col for col in cols if col in comp.columns]
|
| 517 |
+
st.dataframe(comp[display_cols].round(2), use_container_width=True)
|
| 518 |
+
|
| 519 |
+
st.subheader("Team Performance Visualization")
|
| 520 |
+
metric_options = ['PTS', 'REB', 'AST', 'FG_PCT', '3P_PCT', 'FT_PCT']
|
| 521 |
+
available_metrics = [m for m in metric_options if m in comp.columns]
|
| 522 |
+
|
| 523 |
+
if available_metrics:
|
| 524 |
+
selected_metric = st.selectbox("Select Metric", available_metrics)
|
| 525 |
+
|
| 526 |
+
fig = px.bar(
|
| 527 |
+
comp,
|
| 528 |
+
x='Team',
|
| 529 |
+
y=selected_metric,
|
| 530 |
+
color='Team', # Color by team for clarity
|
| 531 |
+
title=f"Team {selected_metric} Comparison ({selected_season_str} Season)",
|
| 532 |
+
barmode='group'
|
| 533 |
+
)
|
| 534 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 535 |
+
else:
|
| 536 |
+
st.info("No common metrics available for visualization.")
|
| 537 |
+
|
| 538 |
|
| 539 |
def awards_predictor():
|
| 540 |
st.markdown('<h2 class="section-header">NBA Awards Predictor</h2>', unsafe_allow_html=True)
|
|
|
|
| 571 |
|
| 572 |
def ai_chat():
|
| 573 |
st.markdown('<h2 class="section-header">AI Chat & Insights</h2>', unsafe_allow_html=True)
|
| 574 |
+
if 'chat_history' not in st.session_state: st.session_state.chat_history=[]
|
| 575 |
+
for msg in st.session_state.chat_history:
|
| 576 |
with st.chat_message(msg["role"]):
|
| 577 |
st.write(msg["content"])
|
| 578 |
if prompt:=st.chat_input("Ask me anything about NBAβ¦"):
|
| 579 |
+
st.session_state.chat_history.append({"role":"user","content":prompt})
|
| 580 |
+
with st.chat_message("user"):
|
| 581 |
+
st.write(prompt) # Display user message immediately
|
| 582 |
with st.chat_message("assistant"):
|
| 583 |
ans = ask_perp(prompt, system="You are an NBA expert analyst AI.", max_tokens=700)
|
| 584 |
st.write(ans)
|
| 585 |
+
st.session_state.chat_history.append({"role":"assistant","content":ans})
|
| 586 |
st.subheader("Quick Actions")
|
| 587 |
c1,c2,c3 = st.columns(3)
|
| 588 |
if c1.button("π Contenders"):
|
| 589 |
+
prompt = "Analyze the current NBA championship contenders for 2024. Who are the top 5 teams and why?"
|
| 590 |
+
response = ask_perp(prompt)
|
| 591 |
+
if response: st.write(response)
|
| 592 |
if c2.button("β Rising Stars"):
|
| 593 |
+
prompt = "Who are the most promising young NBA players to watch in 2024? Focus on players 23 and under."
|
| 594 |
+
response = ask_perp(prompt)
|
| 595 |
+
if response: st.write(response)
|
| 596 |
if c3.button("π Trades"):
|
| 597 |
+
prompt = "What are some potential NBA trades that could happen this season? Analyze team needs and available players."
|
| 598 |
+
response = ask_perp(prompt)
|
| 599 |
+
if response: st.write(response)
|
| 600 |
|
| 601 |
def young_projections():
|
| 602 |
st.markdown('<h2 class="section-header">Young Player Projections</h2>', unsafe_allow_html=True)
|
| 603 |
+
all_p_df = get_player_index()
|
| 604 |
+
all_p = all_p_df['name'].tolist()
|
| 605 |
sp = st.selectbox("Select or enter player", [""]+all_p)
|
| 606 |
if not sp:
|
| 607 |
sp = st.text_input("Enter player name manually")
|
|
|
|
| 621 |
st.write(out)
|
| 622 |
yrs_lbl = [f"Year {i+1}" for i in range(5)]
|
| 623 |
vals = [ppg*(1+0.1*i) for i in range(5)]
|
| 624 |
+
fig = go.Figure()
|
| 625 |
+
fig.add_trace(go.Scatter(x=yrs_lbl, y=vals, mode='lines+markers', name='Projected PPG'))
|
| 626 |
+
fig.update_layout(title=f"{sp} - PPG Projection", xaxis_title="Years", yaxis_title="Points Per Game")
|
| 627 |
+
st.plotly_chart(fig, use_container_width=True)
|
| 628 |
|
| 629 |
def similar_players():
|
| 630 |
st.markdown('<h2 class="section-header">Similar Players Finder</h2>', unsafe_allow_html=True)
|
| 631 |
+
all_p_df = get_player_index()
|
| 632 |
+
all_p = all_p_df['name'].tolist()
|
| 633 |
tp = st.selectbox("Target Player", all_p)
|
| 634 |
+
crit = st.multiselect("Criteria",["Position","Height/Weight","Playing Style","Statistical Profile","Age/Experience"],default=["Playing Style","Statistical Profile"])
|
| 635 |
if tp and crit and st.button("Find Similar"):
|
| 636 |
+
prompt = f"Find top 5 current and top 3 historical similar to {tp} by {', '.join(crit)}. Provide detailed reasoning."
|
| 637 |
st.write(ask_perp(prompt, system="You are a similarity expert AI.", max_tokens=800))
|
| 638 |
st.subheader("Manual Compare")
|
| 639 |
p1 = st.selectbox("Player 1", all_p, key="p1")
|
| 640 |
p2 = st.selectbox("Player 2", all_p, key="p2")
|
| 641 |
if p1 and p2 and p1!=p2 and st.button("Compare Players"):
|
| 642 |
+
prompt = f"Compare {p1} vs {p2} on statistical comparison (current season), playing style similarities and differences, strengths and weaknesses, team impact and role, and overall similarity score (1-10)."
|
| 643 |
st.write(ask_perp(prompt, system="You are a comparison expert AI.", max_tokens=700))
|
| 644 |
|
| 645 |
def roster_builder():
|
| 646 |
st.markdown('<h2 class="section-header">NBA Roster Builder</h2>', unsafe_allow_html=True)
|
| 647 |
+
cap = st.number_input("Salary Cap (Millions)",100,200,136)
|
| 648 |
+
strat = st.selectbox("Strategy",["Championship Contender","Young Core Development","Balanced Veteran Mix","Small Ball","Defense First"])
|
| 649 |
+
pos = st.multiselect("Priority Positions",["Point Guard","Shooting Guard","Small Forward","Power Forward","Center"],default=["Point Guard","Center"])
|
| 650 |
st.subheader("Budget Allocation")
|
| 651 |
cols = st.columns(5)
|
| 652 |
alloc = {}
|
|
|
|
| 654 |
for i,p in enumerate(["PG","SG","SF","PF","C"]):
|
| 655 |
val = cols[i].number_input(f"{p} Budget ($M)",0,50,20, key=f"b{p}")
|
| 656 |
alloc[p]=val; total+=val
|
| 657 |
+
st.write(f"Total Allocated: ${total}M / ${cap}M")
|
| 658 |
+
if total>cap: st.error("Budget exceeds salary cap!")
|
| 659 |
|
| 660 |
+
if st.button("Generate Roster Suggestions"):
|
| 661 |
if total<=cap:
|
| 662 |
prompt = (
|
| 663 |
+
f"Build an NBA roster with the following constraints: "
|
| 664 |
+
f"Salary Cap: ${cap} million, Team Strategy: {strat}, "
|
| 665 |
+
f"Priority Positions: {', '.join(pos)}, "
|
| 666 |
+
f"Position Budgets: {alloc}. "
|
| 667 |
+
"Please provide: 1. Starting lineup with specific player recommendations. "
|
| 668 |
+
"2. Key bench players (6th man, backup center, etc.). "
|
| 669 |
+
"3. Total estimated salary breakdown. "
|
| 670 |
+
"4. Rationale for each major signing. "
|
| 671 |
+
"5. How this roster fits the chosen strategy. "
|
| 672 |
+
"6. Potential weaknesses and how to address them. "
|
| 673 |
+
"Focus on realistic player availability and current market values."
|
| 674 |
)
|
| 675 |
+
st.markdown("### AI Roster Recommendations")
|
| 676 |
+
st.write(ask_perp(prompt, system="You are an NBA roster building expert AI.", max_tokens=900))
|
| 677 |
else:
|
| 678 |
+
st.warning("Please adjust your budget to be within the salary cap before generating suggestions.")
|
| 679 |
|
| 680 |
def trade_analyzer():
|
| 681 |
st.markdown('<h2 class="section-header">Trade Scenario Analyzer</h2>', unsafe_allow_html=True)
|
| 682 |
+
t1 = st.text_input("Team 1 trades (e.g., 'LeBron James for Anthony Davis')")
|
| 683 |
+
t2 = st.text_input("Team 2 trades (e.g., 'Anthony Davis for LeBron James')")
|
| 684 |
if t1 and t2 and st.button("Analyze Trade"):
|
| 685 |
prompt = (
|
| 686 |
+
f"Analyze this potential NBA trade: Team 1 trades: {t1}. Team 2 trades: {t2}. "
|
| 687 |
+
"Please evaluate: 1. Fair value assessment. 2. How this trade helps each team. "
|
| 688 |
+
"3. Salary cap implications. 4. Impact on team chemistry and performance. "
|
| 689 |
+
"5. Likelihood of this trade happening. 6. Alternative trade suggestions. "
|
| 690 |
+
"Consider current team needs and player contracts."
|
| 691 |
)
|
| 692 |
st.write(ask_perp(prompt, system="You are a trade analysis AI.", max_tokens=700))
|
| 693 |
|