### IMPORTACIONES import numpy as np import pandas as pd import random import io import math import os import gradio as gr import plotly.graph_objects as go from plotly.subplots import make_subplots import base64 import sys from sklearn.model_selection import KFold from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score from sklearn.preprocessing import StandardScaler, OneHotEncoder, OrdinalEncoder, LabelEncoder from sklearn.compose import ColumnTransformer from sklearn.pipeline import Pipeline import random import warnings import joblib from joblib import dump, load from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import cross_val_score from sklearn.metrics import accuracy_score import json # Ignorar warnings warnings.filterwarnings('ignore') try: if sys.platform == "linux" and "multiprocessing" in sys.modules: multiprocessing.set_start_method('spawn', force=True) except Exception as e: pass IMAGEN_PORTADA_PATH = "ImagenPortada.jpg" IMAGEN_ENCABEZADO_PATH = "ImagenEncabezado.jpg" # Para cargar imágenes en String Base64 def cargar_imagen_base64(path): try: with open(path, "rb") as f: encoded_image = base64.b64encode(f.read()).decode("utf-8") return f"data:image/jpeg;base64,{encoded_image}", "" except FileNotFoundError: error_msg = f"ERROR: No se encontró el archivo en la ruta: {path}. Usando fallback." return "none", error_msg IMAGEN_PORTADA = cargar_imagen_base64(IMAGEN_PORTADA_PATH)[0] IMAGEN_ENCABEZADO = cargar_imagen_base64(IMAGEN_ENCABEZADO_PATH)[0] ### SE CARGA EL MODELO RANDOM FOREST CON LOS HIPERPARÁMETROS OPTIMIZADOS ### SE CARGA EL MISMO PREPROCESADOR APLICADO ORIGINALMENTE A LOS DATOS COLUMNAS_ORDINALES = ['Educacion'] COLUMNAS_NOMINALES = ['Residencia', 'Genero'] COLUMNAS_NUMERICAS = ['Edad', 'Ingreso'] COLUMNA_TARGET = 'Partido' # Necesario para la decodificación de la predicción # Archivos guardados MODELO_ARCHIVO = 'modelo_rf_optimizado_deap.pkl' PREPROCESADOR_ARCHIVO = 'preprocesador_votacion.pkl' # Cargar los objetos entrenados try: modelo_cargado = load(MODELO_ARCHIVO) preprocesador_cargado = load(PREPROCESADOR_ARCHIVO) # print(f"Modelo y Preprocesador cargados.") # 🚨 DEBUG CRUCIAL: Verificar el estado interno from sklearn.utils.validation import check_is_fitted try: check_is_fitted(preprocesador_cargado) # print("✅ EL PREPROCESADOR ESTÁ CONSIDERADO 'FITTED' INTERNAMENTE.") except Exception as e: # Si esta línea se ejecuta, el objeto guardado ESTÁ CORRUPTO o es la causa del error. print(f"❌ ADVERTENCIA: La verificación de check_is_fitted falló después de la carga: {e}") # Recuperar el LabelEncoder para decodificar la predicción # Como el label Encoder no se guardó antes, hay que reentrenarlo con las etiquetas originales, # entonces se cargan las etiquetas originales para entrenarlo nuevamente. try: df_votacion = pd.read_csv('votacion.csv', encoding='utf-8') le = LabelEncoder() le.fit(df_votacion[COLUMNA_TARGET]) # Para las etiquetas de los partidos políticos except Exception as e: print(f"ERROR al cargar datos o LabelEncoder: {e}. La predicción será un número.") le = None # Si falla, se devuelve el número except FileNotFoundError as e: print(f"ERROR: Archivo no encontrado. Asegúrate de que '{e.filename}' exista.") # Terminar la ejecución si los archivos esenciales faltan ### FUNCIONES PARA GRADIO def intencion_voto(edad, genero, ingreso, educacion, residencia): # DataFrame con los nuevos datos para la predicción # (Con los mismos nombres de columnas y orden del entrenamiento) X_nuevo_df = pd.DataFrame({ 'Edad': [edad], 'Ingreso': [ingreso], 'Educacion': [educacion], 'Residencia': [residencia], 'Genero': [genero] }) # Transformar los datos con el objeto preprocesador entrenado # Se usa .transform(), no .fit_transform()!! X_nuevo_procesado = preprocesador_cargado.transform(X_nuevo_df) # Realizar la predicción # La función .predict() espera datos con la misma forma y transformación. prediccion_encoded = modelo_cargado.predict(X_nuevo_procesado) print(prediccion_encoded) # 4. Decodificar la predicción numérica (0, 1, 2, etc.) a la etiqueta original del partido if le is not None: prediccion_partido = le.inverse_transform(prediccion_encoded) print(prediccion_partido) return prediccion_partido[0] # Retorna la etiqueta del partido else: return f"Predicción numérica (sin decodificar): {prediccion_encoded[0]}" ### DEFINIR LOS ESTILOS PARA LA INTERFAZ GRADIO # Usamos las variables IMAGEN_PORTADA e IMAGEN_ENCABEZADO que contienen el Base64 css_style = f""" /* ESTILO PARA LA TARJETA DE PORTADA*/ .votos-background-card {{ background-image: url('{IMAGEN_PORTADA}'); background-size: cover; background-position: center; background-blend-mode: multiply; background-color: rgba(255, 255, 255, 0.2); border-radius: 15px; box-shadow: 8px 8px 10px rgba(0, 0, 0, 0.7); padding: 30px; min-height: 500px; }} /* ESTILO PARA LA TARJETA DE ENCABEZADO */ .votos-header-card {{ background-image: url('{IMAGEN_ENCABEZADO}'); background-size: cover; border-radius: 15px; box-shadow: 8px 8px 10px rgba(0, 0, 0, 0.7); padding: 30px; height: 50px; }} /* ESTILO PARA EL TÍTULO PRINCIPAL */ .title * {{ font-size: 40px !important; text-shadow: 5px 5px 5px rgba(0, 0, 0, 1); line-height: 1.3; color: white !important; }} /* ESTILO PARA EL SUBTÍTULO */ .subtitle * {{ font-size: 20px !important; font-weight: 400; margin-top: 10px; line-height: 1.3; text-shadow: 3px 3px 3px rgba(0, 0, 0, 1); color: white !important; }} /* ESTILO PARA TÍTULO DE PANEL */ .panel_title * {{ font-size: 20px !important; font-weight: 400; margin-top: 10px; line-height: 1.3; text-shadow: 3px 3px 3px rgba(0, 0, 0, 1); color: white !important; }} /* ESTILO DE RECTÁNGULO NARANJA SEMITRANSPARENTE */ .info-box {{ background-color: rgba(255, 165, 0, 0.6) !important; border-radius: 15px !important; /* border: 2px solid rgba(200, 130, 0, 0.8) !important; */ box-shadow: 0px 4px 8px rgba(0, 0, 0, 0.5) !important; padding: 20px !important; color: white !important; }} """ ### INTERFAZ GRADIO with gr.Blocks(css=css_style) as application: gr.Row(elem_classes=["votos-header-card"]) with gr.Tab("Inicio"): with gr.Column(elem_classes=["votos-background-card"]): gr.Markdown("ESTIMACIÓN DE
LA INTENCIÓN DE VOTO", elem_classes=["title"]) with gr.Row(elem_classes=["info-box"]): gr.Markdown("



