diff --git "a/app.py" "b/app.py" --- "a/app.py" +++ "b/app.py" @@ -1,3301 +1,3305 @@ -""" -APLICACIÓN WEB GRADIO - VISUALIZACIONES ODS -============================================ - -Aplicación interactiva que permite explorar las 10 visualizaciones -de análisis de similaridad ODS a través de una interfaz web amigable. - -Características: -- Interfaz con pestañas para cada visualización -- Explicaciones integradas para público general -- Visualizaciones interactivas (HTML) y estáticas (PNG) -- Estadísticas en tiempo real -- Diseño responsivo y profesional - -Autor: Daniel Sandvoval -Fecha: Noviembre 2025 -""" - -import gradio as gr -import pandas as pd -import numpy as np -from pathlib import Path -import plotly.graph_objects as go -import plotly.express as px -from plotly.subplots import make_subplots -import matplotlib.pyplot as plt -import seaborn as sns -from src.embeddings.modelos_nlp_db import search -from src.embeddings.mass_modelos_nlp_db import search_mass -from huggingface_hub import InferenceClient -# import pandas as pd -import re -import time - - -# Importar funciones de visualización -import sys -# sys.path.insert(0, '/home/claude') -from src.visualization.visualizaciones_ods import ( - cargar_datos, - viz_1_distribucion_por_ods, - viz_2_heatmap_ods_ranking, - viz_3_scatter_3d_interactivo, - viz_4_radar_chart_ods, - viz_5_sunburst_jerarquia, - viz_6_top_indicadores_por_ods, - viz_7_streamgraph_similaridad, - viz_8_violin_plot_ods, - viz_9_dashboard_metricas, - viz_10_matriz_transicion, - viz_19_resumen_tags, - viz_20_pareto_ods, - viz_21_ranking_mixto_masivo, - viz_22_composicion_score_mixto, - viz_24_header_conecta_ods, - analisis_estadistico -) - -# ============================================================================ -# CONFIGURACIÓN GLOBAL -# ============================================================================ -import os -import base64 - -levels = ['ODS_ID','META_ID','INDICADOR_ID'] - - - - -def convertir_logo_a_base64(logo_path): - """Convierte un logo a base64 para incrustar en HTML""" - # try: - # rutas_posibles = [ - # logo_path, - # os.path.join(os.path.dirname(__file__), logo_path), - # os.path.join('/mnt/user-data/outputs', logo_path), - # ] - - # for ruta in rutas_posibles: - # if os.path.exists(ruta): - # with open(ruta, "rb") as image_file: - # encoded = base64.b64encode(image_file.read()).decode() - # return f"data:image/png;base64,{encoded}" - - # print(f"⚠️ Logo no encontrado: {logo_path}") - # return "" - # except Exception as e: - # print(f"⚠️ Error al cargar logo: {e}") - # return "" - ruta = Path('config/institucional/logos') - with open(Path(f'{ruta}/{logo_path}'), "rb") as image_file: - encoded = base64.b64encode(image_file.read()).decode() - return f"data:image/png;base64,{encoded}" - - - -# Cargar logos una sola vez al iniciar -print("Cargando logos institucionales...") -LOGO_GOBIERNO = convertir_logo_a_base64("/institucional/GOBIERNO-DE-COLOMBIA_HORIZONTAL.webp") -LOGO_FONDO = convertir_logo_a_base64("/institucional/LOGO MPTF (ESP).webp") - - -if LOGO_GOBIERNO and LOGO_FONDO: - print("✅ Logos cargados correctamente") -else: - print("⚠️ Algunos logos no se pudieron cargar") - -# Diccionario de colores oficiales de los ODS (Fuente: Guías de la ONU) -colores_ods = { - "1": "#E5243B", # Red - ODS 1: Fin de la Pobreza - "2": "#DDA63A", # Mustard - ODS 2: Hambre Cero - "3": "#4C9F38", # Kelly Green - ODS 3: Salud y Bienestar - "4": "#C5192D", # Dark Red - ODS 4: Educación de Calidad - "5": "#FF3A21", # Red Orange - ODS 5: Igualdad de Género - "6": "#26BDE2", # Bright Blue - ODS 6: Agua Limpia y Saneamiento - "7": "#FCC30B", # Yellow - ODS 7: Energía Asequible y No Contaminante - "8": "#A21942", # Burgundy Red - ODS 8: Trabajo Decente y Crecimiento Económico - "9": "#FD6925", # Orange - ODS 9: Industria, Innovación e Infraestructura - "10": "#DD1367", # Magenta - ODS 10: Reducción de las Desigualdades - "11": "#FD9D24", # Golden Yellow - ODS 11: Ciudades y Comunidades Sostenibles - "12": "#BF8B2E", # Dark Mustard - ODS 12: Producción y Consumo Responsables - "13": "#3F7E44", # Dark Green - ODS 13: Acción por el Clima - "14": "#0A97D9", # Blue - ODS 14: Vida Submarina - "15": "#56C02B", # Lime Green - ODS 15: Vida de Ecosistemas Terrestres - "16": "#00689D", # Royal Blue - ODS 16: Paz, Justicia e Instituciones Sólidas - "17": "#19486A", # Navy Blue - ODS 17: Alianzas para Lograr los Objetivos -} - -# Color adicional para el logotipo general de los ODS -color_logo_ods = "#009EDB" # Logo Blue - -dict_logos = { - 'gobierno': convertir_logo_a_base64("/institucional/GOBIERNO-DE-COLOMBIA_HORIZONTAL.webp"), - 'fondo_un': convertir_logo_a_base64("/institucional/LOGO MPTF (ESP).webp"), - 'logo_conectaods': convertir_logo_a_base64("/institucional/logo_conectaods_2.webp"), - 'ods_1': convertir_logo_a_base64("/ods/S-WEB-Goal-01.webp"), - 'ods_2': convertir_logo_a_base64("/ods/S-WEB-Goal-02.webp"), - 'ods_3': convertir_logo_a_base64("/ods/S-WEB-Goal-03.webp"), - 'ods_4': convertir_logo_a_base64("/ods/S-WEB-Goal-04.webp"), - 'ods_5': convertir_logo_a_base64("/ods/S-WEB-Goal-05.webp"), - 'ods_6': convertir_logo_a_base64("/ods/S-WEB-Goal-06.webp"), - 'ods_7': convertir_logo_a_base64("/ods/S-WEB-Goal-07.webp"), - 'ods_8': convertir_logo_a_base64("/ods/S-WEB-Goal-08.webp"), - 'ods_9': convertir_logo_a_base64("/ods/S-WEB-Goal-09.webp"), - 'ods_10': convertir_logo_a_base64("/ods/S-WEB-Goal-10.webp"), - 'ods_11': convertir_logo_a_base64("/ods/S-WEB-Goal-11.webp"), - 'ods_12': convertir_logo_a_base64("/ods/S-WEB-Goal-12.webp"), - 'ods_13': convertir_logo_a_base64("/ods/S-WEB-Goal-13.webp"), - 'ods_14': convertir_logo_a_base64("/ods/S-WEB-Goal-14.webp"), - 'ods_15': convertir_logo_a_base64("/ods/S-WEB-Goal-15.webp"), - 'ods_16': convertir_logo_a_base64("/ods/S-WEB-Goal-16.webp"), - 'ods_17': convertir_logo_a_base64("/ods/S-WEB-Goal-17.webp"), - # Metas ODS - 'meta_1.1': convertir_logo_a_base64("/metas/MC_Target_1.1.webp"), - 'meta_1.2': convertir_logo_a_base64("/metas/MC_Target_1.2.webp"), - 'meta_1.3': convertir_logo_a_base64("/metas/MC_Target_1.3.webp"), - 'meta_1.4': convertir_logo_a_base64("/metas/MC_Target_1.4.webp"), - 'meta_1.5': convertir_logo_a_base64("/metas/MC_Target_1.5.webp"), - 'meta_1.A': convertir_logo_a_base64("/metas/MC_Target_1.A.webp"), - 'meta_1.B': convertir_logo_a_base64("/metas/MC_Target_1.B.webp"), - 'meta_2.1': convertir_logo_a_base64("/metas/MC_Target_2.1.webp"), - 'meta_2.2': convertir_logo_a_base64("/metas/MC_Target_2.2.webp"), - 'meta_2.3': convertir_logo_a_base64("/metas/MC_Target_2.3.webp"), - 'meta_2.4': convertir_logo_a_base64("/metas/MC_Target_2.4.webp"), - 'meta_2.5': convertir_logo_a_base64("/metas/MC_Target_2.5.webp"), - 'meta_2.A': convertir_logo_a_base64("/metas/MC_Target_2.A.webp"), - 'meta_2.B': convertir_logo_a_base64("/metas/MC_Target_2.B.webp"), - 'meta_2.C': convertir_logo_a_base64("/metas/MC_Target_2.C.webp"), - 'meta_3.1': convertir_logo_a_base64("/metas/MC_Target_3.1.webp"), - 'meta_3.2': convertir_logo_a_base64("/metas/MC_Target_3.2.webp"), - 'meta_3.3': convertir_logo_a_base64("/metas/MC_Target_3.3.webp"), - 'meta_3.4': convertir_logo_a_base64("/metas/MC_Target_3.4.webp"), - 'meta_3.5': convertir_logo_a_base64("/metas/MC_Target_3.5.webp"), - 'meta_3.6': convertir_logo_a_base64("/metas/MC_Target_3.6.webp"), - 'meta_3.7': convertir_logo_a_base64("/metas/MC_Target_3.7.webp"), - 'meta_3.8': convertir_logo_a_base64("/metas/MC_Target_3.8.webp"), - 'meta_3.9': convertir_logo_a_base64("/metas/MC_Target_3.9.webp"), - 'meta_3.A': convertir_logo_a_base64("/metas/MC_Target_3.A.webp"), - 'meta_3.B': convertir_logo_a_base64("/metas/MC_Target_3.B.webp"), - 'meta_3.C': convertir_logo_a_base64("/metas/MC_Target_3.C.webp"), - 'meta_3.D': convertir_logo_a_base64("/metas/MC_Target_3.D.webp"), - 'meta_4.1': convertir_logo_a_base64("/metas/MC_Target_4.1.webp"), - 'meta_4.2': convertir_logo_a_base64("/metas/MC_Target_4.2.webp"), - 'meta_4.3': convertir_logo_a_base64("/metas/MC_Target_4.3.webp"), - 'meta_4.4': convertir_logo_a_base64("/metas/MC_Target_4.4.webp"), - 'meta_4.5': convertir_logo_a_base64("/metas/MC_Target_4.5.webp"), - 'meta_4.6': convertir_logo_a_base64("/metas/MC_Target_4.6.webp"), - 'meta_4.7': convertir_logo_a_base64("/metas/MC_Target_4.7.webp"), - 'meta_4.A': convertir_logo_a_base64("/metas/MC_Target_4.A.webp"), - 'meta_4.B': convertir_logo_a_base64("/metas/MC_Target_4.B.webp"), - 'meta_4.C': convertir_logo_a_base64("/metas/MC_Target_4.C.webp"), - 'meta_5.1': convertir_logo_a_base64("/metas/MC_Target_5.1.webp"), - 'meta_5.2': convertir_logo_a_base64("/metas/MC_Target_5.2.webp"), - 'meta_5.3': convertir_logo_a_base64("/metas/MC_Target_5.3.webp"), - 'meta_5.4': convertir_logo_a_base64("/metas/MC_Target_5.4.webp"), - 'meta_5.5': convertir_logo_a_base64("/metas/MC_Target_5.5.webp"), - 'meta_5.6': convertir_logo_a_base64("/metas/MC_Target_5.6.webp"), - 'meta_5.A': convertir_logo_a_base64("/metas/MC_Target_5.A.webp"), - 'meta_5.B': convertir_logo_a_base64("/metas/MC_Target_5.B.webp"), - 'meta_5.C': convertir_logo_a_base64("/metas/MC_Target_5.C.webp"), - 'meta_6.1': convertir_logo_a_base64("/metas/MC_Target_6.1.webp"), - 'meta_6.2': convertir_logo_a_base64("/metas/MC_Target_6.2.webp"), - 'meta_6.3': convertir_logo_a_base64("/metas/MC_Target_6.3.webp"), - 'meta_6.4': convertir_logo_a_base64("/metas/MC_Target_6.4.webp"), - 'meta_6.5': convertir_logo_a_base64("/metas/MC_Target_6.5.webp"), - 'meta_6.6': convertir_logo_a_base64("/metas/MC_Target_6.6.webp"), - 'meta_6.A': convertir_logo_a_base64("/metas/MC_Target_6.A.webp"), - 'meta_6.B': convertir_logo_a_base64("/metas/MC_Target_6.B.webp"), - 'meta_7.1': convertir_logo_a_base64("/metas/MC_Target_7.1.webp"), - 'meta_7.2': convertir_logo_a_base64("/metas/MC_Target_7.2.webp"), - 'meta_7.3': convertir_logo_a_base64("/metas/MC_Target_7.3.webp"), - 'meta_7.A': convertir_logo_a_base64("/metas/MC_Target_7.A.webp"), - 'meta_7.B': convertir_logo_a_base64("/metas/MC_Target_7.B.webp"), - 'meta_8.1': convertir_logo_a_base64("/metas/MC_Target_8.1.webp"), - 'meta_8.2': convertir_logo_a_base64("/metas/MC_Target_8.2.webp"), - 'meta_8.3': convertir_logo_a_base64("/metas/MC_Target_8.3.webp"), - 'meta_8.4': convertir_logo_a_base64("/metas/MC_Target_8.4.webp"), - 'meta_8.5': convertir_logo_a_base64("/metas/MC_Target_8.5.webp"), - 'meta_8.6': convertir_logo_a_base64("/metas/MC_Target_8.6.webp"), - 'meta_8.7': convertir_logo_a_base64("/metas/MC_Target_8.7.webp"), - 'meta_8.8': convertir_logo_a_base64("/metas/MC_Target_8.8.webp"), - 'meta_8.9': convertir_logo_a_base64("/metas/MC_Target_8.9.webp"), - 'meta_8.10': convertir_logo_a_base64("/metas/MC_Target_8.10.webp"), - 'meta_8.A': convertir_logo_a_base64("/metas/MC_Target_8.A.webp"), - 'meta_8.B': convertir_logo_a_base64("/metas/MC_Target_8.B.webp"), - 'meta_9.1': convertir_logo_a_base64("/metas/MC_Target_9.1.webp"), - 'meta_9.2': convertir_logo_a_base64("/metas/MC_Target_9.2.webp"), - 'meta_9.3': convertir_logo_a_base64("/metas/MC_Target_9.3.webp"), - 'meta_9.4': convertir_logo_a_base64("/metas/MC_Target_9.4.webp"), - 'meta_9.5': convertir_logo_a_base64("/metas/MC_Target_9.5.webp"), - 'meta_9.A': convertir_logo_a_base64("/metas/MC_Target_9.A.webp"), - 'meta_9.B': convertir_logo_a_base64("/metas/MC_Target_9.B.webp"), - 'meta_9.C': convertir_logo_a_base64("/metas/MC_Target_9.C.webp"), - 'meta_10.1': convertir_logo_a_base64("/metas/MC_Target_10.1.webp"), - 'meta_10.2': convertir_logo_a_base64("/metas/MC_Target_10.2.webp"), - 'meta_10.3': convertir_logo_a_base64("/metas/MC_Target_10.3.webp"), - 'meta_10.4': convertir_logo_a_base64("/metas/MC_Target_10.4.webp"), - 'meta_10.5': convertir_logo_a_base64("/metas/MC_Target_10.5.webp"), - 'meta_10.6': convertir_logo_a_base64("/metas/MC_Target_10.6.webp"), - 'meta_10.7': convertir_logo_a_base64("/metas/MC_Target_10.7.webp"), - 'meta_10.A': convertir_logo_a_base64("/metas/MC_Target_10.A.webp"), - 'meta_10.B': convertir_logo_a_base64("/metas/MC_Target_10.B.webp"), - 'meta_10.C': convertir_logo_a_base64("/metas/MC_Target_10.C.webp"), - 'meta_11.1': convertir_logo_a_base64("/metas/MC_Target_11.1.webp"), - 'meta_11.2': convertir_logo_a_base64("/metas/MC_Target_11.2.webp"), - 'meta_11.3': convertir_logo_a_base64("/metas/MC_Target_11.3.webp"), - 'meta_11.4': convertir_logo_a_base64("/metas/MC_Target_11.4.webp"), - 'meta_11.5': convertir_logo_a_base64("/metas/MC_Target_11.5.webp"), - 'meta_11.6': convertir_logo_a_base64("/metas/MC_Target_11.6.webp"), - 'meta_11.7': convertir_logo_a_base64("/metas/MC_Target_11.7.webp"), - 'meta_11.A': convertir_logo_a_base64("/metas/MC_Target_11.A.webp"), - 'meta_11.B': convertir_logo_a_base64("/metas/MC_Target_11.B.webp"), - 'meta_11.C': convertir_logo_a_base64("/metas/MC_Target_11.C.webp"), - 'meta_12.1': convertir_logo_a_base64("/metas/MC_Target_12.1.webp"), - 'meta_12.2': convertir_logo_a_base64("/metas/MC_Target_12.2.webp"), - 'meta_12.3': convertir_logo_a_base64("/metas/MC_Target_12.3.webp"), - 'meta_12.4': convertir_logo_a_base64("/metas/MC_Target_12.4.webp"), - 'meta_12.5': convertir_logo_a_base64("/metas/MC_Target_12.5.webp"), - 'meta_12.6': convertir_logo_a_base64("/metas/MC_Target_12.6.webp"), - 'meta_12.7': convertir_logo_a_base64("/metas/MC_Target_12.7.webp"), - 'meta_12.8': convertir_logo_a_base64("/metas/MC_Target_12.8.webp"), - 'meta_12.A': convertir_logo_a_base64("/metas/MC_Target_12.A.webp"), - 'meta_12.B': convertir_logo_a_base64("/metas/MC_Target_12.B.webp"), - 'meta_12.C': convertir_logo_a_base64("/metas/MC_Target_12.C.webp"), - 'meta_13.1': convertir_logo_a_base64("/metas/MC_Target_13.1.webp"), - 'meta_13.2': convertir_logo_a_base64("/metas/MC_Target_13.2.webp"), - 'meta_13.3': convertir_logo_a_base64("/metas/MC_Target_13.3.webp"), - 'meta_13.A': convertir_logo_a_base64("/metas/MC_Target_13.A.webp"), - 'meta_13.B': convertir_logo_a_base64("/metas/MC_Target_13.B.webp"), - 'meta_14.1': convertir_logo_a_base64("/metas/MC_Target_14.1.webp"), - 'meta_14.2': convertir_logo_a_base64("/metas/MC_Target_14.2.webp"), - 'meta_14.3': convertir_logo_a_base64("/metas/MC_Target_14.3.webp"), - 'meta_14.4': convertir_logo_a_base64("/metas/MC_Target_14.4.webp"), - 'meta_14.5': convertir_logo_a_base64("/metas/MC_Target_14.5.webp"), - 'meta_14.6': convertir_logo_a_base64("/metas/MC_Target_14.6.webp"), - 'meta_14.7': convertir_logo_a_base64("/metas/MC_Target_14.7.webp"), - 'meta_14.A': convertir_logo_a_base64("/metas/MC_Target_14.A.webp"), - 'meta_14.B': convertir_logo_a_base64("/metas/MC_Target_14.B.webp"), - 'meta_14.C': convertir_logo_a_base64("/metas/MC_Target_14.C.webp"), - 'meta_15.1': convertir_logo_a_base64("/metas/MC_Target_15.1.webp"), - 'meta_15.2': convertir_logo_a_base64("/metas/MC_Target_15.2.webp"), - 'meta_15.3': convertir_logo_a_base64("/metas/MC_Target_15.3.webp"), - 'meta_15.4': convertir_logo_a_base64("/metas/MC_Target_15.4.webp"), - 'meta_15.5': convertir_logo_a_base64("/metas/MC_Target_15.5.webp"), - 'meta_15.6': convertir_logo_a_base64("/metas/MC_Target_15.6.webp"), - 'meta_15.7': convertir_logo_a_base64("/metas/MC_Target_15.7.webp"), - 'meta_15.8': convertir_logo_a_base64("/metas/MC_Target_15.8.webp"), - 'meta_15.9': convertir_logo_a_base64("/metas/MC_Target_15.9.webp"), - 'meta_15.A': convertir_logo_a_base64("/metas/MC_Target_15.A.webp"), - 'meta_15.B': convertir_logo_a_base64("/metas/MC_Target_15.B.webp"), - 'meta_15.C': convertir_logo_a_base64("/metas/MC_Target_15.C.webp"), - 'meta_16.1': convertir_logo_a_base64("/metas/MC_Target_16.1.webp"), - 'meta_16.2': convertir_logo_a_base64("/metas/MC_Target_16.2.webp"), - 'meta_16.3': convertir_logo_a_base64("/metas/MC_Target_16.3.webp"), - 'meta_16.4': convertir_logo_a_base64("/metas/MC_Target_16.4.webp"), - 'meta_16.5': convertir_logo_a_base64("/metas/MC_Target_16.5.webp"), - 'meta_16.6': convertir_logo_a_base64("/metas/MC_Target_16.6.webp"), - 'meta_16.7': convertir_logo_a_base64("/metas/MC_Target_16.7.webp"), - 'meta_16.8': convertir_logo_a_base64("/metas/MC_Target_16.8.webp"), - 'meta_16.9': convertir_logo_a_base64("/metas/MC_Target_16.9.webp"), - 'meta_16.10': convertir_logo_a_base64("/metas/MC_Target_16.10.webp"), - 'meta_16.A': convertir_logo_a_base64("/metas/MC_Target_16.A.webp"), - 'meta_16.B': convertir_logo_a_base64("/metas/MC_Target_16.B.webp"), - 'meta_17.1': convertir_logo_a_base64("/metas/MC_Target_17.1.webp"), - 'meta_17.2': convertir_logo_a_base64("/metas/MC_Target_17.2.webp"), - 'meta_17.3': convertir_logo_a_base64("/metas/MC_Target_17.3.webp"), - 'meta_17.4': convertir_logo_a_base64("/metas/MC_Target_17.4.webp"), - 'meta_17.5': convertir_logo_a_base64("/metas/MC_Target_17.5.webp"), - 'meta_17.6': convertir_logo_a_base64("/metas/MC_Target_17.6.webp"), - 'meta_17.7': convertir_logo_a_base64("/metas/MC_Target_17.7.webp"), - 'meta_17.8': convertir_logo_a_base64("/metas/MC_Target_17.8.webp"), - 'meta_17.9': convertir_logo_a_base64("/metas/MC_Target_17.9.webp"), - 'meta_17.10': convertir_logo_a_base64("/metas/MC_Target_17.10.webp"), - 'meta_17.11': convertir_logo_a_base64("/metas/MC_Target_17.11.webp"), - 'meta_17.12': convertir_logo_a_base64("/metas/MC_Target_17.12.webp"), - 'meta_17.13': convertir_logo_a_base64("/metas/MC_Target_17.13.webp"), - 'meta_17.14': convertir_logo_a_base64("/metas/MC_Target_17.14.webp"), - 'meta_17.15': convertir_logo_a_base64("/metas/MC_Target_17.15.webp"), - 'meta_17.16': convertir_logo_a_base64("/metas/MC_Target_17.16.webp"), - 'meta_17.17': convertir_logo_a_base64("/metas/MC_Target_17.17.webp"), - 'meta_17.18': convertir_logo_a_base64("/metas/MC_Target_17.18.webp"), - 'meta_17.19': convertir_logo_a_base64("/metas/MC_Target_17.19.webp"), - -} - -# Ruta al archivo de datos -# # RUTA_DATOS = '/mnt/user-data/uploads/indicadores_markdown.txt' -# RUTA_DATOS = '/content/drive/MyDrive/Compartida/06_Desarrollo de la herramienta IA/01_MPTF /archivos_trabajo/app_visualizaciones/indicadores_markdown.txt' - -# # Cargar datos globalmente para toda la app -# try: -# df_global = cargar_datos(RUTA_DATOS) -DATOS_CARGADOS = True -# print(f"✓ Datos cargados: {len(df_global)} registros") -# except Exception as e: -# df_global = None -# DATOS_CARGADOS = False -# print(f"✗ Error al cargar datos: {e}") - - - -# Estilos CSS personalizados -CUSTOM_CSS = """ -.gradio-container { - font-family: 'Arial', sans-serif; -} -.explanation-box { - background-color: #E8F4F8; - padding: 20px; - border-radius: 10px; - border-left: 5px solid #2E5090; - margin: 10px 0; -} -.stats-box { - background-color: #009EDB; - padding: 15px; - border-radius: 8px; - border: 2px solid #FFD700; - margin: 10px 0; -} -.important-box { - background-color: #FFE6E6; - padding: 15px; - border-radius: 8px; - border-left: 5px solid #C00000; - margin: 10px 0; -} -h1, h2, h3 { - color: #2E5090; -} -.tab-nav button { - font-size: 16px; - padding: 10px 20px; -} - -/* ESTILOS PARA HEADER CON LOGOS INSTITUCIONALES */ -.header-institucional { - display: flex; - justify-content: space-between; - align-items: center; - padding: 20px 40px; - background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 50%, #f8f9fa 100%); - border-bottom: 4px solid #003DA5; - margin-bottom: 25px; - box-shadow: 0 3px 10px rgba(0,0,0,0.08); -} - -.logo-institucional { - height: 40px; - width: auto; - object-fit: contain; -} - -.titulo-institucional { - flex: 1; - text-align: center; - padding: 0 30px; -} - -.titulo-institucional h1 { - margin: 0; - color: #003DA5 !important; - font-size: 28px; - font-weight: 700; -} - -.logo-ods-tbl { - height: 60px; - width: auto; - object-fit: contain; -} - -@media (max-width: 768px) { - .header-institucional { - padding: 15px 20px; - flex-direction: column; - gap: 15px; - } - .logo-institucional { - height: 50px; - } -} -""" - -# ============================================================================ -# FUNCIONES DE CONVERSIÓN DE FIGURAS -# ============================================================================ - -def plotly_to_html(fig): - """Convierte figura Plotly a HTML para mostrar en Gradio""" - return fig.to_html(include_plotlyjs='cdn', full_html=False) - -def matplotlib_to_file(fig, filename): - """Convierte figura Matplotlib a archivo temporal""" - import tempfile - import os - - # Crear directorio temporal si no existe - temp_dir = tempfile.gettempdir() - filepath = os.path.join(temp_dir, filename) - - # Guardar la figura - fig.savefig(filepath, format='png', dpi=150, bbox_inches='tight') - plt.close(fig) - - return filepath - -# ============================================================================ -##### FUNCIONES DE PROCESAMIENTO DE CONSULTA MASIVA -# ============================================================================ - -def calcular_peso_ranking_inverso(rank, nivel): - """ - Peso decrece linealmente: Rank 1 = peso 10, Rank 10 = peso 1 - """ - if nivel == 'ods': - max_rank = 17 - elif nivel == 'meta': - max_rank = 169 - elif nivel == 'indicador': - max_rank = 244 - return max_rank - rank + 1 - -def calcular_corte_pareto(df_iniciativa, nivel, umbral_pareto=0.8, mass = False): - """ - Determina el top_rank usando Pareto (80/20) - - Args: - df_iniciativa: DataFrame de una iniciativa con similaridad - umbral_pareto: % acumulado para corte (default 0.8 = 80%), puede ser string o float - - Returns: - top_rank: Número de elementos que conforman el 80% - """ - # Convertir umbral_pareto a float si viene como string - umbral_pareto = float(umbral_pareto) - if mass: - df_init = df_iniciativa.copy() - # Ordenar por similaridad - df_sorted = df_init.sort_values(f'score', ascending=False).reset_index(drop=True) - # Se aplica calculo exponencial para acentuar y diferenciar los pesos - df_sorted[f'{nivel}_similaridad_cos_exp3'] = df_sorted[f'score'] ** 3 - else: - - # Ordenar por similaridad descendente - df_sorted = df_iniciativa.sort_values(f'{nivel}_similaridad_cos', ascending=False).copy() - df_sorted[f'{nivel}_similaridad_cos_exp3'] = df_sorted[f'{nivel}_similaridad_cos'] ** 3 - - # Normalizar la columna ods_similaridad_cos_exp3 - min_val = df_sorted[f'{nivel}_similaridad_cos_exp3'].min() - max_val = df_sorted[f'{nivel}_similaridad_cos_exp3'].max() - if max_val - min_val > 0: - df_sorted[f'{nivel}_similaridad_cos_exp3_norm'] = (df_sorted[f'{nivel}_similaridad_cos_exp3'] - min_val) / (max_val - min_val) - else: - df_sorted[f'{nivel}_similaridad_cos_exp3_norm'] = 0.0 # Handle case where all values are the same - - # Aplicar Ranking Inverso - df_sorted['peso'] = df_sorted[f'{nivel}_rank'].apply(lambda r: calcular_peso_ranking_inverso(r, nivel)) - df_sorted['similaridad_ponderada'] = df_sorted[f'{nivel}_similaridad_cos_exp3_norm'] * df_sorted['peso'] - - # Calcular suma total - if mass: - total_similaridad = df_sorted[f'score'].sum() - else: - total_similaridad = df_sorted[f'{nivel}_similaridad_cos'].sum() - - total_similaridad_ponderda = df_sorted['similaridad_ponderada'].sum() - - # Calcular acumulado - if mass: - df_sorted['acumulado'] = df_sorted[f'score'].cumsum() - else: - df_sorted['acumulado'] = df_sorted[f'{nivel}_similaridad_cos'].cumsum() - df_sorted['porcentaje_acumulado'] = df_sorted['acumulado'] / total_similaridad - # - df_sorted['acumulado_ponderado'] = df_sorted['similaridad_ponderada'].cumsum() - df_sorted['porcentaje_acumulado_ponderado'] = df_sorted['acumulado_ponderado'] / total_similaridad_ponderda - - # Encontrar punto de corte Pareto - top_rank = (df_sorted['porcentaje_acumulado_ponderado'] <= umbral_pareto).sum() - - # Mínimo 3, máximo 10 - top_rank = max(3, min(top_rank, 245)) - - return top_rank, df_sorted.head(top_rank) - -# Uso por iniciativa -def aplicar_pareto_por_iniciativa(df_ods, nivel, umbral=0.8, mass = False): - """ - Aplica Pareto a cada iniciativa y marca el top - """ - resultados = [] - - for iniciativa_id in df_ods['INICIATIVA_ID'].unique(): - df_init = df_ods[df_ods['INICIATIVA_ID'] == iniciativa_id] - - top_rank, df_top = calcular_corte_pareto(df_init, nivel, umbral, mass) - - # Marcar los que están en el top Pareto - df_top['en_pareto'] = True - df_top['pareto_rank'] = range(1, len(df_top) + 1) - - resultados.append(df_top) - - df_pareto = pd.concat(resultados, ignore_index=True) - - return df_pareto - -def analisis_global_con_pareto(df, #df_metas, df_indicadores, - nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.50, rank_prop=0.15, mass = False): - """ - Análisis global usando Pareto para determinar top_rank por iniciativa - - Flujo: - 1. Por cada iniciativa, calcula su corte Pareto - 2. Solo usa esos ODS "críticos" para el análisis global - 3. Aplica la estrategia de ponderación elegida - """ - # Convertir parámetros a tipos correctos - umbral_pareto = float(umbral_pareto) - - # Validar que DataFrame no esté vacío - if df is None or df.empty: - return gr.update(value=pd.DataFrame()), gr.update(value=pd.DataFrame()) - - try: - # Paso 1: Aplicar Pareto a cada iniciativa - df_pareto = aplicar_pareto_por_iniciativa(df, nivel, umbral_pareto, mass) - - print(f"📊 Análisis con Pareto (umbral {umbral_pareto*100}%):") - print(f" Total registros originales: {len(df)}") - print(f" Registros en zona Pareto: {len(df_pareto)}") - print(f" Reducción: {(1 - len(df_pareto)/len(df))*100:.1f}%") - - # Paso 2: Análisis global solo con elementos Pareto - if metodo == 'ranking_inverso': - df_pareto['peso'] = 11 - df_pareto['pareto_rank'] - df_pareto['score'] = df_pareto[f'{nivel}_similaridad_cos'] * df_pareto['peso'] - resultado = df_pareto.groupby(f'{nivel.upper()}_ID').agg({ - 'score': 'sum', - 'INICIATIVA_ID': 'count' - }).rename(columns={'INICIATIVA_ID': 'frecuencia'}) - - elif metodo == 'exponencial': - df_pareto['peso'] = df_pareto[f'{nivel}_similaridad_cos_norm'] ** 2 - resultado = df_pareto.groupby(f'{nivel.upper()}_ID').agg({ - 'peso': 'sum', - 'INICIATIVA_ID': 'count' - }).rename(columns={'INICIATIVA_ID': 'frecuencia', 'peso': 'score'}) - - elif metodo == 'mixto': - stats = df_pareto.groupby(f'{nivel.upper()}_ID').agg({ - f'{nivel}_similaridad_cos_norm': 'mean', - 'pareto_rank': 'mean', - 'INICIATIVA_ID': 'count' - }) - - # Normalizar componentes - stats['sim_norm'] = stats[f'{nivel}_similaridad_cos_norm'] - stats['rank_norm'] = 1 - (stats['pareto_rank'] - stats['pareto_rank'].min()) / (stats['pareto_rank'].max() - stats['pareto_rank'].min()) - stats['freq_norm'] = stats['INICIATIVA_ID'] / stats['INICIATIVA_ID'].max() - - # Score mixto - stats['score'] = ( - sim_prop * stats['sim_norm'] + - rank_prop * stats['rank_norm'] + - (1 - sim_prop - rank_prop) * stats['freq_norm'] - ) - - resultado = stats[['score', 'INICIATIVA_ID']].reset_index() - resultado = resultado.sort_values('score', ascending=False).reset_index().rename(columns={'INICIATIVA_ID': 'frecuencia', - 'index': 'rank'}) - resultado['rank'] = resultado.index + 1 - - # Crear columna ODS_ID de forma robusta para todos los niveles - if nivel == 'ods': - # Para ODS, el ID ya es el ODS_ID - resultado['ODS_ID'] = resultado['ODS_ID'].astype(str) - elif nivel == 'meta': - # Extraer ODS del META_ID (formato: "1.1", tomar el primer dígito) - def extraer_ods_de_meta(meta_id): - try: - if pd.isna(meta_id): - return None - ods_num = str(meta_id).split('.')[0].strip() - return ods_num if ods_num else None - except: - return None - resultado['ODS_ID'] = resultado['META_ID'].apply(extraer_ods_de_meta) - elif nivel == 'indicador': - # Extraer ODS del INDICADOR_ID (formato: "1.1.1", tomar el primer dígito) - def extraer_ods_de_indicador(ind_id): - try: - if pd.isna(ind_id): - return None - ods_num = str(ind_id).split('.')[0].strip() - return ods_num if ods_num else None - except: - return None - resultado['ODS_ID'] = resultado['INDICADOR_ID'].apply(extraer_ods_de_indicador) - - - return gr.update(value=resultado), gr.update(value=df_pareto) - - except Exception as e: - print(f"Error en analisis_global_con_pareto: {str(e)}") - import traceback - traceback.print_exc() - return gr.update(value=pd.DataFrame()), gr.update(value=pd.DataFrame()) -# ============================================================================ -# FUNCIONES PARA CADA PESTAÑA -# ============================================================================ - -def tab_inicio(df_ods, df_metas, df_indicador): -# def tab_inicio(): - """Pestaña de inicio con resumen general""" - if not DATOS_CARGADOS: - return "⚠️ Error: No se pudieron cargar los datos." - - # Estadísticas básicas - - total_ods = df_ods['ODS_ID'].nunique() - total_metas = df_metas['META_ID'].nunique() - total_indicadores = df_indicador['INDICADOR_ID'].nunique() - sim_media = df_ods['ods_similaridad_cos_normalized'].mean() - sim_max = df_ods['ods_similaridad_cos_normalized'].max() - sim_min = df_ods['ods_similaridad_cos_normalized'].min() - correlacion = df_ods['ods_rank'].corr(df_ods['ods_similaridad_cos_normalized']) - - # Top 4 ODS - top_ods = df_ods.nsmallest(4, 'ods_rank')[['ODS_ID','ods_rank','OBJETIVO','ods_similaridad_cos_normalized']] - top_ods['logo_id'] = top_ods['ODS_ID'].apply(lambda _: f"ods_{_}") - # top_ods = df_ods.groupby('ODS_ID').agg({ - # 'ods_similaridad_cos_normalized': 'mean' - # }).sort_values('ods_similaridad_cos_normalized', ascending=False).head(3)[['ods_similaridad_cos_normalized']] - - # Top ODS referencia - ods_ref = top_ods.ODS_ID - - # Top 3 METAS - - top_metas = pd.DataFrame() - for i in ods_ref: - top_metas_lcl = df_metas[df_metas.ODS_ID == i] - top_metas_lcl = top_metas_lcl.nsmallest(2, 'meta_rank')[['META_ID','meta_rank','META','meta_similaridad_cos_normalized', 'ODS_ID']] - top_metas = pd.concat([top_metas, top_metas_lcl], axis=0) - # top_metas['logo_id'] = top_metas['ODS_ID'].apply(lambda _: f"ods_{_}") - top_metas['logo_id'] = top_metas['META_ID'].apply(lambda _: f"meta_{_.upper()}") - - recomendaciones_tblinput = Path('data/raw/ODS_169_metas_recomendaciones_detalladas.xlsx') - df_recomendaciones = pd.read_excel(recomendaciones_tblinput) - top_metas = top_metas.merge(df_recomendaciones[['Meta_ODS', 'Recomendaciones_territoriales']], left_on='META_ID', right_on='Meta_ODS', how='left') - # top_metas = df_metas.groupby('META_ID').agg({ - # 'meta_similaridad_cos_normalized': 'mean' - # }).sort_values('meta_similaridad_cos_normalized', ascending=False).head(5)[['META_ID','META','meta_similaridad_cos_normalized']] - - # Top 5 indicadores - top_indicador = pd.DataFrame() - for i in ods_ref: - top_indicador_lcl = df_indicador[df_indicador.ODS_ID == i] - top_indicador_lcl = top_indicador_lcl.nsmallest(2, 'indicador_rank')[['INDICADOR_ID', 'indicador_rank', 'INDICADOR', 'indicador_similaridad_cos_normalized', 'ODS_ID']] - top_indicador = pd.concat([top_indicador, top_indicador_lcl], axis=0) - top_indicador['logo_id'] = top_indicador['ODS_ID'].apply(lambda _: f"ods_{_}") - - - html = f""" -
-

