Fix Players page KeyError + use_container_width deprecation

- Players page: fix regression_chart xg_col default (goals_p90 -> xg_p90)
- charts.py: make regression_chart defensive against missing columns
- All pages: replace use_container_width=True with width='stretch'
- Verified: all 5 pages render with zero errors via Playwright browser test
This commit is contained in:
ramseshk
2026-08-11 15:24:39 +08:00
parent 397078d635
commit 30d676517e
5 changed files with 25 additions and 23 deletions
+5 -6
View File
@@ -11,7 +11,7 @@ import streamlit as st
from dashboard.warehouse import load_predictions, load_fixtures, load_players, load_lineups, load_votes from dashboard.warehouse import load_predictions, load_fixtures, load_players, load_lineups, load_votes
from dashboard.viz.components import inject_css, kpi_card, section, insight, role_chip from dashboard.viz.components import inject_css, kpi_card, section, insight, role_chip
from dashboard.viz.charts import fixture_heatmap, kpi_sparkline, error_violins from dashboard.viz.charts import fixture_heatmap, kpi_sparkline, error_violins
from dashboard.viz.template import PITCH_GREEN, GOLD, RED, SKY, VIOLET, WHITE, TEXT_SECONDARY, ROLE_COLORS, ROLE_ICONS from dashboard.viz.template import PITCH_GREEN, GOLD, RED, SKY, VIOLET, WHITE, TEXT_SECONDARY, ROLE_COLORS, ROLE_ICONS, FANTABETO_TEMPLATE
@st.cache_data(ttl=3600) @st.cache_data(ttl=3600)
@@ -87,7 +87,7 @@ def run():
), unsafe_allow_html=True) ), unsafe_allow_html=True)
with k4: with k4:
st.plotly_chart(kpi_sparkline(trend, "FV Trend", PITCH_GREEN), st.plotly_chart(kpi_sparkline(trend, "FV Trend", PITCH_GREEN),
use_container_width=True, config={"displayModeBar": False}) width="stretch", config={"displayModeBar": False})
st.caption("Last 5 GW trend") st.caption("Last 5 GW trend")
st.divider() st.divider()
@@ -98,7 +98,7 @@ def run():
section("📅 Fixture Difficulty") section("📅 Fixture Difficulty")
heatmap_df = _build_fixture_heatmap(players, fixtures) heatmap_df = _build_fixture_heatmap(players, fixtures)
fig = fixture_heatmap(heatmap_df) fig = fixture_heatmap(heatmap_df)
st.plotly_chart(fig, use_container_width=True) st.plotly_chart(fig, width="stretch")
insight("Warmer colors = tougher opponent. Based on opponent avg FV from 25/26.") insight("Warmer colors = tougher opponent. Based on opponent avg FV from 25/26.")
with c2: with c2:
@@ -132,7 +132,7 @@ def run():
] ]
show.columns = ["Player", "Role", "Team", "H/A", "Opponent", "Projected FV", "Status"] show.columns = ["Player", "Role", "Team", "H/A", "Opponent", "Projected FV", "Status"]
st.dataframe( st.dataframe(
show, use_container_width=True, hide_index=True, show, width="stretch", hide_index=True,
column_config={ column_config={
"Player": st.column_config.TextColumn(width="medium"), "Player": st.column_config.TextColumn(width="medium"),
"Projected FV": st.column_config.TextColumn(width="small"), "Projected FV": st.column_config.TextColumn(width="small"),
@@ -151,7 +151,6 @@ def run():
ranks = list(np.cumsum(np.diff([8] + ranks, prepend=8).clip(-2, 2))) ranks = list(np.cumsum(np.diff([8] + ranks, prepend=8).clip(-2, 2)))
import plotly.graph_objects as go import plotly.graph_objects as go
from dashboard.viz.template import FANTABETO_TEMPLATE, PITCH_GREEN, SKY, TEXT_SECONDARY
fig = go.Figure() fig = go.Figure()
fig.add_trace(go.Scatter( fig.add_trace(go.Scatter(
@@ -167,7 +166,7 @@ def run():
xaxis=dict(title=""), xaxis=dict(title=""),
margin=dict(l=10, r=10, t=10, b=10), margin=dict(l=10, r=10, t=10, b=10),
) )
st.plotly_chart(fig, use_container_width=True) st.plotly_chart(fig, width="stretch")
if __name__ == "__main__": if __name__ == "__main__":
+5 -5
View File
@@ -108,7 +108,7 @@ def run():
section("📊 Percentile Radar") section("📊 Percentile Radar")
role_avg = players[players["role"] == role].mean(numeric_only=True) role_avg = players[players["role"] == role].mean(numeric_only=True)
fig = percentile_radar(p, RADAR_METRICS, RADAR_LABELS, role_avg) fig = percentile_radar(p, RADAR_METRICS, RADAR_LABELS, role_avg)
st.plotly_chart(fig, use_container_width=True) st.plotly_chart(fig, width="stretch")
insight("Values normalized vs league average for same role. Outer = better.") insight("Values normalized vs league average for same role. Outer = better.")
with r2: with r2:
@@ -119,7 +119,7 @@ def run():
"games_season": p.get("games_season", 1), "games_season": p.get("games_season", 1),
}]) }])
fig = regression_chart(pdf, selected) fig = regression_chart(pdf, selected)
st.plotly_chart(fig, use_container_width=True) st.plotly_chart(fig, width="stretch")
div = p.get("goals_season", 0) - (p.get("xg_p90", 0) or 0) * p.get("games_season", 1) div = p.get("goals_season", 0) - (p.get("xg_p90", 0) or 0) * p.get("games_season", 1)
div_label = "overperforming" if div > 1 else ("underperforming" if div < -1 else "on par with") div_label = "overperforming" if div > 1 else ("underperforming" if div < -1 else "on par with")
insight(f"{selected} is {div_label} xG by {abs(div):.1f} goals.") insight(f"{selected} is {div_label} xG by {abs(div):.1f} goals.")
@@ -135,13 +135,13 @@ def run():
yellow = p.get("yellow_season", 0) yellow = p.get("yellow_season", 0)
red_c = p.get("red_season", 0) red_c = p.get("red_season", 0)
fig = bonus_donut(goals_26, assists_26, yellow * 0.5 + red_c * 1.0) fig = bonus_donut(goals_26, assists_26, yellow * 0.5 + red_c * 1.0)
st.plotly_chart(fig, use_container_width=True) st.plotly_chart(fig, width="stretch")
insight(f"Season totals: {goals_26:.0f}G + {assists_26:.0f}A — {yellow:.0f}🟨 {red_c:.0f}🟥") insight(f"Season totals: {goals_26:.0f}G + {assists_26:.0f}A — {yellow:.0f}🟨 {red_c:.0f}🟥")
with b2: with b2:
section("⚠️ Card Risk Gauge") section("⚠️ Card Risk Gauge")
fig = card_gauge(p.get("yellow_per_game", 0), p.get("red_per_game", 0)) fig = card_gauge(p.get("yellow_per_game", 0), p.get("red_per_game", 0))
st.plotly_chart(fig, use_container_width=True) st.plotly_chart(fig, width="stretch")
ypg = p.get("yellow_per_game", 0) ypg = p.get("yellow_per_game", 0)
if ypg > 0.2: if ypg > 0.2:
insight(f"⚠️ High yellow risk: {ypg:.2f} per game. Consider rotation in tough fixtures.") insight(f"⚠️ High yellow risk: {ypg:.2f} per game. Consider rotation in tough fixtures.")
@@ -163,7 +163,7 @@ def run():
"matchday", "vote", "goals", "fantavote" "matchday", "vote", "goals", "fantavote"
]].sort_values("matchday", ascending=False) ]].sort_values("matchday", ascending=False)
recent.columns = ["Matchday", "Vote", "Goals", "Fantavote"] recent.columns = ["Matchday", "Vote", "Goals", "Fantavote"]
st.dataframe(recent, use_container_width=True, hide_index=True) st.dataframe(recent, width="stretch", hide_index=True)
if __name__ == "__main__": if __name__ == "__main__":
+3 -3
View File
@@ -151,13 +151,13 @@ def run():
for r in ["P", "D", "C", "A"]: for r in ["P", "D", "C", "A"]:
allocations[r] = sum(s["estimated_price"] for s in selected if s["role"] == r) allocations[r] = sum(s["estimated_price"] for s in selected if s["role"] == r)
fig = budget_waterfall(allocations) fig = budget_waterfall(allocations)
st.plotly_chart(fig, use_container_width=True) st.plotly_chart(fig, width="stretch")
insight("How your budget maps across roles. Aim for ~15% GK, ~35% DEF, ~30% MID, ~20% FWD.") insight("How your budget maps across roles. Aim for ~15% GK, ~35% DEF, ~30% MID, ~20% FWD.")
with c2: with c2:
section("📈 Value Scatter") section("📈 Value Scatter")
fig = value_scatter(players) fig = value_scatter(players)
st.plotly_chart(fig, use_container_width=True) st.plotly_chart(fig, width="stretch")
insight("Top-right: high FV, high price. Bottom-right: value steals. " insight("Top-right: high FV, high price. Bottom-right: value steals. "
"Bubble size = games played. Dashed lines = cost-per-FV-point isolines.") "Bubble size = games played. Dashed lines = cost-per-FV-point isolines.")
@@ -186,7 +186,7 @@ def run():
# ── Grid Auction Heatmap ── # ── Grid Auction Heatmap ──
section("🔢 Grid Auction Simulator") section("🔢 Grid Auction Simulator")
fig = _grid_heatmap(players) fig = _grid_heatmap(players)
st.plotly_chart(fig, use_container_width=True) st.plotly_chart(fig, width="stretch")
insight("Green = good value at that bid multiplier. Red = overpaying. " insight("Green = good value at that bid multiplier. Red = overpaying. "
"Bid at the 'green' multiplier for each player.") "Bid at the 'green' multiplier for each player.")
+4 -4
View File
@@ -147,7 +147,7 @@ def run():
error_df = _build_error_data(preds, votes) error_df = _build_error_data(preds, votes)
if not error_df.empty: if not error_df.empty:
fig = error_violins(error_df) fig = error_violins(error_df)
st.plotly_chart(fig, use_container_width=True) st.plotly_chart(fig, width="stretch")
insight("How prediction errors distribute across roles. Wider = more uncertainty.") insight("How prediction errors distribute across roles. Wider = more uncertainty.")
else: else:
st.info("No actual vote data available to compute errors.") st.info("No actual vote data available to compute errors.")
@@ -156,7 +156,7 @@ def run():
section("🔬 Feature Importance") section("🔬 Feature Importance")
fig = _feature_importance_plot(players) fig = _feature_importance_plot(players)
if fig.data: if fig.data:
st.plotly_chart(fig, use_container_width=True) st.plotly_chart(fig, width="stretch")
insight("Pearson correlation of each feature with season Fantavoto average.") insight("Pearson correlation of each feature with season Fantavoto average.")
else: else:
st.info("Insufficient numeric features for correlation analysis.") st.info("Insufficient numeric features for correlation analysis.")
@@ -169,7 +169,7 @@ def run():
section("📐 Calibration Curve") section("📐 Calibration Curve")
fig = _calibration_curve(preds) fig = _calibration_curve(preds)
if fig.data: if fig.data:
st.plotly_chart(fig, use_container_width=True) st.plotly_chart(fig, width="stretch")
insight("Ideal: points on diagonal → predicted uncertainty matches actual variance.") insight("Ideal: points on diagonal → predicted uncertainty matches actual variance.")
else: else:
st.info("Bootstrap std not available for calibration.") st.info("Bootstrap std not available for calibration.")
@@ -178,7 +178,7 @@ def run():
section("📈 Backtest: League Avg per GW") section("📈 Backtest: League Avg per GW")
fig = _backtest_chart(votes) fig = _backtest_chart(votes)
if fig.data: if fig.data:
st.plotly_chart(fig, use_container_width=True) st.plotly_chart(fig, width="stretch")
insight("Average Fantavoto across the 2025/26 season. Dashed line = 6.0 baseline.") insight("Average Fantavoto across the 2025/26 season. Dashed line = 6.0 baseline.")
else: else:
st.info("No vote data available.") st.info("No vote data available.")
+8 -5
View File
@@ -112,15 +112,18 @@ def percentile_radar(player_row: pd.Series, metrics: list, labels: list,
# ───────────────────────────────────────────────────────────────── # ─────────────────────────────────────────────────────────────────
def regression_chart(df: pd.DataFrame, player: str, def regression_chart(df: pd.DataFrame, player: str,
xg_col="goals_p90", goal_col="goals_season", xg_col="xg_p90", goal_col="goals_season",
games_col="games_season") -> go.Figure: games_col="games_season") -> go.Figure:
"""Rolling actual goals vs expected with divergence shading. """Rolling actual goals vs expected with divergence shading."""
Uses season-level totals as static chart. actual = df[goal_col].iloc[0] if goal_col in df.columns else 0
""" xg_val = df[xg_col].iloc[0] if xg_col in df.columns else 0
games = df[games_col].iloc[0] if games_col in df.columns else 1
expected = xg_val * 1.2 * games
fig = go.Figure() fig = go.Figure()
fig.add_trace(go.Bar( fig.add_trace(go.Bar(
x=["Actual Goals", "Expected (xG * 1.2)"], x=["Actual Goals", "Expected (xG * 1.2)"],
y=[df[goal_col].iloc[0], df[xg_col].iloc[0] * 1.2 * df[games_col].iloc[0]], y=[actual, expected],
marker_color=[PITCH_GREEN, SKY], texttemplate="%{y:.1f}", marker_color=[PITCH_GREEN, SKY], texttemplate="%{y:.1f}",
textposition="outside", textfont=dict(color=TEXT, size=13), textposition="outside", textfont=dict(color=TEXT, size=13),
showlegend=False, showlegend=False,