Esta aplicación se basa en el Método Random Forest
" "aplicado a un dataset de votantes de
" "las últimas elecciones de medio término
" "para estimar la intención de voto en los próximos comicios
" "considerando sus características (edad, género, ingreso,
" "nivel educativo y lugar de residencia).
" "Los hiperparámetros del modelo se optimizaron aplicando
" "un algoritmo genético.", elem_classes=["subtitle"]) with gr.Tab("Datos"): with gr.Row(): gr.Markdown("CONJUNTO SINTÉTICO DE DATOS SOBRE VOTACIÓN OCTUBRE 2025", elem_classes=["panel_title"]) with gr.Row(): data_table = gr.Dataframe(value=df_votacion, headers=list(df_votacion.columns), datatype=["number", "number", "str", "str", "str", "str"], row_count=len(df_votacion), col_count=len(df_votacion.columns), # label="VOTANTES ELECCIONES NACIONALES OCTUBRE 2025", interactive=False, wrap=True) with gr.Tab("Intención de Voto"): with gr.Row(): gr.Markdown("CARACTERÍSTICAS DEL CIUDADANO", elem_classes=["panel_title"]) with gr.Row(): with gr.Column(scale=1): input_edad = gr.Slider(minimum=18, maximum=85, step=1, value=18, label="Edad", interactive=True) input_genero = gr.Radio(choices=["Femenino", "Masculino"], label="Género", value="Femenino", interactive=True) input_ingresos = gr.Slider(minimum=300000, maximum=5000000, step=25000, value=700000, label="Ingresos", interactive=True) with gr.Column(scale=1): input_educacion = gr.Dropdown(choices=["Primario", "Secundario", "Universitario", "Postgrado"], value="Primario", label="Nivel Educativo", interactive=True) input_residencia = gr.Dropdown(choices=["Urbana Alta", "Urbana Media", "Urbana Marginal", "Rural Media", "Rural profunda"], value="Urbana Media", label="Zona de Residencia", interactive=True) run_button = gr.Button("Ejecutar Simulación", interactive=True) with gr.Row(): output_voto = gr.Textbox(label="Intención de Voto", interactive=False, lines=1) run_button.click( intencion_voto, inputs=[input_edad, input_genero, input_ingresos, input_educacion, input_residencia], outputs=[output_voto] ) if __name__ == "__main__": application.launch()