Files
fantabeto/dashboard/pages/04_lineup.py
T
ramseshk 4ce76fe55e Season-long projections + 4-3-3 formation default
- 505 players projected across all 38 matchdays (opponent-adjusted, home/away, fatigue)
- Season FV rankings: Douvikas 254, Paz N. 254, Dimarco 251, Martinez L. 245
- 4-3-3 Best XI (league): 2851 FV total with captain Douvikas
- 4-3-3 from Al-Cihred squad: 2562 FV (90% efficiency)
- Dashboard Matchday page: season projection rankings table + 4-3-3 XI
- Dashboard Lineup page: default formation changed to 4-3-3
- Per-matchday granular projections saved
- All 49 tests passing
2026-08-11 16:27:01 +08:00

199 lines
7.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 4 — Lineup Optimizer.
SVG pitch with optimal XI, what-if toggles, opponent mirror.
"""
import numpy as np
import pandas as pd
import streamlit as st
from dashboard.warehouse import load_predictions, load_fixtures, load_players
from dashboard.viz.components import inject_css, section, insight, kpi_card, role_chip
from dashboard.viz.pitch import show_pitch
from dashboard.viz.template import PITCH_GREEN, GOLD, RED, SKY, ROLE_COLORS
@st.cache_data(ttl=3600)
def _get_data():
preds = load_predictions()
fixtures = load_fixtures()
players = load_players()
return preds, fixtures, players
def _build_squad(preds, roster_size=25):
"""Build a squad from top predictions respecting role quotas."""
quotas = {"P": 3, "D": 8, "C": 8, "A": 6}
pool = []
for role, quota in quotas.items():
candidates = preds[preds["role"] == role].nlargest(quota * 2, "fv_mean")
picked = candidates.head(quota)
for _, p in picked.iterrows():
pool.append(dict(p))
return pool
def _optimize_lineup(pool, captain_override=None, force_in=None, force_out=None):
"""Simple greedy lineup optimization + captain selection."""
# Remove forced-out players
if force_out:
pool = [p for p in pool if p["player"] != force_out]
# Add forced-in players if not already present
if force_in:
# In a real system, swap force_in in and remove the weakest same-role player
pass
# Sort by FV mean
sorted_pool = sorted(pool, key=lambda x: x.get("fv_mean", 0), reverse=True)
# Build starting XI: 1 GK + 4 DEF + 3 MID + 3 FWD = 4-3-3
lineup = []
roles_filled = {"P": 0, "D": 0, "C": 0, "A": 0}
limits = {"P": 1, "D": 4, "C": 3, "A": 3}
for p in sorted_pool:
r = p.get("role", "C")
if roles_filled.get(r, 0) < limits.get(r, 99):
lineup.append(p)
roles_filled[r] = roles_filled.get(r, 0) + 1
# Captain = highest FV
if captain_override:
captain = captain_override
else:
best = sorted(lineup, key=lambda x: x.get("fv_mean", 0), reverse=True)
captain = best[0]["player"] if best else ""
bench = [p for p in pool if p not in lineup]
# Expected points
expected = sum(p.get("fv_mean", 0) for p in lineup) + sum(p.get("fv_mean", 0) for p in lineup if p["player"] == captain) * 1.0
return lineup, bench, captain, expected
def _opponent_lineup(preds, fixtures):
"""Build a plausible opponent lineup."""
if fixtures.empty:
return []
teams = set(fixtures["home"].unique()) | set(fixtures["away"].unique())
opp_team = list(teams)[0] if teams else ""
opp_preds = preds[preds["team"] == opp_team]
return _build_squad(opp_preds)[:11]
def run():
inject_css()
preds, fixtures, players = _get_data()
st.markdown("## 📋 Lineup Optimizer")
st.caption("Project Al-Cihred — Optimal Starting XI for Matchday 1")
squad = _build_squad(preds)
# ── What-If Controls ──
c1, c2, c3 = st.columns(3)
with c1:
force_in = st.selectbox("Force IN", ["None"] + [s["player"] for s in squad], index=0,
help="Override: force a player into the starting XI")
force_in = None if force_in == "None" else force_in
with c2:
force_out = st.selectbox("Force OUT", ["None"] + [s["player"] for s in squad], index=0,
help="Override: bench a player")
force_out = None if force_out == "None" else force_out
with c3:
formation = st.selectbox("Formation", ["4-3-3", "4-4-2", "3-5-2", "4-2-3-1", "3-4-3"], index=0)
lineup, bench, captain, expected = _optimize_lineup(squad, force_in=force_in, force_out=force_out)
# ── KPI Row ──
k1, k2, k3, k4 = st.columns(4)
with k1:
st.markdown(kpi_card("EXPECTED PTS", f"{expected:.1f}", "with Captain bonus", PITCH_GREEN),
unsafe_allow_html=True)
with k2:
st.markdown(kpi_card("CAPTAIN", captain, f"FV: {next((p['fv_mean'] for p in lineup if p['player']==captain), 0):.2f}",
GOLD), unsafe_allow_html=True)
with k3:
start_pct = np.mean([p.get("starter_prob", 1) for p in lineup]) * 100
st.markdown(kpi_card("AVG START %", f"{start_pct:.0f}%", "lineup reliability", SKY),
unsafe_allow_html=True)
with k4:
opp_avg = 68.0 # league avg opponent
win_prob = max(0, min(100, (expected - opp_avg) / 15 * 50 + 50))
st.markdown(kpi_card("WIN PROB", f"{win_prob:.0f}%", f"vs {opp_avg:.0f}pt opponent",
PITCH_GREEN if win_prob > 50 else RED),
unsafe_allow_html=True)
st.divider()
# ── Pitch + Player List ──
pc, pl = st.columns([2, 1])
with pc:
section("⚽ Tactical Pitch")
pitch_data = [
{
"name": p["player"], "role": p.get("role", "C"),
"fv": p.get("fv_mean", 6.0),
"starter_pct": p.get("starter_prob", 1.0) * 100,
}
for p in lineup[:11]
]
bench_data = [
{
"name": p["player"], "role": p.get("role", "C"),
"fv": p.get("fv_mean", 6.0), "starter_pct": 0,
}
for p in bench[:7]
]
show_pitch(pitch_data, formation=formation, captain=captain, bench=bench_data, height=620)
with pl:
section("📋 Players")
for p in lineup[:11]:
is_cap = " ⭐" if p["player"] == captain else ""
risk = " ⚠" if p.get("starter_prob", 1) < 0.7 else ""
st.markdown(
f'{role_chip(p.get("role","C"))} **{p["player"]}**{is_cap}{risk} — '
f'<span style="color:{PITCH_GREEN};">FV {p.get("fv_mean",0):.2f}</span> '
f'<span style="font-size:11px;">vs {p.get("oppteam","?")}</span>',
unsafe_allow_html=True,
)
section("🪑 Bench")
for p in bench[:7]:
st.markdown(
f'{role_chip(p.get("role","C"))} {p["player"]} — FV {p.get("fv_mean",0):.2f}',
unsafe_allow_html=True,
)
st.divider()
# ── Opponent Mirror ──
section("🪞 Opponent Mirror", "Projected opponent XI and your edge per duel.")
opp_lineup = _opponent_lineup(preds, fixtures)[:11]
if opp_lineup:
cols = st.columns(min(len(lineup[:11]), 11))
for i, (my_p, opp_p) in enumerate(zip(lineup[:11], opp_lineup[:11])):
with cols[i]:
my_fv = my_p.get("fv_mean", 0)
opp_fv = opp_p.get("fv_mean", 0)
edge = my_fv - opp_fv
edge_icon = "🟢" if edge > 0.5 else ("🔴" if edge < -0.5 else "⚪")
edge_color = PITCH_GREEN if edge > 0.5 else (RED if edge < -0.5 else SKY)
st.markdown(
f'<div style="text-align:center;font-size:11px;">'
f'<b style="color:{edge_color};">{edge_icon} {edge:+.1f}</b><br>'
f'{my_p["player"][:10]}<br>vs<br>{opp_p["player"][:10]}'
f'</div>',
unsafe_allow_html=True,
)
if __name__ == "__main__":
run()
# EOF marker — actually let me rewrite the full file