daniel-saed commited on
Commit
4a1cf98
·
verified ·
1 Parent(s): 638d8c6

Upload 2 files

Browse files
Files changed (2) hide show
  1. src/api/api.py +190 -189
  2. src/api/load.py +77 -65
src/api/api.py CHANGED
@@ -1,190 +1,191 @@
1
- # ===========================
2
- # SISTEMA DE PREDICCIÓN DE CORNERS - OPTIMIZADO PARA APUESTAS (VERSIÓN COMPLETA)
3
- # ===========================
4
-
5
- import numpy as np
6
- import pandas as pd
7
- import os
8
- from fastapi.responses import JSONResponse
9
- from fastapi import Depends, FastAPI, HTTPException
10
- from fastapi.security.api_key import APIKeyHeader
11
- from fastapi import Security
12
- from fastapi.responses import JSONResponse
13
- from dotenv import load_dotenv
14
- from src.api.load import USE_MODEL
15
-
16
- load_dotenv()
17
-
18
- model = USE_MODEL()
19
-
20
- app = FastAPI()
21
-
22
- # ===========================
23
- # CONFIGURACIÓN API KEY
24
- # ===========================
25
-
26
- API_KEY = os.getenv("API_KEY") # ⚠️ CÁMBIALA POR UNA SEGURA
27
- api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
28
-
29
- async def get_api_key(api_key: str = Security(api_key_header)):
30
- """Validar API Key"""
31
- if api_key != API_KEY:
32
- raise HTTPException(
33
- status_code=401,
34
- detail="API Key inválida o faltante"
35
- )
36
- return api_key
37
-
38
- # ===========================
39
- # HELPER: CONVERTIR NUMPY/PANDAS A TIPOS NATIVOS
40
- # ===========================
41
- def convert_to_native(val):
42
- """Convierte tipos NumPy/Pandas a tipos nativos de Python"""
43
- if isinstance(val, (np.integer, np.int64, np.int32, np.int16, np.int8)):
44
- return int(val)
45
- elif isinstance(val, (np.floating, np.float64, np.float32, np.float16)):
46
- return float(val)
47
- elif isinstance(val, np.ndarray):
48
- return [convert_to_native(item) for item in val.tolist()]
49
- elif isinstance(val, dict):
50
- return {key: convert_to_native(value) for key, value in val.items()}
51
- elif isinstance(val, (list, tuple)):
52
- return [convert_to_native(item) for item in val]
53
- elif isinstance(val, pd.Series):
54
- return convert_to_native(val.to_dict())
55
- elif isinstance(val, pd.DataFrame):
56
- return convert_to_native(val.to_dict(orient='records'))
57
- elif pd.isna(val):
58
- return None
59
- else:
60
- return val
61
-
62
-
63
-
64
-
65
- # ===========================
66
- # ENDPOINTS
67
- # ===========================
68
-
69
- @app.get("/")
70
- def read_root():
71
- """Endpoint raíz con información de la API"""
72
- return {
73
- "api": "Corners Prediction API",
74
- "version": "1.0.0",
75
- "status": "active",
76
- "endpoints": {
77
- "/": "Información de la API",
78
- "/items/": "Predicción de corners (requiere API Key)",
79
- "/health": "Estado de salud"
80
- },
81
- "auth": "Requiere header: X-API-Key"
82
- }
83
-
84
-
85
-
86
- @app.get("/items/")
87
- def predict_corners(
88
- local: str,
89
- visitante: str,
90
- jornada: int,
91
- league_code: str,
92
- temporada: str = "2526",
93
- api_key: str = Depends(get_api_key) # ✅ PROTEGIDO
94
- ):
95
- """
96
- Predecir corners para un partido de fútbol
97
-
98
- Args:
99
- local: Nombre del equipo local (requerido)
100
- visitante: Nombre del equipo visitante (requerido)
101
- jornada: Número de jornada (requerido, min: 1)
102
- league_code: Código de liga (requerido: ESP, GER, FRA, ITA, ENG, NED, POR, BEL)
103
- temporada: Temporada en formato AABB (default: "2526")
104
-
105
- Returns:
106
- JSON con predicción y análisis completo
107
-
108
- Example:
109
- GET /items/?local=Barcelona&visitante=Real%20Madrid&jornada=15&league_code=ESP&temporada=2526
110
- Headers: X-API-Key: tu-clave-secreta-aqui
111
- """
112
-
113
- # ===========================
114
- # VALIDACIONES
115
- # ===========================
116
-
117
- # Validar campos obligatorios
118
- if not local or not visitante:
119
- raise HTTPException(
120
- status_code=400,
121
- detail="Los parámetros 'local' y 'visitante' son obligatorios"
122
- )
123
-
124
- # Validar jornada
125
- if jornada < 1:
126
- raise HTTPException(
127
- status_code=400,
128
- detail="La jornada debe ser mayor o igual a 1"
129
- )
130
-
131
- # Validar liga
132
- valid_leagues = ["ESP", "GER", "FRA", "ITA", "ENG", "NED", "POR", "BEL"]
133
- if league_code not in valid_leagues:
134
- raise HTTPException(
135
- status_code=400,
136
- detail=f"Liga inválida. Ligas válidas: {', '.join(valid_leagues)}"
137
- )
138
-
139
- # ===========================
140
- # PREDICCIÓN
141
- # ===========================
142
-
143
- try:
144
- resultado = model.consume_model_single(
145
- local=local,
146
- visitante=visitante,
147
- jornada=jornada,
148
- temporada=temporada,
149
- league_code=league_code
150
- )
151
-
152
- # Verificar si hubo error en la predicción
153
- if resultado.get("error"):
154
- raise HTTPException(
155
- status_code=422,
156
- detail=f"Error en predicción: {resultado['error']}"
157
- )
158
-
159
- # ✅ CONVERTIR TIPOS NUMPY A NATIVOS
160
- resultado_limpio = convert_to_native(resultado)
161
-
162
- # Agregar metadata
163
- resultado_limpio["metadata"] = {
164
- "api_version": "1.0.0",
165
- "model_version": "v4",
166
- "timestamp": pd.Timestamp.now().isoformat()
167
- }
168
-
169
- return JSONResponse(
170
- status_code=200,
171
- content=resultado_limpio
172
- )
173
-
174
- except HTTPException:
175
- # Re-lanzar excepciones HTTP
176
- raise
177
-
178
- except Exception as e:
179
- # Capturar cualquier otro error
180
- import traceback
181
- error_detail = {
182
- "error": str(e),
183
- "type": type(e).__name__,
184
- "traceback": traceback.format_exc() if app.debug else None
185
- }
186
-
187
- return JSONResponse(
188
- status_code=500,
189
- content=error_detail
 
190
  )
 
