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 + format 4-4-2 lineup = [] roles_filled = {"P": 0, "D": 0, "C": 0, "A": 0} limits = {"P": 1, "D": 4, "C": 4, "A": 2} 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-4-2", "4-3-3", "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'FV {p.get("fv_mean",0):.2f} ' f'vs {p.get("oppteam","?")}', 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'