6c1fb46e8a
- Realistic price model: scales by FVM (Lautaro FVM=370 → ~199 cr, QI×5.7) Formula: qi * max(1.2, fvm/65) with forward premium and small-sample penalty - 5 strategy engines: Balanced, Value, Stars, Safe, MILP-Lite Each with different score functions (value ratio, stability, risk adj) - Strategy comparison radar grid: total FV, efficiency, stars, avg games, budget% - Squad table with color-coded price bands (gold >100cr, sky >50cr, green <50cr) - Market distribution histogram by role - Top value picks section with cr/FV efficiency - Fixed role_chip HTML rendering in all pages (unsafe_allow_html=True) - Verified: 0 errors, 31 role chips, 5 KPIs, 8 charts
171 lines
6.6 KiB
Python
171 lines
6.6 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 2 — Player Intelligence.
|
|
|
|
Radar charts, regression analysis, card risk, RAG news feed.
|
|
"""
|
|
|
|
import numpy as np
|
|
import pandas as pd
|
|
import streamlit as st
|
|
|
|
from dashboard.warehouse import load_players, load_predictions, load_votes, load_lineups
|
|
from dashboard.viz.components import inject_css, section, insight, role_chip
|
|
from dashboard.viz.charts import percentile_radar, regression_chart, bonus_donut, card_gauge
|
|
from dashboard.viz.template import PITCH_GREEN, GOLD, RED, SKY, ROLE_COLORS, ROLE_ICONS
|
|
|
|
|
|
@st.cache_data(ttl=3600)
|
|
def _get_data():
|
|
players = load_players()
|
|
preds = load_predictions()
|
|
votes = load_votes()
|
|
lineups = load_lineups()
|
|
return players, preds, votes, lineups
|
|
|
|
|
|
RADAR_METRICS = [
|
|
"goals_p90", "assists_p90", "xg_p90", "progressive_passes_p90",
|
|
"tackles_p90", "interceptions_p90", "shots_on_target_pct",
|
|
]
|
|
RADAR_LABELS = ["Goals", "Assists", "xG", "Prog Pass", "Tackles", "Interc.", "SoT%"]
|
|
|
|
|
|
def _get_player_data(players, preds, votes, lineups, player_name):
|
|
prow = players[players["player"] == player_name]
|
|
if prow.empty:
|
|
return None, None, None, None
|
|
p = prow.iloc[0]
|
|
|
|
pred_row = preds[preds["player"] == player_name]
|
|
if pred_row.empty:
|
|
pred_row = pd.DataFrame([{"fv_mean": p.get("fv_avg", 6.0)}])
|
|
|
|
vote_row = votes[votes["player"] == player_name]
|
|
line_row = lineups[lineups["player"] == player_name]
|
|
|
|
p["starter_pct"] = float(line_row["starter_pct"].values[0]) if not line_row.empty else 50
|
|
p["fv_projected"] = float(pred_row["fv_mean"].values[0]) if not pred_row.empty else p.get("fv_avg", 6.0)
|
|
p["games_season"] = p.get("games_season", 0)
|
|
p["goals_season"] = p.get("goals_season", 0)
|
|
p["assists_season"] = p.get("assists_season", 0)
|
|
p["yellow_per_game"] = p.get("yellow_season", 0) / max(p.get("games_season", 1), 1)
|
|
p["red_per_game"] = p.get("red_season", 0) / max(p.get("games_season", 1), 1)
|
|
|
|
return p, pred_row, vote_row, line_row
|
|
|
|
|
|
def run():
|
|
inject_css()
|
|
players, preds, votes, lineups = _get_data()
|
|
|
|
st.markdown("## 👤 Player Intelligence")
|
|
st.caption("Deep-dive into any Serie A player — radar profiles, regression analysis, and news feed.")
|
|
|
|
# Search
|
|
c_search, c_role, c_team = st.columns([3, 1, 1])
|
|
with c_search:
|
|
all_players = sorted(players["player"].dropna().unique())
|
|
selected = st.selectbox("Search player", all_players, key="player_search",
|
|
placeholder="Type a name...")
|
|
with c_role:
|
|
role_filter = st.selectbox("Role", ["All", "P", "D", "C", "A"], index=0)
|
|
with c_team:
|
|
teams = sorted(players["team"].dropna().unique())
|
|
team_filter = st.selectbox("Team", ["All"] + teams, index=0)
|
|
|
|
if not selected:
|
|
st.info("Search for a player above to see their profile.")
|
|
return
|
|
|
|
p, pred_row, vote_row, line_row = _get_player_data(players, preds, votes, lineups, selected)
|
|
if p is None:
|
|
st.warning(f"Player '{selected}' not found in database.")
|
|
return
|
|
|
|
# ── Player Header ──
|
|
st.divider()
|
|
h1, h2, h3, h4 = st.columns([2, 1, 1, 1])
|
|
with h1:
|
|
role = p.get("role", "?")
|
|
team = p.get("team", "?")
|
|
st.markdown(f"### {ROLE_ICONS.get(role, '')} {selected}")
|
|
st.caption(f"{ROLE_ICONS.get(role, '')} {role} · {team} · FVM: {p.get('fvm', 0)} · QI: {p.get('qi', 0)} cr")
|
|
with h2:
|
|
fv = p.get("fv_avg", 6.0)
|
|
st.metric("Fantavoto Avg", f"{fv:.2f}", delta=None)
|
|
with h3:
|
|
games = p.get("games_season", 0)
|
|
st.metric("Games (25/26)", f"{games:.0f}")
|
|
with h4:
|
|
sp = p.get("starter_pct", 50)
|
|
st.metric("GW1 Start %", f"{sp:.0f}%")
|
|
|
|
st.divider()
|
|
|
|
# ── Radar + Regression ──
|
|
r1, r2 = st.columns([1, 1])
|
|
with r1:
|
|
section("📊 Percentile Radar")
|
|
role_avg = players[players["role"] == role].mean(numeric_only=True)
|
|
fig = percentile_radar(p, RADAR_METRICS, RADAR_LABELS, role_avg)
|
|
st.plotly_chart(fig, width="stretch")
|
|
insight("Values normalized vs league average for same role. Outer = better.")
|
|
|
|
with r2:
|
|
section("🎯 Goals vs Expected")
|
|
pdf = pd.DataFrame([{
|
|
"goals_season": p.get("goals_season", 0),
|
|
"xg_p90": p.get("xg_p90", 0) or (p.get("xg_season", 0) / 38) if "xg_season" in p else 0,
|
|
"games_season": p.get("games_season", 1),
|
|
}])
|
|
fig = regression_chart(pdf, selected)
|
|
st.plotly_chart(fig, width="stretch")
|
|
div = p.get("goals_season", 0) - (p.get("xg_p90", 0) or 0) * p.get("games_season", 1)
|
|
div_label = "overperforming" if div > 1 else ("underperforming" if div < -1 else "on par with")
|
|
insight(f"{selected} is {div_label} xG by {abs(div):.1f} goals.")
|
|
|
|
st.divider()
|
|
|
|
# ── Bonus/Malus + Card Risk ──
|
|
b1, b2 = st.columns([1, 1])
|
|
with b1:
|
|
section("💰 Bonus / Malus Breakdown")
|
|
goals_26 = p.get("goals_season", 0)
|
|
assists_26 = p.get("assists_season", 0)
|
|
yellow = p.get("yellow_season", 0)
|
|
red_c = p.get("red_season", 0)
|
|
fig = bonus_donut(goals_26, assists_26, yellow * 0.5 + red_c * 1.0)
|
|
st.plotly_chart(fig, width="stretch")
|
|
insight(f"Season totals: {goals_26:.0f}G + {assists_26:.0f}A — {yellow:.0f}🟨 {red_c:.0f}🟥")
|
|
|
|
with b2:
|
|
section("⚠️ Card Risk Gauge")
|
|
fig = card_gauge(p.get("yellow_per_game", 0), p.get("red_per_game", 0))
|
|
st.plotly_chart(fig, width="stretch")
|
|
ypg = p.get("yellow_per_game", 0)
|
|
if ypg > 0.2:
|
|
insight(f"⚠️ High yellow risk: {ypg:.2f} per game. Consider rotation in tough fixtures.")
|
|
else:
|
|
insight(f"Low card risk: {ypg:.2f} yellows per game.")
|
|
|
|
st.divider()
|
|
|
|
# ── RAG News Feed ──
|
|
section("📰 News & Intelligence", "Live updates from Italian sports press (Gazzetta, Sky, Di Marzio).")
|
|
st.info("🔌 RAG news pipeline available when `src/features/news_rag.py` is run. "
|
|
"Shows injury reports, suspensions, tactical shifts, and transfer rumors.")
|
|
|
|
# ── Historical votes table ──
|
|
if vote_row is not None and not vote_row.empty:
|
|
st.divider()
|
|
section("📋 Recent Matchday Votes")
|
|
recent = vote_row.nlargest(10, "matchday")[[
|
|
"matchday", "vote", "goals", "fantavote"
|
|
]].sort_values("matchday", ascending=False)
|
|
recent.columns = ["Matchday", "Vote", "Goals", "Fantavote"]
|
|
st.dataframe(recent, width="stretch", hide_index=True)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
run()
|