- 📊 Tu texto en clave de ODS -

-

- El análisis - identifica que tu texto se relaciona principalmente - con estos Objetivos de Desarrollo Sostenible ODS -

- - - - - -
-

🏆 Top 4 ODS Más Relevantes

-
- {''.join([f''' -
- ODS {row['ODS_ID']} - -
''' for _, row in top_ods.iterrows()])} -
-
- -
-

🎯 Metas Más Relevantes Por ODS Top

-
- {''.join([f''' -
- ODS {row['ODS_ID']} -

Meta: {row['META_ID']}

- - - -
''' for _, row in top_metas.iterrows()])} -
-
- -
-

🎯 Indicadores Más Relevantes Por ODS Top

-
- {''.join([f''' -
- ODS {row['ODS_ID']} -

Ind: {row['INDICADOR_ID']}

- - -

Rank: {row['indicador_rank']}

-
''' for _, row in top_indicador.iterrows()])} -
-
- -
-

📋 Recomendaciones Territoriales por Meta

- - - - - - - - - - {''.join([f''' - - - - - ''' for _, row in df_metas.iterrows() if not pd.isna(row.get('Recomendaciones_territoriales'))])} - -
ODSMeta IDRecomendaciones Territoriales
- ODS {row['ODS_ID']} - {row['META_ID']}{row.get('Recomendaciones_territoriales', 'N/A')}
-
- - -
- """ - return html - -def tab_inicio_mass(df_ods=None, df_metas=None, df_indicador=None): -# def tab_inicio(): - """Pestaña de inicio con resumen general""" - # Validación de entrada - if df_ods is None or df_ods.empty: - return "

⚠️ No hay datos ODS disponibles

" - if df_metas is None or df_metas.empty: - return "

⚠️ No hay datos de METAS disponibles

" - - # if not DATOS_CARGADOS: - # return "⚠️ Error: No se pudieron cargar los datos." - - # print('ODS columns:', df_ods.columns) - # print('METAS columns:', df_metas.columns) - # print('INDICADOR columns:', df_indicador.columns) - - # Estadísticas básicas - - # total_ods = df_ods['ODS_ID'].nunique() - # total_metas = df_metas['META_ID'].nunique() - # total_indicadores = df_indicador['INDICADOR_ID'].nunique() - # sim_media = df_ods['score'].mean() - # sim_max = df_ods['score'].max() - # sim_min = df_ods['score'].min() - # correlacion = df_ods['rank'].corr(df_ods['score']) - - # Top 4 ODS - top_ods = df_ods.nsmallest(4, 'rank')[['ODS_ID','rank','score']] - top_ods['logo_id'] = top_ods['ODS_ID'].apply(lambda _: f"ods_{_}") - # top_ods = df_ods.groupby('ODS_ID').agg({ - # 'score': 'mean' - # }).sort_values('score', ascending=False).head(3)[['score']] - - # Top ODS referencia - ods_ref = top_ods.ODS_ID - - # Top 3 METAS - - top_metas = pd.DataFrame() - for i in ods_ref: - top_metas_lcl = df_metas[df_metas['ODS_ID'] == i] - top_metas_lcl = top_metas_lcl.nsmallest(2, 'rank')[['META_ID','rank','score','ODS_ID']] - top_metas = pd.concat([top_metas, top_metas_lcl], axis=0) - # top_metas['logo_id'] = top_metas['ODS_ID'].apply(lambda _: f"ods_{_}") - top_metas['logo_id'] = top_metas['META_ID'].apply(lambda _: f"meta_{_.upper()}") - # top_metas = df_metas.groupby('META_ID').agg({ - # 'meta_similaridad_cos_normalized': 'mean' - # }).sort_values('meta_similaridad_cos_normalized', ascending=False).head(5)[['META_ID','META','meta_similaridad_cos_normalized']] - - recomendaciones_tblinput = Path('data/raw/ODS_169_metas_recomendaciones_detalladas.xlsx') - df_recomendaciones = pd.read_excel(recomendaciones_tblinput) - top_metas = top_metas.merge(df_recomendaciones[['Meta_ODS', 'Recomendaciones_territoriales']], left_on='META_ID', right_on='Meta_ODS', how='left') - - # Top 5 indicadores - top_indicador = pd.DataFrame() - if df_indicador is not None and not df_indicador.empty and 'ODS_ID' in df_indicador.columns: - for i in ods_ref: - top_indicador_lcl = df_indicador[df_indicador['ODS_ID'] == i] - if not top_indicador_lcl.empty: - # Verificar qué columnas existen en el DataFrame - cols_disponibles = [col for col in ['INDICADOR_ID', 'rank', 'score', 'ODS_ID'] if col in top_indicador_lcl.columns] - top_indicador_lcl = top_indicador_lcl.nsmallest(2, 'rank')[cols_disponibles] if 'rank' in cols_disponibles else top_indicador_lcl[cols_disponibles].head(2) - top_indicador = pd.concat([top_indicador, top_indicador_lcl], axis=0) - if not top_indicador.empty: - top_indicador['logo_id'] = top_indicador['ODS_ID'].apply(lambda _: f"ods_{_}") - else: - top_indicador = pd.DataFrame() - - - html = f""" -
-

- 📊 Tus textos en clave de ODS -

-

- El análisis - identifica que tus textos se relacionan principalmente - con estos Objetivos de Desarrollo Sostenible ODS -

- - - - - -
-

🏆 Top 4 ODS Más Relevantes

-
- {''.join([f''' -
- ODS {row['ODS_ID']} - -
''' for _, row in top_ods.iterrows()])} -
-
- -
-

🎯 Metas Más Relevantes Por ODS Top

-
- {''.join([f''' -
- ODS {row['ODS_ID']} -

Meta: {row['META_ID']}

- - -

Rank: {row['rank']}

-
''' for _, row in top_metas.iterrows()])} -
-
- -
-

🎯 Indicadores Más Relevantes Por ODS Top

-
- {''.join([f''' -
- ODS {row['ODS_ID']} -

Ind: {row['INDICADOR_ID']}

- - -

Rank: {row['rank']}

-
''' for _, row in top_indicador.iterrows()])} -
-
- -
-

📋 Recomendaciones Territoriales por Meta

- - - - - - - - - - {''.join([f''' - - - - - ''' for _, row in df_metas.iterrows() if not pd.isna(row.get('Recomendaciones_territoriales'))])} - -
ODSMeta IDRecomendaciones Territoriales
- ODS {row['ODS_ID']} - {row['META_ID']}{row.get('Recomendaciones_territoriales', 'N/A')}
-
- - -
- """ - return html - -def tab_viz1(df_ods, df_metas, df_indicador): -# def tab_viz1(): - """Visualización 1: Box Plot por ODS""" - if not DATOS_CARGADOS: - return None, "⚠️ Error: No se pudieron cargar los datos." - - fig1 = viz_1_distribucion_por_ods(df_ods, 'ODS_ID', 'ods_similaridad_cos_normalized', 'ODS') - fig2 = viz_1_distribucion_por_ods(df_metas, 'META_ID', 'meta_similaridad_cos_normalized', 'META') - fig3 = viz_1_distribucion_por_ods(df_indicador, 'INDICADOR_ID', 'indicador_similaridad_cos_normalized', 'INDICADOR') - - explicacion = """ - ## 📦 Diagrama de Caja por ODS - - ### ¿Qué muestra? - Esta visualización muestra cómo se distribuyen los valores de similaridad para cada uno de los 17 ODS. - - ### ¿Cómo leerlo? - - **Línea central**: Mediana (valor del medio) - - **Caja**: Rango intercuartílico (Q1 a Q3) - - **Líneas extendidas**: Valores mínimos y máximos normales - - **Puntos fuera**: Valores atípicos (outliers) - - ### Interpretación: - - ✅ **Cajas altas**: Mucha variación entre indicadores del ODS - - ✅ **Cajas pequeñas**: Indicadores consistentes - - ✅ **Mediana alta**: ODS muy relacionado con la iniciativa - - ✅ **Puntos aislados**: Indicadores especialmente relevantes - - ### 💡 Consejo: - Busca ODS con medianas altas y cajas pequeñas para identificar objetivos con indicadores consistentemente relevantes. - """ - - return fig1, fig2, fig3, explicacion - -def tab_viz2(df_global): -# def tab_viz2(): - """Visualización 2: Heatmap ODS × Ranking""" - if not DATOS_CARGADOS: - return None, "⚠️ Error: No se pudieron cargar los datos." - - fig = viz_2_heatmap_ods_ranking(df_global) - filepath = matplotlib_to_file(fig, 'viz2_heatmap.png') - - explicacion = """ - ## 🔥 Mapa de Calor: ODS × Ranking - - ### ¿Qué muestra? - Matriz bidimensional que cruza los 17 ODS (filas) con deciles de ranking (columnas), - mostrando la similaridad promedio en cada celda. - - ### ¿Cómo leerlo? - - 🔴 **Colores cálidos** (rojo/naranja): Alta similaridad - - 🔵 **Colores fríos** (verde/azul): Baja similaridad - - **D1 a D10**: Desde los más relevantes (D1) hasta los menos (D10) - - ### Interpretación: - - ✅ **Fila roja completa**: ODS relevante en todos los rangos - - ✅ **Columna roja**: Varios ODS relevantes en esa posición - - ✅ **Diagonal descendente**: Patrón esperado (a mayor rank, menor similaridad) - - ✅ **Rojo en D1-D2**: Los ODS más críticos - - ### 💡 Consejo: - Identifica rápidamente qué ODS dominan en las posiciones altas del ranking. - """ - - return filepath, explicacion - -def tab_viz3(df_global): -# def tab_viz3(): - """Visualización 3: Scatter 3D Interactivo""" - if not DATOS_CARGADOS: - return None, "⚠️ Error: No se pudieron cargar los datos." - - fig = viz_3_scatter_3d_interactivo(df_global) - - explicacion = """ - ## 🌐 Gráfico 3D Interactivo - - ### ¿Qué muestra? - Visualización tridimensional donde cada punto representa un indicador. - - ### Las tres dimensiones: - - **Eje X**: ODS ID (1-17) - - **Eje Y**: Número de sub-indicador - - **Eje Z**: Similaridad (altura del punto) - - **Tamaño**: Los más grandes = más relevantes - - **Color**: Cada ODS tiene su color - - ### Interactividad: - - 🔄 **Rotar**: Arrastra con el mouse - - 🔍 **Zoom**: Scroll o pinch - - 👆 **Hover**: Pasa el mouse sobre puntos - - ### Interpretación: - - ✅ **Puntos altos**: Alta similaridad - - ✅ **Clusters de color**: Grupo de indicadores relacionados - - ✅ **Puntos grandes y altos**: Los más importantes - - ### 💡 Consejo: - Rota el gráfico para descubrir patrones ocultos y agrupaciones de indicadores. - """ - - return fig, explicacion - -def tab_viz4(df, id_lvl, score, rank, titulo): -# def tab_viz4(): - """Visualización 4: Radar Chart""" - if not DATOS_CARGADOS: - return None, "⚠️ Error: No se pudieron cargar los datos." - - fig = viz_4_radar_chart_ods(df, id_lvl, score, rank, titulo) - - explicacion = """ - ## 🕸️ Gráfico de Radar (Perfil ODS) - - ### ¿Qué muestra? - Gráfico circular que muestra el 'perfil ODS' de tu iniciativa con dos métricas. - - ### Cómo leerlo: - - 🔵 **Polígono azul**: Similaridad promedio por ODS - - 🔴 **Polígono rojo**: Similaridad máxima (mejor indicador) - - **Distancia del centro**: Mayor distancia = mayor similaridad - - ### Interpretación: - - ✅ **Picos hacia afuera**: ODS muy relevantes - - ✅ **Valles hacia dentro**: ODS menos relacionados - - ✅ **Forma circular**: Iniciativa equilibrada - - ✅ **Forma irregular**: Especialización en ODS específicos - - ✅ **Gap azul-rojo grande**: Indicador estrella en ese ODS - - ### 💡 Consejo: - Ideal para presentaciones ejecutivas. Muestra de un vistazo el perfil completo de alineación ODS. - """ - - return fig, explicacion - -def tab_viz5(df, id_lvl, score, rank, titulo): -# def tab_viz5(): - """Visualización 5: Sunburst""" - if not DATOS_CARGADOS: - return None, "⚠️ Error: No se pudieron cargar los datos." - - fig = viz_5_sunburst_jerarquia(df, id_lvl, score, rank, titulo) - - explicacion = """ - ## ☀️ Diagrama de Sol (Sunburst) - - ### ¿Qué muestra? - Diagrama circular jerárquico mostrando ODS (centro) → Indicadores (anillo exterior). - - ### Cómo leerlo: - - **Tamaño del segmento**: Proporcional a la similaridad - - **Color**: Gradiente (más oscuro = mayor similaridad) - - **Nivel 1 (centro)**: Los 17 ODS - - **Nivel 2 (exterior)**: Indicadores individuales - - ### Interactividad: - - 👆 **Click**: Zoom en un ODS específico - - 🔍 **Hover**: Ver código y valor del indicador - - ### Interpretación: - - ✅ **Segmentos grandes**: Indicadores muy relevantes - - ✅ **ODS ocupa mucho espacio**: Muchos indicadores relevantes - - ✅ **Colores oscuros**: Alta similaridad - - ### 💡 Consejo: - Excelente para visualizar la contribución relativa de cada indicador al total. - """ - - return fig, explicacion - -def tab_viz6(df, id_lvl, score, rank, titulo, top_n=3): -# def tab_viz6(): - """Visualización 6: Top Indicadores por ODS""" - if not DATOS_CARGADOS: - return None, "⚠️ Error: No se pudieron cargar los datos." - - fig = viz_6_top_indicadores_por_ods(df, id_lvl, score, rank, titulo, top_n=3) - - explicacion = """ - ## 🏆 Top 5 Indicadores por ODS - - ### ¿Qué muestra? - Barras horizontales con los 5 indicadores más relevantes de cada ODS. - - ### Cómo leerlo: - - **Longitud de barra**: Valor de similaridad - - **Primera barra**: El indicador más relevante - - **Color**: Gradiente por similaridad - - **Cada panel**: Un ODS diferente - - ### Interpretación: - - ✅ **Barra mucho más larga**: Indicador campeón - - ✅ **Barras parejas**: Varios indicadores igualmente relevantes - - ✅ **Comparación entre ODS**: Qué objetivo tiene mejores indicadores - - ### 💡 Consejo: - Perfecta para planificación estratégica. Te dice exactamente en qué indicadores enfocarte por cada ODS. - """ - - return fig, explicacion - -def tab_viz7(df_global): -# def tab_viz7(): - """Visualización 7: Stream Graph""" - if not DATOS_CARGADOS: - return None, "⚠️ Error: No se pudieron cargar los datos." - - fig = viz_7_streamgraph_similaridad(df_global) - - explicacion = """ - ## 🌊 Gráfico de Flujo (Stream Graph) - - ### ¿Qué muestra? - Áreas apiladas que muestran cómo cambia la contribución porcentual de cada ODS - a lo largo del ranking. - - ### Cómo leerlo: - - **Eje horizontal**: Ranking agrupado (izq. = más relevante) - - **Eje vertical**: Porcentaje de contribución (suma 100%) - - **Ancho del color**: Porcentaje del ODS en ese rango - - ### Interpretación: - - ✅ **Color dominante izquierda**: ODS líder en indicadores relevantes - - ✅ **Cambio de color**: Transición de relevancia - - ✅ **Área ancha constante**: ODS presente en todo el ranking - - ✅ **Área que crece/decrece**: ODS relevante en ciertos rangos - - ### 💡 Consejo: - Si un ODS ocupa mucho espacio a la izquierda, domina entre los indicadores más relevantes. - """ - - return fig, explicacion - -def tab_viz8(df_global): -# def tab_viz8(): - """Visualización 8: Violin Plot""" - if not DATOS_CARGADOS: - return None, "⚠️ Error: No se pudieron cargar los datos." - - fig = viz_8_violin_plot_ods(df_global) - - explicacion = """ - ## 🎻 Gráfico de Violín - - ### ¿Qué muestra? - Similar al diagrama de caja pero con más detalle. Muestra la 'forma' completa - de la distribución de similaridad por ODS. - - ### Cómo leerlo: - - **Ancho del violín**: Concentración de valores - - **Caja interior**: Mediana y cuartiles - - **Línea horizontal**: Media (promedio) - - ### Concepto clave: - El ancho representa la **densidad de probabilidad**: donde el violín es más ancho, - es más probable encontrar indicadores con esos valores. - - ### Interpretación: - - ✅ **Violín ancho en un punto**: Muchos indicadores similares - - ✅ **Dos ensanchamientos**: Dos grupos distintos - - ✅ **Violín delgado**: Pocos indicadores en ese rango - - ✅ **Forma simétrica**: Distribución equilibrada - - ### 💡 Consejo: - Detecta distribuciones complejas que el diagrama de caja no puede mostrar. - """ - - return fig, explicacion - -def tab_viz9(df_global): -# def tab_viz9(): - """Visualización 9: Dashboard Integrado""" - if not DATOS_CARGADOS: - return None, "⚠️ Error: No se pudieron cargar los datos." - - fig = viz_9_dashboard_metricas(df_global) - - explicacion = """ - ## 📊 Dashboard Integrado (4 Paneles) - - ### Panel 1 (Superior Izquierdo): Top 10 Indicadores - Barras con los 10 indicadores más relevantes del análisis completo. - - ### Panel 2 (Superior Derecho): Estadísticas por ODS - Tabla con media, desviación estándar, mínimo, máximo y cantidad por ODS. - - ### Panel 3 (Inferior Izquierdo): Histograma Global - Distribución de frecuencias de todos los valores de similaridad. - - ### Panel 4 (Inferior Derecho): Correlación Rank-Similaridad - Scatter plot con línea de tendencia. **CRÍTICO para validación del sistema**. - - ### Validación: - - ✅ **Línea descendente**: Sistema funcionando correctamente - - ✅ **Correlación < -0.7**: Excelente - - ⚠️ **Correlación > -0.4**: Revisar sistema - - ### 💡 Consejo: - Este debe ser tu punto de partida. Vista 360° del análisis completo. - """ - - return fig, explicacion - -def tab_viz10(df_global): -# def tab_viz10(): - """Visualización 10: Matriz de Transición""" - if not DATOS_CARGADOS: - return None, "⚠️ Error: No se pudieron cargar los datos." - - fig = viz_10_matriz_transicion(df_global) - filepath = matplotlib_to_file(fig, 'viz10_matriz_transicion.png') - - explicacion = """ - ## 🔀 Matriz de Transición por Cuartiles - - ### ¿Qué muestra? - Mapa de calor que muestra el porcentaje de cada ODS presente en los 4 cuartiles del ranking. - - ### Cómo leerlo: - - **Filas**: Los 17 ODS - - **Columnas**: Q1 (Top 25%), Q2, Q3, Q4 (Bottom 25%) - - **Valores**: Porcentaje de presencia del ODS - - **Colores**: Naranja/rojo = alta presencia - - ### Interpretación: - - ✅ **Rojo intenso en Q1**: ODS crítico (domina rankings altos) - - ✅ **Colores uniformes**: ODS consistente en todo el ranking - - ✅ **Concentración en un cuartil**: ODS especializado - - ✅ **Claro en Q1, oscuro en Q4**: Más relevante en posiciones bajas - - ### 💡 Consejo: - Analiza la consistencia de relevancia por ODS. Alta presencia en Q1 = crítico para la iniciativa. - """ - - return filepath, explicacion - -def tab_viz18(df, lvl, top): - """Visualización 18: Pivot Table Interactivo""" - - # if not DATOS_RELACIONES_CARGADOS: - # return None, "⚠️ Error: No se pudieron cargar los datos de relaciones." - - # Leer el HTML pre-generado - # html_path = '/mnt/user-data/outputs/pivot_table_ods_interactivo.html' - - # if os.path.exists(html_path): - # with open(html_path, 'r', encoding='utf-8') as f: - # html_content = f.read() - # else: - # Si no existe, generarlo - import tempfile - from pivottablejs import pivot_ui - # from viz_pivot_table import crear_pivot_table_html - import uuid - - # Crear nombre único para evitar conflictos - unique_id = str(uuid.uuid4())[:8] - - # Ruta temporal compatible con HF Spaces y local - if os.path.exists('/tmp'): # Linux/HF Spaces - temp_path = f'/tmp/pivot_table_{lvl}_{str(top).replace(".","")}.html' - else: # Windows local - temp_path = os.path.join(tempfile.gettempdir(), f'pivot_table_{lvl}_{str(top).replace(".","")}.html') - - pivot_ui( - df, - outfile_path=temp_path - ) - - print(f"✓ Pivot Table generado: {temp_path}") - - - with open(temp_path, 'r', encoding='utf-8') as f: - html_content = f.read() - - explicacion = """ - ## 🔄 Tabla Dinámica Interactiva - - Arrastra campos, cambia agregaciones y visualizaciones en tiempo real. - """ - - return (gr.update(value=html_content), gr.update(value=explicacion), gr.update(value=temp_path)) - -def tab_viz19(df_ods_rel, df_metas_rel, df_indicadores_rel): - """Visualización 19: Resumen en Tags""" - # if not DATOS_RELACIONES_CARGADOS: - # return "⚠️ Error: No se pudieron cargar los datos de relaciones.", "" - - html1, html2, html3 = viz_19_resumen_tags(df_ods_rel, df_metas_rel, df_indicadores_rel) - - explicacion = """ - ## 📊 Resumen de Análisis en Tags - - ### ¿Qué muestra? - Métricas clave del análisis presentadas en formato visual de tags/badges. - - ### Métricas incluidas: - - **Iniciativas**: Total de iniciativas analizadas - - **Promedios**: ODS, Metas e Indicadores promedio por iniciativa - - **Más frecuentes**: Elementos que aparecen más veces en el análisis - - ### 💡 Interpretación: - - **Promedios altos**: Iniciativas con enfoque amplio en múltiples ODS - - **Elementos frecuentes**: ODS/Metas/Indicadores prioritarios en el conjunto - """ - - return html1, html2, html3, explicacion - -def tab_viz20(df, nivel, nivel_pareto, mass = False ,iniciativa_id=None): - """Visualización 20: Diagrama de Pareto""" - # if not DATOS_RELACIONES_CARGADOS: - # return None, "" - - # Seleccionar primera iniciativa - # iniciativa_id = df['INICIATIVA_ID'].iloc[0] - - fig, corte_80 = viz_20_pareto_ods(df, nivel, nivel_pareto, mass, iniciativa_id) - - explicacion = f""" - ## 📊 Diagrama de Pareto {nivel.upper()} Global - - ### ¿Qué muestra? - Identifica los **{corte_80} {nivel.upper()} más críticos** que representan el **{nivel_pareto * 100}%** - del valor total de aproximación semántica. - - ### Interpretación: - - **Barras azules**: Aproximación semántica individual de cada {nivel.upper()} - - **Línea roja**: Porcentaje acumulado - - **Zona verde**: Top {corte_80} {nivel.upper()} críticos (regla de Pareto) - - **Línea punteada**: Umbral {nivel_pareto * 100}% - - ### 💡 Uso: - Estos **{corte_80} {nivel.upper()}** deberían ser tu enfoque prioritario, - ya que concentran la mayor parte del valor. - - ### Principio de Pareto: - El 80% de los resultados proviene del 20% de las causas. - En este caso aplicamos un criterio de Pareto ADAPTATIVO: {corte_80} {nivel.upper()} generan el {nivel_pareto * 100}% de la aproximación semántica total. - """ - - return fig, explicacion - -def tab_viz21(df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop=0.75, rank_prop=0.20): - """Visualización 21: Ranking ODS Masivo""" - # Validar entrada - if df is None or df.empty: - return None, "No hay datos para visualizar" - - try: - resultado = df.copy() - - fig = viz_21_ranking_mixto_masivo(resultado, nivel) - - explicacion = f"""## 🏆 Ranking Global de ODS - Análisis Masivo - -### ¿Qué muestra? -Resultado de aplicar **estrategia mixta ponderada** a iniciativas prioritarias. - -### Componentes del Score: -- **50%** Similaridad promedio -- **30%** Ranking promedio (posición) -- **20%** Frecuencia de aparición - -### Interpretación: -- **Barras largas**: ODS con mayor score global -- **Color verde**: Scores más altos -- **Color azul**: Scores intermedios -- **Círculos grandes**: ODS que aparecen en m��s iniciativas - -### Top 3 ODS: -{chr(10).join([f'- **ODS {int(row["ODS_ID"])}**: Score {row["score"]:.4f} ({int(row["frecuencia"])} iniciativas)' - for _, row in resultado.head(3).iterrows()] if len(resultado) > 0 else ['Sin datos'])} - -### 💡 Uso: -Estos ODS son los **prioritarios a nivel agregado** considerando -relevancia, posición y prevalencia en el lote analizado. -""" - - return fig, explicacion - - except Exception as e: - print(f"Error en tab_viz21: {str(e)}") - import traceback - traceback.print_exc() - return None, f"Error en visualización: {str(e)}" - -def tab_viz22(w_sim, w_rank): - """Visualización 22: Composición Score Mixto""" - - # Pesos de la estrategia mixta - # w_sim = 0.5 - # w_rank = 0.3 - # w_freq = 0.2 - - fig = viz_22_composicion_score_mixto(w_sim, w_rank, 1 - (w_sim + w_rank)) - - explicacion = f""" - ## ⚖️ Composición del Score Mixto - - ### ¿Qué muestra? - La distribución porcentual de los 3 componentes que conforman el score. - - ### Componentes: - - **1. Similaridad ({w_sim*100:.0f}%)** - - Qué mide: Relevancia semántica promedio - - Cálculo: Promedio de similaridad coseno normalizada - - Peso: Mayor componente (calidad del match) - - **2. Ranking ({w_rank*100:.0f}%)** - - Qué mide: Posición promedio en las iniciativas - - Cálculo: Promedio de rankings invertido y normalizado - - Peso: Segundo componente (importancia relativa) - - **3. Frecuencia ({(1 - (w_sim + w_rank))*100:.0f}%)** - - Qué mide: Prevalencia en el lote - - Cálculo: Cantidad de iniciativas donde aparece - - Peso: Menor componente (consistencia) - - ### 📐 Fórmula: - - Score = {w_sim} × Similaridad + {w_rank} × Ranking + {1 - (w_sim + w_rank)} × Frecuencia - - - ### 💡 Justificación de Pesos: - - **Similaridad dominante**: La calidad del match es lo más importante - - **Ranking secundario**: La posición importa, pero menos que la similaridad - - **Frecuencia terciaria**: Aparecer en muchas iniciativas suma, pero no define - - ### 🔧 Personalización: - Estos pesos pueden ajustarse según prioridades: - - ¿Más importancia a frecuencia? → Aumentar w_freq a 0.3-0.4 - - ¿Solo calidad del match? → Aumentar w_sim a 0.6-0.7 - """ - - return fig, explicacion - -def tab_viz24(): - """Visualización 24: Header ConectaODS""" - - # Buscar logo en diferentes ubicaciones posibles - posibles_logos = [ - '/mnt/user-data/uploads/logo_conecta_ods.png', - 'logos/conecta_ods.png', - 'assets/logo.png', - None # Fallback a placeholder - ] - - logo_path = None - for ruta in posibles_logos: - if ruta and os.path.exists(ruta): - logo_path = ruta - break - - html = viz_24_header_conecta_ods(logo_path) - - explicacion = """ - ## 🎨 Header ConectaODS - - ### Componente de Presentación - - Este es el header principal de la aplicación que incluye: - - ✅ **Logo** de ConectaODS (o placeholder si no se carga) - ✅ **Título** con versión - ✅ **Eslogan** distintivo - ✅ **Descripción** de la herramienta - ✅ **Badges** informativos (17 ODS, 169 Metas, 244+ Indicadores) - ✅ **Créditos** institucionales - - ### 💡 Personalización: - - Para usar tu propio logo: - 1. Sube la imagen a `/mnt/user-data/uploads/logo_conecta_ods.png` - 2. O colócala en `logos/conecta_ods.png` - 3. Regenera la visualización - - El componente se adapta automáticamente con o sin logo. - """ - - return html #, explicacion - -def tab_estadisticas(df_global): -# def tab_estadisticas(): - """Pestaña con análisis estadístico detallado""" - if not DATOS_CARGADOS: - return "⚠️ Error: No se pudieron cargar los datos." - - # Estadísticas globales - stats = df_global['similaridad_cos'].describe() - correlacion = df_global['rank'].corr(df_global['similaridad_cos']) - - # Por ODS - stats_ods = df_global.groupby('ods_id')['similaridad_cos'].agg([ - ('count', 'count'), - ('mean', 'mean'), - ('std', 'std'), - ('min', 'min'), - ('max', 'max') - ]).round(4) - - # Top 50 - top_50_ods = df_global.nsmallest(50, 'rank')['ods_id'].value_counts() - - html = f""" -
-

