Files
fantabeto/dashboard/pages/02_players.py
T
ramseshk 397078d635 Fix dashboard imports for Streamlit multi-page routing
- Add sys.path.insert at top of app.py and all 5 page files
- Fix .streamlit/config.toml CORS/XSRF conflict
- Remove duplicate st.set_page_config from page files
- Verified: all pages import cleanly, dashboard boots via streamlit run
- All 49 tests passing (31 original + 18 dashboard)
2026-08-11 15:08:09 +08:00

171 lines
6.7 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_chip(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, use_container_width=True)
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, use_container_width=True)
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, use_container_width=True)
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, use_container_width=True)
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, use_container_width=True, hide_index=True)
if __name__ == "__main__":
run()