1
+ # ===========================
2
+ # SISTEMA DE PREDICCIÓN DE CORNERS - OPTIMIZADO PARA APUESTAS (VERSIÓN COMPLETA)
3
+ # ===========================
4
+
5
+ import numpy as np
6
+ import pandas as pd
7
+ import os
8
+ from fastapi.responses import JSONResponse
9
+ from fastapi import Depends, FastAPI, HTTPException
10
+ from fastapi.security.api_key import APIKeyHeader
11
+ from fastapi import Security
12
+ from fastapi.responses import JSONResponse
13
+ from dotenv import load_dotenv
14
+ from src.api.load import USE_MODEL
15
+ #from load import USE_MODEL
16
+
17
+ load_dotenv()
18
+
19
+ model = USE_MODEL()
20
+
21
+ app = FastAPI()
22
+
23
+ # ===========================
24
+ # CONFIGURACIÓN API KEY
25
+ # ===========================
26
+
27
+ API_KEY = os.getenv("API_KEY") # ⚠️ CÁMBIALA POR UNA SEGURA
28
+ api_key_header = APIKeyHeader(name="X-API-Key", auto_error=False)
29
+
30
+ async def get_api_key(api_key: str = Security(api_key_header)):
31
+ """Validar API Key"""
32
+ if api_key != API_KEY:
33
+ raise HTTPException(
34
+ status_code=401,
35
+ detail="API Key inválida o faltante"
36
+ )
37
+ return api_key
38
+
39
+ # ===========================
40
+ # HELPER: CONVERTIR NUMPY/PANDAS A TIPOS NATIVOS
41
+ # ===========================
42
+ def convert_to_native(val):
43
+ """Convierte tipos NumPy/Pandas a tipos nativos de Python"""
44
+ if isinstance(val, (np.integer, np.int64, np.int32, np.int16, np.int8)):
45
+ return int(val)
46
+ elif isinstance(val, (np.floating, np.float64, np.float32, np.float16)):
47
+ return float(val)
48
+ elif isinstance(val, np.ndarray):
49
+ return [convert_to_native(item) for item in val.tolist()]
50
+ elif isinstance(val, dict):
51
+ return {key: convert_to_native(value) for key, value in val.items()}
52
+ elif isinstance(val, (list, tuple)):
53
+ return [convert_to_native(item) for item in val]
54
+ elif isinstance(val, pd.Series):
55
+ return convert_to_native(val.to_dict())
56
+ elif isinstance(val, pd.DataFrame):
57
+ return convert_to_native(val.to_dict(orient='records'))
58
+ elif pd.isna(val):
59
+ return None
60
+ else:
61
+ return val
62
+
63
+
64
+
65
+
66
+ # ===========================
67
+ # ENDPOINTS
68
+ # ===========================
69
+
70
+ @app.get("/")
71
+ def read_root():
72
+ """Endpoint raíz con información de la API"""
73
+ return {
74
+ "api": "Corners Prediction API",
75
+ "version": "1.0.0",
76
+ "status": "active",
77
+ "endpoints": {
78
+ "/": "Información de la API",
79
+ "/items/": "Predicción de corners (requiere API Key)",
80
+ "/health": "Estado de salud"
81
+ },
82
+ "auth": "Requiere header: X-API-Key"
83
+ }
84
+
85
+
86
+
87
+ @app.get("/items/")
88
+ def predict_corners(
89
+ local: str,
90
+ visitante: str,
91
+ jornada: int,
92
+ league_code: str,
93
+ temporada: str = "2526",
94
+ api_key: str = Depends(get_api_key) # ✅ PROTEGIDO
95
+ ):
96
+ """
97
+ Predecir corners para un partido de fútbol
98
+
99
+ Args:
100
+ local: Nombre del equipo local (requerido)
101
+ visitante: Nombre del equipo visitante (requerido)
102
+ jornada: Número de jornada (requerido, min: 1)
103
+ league_code: Código de liga (requerido: ESP, GER, FRA, ITA, ENG, NED, POR, BEL)
104
+ temporada: Temporada en formato AABB (default: "2526")
105
+
106
+ Returns:
107
+ JSON con predicción y análisis completo
108
+
109
+ Example:
110
+ GET /items/?local=Barcelona&visitante=Real%20Madrid&jornada=15&league_code=ESP&temporada=2526
111
+ Headers: X-API-Key: tu-clave-secreta-aqui
112
+ """
113
+
114
+ # ===========================
115
+ # VALIDACIONES
116
+ # ===========================
117
+
118
+ # Validar campos obligatorios
119
+ if not local or not visitante:
120
+ raise HTTPException(
121
+ status_code=400,
122
+ detail="Los parámetros 'local' y 'visitante' son obligatorios"
123
+ )
124
+
125
+ # Validar jornada
126
+ if jornada < 1:
127
+ raise HTTPException(
128
+ status_code=400,
129
+ detail="La jornada debe ser mayor o igual a 1"
130
+ )
131
+
132
+ # Validar liga
133
+ valid_leagues = ["ESP", "GER", "FRA", "ITA", "ENG", "NED", "POR", "BEL"]
134
+ if league_code not in valid_leagues:
135
+ raise HTTPException(
136
+ status_code=400,
137
+ detail=f"Liga inválida. Ligas válidas: {', '.join(valid_leagues)}"
138
+ )
139
+
140
+ # ===========================
141
+ # PREDICCIÓN
142
+ # ===========================
143
+
144
+ try:
145
+ resultado = model.consume_model_single(
146
+ local=local,
147
+ visitante=visitante,
148
+ jornada=jornada,
149
+ temporada=temporada,
150
+ league_code=league_code
151
+ )
152
+
153
+ # Verificar si hubo error en la predicción
154
+ if resultado.get("error"):
155
+ raise HTTPException(
156
+ status_code=422,
157
+ detail=f"Error en predicción: {resultado['error']}"
158
+ )
159
+
160
+ # CONVERTIR TIPOS NUMPY A NATIVOS
161
+ resultado_limpio = convert_to_native(resultado)
162
+
163
+ # Agregar metadata
164
+ resultado_limpio["metadata"] = {
165
+ "api_version": "1.0.0",
166
+ "model_version": "v4",
167
+ "timestamp": pd.Timestamp.now().isoformat()
168
+ }
169
+
170
+ return JSONResponse(
171
+ status_code=200,
172
+ content=resultado_limpio
173
+ )
174
+
175
+ except HTTPException:
176
+ # Re-lanzar excepciones HTTP
177
+ raise
178
+
179
+ except Exception as e:
180
+ # Capturar cualquier otro error
181
+ import traceback
182
+ error_detail = {
183
+ "error": str(e),
184
+ "type": type(e).__name__,
185
+ "traceback": traceback.format_exc() if app.debug else None
186
+ }
187
+
188
+ return JSONResponse(
189
+ status_code=500,
190
+ content=error_detail
191
  )