📈 Análisis Estadístico Detallado

- -
-

1. Estadísticas Globales

- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
Cantidad de datos:{stats['count']:.0f}
Media:{stats['mean']:.4f}
Desviación Estándar:{stats['std']:.4f}
Mínimo:{stats['min']:.4f}
Q1 (Percentil 25):{stats['25%']:.4f}
Mediana (Q2):{stats['50%']:.4f}
Q3 (Percentil 75):{stats['75%']:.4f}
Máximo:{stats['max']:.4f}
-
- -
-

2. Validación del Sistema

-

Correlación Rank vs Similaridad: {correlacion:.4f}

-

Interpretación: - { - "✅ Excelente - Sistema de ranking muy confiable" if correlacion < -0.9 else - "✅ Muy bueno - Sistema de ranking confiable" if correlacion < -0.7 else - "⚠️ Aceptable - Sistema funciona pero puede mejorarse" if correlacion < -0.4 else - "❌ Problema - Revisar cálculo de similaridad o ranking" - } -

-

Una correlación negativa fuerte indica que a mayor ranking (menos relevante), menor es la similaridad, lo cual es el comportamiento esperado.

-
- -
-

3. Estadísticas por ODS

- - - - - - - - - - - - - {''.join([f''' - - - - - - - ''' for idx, row in stats_ods.iterrows()])} - -
ODSCountMediaStdMinMax
ODS {idx}{int(row['count'])}{row['mean']:.4f}{row['std']:.4f}{row['min']:.4f}{row['max']:.4f}
-
- -
-

4. ODS Más Representados en Top 50

- - - - - - - - - - {''.join([f''' - - - - ''' for idx, count in top_50_ods.head(10).items()])} - -
ODSCantidadPorcentaje
ODS {idx}{count}{count/50*100:.1f}%
-
-
- """ - - return html - -# ============================================================================ -# CONSTRUCCIÓN DE LA APLICACIÓN GRADIO -# ============================================================================ - -def crear_app(): - """Crea y configura la aplicación Gradio completa""" - - with gr.Blocks( - title="Sistema de Visualización ODS", - # theme=gr.themes.Soft( - # primary_hue="indigo", - # secondary_hue="orange", - # neutral_hue="slate" - # ), - theme=gr.themes.Default(), - css=CUSTOM_CSS - ) as app: - - gr.HTML(f""" -
-
- Gobierno de Colombia -
-
-

-

-
-
- Fondo Multidonante de las Naciones Unidas -
-
- """) - - # Encabezado principal - - - with gr.Row(): - with gr.Column(scale=1): - gr.HTML(f""" -
- Logo conectaODS - -
- """) - with gr.Column(scale=3): - gr.Markdown(f""" - - # ConectaODS: Tu voz en clave de desarrollo sostenible - ### Explorador Interactivo - - *ConectaODS es una herramienta que convierte - relatos, ideas o iniciativas del territorio en - conexiones claras con los Objetivos de Desarrollo - Sostenible (ODS)* - """) - - - - # Pestañas principales - with gr.Tabs(): - - - def ver_radio(df): - return True - - # PESTAÑA: CONSULTA - with gr.Tab("CONSULTA INDIVIDUAL"): - - txt_ods = gr.Textbox(value="ods", visible=False) - txt_meta = gr.Textbox(value="meta", visible=False) - txt_indicador = gr.Textbox(value="indicador", visible=False) - - with gr.Column(): - gr.Markdown(f""" - - ### Cuéntanos una iniciativa, problema o propuesta de tu territorio. - - *La herramienta analizará el texto y mostrará con qué - Objetivos de Desarrollo Sostenible (ODS) se - relaciona.* - """) - query_in = gr.Textbox(lines=5, placeholder="Escribe aquí tu consulta...", label="Iniciativa a analizar") - query_out = gr.Textbox(lines=5, label="Texto ajustado para lenguaje natural", visible=False) - - btn = gr.Button(value="Analizar mi iniciativa") - msj_procesamiento = gr.Textbox(value="... preparando análisis de iniciativa.", visible=True) - - with gr.Tab("🔍 Resultado"): - - - with gr.Row(): - ods = gr.Dataframe( label="ODS", visible=False)#, buttons=["fullscreen"]) - meta = gr.Dataframe( label="METAS", visible=False)#, buttons=["fullscreen"]) - indicador = gr.Dataframe( label="INDICADORES", visible=False)#, buttons=["fullscreen"]) - - - html_inicio_ods = gr.HTML() #tab_inicio(ods.value) - - # with gr.Row(): - # html_pareto_ods = gr.HTML() #tab_viz20(df_global) - # html_pareto_meta = gr.HTML() - # html_pareto_indicador = gr.HTML() - # PESTAÑA 20: DIAGRAMA DE PARETO - with gr.Tab("📊 Priorización con Pareto"): - gr.HTML(""" -
-

📊 Diagrama de Pareto (Regla 80/20)

-
    -
  1. CRITERIO: Aplicamos un criterio de Pareto ADAPTATIVO calibrado para contextos de clasificación multinivel, - donde el umbral se ajusta según la distribución empírica de similaridad y la granularidad requerida para toma de decisiones.
  2. -
  3. Marcos globales: (UNDP, VNR, EU SDG Strategy) recomiendan 3-5 ODS - prioritarios por ciclo de implementación.
  4. -
  5. Interactúa: Las visualizaciones son dinámicas y permiten exploración
  6. -
-
- """) - - with gr.Row(): - with gr.Column(scale=2): - plotOds = gr.Plot(label="Diagrama de Pareto ODS") - with gr.Column(scale=1): - slider_ods_pareto = gr.Slider(0.1, 1.0, step=0.1, value=0.7,interactive=True, label="Top ODS") - expOds = gr.Markdown() - with gr.Row(): - with gr.Column(scale=2): - plotMeta = gr.Plot(label="Diagrama de Pareto Metas") - with gr.Column(scale=1): - slider_meta_pareto = gr.Slider(0.1, 1.0, step=0.1, value=0.7, interactive=True, label="Top Metas") - expMeta = gr.Markdown() - - with gr.Row(): - with gr.Column(scale=2): - plotIndicador = gr.Plot(label="Diagrama de Pareto Indicadores") - with gr.Column(scale=1): - slider_indicador_pareto = gr.Slider(0.1, 1.0, step=0.1, value=0.7, interactive=True, label="Top Indicadores") - expIndicador = gr.Markdown() - - - - # btn0_i = gr.Button("🔄 Traducción a ODS", variant="primary") - ### Actualizando resultados iniciales - ods.change( - fn=tab_inicio, - inputs=[ods,meta,indicador], - outputs=[html_inicio_ods] - ) - meta.change( - fn=tab_inicio, - inputs=[ods,meta,indicador], - outputs=[html_inicio_ods] - ) - indicador.change( - fn=tab_inicio, - inputs=[ods,meta,indicador], - outputs=[html_inicio_ods] - ) - - ### Actualizando análisis de Pareto - ods.change( - fn=tab_viz20, - inputs=[ods, gr.State('ods'), gr.State(0.7), gr.State(False), gr.State('individual')], - outputs=[plotOds, expOds] - ) - meta.change( - fn=tab_viz20, - inputs=[meta, gr.State('meta'), gr.State(0.7), gr.State(False), gr.State('individual')], - outputs=[plotMeta, expMeta] - ) - indicador.change( - fn=tab_viz20, - inputs=[indicador, gr.State('indicador'), gr.State(0.7), gr.State(False), gr.State('individual')], - outputs=[plotIndicador, expIndicador] - ) - ### Actualizando nivel de pareto - - slider_ods_pareto.change( - fn=tab_viz20, - inputs=[ods, gr.State('ods'), slider_ods_pareto, gr.State(False), gr.State('individual')], - outputs=[plotOds, expOds] - ) - slider_meta_pareto.change( - fn=tab_viz20, - inputs=[meta, gr.State('meta'), slider_meta_pareto, gr.State(False), gr.State('individual')], - outputs=[plotMeta, expMeta] - ) - slider_indicador_pareto.change( - fn=tab_viz20, - inputs=[indicador, gr.State('indicador'), slider_indicador_pareto, gr.State(False), gr.State('individual')], - outputs=[plotIndicador, expIndicador] - ) - - with gr.Tab("📋 Tablas Detalladas"): - es_visible = gr.State(value=False) - - with gr.Row(visible=es_visible): - slider_ods = gr.Slider(1, 5, step=1, value=3,interactive=True, label="Top ODS") - slider_meta = gr.Slider(1, 5, step=1, value=3, interactive=True, label="Top Metas") - slider_indicador = gr.Slider(1, 5, step=1, value=3, interactive=True, label="Top Indicadores") - - def len_dfs(df): - return gr.update(maximum=len(df)) - - - - ods.change(fn=len_dfs, - inputs=[ods], - outputs=[slider_ods], api_name="ods") - - meta.change(fn=len_dfs, - inputs=[meta], - outputs=[slider_meta], api_name="ods") - - indicador.change(fn=len_dfs, - inputs=[indicador], - outputs=[slider_indicador], api_name="ods") - - - - with gr.Row(): - df_ods_filtrado = gr.Dataframe(label="ODS Filtrados") - df_meta_filtrado = gr.Dataframe(label="METAS Filtradas") - df_indicador_filtrado = gr.Dataframe(label="INDICADORES Filtrados") - - with gr.Row(): - - - - slider_ods.release(fn=lambda df, n: df[df[f'ods_rank'] <= n][['ods_rank','ODS_ID','OBJETIVO']] if df is not None else None, - inputs=[ods, slider_ods], - outputs=[df_ods_filtrado], api_name="ods") - - slider_meta.release(fn=lambda df, n: df[df[f'meta_rank'] <= n][['meta_rank','META_ID','META']] if df is not None else None, - inputs=[meta, slider_meta], - outputs=[df_meta_filtrado], api_name="ods") - - slider_indicador.release(fn=lambda df, n: df[df[f'indicador_rank'] <= n][['indicador_rank','INDICADOR_ID','INDICADOR']] if df is not None else None, - inputs=[indicador, slider_indicador], - outputs=[df_indicador_filtrado], api_name="ods") - - - - # with gr.Row(): - # genero = gr.Dataframe( label="Enfoque de genero") - # poblacional = gr.Dataframe( label="Enfoque poblacional") - # etnico = gr.Dataframe( label="Enfoque étnico") - - # with gr.Row(): - # pilar = gr.Dataframe( label="Pilares") - # estrategia = gr.Dataframe( label="Estrategias") - # categoria = gr.Dataframe( label="Categorias") - - with gr.Row(visible=False): - bdl_ods = gr.Dataframe(value=pd.DataFrame(), label="BDL_ODS") - - # with gr.Row(): - - # ods_plot = gr.ScatterPlot( - # value=pd.DataFrame(), - # x="ODS_ID", - # y="ods_rank", - # sort="-y", - # label="Gráfico de Barras ODS", - # # color="cuisine", - # # x_bin=1, - # color_map={ - # "1": "#E5243B", # Red - ODS 1: Fin de la Pobreza - # "2": "#DDA63A", # Mustard - ODS 2: Hambre Cero - # "3": "#4C9F38", # Kelly Green - ODS 3: Salud y Bienestar - # "4": "#C5192D", # Dark Red - ODS 4: Educación de Calidad - # "5": "#FF3A21", # Red Orange - ODS 5: Igualdad de Género - # "6": "#26BDE2", # Bright Blue - ODS 6: Agua Limpia y Saneamiento - # "7": "#FCC30B", # Yellow - ODS 7: Energía Asequible y No Contaminante - # "8": "#A21942", # Burgundy Red - ODS 8: Trabajo Decente y Crecimiento Económico - # "9": "#FD6925", # Orange - ODS 9: Industria, Innovación e Infraestructura - # "10": "#DD1367", # Magenta - ODS 10: Reducción de las Desigualdades - # "11": "#FD9D24", # Golden Yellow - ODS 11: Ciudades y Comunidades Sostenibles - # "12": "#BF8B2E", # Dark Mustard - ODS 12: Producción y Consumo Responsables - # "13": "#3F7E44", # Dark Green - ODS 13: Acción por el Clima - # "14": "#0A97D9", # Blue - ODS 14: Vida Submarina - # "15": "#56C02B", # Lime Green - ODS 15: Vida de Ecosistemas Terrestres - # "16": "#00689D", # Royal Blue - ODS 16: Paz, Justicia e Instituciones Sólidas - # "17": "#19486A", # Navy Blue - ODS 17: Alianzas para Lograr los Objetivos - # }, - # ) - - # def actualizar_ods_plot(df, n): - # if df is not None: - # df_filtrado = df[df['ods_rank'] <= n] - # df_filtrado['ODS_ID'] = df_filtrado['ODS_ID'].astype(str) - # # df_filtrado['ods_rank'] = df_filtrado['ods_rank'].astype(int) - # return df_filtrado - # else: - # return pd.DataFrame() - - # slider_ods.release(fn=lambda df, n: (df[df[f'ods_rank'] <= n], actualizar_ods_plot(df, n)) if df is not None else None, - # inputs=[ods, slider_ods], - # outputs=[df_ods_filtrado, ods_plot], api_name="ods") - - # slider_ods.release(fn=lambda df, n: df[df[f'ods_rank'] <= n]['ODS_ID'].astype(str) if df is not None else None, - # inputs=[ods, slider_ods], - # outputs=[df_ods_filtrado, ods_plot], api_name="ods") - - - - # with gr.Tab("🔄 Pivot Table Iniciativa Individual"): - # with gr.Row(): - # with gr.Column(scale=3): - # html18_ods = gr.HTML(label="Pivot Table Interactivo ODS", elem_id="pivot-container") - # html18_ods_f = gr.File(label="📥 Descargar Pivot Table (HTML)") #gr.HTML(label="Pivot Table Interactivo ODS", elem_id="pivot-container") - # html18_meta = gr.HTML(label="Pivot Table Interactivo METAS", elem_id="pivot-container") - # html18_meta_f = gr.File(label="📥 Descargar Pivot Table (HTML)") #gr.HTML(label="Pivot Table Interactivo METAS", elem_id="pivot-container") - # html18_indicador = gr.HTML(label="Pivot Table Interactivo INDICADORES", elem_id="pivot-container") - # html18_indicador_f = gr.File(label="📥 Descargar Pivot Table (HTML)") #gr.HTML(label="Pivot Table Interactivo INDICADORES", elem_id="pivot-container") - # with gr.Column(scale=1): - # exp18 = gr.Markdown() - - # # def actualizar_pivots(df_ods, df_meta, df_indicador, slider_ods, slider_meta, slider_indicador): - # def actualizar_pivots(df_ods, slider_ods): - # # Actualizar ODS - # if df_ods is not None and len(df_ods) > 0: - # html_ods, exp_ods, file_ods = tab_viz18(df_ods, "ods", slider_ods) - # else: - # html_ods = gr.update(value="

No hay datos ODS para mostrar

") - # exp_ods = gr.update(value="## Sin datos ODS") - # file_ods = gr.update(value=None) - - # # # Actualizar METAS - # # if df_meta is not None and len(df_meta) > 0: - # # html_meta, exp_meta, file_meta = tab_viz18(df_meta, "meta", slider_meta) - # # else: - # # html_meta = gr.update(value="

No hay datos METAS para mostrar

") - # # exp_meta = gr.update(value="## Sin datos METAS") - # # file_meta = gr.update(value=None) - - # # # Actualizar INDICADORES - # # if df_indicador is not None and len(df_indicador) > 0: - # # html_indicador, exp_indicador, file_indicador = tab_viz18(df_indicador, "indicador", slider_indicador) - # # else: - # # html_indicador = gr.update(value="

No hay datos INDICADORES para mostrar

") - # # exp_indicador = gr.update(value="## Sin datos INDICADORES") - # # file_indicador = gr.update(value=None) - - # # Crear explicación combinada - # explicacion_combinada = """ - # ## 🔄 Tablas Dinámicas Interactivas - - # **Arrastra campos** desde la lista de la izquierda al área central para analizar datos. - - # **Funcionalidades:** - # - 📊 Cambia entre tabla, gráfico de barras, línea, etc. - # - 🔢 Modifica agregaciones (suma, promedio, cuenta) - # - 🎯 Filtra datos con arrastrar y soltar - # - 📥 Descarga el HTML para uso externo - - # **Nota:** Cada tabla es independiente y se actualiza automáticamente. - # """ - - # # return ( - # # html_ods, html_meta, html_indicador, - # # gr.update(value=explicacion_combinada), - # # file_ods, file_meta, file_indicador - # # ) - # return ( - # html_ods, - # gr.update(value=explicacion_combinada), - # file_ods - # ) - - # # CONEXIÓN ÚNICA (cuando cambien los 3 dataframes) - # # for df_component in [df_ods_filtrado, df_meta_filtrado, df_indicador_filtrado]: - # df_ods_filtrado.change( - # fn=actualizar_pivots, - # inputs=[df_ods_filtrado, slider_ods], - # outputs=[ - # html18_ods, #html18_meta, html18_indicador, - # exp18, - # html18_ods_f, #html18_meta_f, html18_indicador_f - # ], - # show_progress=False - # ) - # df_meta_filtrado.change( - # fn=actualizar_pivots, - # inputs=[df_meta_filtrado, slider_meta], - # outputs=[ - # # html18_ods, html18_meta, html18_indicador, - # html18_meta, - # exp18, - # # html18_ods_f, html18_meta_f, html18_indicador_f - # html18_meta_f - # ], - # show_progress=False - # ) - # df_indicador_filtrado.change( - # fn=actualizar_pivots, - # inputs=[df_indicador_filtrado, slider_indicador], - # outputs=[ - # # html18_ods, html18_meta, html18_indicador, - # html18_indicador, - # exp18, - # # html18_ods_f, html18_meta_f, html18_indicador_f - # html18_indicador_f - # ], - # show_progress=False - # ) - - # btn.click(search, query_in, [query_out,ods,meta,indicador,genero,poblacional,etnico,pilar,estrategia,categoria,bdl_ods]) - btn.click(search, query_in, [query_out,ods,meta,indicador,bdl_ods, msj_procesamiento], show_progress=True) - btn.click(ver_radio, inputs=[], outputs=[es_visible]) - # btn.click(fn=tab_viz18, inputs=[ods, txt_ods], outputs=[html18_ods, exp18, html18_ods_f]) - # btn.click(fn=tab_viz18, inputs=[meta, txt_meta], outputs=[html18_meta, exp18, html18_meta_f]) - # btn.click(fn=tab_viz18, inputs=[indicador, txt_indicador], outputs=[html18_indicador, exp18, html18_indicador_f]) - - - with gr.Tab('CONSULTA ESPECIALIZADA', visible=False): - - # with gr.Tab("CONSULTA"): - - with gr.Column(): - query_in_esp = gr.Textbox(lines=5, placeholder="Escribe aquí tu consulta...", label="Iniciativa a analizar") - query_out_esp = gr.Textbox(lines=5, label="Texto ajustado para lenguaje natural", visible=False) - - btn_esp = gr.Button(value="Analizar mi iniciativa") - - - # lvl = gr.Dropdown([col for col in bdl_ods_esp.value.columns if 'ID' in col], label='Nivel de análisis') - # score = gr.Dropdown([col for col in bdl_ods_esp.value.columns if 'similaridad' in col], label='Score de medida') - # rank = gr.Dropdown([col for col in bdl_ods_esp.value.columns if 'rank' in col], label='Score de medida') - - with gr.Tab("Clasificaciones"): - with gr.Row(): - ods_esp = gr.Dataframe(value=pd.DataFrame(), label="ODS") - meta_esp = gr.Dataframe(value=pd.DataFrame(), label="METAS") - indicador_esp = gr.Dataframe(value=pd.DataFrame(), label="INDICADORES") - - with gr.Row(): - genero_esp = gr.Dataframe(value=pd.DataFrame(), label="Enfoque de genero") - poblacional_esp = gr.Dataframe(value=pd.DataFrame(), label="Enfoque poblacional") - etnico_esp = gr.Dataframe(value=pd.DataFrame(), label="Enfoque étnico") - - with gr.Row(): - pilar_esp = gr.Dataframe(value=pd.DataFrame(), label="Pilares") - estrategia_esp = gr.Dataframe(value=pd.DataFrame(), label="Estrategias") - categoria_esp = gr.Dataframe(value=pd.DataFrame(), label="Categorias") - - with gr.Row(): - bdl_ods_esp = gr.Dataframe(value=pd.DataFrame(), label="ODS") - - - # PESTAÑA: INICIO - with gr.Tab("🏠 Inicio"): - html_inicio_ods = gr.HTML() #tab_inicio(ods.value) - - - btn0 = gr.Button("🔄 Generar Metricas Iniciales", variant="primary") - btn0.click( - fn=tab_inicio, - inputs=[ods_esp,meta_esp,indicador_esp], - outputs=[html_inicio_ods] - ) - - - # PESTAÑA 1: BOX PLOT - with gr.Tab("📦 1. Box Plot"): - btn1 = gr.Button("🔄 Generar Visualización", variant="primary") - with gr.Row(): - with gr.Column(scale=1): - exp1 = gr.Markdown() - with gr.Row(visible=False): - with gr.Column(scale=2): - plot1_1 = gr.Plot(label="Diagrama de Caja por ODS") - - with gr.Row(): - with gr.Column(scale=2): - plot1_2 = gr.Plot(label="Diagrama de Caja por META") - - with gr.Row(): - with gr.Column(scale=2): - plot1_3 = gr.Plot(label="Diagrama de Caja por INDICADOR") - - - - btn1.click( - fn=tab_viz1, - inputs=[ods_esp, meta_esp, indicador_esp], - outputs=[plot1_1, plot1_2, plot1_3, exp1] - ) - - # PESTAÑA 2: HEATMAP - with gr.Tab("🔥 2. Heatmap"): - with gr.Row(): - with gr.Column(scale=2): - img2 = gr.Image(label="Mapa de Calor ODS × Ranking") - with gr.Column(scale=1): - exp2 = gr.Markdown() - - btn2 = gr.Button("🔄 Generar Visualización", variant="primary") - btn2.click( - fn=tab_viz2, - inputs=[ods_esp], - outputs=[img2, exp2] - ) - - # PESTAÑA 3: SCATTER 3D - with gr.Tab("🌐 3. Scatter 3D"): - with gr.Row(): - with gr.Column(scale=2): - plot3 = gr.Plot(label="Gráfico 3D Interactivo") - with gr.Column(scale=1): - exp3 = gr.Markdown() - - btn3 = gr.Button("🔄 Generar Visualización", variant="primary") - btn3.click( - fn=tab_viz3, - inputs=[ods_esp], - outputs=[plot3, exp3] - ) - - # PESTAÑA 4: RADAR - with gr.Tab("🕸️ 4. Radar Chart"): - with gr.Row(): - with gr.Column(scale=2): - plot4 = gr.Plot(label="Gráfico de Radar") - with gr.Column(scale=1): - exp4 = gr.Markdown() - - btn4 = gr.Button("🔄 Generar Visualización", variant="primary") - btn4.click( - fn=tab_viz4, - inputs=[ods_esp], - outputs=[plot4, exp4] - ) - - # PESTAÑA 5: SUNBURST - with gr.Tab("☀️ 5. Sunburst"): - with gr.Row(): - with gr.Column(scale=2): - plot5 = gr.Plot(label="Diagrama de Sol") - with gr.Column(scale=1): - exp5 = gr.Markdown() - - btn5 = gr.Button("🔄 Generar Visualización", variant="primary") - btn5.click( - fn=tab_viz5, - inputs=[ods_esp], - outputs=[plot5, exp5] - ) - - # PESTAÑA 6: TOP INDICADORES - # with gr.Tab("🏆 6. Top Indicadores"): - # with gr.Row(): - # with gr.Column(scale=2): - # plot6 = gr.Plot(label="Top 5 Indicadores por ODS") - # with gr.Column(scale=1): - # exp6 = gr.Markdown() - - # btn6 = gr.Button("🔄 Generar Visualización", variant="primary") - # btn6.click( - # fn=tab_viz6, - # inputs=[df, id_lvl, score, rank, titulo, top_n=3], - # outputs=[plot6, exp6] - # ) - - # PESTAÑA 7: STREAM GRAPH - with gr.Tab("🌊 7. Stream Graph"): - with gr.Row(): - with gr.Column(scale=2): - plot7 = gr.Plot(label="Gráfico de Flujo") - with gr.Column(scale=1): - exp7 = gr.Markdown() - - btn7 = gr.Button("🔄 Generar Visualización", variant="primary") - btn7.click( - fn=tab_viz7, - inputs=[ods_esp], - outputs=[plot7, exp7] - ) - - # PESTAÑA 8: VIOLIN PLOT - with gr.Tab("🎻 8. Violin Plot"): - with gr.Row(): - with gr.Column(scale=2): - plot8 = gr.Plot(label="Gráfico de Violín") - with gr.Column(scale=1): - exp8 = gr.Markdown() - - btn8 = gr.Button("🔄 Generar Visualización", variant="primary") - btn8.click( - fn=tab_viz8, - inputs=[ods_esp], - outputs=[plot8, exp8] - ) - - # PESTAÑA 9: DASHBOARD - with gr.Tab("📊 9. Dashboard"): - with gr.Row(): - with gr.Column(scale=2): - plot9 = gr.Plot(label="Dashboard Integrado") - with gr.Column(scale=1): - exp9 = gr.Markdown() - - btn9 = gr.Button("🔄 Generar Visualización", variant="primary") - btn9.click( - fn=tab_viz9, - inputs=[ods_esp], - outputs=[plot9, exp9] - ) - - # PESTAÑA 10: MATRIZ TRANSICIÓN - with gr.Tab("🔀 10. Matriz Transición"): - with gr.Row(): - with gr.Column(scale=2): - img10 = gr.Image(label="Matriz de Transición") - with gr.Column(scale=1): - exp10 = gr.Markdown() - - btn10 = gr.Button("🔄 Generar Visualización", variant="primary") - btn10.click( - fn=tab_viz10, - inputs=[ods_esp], - outputs=[img10, exp10] - ) - - # PESTAÑA: ESTADÍSTICAS - with gr.Tab("📈 Estadísticas"): - html_stats = gr.HTML() #tab_estadisticas(ods) - - btn11 = gr.Button("🔄 Generar Estadísticas", variant="primary") - btn11.click( - fn=tab_estadisticas, - inputs=[ods_esp], - outputs=[html_stats] - ) - - btn_esp.click(search, query_in_esp, [query_out_esp,ods_esp,meta_esp,indicador_esp,genero_esp,poblacional_esp,etnico_esp,pilar_esp,estrategia_esp,categoria_esp,bdl_ods_esp]) - - with gr.Tab("CONSULTA MASIVA"): - - client = InferenceClient() - - gr.Markdown("Aquí puedes cargar un archivo con múltiples iniciativas para análisis masivo, con enfoque de contexto o embeddings. (Usa la plantilla proporcionada)") - - def procesar_archivo(file): - if file is None: - return pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), [], 'Error al procesar el archivo: no se ha cargado ningún archivo' - try: - return search_mass(file, 3,3,3) - except Exception as e: - print("Error al leer el archivo:", e) - return pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), [], 'Error al procesar el archivo' - - def upload_file(filepath): - name = Path(filepath).name - return gr.update(label=f"Procesar {name}", value=filepath, visible=True) - - def pre_visual_df(file): - if file is None: - return gr.update(value=None) - try: - df = pd.read_excel(file) - return gr.update(value=df.head(10)) - except Exception as e: - print("Error al leer el archivo:", e) - return gr.update(value=None) - - - - with gr.Row(): - # d = gr.DownloadButton(label=f"Descargar plantilla_iniciativas.xlsx", value=Path('data/plantillas/plantilla_iniciativas.xlsx'), visible=True) - d = gr.DownloadButton(label=f"Descargar plantilla_iniciativas.xlsx", value=Path('data/raw/plantilla_iniciativas.xlsx'), visible=True) - - with gr.Column(): - file_output = gr.File() - u = gr.UploadButton("Upload a file", file_count="single") - pre_df = gr.Dataframe(label="Vista previa de las primeras 10 filas del archivo cargado") - # btn_prev_df = gr.Button("Previsualizar archivo") - - u.upload(upload_file, u, file_output) - file_output.change(pre_visual_df, file_output, pre_df) - # btn_prev_df.click(pre_visual_df, u, pre_df) - - # PESTAÑA: CONSULTA - with gr.Tab("MASIVA CON CONTEXTO"): - # DataFrame vacío con estructura de iniciativas - df_iniciativas_template = pd.DataFrame(columns=['id_unico', 'iniciativa', 'ODS_ID', 'META_ID', 'INDICADOR_ID', 'objetivo', 'ods_rank']) - - - with gr.Column(): - btn_mass_emb = gr.Button("Procesar archivo") - msj_procesamiento_mass = gr.Textbox(value="... preparando para procesar documento", interactive=False) - - # PESTAÑA 19: RESUMEN EN TAGS - # with gr.Tab("🏷️ 19. Resumen Tags"): - with gr.Row(): - with gr.Row(): - with gr.Column(): - html19_1 = gr.HTML(label="Resumen en Tags") - with gr.Row(): - html19_2 = gr.HTML(label="Resumen en Tags") - # with gr.Column(scale=4): - html19_3 = gr.HTML(label="Resumen en Tags") - with gr.Column(visible=False): - exp19 = gr.Markdown() - - - with gr.Row(): - with gr.Column(scale=3): - with gr.Tab("Enfoque TOP ODS"): - html_inicio_mass = gr.HTML(label="Resultados del análisis masivo - vista inicial") - with gr.Tab("Enfoque independiente por ODS - Metas - Indicadores"): - base_ods_mass_mix = gr.Dataframe(label="Prueba-Resultados del análisis masivo ODS", visible=False) - base_meta_mass_mix = gr.Dataframe(label="Resultados del análisis masivo METAS", visible=False) - base_indicadores_mass_mix = gr.Dataframe(label="Resultados del análisis masivo INDICADORES", visible=False) - - base_ods_mass_mix_cat = gr.Dataframe(label="Prueba-Resultados del análisis masivo ODS", visible=False) - base_meta_mass_mix_cat = gr.Dataframe(label="Resultados del análisis masivo METAS", visible=False) - base_indicadores_mass_mix_cat = gr.Dataframe(label="Resultados del análisis masivo INDICADORES", visible=False) - - with gr.Row(): - with gr.Tab("🏆 Ranking Masivo ODS"): - with gr.Row(): - with gr.Column(scale=2): - plot_mass_ods = gr.Plot(label="Ranking Global ODS") - with gr.Column(scale=1): - exp_mass_ods = gr.Markdown() - with gr.Tab("📊 Priorización con Pareto"): - gr.HTML(""" -
-

📊 Diagrama de Pareto (Regla 80/20)

-
    -
  1. CRITERIO: Aplicamos un criterio de Pareto ADAPTATIVO calibrado para contextos de clasificación multinivel, - donde el umbral se ajusta según la distribución empírica de similaridad y la granularidad requerida para toma de decisiones.
  2. -
  3. Marcos globales: (UNDP, VNR, EU SDG Strategy) recomiendan 3-5 ODS - prioritarios por ciclo de implementación.
  4. -
  5. Interactúa: Las visualizaciones son dinámicas y permiten exploración
  6. -
-
- """) - - with gr.Row(): - with gr.Column(scale=2): - plotOds_mass = gr.Plot(label="Diagrama de Pareto Agregado por ODS") - with gr.Column(scale=1): - slider_ods_pareto_mass = gr.Slider(0.1, 1.0, step=0.1, value=0.7,interactive=True, label="Top ODS") - expOds_mass = gr.Markdown() - with gr.Tab('Tabla score ODS'): - output_ods_mass_mix = gr.Dataframe(label="Resultados del análisis masivo ODS") - with gr.Row(): - with gr.Tab("🏆 Ranking Masivo METAS"): - with gr.Row(): - with gr.Column(scale=2): - plot_mass_metas = gr.Plot(label="Ranking Global METAS") - with gr.Column(scale=1): - exp_mass_metas = gr.Markdown() - with gr.Tab("📊 Priorización con Pareto"): - gr.HTML(""" -
-

📊 Diagrama de Pareto (Regla 80/20)

-
    -
  1. CRITERIO: Aplicamos un criterio de Pareto ADAPTATIVO calibrado para contextos de clasificación multinivel, - donde el umbral se ajusta según la distribución empírica de similaridad y la granularidad requerida para toma de decisiones.
  2. -
  3. Marcos globales: (UNDP, VNR, EU SDG Strategy) recomiendan 3-5 ODS - prioritarios por ciclo de implementación.
  4. -
  5. Interactúa: Las visualizaciones son dinámicas y permiten exploración
  6. -
