Files
fantabeto/dashboard/pages/01_matchday.py
T
ramseshk 00dc7f2bcb feat: add dev preview dashboard showcasing all 12 new ML models
- New page 06_dev_preview.py: interactive ML model showcase
- All 12 models initialized from synthetic data with live charts
- Live Auction Simulator: bandit + opponent model + budget optimizer
- Tabbed UI: 6 tabs, one per phase
- Resilient warehouse: returns typed empty DataFrames when no data
- Dev Preview set as default landing page for demo mode
- Live at http://localhost:8507
2026-08-12 11:34:00 +08:00

225 lines
9.4 KiB
Python

import sys; from pathlib import Path; _p = Path(__file__).resolve().parent.parent.parent; str(_p) not in sys.path and sys.path.insert(0, str(_p))
"""Page 1 — Matchday Control Room.
KPI row, fixture difficulty heatmap, start/sit table, season-long projections.
"""
import numpy as np
import pandas as pd
import streamlit as st
import plotly.graph_objects as go
from plotly.subplots import make_subplots
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, FANTABETO_TEMPLATE
)
SEASON_PROJECTIONS = Path(__file__).resolve().parent.parent.parent / "data" / "season_projections_26_27.xlsx"
@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():
inject_css()
preds, fixtures, players, lineups, votes = _get_data()
if preds.empty or len(preds) <= 1:
st.warning("No warehouse data. Run pipeline or use Dev Preview tab.")
return
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),
width="stretch", 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, width="stretch")
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, width="stretch", 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
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, width="stretch")
# ── Season Projections ─────────────────────────────────────────────
st.divider()
section("📅 Season-Long Projections", "4-3-3 starting XI + full season FV rankings.")
if SEASON_PROJECTIONS.exists():
season = pd.read_excel(SEASON_PROJECTIONS)
# 4-3-3 Best XI
s_gk = season[season['role'] == 'P'].nlargest(1, 'total_season_fv')
s_def = season[season['role'] == 'D'].nlargest(4, 'total_season_fv')
s_mid = season[season['role'] == 'C'].nlargest(3, 'total_season_fv')
s_fwd = season[season['role'] == 'A'].nlargest(3, 'total_season_fv')
stotal = s_gk['total_season_fv'].sum() + s_def['total_season_fv'].sum() + \
s_mid['total_season_fv'].sum() + s_fwd['total_season_fv'].sum()
captain_iron = s_fwd.iloc[0]
c_season, c_table = st.columns([1, 2])
with c_season:
st.markdown("**4-3-3 Projected XI (Season)**", unsafe_allow_html=False)
st.markdown(f"🧤 {s_gk.iloc[0]['player']} ({s_gk.iloc[0]['team']}) — {s_gk.iloc[0]['total_season_fv']:.0f} FV")
for _, d in s_def.iterrows():
st.markdown(f"🛡 {d['player']} ({d['team']}) — {d['total_season_fv']:.0f} FV")
for _, m in s_mid.iterrows():
st.markdown(f"⚙ {m['player']} ({m['team']}) — {m['total_season_fv']:.0f} FV")
for _, f in s_fwd.iterrows():
st.markdown(f"⚡ {f['player']} ({f['team']}) — {f['total_season_fv']:.0f} FV")
st.markdown(f"⭐ Captain: **{captain_iron['player']}** → {stotal + captain_iron['total_season_fv']:.0f} FV")
insight(f"Projected season total: {stotal:.0f} FV ({stotal + captain_iron['total_season_fv']:.0f} with captain)")
with c_table:
show = season.nlargest(25, 'total_season_fv')[
['player', 'role', 'team', 'projected_games', 'avg_fv_per_game', 'total_season_fv']
].copy()
show.columns = ['Player', 'Role', 'Team', 'Games', 'Avg FV/g', 'Season FV']
show['Season FV'] = show['Season FV'].astype(float).round(0).astype(int)
show['Avg FV/g'] = show['Avg FV/g'].astype(float).round(2)
st.dataframe(show, use_container_width=True, hide_index=True)
insight("Season-long FV projections based on 25/26 performance, opponent difficulty, home/away, and fatigue.")
else:
st.info("Season projections not yet generated. Run the season projector to populate.")
if __name__ == "__main__":
run()