2026Projections / app.py
Syntrex's picture
Create app.py
e3b6552 verified
import gradio as gr
import pandas as pd
import numpy as np
import xgboost as xgb
import plotly.graph_objects as go
from scipy.stats import gaussian_kde
from pybaseball import pitching_stats, batting_stats, cache
cache.enable()
# --- THEME: DEEP STEALTH ---
STEALTH_CSS = """
.gradio-container { background-color: #0d0d0d !important; color: #e0e0e0 !important; font-family: 'Segoe UI', sans-serif !important; }
.player-header { background: #1a1a1a; border-bottom: 3px solid #D50032; padding: 25px; border-radius: 12px 12px 0 0; text-align: center; }
.stat-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(130px, 1fr)); gap: 12px; padding: 20px; background: #1a1a1a; border-radius: 0 0 12px 12px; }
.stat-card { background: #252525; border: 1px solid #333; border-radius: 8px; padding: 15px; text-align: center; }
.metric-name { font-size: 0.7rem; font-weight: 800; color: #888; text-transform: uppercase; letter-spacing: 1px; }
.metric-value { font-size: 1.8rem; font-weight: 900; color: #ffffff; margin: 4px 0; }
.percentile-bar { height: 6px; background: #333; border-radius: 3px; overflow: hidden; margin-top: 8px; }
.percentile-fill { height: 100%; border-radius: 3px; }
"""
class ProIntelligenceEngine:
def __init__(self):
print("📥 Initializing Hardened Engine...")
p_raw = pitching_stats(2015, 2025, ind=1)
h_raw = batting_stats(2015, 2025, ind=1)
# 1. Clean Hitters: Manually calculate to avoid pybaseball NaNs
h_raw['AVG'] = h_raw['H'] / h_raw['AB'].replace(0, np.nan)
h_raw['OBP'] = (h_raw['H'] + h_raw['BB'] + h_raw['HBP']) / (h_raw['PA'].replace(0, np.nan))
h_raw['SLG'] = (h_raw['H'] + h_raw['2B'] + 2*h_raw['3B'] + 3*h_raw['HR']) / h_raw['AB'].replace(0, np.nan)
h_raw['OPS'] = h_raw['OBP'] + h_raw['SLG']
h_raw['ISO'] = h_raw['SLG'] - h_raw['AVG']
self.p_metrics = ["ERA", "SO", "WHIP", "FIP"]
self.h_metrics = ["PA", "H", "AVG", "HR", "OBP", "ISO", "SLG", "OPS", "SB"]
self.p_feats = ['Age', 'IP', 'SO', 'BB']
self.h_feats = ['Age', 'PA', 'H', 'HR'] # Added HR as a feature for better power-stat training
# Final cleanup
self.p_db = p_raw[self.p_feats + self.p_metrics + ['Name', 'Season']].dropna()
self.h_db = h_raw[self.h_feats + self.h_metrics + ['Name', 'Season']].dropna()
self.models = {}
self._train_models()
def _train_models(self):
for m in self.p_metrics:
self.models[f"P_{m}"] = xgb.train({'objective': 'reg:squarederror'}, xgb.DMatrix(self.p_db[self.p_feats].values, label=self.p_db[m].values))
for m in self.h_metrics:
self.models[f"H_{m}"] = xgb.train({'objective': 'reg:squarederror'}, xgb.DMatrix(self.h_db[self.h_feats].values, label=self.h_db[m].values))
def run_sim(self, name, role, metric, n=1500):
db = self.p_db if role == "Pitcher" else self.h_db
feats = self.p_feats if role == "Pitcher" else self.h_feats
player = db[db['Name'] == name].sort_values('Season').iloc[-1:]
if player.empty: return None
base = player[feats].values[0].astype(float)
base[0] += 1
sim_inputs = base * np.random.normal(1.0, 0.05, (n, len(base)))
return self.models[f"{role[0]}_{metric}"].predict(xgb.DMatrix(sim_inputs))
engine = ProIntelligenceEngine()
def refresh_ui(role, name):
if not name: return "", go.Figure()
metrics = engine.p_metrics if role == "Pitcher" else engine.h_metrics
html = f"<div class='player-header'><h1>{name.upper()}</h1><p style='color:#777;'>2026 PRO PERFORMANCE REPORT</p></div>"
html += "<div class='stat-grid'>"
for m in metrics:
sims = engine.run_sim(name, role, m)
if sims is None: continue
mu = np.mean(sims)
# Pitchers = 2 decimals | Hitters = 3 decimals | Counts = Int
if role == "Pitcher":
val_str = f"{mu:.2f}" if m in ["ERA", "WHIP", "FIP"] else f"{int(mu)}"
else:
if m in ["AVG", "OBP", "SLG", "OPS", "ISO"]:
val_str = f"{mu:.3f}".replace("0.", ".")
else:
val_str = f"{int(mu)}"
db = engine.p_db if role == "Pitcher" else engine.h_db
pct = min(max((mu / (np.mean(db[m]) * 1.8)) * 100, 5), 95)
color = "#ff4b4b" if pct > 70 else "#4b91ff" if pct < 30 else "#fbc531"
html += f"""
<div class='stat-card'>
<div class='metric-name'>{m}</div>
<div class='metric-value'>{val_str}</div>
<div class='percentile-bar'><div class='percentile-fill' style='width:{pct}%; background:{color}; box-shadow: 0 0 8px {color};'></div></div>
</div>"""
html += "</div>"
# Distribution Plot
primary_sims = engine.run_sim(name, role, metrics[0])
low, high = np.percentile(primary_sims, [5, 95])
kde = gaussian_kde(primary_sims)
x = np.linspace(min(primary_sims), max(primary_sims), 200)
fig = go.Figure()
fig.add_trace(go.Scatter(x=x, y=kde(x), fill='tozeroy', line_color='#1A73E8', fillcolor='rgba(26, 115, 232, 0.2)'))
fig.add_vrect(x0=low, x1=high, fillcolor="#fff", opacity=0.05, layer="below")
fig.update_layout(title=dict(text=f"2026 {metrics[0]} STRESS TEST", font=dict(color="#fff")), paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', height=280, margin=dict(l=10,r=10,t=50,b=10), xaxis=dict(showgrid=False, tickfont=dict(color="#444")), yaxis=dict(visible=False))
return html, fig
with gr.Blocks(css=STEALTH_CSS) as demo:
with gr.Row():
role_in = gr.Radio(["Pitcher", "Hitter"], value="Pitcher", label="Category")
player_in = gr.Dropdown(choices=sorted(engine.p_db['Name'].unique().tolist()), label="Search")
out_html = gr.HTML()
out_plot = gr.Plot()
role_in.change(lambda r: gr.update(choices=sorted(engine.p_db['Name'].unique().tolist() if r=="Pitcher" else engine.h_db['Name'].unique().tolist())), inputs=role_in, outputs=player_in)
player_in.change(refresh_ui, inputs=[role_in, player_in], outputs=[out_html, out_plot])
demo.launch()