-
- """) - - with gr.Row(): - with gr.Column(scale=2): - plotMetas_mass = gr.Plot(label="Diagrama de Pareto Agregado por METAS") - with gr.Column(scale=1): - slider_metas_pareto_mass = gr.Slider(0.1, 1.0, step=0.1, value=0.7,interactive=True, label="Top METAS") - expMetas_mass = gr.Markdown() - with gr.Tab('Tabla score METAS'): - output_meta_mass_mix = gr.Dataframe(label="Resultados del análisis masivo METAS") - with gr.Row(): - with gr.Tab("🏆 Ranking Masivo INDICADORES"): - with gr.Row(): - with gr.Column(scale=2): - plot_mass_indicadores = gr.Plot(label="Ranking Global INDICADORES") - with gr.Column(scale=1): - exp_mass_indicadores = gr.Markdown() - with gr.Tab("📊 Priorización con Pareto"): - gr.HTML(""" -
-

📊 Diagrama de Pareto (Regla 80/20)

-
    -
  1. CRITERIO: Aplicamos un criterio de Pareto ADAPTATIVO calibrado para contextos de clasificación multinivel, - donde el umbral se ajusta según la distribución empírica de similaridad y la granularidad requerida para toma de decisiones.
  2. -
  3. Marcos globales: (UNDP, VNR, EU SDG Strategy) recomiendan 3-5 ODS - prioritarios por ciclo de implementación.
  4. -
  5. Interactúa: Las visualizaciones son dinámicas y permiten exploración
  6. -
