mirror of
https://git.turbo-data.com/nprasad2077/Streamlit_NBA.git
synced 2026-11-04 08:24:50 +00:00
330 lines
12 KiB
Python
330 lines
12 KiB
Python
import plotly.express as px
|
|
import streamlit as st
|
|
from streamlit_echarts import st_echarts
|
|
|
|
from utils.charts import apply_arena_theme
|
|
from utils.db import get_selected_season, query
|
|
|
|
SEASON, SEASON_LABEL = get_selected_season()
|
|
|
|
st.title(f":material/science: Player Lab — {SEASON_LABEL}")
|
|
|
|
min_games = st.sidebar.slider("Min Games", 5, 60, 20, key="player_min_games")
|
|
|
|
players = query(
|
|
"""
|
|
SELECT player_name, team, count(*) AS games,
|
|
round(avg(points), 1) AS ppg,
|
|
round(avg(assists), 1) AS apg,
|
|
round(avg(total_rebounds), 1) AS rpg,
|
|
round(avg(steals), 1) AS spg,
|
|
round(avg(blocks), 1) AS bpg,
|
|
round(avg(box_plus_minus), 2) AS bpm,
|
|
round(avg(usage_pct), 1) AS usage,
|
|
round(avg(true_shooting_pct) * 100, 1) AS ts_pct,
|
|
round(avg(net_rating), 1) AS net_rtg,
|
|
sum(CASE WHEN is_double_double THEN 1 ELSE 0 END) AS double_doubles,
|
|
sum(CASE WHEN is_triple_double THEN 1 ELSE 0 END) AS triple_doubles
|
|
FROM main_intermediate.int_player_performance
|
|
WHERE season_start_year = ? AND minutes_played >= 20
|
|
GROUP BY player_name, team
|
|
HAVING count(*) >= ?
|
|
ORDER BY bpm DESC
|
|
""",
|
|
[SEASON, min_games],
|
|
)
|
|
|
|
tab_board, tab_compare, tab_archetypes = st.tabs(
|
|
[
|
|
":material/leaderboard: Leaderboard",
|
|
":material/compare_arrows: Comparison",
|
|
":material/hub: Archetypes",
|
|
]
|
|
)
|
|
|
|
with tab_board:
|
|
if not players.empty:
|
|
fig = px.scatter(
|
|
players,
|
|
x="usage",
|
|
y="ts_pct",
|
|
size="ppg",
|
|
hover_name="player_name",
|
|
color="bpm",
|
|
color_continuous_scale="RdYlGn",
|
|
title="Usage vs True Shooting % (size = PPG, color = BPM)",
|
|
)
|
|
fig.update_layout(xaxis_title="Usage %", yaxis_title="True Shooting %")
|
|
fig.add_hline(y=players["ts_pct"].median(), line_dash="dot", line_color="gray", opacity=0.4)
|
|
fig.add_vline(x=players["usage"].median(), line_dash="dot", line_color="gray", opacity=0.4)
|
|
apply_arena_theme(fig, height=500)
|
|
st.plotly_chart(fig, width="stretch")
|
|
|
|
col1, col2 = st.columns(2)
|
|
with col1:
|
|
fig2 = px.scatter(
|
|
players,
|
|
x="ppg",
|
|
y="apg",
|
|
hover_name="player_name",
|
|
color="bpm",
|
|
color_continuous_scale="RdYlGn",
|
|
size="usage",
|
|
title="Scoring vs Playmaking",
|
|
)
|
|
apply_arena_theme(fig2)
|
|
st.plotly_chart(fig2, width="stretch")
|
|
with col2:
|
|
top_scorers = players.nlargest(15, "ppg")
|
|
fig3 = px.bar(
|
|
top_scorers,
|
|
x="ppg",
|
|
y="player_name",
|
|
orientation="h",
|
|
color="ts_pct",
|
|
color_continuous_scale="RdYlGn",
|
|
title="Top 15 Scorers (color = TS%)",
|
|
)
|
|
fig3.update_layout(yaxis=dict(autorange="reversed"))
|
|
apply_arena_theme(fig3, height=450)
|
|
st.plotly_chart(fig3, width="stretch")
|
|
|
|
st.dataframe(
|
|
players,
|
|
column_config={
|
|
"ts_pct": st.column_config.ProgressColumn(
|
|
"TS%", min_value=40, max_value=75, format="%.1f%%"
|
|
),
|
|
"usage": st.column_config.ProgressColumn(
|
|
"USG%", min_value=5, max_value=40, format="%.1f%%"
|
|
),
|
|
"bpm": st.column_config.NumberColumn("BPM", format="%.2f"),
|
|
},
|
|
width="stretch",
|
|
hide_index=True,
|
|
)
|
|
|
|
with tab_compare:
|
|
player_list = players["player_name"].tolist()
|
|
selected_players = st.multiselect(
|
|
"Select players to compare", player_list, default=player_list[:3]
|
|
)
|
|
if selected_players:
|
|
comp = players[players["player_name"].isin(selected_players)]
|
|
categories = ["ppg", "apg", "rpg", "spg", "bpg", "usage", "ts_pct"]
|
|
labels = ["PPG", "APG", "RPG", "SPG", "BPG", "USG%", "TS%"]
|
|
|
|
RADAR_COLORS = [
|
|
"#E09F3E",
|
|
"#10B981",
|
|
"#38BDF8",
|
|
"#F43F5E",
|
|
"#A855F7",
|
|
"#42D4F4",
|
|
"#F032E6",
|
|
"#BFEF45",
|
|
"#FABED4",
|
|
"#469990",
|
|
]
|
|
radar_indicators = [
|
|
{"name": labels[i], "max": float(players[c].max() * 1.15)}
|
|
for i, c in enumerate(categories)
|
|
]
|
|
radar_data = [
|
|
{
|
|
"value": [float(row[c]) for c in categories],
|
|
"name": row["player_name"],
|
|
"lineStyle": {"color": RADAR_COLORS[i % len(RADAR_COLORS)], "width": 2.5},
|
|
"areaStyle": {"color": RADAR_COLORS[i % len(RADAR_COLORS)], "opacity": 0.3},
|
|
"itemStyle": {"color": RADAR_COLORS[i % len(RADAR_COLORS)]},
|
|
}
|
|
for i, (_, row) in enumerate(comp.iterrows())
|
|
]
|
|
echarts_option = {
|
|
"legend": {"data": [d["name"] for d in radar_data], "top": "bottom"},
|
|
"color": RADAR_COLORS[: len(radar_data)],
|
|
"radar": {
|
|
"indicator": radar_indicators,
|
|
"shape": "polygon",
|
|
"splitArea": {
|
|
"areaStyle": {
|
|
"color": ["#0B0F17", "#151D2A", "#1E293B", "#0B0F17", "#151D2A"]
|
|
}
|
|
},
|
|
"axisLine": {"lineStyle": {"color": "rgba(200, 200, 200, 0.3)"}},
|
|
"splitLine": {"lineStyle": {"color": "rgba(200, 200, 200, 0.2)"}},
|
|
},
|
|
"series": [{"type": "radar", "data": radar_data}],
|
|
}
|
|
st_echarts(options=echarts_option, height="500px")
|
|
|
|
st.dataframe(
|
|
comp.set_index("player_name")[categories + ["bpm", "net_rtg", "games"]].rename(
|
|
columns=dict(zip(categories, labels))
|
|
),
|
|
width="stretch",
|
|
)
|
|
|
|
st.subheader("Parallel Coordinates (Top 15 BPM)")
|
|
top_parallel = players.nlargest(15, "bpm")
|
|
parallel_dims = ["ppg", "apg", "rpg", "usage", "ts_pct", "bpm", "net_rtg"]
|
|
parallel_labels = ["PPG", "APG", "RPG", "USG%", "TS%", "BPM", "Net Rtg"]
|
|
parallel_option = {
|
|
"parallelAxis": [
|
|
{
|
|
"dim": i,
|
|
"name": parallel_labels[i],
|
|
"min": float(top_parallel[parallel_dims[i]].min()),
|
|
"max": float(top_parallel[parallel_dims[i]].max()),
|
|
}
|
|
for i in range(len(parallel_dims))
|
|
],
|
|
"tooltip": {"trigger": "item"},
|
|
"series": [
|
|
{
|
|
"type": "parallel",
|
|
"lineStyle": {"width": 2, "opacity": 0.6},
|
|
"data": [
|
|
[float(row[c]) for c in parallel_dims]
|
|
for _, row in top_parallel.iterrows()
|
|
],
|
|
}
|
|
],
|
|
}
|
|
st_echarts(options=parallel_option, height="400px")
|
|
|
|
with tab_archetypes:
|
|
archetype_players = query(
|
|
"""
|
|
WITH player_archetype_counts AS (
|
|
SELECT player_name, team, usage_tier, impact_tier, shooting_efficiency_tier,
|
|
count(*) AS games_in_archetype
|
|
FROM main_intermediate.int_player_performance
|
|
WHERE season_start_year = ? AND minutes_played >= 20
|
|
AND usage_tier != 'Insufficient Minutes'
|
|
GROUP BY player_name, team, usage_tier, impact_tier, shooting_efficiency_tier
|
|
),
|
|
player_modal AS (
|
|
SELECT *, ROW_NUMBER() OVER (
|
|
PARTITION BY player_name, team ORDER BY games_in_archetype DESC
|
|
) AS rn
|
|
FROM player_archetype_counts
|
|
)
|
|
SELECT player_name, team, usage_tier, impact_tier, shooting_efficiency_tier,
|
|
games_in_archetype
|
|
FROM player_modal
|
|
WHERE rn = 1
|
|
""",
|
|
[SEASON],
|
|
)
|
|
|
|
if not archetype_players.empty:
|
|
archetypes = (
|
|
archetype_players.groupby(["usage_tier", "impact_tier", "shooting_efficiency_tier"])
|
|
.agg(players=("player_name", "count"))
|
|
.reset_index()
|
|
.query("players >= 2")
|
|
.sort_values("players", ascending=False)
|
|
)
|
|
|
|
fig_arch = px.sunburst(
|
|
archetypes,
|
|
path=["usage_tier", "impact_tier", "shooting_efficiency_tier"],
|
|
values="players",
|
|
title="Player Archetype Hierarchy (click segments, then filter below)",
|
|
color="players",
|
|
color_continuous_scale="Blues",
|
|
)
|
|
apply_arena_theme(fig_arch, height=600)
|
|
st.plotly_chart(fig_arch, width="stretch")
|
|
|
|
st.subheader(":material/manage_search: Drill Into Archetype")
|
|
usage_tiers = ["All"] + sorted(archetype_players["usage_tier"].unique().tolist())
|
|
sel_usage = st.selectbox("Usage Tier", usage_tiers, key="arch_usage")
|
|
|
|
filtered = archetype_players
|
|
if sel_usage != "All":
|
|
filtered = filtered[filtered["usage_tier"] == sel_usage]
|
|
|
|
impact_tiers = ["All"] + sorted(filtered["impact_tier"].unique().tolist())
|
|
sel_impact = st.selectbox("Impact Tier", impact_tiers, key="arch_impact")
|
|
|
|
if sel_impact != "All":
|
|
filtered = filtered[filtered["impact_tier"] == sel_impact]
|
|
|
|
eff_tiers = ["All"] + sorted(filtered["shooting_efficiency_tier"].unique().tolist())
|
|
sel_eff = st.selectbox("Shooting Efficiency Tier", eff_tiers, key="arch_eff")
|
|
|
|
if sel_eff != "All":
|
|
filtered = filtered[filtered["shooting_efficiency_tier"] == sel_eff]
|
|
|
|
st.caption(f"Showing {len(filtered)} players matching selection")
|
|
st.dataframe(
|
|
filtered[
|
|
[
|
|
"player_name",
|
|
"team",
|
|
"usage_tier",
|
|
"impact_tier",
|
|
"shooting_efficiency_tier",
|
|
"games_in_archetype",
|
|
]
|
|
]
|
|
.sort_values("games_in_archetype", ascending=False)
|
|
.reset_index(drop=True),
|
|
width="stretch",
|
|
hide_index=True,
|
|
)
|
|
|
|
st.subheader("Player Specialists")
|
|
specialists = query(
|
|
"""
|
|
SELECT player_name, team, count(*) AS games,
|
|
round(avg(points), 1) AS ppg,
|
|
round(avg(box_plus_minus), 2) AS bpm,
|
|
round(100.0 * sum(CASE WHEN is_versatile THEN 1 ELSE 0 END) / count(*), 1) AS pct_versatile,
|
|
round(100.0 * sum(CASE WHEN is_defensive_specialist THEN 1 ELSE 0 END) / count(*), 1) AS pct_def_spec,
|
|
round(100.0 * sum(CASE WHEN is_three_and_d THEN 1 ELSE 0 END) / count(*), 1) AS pct_three_and_d
|
|
FROM main_intermediate.int_player_performance
|
|
WHERE season_start_year = ? AND minutes_played >= 20
|
|
GROUP BY player_name, team
|
|
HAVING count(*) >= ?
|
|
""",
|
|
[SEASON, min_games],
|
|
)
|
|
|
|
col1, col2, col3 = st.columns(3)
|
|
with col1:
|
|
versatile = specialists[specialists["pct_versatile"] >= 25].sort_values(
|
|
"pct_versatile", ascending=False
|
|
)
|
|
st.metric("Versatile", len(versatile))
|
|
st.caption("≥25% of games with double-double stats")
|
|
st.dataframe(
|
|
versatile[["player_name", "team", "ppg", "pct_versatile"]],
|
|
hide_index=True,
|
|
width="stretch",
|
|
)
|
|
with col2:
|
|
def_spec = specialists[specialists["pct_def_spec"] >= 25].sort_values(
|
|
"pct_def_spec", ascending=False
|
|
)
|
|
st.metric("Defensive Specialists", len(def_spec))
|
|
st.caption("≥25% of games as defensive specialist")
|
|
st.dataframe(
|
|
def_spec[["player_name", "team", "ppg", "pct_def_spec"]],
|
|
hide_index=True,
|
|
width="stretch",
|
|
)
|
|
with col3:
|
|
three_d = specialists[specialists["pct_three_and_d"] >= 25].sort_values(
|
|
"pct_three_and_d", ascending=False
|
|
)
|
|
st.metric("Three-and-D", len(three_d))
|
|
st.caption("≥25% of games as 3-and-D player")
|
|
st.dataframe(
|
|
three_d[["player_name", "team", "ppg", "pct_three_and_d"]],
|
|
hide_index=True,
|
|
width="stretch",
|
|
)
|