src/api/load.py CHANGED
@@ -2,6 +2,8 @@
2
  # SISTEMA DE PREDICCIÓN DE CORNERS - OPTIMIZADO PARA APUESTAS (VERSIÓN COMPLETA)
3
  # ===========================
4
 
 
 
5
  import numpy as np
6
  import pandas as pd
7
  import joblib
@@ -9,9 +11,10 @@ from scipy.stats import poisson
9
  from scipy import stats
10
  import os
11
  import sys
12
-
13
- project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))
14
- sys.path.insert(0, project_root)
 
15
  # ===========================
16
  # 1. FUNCIONES FIABILIDAD
17
  # ===========================
@@ -311,6 +314,7 @@ def clasificar_confianza(prob):
311
  else:
312
  return "BAJA ❌"
313
 
 
314
  def get_dataframes(df, season, round_num, local, away, league=None):
315
  """Retorna 8 DataFrames filtrados por equipo, venue y liga"""
316
 
@@ -522,6 +526,8 @@ def get_ppp_difference(df, local, away, season, round_num, league=None):
522
  away_ppp = get_team_ppp(df, away, season, round_num, league)
523
  return local_ppp - away_ppp
524
 
 
 
525
  def predecir_corners(local, visitante, jornada, temporada="2526", league_code="ESP",df_database=pd.DataFrame(),xgb_model="",scaler="",lst_years=[]):
