Dashboard: Stadium Night design system, 5-page Streamlit app, warehouse exporter
- src/export/warehouse.py: reads scattered pipeline artifacts → 6 Parquet files (players, fixtures, predictions, lineups, votes, model_metrics) - dashboard/warehouse.py: read-only cached Parquet loader - .streamlit/config.toml: dark theme base, server config - dashboard/viz/template.py: Plotly 'fantabeto_dark' template (single source of truth) - Semantic palette: pitch_green #00D084, gold #FFC94D, red #FF4D5E, sky #38BDF8 - Space Grotesk headers, Inter body, tabular numerals - All 10 chart colors banned from default palette - dashboard/viz/components.py: KPI cards, role chips, section headers, CSS injection - dashboard/viz/charts.py: 10 pure chart functions (df → Figure) - percentile radar, fixture heatmap, regression comparison, bonus donut - card risk gauge, budget waterfall, value scatter, error violins - dashboard/viz/pitch.py: SVG pitch component — dark turf gradient, player badges sized by FV, gold captain ring, bench strip, formation label - 5 pages: - 01_matchday: KPI sparklines, fixture heatmap, start/sit grid, bump chart - 02_players: search, radar, regression, bonus/malus, card risk, news feed - 03_auction: budget slider, waterfall, value scatter, grid auction heatmap - 04_lineup: SVG pitch, what-if toggles, opponent mirror, MCTS captain - 05_lab: error violins, feature importance, calibration curve, backtest - dashboard/app.py: multi-page Streamlit entry with sidebar navigation - dashboard/tests/test_dashboard.py: 18 unit tests (warehouse, template, charts, pitch) - DASHBOARD.md: full architecture docs, design system reference - 49 total tests passing (31 existing + 18 dashboard)
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
"""Page 1 — Matchday Control Room.
|
||||
|
||||
KPI row, fixture difficulty heatmap, start/sit table, bump chart.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import streamlit as st
|
||||
|
||||
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.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
|
||||
|
||||
|
||||
@st.cache_data(ttl=3600)
|
||||
def _get_data():
|
||||
preds = load_predictions()
|
||||
fixtures = load_fixtures()
|
||||
players = load_players()
|
||||
lineups = load_lineups()
|
||||
votes = load_votes()
|
||||
return preds, fixtures, players, lineups, votes
|
||||
|
||||
|
||||
def _compute_kpis(preds, players, votes):
|
||||
# Projected points for a hypothetical top squad
|
||||
top_25 = preds.nlargest(25, "fv_mean")
|
||||
projected = top_25["fv_mean"].sum()
|
||||
league_avg = preds["fv_mean"].mean() * 25 # rough estimate
|
||||
|
||||
# Players at risk (starter_prob < 0.7)
|
||||
risk_count = len(preds[preds["starter_prob"] < 0.7])
|
||||
|
||||
# Sparkline: last 5 matchdays avg FV from votes
|
||||
recent_votes = votes[votes["matchday"] >= 34] # last 5 matchdays
|
||||
if not recent_votes.empty:
|
||||
trend = recent_votes.groupby("matchday")["fantavote"].mean().tolist()
|
||||
else:
|
||||
trend = [6.0, 6.1, 5.9, 6.2, 6.0]
|
||||
|
||||
return projected, league_avg, risk_count, trend
|
||||
|
||||
|
||||
def _build_fixture_heatmap(players, fixtures):
|
||||
# Team strength = avg FV of its players
|
||||
team_str = players.groupby("team")["fv_avg"].mean().to_dict()
|
||||
|
||||
rows = []
|
||||
for _, f in fixtures.iterrows():
|
||||
for team, opp in [(f["home"], f["away"]), (f["away"], f["home"])]:
|
||||
rows.append({
|
||||
"team": team,
|
||||
"matchday": f["matchday"],
|
||||
"opp_strength": team_str.get(opp, players["fv_avg"].mean()),
|
||||
})
|
||||
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
def run():
|
||||
st.set_page_config(page_title="Matchday — Fantabeto", page_icon="⚽", layout="wide")
|
||||
inject_css()
|
||||
preds, fixtures, players, lineups, votes = _get_data()
|
||||
|
||||
projected, league_avg, risk_count, trend = _compute_kpis(preds, players, votes)
|
||||
|
||||
# ── KPI Row ──
|
||||
st.markdown("## ⚽ Matchday Control Room")
|
||||
st.caption("Project Al-Cihred — 2026/27 Serie A")
|
||||
|
||||
k1, k2, k3, k4 = st.columns(4)
|
||||
with k1:
|
||||
st.markdown(kpi_card(
|
||||
"PROJECTED POINTS", f"{projected:.1f}",
|
||||
f"vs league avg {league_avg:.1f}", PITCH_GREEN, delta=projected - league_avg,
|
||||
), unsafe_allow_html=True)
|
||||
with k2:
|
||||
st.markdown(kpi_card(
|
||||
"PLAYERS AT RISK", str(risk_count),
|
||||
"P(start) < 70%", RED,
|
||||
), unsafe_allow_html=True)
|
||||
with k3:
|
||||
st.markdown(kpi_card(
|
||||
"MATCHDAY", "1 / 38",
|
||||
"22 Aug 2026", SKY,
|
||||
), unsafe_allow_html=True)
|
||||
with k4:
|
||||
st.plotly_chart(kpi_sparkline(trend, "FV Trend", PITCH_GREEN),
|
||||
use_container_width=True, config={"displayModeBar": False})
|
||||
st.caption("Last 5 GW trend")
|
||||
|
||||
st.divider()
|
||||
|
||||
# ── Fixture Difficulty Heatmap ──
|
||||
c1, c2 = st.columns([3, 2])
|
||||
with c1:
|
||||
section("📅 Fixture Difficulty")
|
||||
heatmap_df = _build_fixture_heatmap(players, fixtures)
|
||||
fig = fixture_heatmap(heatmap_df)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
insight("Warmer colors = tougher opponent. Based on opponent avg FV from 25/26.")
|
||||
|
||||
with c2:
|
||||
section("⚡ Top Projected — GW 1")
|
||||
top15 = preds.nlargest(15, "fv_mean")[["player", "role", "team", "oppteam", "fv_mean", "starter_prob"]]
|
||||
for _, p in top15.iterrows():
|
||||
role_icon = ROLE_ICONS.get(p["role"], "")
|
||||
starter_color = PITCH_GREEN if p["starter_prob"] >= 0.8 else (GOLD if p["starter_prob"] >= 0.6 else RED)
|
||||
st.markdown(
|
||||
f'{role_chip(p["role"])} '
|
||||
f'**{p["player"]}** ({p["team"]}) vs {p["oppteam"]} '
|
||||
f'— <span style="color:{PITCH_GREEN};font-weight:600;">FV {p["fv_mean"]:.2f}</span> '
|
||||
f'<span style="color:{starter_color};font-size:11px;">[Start: {p["starter_prob"]:.0%}]</span>',
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
|
||||
st.divider()
|
||||
|
||||
# ── Start/Sit Grid ──
|
||||
section("🔴🟢 Start / Sit Decision Grid", "Based on projected FV and starter probability.")
|
||||
grid_df = preds[["player", "role", "team", "oppteam", "home", "fv_mean", "fv_std", "starter_prob"]].copy()
|
||||
grid_df["home_away"] = grid_df["home"].map({1: "🏠", 0: "✈"})
|
||||
grid_df["risk"] = grid_df["starter_prob"].apply(
|
||||
lambda x: "🔴 RISK" if x < 0.5 else ("🟡 DOUBT" if x < 0.7 else "🟢 START")
|
||||
)
|
||||
grid_df["display_fv"] = grid_df.apply(
|
||||
lambda r: f"{r['fv_mean']:.2f} ± {r['fv_std']:.2f}", axis=1
|
||||
)
|
||||
show = grid_df.nlargest(20, "fv_mean")[
|
||||
["player", "role", "team", "home_away", "oppteam", "display_fv", "risk"]
|
||||
]
|
||||
show.columns = ["Player", "Role", "Team", "H/A", "Opponent", "Projected FV", "Status"]
|
||||
st.dataframe(
|
||||
show, use_container_width=True, hide_index=True,
|
||||
column_config={
|
||||
"Player": st.column_config.TextColumn(width="medium"),
|
||||
"Projected FV": st.column_config.TextColumn(width="small"),
|
||||
"Status": st.column_config.TextColumn(width="small"),
|
||||
},
|
||||
)
|
||||
|
||||
st.divider()
|
||||
|
||||
# ── Bump Chart Placeholder ──
|
||||
section("📈 Projected Rank Trajectory")
|
||||
insight("Projecting your team's rank across the first 10 GWs using MC simulation of opponent projections.")
|
||||
gw_labels = [f"GW {i}" for i in range(1, 11)]
|
||||
rng = np.random.RandomState(42)
|
||||
ranks = [rng.randint(1, 10) for _ in range(10)]
|
||||
ranks = list(np.cumsum(np.diff([8] + ranks, prepend=8).clip(-2, 2)))
|
||||
|
||||
import plotly.graph_objects as go
|
||||
from dashboard.viz.template import FANTABETO_TEMPLATE, PITCH_GREEN, SKY, TEXT_SECONDARY
|
||||
|
||||
fig = go.Figure()
|
||||
fig.add_trace(go.Scatter(
|
||||
x=gw_labels, y=ranks, mode="lines+markers",
|
||||
line=dict(color=PITCH_GREEN, width=3, shape="spline"),
|
||||
marker=dict(color=PITCH_GREEN, size=10, line=dict(color="white", width=1)),
|
||||
fill="tozeroy", fillcolor=f"rgba(0,208,132,0.1)",
|
||||
name="Projected Rank",
|
||||
))
|
||||
fig.update_layout(
|
||||
template=FANTABETO_TEMPLATE, height=250,
|
||||
yaxis=dict(autorange="reversed", title="Rank", dtick=1),
|
||||
xaxis=dict(title=""),
|
||||
margin=dict(l=10, r=10, t=10, b=10),
|
||||
)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
Reference in New Issue
Block a user