-
- """) - - with gr.Row(): - with gr.Column(scale=2): - plotIndicadores_mass = gr.Plot(label="Diagrama de Pareto Agregado por INDICADORES") - with gr.Column(scale=1): - slider_indicadores_pareto_mass = gr.Slider(0.1, 1.0, step=0.1, value=0.7,interactive=True, label="Top INDICADORES") - expIndicadores_mass = gr.Markdown() - with gr.Tab('Tabla score INDICADORES'): - output_indicadores_mass_mix = gr.Dataframe(label="Resultados del análisis masivo INDICADORES") - - - - - - - with gr.Column(scale=1): - # u.upload(fn=upload_file, inputs=[u], outputs=[file_output]) - # categorias = gr.State(value=[]) - def update_choices(listado): - return gr.update(choices=listado) - - with gr.Row(): - - categorias_mass_state = gr.State(value=[]) # Estado para almacenar categorías disponibles - categorias_mass = gr.Dropdown( - choices=categorias_mass_state.value, multiselect=False, label="Categorías"#, info="Selecciona una categoria para aplicar el filtro." - ) - categorias_mass_state.change(fn=update_choices, inputs=[categorias_mass_state], outputs=[categorias_mass]) - - def listar_elementos_categoria(file, categoria): - if file is None: # or categoria not in file.columns: - return "No hay datos disponibles" - df = pd.read_excel(file) - if categoria not in df.columns: - return f"No se encontró la categoría '{categoria}' en el archivo" - elementos = df[categoria].dropna().unique().tolist() - # return ", ".join(elementos) - return gr.update(choices=elementos) - - # filtro_categoria_state = gr.State(value=[]) # Estado para almacenar categorías seleccionadas para filtro - filtro_categoria= gr.Dropdown( - choices=[], multiselect=True, interactive=True, label="Elementos de categoría", info="Selecciona una o varias opciones para aplicar el análisis." - ) - - bttn_filtro_categoria = gr.Button("Aplicar filtro por categoría", variant="secondary") - - categorias_mass.change(fn=listar_elementos_categoria, inputs=[file_output, categorias_mass], outputs=[filtro_categoria]) - - - - - with gr.Row(): - output_ods_mass = gr.Dataframe(label="Resultados del análisis masivo ODS", visible=False) - output_meta_mass = gr.Dataframe(label="Resultados del análisis masivo Metas", visible=False) - output_indicadores_mass = gr.Dataframe(label="Resultados del análisis masivo Indicadores", visible=False) - - ## Opteniendo - - with gr.Row(): - with gr.Column(): - slider_pareto_ods = gr.Slider(0, 1, step=0.05, value=0.40,interactive=True, label="Umbral Pareto ODS / iniciativa") - slider_pareto_meta = gr.Slider(0, 1, step=0.05, value=0.10,interactive=True, label="Umbral Pareto Metas / iniciativa") - slider_pareto_indicador = gr.Slider(0, 1, step=0.05, value=0.10,interactive=True, label="Umbral Pareto Indicadores / iniciativa") - with gr.Column(): - slider_prop_sim = gr.Slider(0, 1, step=0.05, value=0.50,interactive=True, label="Proporción calificación Similaridad") - slider_prop_rank = gr.Slider(0, 1, step=0.05, value=0.35,interactive=True, label="Proporción calificación Ranking") - # prop_frec = gr.State(value=0.20) # 1 - sim - rank - ## Actualización dinámica de proporciones para que siempre sumen 1 - slider_prop_sim.change(fn=lambda sim: gr.update(value=round(0, 2)) if sim is not None else None, inputs=[slider_prop_sim], outputs=[slider_prop_rank]) - slider_prop_sim.change(fn=lambda sim: gr.update(maximum=round(1-sim, 2), value=round(0, 2)) if sim is not None else None, inputs=[slider_prop_sim], outputs=[slider_prop_rank]) - # slider_prop_rank.change(fn=lambda sim, rank: gr.update(value=1-(rank + sim)) if rank is not None else None, inputs=[slider_prop_sim, slider_prop_rank], outputs=[prop_frec]) - - - with gr.Row(): - # with gr.Column(scale=2): - with gr.Tab("Composición del Score"): - plot22 = gr.Plot(label="Composición del Score") - # with gr.Column(scale=1): - with gr.Tab("Detalle de scores"): - exp22 = gr.Markdown() - - # btn22 = gr.Button("🔄 Mostrar Composición", variant="primary") - slider_prop_sim.change( - fn=tab_viz22, - inputs=[slider_prop_sim, slider_prop_rank], - outputs=[plot22, exp22] - ) - slider_prop_rank.change( - fn=tab_viz22, - inputs=[slider_prop_sim, slider_prop_rank], - outputs=[plot22, exp22] - ) - - btn_mass_emb.click( - fn=procesar_archivo, - inputs=[file_output], - outputs=[output_ods_mass, output_meta_mass, output_indicadores_mass,categorias_mass_state, msj_procesamiento_mass], - show_progress=True - ) - - - with gr.Row(): ## Actualizando todas las variables de análisis cada vez que se ajusta un slider o se carga un nuevo archivo, para mantener todo sincronizado - - with gr.Column(): - output_ods_mass.change( - fn=analisis_global_con_pareto, - inputs=[output_ods_mass, gr.State('ods'),slider_pareto_ods, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], - outputs=[output_ods_mass_mix, base_ods_mass_mix] - ) - output_meta_mass.change( - fn=analisis_global_con_pareto, - inputs=[output_meta_mass, gr.State('meta'), slider_pareto_meta, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], - outputs=[output_meta_mass_mix, base_meta_mass_mix] - ) - output_indicadores_mass.change( - fn=analisis_global_con_pareto, - inputs=[output_indicadores_mass, gr.State('indicador'), slider_pareto_indicador, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], - outputs=[output_indicadores_mass_mix, base_indicadores_mass_mix] - ) - - with gr.Column(): - output_ods_mass_mix.change( - fn=tab_inicio_mass, - inputs=[output_ods_mass_mix,output_meta_mass_mix,output_indicadores_mass_mix], - outputs=[html_inicio_mass] - ) - output_meta_mass_mix.change( - fn=tab_inicio_mass, - inputs=[output_ods_mass_mix,output_meta_mass_mix,output_indicadores_mass_mix], - outputs=[html_inicio_mass] - ) - output_indicadores_mass_mix.change( - fn=tab_inicio_mass, - inputs=[output_ods_mass_mix,output_meta_mass_mix,output_indicadores_mass_mix], - outputs=[html_inicio_mass] - ) - - with gr.Column(): - - slider_prop_sim.change( - fn=analisis_global_con_pareto, - inputs=[output_ods_mass, gr.State('ods'), slider_pareto_ods, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], - outputs=[output_ods_mass_mix, base_ods_mass_mix] - ) - slider_prop_sim.change( - fn=analisis_global_con_pareto, - inputs=[output_meta_mass, gr.State('meta'), slider_pareto_meta, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], - outputs=[output_meta_mass_mix, base_meta_mass_mix] - ) - slider_prop_sim.change( - fn=analisis_global_con_pareto, - inputs=[output_indicadores_mass, gr.State('indicador'), slider_pareto_indicador, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], - outputs=[output_indicadores_mass_mix, base_indicadores_mass_mix ] - ) - - slider_prop_rank.change( - fn=analisis_global_con_pareto, - inputs=[output_ods_mass, gr.State('ods'), slider_pareto_ods, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], - outputs=[output_ods_mass_mix, base_ods_mass_mix] - ) - slider_prop_rank.change( - fn=analisis_global_con_pareto, - inputs=[output_meta_mass, gr.State('meta'), slider_pareto_meta, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], - outputs=[output_meta_mass_mix, base_meta_mass_mix ] - ) - slider_prop_rank.change( - fn=analisis_global_con_pareto, - inputs=[output_indicadores_mass, gr.State('indicador'), slider_pareto_indicador, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], - outputs=[output_indicadores_mass_mix, base_indicadores_mass_mix ] - ) - - with gr.Column(): - slider_pareto_ods.change( - fn=analisis_global_con_pareto, - inputs=[output_ods_mass, gr.State('ods'), slider_pareto_ods, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], - outputs=[output_ods_mass_mix, base_ods_mass_mix] - ) - slider_pareto_meta.change( - fn=analisis_global_con_pareto, - inputs=[output_meta_mass, gr.State('meta'), slider_pareto_meta, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], - outputs=[output_meta_mass_mix, base_meta_mass_mix ] - ) - slider_pareto_indicador.change( - fn=analisis_global_con_pareto, - inputs=[output_indicadores_mass, gr.State('indicador'), slider_pareto_indicador, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], - outputs=[output_indicadores_mass_mix, base_indicadores_mass_mix] - ) - - with gr.Column(): - output_ods_mass_mix.change( - fn=tab_viz21, - #df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.75, rank_prop=0.20 - inputs=[output_ods_mass_mix, gr.State('ods'), slider_pareto_ods, gr.State(value='mixto'), slider_prop_sim, slider_prop_rank], - outputs=[plot_mass_ods, exp_mass_ods] - ) - - output_meta_mass_mix.change( - fn=tab_viz21, - #df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.75, rank_prop=0.20 - inputs=[output_meta_mass_mix, gr.State('meta'), slider_pareto_meta, gr.State(value='mixto'), slider_prop_sim, slider_prop_rank], - outputs=[plot_mass_metas, exp_mass_metas] - ) - - output_indicadores_mass_mix.change( - fn=tab_viz21, - #df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.75, rank_prop=0.20 - inputs=[output_indicadores_mass_mix, gr.State('indicador'), slider_pareto_indicador, gr.State(value='mixto'), slider_prop_sim, slider_prop_rank], - outputs=[plot_mass_indicadores, exp_mass_indicadores] - ) - - with gr.Column(): # actualizar grafico barras de puntajes cada vez que se ajusta proporción de similaridad o ranking, para mantener todo sincronizado - - slider_prop_sim.change( - fn=tab_viz21, - #df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.75, rank_prop=0.20 - inputs=[output_ods_mass_mix, gr.State('ods'), slider_pareto_meta, gr.State(value='mixto'), slider_prop_sim, slider_prop_rank], - outputs=[plot_mass_metas, exp_mass_metas] - ) - - slider_prop_rank.change( - fn=tab_viz21, - #df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.75, rank_prop=0.20 - inputs=[output_ods_mass_mix, gr.State('ods'), slider_pareto_indicador, gr.State(value='mixto'), slider_prop_sim, slider_prop_rank], - outputs=[plot_mass_indicadores, exp_mass_indicadores] - ) - - slider_prop_sim.change( - fn=tab_viz21, - #df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.75, rank_prop=0.20 - inputs=[output_meta_mass_mix, gr.State('meta'), slider_pareto_meta, gr.State(value='mixto'), slider_prop_sim, slider_prop_rank], - outputs=[plot_mass_metas, exp_mass_metas] - ) - - slider_prop_rank.change( - fn=tab_viz21, - #df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.75, rank_prop=0.20 - inputs=[output_meta_mass_mix, gr.State('meta'), slider_pareto_indicador, gr.State(value='mixto'), slider_prop_sim, slider_prop_rank], - outputs=[plot_mass_indicadores, exp_mass_indicadores] - ) - - slider_prop_sim.change( - fn=tab_viz21, - #df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.75, rank_prop=0.20 - inputs=[output_indicadores_mass_mix, gr.State('indicador'), slider_pareto_meta, gr.State(value='mixto'), slider_prop_sim, slider_prop_rank], - outputs=[plot_mass_metas, exp_mass_metas] - ) - - slider_prop_rank.change( - fn=tab_viz21, - #df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.75, rank_prop=0.20 - inputs=[output_indicadores_mass_mix, gr.State('indicador'), slider_pareto_indicador, gr.State(value='mixto'), slider_prop_sim, slider_prop_rank], - outputs=[plot_mass_indicadores, exp_mass_indicadores] - ) - - with gr.Column(): - - output_ods_mass_mix.change( - fn=tab_viz20, - inputs=[output_ods_mass_mix, gr.State('ods'), slider_ods_pareto_mass, gr.State(True), gr.State('individual')], - outputs=[plotOds_mass, expOds_mass] - ) - output_meta_mass_mix.change( - fn=tab_viz20, - inputs=[output_meta_mass_mix, gr.State('meta'), slider_metas_pareto_mass, gr.State(True), gr.State('individual')], - outputs=[plotMetas_mass, expMetas_mass] - ) - output_indicadores_mass_mix.change( - fn=tab_viz20, - inputs=[output_indicadores_mass_mix, gr.State('indicador'), slider_indicadores_pareto_mass, gr.State(True), gr.State('individual')], - outputs=[plotIndicadores_mass, expIndicadores_mass] - ) - - ### Actualizando umbral de pareto - - - slider_ods_pareto_mass.change( - fn=tab_viz20, - inputs=[output_ods_mass_mix, gr.State('ods'), slider_ods_pareto_mass, gr.State(True), gr.State('individual')], - outputs=[plotOds_mass, expOds_mass] - ) - slider_metas_pareto_mass.change( - fn=tab_viz20, - inputs=[output_meta_mass_mix, gr.State('meta'), slider_metas_pareto_mass, gr.State(True), gr.State('individual')], - outputs=[plotMetas_mass, expMetas_mass] - ) - slider_indicadores_pareto_mass.change( - fn=tab_viz20, - inputs=[output_indicadores_mass_mix, gr.State('indicador'), slider_indicadores_pareto_mass, gr.State(True), gr.State('individual')], - outputs=[plotIndicadores_mass, expIndicadores_mass] - ) - - with gr.Column(): - # btn19 = gr.Button("🔄 Generar Resumen", variant="primary") - base_ods_mass_mix.change( - fn=tab_viz19, - inputs=[base_ods_mass_mix, base_meta_mass_mix, base_indicadores_mass_mix], - outputs=[html19_1, html19_2, html19_3, exp19] - ) - - base_meta_mass_mix.change( - fn=tab_viz19, - inputs=[base_ods_mass_mix, base_meta_mass_mix, base_indicadores_mass_mix], - outputs=[html19_1, html19_2, html19_3, exp19] - ) - - base_indicadores_mass_mix.change( - fn=tab_viz19, - inputs=[base_ods_mass_mix, base_meta_mass_mix, base_indicadores_mass_mix], - outputs=[html19_1, html19_2, html19_3, exp19] - ) - ## AJUSTE POR FILTROS DE CATEGORIA - with gr.Column(): - bttn_filtro_categoria.click( - fn=lambda df, categoria, elementos: df[df[categoria].isin(elementos)] if df is not None else None, - inputs=[base_ods_mass_mix, categorias_mass, filtro_categoria], - outputs=[base_ods_mass_mix_cat] - ) - - bttn_filtro_categoria.click( - fn=lambda df, categoria, elementos: df[df[categoria].isin(elementos)] if df is not None else None, - inputs=[base_meta_mass_mix, categorias_mass, filtro_categoria], - outputs=[base_meta_mass_mix_cat] - ) - - bttn_filtro_categoria.click( - fn=lambda df, categoria, elementos: df[df[categoria].isin(elementos)] if df is not None else None, - inputs=[base_indicadores_mass_mix, categorias_mass, filtro_categoria], - outputs=[base_indicadores_mass_mix_cat] - ) - - base_ods_mass_mix_cat.change( - fn=tab_viz19, - inputs=[base_ods_mass_mix_cat, base_meta_mass_mix_cat, base_indicadores_mass_mix_cat], - outputs=[html19_1, html19_2, html19_3, exp19] - ) - - base_meta_mass_mix_cat.change( - fn=tab_viz19, - inputs=[base_ods_mass_mix_cat, base_meta_mass_mix_cat, base_indicadores_mass_mix_cat], - outputs=[html19_1, html19_2, html19_3, exp19] - ) - - base_indicadores_mass_mix_cat.change( - fn=tab_viz19, - inputs=[base_ods_mass_mix_cat, base_meta_mass_mix_cat, base_indicadores_mass_mix_cat], - outputs=[html19_1, html19_2, html19_3, exp19] - ) - - - - - # categorias.change(fn=lambda df, n: df[df[f'ods_peso_acumulado_sin_norm'] <= 1-n] if df is not None else None, - # inputs=[output_ods_mass_mix, categorias], - # outputs=[output_ods_mass_mix], api_name="ods") - with gr.Tab("Análisis 2 - Filtrado", visible=False): - with gr.Row(): - with gr.Column(): - slider_massive_ods_sim = gr.Slider(0, 1, step=0.01, value=0.70,interactive=True, label="Sensibilidad ODS") - slider_massive_ods_rank = gr.Slider(0, 10, step=1, value=4,interactive=True, label="Top ODS") - with gr.Column(): - slider_massive_meta_sim = gr.Slider(0, 1, step=0.01, value=0.70,interactive=True, label="Sensibilidad METAS") - slider_massive_meta_rank = gr.Slider(0, 10, step=1, value=4,interactive=True, label="Top METAS") - with gr.Column(): - slider_massive_indicador_sim = gr.Slider(0, 1, step=0.01, value=0.70,interactive=True, label="Sensibilidad INDICADORES") - slider_massive_indicador_rank = gr.Slider(0, 10, step=1, value=4,interactive=True, label="Top INDICADORES") - - with gr.Tab("Tablas Filtradas", visible=True): - with gr.Column(): - df_ods_mass_filtrado = gr.Dataframe(df_iniciativas_template, label="METAS Filtradas") - slider_massive_ods_sim.release(fn=lambda df, n: df[df[f'ods_peso_acumulado_sin_norm'] <= 1-n] if df is not None else None, - inputs=[output_ods_mass, slider_massive_ods_sim], - outputs=[df_ods_mass_filtrado], api_name="ods") - slider_massive_ods_rank.release(fn=lambda df, n: df[df[f'ods_rank'] <= n] if df is not None else None, - inputs=[output_ods_mass, slider_massive_ods_rank], - outputs=[df_ods_mass_filtrado], api_name="ods") - - - df_meta_mass_filtrado = gr.Dataframe(df_iniciativas_template, label="METAS Filtradas") - slider_massive_meta_sim.release(fn=lambda df, n: df[df[f'meta_peso_acumulado_sin_norm'] <= 1-n] if df is not None else None, - inputs=[output_meta_mass, slider_massive_meta_sim], - outputs=[df_meta_mass_filtrado], api_name="ods") - slider_massive_meta_rank.release(fn=lambda df, n: df[df[f'meta_rank'] <= n] if df is not None else None, - inputs=[output_meta_mass, slider_massive_meta_rank], - outputs=[df_meta_mass_filtrado], api_name="ods") - - df_indicador_mass_filtrado = gr.Dataframe(df_iniciativas_template, label="Indicadores Filtradas") - slider_massive_indicador_sim.release(fn=lambda df, n: df[df[f'indicador_peso_acumulado_sin_norm'] <= 1-n] if df is not None else None, - inputs=[output_indicadores_mass, slider_massive_indicador_sim], - outputs=[df_indicador_mass_filtrado], api_name="ods") - slider_massive_indicador_rank.release(fn=lambda df, n: df[df[f'indicador_rank'] <= n] if df is not None else None, - inputs=[output_indicadores_mass, slider_massive_indicador_rank], - outputs=[df_indicador_mass_filtrado], api_name="ods") - - # PESTAÑA Graficas - with gr.Tab("🕸️ Gráficas", visible=False): - - - with gr.Row(): - with gr.Column(scale=2): - plot4 = gr.Plot(label="Gráfico de Radar") - with gr.Column(scale=1): - exp4 = gr.Markdown() - - # btn4_m = gr.Button("🔄 Generar Visualización", variant="primary") - df_ods_mass_filtrado.change( - fn=tab_viz4, - inputs=[df_ods_mass_filtrado, gr.State('ODS_ID'), gr.State('ods_peso_sin_norm'), gr.State('ods_rank'), gr.State('ODS')], - outputs=[plot4, exp4] - ) - - # with gr.Row(): - # with gr.Column(scale=2): - # plot6 = gr.Plot(label="Top 5 Indicadores por ODS") - # with gr.Column(scale=1): - # exp6 = gr.Markdown() - - # # btn6 = gr.Button("🔄 Generar Visualización", variant="primary") - # df_meta_mass_filtrado.change( - # fn=tab_viz6, - # inputs=[df_meta_mass_filtrado, gr.State('ODS_ID'), gr.State('meta_similaridad_cos'), gr.State('meta_rank'), gr.State('METAS'), gr.State(3)], - # outputs=[plot6, exp6] - # ) - - # PESTAÑA 5: SUNBURST - # with gr.Tab("☀️ 5. Sunburst"): - with gr.Row(): - with gr.Column(scale=2): - plot5 = gr.Plot(label="Diagrama de Sol") - with gr.Column(scale=1): - exp5 = gr.Markdown() - - # btn5 = gr.Button("🔄 Generar Visualización", variant="primary") - df_meta_mass_filtrado.change( - fn=tab_viz5, - inputs=[df_meta_mass_filtrado, gr.State('META_ID1'), gr.State('meta_rank'), gr.State('meta_rank'), gr.State('METAS')], - outputs=[plot5, exp5] - ) - - with gr.Tab("MASIVA CON PROMPTING", visible=False): - gr.Markdown("Aquí puedes processar múltiples iniciativas con enfoque de prompting. (Usa la plantilla proporcionada)") - - - - # client = InferenceClient(token="hf_tu_token_aqui") - - # Celda 3: Prompts por nivel - PROMPT_ODS = """Vincula entre 3 y 5 Objetivos de Desarrollo Sostenible (ODS) que tengan una relación directa con la siguiente iniciativa: - - "{iniciativa}" - - Para cada ODS vinculado incluye: - - Número y nombre del ODS - - Justificación breve de por qué se vincula (impacto social, territorial, de paz, infraestructura o inclusión) - - Formato de respuesta (una línea por ODS, ordenados por relevancia de mayor a menor): - ODS [número]: [nombre] – [justificación] - - Responde ÚNICAMENTE con el listado en el formato indicado, sin texto adicional.""" - - - PROMPT_META = """Identifica entre 3 y 5 Metas de los Objetivos de Desarrollo Sostenible (ODS) que tengan relación directa con la siguiente iniciativa: - - "{iniciativa}" - - Para cada meta incluye: - - Código de la meta (ej: 4.2, 1.3, 16.10) - - Descripción de la meta - - Justificación breve del vínculo con la iniciativa - - Formato de respuesta (una línea por meta, ordenadas por relevancia de mayor a menor): - Meta [código]: [descripción] – [justificación] - - Responde ÚNICAMENTE con el listado en el formato indicado, sin texto adicional.""" - - - PROMPT_INDICADOR = """Identifica entre 3 y 5 Indicadores de los Objetivos de Desarrollo Sostenible (ODS) que permitan medir el impacto de la siguiente iniciativa: - - "{iniciativa}" - - Para cada indicador incluye: - - Código del indicador (ej: 4.2.1, 1.3.1, 16.10.1) - - Descripción del indicador - - Justificación breve de cómo se relaciona con la medición de la iniciativa - - Formato de respuesta (una línea por indicador, ordenados por relevancia de mayor a menor): - Indicador [código]: [descripción] – [justificación] - - Responde ÚNICAMENTE con el listado en el formato indicado, sin texto adicional.""" - - - # Celda 4: Funciones de parsing por nivel - def parsear_ods(texto: str) -> list: - """Extrae ODS de la respuesta.""" - items = [] - for linea in texto.strip().split("\n"): - linea = linea.strip() - if not linea: - continue - # Buscar patrón ODS [número]: [nombre] – [justificación] - match = re.match(r"(?:\d+\.\s*)?ODS\s*(\d+):?\s*(.+)", linea, re.IGNORECASE) - if match: - ods_num = match.group(1) - resto = match.group(2).strip() - items.append({ - "ods_id": f"ODS {ods_num}", - "objetivo": resto - }) - return items - - - def parsear_metas(texto: str) -> list: - """Extrae metas de la respuesta.""" - items = [] - for linea in texto.strip().split("\n"): - linea = linea.strip() - if not linea: - continue - # Buscar patrón Meta [código]: [descripción] - match = re.match(r"(?:\d+\.\s*)?Meta\s*([\d\.]+):?\s*(.+)", linea, re.IGNORECASE) - if match: - meta_codigo = match.group(1) - resto = match.group(2).strip() - # Extraer ODS del código de meta - ods_num = meta_codigo.split(".")[0] - items.append({ - "ods_id": f"ODS {ods_num}", - "meta_id": f"Meta {meta_codigo}", - "objetivo": resto - }) - return items - - - def parsear_indicadores(texto: str) -> list: - """Extrae indicadores de la respuesta.""" - items = [] - for linea in texto.strip().split("\n"): - linea = linea.strip() - if not linea: - continue - # Buscar patrón Indicador [código]: [descripción] - match = re.match(r"(?:\d+\.\s*)?Indicador\s*([\d\.]+):?\s*(.+)", linea, re.IGNORECASE) - if match: - ind_codigo = match.group(1) - resto = match.group(2).strip() - # Extraer ODS del código - ods_num = ind_codigo.split(".")[0] - items.append({ - "ods_id": f"ODS {ods_num}", - "indicador_id": f"Indicador {ind_codigo}", - "objetivo": resto - }) - return items - - - # Celda 5: Función de clasificación por nivel - def clasificar_nivel(iniciativa: str, nivel: str) -> dict: - """Clasifica una iniciativa en un nivel específico (ods, meta, indicador).""" - prompts = { - "ods": PROMPT_ODS, - "meta": PROMPT_META, - "indicador": PROMPT_INDICADOR - } - parsers = { - "ods": parsear_ods, - "meta": parsear_metas, - "indicador": parsear_indicadores - } - - try: - respuesta = client.chat_completion( - model="Qwen/Qwen2.5-7B-Instruct", - messages=[{"role": "user", "content": prompts[nivel].format(iniciativa=iniciativa)}], - max_tokens=1000, - temperature=0.3 - ) - texto = respuesta.choices[0].message.content - items = parsers[nivel](texto) - - return { - "iniciativa": iniciativa, - "items": items, - "respuesta_raw": texto, - "error": None - } - except Exception as e: - return { - "iniciativa": iniciativa, - "items": [], - "respuesta_raw": "", - "error": str(e) - } - - - # Celda 6: Función principal de procesamiento por lotes - def procesar_lote(iniciativas: list, mostrar_progreso: bool = True) -> dict: - """ - Procesa un listado de iniciativas y genera 3 DataFrames: - - df_ods: Clasificación a nivel ODS - - df_metas: Clasificación a nivel Meta - - df_indicadores: Clasificación a nivel Indicador - """ - niveles = ["ods", "meta", "indicador"] - resultados = {nivel: [] for nivel in niveles} - - total = len(iniciativas) * len(niveles) - contador = 0 - - for idx, iniciativa in enumerate(iniciativas): - iniciativa_id = f"INI_{idx + 1:04d}" - - for nivel in niveles: - contador += 1 - if mostrar_progreso: - print(f"[{contador}/{total}] {iniciativa_id} - {nivel.upper()}: {iniciativa[:50]}...") - - resultado = clasificar_nivel(iniciativa, nivel) - - if resultado["error"]: - resultados[nivel].append({ - "INICIATIVA_ID": iniciativa_id, - "ODS_ID": "ERROR", - "OBJETIVO": resultado["error"], - "ods_rank": 0, - "iniciativa": iniciativa - }) - else: - for rank, item in enumerate(resultado["items"], start=1): - fila = { - "INICIATIVA_ID": iniciativa_id, - "ODS_ID": item.get("ods_id", ""), - "OBJETIVO": item.get("objetivo", ""), - "ods_rank": rank, - "iniciativa": iniciativa - } - # Agregar columna específica del nivel si aplica - if nivel == "meta": - fila["META_ID"] = item.get("meta_id", "") - elif nivel == "indicador": - fila["INDICADOR_ID"] = item.get("indicador_id", "") - - resultados[nivel].append(fila) - - time.sleep(0.5) # evitar rate limits - - # Crear DataFrames - df_ods = pd.DataFrame(resultados["ods"]) - if not df_ods.empty and "INICIATIVA_ID" in df_ods.columns: - df_ods = df_ods[["INICIATIVA_ID", "ODS_ID", "OBJETIVO", "ods_rank", "iniciativa"]] - - df_metas = pd.DataFrame(resultados["meta"]) - if not df_metas.empty and "META_ID" in df_metas.columns: - df_metas = df_metas[["INICIATIVA_ID", "ODS_ID", "META_ID", "OBJETIVO", "ods_rank", "iniciativa"]] - - df_indicadores = pd.DataFrame(resultados["indicador"]) - if not df_indicadores.empty and "INDICADOR_ID" in df_indicadores.columns: - df_indicadores = df_indicadores[["INICIATIVA_ID", "ODS_ID", "INDICADOR_ID", "OBJETIVO", "ods_rank", "iniciativa"]] - - # return { - # "df_ods": df_ods, - # "df_metas": df_metas, - # "df_indicadores": df_indicadores - # } - return df_ods, df_metas, df_indicadores - - def procesar_desde_archivo(ruta_archivo: str, columna_iniciativa: str = None) -> dict: - """Carga iniciativas desde CSV/Excel y las procesa.""" - if ruta_archivo.endswith(".csv"): - df = pd.read_csv(ruta_archivo) - else: - df = pd.read_excel(ruta_archivo) - - # Detectar columna si no se especifica - if columna_iniciativa is None: - for col in df.columns: - if any(x in col.lower() for x in ["iniciativa", "descripcion", "nombre", "proyecto"]): - columna_iniciativa = col - break - if columna_iniciativa is None: - columna_iniciativa = df.columns[0] - - print(f"Usando columna: {columna_iniciativa}") - print(f"Total iniciativas: {len(df)}") - - iniciativas = df[columna_iniciativa].astype(str).tolist() - return procesar_lote(iniciativas) - - btn_mass_prompt = gr.Button("Procesar archivo") - - with gr.Column(): - output_ods_mass_prompt = gr.Dataframe(label="Resultados del análisis masivo ODS", visible=True, interactive=True) - output_meta_mass_prompt = gr.Dataframe(label="Resultados del análisis masivo Metas", visible=True, interactive=True) - output_indicadores_mass_prompt = gr.Dataframe(label="Resultados del análisis masivo Indicadores", visible=True, interactive=True) - - btn_mass_prompt.click( - fn=procesar_desde_archivo, - inputs=[file_output, gr.State('iniciativa')], - outputs=[output_ods_mass_prompt, output_meta_mass_prompt, output_indicadores_mass_prompt], - show_progress=True - ) - - - # --- - # ### 📚 Recursos Adicionales - # - **Documentación completa**: Consulta los archivos `.md` incluidos - # - **Código fuente**: `visualizaciones_ods.py` - # - **Documento Word**: `GUIA_VISUALIZACIONES_PUBLICO_GENERAL.docx` - - # --- - # *Sistema de Visualización ODS | Febrero 2026 | Desarrollado con Python, Plotly, Matplotlib y Gradio* - # Pie de página - gr.Markdown(""" - --- - --- - *Sistema de Visualización ODS | Febrero 2026 | Desarrollado con Python, Plotly, Matplotlib y Gradio* - """) - - return app - -# ============================================================================ -# EJECUCIÓN DE LA APLICACIÓN -# ============================================================================ - -if __name__ == "__main__": - print("\n" + "="*70) - print("INICIANDO APLICACIÓN GRADIO - VISUALIZACIONES ODS") - print("="*70) - - # if not DATOS_CARGADOS: - # print("\n⚠️ ADVERTENCIA: No se pudieron cargar los datos.") - # print(" Verifica que el archivo existe en:", RUTA_DATOS) - # print(" La aplicación se iniciará pero mostrará errores.") - # else: - # print(f"\n✓ Datos cargados correctamente: {len(df_global)} registros") - # print(f"✓ ODS únicos: {df_global['ods_id'].nunique()}") - - print("\n" + "="*70) - print("CREANDO APLICACIÓN...") - print("="*70) - - app = crear_app() - - print("\n✓ Aplicación creada exitosamente") - print("\n" + "="*70) - print("INICIANDO SERVIDOR WEB...") - print("="*70) - print("\n🌐 La aplicación se abrirá en tu navegador automáticamente") - # print("📍 URL local: http://127.0.0.1:7860") - print("🌍 URL pública: Se generará si share=True\n") - print("💡 Presiona Ctrl+C para detener el servidor\n") - - # Lanzar la aplicación - app.launch( - # theme="light", - # server_name="0.0.0.0", # Permite acceso desde cualquier IP - # server_port=7860, # Puerto por defecto - # share=True, # Cambiar a True para URL pública - show_error=True, # Mostrar errores en la interfaz - # quiet=False # Mostrar logs en consola - debug=True # Modo debug para desarrollo - ) +""" +APLICACIÓN WEB GRADIO - VISUALIZACIONES ODS +============================================ + +Aplicación interactiva que permite explorar las 10 visualizaciones +de análisis de similaridad ODS a través de una interfaz web amigable. + +Características: +- Interfaz con pestañas para cada visualización +- Explicaciones integradas para público general +- Visualizaciones interactivas (HTML) y estáticas (PNG) +- Estadísticas en tiempo real +- Diseño responsivo y profesional + +Autor: Daniel Sandvoval +Fecha: Noviembre 2025 +""" + +import gradio as gr +import pandas as pd +import numpy as np +from pathlib import Path +import plotly.graph_objects as go +import plotly.express as px +from plotly.subplots import make_subplots +import matplotlib.pyplot as plt +import seaborn as sns +from src.embeddings.modelos_nlp_db import search +from src.embeddings.mass_modelos_nlp_db import search_mass +from huggingface_hub import InferenceClient +# import pandas as pd +import re +import time + + +# Importar funciones de visualización +import sys +# sys.path.insert(0, '/home/claude') +from src.visualization.visualizaciones_ods import ( + cargar_datos, + viz_1_distribucion_por_ods, + viz_2_heatmap_ods_ranking, + viz_3_scatter_3d_interactivo, + viz_4_radar_chart_ods, + viz_5_sunburst_jerarquia, + viz_6_top_indicadores_por_ods, + viz_7_streamgraph_similaridad, + viz_8_violin_plot_ods, + viz_9_dashboard_metricas, + viz_10_matriz_transicion, + viz_19_resumen_tags, + viz_20_pareto_ods, + viz_21_ranking_mixto_masivo, + viz_22_composicion_score_mixto, + viz_24_header_conecta_ods, + analisis_estadistico +) + +# ============================================================================ +# CONFIGURACIÓN GLOBAL +# ============================================================================ +import os +import base64 + +levels = ['ODS_ID','META_ID','INDICADOR_ID'] + + + + +def convertir_logo_a_base64(logo_path): + """Convierte un logo a base64 para incrustar en HTML""" + # try: + # rutas_posibles = [ + # logo_path, + # os.path.join(os.path.dirname(__file__), logo_path), + # os.path.join('/mnt/user-data/outputs', logo_path), + # ] + + # for ruta in rutas_posibles: + # if os.path.exists(ruta): + # with open(ruta, "rb") as image_file: + # encoded = base64.b64encode(image_file.read()).decode() + # return f"data:image/png;base64,{encoded}" + + # print(f"⚠️ Logo no encontrado: {logo_path}") + # return "" + # except Exception as e: + # print(f"⚠️ Error al cargar logo: {e}") + # return "" + ruta = Path('config/institucional/logos') + with open(Path(f'{ruta}/{logo_path}'), "rb") as image_file: + encoded = base64.b64encode(image_file.read()).decode() + return f"data:image/png;base64,{encoded}" + + + +# Cargar logos una sola vez al iniciar +print("Cargando logos institucionales...") +LOGO_GOBIERNO = convertir_logo_a_base64("/institucional/GOBIERNO-DE-COLOMBIA_HORIZONTAL.webp") +LOGO_FONDO = convertir_logo_a_base64("/institucional/LOGO MPTF (ESP).webp") + + +if LOGO_GOBIERNO and LOGO_FONDO: + print("✅ Logos cargados correctamente") +else: + print("⚠️ Algunos logos no se pudieron cargar") + +# Diccionario de colores oficiales de los ODS (Fuente: Guías de la ONU) +colores_ods = { + "1": "#E5243B", # Red - ODS 1: Fin de la Pobreza + "2": "#DDA63A", # Mustard - ODS 2: Hambre Cero + "3": "#4C9F38", # Kelly Green - ODS 3: Salud y Bienestar + "4": "#C5192D", # Dark Red - ODS 4: Educación de Calidad + "5": "#FF3A21", # Red Orange - ODS 5: Igualdad de Género + "6": "#26BDE2", # Bright Blue - ODS 6: Agua Limpia y Saneamiento + "7": "#FCC30B", # Yellow - ODS 7: Energía Asequible y No Contaminante + "8": "#A21942", # Burgundy Red - ODS 8: Trabajo Decente y Crecimiento Económico + "9": "#FD6925", # Orange - ODS 9: Industria, Innovación e Infraestructura + "10": "#DD1367", # Magenta - ODS 10: Reducción de las Desigualdades + "11": "#FD9D24", # Golden Yellow - ODS 11: Ciudades y Comunidades Sostenibles + "12": "#BF8B2E", # Dark Mustard - ODS 12: Producción y Consumo Responsables + "13": "#3F7E44", # Dark Green - ODS 13: Acción por el Clima + "14": "#0A97D9", # Blue - ODS 14: Vida Submarina + "15": "#56C02B", # Lime Green - ODS 15: Vida de Ecosistemas Terrestres + "16": "#00689D", # Royal Blue - ODS 16: Paz, Justicia e Instituciones Sólidas + "17": "#19486A", # Navy Blue - ODS 17: Alianzas para Lograr los Objetivos +} + +# Color adicional para el logotipo general de los ODS +color_logo_ods = "#009EDB" # Logo Blue + +dict_logos = { + 'gobierno': convertir_logo_a_base64("/institucional/GOBIERNO-DE-COLOMBIA_HORIZONTAL.webp"), + 'fondo_un': convertir_logo_a_base64("/institucional/LOGO MPTF (ESP).webp"), + 'logo_conectaods': convertir_logo_a_base64("/institucional/logo_conectaods_2.webp"), + 'ods_1': convertir_logo_a_base64("/ods/S-WEB-Goal-01.webp"), + 'ods_2': convertir_logo_a_base64("/ods/S-WEB-Goal-02.webp"), + 'ods_3': convertir_logo_a_base64("/ods/S-WEB-Goal-03.webp"), + 'ods_4': convertir_logo_a_base64("/ods/S-WEB-Goal-04.webp"), + 'ods_5': convertir_logo_a_base64("/ods/S-WEB-Goal-05.webp"), + 'ods_6': convertir_logo_a_base64("/ods/S-WEB-Goal-06.webp"), + 'ods_7': convertir_logo_a_base64("/ods/S-WEB-Goal-07.webp"), + 'ods_8': convertir_logo_a_base64("/ods/S-WEB-Goal-08.webp"), + 'ods_9': convertir_logo_a_base64("/ods/S-WEB-Goal-09.webp"), + 'ods_10': convertir_logo_a_base64("/ods/S-WEB-Goal-10.webp"), + 'ods_11': convertir_logo_a_base64("/ods/S-WEB-Goal-11.webp"), + 'ods_12': convertir_logo_a_base64("/ods/S-WEB-Goal-12.webp"), + 'ods_13': convertir_logo_a_base64("/ods/S-WEB-Goal-13.webp"), + 'ods_14': convertir_logo_a_base64("/ods/S-WEB-Goal-14.webp"), + 'ods_15': convertir_logo_a_base64("/ods/S-WEB-Goal-15.webp"), + 'ods_16': convertir_logo_a_base64("/ods/S-WEB-Goal-16.webp"), + 'ods_17': convertir_logo_a_base64("/ods/S-WEB-Goal-17.webp"), + # Metas ODS + 'meta_1.1': convertir_logo_a_base64("/metas/MC_Target_1.1.webp"), + 'meta_1.2': convertir_logo_a_base64("/metas/MC_Target_1.2.webp"), + 'meta_1.3': convertir_logo_a_base64("/metas/MC_Target_1.3.webp"), + 'meta_1.4': convertir_logo_a_base64("/metas/MC_Target_1.4.webp"), + 'meta_1.5': convertir_logo_a_base64("/metas/MC_Target_1.5.webp"), + 'meta_1.A': convertir_logo_a_base64("/metas/MC_Target_1.A.webp"), + 'meta_1.B': convertir_logo_a_base64("/metas/MC_Target_1.B.webp"), + 'meta_2.1': convertir_logo_a_base64("/metas/MC_Target_2.1.webp"), + 'meta_2.2': convertir_logo_a_base64("/metas/MC_Target_2.2.webp"), + 'meta_2.3': convertir_logo_a_base64("/metas/MC_Target_2.3.webp"), + 'meta_2.4': convertir_logo_a_base64("/metas/MC_Target_2.4.webp"), + 'meta_2.5': convertir_logo_a_base64("/metas/MC_Target_2.5.webp"), + 'meta_2.A': convertir_logo_a_base64("/metas/MC_Target_2.A.webp"), + 'meta_2.B': convertir_logo_a_base64("/metas/MC_Target_2.B.webp"), + 'meta_2.C': convertir_logo_a_base64("/metas/MC_Target_2.C.webp"), + 'meta_3.1': convertir_logo_a_base64("/metas/MC_Target_3.1.webp"), + 'meta_3.2': convertir_logo_a_base64("/metas/MC_Target_3.2.webp"), + 'meta_3.3': convertir_logo_a_base64("/metas/MC_Target_3.3.webp"), + 'meta_3.4': convertir_logo_a_base64("/metas/MC_Target_3.4.webp"), + 'meta_3.5': convertir_logo_a_base64("/metas/MC_Target_3.5.webp"), + 'meta_3.6': convertir_logo_a_base64("/metas/MC_Target_3.6.webp"), + 'meta_3.7': convertir_logo_a_base64("/metas/MC_Target_3.7.webp"), + 'meta_3.8': convertir_logo_a_base64("/metas/MC_Target_3.8.webp"), + 'meta_3.9': convertir_logo_a_base64("/metas/MC_Target_3.9.webp"), + 'meta_3.A': convertir_logo_a_base64("/metas/MC_Target_3.A.webp"), + 'meta_3.B': convertir_logo_a_base64("/metas/MC_Target_3.B.webp"), + 'meta_3.C': convertir_logo_a_base64("/metas/MC_Target_3.C.webp"), + 'meta_3.D': convertir_logo_a_base64("/metas/MC_Target_3.D.webp"), + 'meta_4.1': convertir_logo_a_base64("/metas/MC_Target_4.1.webp"), + 'meta_4.2': convertir_logo_a_base64("/metas/MC_Target_4.2.webp"), + 'meta_4.3': convertir_logo_a_base64("/metas/MC_Target_4.3.webp"), + 'meta_4.4': convertir_logo_a_base64("/metas/MC_Target_4.4.webp"), + 'meta_4.5': convertir_logo_a_base64("/metas/MC_Target_4.5.webp"), + 'meta_4.6': convertir_logo_a_base64("/metas/MC_Target_4.6.webp"), + 'meta_4.7': convertir_logo_a_base64("/metas/MC_Target_4.7.webp"), + 'meta_4.A': convertir_logo_a_base64("/metas/MC_Target_4.A.webp"), + 'meta_4.B': convertir_logo_a_base64("/metas/MC_Target_4.B.webp"), + 'meta_4.C': convertir_logo_a_base64("/metas/MC_Target_4.C.webp"), + 'meta_5.1': convertir_logo_a_base64("/metas/MC_Target_5.1.webp"), + 'meta_5.2': convertir_logo_a_base64("/metas/MC_Target_5.2.webp"), + 'meta_5.3': convertir_logo_a_base64("/metas/MC_Target_5.3.webp"), + 'meta_5.4': convertir_logo_a_base64("/metas/MC_Target_5.4.webp"), + 'meta_5.5': convertir_logo_a_base64("/metas/MC_Target_5.5.webp"), + 'meta_5.6': convertir_logo_a_base64("/metas/MC_Target_5.6.webp"), + 'meta_5.A': convertir_logo_a_base64("/metas/MC_Target_5.A.webp"), + 'meta_5.B': convertir_logo_a_base64("/metas/MC_Target_5.B.webp"), + 'meta_5.C': convertir_logo_a_base64("/metas/MC_Target_5.C.webp"), + 'meta_6.1': convertir_logo_a_base64("/metas/MC_Target_6.1.webp"), + 'meta_6.2': convertir_logo_a_base64("/metas/MC_Target_6.2.webp"), + 'meta_6.3': convertir_logo_a_base64("/metas/MC_Target_6.3.webp"), + 'meta_6.4': convertir_logo_a_base64("/metas/MC_Target_6.4.webp"), + 'meta_6.5': convertir_logo_a_base64("/metas/MC_Target_6.5.webp"), + 'meta_6.6': convertir_logo_a_base64("/metas/MC_Target_6.6.webp"), + 'meta_6.A': convertir_logo_a_base64("/metas/MC_Target_6.A.webp"), + 'meta_6.B': convertir_logo_a_base64("/metas/MC_Target_6.B.webp"), + 'meta_7.1': convertir_logo_a_base64("/metas/MC_Target_7.1.webp"), + 'meta_7.2': convertir_logo_a_base64("/metas/MC_Target_7.2.webp"), + 'meta_7.3': convertir_logo_a_base64("/metas/MC_Target_7.3.webp"), + 'meta_7.A': convertir_logo_a_base64("/metas/MC_Target_7.A.webp"), + 'meta_7.B': convertir_logo_a_base64("/metas/MC_Target_7.B.webp"), + 'meta_8.1': convertir_logo_a_base64("/metas/MC_Target_8.1.webp"), + 'meta_8.2': convertir_logo_a_base64("/metas/MC_Target_8.2.webp"), + 'meta_8.3': convertir_logo_a_base64("/metas/MC_Target_8.3.webp"), + 'meta_8.4': convertir_logo_a_base64("/metas/MC_Target_8.4.webp"), + 'meta_8.5': convertir_logo_a_base64("/metas/MC_Target_8.5.webp"), + 'meta_8.6': convertir_logo_a_base64("/metas/MC_Target_8.6.webp"), + 'meta_8.7': convertir_logo_a_base64("/metas/MC_Target_8.7.webp"), + 'meta_8.8': convertir_logo_a_base64("/metas/MC_Target_8.8.webp"), + 'meta_8.9': convertir_logo_a_base64("/metas/MC_Target_8.9.webp"), + 'meta_8.10': convertir_logo_a_base64("/metas/MC_Target_8.10.webp"), + 'meta_8.A': convertir_logo_a_base64("/metas/MC_Target_8.A.webp"), + 'meta_8.B': convertir_logo_a_base64("/metas/MC_Target_8.B.webp"), + 'meta_9.1': convertir_logo_a_base64("/metas/MC_Target_9.1.webp"), + 'meta_9.2': convertir_logo_a_base64("/metas/MC_Target_9.2.webp"), + 'meta_9.3': convertir_logo_a_base64("/metas/MC_Target_9.3.webp"), + 'meta_9.4': convertir_logo_a_base64("/metas/MC_Target_9.4.webp"), + 'meta_9.5': convertir_logo_a_base64("/metas/MC_Target_9.5.webp"), + 'meta_9.A': convertir_logo_a_base64("/metas/MC_Target_9.A.webp"), + 'meta_9.B': convertir_logo_a_base64("/metas/MC_Target_9.B.webp"), + 'meta_9.C': convertir_logo_a_base64("/metas/MC_Target_9.C.webp"), + 'meta_10.1': convertir_logo_a_base64("/metas/MC_Target_10.1.webp"), + 'meta_10.2': convertir_logo_a_base64("/metas/MC_Target_10.2.webp"), + 'meta_10.3': convertir_logo_a_base64("/metas/MC_Target_10.3.webp"), + 'meta_10.4': convertir_logo_a_base64("/metas/MC_Target_10.4.webp"), + 'meta_10.5': convertir_logo_a_base64("/metas/MC_Target_10.5.webp"), + 'meta_10.6': convertir_logo_a_base64("/metas/MC_Target_10.6.webp"), + 'meta_10.7': convertir_logo_a_base64("/metas/MC_Target_10.7.webp"), + 'meta_10.A': convertir_logo_a_base64("/metas/MC_Target_10.A.webp"), + 'meta_10.B': convertir_logo_a_base64("/metas/MC_Target_10.B.webp"), + 'meta_10.C': convertir_logo_a_base64("/metas/MC_Target_10.C.webp"), + 'meta_11.1': convertir_logo_a_base64("/metas/MC_Target_11.1.webp"), + 'meta_11.2': convertir_logo_a_base64("/metas/MC_Target_11.2.webp"), + 'meta_11.3': convertir_logo_a_base64("/metas/MC_Target_11.3.webp"), + 'meta_11.4': convertir_logo_a_base64("/metas/MC_Target_11.4.webp"), + 'meta_11.5': convertir_logo_a_base64("/metas/MC_Target_11.5.webp"), + 'meta_11.6': convertir_logo_a_base64("/metas/MC_Target_11.6.webp"), + 'meta_11.7': convertir_logo_a_base64("/metas/MC_Target_11.7.webp"), + 'meta_11.A': convertir_logo_a_base64("/metas/MC_Target_11.A.webp"), + 'meta_11.B': convertir_logo_a_base64("/metas/MC_Target_11.B.webp"), + 'meta_11.C': convertir_logo_a_base64("/metas/MC_Target_11.C.webp"), + 'meta_12.1': convertir_logo_a_base64("/metas/MC_Target_12.1.webp"), + 'meta_12.2': convertir_logo_a_base64("/metas/MC_Target_12.2.webp"), + 'meta_12.3': convertir_logo_a_base64("/metas/MC_Target_12.3.webp"), + 'meta_12.4': convertir_logo_a_base64("/metas/MC_Target_12.4.webp"), + 'meta_12.5': convertir_logo_a_base64("/metas/MC_Target_12.5.webp"), + 'meta_12.6': convertir_logo_a_base64("/metas/MC_Target_12.6.webp"), + 'meta_12.7': convertir_logo_a_base64("/metas/MC_Target_12.7.webp"), + 'meta_12.8': convertir_logo_a_base64("/metas/MC_Target_12.8.webp"), + 'meta_12.A': convertir_logo_a_base64("/metas/MC_Target_12.A.webp"), + 'meta_12.B': convertir_logo_a_base64("/metas/MC_Target_12.B.webp"), + 'meta_12.C': convertir_logo_a_base64("/metas/MC_Target_12.C.webp"), + 'meta_13.1': convertir_logo_a_base64("/metas/MC_Target_13.1.webp"), + 'meta_13.2': convertir_logo_a_base64("/metas/MC_Target_13.2.webp"), + 'meta_13.3': convertir_logo_a_base64("/metas/MC_Target_13.3.webp"), + 'meta_13.A': convertir_logo_a_base64("/metas/MC_Target_13.A.webp"), + 'meta_13.B': convertir_logo_a_base64("/metas/MC_Target_13.B.webp"), + 'meta_14.1': convertir_logo_a_base64("/metas/MC_Target_14.1.webp"), + 'meta_14.2': convertir_logo_a_base64("/metas/MC_Target_14.2.webp"), + 'meta_14.3': convertir_logo_a_base64("/metas/MC_Target_14.3.webp"), + 'meta_14.4': convertir_logo_a_base64("/metas/MC_Target_14.4.webp"), + 'meta_14.5': convertir_logo_a_base64("/metas/MC_Target_14.5.webp"), + 'meta_14.6': convertir_logo_a_base64("/metas/MC_Target_14.6.webp"), + 'meta_14.7': convertir_logo_a_base64("/metas/MC_Target_14.7.webp"), + 'meta_14.A': convertir_logo_a_base64("/metas/MC_Target_14.A.webp"), + 'meta_14.B': convertir_logo_a_base64("/metas/MC_Target_14.B.webp"), + 'meta_14.C': convertir_logo_a_base64("/metas/MC_Target_14.C.webp"), + 'meta_15.1': convertir_logo_a_base64("/metas/MC_Target_15.1.webp"), + 'meta_15.2': convertir_logo_a_base64("/metas/MC_Target_15.2.webp"), + 'meta_15.3': convertir_logo_a_base64("/metas/MC_Target_15.3.webp"), + 'meta_15.4': convertir_logo_a_base64("/metas/MC_Target_15.4.webp"), + 'meta_15.5': convertir_logo_a_base64("/metas/MC_Target_15.5.webp"), + 'meta_15.6': convertir_logo_a_base64("/metas/MC_Target_15.6.webp"), + 'meta_15.7': convertir_logo_a_base64("/metas/MC_Target_15.7.webp"), + 'meta_15.8': convertir_logo_a_base64("/metas/MC_Target_15.8.webp"), + 'meta_15.9': convertir_logo_a_base64("/metas/MC_Target_15.9.webp"), + 'meta_15.A': convertir_logo_a_base64("/metas/MC_Target_15.A.webp"), + 'meta_15.B': convertir_logo_a_base64("/metas/MC_Target_15.B.webp"), + 'meta_15.C': convertir_logo_a_base64("/metas/MC_Target_15.C.webp"), + 'meta_16.1': convertir_logo_a_base64("/metas/MC_Target_16.1.webp"), + 'meta_16.2': convertir_logo_a_base64("/metas/MC_Target_16.2.webp"), + 'meta_16.3': convertir_logo_a_base64("/metas/MC_Target_16.3.webp"), + 'meta_16.4': convertir_logo_a_base64("/metas/MC_Target_16.4.webp"), + 'meta_16.5': convertir_logo_a_base64("/metas/MC_Target_16.5.webp"), + 'meta_16.6': convertir_logo_a_base64("/metas/MC_Target_16.6.webp"), + 'meta_16.7': convertir_logo_a_base64("/metas/MC_Target_16.7.webp"), + 'meta_16.8': convertir_logo_a_base64("/metas/MC_Target_16.8.webp"), + 'meta_16.9': convertir_logo_a_base64("/metas/MC_Target_16.9.webp"), + 'meta_16.10': convertir_logo_a_base64("/metas/MC_Target_16.10.webp"), + 'meta_16.A': convertir_logo_a_base64("/metas/MC_Target_16.A.webp"), + 'meta_16.B': convertir_logo_a_base64("/metas/MC_Target_16.B.webp"), + 'meta_17.1': convertir_logo_a_base64("/metas/MC_Target_17.1.webp"), + 'meta_17.2': convertir_logo_a_base64("/metas/MC_Target_17.2.webp"), + 'meta_17.3': convertir_logo_a_base64("/metas/MC_Target_17.3.webp"), + 'meta_17.4': convertir_logo_a_base64("/metas/MC_Target_17.4.webp"), + 'meta_17.5': convertir_logo_a_base64("/metas/MC_Target_17.5.webp"), + 'meta_17.6': convertir_logo_a_base64("/metas/MC_Target_17.6.webp"), + 'meta_17.7': convertir_logo_a_base64("/metas/MC_Target_17.7.webp"), + 'meta_17.8': convertir_logo_a_base64("/metas/MC_Target_17.8.webp"), + 'meta_17.9': convertir_logo_a_base64("/metas/MC_Target_17.9.webp"), + 'meta_17.10': convertir_logo_a_base64("/metas/MC_Target_17.10.webp"), + 'meta_17.11': convertir_logo_a_base64("/metas/MC_Target_17.11.webp"), + 'meta_17.12': convertir_logo_a_base64("/metas/MC_Target_17.12.webp"), + 'meta_17.13': convertir_logo_a_base64("/metas/MC_Target_17.13.webp"), + 'meta_17.14': convertir_logo_a_base64("/metas/MC_Target_17.14.webp"), + 'meta_17.15': convertir_logo_a_base64("/metas/MC_Target_17.15.webp"), + 'meta_17.16': convertir_logo_a_base64("/metas/MC_Target_17.16.webp"), + 'meta_17.17': convertir_logo_a_base64("/metas/MC_Target_17.17.webp"), + 'meta_17.18': convertir_logo_a_base64("/metas/MC_Target_17.18.webp"), + 'meta_17.19': convertir_logo_a_base64("/metas/MC_Target_17.19.webp"), + +} + +# Ruta al archivo de datos +# # RUTA_DATOS = '/mnt/user-data/uploads/indicadores_markdown.txt' +# RUTA_DATOS = '/content/drive/MyDrive/Compartida/06_Desarrollo de la herramienta IA/01_MPTF /archivos_trabajo/app_visualizaciones/indicadores_markdown.txt' + +# # Cargar datos globalmente para toda la app +# try: +# df_global = cargar_datos(RUTA_DATOS) +DATOS_CARGADOS = True +# print(f"✓ Datos cargados: {len(df_global)} registros") +# except Exception as e: +# df_global = None +# DATOS_CARGADOS = False +# print(f"✗ Error al cargar datos: {e}") + + + +# Estilos CSS personalizados +CUSTOM_CSS = """ +.gradio-container { + font-family: 'Arial', sans-serif; +} +.explanation-box { + background-color: #E8F4F8; + padding: 20px; + border-radius: 10px; + border-left: 5px solid #2E5090; + margin: 10px 0; +} +.stats-box { + background-color: #009EDB; + padding: 15px; + border-radius: 8px; + border: 2px solid #FFD700; + margin: 10px 0; +} +.important-box { + background-color: #FFE6E6; + padding: 15px; + border-radius: 8px; + border-left: 5px solid #C00000; + margin: 10px 0; +} +h1, h2, h3 { + color: #2E5090; +} +.tab-nav button { + font-size: 16px; + padding: 10px 20px; +} + +/* ESTILOS PARA HEADER CON LOGOS INSTITUCIONALES */ +.header-institucional { + display: flex; + justify-content: space-between; + align-items: center; + padding: 20px 40px; + background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 50%, #f8f9fa 100%); + border-bottom: 4px solid #003DA5; + margin-bottom: 25px; + box-shadow: 0 3px 10px rgba(0,0,0,0.08); +} + +.logo-institucional { + height: 40px; + width: auto; + object-fit: contain; +} + +.titulo-institucional { + flex: 1; + text-align: center; + padding: 0 30px; +} + +.titulo-institucional h1 { + margin: 0; + color: #003DA5 !important; + font-size: 28px; + font-weight: 700; +} + +.logo-ods-tbl { + height: 60px; + width: auto; + object-fit: contain; +} + +@media (max-width: 768px) { + .header-institucional { + padding: 15px 20px; + flex-direction: column; + gap: 15px; + } + .logo-institucional { + height: 50px; + } +} +""" + +# ============================================================================ +# FUNCIONES DE CONVERSIÓN DE FIGURAS +# ============================================================================ + +def plotly_to_html(fig): + """Convierte figura Plotly a HTML para mostrar en Gradio""" + return fig.to_html(include_plotlyjs='cdn', full_html=False) + +def matplotlib_to_file(fig, filename): + """Convierte figura Matplotlib a archivo temporal""" + import tempfile + import os + + # Crear directorio temporal si no existe + temp_dir = tempfile.gettempdir() + filepath = os.path.join(temp_dir, filename) + + # Guardar la figura + fig.savefig(filepath, format='png', dpi=150, bbox_inches='tight') + plt.close(fig) + + return filepath + +# ============================================================================ +##### FUNCIONES DE PROCESAMIENTO DE CONSULTA MASIVA +# ============================================================================ + +def calcular_peso_ranking_inverso(rank, nivel): + """ + Peso decrece linealmente: Rank 1 = peso 10, Rank 10 = peso 1 + """ + if nivel == 'ods': + max_rank = 17 + elif nivel == 'meta': + max_rank = 169 + elif nivel == 'indicador': + max_rank = 244 + return max_rank - rank + 1 + +def calcular_corte_pareto(df_iniciativa, nivel, umbral_pareto=0.8, mass = False): + """ + Determina el top_rank usando Pareto (80/20) + + Args: + df_iniciativa: DataFrame de una iniciativa con similaridad + umbral_pareto: % acumulado para corte (default 0.8 = 80%), puede ser string o float + + Returns: + top_rank: Número de elementos que conforman el 80% + """ + # Convertir umbral_pareto a float si viene como string + umbral_pareto = float(umbral_pareto) + if mass: + df_init = df_iniciativa.copy() + # Ordenar por similaridad + df_sorted = df_init.sort_values(f'score', ascending=False).reset_index(drop=True) + # Se aplica calculo exponencial para acentuar y diferenciar los pesos + df_sorted[f'{nivel}_similaridad_cos_exp3'] = df_sorted[f'score'] ** 3 + else: + + # Ordenar por similaridad descendente + df_sorted = df_iniciativa.sort_values(f'{nivel}_similaridad_cos', ascending=False).copy() + df_sorted[f'{nivel}_similaridad_cos_exp3'] = df_sorted[f'{nivel}_similaridad_cos'] ** 3 + + # Normalizar la columna ods_similaridad_cos_exp3 + min_val = df_sorted[f'{nivel}_similaridad_cos_exp3'].min() + max_val = df_sorted[f'{nivel}_similaridad_cos_exp3'].max() + if max_val - min_val > 0: + df_sorted[f'{nivel}_similaridad_cos_exp3_norm'] = (df_sorted[f'{nivel}_similaridad_cos_exp3'] - min_val) / (max_val - min_val) + else: + df_sorted[f'{nivel}_similaridad_cos_exp3_norm'] = 0.0 # Handle case where all values are the same + + # Aplicar Ranking Inverso + df_sorted['peso'] = df_sorted[f'{nivel}_rank'].apply(lambda r: calcular_peso_ranking_inverso(r, nivel)) + df_sorted['similaridad_ponderada'] = df_sorted[f'{nivel}_similaridad_cos_exp3_norm'] * df_sorted['peso'] + + # Calcular suma total + if mass: + total_similaridad = df_sorted[f'score'].sum() + else: + total_similaridad = df_sorted[f'{nivel}_similaridad_cos'].sum() + + total_similaridad_ponderda = df_sorted['similaridad_ponderada'].sum() + + # Calcular acumulado + if mass: + df_sorted['acumulado'] = df_sorted[f'score'].cumsum() + else: + df_sorted['acumulado'] = df_sorted[f'{nivel}_similaridad_cos'].cumsum() + df_sorted['porcentaje_acumulado'] = df_sorted['acumulado'] / total_similaridad + # + df_sorted['acumulado_ponderado'] = df_sorted['similaridad_ponderada'].cumsum() + df_sorted['porcentaje_acumulado_ponderado'] = df_sorted['acumulado_ponderado'] / total_similaridad_ponderda + + # Encontrar punto de corte Pareto + top_rank = (df_sorted['porcentaje_acumulado_ponderado'] <= umbral_pareto).sum() + + # Mínimo 3, máximo 10 + top_rank = max(3, min(top_rank, 245)) + + return top_rank, df_sorted.head(top_rank) + +# Uso por iniciativa +def aplicar_pareto_por_iniciativa(df_ods, nivel, umbral=0.8, mass = False): + """ + Aplica Pareto a cada iniciativa y marca el top + """ + resultados = [] + + for iniciativa_id in df_ods['INICIATIVA_ID'].unique(): + df_init = df_ods[df_ods['INICIATIVA_ID'] == iniciativa_id] + + top_rank, df_top = calcular_corte_pareto(df_init, nivel, umbral, mass) + + # Marcar los que están en el top Pareto + df_top['en_pareto'] = True + df_top['pareto_rank'] = range(1, len(df_top) + 1) + + resultados.append(df_top) + + df_pareto = pd.concat(resultados, ignore_index=True) + + return df_pareto + +def analisis_global_con_pareto(df, #df_metas, df_indicadores, + nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.50, rank_prop=0.15, mass = False): + """ + Análisis global usando Pareto para determinar top_rank por iniciativa + + Flujo: + 1. Por cada iniciativa, calcula su corte Pareto + 2. Solo usa esos ODS "críticos" para el análisis global + 3. Aplica la estrategia de ponderación elegida + """ + # Convertir parámetros a tipos correctos + umbral_pareto = float(umbral_pareto) + + # Validar que DataFrame no esté vacío + if df is None or df.empty: + return gr.update(value=pd.DataFrame()), gr.update(value=pd.DataFrame()) + + try: + # Paso 1: Aplicar Pareto a cada iniciativa + df_pareto = aplicar_pareto_por_iniciativa(df, nivel, umbral_pareto, mass) + + print(f"📊 Análisis con Pareto (umbral {umbral_pareto*100}%):") + print(f" Total registros originales: {len(df)}") + print(f" Registros en zona Pareto: {len(df_pareto)}") + print(f" Reducción: {(1 - len(df_pareto)/len(df))*100:.1f}%") + + # Paso 2: Análisis global solo con elementos Pareto + if metodo == 'ranking_inverso': + df_pareto['peso'] = 11 - df_pareto['pareto_rank'] + df_pareto['score'] = df_pareto[f'{nivel}_similaridad_cos'] * df_pareto['peso'] + resultado = df_pareto.groupby(f'{nivel.upper()}_ID').agg({ + 'score': 'sum', + 'INICIATIVA_ID': 'count' + }).rename(columns={'INICIATIVA_ID': 'frecuencia'}) + + elif metodo == 'exponencial': + df_pareto['peso'] = df_pareto[f'{nivel}_similaridad_cos_norm'] ** 2 + resultado = df_pareto.groupby(f'{nivel.upper()}_ID').agg({ + 'peso': 'sum', + 'INICIATIVA_ID': 'count' + }).rename(columns={'INICIATIVA_ID': 'frecuencia', 'peso': 'score'}) + + elif metodo == 'mixto': + stats = df_pareto.groupby(f'{nivel.upper()}_ID').agg({ + f'{nivel}_similaridad_cos_norm': 'mean', + 'pareto_rank': 'mean', + 'INICIATIVA_ID': 'count' + }) + + # Normalizar componentes + stats['sim_norm'] = stats[f'{nivel}_similaridad_cos_norm'] + stats['rank_norm'] = 1 - (stats['pareto_rank'] - stats['pareto_rank'].min()) / (stats['pareto_rank'].max() - stats['pareto_rank'].min()) + stats['freq_norm'] = stats['INICIATIVA_ID'] / stats['INICIATIVA_ID'].max() + + # Score mixto + stats['score'] = ( + sim_prop * stats['sim_norm'] + + rank_prop * stats['rank_norm'] + + (1 - sim_prop - rank_prop) * stats['freq_norm'] + ) + + resultado = stats[['score', 'INICIATIVA_ID']].reset_index() + resultado = resultado.sort_values('score', ascending=False).reset_index().rename(columns={'INICIATIVA_ID': 'frecuencia', + 'index': 'rank'}) + resultado['rank'] = resultado.index + 1 + + # Crear columna ODS_ID de forma robusta para todos los niveles + if nivel == 'ods': + # Para ODS, el ID ya es el ODS_ID + resultado['ODS_ID'] = resultado['ODS_ID'].astype(str) + elif nivel == 'meta': + # Extraer ODS del META_ID (formato: "1.1", tomar el primer dígito) + def extraer_ods_de_meta(meta_id): + try: + if pd.isna(meta_id): + return None + ods_num = str(meta_id).split('.')[0].strip() + return ods_num if ods_num else None + except: + return None + resultado['ODS_ID'] = resultado['META_ID'].apply(extraer_ods_de_meta) + elif nivel == 'indicador': + # Extraer ODS del INDICADOR_ID (formato: "1.1.1", tomar el primer dígito) + def extraer_ods_de_indicador(ind_id): + try: + if pd.isna(ind_id): + return None + ods_num = str(ind_id).split('.')[0].strip() + return ods_num if ods_num else None + except: + return None + resultado['ODS_ID'] = resultado['INDICADOR_ID'].apply(extraer_ods_de_indicador) + + + return gr.update(value=resultado), gr.update(value=df_pareto) + + except Exception as e: + print(f"Error en analisis_global_con_pareto: {str(e)}") + import traceback + traceback.print_exc() + return gr.update(value=pd.DataFrame()), gr.update(value=pd.DataFrame()) +# ============================================================================ +# FUNCIONES PARA CADA PESTAÑA +# ============================================================================ + +def tab_inicio(df_ods, df_metas, df_indicador): +# def tab_inicio(): + """Pestaña de inicio con resumen general""" + if not DATOS_CARGADOS: + return "⚠️ Error: No se pudieron cargar los datos." + + # Estadísticas básicas + + total_ods = df_ods['ODS_ID'].nunique() + total_metas = df_metas['META_ID'].nunique() + total_indicadores = df_indicador['INDICADOR_ID'].nunique() + sim_media = df_ods['ods_similaridad_cos_normalized'].mean() + sim_max = df_ods['ods_similaridad_cos_normalized'].max() + sim_min = df_ods['ods_similaridad_cos_normalized'].min() + correlacion = df_ods['ods_rank'].corr(df_ods['ods_similaridad_cos_normalized']) + + # Top 4 ODS + top_ods = df_ods.nsmallest(4, 'ods_rank')[['ODS_ID','ods_rank','OBJETIVO','ods_similaridad_cos_normalized']] + top_ods['logo_id'] = top_ods['ODS_ID'].apply(lambda _: f"ods_{_}") + # top_ods = df_ods.groupby('ODS_ID').agg({ + # 'ods_similaridad_cos_normalized': 'mean' + # }).sort_values('ods_similaridad_cos_normalized', ascending=False).head(3)[['ods_similaridad_cos_normalized']] + + # Top ODS referencia + ods_ref = top_ods.ODS_ID + + # Top 3 METAS + + top_metas = pd.DataFrame() + for i in ods_ref: + top_metas_lcl = df_metas[df_metas.ODS_ID == i] + top_metas_lcl = top_metas_lcl.nsmallest(2, 'meta_rank')[['META_ID','meta_rank','META','meta_similaridad_cos_normalized', 'ODS_ID']] + top_metas = pd.concat([top_metas, top_metas_lcl], axis=0) + # top_metas['logo_id'] = top_metas['ODS_ID'].apply(lambda _: f"ods_{_}") + top_metas['logo_id'] = top_metas['META_ID'].apply(lambda _: f"meta_{_.upper()}") + + recomendaciones_tblinput = Path('data/raw/ODS_169_metas_recomendaciones_detalladas.xlsx') + df_recomendaciones = pd.read_excel(recomendaciones_tblinput) + top_metas = top_metas.merge(df_recomendaciones[['Meta_ODS', 'Recomendaciones_territoriales']], left_on='META_ID', right_on='Meta_ODS', how='left') + # top_metas = df_metas.groupby('META_ID').agg({ + # 'meta_similaridad_cos_normalized': 'mean' + # }).sort_values('meta_similaridad_cos_normalized', ascending=False).head(5)[['META_ID','META','meta_similaridad_cos_normalized']] + + # Top 5 indicadores + top_indicador = pd.DataFrame() + for i in ods_ref: + top_indicador_lcl = df_indicador[df_indicador.ODS_ID == i] + top_indicador_lcl = top_indicador_lcl.nsmallest(2, 'indicador_rank')[['INDICADOR_ID', 'indicador_rank', 'INDICADOR', 'indicador_similaridad_cos_normalized', 'ODS_ID']] + top_indicador = pd.concat([top_indicador, top_indicador_lcl], axis=0) + top_indicador['logo_id'] = top_indicador['ODS_ID'].apply(lambda _: f"ods_{_}") + + + html = f""" +
+

+ 📊 Tu texto en clave de ODS +

+

+ El análisis + identifica que tu texto se relaciona principalmente + con estos Objetivos de Desarrollo Sostenible ODS +

+ + + + + +
+

🏆 Top 4 ODS Más Relevantes

+
+ {''.join([f''' +
+ ODS {row['ODS_ID']} + +
''' for _, row in top_ods.iterrows()])} +
+
+ +
+

🎯 Metas Más Relevantes Por ODS Top

+
+ {''.join([f''' +
+ ODS {row['ODS_ID']} +

Meta: {row['META_ID']}

+ + + +
''' for _, row in top_metas.iterrows()])} +
+
+ + + +

Rank: {row['indicador_rank']}

+
''' for _, row in top_indicador.iterrows()])} + + + --> + +
+

📋 Recomendaciones Territoriales por Meta

+ + + + + + + + + + {''.join([f''' + + + + + ''' for _, row in df_metas.iterrows() if not pd.isna(row.get('Recomendaciones_territoriales'))])} + +
ODSMeta IDRecomendaciones Territoriales
+ ODS {row['ODS_ID']} + {row['META_ID']}{row.get('Recomendaciones_territoriales', 'N/A')}
+
+ + + + """ + return html + +def tab_inicio_mass(df_ods=None, df_metas=None, df_indicador=None): +# def tab_inicio(): + """Pestaña de inicio con resumen general""" + # Validación de entrada + if df_ods is None or df_ods.empty: + return "

⚠️ No hay datos ODS disponibles

" + if df_metas is None or df_metas.empty: + return "

⚠️ No hay datos de METAS disponibles

" + + # if not DATOS_CARGADOS: + # return "⚠️ Error: No se pudieron cargar los datos." + + # print('ODS columns:', df_ods.columns) + # print('METAS columns:', df_metas.columns) + # print('INDICADOR columns:', df_indicador.columns) + + # Estadísticas básicas + + # total_ods = df_ods['ODS_ID'].nunique() + # total_metas = df_metas['META_ID'].nunique() + # total_indicadores = df_indicador['INDICADOR_ID'].nunique() + # sim_media = df_ods['score'].mean() + # sim_max = df_ods['score'].max() + # sim_min = df_ods['score'].min() + # correlacion = df_ods['rank'].corr(df_ods['score']) + + # Top 4 ODS + top_ods = df_ods.nsmallest(4, 'rank')[['ODS_ID','rank','score']] + top_ods['logo_id'] = top_ods['ODS_ID'].apply(lambda _: f"ods_{_}") + # top_ods = df_ods.groupby('ODS_ID').agg({ + # 'score': 'mean' + # }).sort_values('score', ascending=False).head(3)[['score']] + + # Top ODS referencia + ods_ref = top_ods.ODS_ID + + # Top 3 METAS + + top_metas = pd.DataFrame() + for i in ods_ref: + top_metas_lcl = df_metas[df_metas['ODS_ID'] == i] + top_metas_lcl = top_metas_lcl.nsmallest(2, 'rank')[['META_ID','rank','score','ODS_ID']] + top_metas = pd.concat([top_metas, top_metas_lcl], axis=0) + # top_metas['logo_id'] = top_metas['ODS_ID'].apply(lambda _: f"ods_{_}") + top_metas['logo_id'] = top_metas['META_ID'].apply(lambda _: f"meta_{_.upper()}") + # top_metas = df_metas.groupby('META_ID').agg({ + # 'meta_similaridad_cos_normalized': 'mean' + # }).sort_values('meta_similaridad_cos_normalized', ascending=False).head(5)[['META_ID','META','meta_similaridad_cos_normalized']] + + recomendaciones_tblinput = Path('data/raw/ODS_169_metas_recomendaciones_detalladas.xlsx') + df_recomendaciones = pd.read_excel(recomendaciones_tblinput) + top_metas = top_metas.merge(df_recomendaciones[['Meta_ODS', 'Recomendaciones_territoriales']], left_on='META_ID', right_on='Meta_ODS', how='left') + + # Top 5 indicadores + top_indicador = pd.DataFrame() + if df_indicador is not None and not df_indicador.empty and 'ODS_ID' in df_indicador.columns: + for i in ods_ref: + top_indicador_lcl = df_indicador[df_indicador['ODS_ID'] == i] + if not top_indicador_lcl.empty: + # Verificar qué columnas existen en el DataFrame + cols_disponibles = [col for col in ['INDICADOR_ID', 'rank', 'score', 'ODS_ID'] if col in top_indicador_lcl.columns] + top_indicador_lcl = top_indicador_lcl.nsmallest(2, 'rank')[cols_disponibles] if 'rank' in cols_disponibles else top_indicador_lcl[cols_disponibles].head(2) + top_indicador = pd.concat([top_indicador, top_indicador_lcl], axis=0) + if not top_indicador.empty: + top_indicador['logo_id'] = top_indicador['ODS_ID'].apply(lambda _: f"ods_{_}") + else: + top_indicador = pd.DataFrame() + + + html = f""" +
+

+ 📊 Tus textos en clave de ODS +

+

+ El análisis + identifica que tus textos se relacionan principalmente + con estos Objetivos de Desarrollo Sostenible ODS +

+ + + + + +
+

🏆 Top 4 ODS Más Relevantes

+
+ {''.join([f''' +
+ ODS {row['ODS_ID']} + +
''' for _, row in top_ods.iterrows()])} +
+
+ +
+

🎯 Metas Más Relevantes Por ODS Top

+
+ {''.join([f''' +
+ ODS {row['ODS_ID']} +

Meta: {row['META_ID']}

+ + +

Rank: {row['rank']}

+
''' for _, row in top_metas.iterrows()])} +
+
+ + +

Rank: {row['rank']}

+
''' for _, row in top_indicador.iterrows()])} + + + --> + +
+

