|
|
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 |
|
|
|
|
|
|
|
|
OPENAI_API_KEY = os.environ.get('OPENAI_API_KEY', '') |
|
|
|
|
|
|
|
|
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" |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
last_5 = team_matches.tail(5).copy() |
|
|
|
|
|
|
|
|
categories = {} |
|
|
|
|
|
|
|
|
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]) |
|
|
} |
|
|
|
|
|
|
|
|
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]) |
|
|
} |
|
|
|
|
|
|
|
|
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]) |
|
|
} |
|
|
|
|
|
|
|
|
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]) |
|
|
} |
|
|
|
|
|
|
|
|
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]) |
|
|
} |
|
|
|
|
|
|
|
|
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()) |
|
|
} |
|
|
|
|
|
|
|
|
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>' |
|
|
|
|
|
|
|
|
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>' |
|
|
|
|
|
|
|
|
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(): |
|
|
|
|
|
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> |
|
|
''' |
|
|
|
|
|
|
|
|
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'] |
|
|
|
|
|
|
|
|
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) |
|
|
] |
|
|
|
|
|
|
|
|
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", "" |
|
|
|
|
|
|
|
|
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') |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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. |
|
|
""" |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
home = self.analysis_data['home_analysis'] |
|
|
away = self.analysis_data['away_analysis'] |
|
|
|
|
|
|
|
|
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.""" |
|
|
|
|
|
|
|
|
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)}" |
|
|
|
|
|
|
|
|
analyzer = FootballAnalyzer() |
|
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
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) |