526
  """
527
  Predice corners totales con análisis completo para apuestas
@@ -1074,81 +1080,87 @@ class USE_MODEL():
1074
  self.init_variables()
1075
 
1076
  def load_models(self):
1077
- """Cargar modelos con manejo de errores y rutas flexibles"""
1078
-
1079
- # ===========================
1080
- # CONFIGURACIÓN DE RUTAS
1081
- # ===========================
1082
-
1083
- # Obtener directorio raíz del proyecto
1084
- project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))
1085
- models_dir = os.path.join(project_root, 'models')
1086
 
1087
- # Buscar archivos más recientes
1088
- model_files = [f for f in os.listdir(models_dir) if f.startswith('xgboost_corners') and f.endswith('.pkl')]
1089
- scaler_files = [f for f in os.listdir(models_dir) if f.startswith('scaler_corners') and f.endswith('.pkl')]
1090
 
1091
- if not model_files or not scaler_files:
1092
- raise FileNotFoundError(
1093
- f"\n❌ ERROR: No se encontraron modelos en '{models_dir}'\n"
1094
- f" Modelos disponibles: {model_files}\n"
1095
- f" Scalers disponibles: {scaler_files}\n\n"
1096
- f"💡 Solución: Entrena un modelo primero ejecutando:\n"
1097
- f" python src/models/train_model.py\n"
1098
- )
1099
-
1100
- # Tomar el más reciente (o específico)
1101
- model_file = sorted(model_files)[-1] # Último alfabéticamente
1102
- scaler_file = sorted(scaler_files)[-1]
1103
-
1104
- model_path = os.path.join(models_dir, model_file)
1105
- scaler_path = os.path.join(models_dir, scaler_file)
1106
-
1107
- print(f"📦 Cargando modelo: {model_file}")
1108
- print(f"📦 Cargando scaler: {scaler_file}")
1109
 
1110
  try:
1111
- self.xgb_model = joblib.load(model_path)
1112
- self.scaler = joblib.load(scaler_path)
1113
- print("✅ Modelos cargados correctamente")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1114
  except Exception as e:
1115
  raise Exception(f"❌ Error cargando modelos: {str(e)}")
1116
 
1117
  def load_data(self):
1118
- """Cargar datos con manejo de errores"""
1119
-
1120
- project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))
1121
 
1122
- historic_path = os.path.join(project_root, "dataset/cleaned/dataset_cleaned.csv")
1123
- current_path = os.path.join(project_root, "dataset/cleaned/dataset_cleaned_current_year.csv")
1124
 
1125
- print(f"📂 Buscando datos en: {historic_path}")
 
 
1126
 
1127
- if not os.path.exists(historic_path):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1128
  raise FileNotFoundError(
1129
- f"\n❌ ERROR: No se encontró dataset histórico\n"
1130
- f" Ruta buscada: {historic_path}\n\n"
1131
- f"💡 Solución: Genera el dataset ejecutando:\n"
1132
- f" python src/process_data/generate_dataset.py\n"
1133
  )
1134
-
1135
- self.df_dataset_historic = pd.read_csv(historic_path)
1136
- print(f"✅ Dataset histórico cargado: {len(self.df_dataset_historic)} registros")
1137
-
1138
- # Intentar cargar año actual
1139
- if os.path.exists(current_path):
1140
- self.df_dataset_current_year = pd.read_csv(current_path)
1141
- print(f"✅ Dataset año actual cargado: {len(self.df_dataset_current_year)} registros")
1142
- self.df_dataset = pd.concat([self.df_dataset_historic, self.df_dataset_current_year])
1143
- else:
1144
- print("⚠️ No se encontró dataset del año actual, usando solo histórico")
1145
- self.df_dataset = self.df_dataset_historic
1146
-
1147
- # Limpieza
1148
- self.df_dataset["season"] = self.df_dataset["season"].astype(str)
1149
- self.df_dataset["Performance_Save%"].fillna(0, inplace=True)
1150
-
1151
- print(f"✅ Total registros: {len(self.df_dataset)}")
1152
 
1153
  def init_variables(self):
1154
  self.lst_years = ["1819", "1920", "2021", "2122", "2223", "2324", "2425", "2526"]
 
2
  # SISTEMA DE PREDICCIÓN DE CORNERS - OPTIMIZADO PARA APUESTAS (VERSIÓN COMPLETA)
3
  # ===========================
4
 
5
+ import requests
6
+ import tempfile
7
  import numpy as np
8
  import pandas as pd
9
  import joblib
 
11
  from scipy import stats
12
  import os
13
  import sys
14
+ from src.process_data.process_dataset import get_dataframes,get_head_2_head,get_points_from_result,get_team_ppp,get_ppp_difference,get_average
15
+ #from process_data.process_dataset import get_dataframes,get_head_2_head,get_points_from_result,get_team_ppp,get_ppp_difference,get_average
16
+ #project_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '../..'))
17
+ #sys.path.insert(0, project_root)
18
  # ===========================
19
  # 1. FUNCIONES FIABILIDAD
20
  # ===========================
 
314
  else:
315
  return "BAJA ❌"
316
 
317
+ '''
318
  def get_dataframes(df, season, round_num, local, away, league=None):
319
  """Retorna 8 DataFrames filtrados por equipo, venue y liga"""
320
 
 
526
  away_ppp = get_team_ppp(df, away, season, round_num, league)
527
  return local_ppp - away_ppp
528
 
529
+ '''
530
+
531
  def predecir_corners(local, visitante, jornada, temporada="2526", league_code="ESP",df_database=pd.DataFrame(),xgb_model="",scaler="",lst_years=[]):
532
  """