📋 Recomendaciones Territoriales por Meta

+ + + + + + + + + + {''.join([f''' + + + + + ''' for _, row in df_metas.iterrows() if not pd.isna(row.get('Recomendaciones_territoriales'))])} + +
ODSMeta IDRecomendaciones Territoriales
+ ODS {row['ODS_ID']} + {row['META_ID']}{row.get('Recomendaciones_territoriales', 'N/A')}
+
+ + + + """ + return html + +def tab_viz1(df_ods, df_metas, df_indicador): +# def tab_viz1(): + """Visualización 1: Box Plot por ODS""" + if not DATOS_CARGADOS: + return None, "⚠️ Error: No se pudieron cargar los datos." + + fig1 = viz_1_distribucion_por_ods(df_ods, 'ODS_ID', 'ods_similaridad_cos_normalized', 'ODS') + fig2 = viz_1_distribucion_por_ods(df_metas, 'META_ID', 'meta_similaridad_cos_normalized', 'META') + fig3 = viz_1_distribucion_por_ods(df_indicador, 'INDICADOR_ID', 'indicador_similaridad_cos_normalized', 'INDICADOR') + + explicacion = """ + ## 📦 Diagrama de Caja por ODS + + ### ¿Qué muestra? + Esta visualización muestra cómo se distribuyen los valores de similaridad para cada uno de los 17 ODS. + + ### ¿Cómo leerlo? + - **Línea central**: Mediana (valor del medio) + - **Caja**: Rango intercuartílico (Q1 a Q3) + - **Líneas extendidas**: Valores mínimos y máximos normales + - **Puntos fuera**: Valores atípicos (outliers) + + ### Interpretación: + - ✅ **Cajas altas**: Mucha variación entre indicadores del ODS + - ✅ **Cajas pequeñas**: Indicadores consistentes + - ✅ **Mediana alta**: ODS muy relacionado con la iniciativa + - ✅ **Puntos aislados**: Indicadores especialmente relevantes + + ### 💡 Consejo: + Busca ODS con medianas altas y cajas pequeñas para identificar objetivos con indicadores consistentemente relevantes. + """ + + return fig1, fig2, fig3, explicacion + +def tab_viz2(df_global): +# def tab_viz2(): + """Visualización 2: Heatmap ODS × Ranking""" + if not DATOS_CARGADOS: + return None, "⚠️ Error: No se pudieron cargar los datos." + + fig = viz_2_heatmap_ods_ranking(df_global) + filepath = matplotlib_to_file(fig, 'viz2_heatmap.png') + + explicacion = """ + ## 🔥 Mapa de Calor: ODS × Ranking + + ### ¿Qué muestra? + Matriz bidimensional que cruza los 17 ODS (filas) con deciles de ranking (columnas), + mostrando la similaridad promedio en cada celda. + + ### ¿Cómo leerlo? + - 🔴 **Colores cálidos** (rojo/naranja): Alta similaridad + - 🔵 **Colores fríos** (verde/azul): Baja similaridad + - **D1 a D10**: Desde los más relevantes (D1) hasta los menos (D10) + + ### Interpretación: + - ✅ **Fila roja completa**: ODS relevante en todos los rangos + - ✅ **Columna roja**: Varios ODS relevantes en esa posición + - ✅ **Diagonal descendente**: Patrón esperado (a mayor rank, menor similaridad) + - ✅ **Rojo en D1-D2**: Los ODS más críticos + + ### 💡 Consejo: + Identifica rápidamente qué ODS dominan en las posiciones altas del ranking. + """ + + return filepath, explicacion + +def tab_viz3(df_global): +# def tab_viz3(): + """Visualización 3: Scatter 3D Interactivo""" + if not DATOS_CARGADOS: + return None, "⚠️ Error: No se pudieron cargar los datos." + + fig = viz_3_scatter_3d_interactivo(df_global) + + explicacion = """ + ## 🌐 Gráfico 3D Interactivo + + ### ¿Qué muestra? + Visualización tridimensional donde cada punto representa un indicador. + + ### Las tres dimensiones: + - **Eje X**: ODS ID (1-17) + - **Eje Y**: Número de sub-indicador + - **Eje Z**: Similaridad (altura del punto) + - **Tamaño**: Los más grandes = más relevantes + - **Color**: Cada ODS tiene su color + + ### Interactividad: + - 🔄 **Rotar**: Arrastra con el mouse + - 🔍 **Zoom**: Scroll o pinch + - 👆 **Hover**: Pasa el mouse sobre puntos + + ### Interpretación: + - ✅ **Puntos altos**: Alta similaridad + - ✅ **Clusters de color**: Grupo de indicadores relacionados + - ✅ **Puntos grandes y altos**: Los más importantes + + ### 💡 Consejo: + Rota el gráfico para descubrir patrones ocultos y agrupaciones de indicadores. + """ + + return fig, explicacion + +def tab_viz4(df, id_lvl, score, rank, titulo): +# def tab_viz4(): + """Visualización 4: Radar Chart""" + if not DATOS_CARGADOS: + return None, "⚠️ Error: No se pudieron cargar los datos." + + fig = viz_4_radar_chart_ods(df, id_lvl, score, rank, titulo) + + explicacion = """ + ## 🕸️ Gráfico de Radar (Perfil ODS) + + ### ¿Qué muestra? + Gráfico circular que muestra el 'perfil ODS' de tu iniciativa con dos métricas. + + ### Cómo leerlo: + - 🔵 **Polígono azul**: Similaridad promedio por ODS + - 🔴 **Polígono rojo**: Similaridad máxima (mejor indicador) + - **Distancia del centro**: Mayor distancia = mayor similaridad + + ### Interpretación: + - ✅ **Picos hacia afuera**: ODS muy relevantes + - ✅ **Valles hacia dentro**: ODS menos relacionados + - ✅ **Forma circular**: Iniciativa equilibrada + - ✅ **Forma irregular**: Especialización en ODS específicos + - ✅ **Gap azul-rojo grande**: Indicador estrella en ese ODS + + ### 💡 Consejo: + Ideal para presentaciones ejecutivas. Muestra de un vistazo el perfil completo de alineación ODS. + """ + + return fig, explicacion + +def tab_viz5(df, id_lvl, score, rank, titulo): +# def tab_viz5(): + """Visualización 5: Sunburst""" + if not DATOS_CARGADOS: + return None, "⚠️ Error: No se pudieron cargar los datos." + + fig = viz_5_sunburst_jerarquia(df, id_lvl, score, rank, titulo) + + explicacion = """ + ## ☀️ Diagrama de Sol (Sunburst) + + ### ¿Qué muestra? + Diagrama circular jerárquico mostrando ODS (centro) → Indicadores (anillo exterior). + + ### Cómo leerlo: + - **Tamaño del segmento**: Proporcional a la similaridad + - **Color**: Gradiente (más oscuro = mayor similaridad) + - **Nivel 1 (centro)**: Los 17 ODS + - **Nivel 2 (exterior)**: Indicadores individuales + + ### Interactividad: + - 👆 **Click**: Zoom en un ODS específico + - 🔍 **Hover**: Ver código y valor del indicador + + ### Interpretación: + - ✅ **Segmentos grandes**: Indicadores muy relevantes + - ✅ **ODS ocupa mucho espacio**: Muchos indicadores relevantes + - ✅ **Colores oscuros**: Alta similaridad + + ### 💡 Consejo: + Excelente para visualizar la contribución relativa de cada indicador al total. + """ + + return fig, explicacion + +def tab_viz6(df, id_lvl, score, rank, titulo, top_n=3): +# def tab_viz6(): + """Visualización 6: Top Indicadores por ODS""" + if not DATOS_CARGADOS: + return None, "⚠️ Error: No se pudieron cargar los datos." + + fig = viz_6_top_indicadores_por_ods(df, id_lvl, score, rank, titulo, top_n=3) + + explicacion = """ + ## 🏆 Top 5 Indicadores por ODS + + ### ¿Qué muestra? + Barras horizontales con los 5 indicadores más relevantes de cada ODS. + + ### Cómo leerlo: + - **Longitud de barra**: Valor de similaridad + - **Primera barra**: El indicador más relevante + - **Color**: Gradiente por similaridad + - **Cada panel**: Un ODS diferente + + ### Interpretación: + - ✅ **Barra mucho más larga**: Indicador campeón + - ✅ **Barras parejas**: Varios indicadores igualmente relevantes + - ✅ **Comparación entre ODS**: Qué objetivo tiene mejores indicadores + + ### 💡 Consejo: + Perfecta para planificación estratégica. Te dice exactamente en qué indicadores enfocarte por cada ODS. + """ + + return fig, explicacion + +def tab_viz7(df_global): +# def tab_viz7(): + """Visualización 7: Stream Graph""" + if not DATOS_CARGADOS: + return None, "⚠️ Error: No se pudieron cargar los datos." + + fig = viz_7_streamgraph_similaridad(df_global) + + explicacion = """ + ## 🌊 Gráfico de Flujo (Stream Graph) + + ### ¿Qué muestra? + Áreas apiladas que muestran cómo cambia la contribución porcentual de cada ODS + a lo largo del ranking. + + ### Cómo leerlo: + - **Eje horizontal**: Ranking agrupado (izq. = más relevante) + - **Eje vertical**: Porcentaje de contribución (suma 100%) + - **Ancho del color**: Porcentaje del ODS en ese rango + + ### Interpretación: + - ✅ **Color dominante izquierda**: ODS líder en indicadores relevantes + - ✅ **Cambio de color**: Transición de relevancia + - ✅ **Área ancha constante**: ODS presente en todo el ranking + - ✅ **Área que crece/decrece**: ODS relevante en ciertos rangos + + ### 💡 Consejo: + Si un ODS ocupa mucho espacio a la izquierda, domina entre los indicadores más relevantes. + """ + + return fig, explicacion + +def tab_viz8(df_global): +# def tab_viz8(): + """Visualización 8: Violin Plot""" + if not DATOS_CARGADOS: + return None, "⚠️ Error: No se pudieron cargar los datos." + + fig = viz_8_violin_plot_ods(df_global) + + explicacion = """ + ## 🎻 Gráfico de Violín + + ### ¿Qué muestra? + Similar al diagrama de caja pero con más detalle. Muestra la 'forma' completa + de la distribución de similaridad por ODS. + + ### Cómo leerlo: + - **Ancho del violín**: Concentración de valores + - **Caja interior**: Mediana y cuartiles + - **Línea horizontal**: Media (promedio) + + ### Concepto clave: + El ancho representa la **densidad de probabilidad**: donde el violín es más ancho, + es más probable encontrar indicadores con esos valores. + + ### Interpretación: + - ✅ **Violín ancho en un punto**: Muchos indicadores similares + - ✅ **Dos ensanchamientos**: Dos grupos distintos + - ✅ **Violín delgado**: Pocos indicadores en ese rango + - ✅ **Forma simétrica**: Distribución equilibrada + + ### 💡 Consejo: + Detecta distribuciones complejas que el diagrama de caja no puede mostrar. + """ + + return fig, explicacion + +def tab_viz9(df_global): +# def tab_viz9(): + """Visualización 9: Dashboard Integrado""" + if not DATOS_CARGADOS: + return None, "⚠️ Error: No se pudieron cargar los datos." + + fig = viz_9_dashboard_metricas(df_global) + + explicacion = """ + ## 📊 Dashboard Integrado (4 Paneles) + + ### Panel 1 (Superior Izquierdo): Top 10 Indicadores + Barras con los 10 indicadores más relevantes del análisis completo. + + ### Panel 2 (Superior Derecho): Estadísticas por ODS + Tabla con media, desviación estándar, mínimo, máximo y cantidad por ODS. + + ### Panel 3 (Inferior Izquierdo): Histograma Global + Distribución de frecuencias de todos los valores de similaridad. + + ### Panel 4 (Inferior Derecho): Correlación Rank-Similaridad + Scatter plot con línea de tendencia. **CRÍTICO para validación del sistema**. + + ### Validación: + - ✅ **Línea descendente**: Sistema funcionando correctamente + - ✅ **Correlación < -0.7**: Excelente + - ⚠️ **Correlación > -0.4**: Revisar sistema + + ### 💡 Consejo: + Este debe ser tu punto de partida. Vista 360° del análisis completo. + """ + + return fig, explicacion + +def tab_viz10(df_global): +# def tab_viz10(): + """Visualización 10: Matriz de Transición""" + if not DATOS_CARGADOS: + return None, "⚠️ Error: No se pudieron cargar los datos." + + fig = viz_10_matriz_transicion(df_global) + filepath = matplotlib_to_file(fig, 'viz10_matriz_transicion.png') + + explicacion = """ + ## 🔀 Matriz de Transición por Cuartiles + + ### ¿Qué muestra? + Mapa de calor que muestra el porcentaje de cada ODS presente en los 4 cuartiles del ranking. + + ### Cómo leerlo: + - **Filas**: Los 17 ODS + - **Columnas**: Q1 (Top 25%), Q2, Q3, Q4 (Bottom 25%) + - **Valores**: Porcentaje de presencia del ODS + - **Colores**: Naranja/rojo = alta presencia + + ### Interpretación: + - ✅ **Rojo intenso en Q1**: ODS crítico (domina rankings altos) + - ✅ **Colores uniformes**: ODS consistente en todo el ranking + - ✅ **Concentración en un cuartil**: ODS especializado + - ✅ **Claro en Q1, oscuro en Q4**: Más relevante en posiciones bajas + + ### 💡 Consejo: + Analiza la consistencia de relevancia por ODS. Alta presencia en Q1 = crítico para la iniciativa. + """ + + return filepath, explicacion + +def tab_viz18(df, lvl, top): + """Visualización 18: Pivot Table Interactivo""" + + # if not DATOS_RELACIONES_CARGADOS: + # return None, "⚠️ Error: No se pudieron cargar los datos de relaciones." + + # Leer el HTML pre-generado + # html_path = '/mnt/user-data/outputs/pivot_table_ods_interactivo.html' + + # if os.path.exists(html_path): + # with open(html_path, 'r', encoding='utf-8') as f: + # html_content = f.read() + # else: + # Si no existe, generarlo + import tempfile + from pivottablejs import pivot_ui + # from viz_pivot_table import crear_pivot_table_html + import uuid + + # Crear nombre único para evitar conflictos + unique_id = str(uuid.uuid4())[:8] + + # Ruta temporal compatible con HF Spaces y local + if os.path.exists('/tmp'): # Linux/HF Spaces + temp_path = f'/tmp/pivot_table_{lvl}_{str(top).replace(".","")}.html' + else: # Windows local + temp_path = os.path.join(tempfile.gettempdir(), f'pivot_table_{lvl}_{str(top).replace(".","")}.html') + + pivot_ui( + df, + outfile_path=temp_path + ) + + print(f"✓ Pivot Table generado: {temp_path}") + + + with open(temp_path, 'r', encoding='utf-8') as f: + html_content = f.read() + + explicacion = """ + ## 🔄 Tabla Dinámica Interactiva + + Arrastra campos, cambia agregaciones y visualizaciones en tiempo real. + """ + + return (gr.update(value=html_content), gr.update(value=explicacion), gr.update(value=temp_path)) + +def tab_viz19(df_ods_rel, df_metas_rel, df_indicadores_rel): + """Visualización 19: Resumen en Tags""" + # if not DATOS_RELACIONES_CARGADOS: + # return "⚠️ Error: No se pudieron cargar los datos de relaciones.", "" + + html1, html2, html3 = viz_19_resumen_tags(df_ods_rel, df_metas_rel, df_indicadores_rel) + + explicacion = """ + ## 📊 Resumen de Análisis en Tags + + ### ¿Qué muestra? + Métricas clave del análisis presentadas en formato visual de tags/badges. + + ### Métricas incluidas: + - **Iniciativas**: Total de iniciativas analizadas + - **Promedios**: ODS, Metas e Indicadores promedio por iniciativa + - **Más frecuentes**: Elementos que aparecen más veces en el análisis + + ### 💡 Interpretación: + - **Promedios altos**: Iniciativas con enfoque amplio en múltiples ODS + - **Elementos frecuentes**: ODS/Metas/Indicadores prioritarios en el conjunto + """ + + return html1, html2, html3, explicacion + +def tab_viz20(df, nivel, nivel_pareto, mass = False ,iniciativa_id=None): + """Visualización 20: Diagrama de Pareto""" + # if not DATOS_RELACIONES_CARGADOS: + # return None, "" + + # Seleccionar primera iniciativa + # iniciativa_id = df['INICIATIVA_ID'].iloc[0] + + fig, corte_80 = viz_20_pareto_ods(df, nivel, nivel_pareto, mass, iniciativa_id) + + explicacion = f""" + ## 📊 Diagrama de Pareto {nivel.upper()} Global + + ### ¿Qué muestra? + Identifica los **{corte_80} {nivel.upper()} más críticos** que representan el **{nivel_pareto * 100}%** + del valor total de aproximación semántica. + + ### Interpretación: + - **Barras azules**: Aproximación semántica individual de cada {nivel.upper()} + - **Línea roja**: Porcentaje acumulado + - **Zona verde**: Top {corte_80} {nivel.upper()} críticos (regla de Pareto) + - **Línea punteada**: Umbral {nivel_pareto * 100}% + + ### 💡 Uso: + Estos **{corte_80} {nivel.upper()}** deberían ser tu enfoque prioritario, + ya que concentran la mayor parte del valor. + + ### Principio de Pareto: + El 80% de los resultados proviene del 20% de las causas. + En este caso aplicamos un criterio de Pareto ADAPTATIVO: {corte_80} {nivel.upper()} generan el {nivel_pareto * 100}% de la aproximación semántica total. + """ + + return fig, explicacion + +def tab_viz21(df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop=0.75, rank_prop=0.20): + """Visualización 21: Ranking ODS Masivo""" + # Validar entrada + if df is None or df.empty: + return None, "No hay datos para visualizar" + + try: + resultado = df.copy() + + fig = viz_21_ranking_mixto_masivo(resultado, nivel) + + explicacion = f"""## 🏆 Ranking Global de ODS - Análisis Masivo + +### ¿Qué muestra? +Resultado de aplicar **estrategia mixta ponderada** a iniciativas prioritarias. + +### Componentes del Score: +- **50%** Similaridad promedio +- **30%** Ranking promedio (posición) +- **20%** Frecuencia de aparición + +### Interpretación: +- **Barras largas**: ODS con mayor score global +- **Color verde**: Scores más altos +- **Color azul**: Scores intermedios +- **Círculos grandes**: ODS que aparecen en más iniciativas + +### Top 3 ODS: +{chr(10).join([f'- **ODS {int(row["ODS_ID"])}**: Score {row["score"]:.4f} ({int(row["frecuencia"])} iniciativas)' + for _, row in resultado.head(3).iterrows()] if len(resultado) > 0 else ['Sin datos'])} + +### 💡 Uso: +Estos ODS son los **prioritarios a nivel agregado** considerando +relevancia, posición y prevalencia en el lote analizado. +""" + + return fig, explicacion + + except Exception as e: + print(f"Error en tab_viz21: {str(e)}") + import traceback + traceback.print_exc() + return None, f"Error en visualización: {str(e)}" + +def tab_viz22(w_sim, w_rank): + """Visualización 22: Composición Score Mixto""" + + # Pesos de la estrategia mixta + # w_sim = 0.5 + # w_rank = 0.3 + # w_freq = 0.2 + + fig = viz_22_composicion_score_mixto(w_sim, w_rank, 1 - (w_sim + w_rank)) + + explicacion = f""" + ## ⚖️ Composición del Score Mixto + + ### ¿Qué muestra? + La distribución porcentual de los 3 componentes que conforman el score. + + ### Componentes: + + **1. Similaridad ({w_sim*100:.0f}%)** + - Qué mide: Relevancia semántica promedio + - Cálculo: Promedio de similaridad coseno normalizada + - Peso: Mayor componente (calidad del match) + + **2. Ranking ({w_rank*100:.0f}%)** + - Qué mide: Posición promedio en las iniciativas + - Cálculo: Promedio de rankings invertido y normalizado + - Peso: Segundo componente (importancia relativa) + + **3. Frecuencia ({(1 - (w_sim + w_rank))*100:.0f}%)** + - Qué mide: Prevalencia en el lote + - Cálculo: Cantidad de iniciativas donde aparece + - Peso: Menor componente (consistencia) + + ### 📐 Fórmula: + + Score = {w_sim} × Similaridad + {w_rank} × Ranking + {1 - (w_sim + w_rank)} × Frecuencia + + + ### 💡 Justificación de Pesos: + - **Similaridad dominante**: La calidad del match es lo más importante + - **Ranking secundario**: La posición importa, pero menos que la similaridad + - **Frecuencia terciaria**: Aparecer en muchas iniciativas suma, pero no define + + ### 🔧 Personalización: + Estos pesos pueden ajustarse según prioridades: + - ¿Más importancia a frecuencia? → Aumentar w_freq a 0.3-0.4 + - ¿Solo calidad del match? → Aumentar w_sim a 0.6-0.7 + """ + + return fig, explicacion + +def tab_viz24(): + """Visualización 24: Header ConectaODS""" + + # Buscar logo en diferentes ubicaciones posibles + posibles_logos = [ + '/mnt/user-data/uploads/logo_conecta_ods.png', + 'logos/conecta_ods.png', + 'assets/logo.png', + None # Fallback a placeholder + ] + + logo_path = None + for ruta in posibles_logos: + if ruta and os.path.exists(ruta): + logo_path = ruta + break + + html = viz_24_header_conecta_ods(logo_path) + + explicacion = """ + ## 🎨 Header ConectaODS + + ### Componente de Presentación + + Este es el header principal de la aplicación que incluye: + + ✅ **Logo** de ConectaODS (o placeholder si no se carga) + ✅ **Título** con versión + ✅ **Eslogan** distintivo + ✅ **Descripción** de la herramienta + ✅ **Badges** informativos (17 ODS, 169 Metas, 244+ Indicadores) + ✅ **Créditos** institucionales + + ### 💡 Personalización: + + Para usar tu propio logo: + 1. Sube la imagen a `/mnt/user-data/uploads/logo_conecta_ods.png` + 2. O colócala en `logos/conecta_ods.png` + 3. Regenera la visualización + + El componente se adapta automáticamente con o sin logo. + """ + + return html #, explicacion + +def tab_estadisticas(df_global): +# def tab_estadisticas(): + """Pestaña con análisis estadístico detallado""" + if not DATOS_CARGADOS: + return "⚠️ Error: No se pudieron cargar los datos." + + # Estadísticas globales + stats = df_global['similaridad_cos'].describe() + correlacion = df_global['rank'].corr(df_global['similaridad_cos']) + + # Por ODS + stats_ods = df_global.groupby('ods_id')['similaridad_cos'].agg([ + ('count', 'count'), + ('mean', 'mean'), + ('std', 'std'), + ('min', 'min'), + ('max', 'max') + ]).round(4) + + # Top 50 + top_50_ods = df_global.nsmallest(50, 'rank')['ods_id'].value_counts() + + html = f""" +
+

