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
This commit is contained in:
@@ -1,17 +1,24 @@
|
|||||||
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))
|
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.
|
"""Page 1 — Matchday Control Room.
|
||||||
|
|
||||||
KPI row, fixture difficulty heatmap, start/sit table, bump chart.
|
KPI row, fixture difficulty heatmap, start/sit table, season-long projections.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import streamlit as st
|
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.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.components import inject_css, kpi_card, section, insight, role_chip
|
||||||
from dashboard.viz.charts import fixture_heatmap, kpi_sparkline, error_violins
|
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
|
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)
|
@st.cache_data(ttl=3600)
|
||||||
@@ -168,6 +175,47 @@ def run():
|
|||||||
)
|
)
|
||||||
st.plotly_chart(fig, width="stretch")
|
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__":
|
if __name__ == "__main__":
|
||||||
run()
|
run()
|
||||||
|
|||||||
@@ -48,10 +48,10 @@ def _optimize_lineup(pool, captain_override=None, force_in=None, force_out=None)
|
|||||||
# Sort by FV mean
|
# Sort by FV mean
|
||||||
sorted_pool = sorted(pool, key=lambda x: x.get("fv_mean", 0), reverse=True)
|
sorted_pool = sorted(pool, key=lambda x: x.get("fv_mean", 0), reverse=True)
|
||||||
|
|
||||||
# Build starting XI: 1 GK + format 4-4-2
|
# Build starting XI: 1 GK + 4 DEF + 3 MID + 3 FWD = 4-3-3
|
||||||
lineup = []
|
lineup = []
|
||||||
roles_filled = {"P": 0, "D": 0, "C": 0, "A": 0}
|
roles_filled = {"P": 0, "D": 0, "C": 0, "A": 0}
|
||||||
limits = {"P": 1, "D": 4, "C": 4, "A": 2}
|
limits = {"P": 1, "D": 4, "C": 3, "A": 3}
|
||||||
|
|
||||||
for p in sorted_pool:
|
for p in sorted_pool:
|
||||||
r = p.get("role", "C")
|
r = p.get("role", "C")
|
||||||
@@ -104,7 +104,7 @@ def run():
|
|||||||
help="Override: bench a player")
|
help="Override: bench a player")
|
||||||
force_out = None if force_out == "None" else force_out
|
force_out = None if force_out == "None" else force_out
|
||||||
with c3:
|
with c3:
|
||||||
formation = st.selectbox("Formation", ["4-4-2", "4-3-3", "3-5-2", "4-2-3-1", "3-4-3"], index=0)
|
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)
|
lineup, bench, captain, expected = _optimize_lineup(squad, force_in=force_in, force_out=force_out)
|
||||||
|
|
||||||
@@ -194,3 +194,5 @@ def run():
|
|||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
run()
|
run()
|
||||||
|
|
||||||
|
# EOF marker — actually let me rewrite the full file
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user