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']}]({dict_logos[row['logo_id']]})
-
-
''' for _, row in top_ods.iterrows()])}
-
-
-
-
-
🎯 Metas Más Relevantes Por ODS Top
-
- {''.join([f'''
-
-
![ODS {row['ODS_ID']}]({dict_logos[row['logo_id']]})
-
Meta: {row['META_ID']}
-
-
-
-
''' for _, row in top_metas.iterrows()])}
-
-
-
-
-
🎯 Indicadores Más Relevantes Por ODS Top
-
- {''.join([f'''
-
-
![ODS {row['ODS_ID']}]({dict_logos[row['logo_id']]})
-
Ind: {row['INDICADOR_ID']}
-
-
-
Rank: {row['indicador_rank']}
-
''' for _, row in top_indicador.iterrows()])}
-
-
-
-
-
📋 Recomendaciones Territoriales por Meta
-
-
-
- | ODS |
- Meta ID |
- Recomendaciones Territoriales |
-
-
-
- {''.join([f'''
-
-
-
- |
- {row['META_ID']} |
- {row.get('Recomendaciones_territoriales', 'N/A')} |
-
''' for _, row in df_metas.iterrows() if not pd.isna(row.get('Recomendaciones_territoriales'))])}
-
-
-
-
-
-
- """
- 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']}]({dict_logos[row['logo_id']]})
-
-
''' for _, row in top_ods.iterrows()])}
-
-
-
-
-
🎯 Metas Más Relevantes Por ODS Top
-
- {''.join([f'''
-
-
![ODS {row['ODS_ID']}]({dict_logos[row['logo_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']}]({dict_logos[row['logo_id']]})
-
Ind: {row['INDICADOR_ID']}
-
-
-
Rank: {row['rank']}
-
''' for _, row in top_indicador.iterrows()])}
-
-
-
-
-
📋 Recomendaciones Territoriales por Meta
-
-
-
- | ODS |
- Meta ID |
- Recomendaciones Territoriales |
-
-
-
- {''.join([f'''
-
-
-
- |
- {row['META_ID']} |
- {row.get('Recomendaciones_territoriales', 'N/A')} |
-
''' for _, row in df_metas.iterrows() if not pd.isna(row.get('Recomendaciones_territoriales'))])}
-
-
-
-
-
-
- """
- 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
-
-
-
- | ODS |
- Count |
- Media |
- Std |
- Min |
- Max |
-
-
-
- {''.join([f'''
- | ODS {idx} |
- {int(row['count'])} |
- {row['mean']:.4f} |
- {row['std']:.4f} |
- {row['min']:.4f} |
- {row['max']:.4f} |
-
''' for idx, row in stats_ods.iterrows()])}
-
-
-
-
-
-
4. ODS Más Representados en Top 50
-
-
-
- | ODS |
- Cantidad |
- Porcentaje |
-
-
-
- {''.join([f'''
- | ODS {idx} |
- {count} |
- {count/50*100:.1f}% |
-
''' for idx, count in top_50_ods.head(10).items()])}
-
-
-
-
- """
-
- 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"""
-
- """)
-
- # Encabezado principal
-
-
- with gr.Row():
- with gr.Column(scale=1):
- gr.HTML(f"""
-
-

-
-
- """)
- 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)
-
- - 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.
- - Marcos globales: (UNDP, VNR, EU SDG Strategy) recomiendan 3-5 ODS
- prioritarios por ciclo de implementación.
- - Interactúa: Las visualizaciones son dinámicas y permiten exploración
-
-
- """)
-
- 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)
-
- - 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.
- - Marcos globales: (UNDP, VNR, EU SDG Strategy) recomiendan 3-5 ODS
- prioritarios por ciclo de implementación.
- - Interactúa: Las visualizaciones son dinámicas y permiten exploración
-
-
- """)
-
- 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)
-
- - 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.
- - Marcos globales: (UNDP, VNR, EU SDG Strategy) recomiendan 3-5 ODS
- prioritarios por ciclo de implementación.
- - Interactúa: Las visualizaciones son dinámicas y permiten exploración
-
-
- """)
-
- 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)
-
- - 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.
- - Marcos globales: (UNDP, VNR, EU SDG Strategy) recomiendan 3-5 ODS
- prioritarios por ciclo de implementación.
- - Interactúa: Las visualizaciones son dinámicas y permiten exploración
-
-
- """)
-
- 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']}]({dict_logos[row['logo_id']]})
+
+
''' for _, row in top_ods.iterrows()])}
+
+
+
+
+
🎯 Metas Más Relevantes Por ODS Top
+
+ {''.join([f'''
+
+
![ODS {row['ODS_ID']}]({dict_logos[row['logo_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
+
+
+
+ | ODS |
+ Meta ID |
+ Recomendaciones Territoriales |
+
+
+
+ {''.join([f'''
+
+
+
+ |
+ {row['META_ID']} |
+ {row.get('Recomendaciones_territoriales', 'N/A')} |
+
''' for _, row in df_metas.iterrows() if not pd.isna(row.get('Recomendaciones_territoriales'))])}
+
+
+
+
+
+
+ """
+ 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']}]({dict_logos[row['logo_id']]})
+
+
''' for _, row in top_ods.iterrows()])}
+
+
+
+
+
🎯 Metas Más Relevantes Por ODS Top
+
+ {''.join([f'''
+
+
![ODS {row['ODS_ID']}]({dict_logos[row['logo_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
+
+
+
+ | ODS |
+ Meta ID |
+ Recomendaciones Territoriales |
+
+
+
+ {''.join([f'''
+
+
+
+ |
+ {row['META_ID']} |
+ {row.get('Recomendaciones_territoriales', 'N/A')} |
+
''' for _, row in df_metas.iterrows() if not pd.isna(row.get('Recomendaciones_territoriales'))])}
+
+
+
+
+
+
+ """
+ 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
+
+
+
+ | ODS |
+ Count |
+ Media |
+ Std |
+ Min |
+ Max |
+
+
+
+ {''.join([f'''
+ | ODS {idx} |
+ {int(row['count'])} |
+ {row['mean']:.4f} |
+ {row['std']:.4f} |
+ {row['min']:.4f} |
+ {row['max']:.4f} |
+
''' for idx, row in stats_ods.iterrows()])}
+
+
+
+
+
+
4. ODS Más Representados en Top 50
+
+
+
+ | ODS |
+ Cantidad |
+ Porcentaje |
+
+
+
+ {''.join([f'''
+ | ODS {idx} |
+ {count} |
+ {count/50*100:.1f}% |
+
''' for idx, count in top_50_ods.head(10).items()])}
+
+
+
+
+ """
+
+ 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"""
+
+ """)
+
+ # Encabezado principal
+
+
+ with gr.Row():
+ with gr.Column(scale=1):
+ gr.HTML(f"""
+
+

+
+
+ """)
+ 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)
+
+ - 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.
+ - Marcos globales: (UNDP, VNR, EU SDG Strategy) recomiendan 3-5 ODS
+ prioritarios por ciclo de implementación.
+ - Interactúa: Las visualizaciones son dinámicas y permiten exploración
+
+
+ """)
+
+ 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)
+
+ - 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.
+ - Marcos globales: (UNDP, VNR, EU SDG Strategy) recomiendan 3-5 ODS
+ prioritarios por ciclo de implementación.
+ - Interactúa: Las visualizaciones son dinámicas y permiten exploración
+
+
+ """)
+
+ 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)
+
+ - 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.
+ - Marcos globales: (UNDP, VNR, EU SDG Strategy) recomiendan 3-5 ODS
+ prioritarios por ciclo de implementación.
+ - Interactúa: Las visualizaciones son dinámicas y permiten exploración
+
+
+ """)
+
+ 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)
+
+ - 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.
+ - Marcos globales: (UNDP, VNR, EU SDG Strategy) recomiendan 3-5 ODS
+ prioritarios por ciclo de implementación.
+ - Interactúa: Las visualizaciones son dinámicas y permiten exploración
+
+
+ """)
+
+ 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
+ )