📈 Análisis Estadístico Detallado

+ +
+

1. Estadísticas Globales

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Cantidad de datos:{stats['count']:.0f}
Media:{stats['mean']:.4f}
Desviación Estándar:{stats['std']:.4f}
Mínimo:{stats['min']:.4f}
Q1 (Percentil 25):{stats['25%']:.4f}
Mediana (Q2):{stats['50%']:.4f}
Q3 (Percentil 75):{stats['75%']:.4f}
Máximo:{stats['max']:.4f}
+
+ +
+

2. Validación del Sistema

+

Correlación Rank vs Similaridad: {correlacion:.4f}

+

Interpretación: + { + "✅ Excelente - Sistema de ranking muy confiable" if correlacion < -0.9 else + "✅ Muy bueno - Sistema de ranking confiable" if correlacion < -0.7 else + "⚠️ Aceptable - Sistema funciona pero puede mejorarse" if correlacion < -0.4 else + "❌ Problema - Revisar cálculo de similaridad o ranking" + } +

+

Una correlación negativa fuerte indica que a mayor ranking (menos relevante), menor es la similaridad, lo cual es el comportamiento esperado.

+
+ +
+

3. Estadísticas por ODS

+ + + + + + + + + + + + + {''.join([f''' + + + + + + + ''' for idx, row in stats_ods.iterrows()])} + +
ODSCountMediaStdMinMax
ODS {idx}{int(row['count'])}{row['mean']:.4f}{row['std']:.4f}{row['min']:.4f}{row['max']:.4f}
+
+ +
+

4. ODS Más Representados en Top 50

+ + + + + + + + + + {''.join([f''' + + + + ''' for idx, count in top_50_ods.head(10).items()])} + +
ODSCantidadPorcentaje
ODS {idx}{count}{count/50*100:.1f}%
+
+
+ """ + + return html + +# ============================================================================ +# CONSTRUCCIÓN DE LA APLICACIÓN GRADIO +# ============================================================================ + +def crear_app(): + """Crea y configura la aplicación Gradio completa""" + + with gr.Blocks( + title="Sistema de Visualización ODS", + # theme=gr.themes.Soft( + # primary_hue="indigo", + # secondary_hue="orange", + # neutral_hue="slate" + # ), + theme=gr.themes.Default(), + css=CUSTOM_CSS + ) as app: + + gr.HTML(f""" +
+
+ Gobierno de Colombia +
+
+

+

+
+
+ Fondo Multidonante de las Naciones Unidas +
+
+ """) + + # Encabezado principal + + + with gr.Row(): + with gr.Column(scale=1): + gr.HTML(f""" +
+ Logo conectaODS + +
+ """) + with gr.Column(scale=3): + gr.Markdown(f""" + + # ConectaODS: Tu voz en clave de desarrollo sostenible + ### Explorador Interactivo + + *ConectaODS es una herramienta que convierte + relatos, ideas o iniciativas del territorio en + conexiones claras con los Objetivos de Desarrollo + Sostenible (ODS)* + """) + + + + # Pestañas principales + with gr.Tabs(): + + + def ver_radio(df): + return True + + # PESTAÑA: CONSULTA + with gr.Tab("CONSULTA INDIVIDUAL"): + + txt_ods = gr.Textbox(value="ods", visible=False) + txt_meta = gr.Textbox(value="meta", visible=False) + txt_indicador = gr.Textbox(value="indicador", visible=False) + + with gr.Column(): + gr.Markdown(f""" + + ### Cuéntanos una iniciativa, problema o propuesta de tu territorio. + + *La herramienta analizará el texto y mostrará con qué + Objetivos de Desarrollo Sostenible (ODS) se + relaciona.* + """) + query_in = gr.Textbox(lines=5, placeholder="Escribe aquí tu consulta...", label="Iniciativa a analizar") + query_out = gr.Textbox(lines=5, label="Texto ajustado para lenguaje natural", visible=False) + + btn = gr.Button(value="Analizar mi iniciativa") + msj_procesamiento = gr.Textbox(value="... preparando análisis de iniciativa.", visible=True) + + with gr.Tab("🔍 Resultado"): + + + with gr.Row(): + ods = gr.Dataframe( label="ODS", visible=False)#, buttons=["fullscreen"]) + meta = gr.Dataframe( label="METAS", visible=False)#, buttons=["fullscreen"]) + indicador = gr.Dataframe( label="INDICADORES", visible=False)#, buttons=["fullscreen"]) + + + html_inicio_ods = gr.HTML() #tab_inicio(ods.value) + + # with gr.Row(): + # html_pareto_ods = gr.HTML() #tab_viz20(df_global) + # html_pareto_meta = gr.HTML() + # html_pareto_indicador = gr.HTML() + # PESTAÑA 20: DIAGRAMA DE PARETO + with gr.Tab("📊 Priorización con Pareto", visible=False): + gr.HTML(""" +
+

📊 Diagrama de Pareto (Regla 80/20)

+
    +
  1. CRITERIO: Aplicamos un criterio de Pareto ADAPTATIVO calibrado para contextos de clasificación multinivel, + donde el umbral se ajusta según la distribución empírica de similaridad y la granularidad requerida para toma de decisiones.
  2. +
  3. Marcos globales: (UNDP, VNR, EU SDG Strategy) recomiendan 3-5 ODS + prioritarios por ciclo de implementación.
  4. +
  5. Interactúa: Las visualizaciones son dinámicas y permiten exploración
  6. +
+
+ """) + + with gr.Row(): + with gr.Column(scale=2): + plotOds = gr.Plot(label="Diagrama de Pareto ODS") + with gr.Column(scale=1): + slider_ods_pareto = gr.Slider(0.1, 1.0, step=0.1, value=0.7,interactive=True, label="Top ODS") + expOds = gr.Markdown() + with gr.Row(): + with gr.Column(scale=2): + plotMeta = gr.Plot(label="Diagrama de Pareto Metas") + with gr.Column(scale=1): + slider_meta_pareto = gr.Slider(0.1, 1.0, step=0.1, value=0.7, interactive=True, label="Top Metas") + expMeta = gr.Markdown() + + with gr.Row(): + with gr.Column(scale=2): + plotIndicador = gr.Plot(label="Diagrama de Pareto Indicadores") + with gr.Column(scale=1): + slider_indicador_pareto = gr.Slider(0.1, 1.0, step=0.1, value=0.7, interactive=True, label="Top Indicadores") + expIndicador = gr.Markdown() + + + + # btn0_i = gr.Button("🔄 Traducción a ODS", variant="primary") + ### Actualizando resultados iniciales + ods.change( + fn=tab_inicio, + inputs=[ods,meta,indicador], + outputs=[html_inicio_ods] + ) + meta.change( + fn=tab_inicio, + inputs=[ods,meta,indicador], + outputs=[html_inicio_ods] + ) + indicador.change( + fn=tab_inicio, + inputs=[ods,meta,indicador], + outputs=[html_inicio_ods] + ) + + ### Actualizando análisis de Pareto + ods.change( + fn=tab_viz20, + inputs=[ods, gr.State('ods'), gr.State(0.7), gr.State(False), gr.State('individual')], + outputs=[plotOds, expOds] + ) + meta.change( + fn=tab_viz20, + inputs=[meta, gr.State('meta'), gr.State(0.7), gr.State(False), gr.State('individual')], + outputs=[plotMeta, expMeta] + ) + indicador.change( + fn=tab_viz20, + inputs=[indicador, gr.State('indicador'), gr.State(0.7), gr.State(False), gr.State('individual')], + outputs=[plotIndicador, expIndicador] + ) + ### Actualizando nivel de pareto + + slider_ods_pareto.change( + fn=tab_viz20, + inputs=[ods, gr.State('ods'), slider_ods_pareto, gr.State(False), gr.State('individual')], + outputs=[plotOds, expOds] + ) + slider_meta_pareto.change( + fn=tab_viz20, + inputs=[meta, gr.State('meta'), slider_meta_pareto, gr.State(False), gr.State('individual')], + outputs=[plotMeta, expMeta] + ) + slider_indicador_pareto.change( + fn=tab_viz20, + inputs=[indicador, gr.State('indicador'), slider_indicador_pareto, gr.State(False), gr.State('individual')], + outputs=[plotIndicador, expIndicador] + ) + + with gr.Tab("📋 Tablas Detalladas"): + es_visible = gr.State(value=False) + + with gr.Row(visible=es_visible): + slider_ods = gr.Slider(1, 5, step=1, value=3,interactive=True, label="Top ODS") + slider_meta = gr.Slider(1, 5, step=1, value=3, interactive=True, label="Top Metas") + slider_indicador = gr.Slider(1, 5, step=1, value=3, interactive=True, label="Top Indicadores") + + def len_dfs(df): + return gr.update(maximum=len(df)) + + + + ods.change(fn=len_dfs, + inputs=[ods], + outputs=[slider_ods], api_name="ods") + + meta.change(fn=len_dfs, + inputs=[meta], + outputs=[slider_meta], api_name="ods") + + indicador.change(fn=len_dfs, + inputs=[indicador], + outputs=[slider_indicador], api_name="ods") + + + + with gr.Row(): + df_ods_filtrado = gr.Dataframe(label="ODS Filtrados") + df_meta_filtrado = gr.Dataframe(label="METAS Filtradas") + df_indicador_filtrado = gr.Dataframe(label="INDICADORES Filtrados") + + with gr.Row(): + + + + slider_ods.release(fn=lambda df, n: df[df[f'ods_rank'] <= n][['ods_rank','ODS_ID','OBJETIVO']] if df is not None else None, + inputs=[ods, slider_ods], + outputs=[df_ods_filtrado], api_name="ods") + + slider_meta.release(fn=lambda df, n: df[df[f'meta_rank'] <= n][['meta_rank','META_ID','META']] if df is not None else None, + inputs=[meta, slider_meta], + outputs=[df_meta_filtrado], api_name="ods") + + slider_indicador.release(fn=lambda df, n: df[df[f'indicador_rank'] <= n][['indicador_rank','INDICADOR_ID','INDICADOR']] if df is not None else None, + inputs=[indicador, slider_indicador], + outputs=[df_indicador_filtrado], api_name="ods") + + + + # with gr.Row(): + # genero = gr.Dataframe( label="Enfoque de genero") + # poblacional = gr.Dataframe( label="Enfoque poblacional") + # etnico = gr.Dataframe( label="Enfoque étnico") + + # with gr.Row(): + # pilar = gr.Dataframe( label="Pilares") + # estrategia = gr.Dataframe( label="Estrategias") + # categoria = gr.Dataframe( label="Categorias") + + with gr.Row(visible=False): + bdl_ods = gr.Dataframe(value=pd.DataFrame(), label="BDL_ODS") + + # with gr.Row(): + + # ods_plot = gr.ScatterPlot( + # value=pd.DataFrame(), + # x="ODS_ID", + # y="ods_rank", + # sort="-y", + # label="Gráfico de Barras ODS", + # # color="cuisine", + # # x_bin=1, + # color_map={ + # "1": "#E5243B", # Red - ODS 1: Fin de la Pobreza + # "2": "#DDA63A", # Mustard - ODS 2: Hambre Cero + # "3": "#4C9F38", # Kelly Green - ODS 3: Salud y Bienestar + # "4": "#C5192D", # Dark Red - ODS 4: Educación de Calidad + # "5": "#FF3A21", # Red Orange - ODS 5: Igualdad de Género + # "6": "#26BDE2", # Bright Blue - ODS 6: Agua Limpia y Saneamiento + # "7": "#FCC30B", # Yellow - ODS 7: Energía Asequible y No Contaminante + # "8": "#A21942", # Burgundy Red - ODS 8: Trabajo Decente y Crecimiento Económico + # "9": "#FD6925", # Orange - ODS 9: Industria, Innovación e Infraestructura + # "10": "#DD1367", # Magenta - ODS 10: Reducción de las Desigualdades + # "11": "#FD9D24", # Golden Yellow - ODS 11: Ciudades y Comunidades Sostenibles + # "12": "#BF8B2E", # Dark Mustard - ODS 12: Producción y Consumo Responsables + # "13": "#3F7E44", # Dark Green - ODS 13: Acción por el Clima + # "14": "#0A97D9", # Blue - ODS 14: Vida Submarina + # "15": "#56C02B", # Lime Green - ODS 15: Vida de Ecosistemas Terrestres + # "16": "#00689D", # Royal Blue - ODS 16: Paz, Justicia e Instituciones Sólidas + # "17": "#19486A", # Navy Blue - ODS 17: Alianzas para Lograr los Objetivos + # }, + # ) + + # def actualizar_ods_plot(df, n): + # if df is not None: + # df_filtrado = df[df['ods_rank'] <= n] + # df_filtrado['ODS_ID'] = df_filtrado['ODS_ID'].astype(str) + # # df_filtrado['ods_rank'] = df_filtrado['ods_rank'].astype(int) + # return df_filtrado + # else: + # return pd.DataFrame() + + # slider_ods.release(fn=lambda df, n: (df[df[f'ods_rank'] <= n], actualizar_ods_plot(df, n)) if df is not None else None, + # inputs=[ods, slider_ods], + # outputs=[df_ods_filtrado, ods_plot], api_name="ods") + + # slider_ods.release(fn=lambda df, n: df[df[f'ods_rank'] <= n]['ODS_ID'].astype(str) if df is not None else None, + # inputs=[ods, slider_ods], + # outputs=[df_ods_filtrado, ods_plot], api_name="ods") + + + + # with gr.Tab("🔄 Pivot Table Iniciativa Individual"): + # with gr.Row(): + # with gr.Column(scale=3): + # html18_ods = gr.HTML(label="Pivot Table Interactivo ODS", elem_id="pivot-container") + # html18_ods_f = gr.File(label="📥 Descargar Pivot Table (HTML)") #gr.HTML(label="Pivot Table Interactivo ODS", elem_id="pivot-container") + # html18_meta = gr.HTML(label="Pivot Table Interactivo METAS", elem_id="pivot-container") + # html18_meta_f = gr.File(label="📥 Descargar Pivot Table (HTML)") #gr.HTML(label="Pivot Table Interactivo METAS", elem_id="pivot-container") + # html18_indicador = gr.HTML(label="Pivot Table Interactivo INDICADORES", elem_id="pivot-container") + # html18_indicador_f = gr.File(label="📥 Descargar Pivot Table (HTML)") #gr.HTML(label="Pivot Table Interactivo INDICADORES", elem_id="pivot-container") + # with gr.Column(scale=1): + # exp18 = gr.Markdown() + + # # def actualizar_pivots(df_ods, df_meta, df_indicador, slider_ods, slider_meta, slider_indicador): + # def actualizar_pivots(df_ods, slider_ods): + # # Actualizar ODS + # if df_ods is not None and len(df_ods) > 0: + # html_ods, exp_ods, file_ods = tab_viz18(df_ods, "ods", slider_ods) + # else: + # html_ods = gr.update(value="

No hay datos ODS para mostrar

") + # exp_ods = gr.update(value="## Sin datos ODS") + # file_ods = gr.update(value=None) + + # # # Actualizar METAS + # # if df_meta is not None and len(df_meta) > 0: + # # html_meta, exp_meta, file_meta = tab_viz18(df_meta, "meta", slider_meta) + # # else: + # # html_meta = gr.update(value="

No hay datos METAS para mostrar

") + # # exp_meta = gr.update(value="## Sin datos METAS") + # # file_meta = gr.update(value=None) + + # # # Actualizar INDICADORES + # # if df_indicador is not None and len(df_indicador) > 0: + # # html_indicador, exp_indicador, file_indicador = tab_viz18(df_indicador, "indicador", slider_indicador) + # # else: + # # html_indicador = gr.update(value="

No hay datos INDICADORES para mostrar

") + # # exp_indicador = gr.update(value="## Sin datos INDICADORES") + # # file_indicador = gr.update(value=None) + + # # Crear explicación combinada + # explicacion_combinada = """ + # ## 🔄 Tablas Dinámicas Interactivas + + # **Arrastra campos** desde la lista de la izquierda al área central para analizar datos. + + # **Funcionalidades:** + # - 📊 Cambia entre tabla, gráfico de barras, línea, etc. + # - 🔢 Modifica agregaciones (suma, promedio, cuenta) + # - 🎯 Filtra datos con arrastrar y soltar + # - 📥 Descarga el HTML para uso externo + + # **Nota:** Cada tabla es independiente y se actualiza automáticamente. + # """ + + # # return ( + # # html_ods, html_meta, html_indicador, + # # gr.update(value=explicacion_combinada), + # # file_ods, file_meta, file_indicador + # # ) + # return ( + # html_ods, + # gr.update(value=explicacion_combinada), + # file_ods + # ) + + # # CONEXIÓN ÚNICA (cuando cambien los 3 dataframes) + # # for df_component in [df_ods_filtrado, df_meta_filtrado, df_indicador_filtrado]: + # df_ods_filtrado.change( + # fn=actualizar_pivots, + # inputs=[df_ods_filtrado, slider_ods], + # outputs=[ + # html18_ods, #html18_meta, html18_indicador, + # exp18, + # html18_ods_f, #html18_meta_f, html18_indicador_f + # ], + # show_progress=False + # ) + # df_meta_filtrado.change( + # fn=actualizar_pivots, + # inputs=[df_meta_filtrado, slider_meta], + # outputs=[ + # # html18_ods, html18_meta, html18_indicador, + # html18_meta, + # exp18, + # # html18_ods_f, html18_meta_f, html18_indicador_f + # html18_meta_f + # ], + # show_progress=False + # ) + # df_indicador_filtrado.change( + # fn=actualizar_pivots, + # inputs=[df_indicador_filtrado, slider_indicador], + # outputs=[ + # # html18_ods, html18_meta, html18_indicador, + # html18_indicador, + # exp18, + # # html18_ods_f, html18_meta_f, html18_indicador_f + # html18_indicador_f + # ], + # show_progress=False + # ) + + # btn.click(search, query_in, [query_out,ods,meta,indicador,genero,poblacional,etnico,pilar,estrategia,categoria,bdl_ods]) + btn.click(search, query_in, [query_out,ods,meta,indicador,bdl_ods, msj_procesamiento], show_progress=True) + btn.click(ver_radio, inputs=[], outputs=[es_visible]) + # btn.click(fn=tab_viz18, inputs=[ods, txt_ods], outputs=[html18_ods, exp18, html18_ods_f]) + # btn.click(fn=tab_viz18, inputs=[meta, txt_meta], outputs=[html18_meta, exp18, html18_meta_f]) + # btn.click(fn=tab_viz18, inputs=[indicador, txt_indicador], outputs=[html18_indicador, exp18, html18_indicador_f]) + + + with gr.Tab('CONSULTA ESPECIALIZADA', visible=False): + + # with gr.Tab("CONSULTA"): + + with gr.Column(): + query_in_esp = gr.Textbox(lines=5, placeholder="Escribe aquí tu consulta...", label="Iniciativa a analizar") + query_out_esp = gr.Textbox(lines=5, label="Texto ajustado para lenguaje natural", visible=False) + + btn_esp = gr.Button(value="Analizar mi iniciativa") + + + # lvl = gr.Dropdown([col for col in bdl_ods_esp.value.columns if 'ID' in col], label='Nivel de análisis') + # score = gr.Dropdown([col for col in bdl_ods_esp.value.columns if 'similaridad' in col], label='Score de medida') + # rank = gr.Dropdown([col for col in bdl_ods_esp.value.columns if 'rank' in col], label='Score de medida') + + with gr.Tab("Clasificaciones"): + with gr.Row(): + ods_esp = gr.Dataframe(value=pd.DataFrame(), label="ODS") + meta_esp = gr.Dataframe(value=pd.DataFrame(), label="METAS") + indicador_esp = gr.Dataframe(value=pd.DataFrame(), label="INDICADORES") + + with gr.Row(): + genero_esp = gr.Dataframe(value=pd.DataFrame(), label="Enfoque de genero") + poblacional_esp = gr.Dataframe(value=pd.DataFrame(), label="Enfoque poblacional") + etnico_esp = gr.Dataframe(value=pd.DataFrame(), label="Enfoque étnico") + + with gr.Row(): + pilar_esp = gr.Dataframe(value=pd.DataFrame(), label="Pilares") + estrategia_esp = gr.Dataframe(value=pd.DataFrame(), label="Estrategias") + categoria_esp = gr.Dataframe(value=pd.DataFrame(), label="Categorias") + + with gr.Row(): + bdl_ods_esp = gr.Dataframe(value=pd.DataFrame(), label="ODS") + + + # PESTAÑA: INICIO + with gr.Tab("🏠 Inicio"): + html_inicio_ods = gr.HTML() #tab_inicio(ods.value) + + + btn0 = gr.Button("🔄 Generar Metricas Iniciales", variant="primary") + btn0.click( + fn=tab_inicio, + inputs=[ods_esp,meta_esp,indicador_esp], + outputs=[html_inicio_ods] + ) + + + # PESTAÑA 1: BOX PLOT + with gr.Tab("📦 1. Box Plot"): + btn1 = gr.Button("🔄 Generar Visualización", variant="primary") + with gr.Row(): + with gr.Column(scale=1): + exp1 = gr.Markdown() + with gr.Row(visible=False): + with gr.Column(scale=2): + plot1_1 = gr.Plot(label="Diagrama de Caja por ODS") + + with gr.Row(): + with gr.Column(scale=2): + plot1_2 = gr.Plot(label="Diagrama de Caja por META") + + with gr.Row(): + with gr.Column(scale=2): + plot1_3 = gr.Plot(label="Diagrama de Caja por INDICADOR") + + + + btn1.click( + fn=tab_viz1, + inputs=[ods_esp, meta_esp, indicador_esp], + outputs=[plot1_1, plot1_2, plot1_3, exp1] + ) + + # PESTAÑA 2: HEATMAP + with gr.Tab("🔥 2. Heatmap"): + with gr.Row(): + with gr.Column(scale=2): + img2 = gr.Image(label="Mapa de Calor ODS × Ranking") + with gr.Column(scale=1): + exp2 = gr.Markdown() + + btn2 = gr.Button("🔄 Generar Visualización", variant="primary") + btn2.click( + fn=tab_viz2, + inputs=[ods_esp], + outputs=[img2, exp2] + ) + + # PESTAÑA 3: SCATTER 3D + with gr.Tab("🌐 3. Scatter 3D"): + with gr.Row(): + with gr.Column(scale=2): + plot3 = gr.Plot(label="Gráfico 3D Interactivo") + with gr.Column(scale=1): + exp3 = gr.Markdown() + + btn3 = gr.Button("🔄 Generar Visualización", variant="primary") + btn3.click( + fn=tab_viz3, + inputs=[ods_esp], + outputs=[plot3, exp3] + ) + + # PESTAÑA 4: RADAR + with gr.Tab("🕸️ 4. Radar Chart"): + with gr.Row(): + with gr.Column(scale=2): + plot4 = gr.Plot(label="Gráfico de Radar") + with gr.Column(scale=1): + exp4 = gr.Markdown() + + btn4 = gr.Button("🔄 Generar Visualización", variant="primary") + btn4.click( + fn=tab_viz4, + inputs=[ods_esp], + outputs=[plot4, exp4] + ) + + # PESTAÑA 5: SUNBURST + with gr.Tab("☀️ 5. Sunburst"): + with gr.Row(): + with gr.Column(scale=2): + plot5 = gr.Plot(label="Diagrama de Sol") + with gr.Column(scale=1): + exp5 = gr.Markdown() + + btn5 = gr.Button("🔄 Generar Visualización", variant="primary") + btn5.click( + fn=tab_viz5, + inputs=[ods_esp], + outputs=[plot5, exp5] + ) + + # PESTAÑA 6: TOP INDICADORES + # with gr.Tab("🏆 6. Top Indicadores"): + # with gr.Row(): + # with gr.Column(scale=2): + # plot6 = gr.Plot(label="Top 5 Indicadores por ODS") + # with gr.Column(scale=1): + # exp6 = gr.Markdown() + + # btn6 = gr.Button("🔄 Generar Visualización", variant="primary") + # btn6.click( + # fn=tab_viz6, + # inputs=[df, id_lvl, score, rank, titulo, top_n=3], + # outputs=[plot6, exp6] + # ) + + # PESTAÑA 7: STREAM GRAPH + with gr.Tab("🌊 7. Stream Graph"): + with gr.Row(): + with gr.Column(scale=2): + plot7 = gr.Plot(label="Gráfico de Flujo") + with gr.Column(scale=1): + exp7 = gr.Markdown() + + btn7 = gr.Button("🔄 Generar Visualización", variant="primary") + btn7.click( + fn=tab_viz7, + inputs=[ods_esp], + outputs=[plot7, exp7] + ) + + # PESTAÑA 8: VIOLIN PLOT + with gr.Tab("🎻 8. Violin Plot"): + with gr.Row(): + with gr.Column(scale=2): + plot8 = gr.Plot(label="Gráfico de Violín") + with gr.Column(scale=1): + exp8 = gr.Markdown() + + btn8 = gr.Button("🔄 Generar Visualización", variant="primary") + btn8.click( + fn=tab_viz8, + inputs=[ods_esp], + outputs=[plot8, exp8] + ) + + # PESTAÑA 9: DASHBOARD + with gr.Tab("📊 9. Dashboard"): + with gr.Row(): + with gr.Column(scale=2): + plot9 = gr.Plot(label="Dashboard Integrado") + with gr.Column(scale=1): + exp9 = gr.Markdown() + + btn9 = gr.Button("🔄 Generar Visualización", variant="primary") + btn9.click( + fn=tab_viz9, + inputs=[ods_esp], + outputs=[plot9, exp9] + ) + + # PESTAÑA 10: MATRIZ TRANSICIÓN + with gr.Tab("🔀 10. Matriz Transición"): + with gr.Row(): + with gr.Column(scale=2): + img10 = gr.Image(label="Matriz de Transición") + with gr.Column(scale=1): + exp10 = gr.Markdown() + + btn10 = gr.Button("🔄 Generar Visualización", variant="primary") + btn10.click( + fn=tab_viz10, + inputs=[ods_esp], + outputs=[img10, exp10] + ) + + # PESTAÑA: ESTADÍSTICAS + with gr.Tab("📈 Estadísticas"): + html_stats = gr.HTML() #tab_estadisticas(ods) + + btn11 = gr.Button("🔄 Generar Estadísticas", variant="primary") + btn11.click( + fn=tab_estadisticas, + inputs=[ods_esp], + outputs=[html_stats] + ) + + btn_esp.click(search, query_in_esp, [query_out_esp,ods_esp,meta_esp,indicador_esp,genero_esp,poblacional_esp,etnico_esp,pilar_esp,estrategia_esp,categoria_esp,bdl_ods_esp]) + + with gr.Tab("CONSULTA MASIVA"): + + client = InferenceClient() + + gr.Markdown("Aquí puedes cargar un archivo con múltiples iniciativas para análisis masivo, con enfoque de contexto o embeddings. (Usa la plantilla proporcionada)") + + def procesar_archivo(file): + if file is None: + return pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), [], 'Error al procesar el archivo: no se ha cargado ningún archivo' + try: + return search_mass(file, 3,3,3) + except Exception as e: + print("Error al leer el archivo:", e) + return pd.DataFrame(), pd.DataFrame(), pd.DataFrame(), [], 'Error al procesar el archivo' + + def upload_file(filepath): + name = Path(filepath).name + return gr.update(label=f"Procesar {name}", value=filepath, visible=True) + + def pre_visual_df(file): + if file is None: + return gr.update(value=None) + try: + df = pd.read_excel(file) + return gr.update(value=df.head(10)) + except Exception as e: + print("Error al leer el archivo:", e) + return gr.update(value=None) + + + + with gr.Row(): + # d = gr.DownloadButton(label=f"Descargar plantilla_iniciativas.xlsx", value=Path('data/plantillas/plantilla_iniciativas.xlsx'), visible=True) + d = gr.DownloadButton(label=f"Descargar plantilla_iniciativas.xlsx", value=Path('data/raw/plantilla_iniciativas.xlsx'), visible=True) + + with gr.Column(): + file_output = gr.File() + u = gr.UploadButton("Upload a file", file_count="single", visible=False) + pre_df = gr.Dataframe(label="Vista previa de las primeras 10 filas del archivo cargado") + # btn_prev_df = gr.Button("Previsualizar archivo") + + u.upload(upload_file, u, file_output) + file_output.change(pre_visual_df, file_output, pre_df) + # btn_prev_df.click(pre_visual_df, u, pre_df) + + # PESTAÑA: CONSULTA + with gr.Tab("MASIVA CON CONTEXTO"): + # DataFrame vacío con estructura de iniciativas + df_iniciativas_template = pd.DataFrame(columns=['id_unico', 'iniciativa', 'ODS_ID', 'META_ID', 'INDICADOR_ID', 'objetivo', 'ods_rank']) + + + with gr.Column(): + btn_mass_emb = gr.Button("Procesar archivo") + msj_procesamiento_mass = gr.Textbox(value="... preparando para procesar documento", interactive=False) + + # PESTAÑA 19: RESUMEN EN TAGS + # with gr.Tab("🏷️ 19. Resumen Tags"): + with gr.Row(): + with gr.Row(): + with gr.Column(): + html19_1 = gr.HTML(label="Resumen en Tags") + with gr.Row(visible=False): + html19_2 = gr.HTML(label="Resumen en Tags") + # with gr.Column(scale=4): + html19_3 = gr.HTML(label="Resumen en Tags") + with gr.Column(visible=False): + exp19 = gr.Markdown() + + + with gr.Row(): + with gr.Column(scale=3): + with gr.Tab("Enfoque TOP ODS"): + html_inicio_mass = gr.HTML(label="Resultados del análisis masivo - vista inicial") + with gr.Tab("Enfoque independiente por ODS - Metas - Indicadores"): + base_ods_mass_mix = gr.Dataframe(label="Prueba-Resultados del análisis masivo ODS", visible=False) + base_meta_mass_mix = gr.Dataframe(label="Resultados del análisis masivo METAS", visible=False) + base_indicadores_mass_mix = gr.Dataframe(label="Resultados del análisis masivo INDICADORES", visible=False) + + base_ods_mass_mix_cat = gr.Dataframe(label="Prueba-Resultados del análisis masivo ODS", visible=False) + base_meta_mass_mix_cat = gr.Dataframe(label="Resultados del análisis masivo METAS", visible=False) + base_indicadores_mass_mix_cat = gr.Dataframe(label="Resultados del análisis masivo INDICADORES", visible=False) + + with gr.Row(): + with gr.Tab("🏆 Ranking Masivo ODS"): + with gr.Row(): + with gr.Column(scale=2): + plot_mass_ods = gr.Plot(label="Ranking Global ODS") + with gr.Column(scale=1): + exp_mass_ods = gr.Markdown() + with gr.Tab("📊 Priorización con Pareto"): + gr.HTML(""" +
+

📊 Diagrama de Pareto (Regla 80/20)

+
    +
  1. CRITERIO: Aplicamos un criterio de Pareto ADAPTATIVO calibrado para contextos de clasificación multinivel, + donde el umbral se ajusta según la distribución empírica de similaridad y la granularidad requerida para toma de decisiones.
  2. +
  3. Marcos globales: (UNDP, VNR, EU SDG Strategy) recomiendan 3-5 ODS + prioritarios por ciclo de implementación.
  4. +
  5. Interactúa: Las visualizaciones son dinámicas y permiten exploración
  6. +
+
+ """) + + with gr.Row(): + with gr.Column(scale=2): + plotOds_mass = gr.Plot(label="Diagrama de Pareto Agregado por ODS") + with gr.Column(scale=1): + slider_ods_pareto_mass = gr.Slider(0.1, 1.0, step=0.1, value=0.7,interactive=True, label="Top ODS") + expOds_mass = gr.Markdown() + with gr.Tab('Tabla score ODS'): + output_ods_mass_mix = gr.Dataframe(label="Resultados del análisis masivo ODS") + with gr.Row(): + with gr.Tab("🏆 Ranking Masivo METAS"): + with gr.Row(): + with gr.Column(scale=2): + plot_mass_metas = gr.Plot(label="Ranking Global METAS") + with gr.Column(scale=1): + exp_mass_metas = gr.Markdown() + with gr.Tab("📊 Priorización con Pareto", visible=False): + gr.HTML(""" +
+

📊 Diagrama de Pareto (Regla 80/20)

+
    +
  1. CRITERIO: Aplicamos un criterio de Pareto ADAPTATIVO calibrado para contextos de clasificación multinivel, + donde el umbral se ajusta según la distribución empírica de similaridad y la granularidad requerida para toma de decisiones.
  2. +
  3. Marcos globales: (UNDP, VNR, EU SDG Strategy) recomiendan 3-5 ODS + prioritarios por ciclo de implementación.
  4. +
  5. Interactúa: Las visualizaciones son dinámicas y permiten exploración
  6. +
+
+ """) + + with gr.Row(): + with gr.Column(scale=2): + plotMetas_mass = gr.Plot(label="Diagrama de Pareto Agregado por METAS") + with gr.Column(scale=1): + slider_metas_pareto_mass = gr.Slider(0.1, 1.0, step=0.1, value=0.7,interactive=True, label="Top METAS") + expMetas_mass = gr.Markdown() + with gr.Tab('Tabla score METAS'): + output_meta_mass_mix = gr.Dataframe(label="Resultados del análisis masivo METAS") + with gr.Row(): + with gr.Tab("🏆 Ranking Masivo INDICADORES"): + with gr.Row(): + with gr.Column(scale=2): + plot_mass_indicadores = gr.Plot(label="Ranking Global INDICADORES") + with gr.Column(scale=1): + exp_mass_indicadores = gr.Markdown() + with gr.Tab("📊 Priorización con Pareto"): + gr.HTML(""" +
+

