Files
fantabeto/dashboard/pages/06_dev_preview.py
T
ramseshk 00dc7f2bcb feat: add dev preview dashboard showcasing all 12 new ML models
- New page 06_dev_preview.py: interactive ML model showcase
- All 12 models initialized from synthetic data with live charts
- Live Auction Simulator: bandit + opponent model + budget optimizer
- Tabbed UI: 6 tabs, one per phase
- Resilient warehouse: returns typed empty DataFrames when no data
- Dev Preview set as default landing page for demo mode
- Live at http://localhost:8507
2026-08-12 11:34:00 +08:00

876 lines
42 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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"<br><span style='color:{TEXT_SECONDARY};font-size:12px;'>Opponents: 3 remaining, avg budget ~{sim_budget}cr</span>",
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()