ap2.0 / app.py
spjasper's picture
Update app.py
0f68c43 verified
import gradio as gr
import pandas as pd
import requests
import numpy as np
from datetime import datetime
import json
from collections import Counter
import os
from io import StringIO
# Configuración de OpenAI
OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY', '')
# Mapeo de códigos de liga
LEAGUE_CODES = {
'E0': 'Premier League', 'E1': 'Championship', 'E2': 'League One', 'E3': 'League Two',
'SP1': 'La Liga', 'SP2': 'La Liga 2', 'I1': 'Serie A', 'I2': 'Serie B',
'D1': 'Bundesliga', 'D2': 'Bundesliga 2', 'F1': 'Ligue 1', 'F2': 'Ligue 2',
'N1': 'Eredivisie', 'B1': 'Jupiler League', 'P1': 'Liga Portugal',
'T1': 'Super Lig', 'G1': 'Super League'
}
class FootballAnalyzer:
def __init__(self):
self.fixtures_df = None
self.league_data = None
self.selected_match = None
self.analysis_data = None
def download_fixtures(self):
"""Descarga el archivo fixtures.csv"""
try:
url = 'https://www.football-data.co.uk/fixtures.csv'
response = requests.get(url, timeout=10)
response.raise_for_status()
with open('fixtures.csv', 'wb') as f:
f.write(response.content)
self.fixtures_df = pd.read_csv('fixtures.csv')
matches = []
for idx, row in self.fixtures_df.iterrows():
league_name = LEAGUE_CODES.get(row['Div'], row['Div'])
match_str = f"{league_name} | {row['Date']} {row['Time']} | {row['HomeTeam']} vs {row['AwayTeam']}"
matches.append(match_str)
return gr.update(choices=matches, value=None), "✅ Fixtures descargados exitosamente"
except Exception as e:
return gr.update(choices=[]), f"❌ Error: {str(e)}"
def show_bet365_odds(self, selected_match_str):
"""Muestra las cuotas de Bet365"""
if not selected_match_str or self.fixtures_df is None:
return "Selecciona un partido primero"
try:
parts = selected_match_str.split('|')
teams = parts[2].strip().split(' vs ')
home_team = teams[0].strip()
away_team = teams[1].strip()
match_row = self.fixtures_df[
(self.fixtures_df['HomeTeam'] == home_team) &
(self.fixtures_df['AwayTeam'] == away_team)
].iloc[0]
self.selected_match = match_row
odds_info = f"""
### 📊 Cuotas Bet365
**{home_team} vs {away_team}**
- 🏠 **Local ({home_team}):** {match_row.get('B365H', 'N/A')}
- 🤝 **Empate:** {match_row.get('B365D', 'N/A')}
- ✈️ **Visitante ({away_team}):** {match_row.get('B365A', 'N/A')}
**Liga:** {LEAGUE_CODES.get(match_row['Div'], match_row['Div'])}
**Fecha:** {match_row['Date']} {match_row['Time']}
"""
return odds_info
except Exception as e:
return f"❌ Error: {str(e)}"
def download_league_data(self, selected_match_str):
"""Descarga datos históricos de la liga"""
if not selected_match_str or self.selected_match is None:
return "Primero selecciona un partido y visualiza las cuotas", None, None
try:
league_code = self.selected_match['Div']
url = f'https://www.football-data.co.uk/mmz4281/2526/{league_code}.csv'
response = requests.get(url, timeout=10)
response.raise_for_status()
filename = f'league_{league_code}.csv'
with open(filename, 'wb') as f:
f.write(response.content)
self.league_data = pd.read_csv(filename)
message = f"✅ Datos descargados: {len(self.league_data)} partidos"
# Generar análisis
analysis_md, tables_html = self.analyze_teams()
return message, analysis_md, tables_html
except Exception as e:
return f"❌ Error: {str(e)}", None, None
def calculate_statistics(self, values):
"""Calcula todas las estadísticas relevantes"""
if len(values) == 0:
return {
'media': 0, 'mediana': 0, 'moda': 0, 'volatilidad': 0,
'desv_std': 0, 'min': 0, 'max': 0, 'total': 0,
'p25': 0, 'p75': 0, 'coef_var': 0
}
values_clean = [v for v in values if pd.notna(v) and str(v).strip() != '']
if len(values_clean) == 0:
return {
'media': 0, 'mediana': 0, 'moda': 0, 'volatilidad': 0,
'desv_std': 0, 'min': 0, 'max': 0, 'total': 0,
'p25': 0, 'p75': 0, 'coef_var': 0
}
values_clean = [float(v) for v in values_clean]
media = np.mean(values_clean)
mediana = np.median(values_clean)
counter = Counter(values_clean)
moda = counter.most_common(1)[0][0] if counter else 0
desv_std = np.std(values_clean)
volatilidad = (desv_std / media * 100) if media != 0 else 0
p25 = np.percentile(values_clean, 25)
p75 = np.percentile(values_clean, 75)
return {
'media': round(media, 2),
'mediana': round(mediana, 2),
'moda': round(moda, 2),
'volatilidad': round(volatilidad, 2),
'desv_std': round(desv_std, 2),
'min': round(min(values_clean), 2),
'max': round(max(values_clean), 2),
'total': round(sum(values_clean), 2),
'p25': round(p25, 2),
'p75': round(p75, 2),
'coef_var': round(volatilidad, 2)
}
def analyze_team_comprehensive(self, team_data, team_name, is_home):
"""Análisis comprehensivo de un equipo con TODAS las categorías"""
if len(team_data) == 0:
return None
# Filtrar partidos como local o visitante
if is_home:
team_matches = team_data[team_data['HomeTeam'] == team_name].copy()
else:
team_matches = team_data[team_data['AwayTeam'] == team_name].copy()
if len(team_matches) == 0:
return None
# Obtener últimos 5 partidos
last_5 = team_matches.tail(5).copy()
# Definir todas las categorías a analizar
categories = {}
# GOLES
if is_home:
categories['Goles Favor'] = {
'season': team_matches['FTHG'].astype(float),
'last5': last_5['FTHG'].astype(float)
}
categories['Goles Contra'] = {
'season': team_matches['FTAG'].astype(float),
'last5': last_5['FTAG'].astype(float)
}
categories['Goles Medio Tiempo Favor'] = {
'season': team_matches['HTHG'].astype(float) if 'HTHG' in team_matches.columns else pd.Series([0]),
'last5': last_5['HTHG'].astype(float) if 'HTHG' in last_5.columns else pd.Series([0])
}
categories['Goles Medio Tiempo Contra'] = {
'season': team_matches['HTAG'].astype(float) if 'HTAG' in team_matches.columns else pd.Series([0]),
'last5': last_5['HTAG'].astype(float) if 'HTAG' in last_5.columns else pd.Series([0])
}
else:
categories['Goles Favor'] = {
'season': team_matches['FTAG'].astype(float),
'last5': last_5['FTAG'].astype(float)
}
categories['Goles Contra'] = {
'season': team_matches['FTHG'].astype(float),
'last5': last_5['FTHG'].astype(float)
}
categories['Goles Medio Tiempo Favor'] = {
'season': team_matches['HTAG'].astype(float) if 'HTAG' in team_matches.columns else pd.Series([0]),
'last5': last_5['HTAG'].astype(float) if 'HTAG' in last_5.columns else pd.Series([0])
}
categories['Goles Medio Tiempo Contra'] = {
'season': team_matches['HTHG'].astype(float) if 'HTHG' in team_matches.columns else pd.Series([0]),
'last5': last_5['HTHG'].astype(float) if 'HTHG' in last_5.columns else pd.Series([0])
}
# TIROS
if is_home:
categories['Tiros Totales'] = {
'season': team_matches['HS'].astype(float) if 'HS' in team_matches.columns else pd.Series([0]),
'last5': last_5['HS'].astype(float) if 'HS' in last_5.columns else pd.Series([0])
}
categories['Tiros al Arco'] = {
'season': team_matches['HST'].astype(float) if 'HST' in team_matches.columns else pd.Series([0]),
'last5': last_5['HST'].astype(float) if 'HST' in last_5.columns else pd.Series([0])
}
categories['Tiros Contra'] = {
'season': team_matches['AS'].astype(float) if 'AS' in team_matches.columns else pd.Series([0]),
'last5': last_5['AS'].astype(float) if 'AS' in last_5.columns else pd.Series([0])
}
categories['Tiros al Arco Contra'] = {
'season': team_matches['AST'].astype(float) if 'AST' in team_matches.columns else pd.Series([0]),
'last5': last_5['AST'].astype(float) if 'AST' in last_5.columns else pd.Series([0])
}
else:
categories['Tiros Totales'] = {
'season': team_matches['AS'].astype(float) if 'AS' in team_matches.columns else pd.Series([0]),
'last5': last_5['AS'].astype(float) if 'AS' in last_5.columns else pd.Series([0])
}
categories['Tiros al Arco'] = {
'season': team_matches['AST'].astype(float) if 'AST' in team_matches.columns else pd.Series([0]),
'last5': last_5['AST'].astype(float) if 'AST' in last_5.columns else pd.Series([0])
}
categories['Tiros Contra'] = {
'season': team_matches['HS'].astype(float) if 'HS' in team_matches.columns else pd.Series([0]),
'last5': last_5['HS'].astype(float) if 'HS' in last_5.columns else pd.Series([0])
}
categories['Tiros al Arco Contra'] = {
'season': team_matches['HST'].astype(float) if 'HST' in team_matches.columns else pd.Series([0]),
'last5': last_5['HST'].astype(float) if 'HST' in last_5.columns else pd.Series([0])
}
# CORNERS
if is_home:
categories['Corners Favor'] = {
'season': team_matches['HC'].astype(float) if 'HC' in team_matches.columns else pd.Series([0]),
'last5': last_5['HC'].astype(float) if 'HC' in last_5.columns else pd.Series([0])
}
categories['Corners Contra'] = {
'season': team_matches['AC'].astype(float) if 'AC' in team_matches.columns else pd.Series([0]),
'last5': last_5['AC'].astype(float) if 'AC' in last_5.columns else pd.Series([0])
}
else:
categories['Corners Favor'] = {
'season': team_matches['AC'].astype(float) if 'AC' in team_matches.columns else pd.Series([0]),
'last5': last_5['AC'].astype(float) if 'AC' in last_5.columns else pd.Series([0])
}
categories['Corners Contra'] = {
'season': team_matches['HC'].astype(float) if 'HC' in team_matches.columns else pd.Series([0]),
'last5': last_5['HC'].astype(float) if 'HC' in last_5.columns else pd.Series([0])
}
# FALTAS
if is_home:
categories['Faltas Cometidas'] = {
'season': team_matches['HF'].astype(float) if 'HF' in team_matches.columns else pd.Series([0]),
'last5': last_5['HF'].astype(float) if 'HF' in last_5.columns else pd.Series([0])
}
categories['Faltas Recibidas'] = {
'season': team_matches['AF'].astype(float) if 'AF' in team_matches.columns else pd.Series([0]),
'last5': last_5['AF'].astype(float) if 'AF' in last_5.columns else pd.Series([0])
}
else:
categories['Faltas Cometidas'] = {
'season': team_matches['AF'].astype(float) if 'AF' in team_matches.columns else pd.Series([0]),
'last5': last_5['AF'].astype(float) if 'AF' in last_5.columns else pd.Series([0])
}
categories['Faltas Recibidas'] = {
'season': team_matches['HF'].astype(float) if 'HF' in team_matches.columns else pd.Series([0]),
'last5': last_5['HF'].astype(float) if 'HF' in last_5.columns else pd.Series([0])
}
# TARJETAS
if is_home:
categories['Tarjetas Amarillas'] = {
'season': team_matches['HY'].astype(float) if 'HY' in team_matches.columns else pd.Series([0]),
'last5': last_5['HY'].astype(float) if 'HY' in last_5.columns else pd.Series([0])
}
categories['Tarjetas Rojas'] = {
'season': team_matches['HR'].astype(float) if 'HR' in team_matches.columns else pd.Series([0]),
'last5': last_5['HR'].astype(float) if 'HR' in last_5.columns else pd.Series([0])
}
categories['Tarjetas Amarillas Contra'] = {
'season': team_matches['AY'].astype(float) if 'AY' in team_matches.columns else pd.Series([0]),
'last5': last_5['AY'].astype(float) if 'AY' in last_5.columns else pd.Series([0])
}
categories['Tarjetas Rojas Contra'] = {
'season': team_matches['AR'].astype(float) if 'AR' in team_matches.columns else pd.Series([0]),
'last5': last_5['AR'].astype(float) if 'AR' in last_5.columns else pd.Series([0])
}
else:
categories['Tarjetas Amarillas'] = {
'season': team_matches['AY'].astype(float) if 'AY' in team_matches.columns else pd.Series([0]),
'last5': last_5['AY'].astype(float) if 'AY' in last_5.columns else pd.Series([0])
}
categories['Tarjetas Rojas'] = {
'season': team_matches['AR'].astype(float) if 'AR' in team_matches.columns else pd.Series([0]),
'last5': last_5['AR'].astype(float) if 'AR' in last_5.columns else pd.Series([0])
}
categories['Tarjetas Amarillas Contra'] = {
'season': team_matches['HY'].astype(float) if 'HY' in team_matches.columns else pd.Series([0]),
'last5': last_5['HY'].astype(float) if 'HY' in last_5.columns else pd.Series([0])
}
categories['Tarjetas Rojas Contra'] = {
'season': team_matches['HR'].astype(float) if 'HR' in team_matches.columns else pd.Series([0]),
'last5': last_5['HR'].astype(float) if 'HR' in last_5.columns else pd.Series([0])
}
# Calcular estadísticas para cada categoría
stats_dict = {}
for cat_name, cat_data in categories.items():
stats_dict[cat_name] = {
'temporada': self.calculate_statistics(cat_data['season'].tolist()),
'ultimos_5': self.calculate_statistics(cat_data['last5'].tolist())
}
# Calcular resultados (W/D/L)
results = []
for _, match in team_matches.iterrows():
if is_home:
if match['FTHG'] > match['FTAG']:
results.append('W')
elif match['FTHG'] < match['FTAG']:
results.append('L')
else:
results.append('D')
else:
if match['FTAG'] > match['FTHG']:
results.append('W')
elif match['FTAG'] < match['FTHG']:
results.append('L')
else:
results.append('D')
last_5_results = results[-5:] if len(results) >= 5 else results
return {
'partidos_totales': len(team_matches),
'victorias': results.count('W'),
'empates': results.count('D'),
'derrotas': results.count('L'),
'forma_reciente': ''.join(last_5_results),
'estadisticas': stats_dict,
'partidos_last5': len(last_5)
}
def create_stats_tables(self, home_analysis, away_analysis, home_team, away_team):
"""Crea tablas HTML con todas las estadísticas"""
html = f"""
<style>
.stats-container {{ margin: 20px 0; }}
.stats-table {{
width: 100%;
border-collapse: collapse;
margin: 15px 0;
font-size: 13px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}}
.stats-table th {{
background: #2c3e50;
color: white;
padding: 12px 8px;
text-align: left;
font-weight: 600;
}}
.stats-table td {{
padding: 10px 8px;
border-bottom: 1px solid #ddd;
}}
.stats-table tr:hover {{ background: #f5f5f5; }}
.team-header {{
background: #34495e;
color: white;
padding: 15px;
margin: 20px 0 10px 0;
border-radius: 5px;
font-size: 18px;
font-weight: bold;
}}
.section-header {{
background: #3498db;
color: white;
padding: 10px;
margin: 15px 0 5px 0;
border-radius: 3px;
font-weight: bold;
}}
.metric {{ font-weight: 600; }}
.volatility-low {{ color: #27ae60; }}
.volatility-medium {{ color: #f39c12; }}
.volatility-high {{ color: #e74c3c; }}
</style>
"""
def format_volatility(vol):
if vol < 30:
return f'<span class="volatility-low">{vol}%</span>'
elif vol < 60:
return f'<span class="volatility-medium">{vol}%</span>'
else:
return f'<span class="volatility-high">{vol}%</span>'
# Tabla para cada equipo
for team_name, analysis, is_home in [(home_team, home_analysis, True), (away_team, away_analysis, False)]:
position = "Local" if is_home else "Visitante"
html += f'<div class="team-header">🏟️ {team_name} ({position})</div>'
html += f'<p><strong>Récord:</strong> {analysis["victorias"]}V - {analysis["empates"]}E - {analysis["derrotas"]}D | '
html += f'<strong>Forma Reciente:</strong> {analysis["forma_reciente"]}</p>'
# Tabla de todas las categorías
html += '<table class="stats-table">'
html += '''
<tr>
<th>Categoría</th>
<th>Período</th>
<th>Media</th>
<th>Mediana</th>
<th>Moda</th>
<th>Min-Max</th>
<th>Volatilidad</th>
<th>Desv.Std</th>
<th>Total</th>
</tr>
'''
for cat_name, cat_stats in analysis['estadisticas'].items():
# Fila temporada completa
s = cat_stats['temporada']
html += f'''
<tr>
<td class="metric" rowspan="2">{cat_name}</td>
<td><strong>Temporada ({analysis["partidos_totales"]} PJ)</strong></td>
<td>{s["media"]}</td>
<td>{s["mediana"]}</td>
<td>{s["moda"]}</td>
<td>{s["min"]} - {s["max"]}</td>
<td>{format_volatility(s["volatilidad"])}</td>
<td>{s["desv_std"]}</td>
<td>{s["total"]}</td>
</tr>
'''
# Fila últimos 5
l5 = cat_stats['ultimos_5']
html += f'''
<tr>
<td><strong>Últimos 5</strong></td>
<td>{l5["media"]}</td>
<td>{l5["mediana"]}</td>
<td>{l5["moda"]}</td>
<td>{l5["min"]} - {l5["max"]}</td>
<td>{format_volatility(l5["volatilidad"])}</td>
<td>{l5["desv_std"]}</td>
<td>{l5["total"]}</td>
</tr>
'''
html += '</table>'
return html
def analyze_teams(self):
"""Realiza análisis completo y genera reporte + tablas"""
if self.league_data is None or self.selected_match is None:
return "No hay datos para analizar", ""
home_team = self.selected_match['HomeTeam']
away_team = self.selected_match['AwayTeam']
# Filtrar datos
home_data = self.league_data[
(self.league_data['HomeTeam'] == home_team) |
(self.league_data['AwayTeam'] == home_team)
]
away_data = self.league_data[
(self.league_data['HomeTeam'] == away_team) |
(self.league_data['AwayTeam'] == away_team)
]
# Análisis completo
home_analysis = self.analyze_team_comprehensive(home_data, home_team, True)
away_analysis = self.analyze_team_comprehensive(away_data, away_team, False)
if home_analysis is None or away_analysis is None:
return "No hay suficientes datos históricos", ""
# Guardar para OpenAI
self.analysis_data = {
'home_team': home_team,
'away_team': away_team,
'home_analysis': home_analysis,
'away_analysis': away_analysis,
'bet365_odds': {
'home': self.selected_match.get('B365H'),
'draw': self.selected_match.get('B365D'),
'away': self.selected_match.get('B365A')
}
}
# Generar reporte markdown
report = f"""
# 📊 ANÁLISIS ESTADÍSTICO COMPLETO
## ⚽ {home_team} (Local) vs {away_team} (Visitante)
### Resumen General
**{home_team} (Local)**
- Partidos: {home_analysis['partidos_totales']}
- Récord: {home_analysis['victorias']}V - {home_analysis['empates']}E - {home_analysis['derrotas']}D
- Forma reciente: {home_analysis['forma_reciente']}
**{away_team} (Visitante)**
- Partidos: {away_analysis['partidos_totales']}
- Récord: {away_analysis['victorias']}V - {away_analysis['empates']}E - {away_analysis['derrotas']}D
- Forma reciente: {away_analysis['forma_reciente']}
---
### 📈 Interpretación de Volatilidad
- **< 30%**: Muy consistente (predecible)
- **30-60%**: Moderadamente consistente
- **> 60%**: Muy inconsistente (impredecible)
Las tablas detalladas se muestran a continuación con todas las categorías analizadas.
"""
# Generar tablas HTML
tables_html = self.create_stats_tables(home_analysis, away_analysis, home_team, away_team)
return report, tables_html
def get_ai_prediction(self, api_key):
"""Obtiene predicción profesional usando el prompt de apostador cuantitativo"""
if not hasattr(self, 'analysis_data') or self.analysis_data is None:
return "Primero debes descargar y analizar los datos del partido"
if not api_key:
return "⚠️ Por favor ingresa tu API Key de OpenAI"
try:
# Preparar datos estructurados para el prompt
home = self.analysis_data['home_analysis']
away = self.analysis_data['away_analysis']
# Construir el prompt del apostador profesional
prompt = f"""Actúa como un apostador profesional de fútbol y analista cuantitativo senior, especializado en valor esperado (EV), modelos probabilísticos (Poisson), simulación Monte Carlo, criterio de Kelly completo y dinámica de mercados de apuestas.
Tu objetivo es maximizar el crecimiento logarítmico del capital, identificando ineficiencias reales del mercado, validándolas con movimiento de líneas en Internet, y descartando apuestas con riesgo de ruina inaceptable, incluso si tienen EV positivo.
## PARTIDO A ANALIZAR
**{self.analysis_data['home_team']} (Local) vs {self.analysis_data['away_team']} (Visitante)**
### CUOTAS BET365 (Línea actual)
- Local (1): {self.analysis_data['bet365_odds']['home']}
- Empate (X): {self.analysis_data['bet365_odds']['draw']}
- Visitante (2): {self.analysis_data['bet365_odds']['away']}
## DATOS ESTADÍSTICOS COMPLETOS
### {self.analysis_data['home_team']} (LOCAL)
**Rendimiento General:**
- Partidos como local: {home['partidos_totales']}
- Récord: {home['victorias']}V-{home['empates']}E-{home['derrotas']}D
- Forma reciente (últimos 5): {home['forma_reciente']}
**Goles:**
- Media temporada: {home['estadisticas']['Goles Favor']['temporada']['media']} goles/partido
- Media últimos 5: {home['estadisticas']['Goles Favor']['ultimos_5']['media']} goles/partido
- Mediana temporada: {home['estadisticas']['Goles Favor']['temporada']['mediana']}
- Moda temporada: {home['estadisticas']['Goles Favor']['temporada']['moda']}
- Volatilidad temporada: {home['estadisticas']['Goles Favor']['temporada']['volatilidad']}%
- Volatilidad últimos 5: {home['estadisticas']['Goles Favor']['ultimos_5']['volatilidad']}%
**Goles en Contra:**
- Media temporada: {home['estadisticas']['Goles Contra']['temporada']['media']} goles/partido
- Media últimos 5: {home['estadisticas']['Goles Contra']['ultimos_5']['media']} goles/partido
- Volatilidad temporada: {home['estadisticas']['Goles Contra']['temporada']['volatilidad']}%
**Tiros al Arco:**
- Media temporada: {home['estadisticas']['Tiros al Arco']['temporada']['media']}
- Media últimos 5: {home['estadisticas']['Tiros al Arco']['ultimos_5']['media']}
- Volatilidad: {home['estadisticas']['Tiros al Arco']['temporada']['volatilidad']}%
**Corners:**
- Media temporada: {home['estadisticas']['Corners Favor']['temporada']['media']}
- Media últimos 5: {home['estadisticas']['Corners Favor']['ultimos_5']['media']}
- Volatilidad: {home['estadisticas']['Corners Favor']['temporada']['volatilidad']}%
**Disciplina:**
- Tarjetas amarillas (media): {home['estadisticas']['Tarjetas Amarillas']['temporada']['media']}
- Faltas (media): {home['estadisticas']['Faltas Cometidas']['temporada']['media']}
### {self.analysis_data['away_team']} (VISITANTE)
**Rendimiento General:**
- Partidos como visitante: {away['partidos_totales']}
- Récord: {away['victorias']}V-{away['empates']}E-{away['derrotas']}D
- Forma reciente (últimos 5): {away['forma_reciente']}
**Goles:**
- Media temporada: {away['estadisticas']['Goles Favor']['temporada']['media']} goles/partido
- Media últimos 5: {away['estadisticas']['Goles Favor']['ultimos_5']['media']} goles/partido
- Mediana temporada: {away['estadisticas']['Goles Favor']['temporada']['mediana']}
- Moda temporada: {away['estadisticas']['Goles Favor']['temporada']['moda']}
- Volatilidad temporada: {away['estadisticas']['Goles Favor']['temporada']['volatilidad']}%
- Volatilidad últimos 5: {away['estadisticas']['Goles Favor']['ultimos_5']['volatilidad']}%
**Goles en Contra:**
- Media temporada: {away['estadisticas']['Goles Contra']['temporada']['media']} goles/partido
- Media últimos 5: {away['estadisticas']['Goles Contra']['ultimos_5']['media']} goles/partido
- Volatilidad temporada: {away['estadisticas']['Goles Contra']['temporada']['volatilidad']}%
**Tiros al Arco:**
- Media temporada: {away['estadisticas']['Tiros al Arco']['temporada']['media']}
- Media últimos 5: {away['estadisticas']['Tiros al Arco']['ultimos_5']['media']}
- Volatilidad: {away['estadisticas']['Tiros al Arco']['temporada']['volatilidad']}%
**Corners:**
- Media temporada: {away['estadisticas']['Corners Favor']['temporada']['media']}
- Media últimos 5: {away['estadisticas']['Corners Favor']['ultimos_5']['media']}
- Volatilidad: {away['estadisticas']['Corners Favor']['temporada']['volatilidad']}%
**Disciplina:**
- Tarjetas amarillas (media): {away['estadisticas']['Tarjetas Amarillas']['temporada']['media']}
- Faltas (media): {away['estadisticas']['Faltas Cometidas']['temporada']['media']}
## INSTRUCCIONES DE ANÁLISIS
Debes analizar AL MENOS 5 categorías de mercado diferentes de forma independiente:
1. **Goles**: Over/Under, Ambos Marcan, Asian Handicap
2. **Resultado**: 1X2, Draw No Bet, Double Chance
3. **Resultado Medio Tiempo**: HT 1X2, HT Asian Handicap
4. **Corners**: Totales, Por equipo, Hándicap asiático
5. **Mercados alternativos**: Líneas asiáticas secundarias
Para CADA categoría que analices, DEBES proporcionar:
### 1. Modelado Probabilístico (Poisson)
- Calcula λ (lambda) local y visitante basado en las medias
- Deriva probabilidades para cada resultado
- Considera la volatilidad para ajustar el modelo
### 2. Conversión de Cuotas y Valor Esperado
- Probabilidad implícita de las cuotas Bet365
- Probabilidad real estimada (tu modelo)
- **EV = (P_real × Cuota) - 1**
- Solo continuar si **EV > 0**
### 3. Simulación Monte Carlo (10,000+ iteraciones)
- P&L esperado
- Varianza y desviación estándar
- Percentiles (5%, 25%, 50%, 75%, 95%)
- Probabilidad de pérdida
### 4. Stake con Kelly Completo
- **f* = [(b × p) - q] / b**
- b = cuota - 1
- p = probabilidad real
- q = 1 - p
- % de banca a apostar
- Crecimiento esperado
- Riesgo de ruina
### 5. Formato de Salida Estricto para CADA apuesta recomendada:
```
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
MERCADO: [Nombre del mercado]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 LÍNEA Y CUOTAS
• Apuesta: [descripción exacta]
• Cuota: [valor]
🎲 MODELO PROBABILÍSTICO
• λ Local: [valor]
• λ Visitante: [valor]
• Probabilidad Real: [XX]%
• Probabilidad Implícita: [XX]%
💰 VALOR ESPERADO
• EV: +[XX]%
• Edge sobre mercado: [XX]%
🎰 SIMULACIÓN MONTE CARLO (10,000 iteraciones)
• P&L Esperado: +$[XX] por $100 apostados
• Desv. Estándar: $[XX]
• Percentil 5%: $[XX]
• Percentil 95%: $[XX]
• Probabilidad de pérdida: [XX]%
💎 KELLY COMPLETO
• Stake óptimo: [X.XX]% de banca
• Crecimiento esperado: +[XX]%
• Volatilidad de banca: [XX]%
⚠️ RIESGO DE RUINA
• Riesgo: [BAJO / MEDIO / ALTO]
• Max Drawdown esperado: [XX]%
• Confianza: [X]/10
📝 JUSTIFICACIÓN
[Explicación técnica del edge identificado, por qué el mercado está mal preciado, factores que respaldan la apuesta]
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
```
## PRINCIPIOS FINALES
- NO fuerces apuestas si no hay valor real
- Descarta apuestas con riesgo de ruina alto aunque tengan EV+
- Prioriza supervivencia del capital sobre maximización teórica
- Si necesitas información adicional (lesiones, clima, árbitro), búscala
- Considera el contraste entre volatilidad de temporada completa vs últimos 5 partidos
- La consistencia (baja volatilidad) aumenta la confiabilidad del modelo
Proporciona tu análisis completo ahora."""
# Llamar a OpenAI
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {api_key}'
}
data = {
'model': 'gpt-4o',
'messages': [{'role': 'user', 'content': prompt}],
'temperature': 0.7,
'max_tokens': 4000
}
response = requests.post(
'https://api.openai.com/v1/chat/completions',
headers=headers,
json=data,
timeout=120
)
if response.status_code == 200:
result = response.json()
return result['choices'][0]['message']['content']
else:
return f"❌ Error en API OpenAI: {response.status_code}\n{response.text}"
except Exception as e:
return f"❌ Error: {str(e)}"
# Crear instancia
analyzer = FootballAnalyzer()
# Interfaz Gradio
with gr.Blocks(title="⚽ Analizador Profesional de Apuestas de Fútbol", theme=gr.themes.Soft()) as app:
gr.Markdown("""
# ⚽ Analizador Profesional de Apuestas de Fútbol
### 📊 Análisis cuantitativo completo con todas las categorías estadísticas
**Incluye**: Goles, Tiros, Corners, Faltas, Tarjetas | Temporada completa + Últimos 5 partidos | Media, Mediana, Moda, Volatilidad
""")
with gr.Row():
with gr.Column(scale=1):
gr.Markdown("### 📥 Paso 1: Descargar Fixtures")
download_btn = gr.Button("Descargar Fixtures", variant="primary", size="lg")
status_fixtures = gr.Textbox(label="Estado", interactive=False)
gr.Markdown("### 🎯 Paso 2: Seleccionar Partido")
match_dropdown = gr.Dropdown(label="Partidos Disponibles", choices=[], interactive=True)
gr.Markdown("### 📊 Paso 3: Ver Cuotas")
show_odds_btn = gr.Button("Ver Cuotas Bet365", variant="secondary")
gr.Markdown("### 📈 Paso 4: Análisis Completo")
analyze_btn = gr.Button("Descargar Datos y Analizar", variant="primary", size="lg")
status_analysis = gr.Textbox(label="Estado", interactive=False)
with gr.Column(scale=2):
odds_output = gr.Markdown(label="Cuotas")
analysis_output = gr.Markdown(label="Resumen")
gr.Markdown("## 📊 Tablas Estadísticas Detalladas")
tables_output = gr.HTML(label="Estadísticas Completas")
gr.Markdown("---")
gr.Markdown("## 🤖 Análisis Profesional con IA (Apostador Cuantitativo)")
gr.Markdown("""
**Incluye**: Modelado Poisson, Simulación Monte Carlo, Criterio de Kelly, Análisis de 5+ categorías de mercado
""")
with gr.Row():
api_key_input = gr.Textbox(
label="OpenAI API Key",
placeholder="sk-...",
type="password"
)
predict_btn = gr.Button("🔮 Análisis Cuantitativo Completo", variant="primary", size="lg")
ai_prediction_output = gr.Markdown()
# Events
download_btn.click(
fn=analyzer.download_fixtures,
outputs=[match_dropdown, status_fixtures]
)
show_odds_btn.click(
fn=analyzer.show_bet365_odds,
inputs=[match_dropdown],
outputs=[odds_output]
)
analyze_btn.click(
fn=analyzer.download_league_data,
inputs=[match_dropdown],
outputs=[status_analysis, analysis_output, tables_output]
)
predict_btn.click(
fn=analyzer.get_ai_prediction,
inputs=[api_key_input],
outputs=[ai_prediction_output]
)
gr.Markdown("""
---
### 📝 Leyenda de Volatilidad
- **Verde (< 30%)**: Muy consistente - Alta predictibilidad
- **Naranja (30-60%)**: Moderadamente consistente
- **Rojo (> 60%)**: Muy inconsistente - Baja predictibilidad
### 🎯 Categorías Analizadas
**Goles**: A favor, En contra, Medio tiempo | **Tiros**: Totales, Al arco, A favor y contra
**Corners**: A favor, Contra | **Faltas**: Cometidas, Recibidas
**Tarjetas**: Amarillas, Rojas (propias y del rival)
Cada categoría incluye: Media, Mediana, Moda, Min-Max, Volatilidad, Desv.Std, Total
""")
if __name__ == "__main__":
app.launch(share=True)