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, 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()