8e759b0d0f
- LightGBM trained on 11,300 per-matchday votes (R²=0.147, RMSE=1.19) - MILP with exact 499-500cr budget: 3GK+8DEF+8MID+6FWD - Practical constraints: 2 starting GKs, 21/25 reliable (>15g) - Realistic prices: Lautaro 229cr, Malen 220cr, Douvikas 56cr - Squad: Svilar(21)+Carnesecchi(18)+Christensen(1)=40GK · 8DEF 128cr · 8MID 152cr · 6FWD 180cr - Every player has 2-3 alternatives - Updated dashboard auction page to load the plan
235 lines
9.2 KiB
Python
235 lines
9.2 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 3 — Auction War Room.
|
||
|
||
Loads the ML-optimized Project Al-Cihred auction plan with alternatives.
|
||
"""
|
||
|
||
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_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, VIOLET, BG, CARD_BG, BORDER, TEXT_SECONDARY,
|
||
WHITE, FANTABETO_TEMPLATE, HEATMAP_COLORS, ROLE_COLORS, ROLE_ICONS,
|
||
)
|
||
|
||
AUCTION_PLAN = Path(__file__).resolve().parent.parent.parent / "data" / "auction_plan_al_cihred.xlsx"
|
||
PROJECTIONS = Path(__file__).resolve().parent.parent.parent / "data" / "player_projections_26_27.xlsx"
|
||
|
||
|
||
@st.cache_data(ttl=3600)
|
||
def _get_data():
|
||
players = load_players()
|
||
preds = load_predictions()
|
||
# Load the precomputed auction plan
|
||
plan = None
|
||
if AUCTION_PLAN.exists():
|
||
plan = pd.read_excel(AUCTION_PLAN)
|
||
proj = None
|
||
if PROJECTIONS.exists():
|
||
proj = pd.read_excel(PROJECTIONS)
|
||
return players, preds, plan, proj
|
||
|
||
|
||
def _price_dist_chart(proj):
|
||
if proj is None:
|
||
return go.Figure()
|
||
df = proj.copy()
|
||
df["price"] = df["price"].fillna(0)
|
||
fig = go.Figure()
|
||
for role, color in ROLE_COLORS.items():
|
||
rdf = df[df["role"] == role]["price"]
|
||
if rdf.empty:
|
||
continue
|
||
fig.add_trace(go.Histogram(
|
||
x=rdf, name=role, marker_color=color, opacity=0.7, nbinsx=50,
|
||
hovertemplate=f"{role} %{{x:.0f}} cr<extra></extra>",
|
||
))
|
||
fig.update_layout(
|
||
template=FANTABETO_TEMPLATE, height=300, barmode="overlay",
|
||
xaxis_title="Estimated Auction Price (cr)", yaxis_title="Players", bargap=0.05,
|
||
)
|
||
return fig
|
||
|
||
|
||
def _role_radar(squad):
|
||
"""Radar showing squad balance: avg FV, games, stability per role."""
|
||
roles = ["P", "D", "C", "A"]
|
||
avg_fv, avg_g, avg_s = [], [], []
|
||
for r in roles:
|
||
rs = [s for s in squad if s.get("role") == r]
|
||
avg_fv.append(np.mean([s.get("fv_proj", 0) for s in rs]) if rs else 0)
|
||
avg_g.append(np.mean([s.get("games", 0) for s in rs]) if rs else 0)
|
||
avg_s.append(np.mean([s.get("stability", 0) for s in rs]) if rs else 0)
|
||
|
||
fig = go.Figure()
|
||
fig.add_trace(go.Scatterpolar(
|
||
r=avg_fv + [avg_fv[0]], theta=roles + [roles[0]],
|
||
fill="toself", fillcolor=f"rgba(56,189,248,0.2)",
|
||
line=dict(color=SKY, width=2), name="Avg FV",
|
||
))
|
||
fig.add_trace(go.Scatterpolar(
|
||
r=avg_g + [avg_g[0]], theta=roles + [roles[0]],
|
||
fill="toself", fillcolor=f"rgba(0,208,132,0.15)",
|
||
line=dict(color=PITCH_GREEN, width=2), name="Avg Games",
|
||
))
|
||
fig.update_layout(
|
||
template=FANTABETO_TEMPLATE, height=280,
|
||
polar=dict(
|
||
radialaxis=dict(showticklabels=False, gridcolor=GRIDLINE),
|
||
angularaxis=dict(gridcolor=GRIDLINE),
|
||
bgcolor=BG,
|
||
),
|
||
showlegend=True, legend=dict(orientation="h", yanchor="bottom", y=-0.2),
|
||
margin=dict(l=30, r=30, t=30, b=60),
|
||
)
|
||
return fig
|
||
|
||
|
||
def run():
|
||
inject_css()
|
||
players, preds, plan, proj = _get_data()
|
||
|
||
st.markdown("## 💰 Auction War Room")
|
||
st.caption("Project Al-Cihred — ML-Optimized Auction Strategy for 2026/27")
|
||
|
||
# Load plan data into squad list
|
||
squad = []
|
||
total_cost = 0
|
||
total_fv = 0
|
||
if plan is not None and len(plan) > 0:
|
||
for _, p in plan.iterrows():
|
||
s = {
|
||
"player": p.get("player", ""), "role": p.get("role", ""),
|
||
"team": p.get("team", ""), "price": p.get("bid_cap", 0),
|
||
"qi": p.get("qi", 0), "fvm": p.get("fvm", 0),
|
||
"fv_proj": p.get("fv_proj", 0), "mv_proj": p.get("mv_proj", 0),
|
||
"goals": p.get("goals", 0), "assists": p.get("assists", 0),
|
||
"games": p.get("games", 0), "starter_pct": p.get("starter%", 0),
|
||
"stability": p.get("stability", 0),
|
||
}
|
||
s["alts"] = []
|
||
for j in range(1, 3):
|
||
if f"alt{j}" in plan.columns and pd.notna(p.get(f"alt{j}")):
|
||
s["alts"].append({
|
||
"player": p.get(f"alt{j}"), "price": p.get(f"alt{j}_price"),
|
||
"fv": p.get(f"alt{j}_fv"),
|
||
})
|
||
squad.append(s)
|
||
total_cost += s["price"]
|
||
total_fv += s["fv_proj"]
|
||
else:
|
||
st.warning("⚠️ No auction plan found. Run `python -c '...'` to generate one.")
|
||
return
|
||
|
||
# ── KPI Row ──
|
||
k1, k2, k3, k4, k5 = st.columns(5)
|
||
with k1:
|
||
st.markdown(kpi_card("BUDGET USED", f"{total_cost} cr",
|
||
"500 cr ceiling", PITCH_GREEN if total_cost >= 499 else GOLD),
|
||
unsafe_allow_html=True)
|
||
with k2:
|
||
st.markdown(kpi_card("PROJ FV", f"{total_fv:.1f}",
|
||
f"{total_fv/total_cost:.3f} cr/FV", GOLD), unsafe_allow_html=True)
|
||
with k3:
|
||
stable = sum(1 for s in squad if s["games"] >= 15)
|
||
st.markdown(kpi_card("RELIABLE", f"{stable}/25",
|
||
"players >15 games", PITCH_GREEN), unsafe_allow_html=True)
|
||
with k4:
|
||
avg_g = np.mean([s["games"] for s in squad])
|
||
st.markdown(kpi_card("AVG GAMES", f"{avg_g:.0f}",
|
||
"25/26 experience", SKY), unsafe_allow_html=True)
|
||
with k5:
|
||
stars = sum(1 for s in squad if s["fvm"] > 100)
|
||
st.markdown(kpi_card("PREMIUM", str(stars),
|
||
"FVM > 100", GOLD), unsafe_allow_html=True)
|
||
|
||
st.divider()
|
||
|
||
# ── Squad Table + Radar ──
|
||
c1, c2 = st.columns([3, 2])
|
||
|
||
with c1:
|
||
section("🎯 Project Al-Cihred — Final Squad")
|
||
role_groups = {"P": "🧤 Goalkeepers", "D": "🛡 Defenders",
|
||
"C": "⚙ Midfielders", "A": "⚡ Forwards"}
|
||
|
||
for role, label in role_groups.items():
|
||
rs = [s for s in squad if s["role"] == role]
|
||
if not rs:
|
||
continue
|
||
cost = sum(s["price"] for s in rs)
|
||
st.markdown(f"**{label}** ({len(rs)}) — *{cost} cr*", unsafe_allow_html=False)
|
||
|
||
for s in rs:
|
||
chip = role_chip(s["role"])
|
||
pc = GOLD if s["price"] >= 30 else (SKY if s["price"] >= 15 else PITCH_GREEN)
|
||
risk_warn = " ⚠️" if s["starter_pct"] < 0.5 else ""
|
||
st.markdown(
|
||
f'{chip} **{s["player"]}** ({s["team"]}) — '
|
||
f'FV: {s["fv_proj"]:.2f} · '
|
||
f'<span style="color:{pc};font-weight:600;">{s["price"]:.0f} cr</span> · '
|
||
f'{int(s["goals"])}G {int(s["assists"])}A · {int(s["games"])}gms '
|
||
f'· FVM {int(s["fvm"])}{risk_warn}',
|
||
unsafe_allow_html=True,
|
||
)
|
||
|
||
if s["alts"]:
|
||
alt_text = " · ".join(
|
||
f'<span style="color:{TEXT_SECONDARY};font-size:11px;">⟳ [{a["player"]} · {int(a["price"])}cr]</span>'
|
||
for a in s["alts"]
|
||
)
|
||
st.markdown(f'<span style="margin-left:22px;">{alt_text}</span>',
|
||
unsafe_allow_html=True)
|
||
|
||
with c2:
|
||
section("📊 Squad Balance Radar")
|
||
fig = _role_radar(squad)
|
||
st.plotly_chart(fig, width="stretch")
|
||
insight("Balanced squad across roles: defenders with high reliability, forwards with high FV ceiling.")
|
||
|
||
# Budget allocations
|
||
allocations = {}
|
||
for r in ["P", "D", "C", "A"]:
|
||
allocations[r] = sum(s["price"] for s in squad if s["role"] == r)
|
||
fig = budget_waterfall(allocations)
|
||
st.plotly_chart(fig, width="stretch")
|
||
|
||
st.divider()
|
||
|
||
# ── Market Analysis ──
|
||
section("📈 Market Overview")
|
||
c3, c4 = st.columns([1, 1])
|
||
with c3:
|
||
fig = _price_dist_chart(proj)
|
||
st.plotly_chart(fig, width="stretch")
|
||
insight("Price distribution by role. Forwards and elite midfielders at premium.")
|
||
with c4:
|
||
section("🔍 Value Scatter")
|
||
fig = value_scatter(players)
|
||
st.plotly_chart(fig, width="stretch")
|
||
|
||
# ── Methodology ──
|
||
st.divider()
|
||
with st.expander("⚙️ Methodology — How this squad was computed"):
|
||
st.markdown("""
|
||
**Model**: LightGBM trained on 11,300 per-matchday votes from 2025/26.
|
||
**Features**: role, vote_avg, fv_avg, goals/game, assists/game, yellow/game, red/game, games_played.
|
||
**Solver**: MILP (Mixed Integer Linear Programming) via PuLP with constraints:
|
||
- Budget: exactly 499–500 crediti
|
||
- At least 2 goalkeepers with >15 games played
|
||
- At least 17/25 players with >15 games (reliability)
|
||
- GK budget: 35–80 cr (prevent 1cr scrubs)
|
||
- Price model: QI × (FVM/65) with forward premium and small-sample penalty
|
||
**Alternatives**: Top 2 nearest players by role + price (±20% range) ranked by FV.
|
||
""", unsafe_allow_html=False)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
run()
|