📊 Diagrama de Pareto (Regla 80/20)

+
    +
  1. CRITERIO: Aplicamos un criterio de Pareto ADAPTATIVO calibrado para contextos de clasificación multinivel, + donde el umbral se ajusta según la distribución empírica de similaridad y la granularidad requerida para toma de decisiones.
  2. +
  3. Marcos globales: (UNDP, VNR, EU SDG Strategy) recomiendan 3-5 ODS + prioritarios por ciclo de implementación.
  4. +
  5. Interactúa: Las visualizaciones son dinámicas y permiten exploración
  6. +
+
+ """) + + with gr.Row(): + with gr.Column(scale=2): + plotIndicadores_mass = gr.Plot(label="Diagrama de Pareto Agregado por INDICADORES") + with gr.Column(scale=1): + slider_indicadores_pareto_mass = gr.Slider(0.1, 1.0, step=0.1, value=0.7,interactive=True, label="Top INDICADORES") + expIndicadores_mass = gr.Markdown() + with gr.Tab('Tabla score INDICADORES'): + output_indicadores_mass_mix = gr.Dataframe(label="Resultados del análisis masivo INDICADORES") + + + + + + + with gr.Column(scale=1, visible=False): + # u.upload(fn=upload_file, inputs=[u], outputs=[file_output]) + # categorias = gr.State(value=[]) + def update_choices(listado): + return gr.update(choices=listado) + + with gr.Row(): + + categorias_mass_state = gr.State(value=[]) # Estado para almacenar categorías disponibles + categorias_mass = gr.Dropdown( + choices=categorias_mass_state.value, multiselect=False, label="Categorías"#, info="Selecciona una categoria para aplicar el filtro." + ) + categorias_mass_state.change(fn=update_choices, inputs=[categorias_mass_state], outputs=[categorias_mass]) + + def listar_elementos_categoria(file, categoria): + if file is None: # or categoria not in file.columns: + return "No hay datos disponibles" + df = pd.read_excel(file) + if categoria not in df.columns: + return f"No se encontró la categoría '{categoria}' en el archivo" + elementos = df[categoria].dropna().unique().tolist() + # return ", ".join(elementos) + return gr.update(choices=elementos) + + # filtro_categoria_state = gr.State(value=[]) # Estado para almacenar categorías seleccionadas para filtro + filtro_categoria= gr.Dropdown( + choices=[], multiselect=True, interactive=True, label="Elementos de categoría", info="Selecciona una o varias opciones para aplicar el análisis." + ) + + bttn_filtro_categoria = gr.Button("Aplicar filtro por categoría", variant="secondary") + + categorias_mass.change(fn=listar_elementos_categoria, inputs=[file_output, categorias_mass], outputs=[filtro_categoria]) + + + + + with gr.Row(): + output_ods_mass = gr.Dataframe(label="Resultados del análisis masivo ODS", visible=False) + output_meta_mass = gr.Dataframe(label="Resultados del análisis masivo Metas", visible=False) + output_indicadores_mass = gr.Dataframe(label="Resultados del análisis masivo Indicadores", visible=False) + + ## Opteniendo + + with gr.Row(): + with gr.Column(): + slider_pareto_ods = gr.Slider(0, 1, step=0.05, value=0.40,interactive=True, label="Umbral Pareto ODS / iniciativa") + slider_pareto_meta = gr.Slider(0, 1, step=0.05, value=0.10,interactive=True, label="Umbral Pareto Metas / iniciativa") + slider_pareto_indicador = gr.Slider(0, 1, step=0.05, value=0.10,interactive=True, label="Umbral Pareto Indicadores / iniciativa") + with gr.Column(): + slider_prop_sim = gr.Slider(0, 1, step=0.05, value=0.50,interactive=True, label="Proporción calificación Similaridad") + slider_prop_rank = gr.Slider(0, 1, step=0.05, value=0.35,interactive=True, label="Proporción calificación Ranking") + # prop_frec = gr.State(value=0.20) # 1 - sim - rank + ## Actualización dinámica de proporciones para que siempre sumen 1 + slider_prop_sim.change(fn=lambda sim: gr.update(value=round(0, 2)) if sim is not None else None, inputs=[slider_prop_sim], outputs=[slider_prop_rank]) + slider_prop_sim.change(fn=lambda sim: gr.update(maximum=round(1-sim, 2), value=round(0, 2)) if sim is not None else None, inputs=[slider_prop_sim], outputs=[slider_prop_rank]) + # slider_prop_rank.change(fn=lambda sim, rank: gr.update(value=1-(rank + sim)) if rank is not None else None, inputs=[slider_prop_sim, slider_prop_rank], outputs=[prop_frec]) + + + with gr.Row(): + # with gr.Column(scale=2): + with gr.Tab("Composición del Score"): + plot22 = gr.Plot(label="Composición del Score") + # with gr.Column(scale=1): + with gr.Tab("Detalle de scores"): + exp22 = gr.Markdown() + + # btn22 = gr.Button("🔄 Mostrar Composición", variant="primary") + slider_prop_sim.change( + fn=tab_viz22, + inputs=[slider_prop_sim, slider_prop_rank], + outputs=[plot22, exp22] + ) + slider_prop_rank.change( + fn=tab_viz22, + inputs=[slider_prop_sim, slider_prop_rank], + outputs=[plot22, exp22] + ) + + btn_mass_emb.click( + fn=procesar_archivo, + inputs=[file_output], + outputs=[output_ods_mass, output_meta_mass, output_indicadores_mass,categorias_mass_state, msj_procesamiento_mass], + show_progress=True + ) + + + with gr.Row(): ## Actualizando todas las variables de análisis cada vez que se ajusta un slider o se carga un nuevo archivo, para mantener todo sincronizado + + with gr.Column(): + output_ods_mass.change( + fn=analisis_global_con_pareto, + inputs=[output_ods_mass, gr.State('ods'),slider_pareto_ods, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], + outputs=[output_ods_mass_mix, base_ods_mass_mix] + ) + output_meta_mass.change( + fn=analisis_global_con_pareto, + inputs=[output_meta_mass, gr.State('meta'), slider_pareto_meta, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], + outputs=[output_meta_mass_mix, base_meta_mass_mix] + ) + output_indicadores_mass.change( + fn=analisis_global_con_pareto, + inputs=[output_indicadores_mass, gr.State('indicador'), slider_pareto_indicador, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], + outputs=[output_indicadores_mass_mix, base_indicadores_mass_mix] + ) + + with gr.Column(): + output_ods_mass_mix.change( + fn=tab_inicio_mass, + inputs=[output_ods_mass_mix,output_meta_mass_mix,output_indicadores_mass_mix], + outputs=[html_inicio_mass] + ) + output_meta_mass_mix.change( + fn=tab_inicio_mass, + inputs=[output_ods_mass_mix,output_meta_mass_mix,output_indicadores_mass_mix], + outputs=[html_inicio_mass] + ) + output_indicadores_mass_mix.change( + fn=tab_inicio_mass, + inputs=[output_ods_mass_mix,output_meta_mass_mix,output_indicadores_mass_mix], + outputs=[html_inicio_mass] + ) + + with gr.Column(): + + slider_prop_sim.change( + fn=analisis_global_con_pareto, + inputs=[output_ods_mass, gr.State('ods'), slider_pareto_ods, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], + outputs=[output_ods_mass_mix, base_ods_mass_mix] + ) + slider_prop_sim.change( + fn=analisis_global_con_pareto, + inputs=[output_meta_mass, gr.State('meta'), slider_pareto_meta, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], + outputs=[output_meta_mass_mix, base_meta_mass_mix] + ) + slider_prop_sim.change( + fn=analisis_global_con_pareto, + inputs=[output_indicadores_mass, gr.State('indicador'), slider_pareto_indicador, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], + outputs=[output_indicadores_mass_mix, base_indicadores_mass_mix ] + ) + + slider_prop_rank.change( + fn=analisis_global_con_pareto, + inputs=[output_ods_mass, gr.State('ods'), slider_pareto_ods, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], + outputs=[output_ods_mass_mix, base_ods_mass_mix] + ) + slider_prop_rank.change( + fn=analisis_global_con_pareto, + inputs=[output_meta_mass, gr.State('meta'), slider_pareto_meta, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], + outputs=[output_meta_mass_mix, base_meta_mass_mix ] + ) + slider_prop_rank.change( + fn=analisis_global_con_pareto, + inputs=[output_indicadores_mass, gr.State('indicador'), slider_pareto_indicador, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], + outputs=[output_indicadores_mass_mix, base_indicadores_mass_mix ] + ) + + with gr.Column(): + slider_pareto_ods.change( + fn=analisis_global_con_pareto, + inputs=[output_ods_mass, gr.State('ods'), slider_pareto_ods, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], + outputs=[output_ods_mass_mix, base_ods_mass_mix] + ) + slider_pareto_meta.change( + fn=analisis_global_con_pareto, + inputs=[output_meta_mass, gr.State('meta'), slider_pareto_meta, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], + outputs=[output_meta_mass_mix, base_meta_mass_mix ] + ) + slider_pareto_indicador.change( + fn=analisis_global_con_pareto, + inputs=[output_indicadores_mass, gr.State('indicador'), slider_pareto_indicador, gr.State('mixto'), slider_prop_sim, slider_prop_rank, gr.State(False)], + outputs=[output_indicadores_mass_mix, base_indicadores_mass_mix] + ) + + with gr.Column(): + output_ods_mass_mix.change( + fn=tab_viz21, + #df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.75, rank_prop=0.20 + inputs=[output_ods_mass_mix, gr.State('ods'), slider_pareto_ods, gr.State(value='mixto'), slider_prop_sim, slider_prop_rank], + outputs=[plot_mass_ods, exp_mass_ods] + ) + + output_meta_mass_mix.change( + fn=tab_viz21, + #df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.75, rank_prop=0.20 + inputs=[output_meta_mass_mix, gr.State('meta'), slider_pareto_meta, gr.State(value='mixto'), slider_prop_sim, slider_prop_rank], + outputs=[plot_mass_metas, exp_mass_metas] + ) + + output_indicadores_mass_mix.change( + fn=tab_viz21, + #df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.75, rank_prop=0.20 + inputs=[output_indicadores_mass_mix, gr.State('indicador'), slider_pareto_indicador, gr.State(value='mixto'), slider_prop_sim, slider_prop_rank], + outputs=[plot_mass_indicadores, exp_mass_indicadores] + ) + + with gr.Column(): # actualizar grafico barras de puntajes cada vez que se ajusta proporción de similaridad o ranking, para mantener todo sincronizado + + slider_prop_sim.change( + fn=tab_viz21, + #df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.75, rank_prop=0.20 + inputs=[output_ods_mass_mix, gr.State('ods'), slider_pareto_meta, gr.State(value='mixto'), slider_prop_sim, slider_prop_rank], + outputs=[plot_mass_metas, exp_mass_metas] + ) + + slider_prop_rank.change( + fn=tab_viz21, + #df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.75, rank_prop=0.20 + inputs=[output_ods_mass_mix, gr.State('ods'), slider_pareto_indicador, gr.State(value='mixto'), slider_prop_sim, slider_prop_rank], + outputs=[plot_mass_indicadores, exp_mass_indicadores] + ) + + slider_prop_sim.change( + fn=tab_viz21, + #df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.75, rank_prop=0.20 + inputs=[output_meta_mass_mix, gr.State('meta'), slider_pareto_meta, gr.State(value='mixto'), slider_prop_sim, slider_prop_rank], + outputs=[plot_mass_metas, exp_mass_metas] + ) + + slider_prop_rank.change( + fn=tab_viz21, + #df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.75, rank_prop=0.20 + inputs=[output_meta_mass_mix, gr.State('meta'), slider_pareto_indicador, gr.State(value='mixto'), slider_prop_sim, slider_prop_rank], + outputs=[plot_mass_indicadores, exp_mass_indicadores] + ) + + slider_prop_sim.change( + fn=tab_viz21, + #df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.75, rank_prop=0.20 + inputs=[output_indicadores_mass_mix, gr.State('indicador'), slider_pareto_meta, gr.State(value='mixto'), slider_prop_sim, slider_prop_rank], + outputs=[plot_mass_metas, exp_mass_metas] + ) + + slider_prop_rank.change( + fn=tab_viz21, + #df, nivel, umbral_pareto=0.8, metodo='mixto', sim_prop = 0.75, rank_prop=0.20 + inputs=[output_indicadores_mass_mix, gr.State('indicador'), slider_pareto_indicador, gr.State(value='mixto'), slider_prop_sim, slider_prop_rank], + outputs=[plot_mass_indicadores, exp_mass_indicadores] + ) + + with gr.Column(): + + output_ods_mass_mix.change( + fn=tab_viz20, + inputs=[output_ods_mass_mix, gr.State('ods'), slider_ods_pareto_mass, gr.State(True), gr.State('individual')], + outputs=[plotOds_mass, expOds_mass] + ) + output_meta_mass_mix.change( + fn=tab_viz20, + inputs=[output_meta_mass_mix, gr.State('meta'), slider_metas_pareto_mass, gr.State(True), gr.State('individual')], + outputs=[plotMetas_mass, expMetas_mass] + ) + output_indicadores_mass_mix.change( + fn=tab_viz20, + inputs=[output_indicadores_mass_mix, gr.State('indicador'), slider_indicadores_pareto_mass, gr.State(True), gr.State('individual')], + outputs=[plotIndicadores_mass, expIndicadores_mass] + ) + + ### Actualizando umbral de pareto + + + slider_ods_pareto_mass.change( + fn=tab_viz20, + inputs=[output_ods_mass_mix, gr.State('ods'), slider_ods_pareto_mass, gr.State(True), gr.State('individual')], + outputs=[plotOds_mass, expOds_mass] + ) + slider_metas_pareto_mass.change( + fn=tab_viz20, + inputs=[output_meta_mass_mix, gr.State('meta'), slider_metas_pareto_mass, gr.State(True), gr.State('individual')], + outputs=[plotMetas_mass, expMetas_mass] + ) + slider_indicadores_pareto_mass.change( + fn=tab_viz20, + inputs=[output_indicadores_mass_mix, gr.State('indicador'), slider_indicadores_pareto_mass, gr.State(True), gr.State('individual')], + outputs=[plotIndicadores_mass, expIndicadores_mass] + ) + + with gr.Column(): + # btn19 = gr.Button("🔄 Generar Resumen", variant="primary") + base_ods_mass_mix.change( + fn=tab_viz19, + inputs=[base_ods_mass_mix, base_meta_mass_mix, base_indicadores_mass_mix], + outputs=[html19_1, html19_2, html19_3, exp19] + ) + + base_meta_mass_mix.change( + fn=tab_viz19, + inputs=[base_ods_mass_mix, base_meta_mass_mix, base_indicadores_mass_mix], + outputs=[html19_1, html19_2, html19_3, exp19] + ) + + base_indicadores_mass_mix.change( + fn=tab_viz19, + inputs=[base_ods_mass_mix, base_meta_mass_mix, base_indicadores_mass_mix], + outputs=[html19_1, html19_2, html19_3, exp19] + ) + ## AJUSTE POR FILTROS DE CATEGORIA + with gr.Column(): + bttn_filtro_categoria.click( + fn=lambda df, categoria, elementos: df[df[categoria].isin(elementos)] if df is not None else None, + inputs=[base_ods_mass_mix, categorias_mass, filtro_categoria], + outputs=[base_ods_mass_mix_cat] + ) + + bttn_filtro_categoria.click( + fn=lambda df, categoria, elementos: df[df[categoria].isin(elementos)] if df is not None else None, + inputs=[base_meta_mass_mix, categorias_mass, filtro_categoria], + outputs=[base_meta_mass_mix_cat] + ) + + bttn_filtro_categoria.click( + fn=lambda df, categoria, elementos: df[df[categoria].isin(elementos)] if df is not None else None, + inputs=[base_indicadores_mass_mix, categorias_mass, filtro_categoria], + outputs=[base_indicadores_mass_mix_cat] + ) + + base_ods_mass_mix_cat.change( + fn=tab_viz19, + inputs=[base_ods_mass_mix_cat, base_meta_mass_mix_cat, base_indicadores_mass_mix_cat], + outputs=[html19_1, html19_2, html19_3, exp19] + ) + + base_meta_mass_mix_cat.change( + fn=tab_viz19, + inputs=[base_ods_mass_mix_cat, base_meta_mass_mix_cat, base_indicadores_mass_mix_cat], + outputs=[html19_1, html19_2, html19_3, exp19] + ) + + base_indicadores_mass_mix_cat.change( + fn=tab_viz19, + inputs=[base_ods_mass_mix_cat, base_meta_mass_mix_cat, base_indicadores_mass_mix_cat], + outputs=[html19_1, html19_2, html19_3, exp19] + ) + + + + + # categorias.change(fn=lambda df, n: df[df[f'ods_peso_acumulado_sin_norm'] <= 1-n] if df is not None else None, + # inputs=[output_ods_mass_mix, categorias], + # outputs=[output_ods_mass_mix], api_name="ods") + with gr.Tab("Análisis 2 - Filtrado", visible=False): + with gr.Row(): + with gr.Column(): + slider_massive_ods_sim = gr.Slider(0, 1, step=0.01, value=0.70,interactive=True, label="Sensibilidad ODS") + slider_massive_ods_rank = gr.Slider(0, 10, step=1, value=4,interactive=True, label="Top ODS") + with gr.Column(): + slider_massive_meta_sim = gr.Slider(0, 1, step=0.01, value=0.70,interactive=True, label="Sensibilidad METAS") + slider_massive_meta_rank = gr.Slider(0, 10, step=1, value=4,interactive=True, label="Top METAS") + with gr.Column(): + slider_massive_indicador_sim = gr.Slider(0, 1, step=0.01, value=0.70,interactive=True, label="Sensibilidad INDICADORES") + slider_massive_indicador_rank = gr.Slider(0, 10, step=1, value=4,interactive=True, label="Top INDICADORES") + + with gr.Tab("Tablas Filtradas", visible=True): + with gr.Column(): + df_ods_mass_filtrado = gr.Dataframe(df_iniciativas_template, label="METAS Filtradas") + slider_massive_ods_sim.release(fn=lambda df, n: df[df[f'ods_peso_acumulado_sin_norm'] <= 1-n] if df is not None else None, + inputs=[output_ods_mass, slider_massive_ods_sim], + outputs=[df_ods_mass_filtrado], api_name="ods") + slider_massive_ods_rank.release(fn=lambda df, n: df[df[f'ods_rank'] <= n] if df is not None else None, + inputs=[output_ods_mass, slider_massive_ods_rank], + outputs=[df_ods_mass_filtrado], api_name="ods") + + + df_meta_mass_filtrado = gr.Dataframe(df_iniciativas_template, label="METAS Filtradas") + slider_massive_meta_sim.release(fn=lambda df, n: df[df[f'meta_peso_acumulado_sin_norm'] <= 1-n] if df is not None else None, + inputs=[output_meta_mass, slider_massive_meta_sim], + outputs=[df_meta_mass_filtrado], api_name="ods") + slider_massive_meta_rank.release(fn=lambda df, n: df[df[f'meta_rank'] <= n] if df is not None else None, + inputs=[output_meta_mass, slider_massive_meta_rank], + outputs=[df_meta_mass_filtrado], api_name="ods") + + df_indicador_mass_filtrado = gr.Dataframe(df_iniciativas_template, label="Indicadores Filtradas") + slider_massive_indicador_sim.release(fn=lambda df, n: df[df[f'indicador_peso_acumulado_sin_norm'] <= 1-n] if df is not None else None, + inputs=[output_indicadores_mass, slider_massive_indicador_sim], + outputs=[df_indicador_mass_filtrado], api_name="ods") + slider_massive_indicador_rank.release(fn=lambda df, n: df[df[f'indicador_rank'] <= n] if df is not None else None, + inputs=[output_indicadores_mass, slider_massive_indicador_rank], + outputs=[df_indicador_mass_filtrado], api_name="ods") + + # PESTAÑA Graficas + with gr.Tab("🕸️ Gráficas", visible=False): + + + with gr.Row(): + with gr.Column(scale=2): + plot4 = gr.Plot(label="Gráfico de Radar") + with gr.Column(scale=1): + exp4 = gr.Markdown() + + # btn4_m = gr.Button("🔄 Generar Visualización", variant="primary") + df_ods_mass_filtrado.change( + fn=tab_viz4, + inputs=[df_ods_mass_filtrado, gr.State('ODS_ID'), gr.State('ods_peso_sin_norm'), gr.State('ods_rank'), gr.State('ODS')], + outputs=[plot4, exp4] + ) + + # with gr.Row(): + # with gr.Column(scale=2): + # plot6 = gr.Plot(label="Top 5 Indicadores por ODS") + # with gr.Column(scale=1): + # exp6 = gr.Markdown() + + # # btn6 = gr.Button("🔄 Generar Visualización", variant="primary") + # df_meta_mass_filtrado.change( + # fn=tab_viz6, + # inputs=[df_meta_mass_filtrado, gr.State('ODS_ID'), gr.State('meta_similaridad_cos'), gr.State('meta_rank'), gr.State('METAS'), gr.State(3)], + # outputs=[plot6, exp6] + # ) + + # PESTAÑA 5: SUNBURST + # with gr.Tab("☀️ 5. Sunburst"): + with gr.Row(): + with gr.Column(scale=2): + plot5 = gr.Plot(label="Diagrama de Sol") + with gr.Column(scale=1): + exp5 = gr.Markdown() + + # btn5 = gr.Button("🔄 Generar Visualización", variant="primary") + df_meta_mass_filtrado.change( + fn=tab_viz5, + inputs=[df_meta_mass_filtrado, gr.State('META_ID1'), gr.State('meta_rank'), gr.State('meta_rank'), gr.State('METAS')], + outputs=[plot5, exp5] + ) + + with gr.Tab("MASIVA CON PROMPTING", visible=False): + gr.Markdown("Aquí puedes processar múltiples iniciativas con enfoque de prompting. (Usa la plantilla proporcionada)") + + + + # client = InferenceClient(token="hf_tu_token_aqui") + + # Celda 3: Prompts por nivel + PROMPT_ODS = """Vincula entre 3 y 5 Objetivos de Desarrollo Sostenible (ODS) que tengan una relación directa con la siguiente iniciativa: + + "{iniciativa}" + + Para cada ODS vinculado incluye: + - Número y nombre del ODS + - Justificación breve de por qué se vincula (impacto social, territorial, de paz, infraestructura o inclusión) + + Formato de respuesta (una línea por ODS, ordenados por relevancia de mayor a menor): + ODS [número]: [nombre] – [justificación] + + Responde ÚNICAMENTE con el listado en el formato indicado, sin texto adicional.""" + + + PROMPT_META = """Identifica entre 3 y 5 Metas de los Objetivos de Desarrollo Sostenible (ODS) que tengan relación directa con la siguiente iniciativa: + + "{iniciativa}" + + Para cada meta incluye: + - Código de la meta (ej: 4.2, 1.3, 16.10) + - Descripción de la meta + - Justificación breve del vínculo con la iniciativa + + Formato de respuesta (una línea por meta, ordenadas por relevancia de mayor a menor): + Meta [código]: [descripción] – [justificación] + + Responde ÚNICAMENTE con el listado en el formato indicado, sin texto adicional.""" + + + PROMPT_INDICADOR = """Identifica entre 3 y 5 Indicadores de los Objetivos de Desarrollo Sostenible (ODS) que permitan medir el impacto de la siguiente iniciativa: + + "{iniciativa}" + + Para cada indicador incluye: + - Código del indicador (ej: 4.2.1, 1.3.1, 16.10.1) + - Descripción del indicador + - Justificación breve de cómo se relaciona con la medición de la iniciativa + + Formato de respuesta (una línea por indicador, ordenados por relevancia de mayor a menor): + Indicador [código]: [descripción] – [justificación] + + Responde ÚNICAMENTE con el listado en el formato indicado, sin texto adicional.""" + + + # Celda 4: Funciones de parsing por nivel + def parsear_ods(texto: str) -> list: + """Extrae ODS de la respuesta.""" + items = [] + for linea in texto.strip().split("\n"): + linea = linea.strip() + if not linea: + continue + # Buscar patrón ODS [número]: [nombre] – [justificación] + match = re.match(r"(?:\d+\.\s*)?ODS\s*(\d+):?\s*(.+)", linea, re.IGNORECASE) + if match: + ods_num = match.group(1) + resto = match.group(2).strip() + items.append({ + "ods_id": f"ODS {ods_num}", + "objetivo": resto + }) + return items + + + def parsear_metas(texto: str) -> list: + """Extrae metas de la respuesta.""" + items = [] + for linea in texto.strip().split("\n"): + linea = linea.strip() + if not linea: + continue + # Buscar patrón Meta [código]: [descripción] + match = re.match(r"(?:\d+\.\s*)?Meta\s*([\d\.]+):?\s*(.+)", linea, re.IGNORECASE) + if match: + meta_codigo = match.group(1) + resto = match.group(2).strip() + # Extraer ODS del código de meta + ods_num = meta_codigo.split(".")[0] + items.append({ + "ods_id": f"ODS {ods_num}", + "meta_id": f"Meta {meta_codigo}", + "objetivo": resto + }) + return items + + + def parsear_indicadores(texto: str) -> list: + """Extrae indicadores de la respuesta.""" + items = [] + for linea in texto.strip().split("\n"): + linea = linea.strip() + if not linea: + continue + # Buscar patrón Indicador [código]: [descripción] + match = re.match(r"(?:\d+\.\s*)?Indicador\s*([\d\.]+):?\s*(.+)", linea, re.IGNORECASE) + if match: + ind_codigo = match.group(1) + resto = match.group(2).strip() + # Extraer ODS del código + ods_num = ind_codigo.split(".")[0] + items.append({ + "ods_id": f"ODS {ods_num}", + "indicador_id": f"Indicador {ind_codigo}", + "objetivo": resto + }) + return items + + + # Celda 5: Función de clasificación por nivel + def clasificar_nivel(iniciativa: str, nivel: str) -> dict: + """Clasifica una iniciativa en un nivel específico (ods, meta, indicador).""" + prompts = { + "ods": PROMPT_ODS, + "meta": PROMPT_META, + "indicador": PROMPT_INDICADOR + } + parsers = { + "ods": parsear_ods, + "meta": parsear_metas, + "indicador": parsear_indicadores + } + + try: + respuesta = client.chat_completion( + model="Qwen/Qwen2.5-7B-Instruct", + messages=[{"role": "user", "content": prompts[nivel].format(iniciativa=iniciativa)}], + max_tokens=1000, + temperature=0.3 + ) + texto = respuesta.choices[0].message.content + items = parsers[nivel](texto) + + return { + "iniciativa": iniciativa, + "items": items, + "respuesta_raw": texto, + "error": None + } + except Exception as e: + return { + "iniciativa": iniciativa, + "items": [], + "respuesta_raw": "", + "error": str(e) + } + + + # Celda 6: Función principal de procesamiento por lotes + def procesar_lote(iniciativas: list, mostrar_progreso: bool = True) -> dict: + """ + Procesa un listado de iniciativas y genera 3 DataFrames: + - df_ods: Clasificación a nivel ODS + - df_metas: Clasificación a nivel Meta + - df_indicadores: Clasificación a nivel Indicador + """ + niveles = ["ods", "meta", "indicador"] + resultados = {nivel: [] for nivel in niveles} + + total = len(iniciativas) * len(niveles) + contador = 0 + + for idx, iniciativa in enumerate(iniciativas): + iniciativa_id = f"INI_{idx + 1:04d}" + + for nivel in niveles: + contador += 1 + if mostrar_progreso: + print(f"[{contador}/{total}] {iniciativa_id} - {nivel.upper()}: {iniciativa[:50]}...") + + resultado = clasificar_nivel(iniciativa, nivel) + + if resultado["error"]: + resultados[nivel].append({ + "INICIATIVA_ID": iniciativa_id, + "ODS_ID": "ERROR", + "OBJETIVO": resultado["error"], + "ods_rank": 0, + "iniciativa": iniciativa + }) + else: + for rank, item in enumerate(resultado["items"], start=1): + fila = { + "INICIATIVA_ID": iniciativa_id, + "ODS_ID": item.get("ods_id", ""), + "OBJETIVO": item.get("objetivo", ""), + "ods_rank": rank, + "iniciativa": iniciativa + } + # Agregar columna específica del nivel si aplica + if nivel == "meta": + fila["META_ID"] = item.get("meta_id", "") + elif nivel == "indicador": + fila["INDICADOR_ID"] = item.get("indicador_id", "") + + resultados[nivel].append(fila) + + time.sleep(0.5) # evitar rate limits + + # Crear DataFrames + df_ods = pd.DataFrame(resultados["ods"]) + if not df_ods.empty and "INICIATIVA_ID" in df_ods.columns: + df_ods = df_ods[["INICIATIVA_ID", "ODS_ID", "OBJETIVO", "ods_rank", "iniciativa"]] + + df_metas = pd.DataFrame(resultados["meta"]) + if not df_metas.empty and "META_ID" in df_metas.columns: + df_metas = df_metas[["INICIATIVA_ID", "ODS_ID", "META_ID", "OBJETIVO", "ods_rank", "iniciativa"]] + + df_indicadores = pd.DataFrame(resultados["indicador"]) + if not df_indicadores.empty and "INDICADOR_ID" in df_indicadores.columns: + df_indicadores = df_indicadores[["INICIATIVA_ID", "ODS_ID", "INDICADOR_ID", "OBJETIVO", "ods_rank", "iniciativa"]] + + # return { + # "df_ods": df_ods, + # "df_metas": df_metas, + # "df_indicadores": df_indicadores + # } + return df_ods, df_metas, df_indicadores + + def procesar_desde_archivo(ruta_archivo: str, columna_iniciativa: str = None) -> dict: + """Carga iniciativas desde CSV/Excel y las procesa.""" + if ruta_archivo.endswith(".csv"): + df = pd.read_csv(ruta_archivo) + else: + df = pd.read_excel(ruta_archivo) + + # Detectar columna si no se especifica + if columna_iniciativa is None: + for col in df.columns: + if any(x in col.lower() for x in ["iniciativa", "descripcion", "nombre", "proyecto"]): + columna_iniciativa = col + break + if columna_iniciativa is None: + columna_iniciativa = df.columns[0] + + print(f"Usando columna: {columna_iniciativa}") + print(f"Total iniciativas: {len(df)}") + + iniciativas = df[columna_iniciativa].astype(str).tolist() + return procesar_lote(iniciativas) + + btn_mass_prompt = gr.Button("Procesar archivo") + + with gr.Column(): + output_ods_mass_prompt = gr.Dataframe(label="Resultados del análisis masivo ODS", visible=True, interactive=True) + output_meta_mass_prompt = gr.Dataframe(label="Resultados del análisis masivo Metas", visible=True, interactive=True) + output_indicadores_mass_prompt = gr.Dataframe(label="Resultados del análisis masivo Indicadores", visible=True, interactive=True) + + btn_mass_prompt.click( + fn=procesar_desde_archivo, + inputs=[file_output, gr.State('iniciativa')], + outputs=[output_ods_mass_prompt, output_meta_mass_prompt, output_indicadores_mass_prompt], + show_progress=True + ) + + + # --- + # ### 📚 Recursos Adicionales + # - **Documentación completa**: Consulta los archivos `.md` incluidos + # - **Código fuente**: `visualizaciones_ods.py` + # - **Documento Word**: `GUIA_VISUALIZACIONES_PUBLICO_GENERAL.docx` + + # --- + # *Sistema de Visualización ODS | Febrero 2026 | Desarrollado con Python, Plotly, Matplotlib y Gradio* + # Pie de página + gr.Markdown(""" + --- + --- + *Sistema de Visualización ODS | Febrero 2026 | Desarrollado con Python, Plotly, Matplotlib y Gradio* + """) + + return app + +# ============================================================================ +# EJECUCIÓN DE LA APLICACIÓN +# ============================================================================ + +if __name__ == "__main__": + print("\n" + "="*70) + print("INICIANDO APLICACIÓN GRADIO - VISUALIZACIONES ODS") + print("="*70) + + # if not DATOS_CARGADOS: + # print("\n⚠️ ADVERTENCIA: No se pudieron cargar los datos.") + # print(" Verifica que el archivo existe en:", RUTA_DATOS) + # print(" La aplicación se iniciará pero mostrará errores.") + # else: + # print(f"\n✓ Datos cargados correctamente: {len(df_global)} registros") + # print(f"✓ ODS únicos: {df_global['ods_id'].nunique()}") + + print("\n" + "="*70) + print("CREANDO APLICACIÓN...") + print("="*70) + + app = crear_app() + + print("\n✓ Aplicación creada exitosamente") + print("\n" + "="*70) + print("INICIANDO SERVIDOR WEB...") + print("="*70) + print("\n🌐 La aplicación se abrirá en tu navegador automáticamente") + # print("📍 URL local: http://127.0.0.1:7860") + print("🌍 URL pública: Se generará si share=True\n") + print("💡 Presiona Ctrl+C para detener el servidor\n") + + # Lanzar la aplicación + app.launch( + # theme="light", + # server_name="0.0.0.0", # Permite acceso desde cualquier IP + # server_port=7860, # Puerto por defecto + # share=True, # Cambiar a True para URL pública + show_error=True, # Mostrar errores en la interfaz + # quiet=False # Mostrar logs en consola + debug=True # Modo debug para desarrollo + )