Files
nprasad2077 11a5cc0f18
CI Health Check & Quality Gate / Code, dbt & Live App Health Check (pull_request) Successful in 1m11s
shot chart and navbar
2026-10-28 02:37:56 -05:00

284 lines
9.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import plotly.express as px
import plotly.graph_objects as go
import streamlit as st
from utils.charts import AMBER, MADE_EMERALD, MISS_CRIMSON, apply_arena_theme
from utils.db import get_selected_season, query
SEASON, SEASON_LABEL = get_selected_season()
st.title(f":material/shield: Team Intelligence — {SEASON_LABEL}")
ratings = query(
"""
SELECT team, count(*) AS games,
sum(CASE WHEN game_result = 'W' THEN 1 ELSE 0 END) AS wins,
sum(CASE WHEN game_result = 'L' THEN 1 ELSE 0 END) AS losses,
round(100.0 * sum(CASE WHEN game_result = 'W' THEN 1 ELSE 0 END) / count(*), 1) AS win_pct,
round(avg(offensive_rating), 1) AS off_rtg,
round(avg(defensive_rating), 1) AS def_rtg,
round(avg(net_rating), 1) AS net_rtg,
round(avg(pace), 1) AS pace,
round(avg(effective_fg_pct) * 100, 1) AS efg_pct,
round(avg(turnover_rate) * 100, 1) AS tov_rate,
round(avg(offensive_rebound_rate) * 100, 1) AS orb_rate,
round(avg(free_throw_rate) * 100, 1) AS ft_rate
FROM main_intermediate.int_team_performance
WHERE season_start_year = ?
GROUP BY team ORDER BY net_rtg DESC
""",
[SEASON],
)
tab_ratings, tab_factors, tab_drilldown = st.tabs(
[
":material/bar_chart: Ratings & Standings",
":material/grid_view: Four Factors & Styles",
":material/manage_search: Team Drill-Down",
]
)
with tab_ratings:
fig = px.scatter(
ratings,
x="off_rtg",
y="def_rtg",
text="team",
size="win_pct",
color="net_rtg",
color_continuous_scale="RdYlGn",
title="Offensive vs Defensive Rating (size = Win%)",
)
fig.update_traces(textposition="top center", marker=dict(sizemin=8))
avg_off = ratings["off_rtg"].mean()
avg_def = ratings["def_rtg"].mean()
fig.add_hline(y=avg_def, line_dash="dash", line_color="gray", opacity=0.4)
fig.add_vline(x=avg_off, line_dash="dash", line_color="gray", opacity=0.4)
fig.update_layout(
xaxis_title="Offensive Rating → (higher = better)",
yaxis_title="← Defensive Rating (lower = better)",
yaxis=dict(autorange="reversed"),
)
apply_arena_theme(fig, height=550)
st.plotly_chart(fig, width="stretch")
win_sorted = ratings.sort_values("win_pct", ascending=True)
fig_win = px.bar(
win_sorted,
x="win_pct",
y="team",
orientation="h",
color="net_rtg",
color_continuous_scale="RdYlGn",
title="Win % (color = Net Rating)",
)
fig_win.update_layout(yaxis_title="", xaxis_title="Win %")
fig_win.add_vline(x=50, line_dash="dash", line_color="gray", opacity=0.5)
apply_arena_theme(fig_win, height=600)
st.plotly_chart(fig_win, width="stretch")
st.dataframe(
ratings,
column_config={
"win_pct": st.column_config.ProgressColumn(
"Win%", min_value=0, max_value=100, format="%.1f%%"
),
"efg_pct": st.column_config.ProgressColumn(
"eFG%", min_value=45, max_value=60, format="%.1f%%"
),
},
hide_index=True,
width="stretch",
)
with tab_factors:
st.subheader("Four Factors")
st.caption(
"The four key drivers of team success: eFG%, Turnover Rate, Offensive Rebound Rate, Free Throw Rate"
)
col1, col2 = st.columns(2)
with col1:
top10 = ratings.nlargest(10, "efg_pct")
fig = px.bar(
top10,
x="efg_pct",
y="team",
orientation="h",
color="efg_pct",
color_continuous_scale="Greens",
title="eFG% (Top 10)",
)
fig.update_layout(yaxis=dict(autorange="reversed"), showlegend=False)
apply_arena_theme(fig, height=350)
st.plotly_chart(fig, width="stretch")
with col2:
top10 = ratings.nsmallest(10, "tov_rate")
fig = px.bar(
top10,
x="tov_rate",
y="team",
orientation="h",
color="tov_rate",
color_continuous_scale="Reds_r",
title="Turnover Rate (Top 10 — lowest)",
)
fig.update_layout(yaxis=dict(autorange="reversed"), showlegend=False)
apply_arena_theme(fig, height=350)
st.plotly_chart(fig, width="stretch")
col3, col4 = st.columns(2)
with col3:
top10 = ratings.nlargest(10, "orb_rate")
fig = px.bar(
top10,
x="orb_rate",
y="team",
orientation="h",
color="orb_rate",
color_continuous_scale="Purples",
title="Offensive Rebound Rate (Top 10)",
)
fig.update_layout(yaxis=dict(autorange="reversed"), showlegend=False)
apply_arena_theme(fig, height=350)
st.plotly_chart(fig, width="stretch")
with col4:
top10 = ratings.nlargest(10, "ft_rate")
fig = px.bar(
top10,
x="ft_rate",
y="team",
orientation="h",
color="ft_rate",
color_continuous_scale="Oranges",
title="Free Throw Rate (Top 10)",
)
fig.update_layout(yaxis=dict(autorange="reversed"), showlegend=False)
apply_arena_theme(fig, height=350)
st.plotly_chart(fig, width="stretch")
st.subheader("Team Styles")
col1, col2 = st.columns(2)
with col1:
styles = query(
"""
SELECT shot_selection_style, ball_movement_style, count(*) AS games,
round(avg(CASE WHEN game_result = 'W' THEN 1.0 ELSE 0.0 END) * 100, 1) AS win_pct
FROM main_intermediate.int_team_performance
WHERE season_start_year = ?
GROUP BY shot_selection_style, ball_movement_style
ORDER BY win_pct DESC
""",
[SEASON],
)
fig = px.sunburst(
styles,
path=["shot_selection_style", "ball_movement_style"],
values="games",
color="win_pct",
color_continuous_scale="RdYlGn",
title="Offense: Shot Selection × Ball Movement",
)
apply_arena_theme(fig)
st.plotly_chart(fig, width="stretch")
with col2:
defense = query(
"""
SELECT defensive_activity, ball_movement_style, count(*) AS games,
round(avg(CASE WHEN game_result = 'W' THEN 1.0 ELSE 0.0 END) * 100, 1) AS win_pct
FROM main_intermediate.int_team_performance
WHERE season_start_year = ?
GROUP BY defensive_activity, ball_movement_style
ORDER BY win_pct DESC
""",
[SEASON],
)
fig = px.sunburst(
defense,
path=["defensive_activity", "ball_movement_style"],
values="games",
color="win_pct",
color_continuous_scale="RdYlGn",
title="Defense: Activity × Ball Movement",
)
apply_arena_theme(fig)
st.plotly_chart(fig, width="stretch")
with tab_drilldown:
team_list = ratings["team"].tolist()
selected_team = st.selectbox("Select Team", team_list)
trajectory = query(
"""
SELECT game_date::date::varchar AS game_date,
round(net_rating, 1) AS net_rtg,
round(avg(net_rating) OVER (
ORDER BY game_date ROWS BETWEEN 9 PRECEDING AND CURRENT ROW
), 1) AS rolling_net_rtg,
round(avg(offensive_rating) OVER (
ORDER BY game_date ROWS BETWEEN 9 PRECEDING AND CURRENT ROW
), 1) AS rolling_off,
round(avg(defensive_rating) OVER (
ORDER BY game_date ROWS BETWEEN 9 PRECEDING AND CURRENT ROW
), 1) AS rolling_def
FROM main_intermediate.int_team_performance
WHERE season_start_year = ? AND team = ?
ORDER BY game_date
""",
[SEASON, selected_team],
)
if not trajectory.empty:
fig = go.Figure()
fig.add_trace(
go.Scatter(
x=trajectory["game_date"],
y=trajectory["rolling_net_rtg"],
name="Net Rating",
mode="lines",
line=dict(color=AMBER, width=3),
)
)
fig.add_trace(
go.Scatter(
x=trajectory["game_date"],
y=trajectory["rolling_off"],
name="Offense",
mode="lines",
line=dict(color=MADE_EMERALD, width=1.5, dash="dot"),
)
)
fig.add_trace(
go.Scatter(
x=trajectory["game_date"],
y=trajectory["rolling_def"],
name="Defense",
mode="lines",
line=dict(color=MISS_CRIMSON, width=1.5, dash="dot"),
)
)
fig.add_hline(y=0, line_dash="dash", line_color="gray", opacity=0.4)
fig.update_layout(
title=f"{selected_team} — 10-Game Rolling Ratings",
xaxis_title="Date",
yaxis_title="Rating",
)
apply_arena_theme(fig, height=400)
st.plotly_chart(fig, width="stretch")
st.subheader(f"{selected_team} Game Log")
game_log = query(
"""
SELECT game_date::date::varchar AS date, opponent_team AS opp, game_result AS result,
points AS pts, round(offensive_rating, 1) AS off_rtg,
round(defensive_rating, 1) AS def_rtg, round(net_rating, 1) AS net_rtg,
round(pace, 1) AS pace
FROM main_intermediate.int_team_performance
WHERE season_start_year = ? AND team = ?
ORDER BY game_date DESC
""",
[SEASON, selected_team],
)
st.dataframe(game_log, hide_index=True, width="stretch")