533
  Predice corners totales con análisis completo para apuestas
 
1080
  self.init_variables()
1081
 
1082
  def load_models(self):
1083
+ """Cargar modelos desde GitHub usando raw URLs"""
 
 
 
 
 
 
 
 
1084
 
1085
+ print("📦 Cargando modelos desde GitHub...")
 
 
1086
 
1087
+ # URLs de descarga directa (raw.githubusercontent.com)
1088
+ base_url = "https://raw.githubusercontent.com/danielsaed/futbol_corners_forecast/refs/heads/main/models"
1089
+ model_url = f"{base_url}/xgboost_corners_v4_retrain.pkl"
1090
+ scaler_url = f"{base_url}/scaler_corners_v4_retrain.pkl"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1091
 
1092
  try:
1093
+ # Descargar modelo
1094
+ print(f"📥 Descargando modelo desde: {model_url}")
1095
+ response_model = requests.get(model_url, timeout=30)
1096
+ response_model.raise_for_status()
1097
+
1098
+ # Descargar scaler
1099
+ print(f"📥 Descargando scaler desde: {scaler_url}")
1100
+ response_scaler = requests.get(scaler_url, timeout=30)
1101
+ response_scaler.raise_for_status()
1102
+
1103
+ # Guardar temporalmente y cargar
1104
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.pkl') as tmp_model:
1105
+ tmp_model.write(response_model.content)
1106
+ tmp_model_path = tmp_model.name
1107
+
1108
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.pkl') as tmp_scaler:
1109
+ tmp_scaler.write(response_scaler.content)
1110
+ tmp_scaler_path = tmp_scaler.name
1111
+
1112
+ # Cargar modelos desde archivos temporales
1113
+ self.xgb_model = joblib.load(tmp_model_path)
1114
+ self.scaler = joblib.load(tmp_scaler_path)
1115
+
1116
+ # Limpiar archivos temporales
1117
+ os.unlink(tmp_model_path)
1118
+ os.unlink(tmp_scaler_path)
1119
+
1120
+ print("✅ Modelos cargados correctamente desde GitHub")
1121
+
1122
+ except requests.exceptions.RequestException as e:
1123
+ raise Exception(f"❌ Error descargando modelos: {str(e)}")
1124
  except Exception as e:
1125
  raise Exception(f"❌ Error cargando modelos: {str(e)}")
1126
 
1127
  def load_data(self):
1128
+ """Cargar datos desde GitHub"""
 
 
1129
 
1130
+ print("📂 Cargando datos desde GitHub...")
 
1131
 
1132
+ base_url = "https://raw.githubusercontent.com/danielsaed/futbol_corners_forecast/refs/heads/main/dataset/cleaned"
1133
+ historic_url = f"{base_url}/dataset_cleaned.csv"
1134
+ current_url = f"{base_url}/dataset_cleaned_current_year.csv"
1135
 
1136
+ try:
1137
+ # Cargar dataset histórico
1138
+ print(f"📥 Descargando dataset histórico...")
1139
+ self.df_dataset_historic = pd.read_csv(historic_url)
1140
+ print(f"✅ Dataset histórico cargado: {len(self.df_dataset_historic)} registros")
1141
+
1142
+ # Intentar cargar año actual
1143
+ try:
1144
+ print(f"📥 Descargando dataset año actual...")
1145
+ self.df_dataset_current_year = pd.read_csv(current_url)
1146
+ print(f"✅ Dataset año actual cargado: {len(self.df_dataset_current_year)} registros")
1147
+ self.df_dataset = pd.concat([self.df_dataset_historic, self.df_dataset_current_year])
1148
+ except:
1149
+ print("⚠️ No se pudo cargar dataset del año actual, usando solo histórico")
1150
+ self.df_dataset = self.df_dataset_historic
1151
+
1152
+ # Limpieza
1153
+ self.df_dataset["season"] = self.df_dataset["season"].astype(str)
1154
+ self.df_dataset["Performance_Save%"].fillna(0, inplace=True)
1155
+
1156
+ print(f"✅ Total registros: {len(self.df_dataset)}")
1157
+
1158
+ except Exception as e:
1159
  raise FileNotFoundError(
1160
+ f"\n❌ ERROR: No se pudieron cargar los datos desde GitHub\n"
1161
+ f" Error: {str(e)}\n\n"
1162
+ f"💡 Verifica que los archivos existan en el repositorio\n"
 
1163
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1164
 
1165
  def init_variables(self):
1166
  self.lst_years = ["1819", "1920", "2021", "2122", "2223", "2324", "2425", "2526"]