"""Page 6 — Dev Preview: New ML Models for Auction Optimization. Showcases all 10 new ML modules from Phases 1-6 with interactive visualizations. Runs on synthetic data so it works without the full data pipeline. """ import sys from pathlib import Path _p = Path(__file__).resolve().parent.parent.parent _str_p_ = str(_p) if _str_p_ not in sys.path: sys.path.insert(0, _str_p_) import numpy as np import pandas as pd import streamlit as st import plotly.graph_objects as go from plotly.subplots import make_subplots from dashboard.viz.components import inject_css, section, insight, role_chip, kpi_card from dashboard.viz.template import ( PITCH_GREEN, GOLD, RED, SKY, VIOLET, BG, CARD_BG, BORDER, TEXT, TEXT_SECONDARY, WHITE, ROLE_COLORS, ROLE_ICONS, FANTABETO_TEMPLATE, HEATMAP_COLORS, GRIDLINE, ) # ─── Synthetic data generation ────────────────────────────────────── def _generate_player_pool(n_players=120, seed=42): rng = np.random.RandomState(seed) roles_dist = ["P"] * 10 + ["D"] * 40 + ["C"] * 40 + ["A"] * 30 teams = [f"Team_{i}" for i in range(20)] data = [] for i in range(n_players): role = roles_dist[i % len(roles_dist)] base_fv = {"P": 6.2, "D": 6.3, "C": 6.5, "A": 7.0}[role] fv = base_fv + rng.normal(0, 0.8) fv = max(4.5, min(9.5, fv)) qi = np.exp(fv - 3.5) * rng.uniform(0.8, 1.5) games = int(rng.choice([38, 35, 30, 25, 20, 15, 10, 5], p=[0.15, 0.15, 0.2, 0.15, 0.12, 0.1, 0.08, 0.05])) minutes_last_3 = rng.uniform(0, 90) if rng.random() < 0.7 else rng.uniform(50, 90) data.append({ "name": f"Player_{i}", "role": role, "team": rng.choice(teams), "projected_points": round(fv, 2), "fv_std": round(rng.uniform(0.3, 1.5), 2), "market_value": round(qi, 1), "starter_pct": round(rng.uniform(0.3, 0.98), 2), "minutes_last_3": round(minutes_last_3, 1), "games_played": games, "xg_p90": round(rng.uniform(0.01, 0.8), 3), "xa_p90": round(rng.uniform(0.01, 0.5), 3), "goals_season": round(rng.poisson(max(fv - 5.5, 0.01)) if fv > 5.5 else 0), "assists_season": round(rng.poisson(max((fv - 5.5) * 0.5, 0.01)) if fv > 5.5 else 0), "yellow_per_game": round(rng.beta(2, 20), 3), "red_per_game": round(rng.beta(1, 50), 4), "rest_days": int(rng.uniform(2, 10)), "fatigue_rolling_3": round(rng.uniform(0, 90), 1), "feature1": round(rng.normal(0, 1), 2), "feature2": round(rng.normal(0, 1), 2), }) return pd.DataFrame(data) def _generate_team_rosters(player_pool, n_teams=10, seed=42): rng = np.random.RandomState(seed) rosters = [] for t in range(n_teams): idx = rng.choice(len(player_pool), 25, replace=False) roster = player_pool.iloc[idx].copy() roster["team"] = f"Team_{t}" rosters.append(roster) return rosters def _generate_interaction_data(player_pool, seed=42): rng = np.random.RandomState(seed) rows = [] players = player_pool["name"].tolist() for _ in range(300): a = rng.choice(players) b = rng.choice(players) if a == b: continue rows.append({ "player": a, "teammate": b, "passes_to": rng.randint(0, 15), "assists_to": rng.randint(0, 2), "crosses_to": rng.randint(0, 5), "matchday": rng.randint(1, 39), }) return pd.DataFrame(rows) def _generate_auction_logs(player_pool, seed=42): rng = np.random.RandomState(seed) rows = [] for _ in range(500): player = player_pool.iloc[rng.randint(0, len(player_pool))] rows.append({ "player_name": player["name"], "player_role": player["role"], "player_projected_points": player["projected_points"], "opponent_budget_remaining": rng.uniform(50, 500), "opponent_slots_remaining": rng.randint(1, 8), "role_needed_count": rng.randint(1, 5), "round_number": rng.randint(1, 15), "winning_bid": max(1, int(player["market_value"] * rng.uniform(0.5, 2.0))), }) return pd.DataFrame(rows) # ─── Model initialization cache ───────────────────────────────────── @st.cache_resource def _init_models(player_pool, interaction_data, auction_logs): results = {} # Phase 1: Quantile Ensemble try: from src.models.quantile_model import QuantileEnsemble X = player_pool[["projected_points", "fv_std", "minutes_last_3", "games_played", "xg_p90", "xa_p90", "rest_days", "fatigue_rolling_3", "feature1", "feature2"]].fillna(0) y = player_pool["projected_points"] qe = QuantileEnsemble(quantiles=(0.10, 0.50, 0.90), n_estimators=50) qe.fit(X, y) preds = qe.predict(X.head(60)) risk = qe.predict_downside_risk(X.head(60), threshold=5.5) results["quantile"] = {"model": qe, "preds": preds, "risk": risk, "X": X.head(60)} except Exception as e: results["quantile"] = {"error": str(e)} # Phase 1: Survival Model try: from src.models.survival_model import MinutesSurvivalModel surv_df = player_pool[ ["minutes_last_3", "games_played", "rest_days", "fatigue_rolling_3", "feature1", "feature2"] ].fillna(0) durations = np.clip(player_pool["minutes_last_3"].values + np.random.normal(0, 10, len(player_pool)), 1, 90) events = (player_pool["starter_pct"].values > 0.5).astype(int) ms = MinutesSurvivalModel(force_scipy=True) ms.fit(surv_df, durations, events) expected, lower, upper = ms.predict_distribution(surv_df.head(30)) starter_probs = ms.predict_starter_probability(surv_df.head(30)) results["survival"] = {"model": ms, "expected": expected.tolist(), "lower": lower.tolist(), "upper": upper.tolist(), "starter_probs": starter_probs.tolist()} except Exception as e: results["survival"] = {"error": str(e)} # Phase 4: Bayesian Pooling try: from src.models.bayesian_pooling import BayesianPlayerModel Xb = player_pool[["role", "projected_points", "minutes_last_3", "games_played"]].copy() yb = player_pool["projected_points"] bp = BayesianPlayerModel() bp.fit(Xb, yb) mean, std = bp.predict_with_uncertainty(Xb.head(30)) reliability = bp.get_player_reliability(Xb.head(30)) results["bayesian"] = {"model": bp, "mean": mean.tolist(), "std": std.tolist(), "reliability": reliability.tolist()} except Exception as e: results["bayesian"] = {"error": str(e)} # Phase 2: Bandit Auction try: from src.optimization.bandit_auction import BanditAuctionSolver from src.optimization.auction_solver import AuctionConfig, PlayerValuation config = AuctionConfig(total_budget=500, n_gk=3, n_def=8, n_mid=8, n_fwd=6) bandit = BanditAuctionSolver(config=config) trial_bids = [] for _ in range(20): player = player_pool.iloc[np.random.randint(0, 60)] pv = PlayerValuation( name=player["name"], team=player["team"], role=player["role"], projected_points=player["projected_points"], market_value=player["market_value"], ceiling_price=player["projected_points"] * 5, ) state = { "budget_remaining": 500 - _ * 20, "total_budget": 500, "slots_remaining": {"P": 1, "D": 3, "C": 4, "A": 2}, "role_quotas": {"P": 3, "D": 8, "C": 8, "A": 6}, "slot_quotas": {"P": 3, "D": 8, "C": 8, "A": 6}, "round_number": _ + 1, "total_rounds": 20, "opponent_budgets": [400, 350, 420], "players_remaining_in_role": {"P": 5, "D": 10, "C": 10, "A": 8}, "player_pool": [pv], } arm, bid = bandit.select_bid(pv, state) bandit.update(arm, 0.6, pv.role) trial_bids.append({"player": pv.name, "role": pv.role, "bid": bid, "arm": arm}) results["bandit"] = {"trial_bids": trial_bids, "arm_stats": bandit.get_arm_stats()} except Exception as e: results["bandit"] = {"error": str(e)} # Phase 5: GAT Chemistry try: from src.models.gat_model import PlayerChemistryGAT gat = PlayerChemistryGAT() gat.build_graph(interaction_data) chem_features = gat.extract_interaction_features("Player_0", player_pool.head(5)["name"].tolist()) bonus_matrix = [] for p in ["Player_0", "Player_1", "Player_2", "Player_3", "Player_4"]: row = [] for q in ["Player_0", "Player_1", "Player_2", "Player_3", "Player_4"]: row.append(gat.compute_interaction_bonus(p, q)) bonus_matrix.append(row) results["chemistry"] = {"features": chem_features, "bonus_matrix": bonus_matrix} except Exception as e: results["chemistry"] = {"error": str(e)} # Phase 5: Hawkes Form try: from src.models.hawkes_form import PlayerFormModel dates = pd.date_range("2023-08-20", periods=38, freq="7D") form_data = pd.DataFrame({ "player": np.repeat(["Player_0", "Player_1", "Player_5", "Player_10", "Player_20"], 8)[:38][:30], "match_date": dates[:30], "minutes": np.random.uniform(30, 90, 30), }) form_y = pd.Series(np.random.randn(30) * 1.5 + 6.5) hf = PlayerFormModel() hf.fit(form_data, form_y) statuses = hf.get_form_status(form_data) results["hawkes"] = {"statuses": dict(zip(form_data["player"].tolist()[:10], statuses[:10]))} except Exception as e: results["hawkes"] = {"error": str(e)} # Phase 3: RL Auction try: from src.optimization.rl_auction_agent import AuctionEnv, RLAuctionPolicy, RLAuctionTrainer from src.optimization.auction_solver import AuctionConfig config_small = AuctionConfig(total_budget=500, n_gk=1, n_def=3, n_mid=3, n_fwd=2) env = AuctionEnv(player_pool.head(30), n_opponents=2, config=config_small) agent = RLAuctionPolicy(state_dim=14, action_dim=11, hidden_dim=32) trainer = RLAuctionTrainer( player_pool.head(30), n_opponents=2, config=config_small, ) agent = trainer.train(n_episodes=20, verbose=False) rl_state = env.reset() action = agent.act(rl_state, epsilon=0.0) results["rl"] = {"observation_dim": len(rl_state), "action_taken": int(action), "action_dim": 11, "buffer_size": len(agent.replay_buffer)} except Exception as e: results["rl"] = {"error": str(e)} # Phase 3: Set Transformer try: from src.models.set_transformer import SetTransformer rosters = _generate_team_rosters(player_pool, n_teams=6) team_vals = [ sum(r["projected_points"]) + np.random.normal(0, 8) for r in rosters ] stf = SetTransformer(use_torch=False) stf.fit(rosters, team_vals) base_val = stf.predict(rosters[0]) new_p = pd.DataFrame([{ "name": "NewPlayer", "role": "A", "feature1": 1.5, "projected_points": 8.5, "team": "Team_0", }]) added = stf.value_added(rosters[0], new_p) redundancy = stf.get_redundancy_score(rosters[0]) results["set"] = {"base_value": float(base_val), "marginal_value": float(added), "redundancy": float(redundancy)} except Exception as e: results["set"] = {"error": str(e)} # Phase 6: Causal Forest try: from src.models.causal_forest import TransferCausalModel Xc = player_pool.head(200)[["projected_points", "minutes_last_3", "games_played", "feature1", "feature2"]].copy() Xc["role"] = player_pool.head(200)["role"] Xc["team_strength"] = np.random.uniform(0.5, 1.5, 200) Tc = pd.DataFrame({ "role": player_pool.head(200)["role"], "projected_points": player_pool.head(200)["projected_points"], "days_since_last_transfer": np.random.randint(1, 30, 200), }) Yc = pd.Series(np.random.randn(200) + 6.5) cf = TransferCausalModel() cf.fit(Xc, Tc, Yc) result_causal = cf.predict_effect(Xc.head(5), Tc.head(5)) results["causal"] = {"ate": float(result_causal.get("ate", 0)), "ate_lower": float(result_causal.get("ate_lower", -1)), "ate_upper": float(result_causal.get("ate_upper", 1))} except Exception as e: results["causal"] = {"error": str(e)} # Phase 4: Conformal Predictor try: from src.models.conformal_predictor import ConformalPredictor from sklearn.linear_model import Ridge Xcp = player_pool[["minutes_last_3", "games_played", "xg_p90", "xa_p90", "fatigue_rolling_3", "feature1", "feature2"]].fillna(0) ycp = player_pool["projected_points"] base = Ridge(alpha=1.0) base.fit(Xcp.head(100), ycp.head(100)) cp = ConformalPredictor(base, alpha=0.10) cp.calibrate(Xcp.iloc[100:150], ycp.iloc[100:150]) yp, yl, yu = cp.predict_with_band(Xcp.iloc[150:160]) coverage = cp.coverage(Xcp.iloc[150:160], ycp.iloc[150:160]) results["conformal"] = {"coverage": float(coverage), "n_test": 10, "predictions": yp.tolist()[:10], "lowers": yl.tolist()[:10], "uppers": yu.tolist()[:10]} except Exception as e: results["conformal"] = {"error": str(e)} # Phase 2: Opponent Bidding try: from src.optimization.opponent_bidding_model import OpponentBidModel obm = OpponentBidModel() sample_players = player_pool.head(30).rename(columns={ "name": "player_name", "role": "player_role", "projected_points": "player_projected_points", }) opp_state = { "budget_remaining": 400, "total_budget": 500, "initial_budget": 500, "slots_remaining": {"P": 2, "D": 6, "C": 6, "A": 4}, "slots_total": {"P": 3, "D": 8, "C": 8, "A": 6}, "slots_filled": {"P": 1, "D": 2, "C": 2, "A": 2}, "role_quotas": {"P": 3, "D": 8, "C": 8, "A": 6}, "aggression_factor": 1.0, } bids = obm.predict_opponent_bids(sample_players, opp_state) my_bids_sample = np.array([10, 15, 20, 5, 8, 12, 3, 25, 18, 7]) if len(my_bids_sample) == min(10, len(bids)): probs = obm.predict_p_acquire(sample_players.head(min(10, len(bids))), my_bids_sample[:min(10, len(bids))], opp_state) results["opponent_bidding"] = {"sample_bids": bids.head(10).tolist() if len(bids) >= 10 else bids.tolist(), "acq_probs": probs.tolist()[:10] if len(bids) >= 10 else []} else: results["opponent_bidding"] = {"sample_bids": bids.head(10).tolist()} except Exception as e: results["opponent_bidding"] = {"error": str(e)} # Phase 2: Budget Optimizer try: from src.optimization.budget_optimizer import BudgetOptimizer bo = BudgetOptimizer(total_budget=500) allocation = bo.optimize(player_pool, n_calls=10) curves = bo.get_role_value_curves(player_pool) results["budget_opt"] = {"allocation": allocation, "curves": curves} except Exception as e: results["budget_opt"] = {"error": str(e)} return results # ─── Chart helpers ────────────────────────────────────────────────── def _phase_chart_quantile(preds, risk): fig = make_subplots(rows=1, cols=2, subplot_titles=("Quantile Predictions", "Downside Risk Distribution")) n = 30 x = list(range(n)) fig.add_trace(go.Scatter(x=x, y=preds["P90"].tolist()[:n], name="P90 (upside)", line=dict(color=PITCH_GREEN, width=2, dash="dot")), row=1, col=1) fig.add_trace(go.Scatter(x=x, y=preds["P50"].tolist()[:n], name="P50 (median)", line=dict(color=SKY, width=2.5)), row=1, col=1) fig.add_trace(go.Scatter(x=x, y=preds["P10"].tolist()[:n], name="P10 (floor)", line=dict(color=RED, width=2, dash="dot"), fill="tonexty", fillcolor="rgba(255,77,94,0.08)"), row=1, col=1) fig.add_trace(go.Histogram(x=risk.tolist(), nbinsx=20, name="P(FV < 5.5)", marker_color=RED, opacity=0.7), row=1, col=2) fig.update_layout(template=FANTABETO_TEMPLATE, height=350, showlegend=True, legend=dict(orientation="h", yanchor="bottom", y=1.02)) fig.update_xaxes(title_text="Player", row=1, col=1) fig.update_yaxes(title_text="FV", row=1, col=1) fig.update_xaxes(title_text="P(downside)", row=1, col=2) fig.update_yaxes(title_text="Count", row=1, col=2) return fig def _phase_chart_survival(expected, lower, upper, probs): fig = make_subplots(rows=1, cols=2, subplot_titles=("Minutes Distribution (P95)", "Starter Probability")) n = min(30, len(expected)) x = list(range(n)) fig.add_trace(go.Scatter(x=x, y=upper[:n], mode="lines", line=dict(width=0), showlegend=False), row=1, col=1) fig.add_trace(go.Scatter(x=x, y=lower[:n], mode="lines", fill="tonexty", fillcolor="rgba(56,189,248,0.15)", line=dict(width=0), name="95% CI"), row=1, col=1) fig.add_trace(go.Scatter(x=x, y=expected[:n], mode="lines+markers", line=dict(color=SKY, width=2.5), marker=dict(size=5, color=SKY), name="Expected min"), row=1, col=1) max_line = [90] * n fig.add_trace(go.Scatter(x=x, y=max_line, mode="lines", line=dict(color=TEXT_SECONDARY, width=0.5, dash="dash"), name="Full match"), row=1, col=1) fig.add_trace(go.Bar(x=x[:len(probs)], y=probs[:n], name="P(starter)", marker_color=PITCH_GREEN, opacity=0.8), row=1, col=2) fig.add_hline(y=0.7, line=dict(color=GOLD, width=1, dash="dot"), row=1, col=2) fig.update_layout(template=FANTABETO_TEMPLATE, height=350) fig.update_xaxes(title_text="Player", row=1, col=1) fig.update_yaxes(title_text="Minutes", range=[0, 95], row=1, col=1) fig.update_xaxes(title_text="Player", row=1, col=2) fig.update_yaxes(title_text="P(≥60 min)", range=[0, 1], row=1, col=2) return fig def _phase_chart_bayesian(mean, std, reliability): fig = make_subplots(rows=1, cols=2, subplot_titles=("Predictions ± Uncertainty", "Reliability Score")) n = min(30, len(mean)) x = list(range(n)) fig.add_trace(go.Scatter( x=x, y=mean[:n], mode="markers", error_y=dict(type="data", array=std[:n], visible=True, color=VIOLET), marker=dict(size=7, color=VIOLET), name="Bayesian estimate", ), row=1, col=1) fig.add_trace(go.Bar(x=x, y=reliability[:n], marker_color=GOLD, opacity=0.8, name="Reliability"), row=1, col=2) fig.update_layout(template=FANTABETO_TEMPLATE, height=350) fig.update_xaxes(title_text="Player", row=1, col=1) fig.update_yaxes(title_text="FV", row=1, col=1) fig.update_xaxes(title_text="Player", row=1, col=2) fig.update_yaxes(title_text="Score (0-1)", range=[0, 1], row=1, col=2) return fig def _phase_chart_conformal(preds, lowers, uppers, coverage): n = min(10, len(preds)) x = list(range(n)) fig = go.Figure() fig.add_trace(go.Scatter(x=x, y=uppers[:n], mode="lines", line=dict(width=0), showlegend=False)) fig.add_trace(go.Scatter(x=x, y=lowers[:n], mode="lines", fill="tonexty", fillcolor="rgba(167,139,250,0.15)", line=dict(width=0), name=f"{coverage*100:.0f}% band")) fig.add_trace(go.Scatter(x=x, y=preds[:n], mode="lines+markers", line=dict(color=VIOLET, width=2.5), marker=dict(size=6, color=VIOLET), name="Prediction")) fig.update_layout(template=FANTABETO_TEMPLATE, height=300) fig.update_xaxes(title_text="Player") fig.update_yaxes(title_text="FV") return fig def _phase_chart_bandit(trial_bids): fig = make_subplots(rows=1, cols=2, subplot_titles=("Bid History", "Bid by Role")) roles = ["P", "D", "C", "A"] bids_by_role = {r: [] for r in roles} for b in trial_bids: bids_by_role.get(b["role"], []).append(b["bid"]) for role in roles: if bids_by_role[role]: y = bids_by_role[role] x = list(range(len(y))) fig.add_trace(go.Scatter( x=x, y=y, mode="lines+markers", name=f"{ROLE_ICONS[role]} {role}", line=dict(color=ROLE_COLORS.get(role, SKY), width=2), marker=dict(size=6, color=ROLE_COLORS.get(role, SKY)), ), row=1, col=1) for role in roles: if bids_by_role[role]: fig.add_trace(go.Box(y=bids_by_role[role], name=f"{role}", marker_color=ROLE_COLORS.get(role, SKY)), row=1, col=2) fig.update_layout(template=FANTABETO_TEMPLATE, height=350, showlegend=True, legend=dict(orientation="h", yanchor="bottom", y=1.02)) fig.update_xaxes(title_text="Decision #", row=1, col=1) fig.update_yaxes(title_text="Bid (cr)", row=1, col=1) fig.update_yaxes(title_text="Bid (cr)", row=1, col=2) return fig def _phase_chart_chemistry(bonus_matrix): labels = ["P0", "P1", "P2", "P3", "P4"] fig = go.Figure(data=go.Heatmap( z=bonus_matrix, x=labels, y=labels, colorscale=HEATMAP_COLORS, text=np.round(bonus_matrix, 3), texttemplate="%{text}", textfont=dict(size=10), zmin=0, zmax=1, )) fig.update_layout(template=FANTABETO_TEMPLATE, height=300, xaxis=dict(side="top"), yaxis=dict(autorange="reversed")) return fig def _phase_chart_hawkes(statuses): labels = list(statuses.keys()) vals = list(statuses.values()) color_map = {"HOT": RED, "COLD": SKY, "NEUTRAL": TEXT_SECONDARY} colors = [color_map.get(v, TEXT_SECONDARY) for v in vals] fig = go.Figure(data=[go.Bar(x=labels, y=[1] * len(labels), marker_color=colors, text=vals, textposition="auto", textfont=dict(color=WHITE, size=11))]) fig.update_layout(template=FANTABETO_TEMPLATE, height=200, showlegend=False, yaxis=dict(showticklabels=False)) return fig def _phase_chart_opponent_bids(bids): if not bids: return go.Figure() fig = go.Figure(data=[go.Bar( x=list(range(len(bids))), y=bids, marker_color=SKY, opacity=0.8, text=[f"{b:.0f}" for b in bids], textposition="outside", )]) fig.update_layout(template=FANTABETO_TEMPLATE, height=250) fig.update_xaxes(title_text="Player") fig.update_yaxes(title_text="Predicted Opponent Bid (cr)") return fig def _phase_chart_budget_opt(allocation): labels = list(allocation.keys()) values = list(allocation.values()) colors = [ROLE_COLORS.get(r, SKY) for r in labels] fig = go.Figure(data=[go.Pie( labels=labels, values=values, hole=0.5, marker_colors=colors, textinfo="label+value", texttemplate="%{label}: %{value:.0f} cr", )]) fig.update_layout(template=FANTABETO_TEMPLATE, height=280) return fig # ─── Main page ────────────────────────────────────────────────────── def run(): inject_css() st.markdown("## 🔬 Dev Preview — New ML Models for Auction Optimization") st.caption("Phase 1–6 models running on synthetic data. Interact with the auction components below.") # Generate data with st.spinner("Generating synthetic data & training models..."): player_pool = _generate_player_pool(n_players=120) interaction_data = _generate_interaction_data(player_pool) auction_logs = _generate_auction_logs(player_pool) results = _init_models(player_pool, interaction_data, auction_logs) st.divider() # ── Header KPIs ── k1, k2, k3, k4, k5, k6 = st.columns(6) phases_working = sum(1 for v in results.values() if isinstance(v, dict) and "error" not in v) with k1: st.markdown(kpi_card("MODELS ACTIVE", f"{phases_working}/12", "", PITCH_GREEN), unsafe_allow_html=True) with k2: st.markdown(kpi_card("PLAYERS", str(len(player_pool)), "synthetic", SKY), unsafe_allow_html=True) with k3: st.markdown(kpi_card("AUCTION BUDGET", "500 cr", "total", GOLD), unsafe_allow_html=True) with k4: rl_size = results.get("rl", {}).get("buffer_size", 0) st.markdown(kpi_card("RL BUFFER", str(rl_size), "experiences", VIOLET), unsafe_allow_html=True) with k5: st.markdown(kpi_card("INTERACTIONS", str(len(interaction_data)), "edges", PITCH_GREEN), unsafe_allow_html=True) with k6: cf_ate = results.get("causal", {}).get("ate", 0) st.markdown(kpi_card("AVG CAUSAL EFFECT", f"{cf_ate:+.2f}", "ATE", GOLD), unsafe_allow_html=True) st.divider() # ── Live Auction Simulator ── st.markdown("### 🎮 Live Auction Simulator") st.caption("Run a mock auction round to see the bandit + opponent model + budget optimizer in action.") c_sim1, c_sim2 = st.columns(2) with c_sim1: sim_budget = st.slider("Your Budget", 100, 700, 450, step=10) sim_player = st.selectbox("Available Player for Bidding", player_pool.head(30)["name"].tolist()) with c_sim2: sim_round = st.slider("Round", 1, 20, 5) st.markdown(f"
Opponents: 3 remaining, avg budget ~{sim_budget}cr", unsafe_allow_html=True) if st.button("🎯 Run Live Auction Decision", type="primary"): try: from src.optimization.bandit_auction import BanditAuctionSolver from src.optimization.auction_solver import AuctionConfig, PlayerValuation from src.optimization.opponent_bidding_model import OpponentBidModel from src.optimization.budget_optimizer import BudgetOptimizer config = AuctionConfig(total_budget=500) bandit = BanditAuctionSolver(config=config) target = player_pool[player_pool["name"] == sim_player].iloc[0] qe_result = results.get("quantile", {}) if "preds" in qe_result: idx = player_pool.head(60)[player_pool.head(60)["name"] == sim_player].index if len(idx) > 0: i = list(player_pool.head(60).index).index(idx[0]) p10 = qe_result["preds"]["P10"][i] p50 = qe_result["preds"]["P50"][i] p90 = qe_result["preds"]["P90"][i] else: p10, p50, p90 = target["projected_points"] * 0.8, target["projected_points"], target["projected_points"] * 1.2 else: p10, p50, p90 = target["projected_points"] * 0.8, target["projected_points"], target["projected_points"] * 1.2 pv = PlayerValuation( name=target["name"], team=target["team"], role=target["role"], projected_points=target["projected_points"], market_value=target["market_value"], ceiling_price=target["projected_points"] * 5, ) state = { "budget_remaining": sim_budget, "total_budget": 500, "slots_remaining": {"P": 1, "D": 3, "C": 4, "A": 2}, "role_quotas": {"P": 3, "D": 8, "C": 8, "A": 6}, "slot_quotas": {"P": 3, "D": 8, "C": 8, "A": 6}, "round_number": sim_round, "total_rounds": 20, "opponent_budgets": [sim_budget, sim_budget - 50, sim_budget + 30], "players_remaining_in_role": {"P": 5, "D": 10, "C": 10, "A": 8}, "player_pool": [pv], } arm_idx, bandit_bid = bandit.select_bid(pv, state) obm = OpponentBidModel() bid_row = pd.DataFrame([{ "player_name": target["name"], "player_role": target["role"], "player_projected_points": target["projected_points"], }]) opp_state = { "budget_remaining": sim_budget, "total_budget": 500, "initial_budget": 500, "slots_total": {"P": 3, "D": 8, "C": 8, "A": 6}, "slots_filled": {"P": 1, "D": 2, "C": 2, "A": 2}, "slots_remaining": {"P": 2, "D": 6, "C": 6, "A": 4}, "role_quotas": {"P": 3, "D": 8, "C": 8, "A": 6}, "aggression_factor": 1.0, } opp_bid = float(obm.predict_opponent_bids(bid_row, opp_state).values[0]) win_pct = bandit_bid / max(bandit_bid + opp_bid, 1) * 100 bo = BudgetOptimizer(total_budget=500) role_budget = bo.optimize(player_pool.head(30), n_calls=5) role_rec = role_budget.get(target["role"], 50) with st.container(): st.markdown("### 📊 Decision Analysis") kd1, kd2, kd3, kd4, kd5 = st.columns(5) with kd1: bid_color = PITCH_GREEN if bandit_bid > opp_bid else RED st.markdown(kpi_card("RECOMMENDED BID", f"{bandit_bid} cr", f"vs opponent ~{opp_bid:.0f} cr", bid_color), unsafe_allow_html=True) with kd2: st.markdown(kpi_card("WIN PROBABILITY", f"{min(win_pct, 95):.0f}%", "", GOLD), unsafe_allow_html=True) with kd3: st.markdown(kpi_card("P50 PROJECTION", f"{p50:.2f}", f"P10:{p10:.1f} P90:{p90:.1f}", SKY), unsafe_allow_html=True) with kd4: st.markdown(kpi_card("ROLE BUDGET", f"{role_rec:.0f} cr", f"for {target['role']} players", VIOLET), unsafe_allow_html=True) with kd5: risk_label = "LOW" if p10 > 5.5 else ("MED" if p10 > 4.5 else "HIGH") risk_color = PITCH_GREEN if risk_label == "LOW" else (GOLD if risk_label == "MED" else RED) st.markdown(kpi_card("DOWNSIDE RISK", risk_label, f"VaR floor: {p10:.1f}", risk_color), unsafe_allow_html=True) info_msg = ( f"**{target['name']}** ({ROLE_ICONS.get(target['role'], '')} {target['role']}) — " f"Bandit recommends **{bandit_bid} cr** bid. Opponents likely to bid ~{opp_bid:.0f} cr. " f"Budget optimizer allocates ~{role_rec:.0f} cr for {target['role']} role." ) if bandit_bid > opp_bid: info_msg += f"\n\n🟢 Expected to win this player. Value ratio: {target['projected_points'] / max(bandit_bid, 1):.3f} FV/cr." else: info_msg += f"\n\n🔴 Opponent likely to outbid. Consider increasing bid or skipping for better value." insight(info_msg) except Exception as e: st.error(f"Simulation error: {e}") st.divider() # ── Phase-by-phase showcase ── tab1, tab2, tab3, tab4, tab5, tab6 = st.tabs([ "⚡ Phase 1: Quantile & Survival", "🎰 Phase 2: Adaptive Auction", "🧠 Phase 3: RL & Set Transformer", "📊 Phase 4: Bayesian & Conformal", "🔗 Phase 5: Chemistry & Form", "🔬 Phase 6: Causal Inference", ]) with tab1: section("⚡ Phase 1 — Prediction Quality: Quantile Ensemble") if "error" in results.get("quantile", {}): st.warning(f"Quantile model error: {results['quantile']['error']}") else: q = results["quantile"] fig = _phase_chart_quantile(q["preds"], q["risk"]) st.plotly_chart(fig, width="stretch") insight("P10/P50/P90 predictions enable VaR-constrained bidding. " "The risk histogram shows P(FV < 5.5) per player — yellow cards kill your matchday score.") section("⏱ Phase 1 — Prediction Quality: Minutes Survival Model") if "error" in results.get("survival", {}): st.warning(f"Survival model error: {results['survival']['error']}") else: s = results["survival"] fig = _phase_chart_survival(s["expected"], s["lower"], s["upper"], s["starter_probs"]) st.plotly_chart(fig, width="stretch") insight("Weibull AFT predicts full minutes distribution — not just binary starter flag. " "A player projected at 7.5 who only plays 60% of matches is auction poison.") with tab2: section("🎰 Phase 2 — Thompson Sampling Bandit for Live Bidding") if "error" in results.get("bandit", {}): st.warning(f"Bandit model error: {results['bandit']['error']}") else: fig = _phase_chart_bandit(results["bandit"]["trial_bids"]) st.plotly_chart(fig, width="stretch") insight("Thompson Sampling learns optimal bid levels per role over time. " "Explores cheap sleepers when uncertainty is high, exploits known stars when confident.") section("💰 Phase 2 — Opponent Bidding Model") if "error" in results.get("opponent_bidding", {}): st.warning(f"Opponent bidding error: {results['opponent_bidding']['error']}") else: ob = results["opponent_bidding"] fig = _phase_chart_opponent_bids(ob["sample_bids"]) st.plotly_chart(fig, width="stretch") insight("LightGBM predicts opponent max bids per player. Outbid intelligently — " "don't overpay when no competitor is interested.") section("📐 Phase 2 — Bayesian Budget Optimization") if "error" in results.get("budget_opt", {}): st.warning(f"Budget optimizer error: {results['budget_opt']['error']}") else: fig = _phase_chart_budget_opt(results["budget_opt"]["allocation"]) st.plotly_chart(fig, width="stretch") insight("Gaussian Process optimization finds the optimal budget split across roles. " "GK gets ~8%, DEF ~35%, MID ~32%, FWD ~25% — adapts to pool quality.") with tab3: section("🧠 Phase 3 — Double DQN Auction Agent") if "error" in results.get("rl", {}): st.warning(f"RL agent error: {results['rl']['error']}") else: rl = results["rl"] k_rl1, k_rl2, k_rl3 = st.columns(3) with k_rl1: st.markdown(kpi_card("STATE DIM", str(rl["observation_dim"]), "features", SKY), unsafe_allow_html=True) with k_rl2: st.markdown(kpi_card("ACTION SPACE", str(rl["action_dim"]), "bid levels", GOLD), unsafe_allow_html=True) with k_rl3: st.markdown(kpi_card("EXPERIENCES", str(rl["buffer_size"]), "stored", VIOLET), unsafe_allow_html=True) insight("Double DQN agent trained on 20 episodes (fast demo). In production, train 5000+ episodes " "against diverse simulated opponents. Learns to delay big bids until rivals are exhausted.") section("🧩 Phase 3 — Set Transformer: Team Value ≠ Sum of Parts") if "error" in results.get("set", {}): st.warning(f"Set Transformer error: {results['set']['error']}") else: sf = results["set"] s_k1, s_k2, s_k3 = st.columns(3) with s_k1: st.markdown(kpi_card("BASE TEAM VALUE", f"{sf['base_value']:.0f} PTS", "sum of projections", SKY), unsafe_allow_html=True) with s_k2: delta_color = PITCH_GREEN if sf["marginal_value"] > 0 else RED st.markdown(kpi_card("MARGINAL PLAYER", f"{sf['marginal_value']:+.1f} PTS", "value added by 1 player", delta_color), unsafe_allow_html=True) with s_k3: red_color = PITCH_GREEN if sf["redundancy"] < 0.5 else GOLD st.markdown(kpi_card("REDUNDANCY", f"{sf['redundancy']:.2f}", "0=diverse 1=overlapping", red_color), unsafe_allow_html=True) insight("Set Transformer captures non-linear synergies. Two playmakers overlapping = " "worth less than sum of parts. Redundancy score warns you before overpaying.") with tab4: section("📊 Phase 4 — Hierarchical Bayesian Pooling") if "error" in results.get("bayesian", {}): st.warning(f"Bayesian model error: {results['bayesian']['error']}") else: b = results["bayesian"] fig = _phase_chart_bayesian(b["mean"], b["std"], b["reliability"]) st.plotly_chart(fig, width="stretch") insight("Hierarchical model shrinks rookies toward role mean. Low reliability = high uncertainty. " "Don't pay premium prices for players with <10 career games.") section("🎯 Phase 4 — Conformal Prediction Bands") if "error" in results.get("conformal", {}): st.warning(f"Conformal predictor error: {results['conformal']['error']}") else: cp_r = results["conformal"] fig = _phase_chart_conformal(cp_r["predictions"], cp_r["lowers"], cp_r["uppers"], cp_r["coverage"]) st.plotly_chart(fig, width="stretch") insight(f"Conformal bands: {cp_r['coverage']*100:.0f}% coverage on test set. " "Model-agnostic calibrated intervals — no distributional assumptions needed.") with tab5: section("🔗 Phase 5 — Graph Attention Network: Player Chemistry") if "error" in results.get("chemistry", {}): st.warning(f"Chemistry model error: {results['chemistry']['error']}") else: fig = _phase_chart_chemistry(results["chemistry"]["bonus_matrix"]) st.plotly_chart(fig, width="stretch") insight("Pairwise chemistry bonuses from pass networks, assists, and crosses. " "A strong winger→striker edge boosts both players. Target linked pairs in the auction.") section("🔥 Phase 5 — Hawkes Process: Form Momentum") if "error" in results.get("hawkes", {}): st.warning(f"Form model error: {results['hawkes']['error']}") else: fig = _phase_chart_hawkes(results["hawkes"]["statuses"]) st.plotly_chart(fig, width="stretch") insight("Self-exciting process detects HOT/COLD streaks. A HOT player on a 5-game scoring run " "has temporarily elevated projection. Exploit recency bias in your opponents.") with tab6: section("🔬 Phase 6 — Causal Forest: Transfer Effect Analysis") if "error" in results.get("causal", {}): st.warning(f"Causal forest error: {results['causal']['error']}") else: cf = results["causal"] k_c1, k_c2, k_c3 = st.columns(3) with k_c1: color = PITCH_GREEN if cf["ate"] > 0 else RED st.markdown(kpi_card("AVG TREATMENT EFFECT", f"{cf['ate']:+.3f}", "adding a player", color), unsafe_allow_html=True) with k_c2: st.markdown(kpi_card("CI LOWER", f"{cf['ate_lower']:+.3f}", "95% confidence", TEXT_SECONDARY), unsafe_allow_html=True) with k_c3: st.markdown(kpi_card("CI UPPER", f"{cf['ate_upper']:+.3f}", "95% confidence", TEXT_SECONDARY), unsafe_allow_html=True) insight("Causal forest estimates the TRUE effect of a roster change, controlling for confounders. " "Adding a top midfielder doesn't help if you already have 5 strong mids — " "the diminishing returns are captured in the CATE.") # ── Methodology ── st.divider() with st.expander("⚙️ Methodology — 10 ML Models Explained"): st.markdown(""" | # | Model | Type | What It Does | |---|-------|------|-------------| | 1 | QuantileEnsemble | LightGBM quantile | P10/P50/P90 predictions → risk-aware bidding | | 2 | MinutesSurvivalModel | Weibull AFT | Full minutes distribution, starter probability | | 3 | BanditAuctionSolver | Thompson Sampling | Optimal bid per round balancing explore/exploit | | 4 | OpponentBidModel | LightGBM regressor | Predicts competitor max bid per player | | 5 | BudgetOptimizer | Bayesian Optimization | Optimal budget split across GK/DEF/MID/FWD | | 6 | PlayerChemistryGAT | Graph Attention Network | Player synergy bonuses from pass networks | | 7 | PlayerFormModel | Hawkes Process | Momentum/decorrelation hot streak detection | | 8 | BayesianPlayerModel | Hierarchical Bayes | Rookie uncertainty via role-level shrinkage | | 9 | ConformalPredictor | Conformal inference | Calibrated prediction bands, model-agnostic | | 10 | SetTransformer | Transformer on sets | Team composition value beyond sum-of-parts | | 11 | RLAuctionPolicy | Double DQN | RL agent for sequential auction strategy | | 12 | TransferCausalModel | Causal Forest | Causal effect of transfer on team performance | """, unsafe_allow_html=False) st.divider() st.caption("Dev Preview v1.0 — all models running on synthetic data. Connect real pipeline for production use.") if __name__ == "__main__": run()