daniel-saed commited on
Commit
4bf43c3
·
verified ·
1 Parent(s): 7b16da5

Upload streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +349 -74
streamlit_app.py CHANGED
@@ -27,11 +27,32 @@ st.markdown("""
27
  """, unsafe_allow_html=True)
28
 
29
  # --- CONSTANTES DEL MODELO ---
30
- MSE_MODELO = 1.9
31
- RMSE_MODELO = 2.42
32
  R2_MODELO = 0.39
33
  N_SIMULACIONES = 5000
34
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  # --- FUNCIONES AUXILIARES ---
36
  def probabilidad_a_momio(probabilidad):
37
  """Convierte probabilidad (%) a momio decimal"""
@@ -162,8 +183,11 @@ st.markdown("<h1 style='text-align: center;'>Corners Forecast</h1>", unsafe_allo
162
  # --- CARGAR DATOS ---
163
  @st.cache_data
164
  def cargar_datos():
165
- df = pd.read_csv(r"https://raw.githubusercontent.com/danielsaed/futbol_corners_forecast/refs/heads/main/dataset/cleaned/dataset_cleaned.csv")
166
- return df[['local','league']].drop_duplicates()
 
 
 
167
 
168
  df = cargar_datos()
169
 
@@ -249,7 +273,7 @@ with cl2:
249
  if jornada:
250
  option_local = st.selectbox(
251
  "🏠 Equipo Local",
252
- list(df["local"][df["league"] == LEAGUES_DICT[option]]),
253
  index=None,
254
  placeholder="Equipo local",
255
  key="local_select"
@@ -272,7 +296,7 @@ with cl4:
272
  if jornada:
273
  option_away = st.selectbox(
274
  "✈️ Equipo Visitante",
275
- list(df["local"][df["league"] == LEAGUES_DICT[option]]),
276
  index=None,
277
  placeholder="Equipo visitante",
278
  key="away_select"
@@ -307,6 +331,7 @@ if option and option_local and option_away and st.session_state.prediccion_reali
307
  with st.spinner('🔮 Generando predicción con análisis de incertidumbre...'):
308
 
309
  url = "https://daniel-saed-futbol-corners-forecast-api.hf.space/items/"
 
310
  headers = {"X-API-Key": API_KEY}
311
  params = {
312
  "local": option_local,
@@ -322,6 +347,7 @@ if option and option_local and option_away and st.session_state.prediccion_reali
322
  if response.status_code == 200:
323
  st.session_state.resultado_api = response.json()
324
  st.success("✅ Predicción generada")
 
325
  elif response.status_code == 401:
326
  st.error("❌ Error de Autenticación - API Key inválida")
327
  st.stop()
@@ -349,93 +375,343 @@ if option and option_local and option_away and st.session_state.prediccion_reali
349
  resultado = st.session_state.resultado_api
350
  lambda_pred = resultado['prediccion']
351
 
352
- st.write("")
353
- st.write("")
 
 
354
 
355
- # ============================================
356
- # 1. PREDICCIÓN PRINCIPAL
357
- # ============================================
358
 
359
- lambda_low = max(0, lambda_pred - 1.96 * RMSE_MODELO)
360
- lambda_high = lambda_pred + 1.96 * RMSE_MODELO
 
361
 
362
- st.markdown("## 🎯 Predicción de Corners")
 
 
363
 
 
364
  st.write("")
365
 
366
- col_pred1, col_pred2, col_pred3 = st.columns(3)
367
-
368
- with col_pred1:
369
- st.metric(
370
- label="Corners Esperados",
371
- value=f"{lambda_pred:.1f}",
372
- help="Valor esperado (λ) del modelo"
373
- )
374
-
375
- with col_pred2:
376
- st.metric(
377
- label="Límite Inferior",
378
- value=f"{lambda_low:.1f}",
379
- delta=f"{lambda_low - lambda_pred:.1f}",
380
- help="Intervalo de confianza 95% (inferior)"
381
- )
382
 
383
- with col_pred3:
384
- st.metric(
385
- label="Límite Superior",
386
- value=f"{lambda_high:.1f}",
387
- delta=f"{lambda_high - lambda_pred:.1f}",
388
- help="Intervalo de confianza 95% (superior)"
389
- )
390
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
391
  st.write("")
392
  st.write("")
393
- st.markdown("---")
394
  st.write("")
395
  st.write("")
 
 
 
396
 
397
  # ============================================
398
  # 2. ANÁLISIS DE EQUIPOS
399
  # ============================================
400
 
401
- stats_data = resultado['stats']
402
- local_ck = stats_data['local_ck']
403
- away_ck = stats_data['away_ck']
404
- local_ck_received = stats_data['local_ck_received']
405
- away_ck_received = stats_data['away_ck_received']
406
- h2h_total = stats_data['h2h_total']
407
- partido_esperado = stats_data['partido_esperado']
 
 
 
408
 
409
  riesgo = resultado['riesgo']
410
 
411
- st.markdown("### Análisis de Corners")
412
-
413
- df_corners = pd.DataFrame({
414
- 'Métrica': ['Corners Generados ⚽', 'Corners Concedidos 🛡️', 'Head to Head'],
415
- f'🏠 {option_local}': [f'{local_ck:.2f}', f'{local_ck_received:.2f}','---'],
416
- f'✈️ {option_away}': [f'{away_ck:.2f}', f'{away_ck_received:.2f}','---'],
417
- '🎯 Total': [
418
- f'{(local_ck + away_ck):.2f}',
419
- f'{(local_ck_received + away_ck_received):.2f}',
420
- f"{h2h_total:.2f}"
421
- ]
422
- })
423
-
424
- st.dataframe(
425
- df_corners,
426
- hide_index=True,
427
- use_container_width=True,
428
- column_config={
429
- 'Métrica': st.column_config.TextColumn('📊 Métrica', width='medium'),
430
- f'🏠 {option_local}': st.column_config.TextColumn(f'🏠 {option_local}', width='medium'),
431
- f'✈️ {option_away}': st.column_config.TextColumn(f'✈️ {option_away}', width='medium'),
432
- '🎯 Total': st.column_config.TextColumn('🎯 Total', width='medium')
433
- }
434
- )
435
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
436
  st.write("")
437
  st.write("")
 
 
 
 
 
438
 
 
 
 
439
  st.markdown("### Fiabilidad")
440
 
441
  col_fiab1, col_fiab2, col_fiab3 = st.columns(3)
@@ -661,16 +937,15 @@ if option and option_local and option_away and st.session_state.prediccion_reali
661
 
662
  st.plotly_chart(fig_under, use_container_width=True)
663
 
664
- st.write("")
665
- st.write("")
666
  st.markdown("---")
667
  st.write("")
668
- st.write("")
669
 
670
  # ============================================
671
  # 4. CALCULADORA
672
  # ============================================
673
- st.markdown("## 💰 Calculadora de Valor")
674
 
675
  st.write("")
676
 
@@ -817,7 +1092,7 @@ else:
817
 
818
  # Sidebar
819
  with st.sidebar:
820
- st.markdown("## Corners Forecast")
821
 
822
  st.markdown("---")
823
 
 
27
  """, unsafe_allow_html=True)
28
 
29
  # --- CONSTANTES DEL MODELO ---
30
+ MSE_MODELO = 1.99
31
+ RMSE_MODELO = 2.4
32
  R2_MODELO = 0.39
33
  N_SIMULACIONES = 5000
34
 
35
+ # --- ERRORES ESTIMADOS POR MODELO (RMSE) ---
36
+ # Corners
37
+ RMSE_CK_TOTAL = 1.99
38
+ RMSE_CK_LOCAL = 1.64
39
+ RMSE_CK_AWAY = 1.45
40
+
41
+ # Goles
42
+ RMSE_GF_TOTAL = .95
43
+ RMSE_GF_LOCAL = .6
44
+ RMSE_GF_AWAY = .6
45
+
46
+ # xG (Goles Esperados)
47
+ RMSE_XG_TOTAL = 1
48
+ RMSE_XG_LOCAL = .6
49
+ RMSE_XG_AWAY = .6
50
+
51
+ # Tiros a Puerta (Shots on Target)
52
+ RMSE_ST_TOTAL = 1.7
53
+ RMSE_ST_LOCAL = 1.4
54
+ RMSE_ST_AWAY = 1.3
55
+
56
  # --- FUNCIONES AUXILIARES ---
57
  def probabilidad_a_momio(probabilidad):
58
  """Convierte probabilidad (%) a momio decimal"""
 
183
  # --- CARGAR DATOS ---
184
  @st.cache_data
185
  def cargar_datos():
186
+ df_historic = pd.read_csv(r"https://raw.githubusercontent.com/danielsaed/futbol_corners_forecast/refs/heads/main/dataset/cleaned/dataset_cleaned.csv")
187
+ df_current_year = pd.read_csv(r"https://raw.githubusercontent.com/danielsaed/futbol_corners_forecast/refs/heads/main/dataset/cleaned/dataset_cleaned_current_year.csv")
188
+
189
+ df = pd.concat([df_historic,df_current_year])
190
+ return df[['local','league','season']].drop_duplicates()
191
 
192
  df = cargar_datos()
193
 
 
273
  if jornada:
274
  option_local = st.selectbox(
275
  "🏠 Equipo Local",
276
+ list(df["local"][(df["league"] == LEAGUES_DICT[option]) & (df["season"] == temporada)]),
277
  index=None,
278
  placeholder="Equipo local",
279
  key="local_select"
 
296
  if jornada:
297
  option_away = st.selectbox(
298
  "✈️ Equipo Visitante",
299
+ list(df["local"][(df["league"] == LEAGUES_DICT[option]) & (df["season"] == temporada)]),
300
  index=None,
301
  placeholder="Equipo visitante",
302
  key="away_select"
 
331
  with st.spinner('🔮 Generando predicción con análisis de incertidumbre...'):
332
 
333
  url = "https://daniel-saed-futbol-corners-forecast-api.hf.space/items/"
334
+ #url = "http://localhost:7860/items/"
335
  headers = {"X-API-Key": API_KEY}
336
  params = {
337
  "local": option_local,
 
347
  if response.status_code == 200:
348
  st.session_state.resultado_api = response.json()
349
  st.success("✅ Predicción generada")
350
+
351
  elif response.status_code == 401:
352
  st.error("❌ Error de Autenticación - API Key inválida")
353
  st.stop()
 
375
  resultado = st.session_state.resultado_api
376
  lambda_pred = resultado['prediccion']
377
 
378
+ # Extraer predicciones detalladas
379
+ pred_ck_total = resultado.get('prediccion', 0)
380
+ pred_ck_local = resultado.get('prediccion_local', 0)
381
+ pred_ck_away = resultado.get('prediccion_away', 0)
382
 
383
+ pred_xg_total = resultado.get('prediccion_xg', 0)
384
+ pred_xg_local = resultado.get('prediccion_xg_local', 0)
385
+ pred_xg_away = resultado.get('prediccion_xg_away', 0)
386
 
387
+ pred_gf_total = resultado.get('prediccion_gf', 0)
388
+ pred_gf_local = resultado.get('prediccion_gf_local', 0)
389
+ pred_gf_away = resultado.get('prediccion_gf_away', 0)
390
 
391
+ pred_st_total = resultado.get('prediccion_st', 0)
392
+ pred_st_local = resultado.get('prediccion_st_local', 0)
393
+ pred_st_away = resultado.get('prediccion_st_away', 0)
394
 
395
+ st.write("")
396
  st.write("")
397
 
398
+ # ============================================
399
+ # 1. PREDICCIONES MACHINE LEARNING
400
+ # ============================================
 
 
 
 
 
 
 
 
 
 
 
 
 
401
 
402
+ st.markdown("# Predicciones")
403
+ st.write("")
404
+ st.caption("Modelos XGBoost entrenados con alrededor de 13,000 partidos utilizando metricas avanzadas de futbol de las principales ligas europeas (2018 a 2025). Datos obtenidos de OPTA.")
 
 
 
 
405
 
406
+ def mostrar_bloque_prediccion(titulo, total, local, away, rmse_total, rmse_local, rmse_away, icono):
407
+ st.markdown(f"#### {icono} {titulo}")
408
+ c1, c2, c3 = st.columns(3)
409
+ with c1:
410
+ st.metric("Total", f"{total:.2f}", delta=f"± {rmse_total}", delta_color="off", help=f"RMSE estimado: {rmse_total}")
411
+ with c2:
412
+ st.metric(f"Local ({option_local})", f"{local:.2f}", delta=f"± {rmse_local}", delta_color="off", help=f"RMSE estimado: {rmse_local}")
413
+ with c3:
414
+ st.metric(f"Visitante ({option_away})", f"{away:.2f}", delta=f"± {rmse_away}", delta_color="off", help=f"RMSE estimado: {rmse_away}")
415
+ st.divider()
416
+
417
+ # 1. Tiros de Esquina
418
+ mostrar_bloque_prediccion(
419
+ "Tiros de esquina",
420
+ pred_ck_total, pred_ck_local, pred_ck_away,
421
+ RMSE_CK_TOTAL, RMSE_CK_LOCAL, RMSE_CK_AWAY,
422
+ "🚩"
423
+ )
424
+
425
+ # 2. Goles
426
+ mostrar_bloque_prediccion(
427
+ "Goles",
428
+ pred_gf_total, pred_gf_local, pred_gf_away,
429
+ RMSE_GF_TOTAL, RMSE_GF_LOCAL, RMSE_GF_AWAY,
430
+ "⚽"
431
+ )
432
+
433
+ # 3. xG (Goles Esperados)
434
+ mostrar_bloque_prediccion(
435
+ "xG (Goles Esperados)",
436
+ pred_xg_total, pred_xg_local, pred_xg_away,
437
+ RMSE_XG_TOTAL, RMSE_XG_LOCAL, RMSE_XG_AWAY,
438
+ "📈"
439
+ )
440
+
441
+ # 4. Tiros a Puerta
442
+ mostrar_bloque_prediccion(
443
+ "Tiros a puerta",
444
+ pred_st_total, pred_st_local, pred_st_away,
445
+ RMSE_ST_TOTAL, RMSE_ST_LOCAL, RMSE_ST_AWAY,
446
+ "🎯")
447
+
448
+ st.write("")
449
+ st.write("")
450
  st.write("")
451
  st.write("")
 
452
  st.write("")
453
  st.write("")
454
+ st.write("")
455
+ st.write("")
456
+
457
 
458
  # ============================================
459
  # 2. ANÁLISIS DE EQUIPOS
460
  # ============================================
461
 
462
+ # Extraer datos nuevos
463
+ # Extraer datos nuevos
464
+ # Extraer datos nuevos
465
+ stats_ck = resultado.get('stats_ck', {})
466
+ stats_gf = resultado.get('stats_gf', {})
467
+ stats_xg = resultado.get('stats_xg', {})
468
+ stats_st = resultado.get('stats_st', {}) # Nuevo
469
+
470
+ ppp_local = resultado.get('ppp_local', 0)
471
+ ppp_away = resultado.get('ppp_away', 0)
472
 
473
  riesgo = resultado['riesgo']
474
 
475
+ st.markdown("# Stats")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
476
 
477
+ # Métrica de Forma (PPP)
478
+ col_form1, col_form2, col_form3 = st.columns(3)
479
+ with col_form1:
480
+ st.metric("Forma Local (PPP)", f"{ppp_local:.2f}", help="Puntos por Partido")
481
+ with col_form2:
482
+ diff_ppp = ppp_local - ppp_away
483
+ st.metric("Diferencia de Nivel", f"{diff_ppp:.2f}", delta_color="off", help="Diferencia de PPP (Local - Visitante)")
484
+ with col_form3:
485
+ st.metric("Forma Visitante (PPP)", f"{ppp_away:.2f}", help="Puntos por Partido")
486
+
487
+ st.write("")
488
+
489
+ # --- FUNCIÓN PARA RENDERIZAR PESTAÑAS ---
490
+ def render_stats_tab(stats_data, type_key, label_metric):
491
+ """Renderiza el contenido de una pestaña de estadísticas con el nuevo layout"""
492
+
493
+ # --- 1. PREPARAR DATOS ---
494
+ # General
495
+ l_h = stats_data.get(f'local_{type_key}_home', 0)
496
+ l_a = stats_data.get(f'local_{type_key}_away', 0)
497
+ a_h = stats_data.get(f'away_{type_key}_home', 0)
498
+ a_a = stats_data.get(f'away_{type_key}_away', 0)
499
+
500
+ l_rec_h = stats_data.get(f'local_{type_key}_received_home', 0)
501
+ l_rec_a = stats_data.get(f'local_{type_key}_received_away', 0)
502
+ a_rec_h = stats_data.get(f'away_{type_key}_received_home', 0)
503
+ a_rec_a = stats_data.get(f'away_{type_key}_received_away', 0)
504
+
505
+ # Forma
506
+ l_h_f = stats_data.get(f'local_{type_key}_home_form', 0)
507
+ l_a_f = stats_data.get(f'local_{type_key}_away_form', 0)
508
+ a_h_f = stats_data.get(f'away_{type_key}_home_form', 0)
509
+ a_a_f = stats_data.get(f'away_{type_key}_away_form', 0)
510
+
511
+ l_rec_h_f = stats_data.get(f'local_{type_key}_received_home_form', 0)
512
+ l_rec_a_f = stats_data.get(f'local_{type_key}_received_away_form', 0)
513
+ a_rec_h_f = stats_data.get(f'away_{type_key}_received_home_form', 0)
514
+ a_rec_a_f = stats_data.get(f'away_{type_key}_received_away_form', 0)
515
+
516
+ # Globales (Promedio simple Home+Away)
517
+ l_g = (l_h + l_a) / 2
518
+ a_g = (a_h + a_a) / 2
519
+ l_rec_g = (l_rec_h + l_rec_a) / 2
520
+ a_rec_g = (a_rec_h + a_rec_a) / 2
521
+
522
+ l_g_f = (l_h_f + l_a_f) / 2
523
+ a_g_f = (a_h_f + a_a_f) / 2
524
+ l_rec_g_f = (l_rec_h_f + l_rec_a_f) / 2
525
+ a_rec_g_f = (a_rec_h_f + a_rec_a_f) / 2
526
+
527
+ # --- FUNCIÓN AUXILIAR PARA MOSTRAR TABLA CON TOTALES ---
528
+ def display_styled_df(teams, favors, contras):
529
+ df = pd.DataFrame({
530
+ 'Equipo': teams,
531
+ 'A Favor': favors,
532
+ 'En Contra': contras
533
+ })
534
+
535
+ # Calcular Totales por Fila (Total del equipo)
536
+ df['Total'] = df['A Favor'] + df['En Contra']
537
+
538
+ # Calcular Totales por Columna (Suma de ambos equipos)
539
+ # NOTA: El total de totales (esquina inferior derecha) se deja vacío
540
+ total_row = pd.DataFrame({
541
+ 'Equipo': ['TOTAL'],
542
+ 'A Favor': [df['A Favor'].sum()],
543
+ 'En Contra': [df['En Contra'].sum()],
544
+ 'Total': [0]
545
+ })
546
+
547
+ df_final = pd.concat([df, total_row], ignore_index=True)
548
+
549
+ # Estilos
550
+ # na_rep="" hace que el None se muestre como celda vacía
551
+ styler = df_final.style.format(subset=['A Favor', 'En Contra', 'Total'], formatter="{:.2f}", na_rep="")
552
+
553
+ # Estilo: Fondo transparente y texto gris
554
+ style_css = 'color: #888888; font-weight: bold;'
555
+
556
+ # Resaltar última fila (Totales de columna)
557
+ styler.apply(lambda x: [style_css if x.name == df_final.index[-1] else '' for _ in x], axis=1)
558
+
559
+ # Resaltar columna Total (Totales de fila)
560
+ styler.apply(lambda x: [style_css if x.name == 'Total' else '' for _ in x], axis=0)
561
+
562
+ st.dataframe(styler, hide_index=True, use_container_width=True)
563
+
564
+ # --- 2. RENDERIZAR SECCIÓN GENERAL ---
565
+ st.markdown("#### 📊 Datos Generales (Temporada)")
566
+ c1, c2, c3 = st.columns(3)
567
+
568
+ # Columna 1: Contexto Real
569
+ with c1:
570
+ st.caption("🏟️ Contexto (Local en Casa / Vis. Fuera)")
571
+ display_styled_df(
572
+ [f'🏠 {option_local}', f'✈️ {option_away}'],
573
+ [l_h, a_a],
574
+ [l_rec_h, a_rec_a]
575
+ )
576
+
577
+ # Columna 2: Inversa
578
+ with c2:
579
+ st.caption("🔄 Inversa (Local Fuera / Vis. Casa)")
580
+ display_styled_df(
581
+ [f'✈️ {option_local}', f'🏠 {option_away}'],
582
+ [l_a, a_h],
583
+ [l_rec_a, a_rec_h]
584
+ )
585
+
586
+ # Columna 3: Global
587
+ with c3:
588
+ st.caption("🌍 Global (Promedio Total)")
589
+ display_styled_df(
590
+ [f'{option_local}', f'{option_away}'],
591
+ [l_g, a_g],
592
+ [l_rec_g, a_rec_g]
593
+ )
594
+
595
+ # --- 3. RENDERIZAR SECCIÓN FORMA ---
596
+ st.markdown("#### 🔥 Estado de Forma (Últimos 6 Partidos)")
597
+ c1_f, c2_f, c3_f = st.columns(3)
598
+
599
+ # Columna 1: Contexto Forma
600
+ with c1_f:
601
+ st.caption("🏟️ Contexto (Forma)")
602
+ display_styled_df(
603
+ [f'🏠 {option_local}', f'✈️ {option_away}'],
604
+ [l_h_f, a_a_f],
605
+ [l_rec_h_f, a_rec_a_f]
606
+ )
607
+
608
+ # Columna 2: Inversa Forma
609
+ with c2_f:
610
+ st.caption("🔄 Inversa (Forma)")
611
+ display_styled_df(
612
+ [f'✈️ {option_local}', f'🏠 {option_away}'],
613
+ [l_a_f, a_h_f],
614
+ [l_rec_a_f, a_rec_h_f]
615
+ )
616
+
617
+ # Columna 3: Global Forma
618
+ with c3_f:
619
+ st.caption("🌍 Global (Forma)")
620
+ display_styled_df(
621
+ [f'{option_local}', f'{option_away}'],
622
+ [l_g_f, a_g_f],
623
+ [l_rec_g_f, a_rec_g_f]
624
+ )
625
+
626
+ # --- 4. RENDERIZAR H2H ---
627
+ st.markdown("#### ⚔️ Head to Head (H2H)")
628
+ h2h_val = stats_data.get(f'h2h_{type_key}_total', 0)
629
+ st.metric(f"Promedio {label_metric} H2H", f"{h2h_val:.2f}")
630
+
631
+
632
+ # Tabs para las diferentes estadísticas
633
+ tab_ck, tab_gf, tab_xg, tab_st = st.tabs(["🚩 Corners", "⚽ Goles", "📈 xG (Esperados)", "🎯 Tiros a Puerta"])
634
+
635
+ with tab_ck:
636
+ render_stats_tab(stats_ck, 'ck', 'Corners')
637
+
638
+ with tab_gf:
639
+ render_stats_tab(stats_gf, 'gf', 'Goles')
640
+
641
+ with tab_xg:
642
+ render_stats_tab(stats_xg, 'xg', 'xG')
643
+
644
+ with tab_st:
645
+ render_stats_tab(stats_st, 'st', 'Tiros a Puerta')
646
+
647
+ # --- MOSTRAR TABLA H2H DETALLADA ---
648
+ if 'h2h_matches' in resultado and resultado['h2h_matches']:
649
+ st.markdown("### 📜 Historial de Partidos (H2H)")
650
+
651
+ h2h_data = []
652
+ for match in resultado['h2h_matches']:
653
+ # Datos del equipo local en ese partido
654
+ home_team = match['match_home_team']
655
+ away_team = match['match_away_team']
656
+
657
+ # Identificar stats correctas
658
+ if match['local_team_stats']['team'] == home_team:
659
+ home_stats = match['local_team_stats']
660
+ away_stats = match['away_team_stats']
661
+ else:
662
+ home_stats = match['away_team_stats']
663
+ away_stats = match['local_team_stats']
664
+
665
+ h2h_data.append({
666
+ 'Temporada': match['season'],
667
+ 'Jornada': match['round'],
668
+ 'Local': home_team,
669
+ 'Visitante': away_team,
670
+ 'Goles L': home_stats['goals'],
671
+ 'Goles V': away_stats['goals'],
672
+ 'Corners L': home_stats['corners'],
673
+ 'Corners V': away_stats['corners'],
674
+ 'xG L': home_stats['xg'],
675
+ 'xG V': away_stats['xg'],
676
+ 'SoT L': home_stats['sot'],
677
+ 'SoT V': away_stats['sot']
678
+ })
679
+
680
+ df_h2h = pd.DataFrame(h2h_data)
681
+
682
+ st.dataframe(
683
+ df_h2h,
684
+ hide_index=True,
685
+ use_container_width=True,
686
+ column_config={
687
+ 'Temporada': st.column_config.TextColumn('📅 Temp', width='small'),
688
+ 'Jornada': st.column_config.NumberColumn('#', width='small', format="%d"),
689
+ 'Goles L': st.column_config.NumberColumn('⚽ L', format="%.0f"),
690
+ 'Goles V': st.column_config.NumberColumn('⚽ V', format="%.0f"),
691
+ 'Corners L': st.column_config.NumberColumn('🚩 L', format="%.0f"),
692
+ 'Corners V': st.column_config.NumberColumn('🚩 V', format="%.0f"),
693
+ 'xG L': st.column_config.NumberColumn('📈 xG L', format="%.2f"),
694
+ 'xG V': st.column_config.NumberColumn('📈 xG V', format="%.2f"),
695
+ 'SoT L': st.column_config.NumberColumn('🎯 SoT L', format="%.0f"),
696
+ 'SoT V': st.column_config.NumberColumn('🎯 SoT V', format="%.0f"),
697
+ }
698
+ )
699
+ st.divider()
700
+ st.write("")
701
+ st.write("")
702
+ st.write("")
703
+ st.write("")
704
  st.write("")
705
  st.write("")
706
+ st.write("")
707
+ st.write("")
708
+
709
+
710
+
711
 
712
+ st.markdown("# Momios y Valor de Apuesta")
713
+ st.write("")
714
+ st.write("")
715
  st.markdown("### Fiabilidad")
716
 
717
  col_fiab1, col_fiab2, col_fiab3 = st.columns(3)
 
937
 
938
  st.plotly_chart(fig_under, use_container_width=True)
939
 
940
+
 
941
  st.markdown("---")
942
  st.write("")
943
+
944
 
945
  # ============================================
946
  # 4. CALCULADORA
947
  # ============================================
948
+ st.markdown("### 💰 Calculadora de Valor")
949
 
950
  st.write("")
951
 
 
1092
 
1093
  # Sidebar
1094
  with st.sidebar:
1095
+ st.markdown("# Corners Forecast")
1096
 
1097
  st.markdown("---")
1098