CI Health Check & Quality Gate / Code, dbt & Live App Health Check (pull_request) Successful in 1m11s
284 lines
9.7 KiB
Python
284 lines
9.7 KiB
Python
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")
|