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,195 @@
|
||||
"""Page 3 — Auction War Room.
|
||||
|
||||
Budget waterfall, value scatter, grid-auction heatmap, budget slider simulator.
|
||||
"""
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import streamlit as st
|
||||
import plotly.graph_objects as go
|
||||
|
||||
from dashboard.warehouse import load_players, load_predictions
|
||||
from dashboard.viz.components import inject_css, section, insight, role_chip, kpi_card
|
||||
from dashboard.viz.charts import budget_waterfall, value_scatter
|
||||
from dashboard.viz.template import (
|
||||
PITCH_GREEN, GOLD, RED, SKY, BG, CARD_BG, BORDER, TEXT_SECONDARY,
|
||||
WHITE, FANTABETO_TEMPLATE, HEATMAP_COLORS, ROLE_COLORS,
|
||||
)
|
||||
|
||||
|
||||
@st.cache_data(ttl=3600)
|
||||
def _get_data():
|
||||
players = load_players()
|
||||
preds = load_predictions()
|
||||
return players, preds
|
||||
|
||||
|
||||
def _compute_auction(players, budget, gk, df, mf, fw):
|
||||
"""Greedy knapsack auction solver."""
|
||||
quotas = {"P": gk, "D": df, "C": mf, "A": fw}
|
||||
filled = {"P": 0, "D": 0, "C": 0, "A": 0}
|
||||
remaining = budget
|
||||
|
||||
df = players.copy()
|
||||
df["value_ratio"] = df["fv_avg"] / df["qi"].clip(lower=1)
|
||||
df["estimated_price"] = df["qi"] * np.clip(np.random.RandomState(42).normal(2.5, 0.8, len(df)), 0.8, 6)
|
||||
|
||||
scored = []
|
||||
for _, p in df.iterrows():
|
||||
role = p["role"]
|
||||
if role not in quotas:
|
||||
continue
|
||||
scored.append((p["value_ratio"] * p["fv_avg"], p))
|
||||
scored.sort(key=lambda x: -x[0])
|
||||
|
||||
selected = []
|
||||
for _, p in scored:
|
||||
role = p["role"]
|
||||
if filled[role] >= quotas[role]:
|
||||
continue
|
||||
price = p["estimated_price"]
|
||||
if price > remaining:
|
||||
continue
|
||||
selected.append({
|
||||
"player": p["player"], "role": role, "team": p["team"],
|
||||
"fv_avg": p["fv_avg"], "qi": p["qi"],
|
||||
"estimated_price": price,
|
||||
"games_season": p.get("games_season", 30),
|
||||
})
|
||||
remaining -= price
|
||||
filled[role] += 1
|
||||
|
||||
total = budget - remaining
|
||||
total_fv = sum(s["fv_avg"] for s in selected)
|
||||
return selected, total, total_fv, remaining
|
||||
|
||||
|
||||
def _grid_heatmap(players):
|
||||
"""Simplified grid auction heatmap: top players × bid levels."""
|
||||
top = players.nlargest(10, "fv_avg")[
|
||||
["player", "role", "fv_avg", "qi"]
|
||||
].copy()
|
||||
bid_multipliers = [1.0, 1.5, 2.0, 2.5, 3.0, 4.0, 5.0]
|
||||
|
||||
matrix = []
|
||||
labels = []
|
||||
for _, p in top.iterrows():
|
||||
row = []
|
||||
for mult in bid_multipliers:
|
||||
bid = p["qi"] * mult
|
||||
surplus = p["fv_avg"] * 3 - bid # rough value
|
||||
row.append(max(0, surplus))
|
||||
matrix.append(row)
|
||||
labels.append(p["player"])
|
||||
|
||||
fig = go.Figure(data=go.Heatmap(
|
||||
z=matrix,
|
||||
x=[f"{m}x QI" for m in bid_multipliers],
|
||||
y=labels,
|
||||
colorscale=HEATMAP_COLORS,
|
||||
hovertemplate="%{y}<br>Bid: %{x}<br>Surplus: %{z:.0f}<extra></extra>",
|
||||
))
|
||||
fig.update_layout(
|
||||
template=FANTABETO_TEMPLATE, height=350,
|
||||
xaxis=dict(side="top"),
|
||||
yaxis=dict(autorange="reversed"),
|
||||
)
|
||||
return fig
|
||||
|
||||
|
||||
def run():
|
||||
st.set_page_config(page_title="Auction — Fantabeto", page_icon="💰", layout="wide")
|
||||
inject_css()
|
||||
players, preds = _get_data()
|
||||
|
||||
st.markdown("## 💰 Auction War Room")
|
||||
st.caption("Project Al-Cihred — Draft Strategy for 2026/27 Classic Auction")
|
||||
|
||||
# ── Budget controls ──
|
||||
c_budget, c_gk, c_def, c_mid, c_fwd = st.columns(5)
|
||||
with c_budget:
|
||||
budget = st.slider("Budget (cr)", 300, 700, 500, 10)
|
||||
with c_gk:
|
||||
n_gk = st.number_input("GK", 1, 5, 3)
|
||||
with c_def:
|
||||
n_def = st.number_input("DEF", 3, 12, 8)
|
||||
with c_mid:
|
||||
n_mid = st.number_input("MID", 3, 12, 8)
|
||||
with c_fwd:
|
||||
n_fwd = st.number_input("FWD", 1, 8, 6)
|
||||
|
||||
selected, total_cost, total_fv, remaining = _compute_auction(
|
||||
players, budget, n_gk, n_def, n_mid, n_fwd
|
||||
)
|
||||
|
||||
# ── KPI Row ──
|
||||
k1, k2, k3, k4 = st.columns(4)
|
||||
with k1:
|
||||
st.markdown(kpi_card("PLAYERS DRAFTED", str(len(selected)),
|
||||
f"{n_gk+n_def+n_mid+n_fwd} target", SKY),
|
||||
unsafe_allow_html=True)
|
||||
with k2:
|
||||
st.markdown(kpi_card("TOTAL SPENT", f"{total_cost:.0f} cr",
|
||||
f"{remaining:.0f} cr remaining", PITCH_GREEN),
|
||||
unsafe_allow_html=True)
|
||||
with k3:
|
||||
st.markdown(kpi_card("PROJECTED FV", f"{total_fv:.1f}",
|
||||
f"{total_fv / max(total_cost, 1):.2f} cr/FV", GOLD),
|
||||
unsafe_allow_html=True)
|
||||
with k4:
|
||||
st.markdown(kpi_card("AVG PRICE", f"{total_cost / max(len(selected), 1):.0f} cr",
|
||||
"per player", SKY),
|
||||
unsafe_allow_html=True)
|
||||
|
||||
st.divider()
|
||||
|
||||
# ── Budget Waterfall + Value Scatter ──
|
||||
c1, c2 = st.columns([2, 3])
|
||||
with c1:
|
||||
section("💧 Budget Allocation")
|
||||
allocations = {}
|
||||
for r in ["P", "D", "C", "A"]:
|
||||
allocations[r] = sum(s["estimated_price"] for s in selected if s["role"] == r)
|
||||
fig = budget_waterfall(allocations)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
insight("How your budget maps across roles. Aim for ~15% GK, ~35% DEF, ~30% MID, ~20% FWD.")
|
||||
|
||||
with c2:
|
||||
section("📈 Value Scatter")
|
||||
fig = value_scatter(players)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
insight("Top-right: high FV, high price. Bottom-right: value steals. "
|
||||
"Bubble size = games played. Dashed lines = cost-per-FV-point isolines.")
|
||||
|
||||
st.divider()
|
||||
|
||||
# ── Target Squad ──
|
||||
section("🎯 Recommended Squad")
|
||||
if selected:
|
||||
squad_df = pd.DataFrame(selected)
|
||||
for role in ["P", "D", "C", "A"]:
|
||||
rdf = squad_df[squad_df["role"] == role]
|
||||
if rdf.empty:
|
||||
continue
|
||||
role_name = {"P": "Goalkeepers", "D": "Defenders", "C": "Midfielders", "A": "Forwards"}[role]
|
||||
st.markdown(f"**{role_name}** {role_chip(role)}")
|
||||
for _, p in rdf.iterrows():
|
||||
st.markdown(
|
||||
f"- **{p['player']}** ({p['team']}) — "
|
||||
f"FV: {p['fv_avg']:.2f} | "
|
||||
f"Max bid: {p['estimated_price']:.0f} cr | "
|
||||
f"Games: {p['games_season']:.0f}",
|
||||
)
|
||||
|
||||
st.divider()
|
||||
|
||||
# ── Grid Auction Heatmap ──
|
||||
section("🔢 Grid Auction Simulator")
|
||||
fig = _grid_heatmap(players)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
insight("Green = good value at that bid multiplier. Red = overpaying. "
|
||||
"Bid at the 'green' multiplier for each player.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run()
|
||||
Reference in New Issue
Block a user