"""
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 src.embeddings.llm_clasificador_HF import procesar_lote
from huggingface_hub import InferenceClient
# import pandas as pd
import re
import time
import pytz
from datetime import datetime, timedelta
# 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_23_nube_palabras_simple,
viz_24_header_conecta_ods,
analisis_estadistico
)
# ============================================================================
# CONFIGURACIÓN GLOBAL
# ============================================================================
import os
import base64
levels = ['ODS_ID','META_ID','INDICADOR_ID']
def generarFecha():
bogota_timezone = pytz.timezone('America/Bogota')
bogota_date = datetime.now(bogota_timezone)
fecha = bogota_date.strftime('%Y_%m_%d_%H_%M_%S')
anio = bogota_date.strftime('%Y')
mes = bogota_date.strftime('%m')
dia = bogota_date.strftime('%d')
hora = bogota_date.strftime('%H')
minuto = bogota_date.strftime('%M')
segundo = bogota_date.strftime('%S')
return fecha, anio, mes, dia, hora, minuto, segundo
def actualizar_fecha():
fecha, anio, mes, dia, hora, minuto, segundo = generarFecha()
return f"Fecha de consulta: {anio}-{mes}-{dia} {hora}:{minuto}"
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-ODS-aprobado.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'''
''' for _, row in top_ods.iterrows()])}
🎯 Metas Más Relevantes Por ODS Top
{''.join([f'''
Meta: {row['META_ID']}
''' for _, row in top_metas.iterrows()])}
📋 Recomendaciones Territoriales por Meta
| ODS |
Meta ID |
Recomendaciones Territoriales |
{''.join([f'''
|
{row['META_ID']} |
{row['Recomendaciones_territoriales']} |
''' for _, row in top_metas.iterrows()])}
"""
return html
def tab_inicio_prompt(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, 'rank')[['rank', 'ODS_ID', 'score', 'frecuencia']]
top_ods['logo_id'] = top_ods['ODS_ID'].apply(lambda _: f"ods_{str(_)}")
# 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 = df_metas.nsmallest(8, 'rank')[['rank', 'META_ID', 'score', 'frecuencia']]
# top_metas = df_metas[['rank', 'META_ID', 'score', 'frecuencia']]
# 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_{str(_).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 = df_metas.nsmallest(4, 'ods_rank')[['rank', 'INDICADOR_ID', 'score', 'frecuencia']]
# top_indicador['ODS_ID'] = top_indicador['INDICADOR_ID'].apply(lambda x: str(x).split('.')[0] if pd.notna(x) else None)
# top_indicador['logo_id'] = top_indicador['ODS_ID'].apply(lambda _: f"ods_{str(_)}")
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'''
''' for _, row in top_ods.iterrows()])}
🎯 Metas Más Relevantes Por ODS Top
{''.join([f'''
Meta: {row['META_ID']}
''' for _, row in top_metas.iterrows()])}
📋 Acciones Territoriales asociadas a Metas ODS
| META |
Meta ID |
Desde las metas de ODS, ¿Cuáles son las acciones asociadas con la iniciativa, problemática u oportunidad en tu territorio? |
{''.join([f'''
|
{row['META_ID']} |
{row['Recomendaciones_territoriales']} |
''' for _, row in top_metas.iterrows()])}
"""
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 = df_metas.nsmallest(8, 'rank')[['META_ID','rank','score','ODS_ID']]
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'''
''' for _, row in top_ods.iterrows()])}
🎯 Metas Más Relevantes Por ODS Top
{''.join([f'''
Meta: {row['META_ID']}
''' for _, row in top_metas.iterrows()])}
📋 Acciones Territoriales asociadas a Metas ODS
| ODS |
Meta ID |
Desde las metas de ODS, ¿Cuáles son las acciones asociadas con la iniciativa, problemática u oportunidad en tu territorio? |
{''.join([f'''
|
{row['META_ID']} |
{row['Recomendaciones_territoriales']} |
''' for _, row in top_metas.iterrows()])}
"""
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()
if nivel == 'meta':
desc_metas = Path('data/raw/tabla_lvlMetaOds.xlsx')
df_descmetas = pd.read_excel(desc_metas)
df_descmetas['ID_META'] = df_descmetas['ID_META'].apply(lambda _: str(_).upper())
resultado = resultado.merge(df_descmetas[['ID_META', 'OBJETIVO', 'META']], left_on='META_ID', right_on='ID_META', how='left')
elif nivel == 'ods':
desc_metas = Path('data/raw/tabla_lvlMetaOds.xlsx')
df_descmetas = pd.read_excel(desc_metas)
df_descmetas['ID_OBJETIVO'] = df_descmetas['ID_OBJETIVO'].apply(lambda _: str(_).upper())
df_descmetas = df_descmetas[['ID_OBJETIVO', 'OBJETIVO']].drop_duplicates()
resultado = resultado.merge(df_descmetas, left_on='ODS_ID', right_on='ID_OBJETIVO', how='left')
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:
- Ranking promedio (posición)
- Frecuencia de aparición
### Interpretación:
- **Barras largas**: {nivel} con mayor score global
- **Color verde**: Scores más altos
- **Color azul**: Scores intermedios
- **Círculos grandes**: {nivel} que aparecen en más iniciativas
### Top 3 ODS:
{chr(10).join([f'- **{nivel} {row[f"{nivel.upper()}_ID"]}**: Score {row["score"]:.4f} ({int(row["frecuencia"])} iniciativas)'
for _, row in resultado.head(3).iterrows()] if len(resultado) > 0 else ['Sin datos'])}
### 💡 Uso:
Est@s {nivel} son los **prioritarios a nivel agregado** considerando
relevancia, posición y prevalencia en el grupo de textos analizados.
"""
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_viz23(df_texto, columna, max_palabras=100):
"""Visualización 23: Nube de Palabras - Paloma de Paz"""
# if not DATOS_RELACIONES_CARGADOS:
# return None, ""
# try:
# # Seleccionar columna con más texto
# # Probar primero con descripciones de ODS, luego metas, luego indicadores
# if 'OBJETIVO' in df_ods_rel.columns and df_ods_rel['OBJETIVO'].notna().sum() > 0:
# df_texto = df_ods_rel
# columna = 'OBJETIVO'
# tipo = 'Objetivos (ODS)'
# elif 'META' in df_metas_rel.columns and df_metas_rel['META'].notna().sum() > 0:
# df_texto = df_metas_rel
# columna = 'META'
# tipo = 'Metas'
# elif 'INDICADOR' in df_indicadores_rel.columns and df_indicadores_rel['INDICADOR'].notna().sum() > 0:
# df_texto = df_indicadores_rel
# columna = 'INDICADOR'
# tipo = 'Indicadores'
# else:
# return None, "⚠️ No se encontraron columnas de texto para analizar."
# Generar nube de palabras
# filepath = viz_23_nube_palabras_paloma(df_texto, columna, max_palabras=100)
# Si falla, intentar versión simple
# if not os.path.exists(filepath):
filepath = viz_23_nube_palabras_simple(df_texto, columna, max_palabras=100)
explicacion = f"""
## 🕊️ Nube de Palabras - Paloma de Paz
### ¿Qué muestra?
Las **palabras más frecuentes** encontradas en el documento
### Interpretación:
- **Tamaño grande**: Palabras que aparecen con más frecuencia
- **Tamaño pequeño**: Palabras menos frecuentes
- **Posición**: Aleatoria dentro de la forma de paloma
### 🔍 Análisis:
- Total de registros analizados: {len(df_texto)}
- Palabras únicas mostradas: ~100
- Stopwords excluidas: sí (artículos, preposiciones, etc.)
### 💡 Uso:
Identifica rápidamente los **temas centrales** y **conceptos clave**
que dominan el conjunto de textos analizados.
### 🎨 Simbología:
La paloma de paz representa el espíritu de los Objetivos de Desarrollo
Sostenible: paz, bienestar y desarrollo para todos.
"""
return filepath, 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"""
from src.embeddings.llm_clasificador_HF_gem import procesar_lote_llm
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 basada en inteligencia artificial que traduce
# relatos de iniciativas asociadas a problematicas y oportunidades a nivel territorial en lenguaje de Objetivos de Desarrollo Soistenible - ODS*
# """)
gr.HTML(f"""
""")
# Pestañas principales
with gr.Tabs():
def ver_radio(df):
return True
# PESTAÑA: CONSULTA
with gr.Tab("CONSULTA INDIVIDUAL PROMPT"):
# 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 de solución asociada a una problemática u oportunidad de tu territorio.
*La herramienta analizará el texto y mostrará con qué Objetivos y de Desarrollo Sostenible (ODS) y metas se relaciona,
facilitando la comprensión y la priorización de acciones para respuesta de transformación integral.*
""")
query_in_prt = gr.Textbox(lines=5, placeholder="Escribe aquí tu consulta...", label="Iniciativa a analizar")
lst_query_in_prt = gr.State() #gr.Textbox(value="[]", visible=False)
def add_query_list(query):
return gr.update(value=[query])
query_in_prt.change(
fn = add_query_list,
inputs = query_in_prt,
outputs = lst_query_in_prt)
query_out_prt = gr.Textbox(lines=5, label="Texto ajustado para lenguaje natural", visible=False)
txt_municipio = gr.Textbox(lines=1, value="Municipio de consulta ...", label="Municipio", interactive=True)
btn_prt = gr.Button(value="Analizar texto en lenguaje de ODS")
msj_procesamiento_prt = gr.Textbox(value="... preparando análisis de iniciativa.", label='' , visible=True)
with gr.Tab("🔍 Resultado"):
with gr.Row(visible=False):
ods_prt = gr.Dataframe( label="ODS")#, buttons=["fullscreen"])
meta_prt = gr.Dataframe( label="METAS")#, buttons=["fullscreen"])
indicador_prt = gr.Dataframe( label="INDICADORES")#, buttons=["fullscreen"])
html_inicio_ods_prt = gr.HTML() #tab_inicio(ods.value)
#### Actualizando resultados iniciales
ods_prt.change(
fn=tab_inicio_prompt,
inputs=[ods_prt,meta_prt,indicador_prt],
outputs=[html_inicio_ods_prt]
)
meta_prt.change(
fn=tab_inicio_prompt,
inputs=[ods_prt,meta_prt,indicador_prt],
outputs=[html_inicio_ods_prt]
)
indicador_prt.change(
fn=tab_inicio_prompt,
inputs=[ods_prt,meta_prt,indicador_prt],
outputs=[html_inicio_ods_prt]
)
# Función wrapper para convertir string a lista
def procesar_consulta(query_text):
"""Convierte el texto de consulta en una lista y llama a procesar_lote"""
if not query_text or not query_text.strip():
return pd.DataFrame(), pd.DataFrame(), pd.DataFrame()
# Pasar como lista con un único elemento
return procesar_lote_llm([query_text.strip()])
with gr.Tab("Resultados por ODS - Metas"):
with gr.Row():
with gr.Tab("🏆 Ranking Masivo ODS"):
with gr.Row():
with gr.Column(scale=2):
plot_mass_ods_ind = gr.Plot(label="Ranking Global ODS")
with gr.Column(scale=1):
exp_mass_ods_ind = gr.Markdown()
with gr.Tab('Tabla score ODS'):
output_ods_mass_mix_ind = 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_ind = gr.Plot(label="Ranking Global METAS")
with gr.Column(scale=1):
exp_mass_metas_ind = gr.Markdown()
with gr.Tab('Tabla score METAS'):
output_meta_mass_mix_ind = gr.Dataframe(label="Resultados del análisis masivo METAS")
with gr.Row(visible=False):
with gr.Tab("🏆 Ranking Masivo INDICADORES"):
with gr.Row():
with gr.Column(scale=2):
plot_mass_indicadores_ind = gr.Plot(label="Ranking Global INDICADORES")
with gr.Column(scale=1):
exp_mass_indicadores_ind = gr.Markdown()
with gr.Tab('Tabla score INDICADORES'):
output_indicadores_mass_mix_ind = gr.Dataframe(label="Resultados del análisis masivo INDICADORES")
## logicas de actualizaciones
with gr.Row(visible=False):
ods_prt.change(
fn=tab_viz21,
inputs=[ods_prt, gr.State('ods')],
outputs=[plot_mass_ods_ind, exp_mass_ods_ind]
)
meta_prt.change(
fn=tab_viz21,
inputs=[meta_prt, gr.State('meta')],
outputs=[plot_mass_metas_ind, exp_mass_metas_ind]
)
indicador_prt.change(
fn=tab_viz21,
inputs=[indicador_prt, gr.State('indicador')],
outputs=[plot_mass_indicadores_ind, exp_mass_indicadores_ind]
)
btn_prt.click(procesar_consulta, query_in_prt, [ods_prt,meta_prt,indicador_prt,msj_procesamiento_prt], show_progress=True)
btn_prt.click(procesar_consulta, query_in_prt, [output_ods_mass_mix_ind,output_meta_mass_mix_ind,output_indicadores_mass_mix_ind,msj_procesamiento_prt], show_progress=True)
txt_fecha = gr.Textbox(value="Fecha de consulta: ", interactive=False, show_label=False)
ods_prt.change(fn= actualizar_fecha, inputs=[], outputs=txt_fecha)
# btn_prt.click(procesar_lote, query_in_prt, [ods_prt,meta_prt,indicador_prt], show_progress=True)
with gr.Tab("CONSULTA INDIVIDUAL", visible=False):
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 de soluciòn asociadas a problematicas u oportunidades de tu territorio.")
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)
def num_iniciaticas(file):
if file is None:
return gr.update(value=None)
try:
df = pd.read_excel(file)
return gr.update(value=len(df['iniciativa'].dropna().unique().tolist()))
except Exception as e:
print("Error al leer el archivo:", e)
return gr.update(value=0)
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 para consulta masiva", 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
def leer_tabla(file_input, col_iniciativa='iniciativa'):
if file_input is None:
return gr.update(value=None)
df = pd.read_excel(file_input)
return gr.update(value=df[['id_unico','iniciativa']])
df_tx_iniciativas = gr.Dataframe(label="Vista previa de las primeras 10 filas del archivo cargado", visible=False)
with gr.Tab("MASIVA"):
# 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'])
titulo_mass = gr.HTML()
def actualizar_titulo(filepath):
try:
name = Path(filepath).name
return gr.update(value=f"""
""")
except:
return ''
file_output.change(actualizar_titulo, file_output, titulo_mass)
with gr.Column():
btn_mass_emb_pmt = gr.Button("Analizar archivo en lenguaje de ODS")
msj_procesamiento_mass_pmt = gr.Textbox(value="... preparando para procesar documento", interactive=False, show_label=False)
# PESTAÑA 19: RESUMEN EN TAGS
# with gr.Tab("🏷️ 19. Resumen Tags"):
with gr.Row():
with gr.Row():
# with gr.Column(scale=1):
# html19_1_pmt = gr.HTML(label="Resumen en Tags")
# with gr.Row(visible=False):
# html19_2_pmt = gr.HTML(label="Resumen en Tags")
# # with gr.Column(scale=4):
# html19_3_pmt = gr.HTML(label="Resumen en Tags")
# with gr.Column(scale=4):
file_output.change(leer_tabla,
file_output,
df_tx_iniciativas)
with gr.Column(scale=2):
def burbuja_nini(n_iniciativas):
try:
return f"""
Métricas Generales
Iniciativas Analizadas
{n_iniciativas}
"""
except:
return ""
n_ini_state = gr.State(value=0)
html_nini = gr.HTML()
file_output.change(num_iniciaticas, file_output, n_ini_state)
n_ini_state.change(burbuja_nini, n_ini_state, html_nini)
with gr.Column(scale=3):
img24_pmt = gr.Image(label="Nube de Palabras", type="filepath")
with gr.Column(visible=False):
exp24 = gr.Markdown()
# btn23 = gr.Button("🔄 Generar Nube de Palabras", variant="primary")
df_tx_iniciativas.change(
fn=tab_viz23,
inputs=[df_tx_iniciativas, gr.State('iniciativa')],
outputs=[img24_pmt, exp24]
)
with gr.Row(visible=False):
def update_choices(listado):
return gr.update(choices=listado)
categorias_mass_state_pmt = gr.State(value=[]) # Estado para almacenar categorías disponibles
iniciativas_pmt = gr.State(value=[])
categorias_mass_pmt = gr.Dropdown(
choices=categorias_mass_state_pmt.value, multiselect=False, label="Categorías"#, info="Selecciona una categoria para aplicar el filtro."
)
categorias_mass_state_pmt.change(fn=update_choices, inputs=[categorias_mass_state_pmt], outputs=[categorias_mass_pmt])
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)
def listar_iniciativas(file):
if file is None:
return []
try:
df = pd.read_excel(file)
if 'iniciativa' not in df.columns:
return []
elementos = df['iniciativa'].dropna().unique().tolist()
print(f"✅ Iniciativas cargadas: {len(elementos)}")
return elementos
except Exception as e:
print(f"❌ Error al cargar iniciativas: {e}")
return []
# filtro_categoria_state = gr.State(value=[]) # Estado para almacenar categorías seleccionadas para filtro
filtro_categoria_pmt= gr.Dropdown(
choices=[], multiselect=True, interactive=True, label="Elementos de categoría", info="Selecciona una o varias opciones para aplicar el análisis."
)
file_output.change(listar_iniciativas,
file_output,
iniciativas_pmt)
bttn_filtro_categoria_pmt = gr.Button("Aplicar filtro por categoría", variant="secondary")
categorias_mass_pmt.change(fn=listar_elementos_categoria, inputs=[file_output, categorias_mass_pmt], outputs=[filtro_categoria_pmt])
with gr.Row():
with gr.Column(scale=3):
with gr.Tab("TOP ODS"):
html_inicio_mass_pmt = gr.HTML(label="Resultados del análisis masivo - vista inicial")
with gr.Tab("Resultados por ODS - Metas"):
base_ods_mass_mix_pmt = gr.Dataframe(label="Prueba-Resultados del análisis masivo ODS", visible=False)
base_meta_mass_mix_pmt = gr.Dataframe(label="Resultados del análisis masivo METAS", visible=False)
base_indicadores_mass_mix_pmt = gr.Dataframe(label="Resultados del análisis masivo INDICADORES", visible=False)
base_ods_mass_mix_cat_pmt = gr.Dataframe(label="Prueba-Resultados del análisis masivo ODS", visible=False)
base_meta_mass_mix_cat_pmt = gr.Dataframe(label="Resultados del análisis masivo METAS", visible=False)
base_indicadores_mass_mix_cat_pmt = gr.Dataframe(label="Resultados del análisis masivo INDICADORES", visible=False)
# output_ods_mass_mix_cat_pmt = gr.Dataframe(label="Resultados del análisis masivo ODS - Categorización", visible=False)
# output_meta_mass_mix_cat_pmt = gr.Dataframe(label="Resultados del análisis masivo METAS - Categorización", visible=False)
# output_indicadores_mass_mix_cat_pmt = gr.Dataframe(label="Resultados del análisis masivo INDICADORES - Categorización", visible=False)
with gr.Row():
with gr.Tab("🏆 Ranking Masivo ODS"):
with gr.Row():
with gr.Column(scale=2):
plot_mass_ods_pmt = gr.Plot(label="Ranking Global ODS")
with gr.Column(scale=1):
exp_mass_ods = gr.Markdown()
with gr.Tab('Tabla score ODS'):
output_ods_mass_mix_pmt = 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_pmt = gr.Plot(label="Ranking Global METAS")
with gr.Column(scale=1):
exp_mass_metas = gr.Markdown()
with gr.Tab('Tabla score METAS'):
output_meta_mass_mix_pmt = gr.Dataframe(label="Resultados del análisis masivo METAS")
with gr.Row(visible=False):
with gr.Tab("🏆 Ranking Masivo INDICADORES"):
with gr.Row():
with gr.Column(scale=2):
plot_mass_indicadores_pmt = gr.Plot(label="Ranking Global INDICADORES")
with gr.Column(scale=1):
exp_mass_indicadores = gr.Markdown()
with gr.Tab('Tabla score INDICADORES'):
output_indicadores_mass_mix_pmt = gr.Dataframe(label="Resultados del análisis masivo INDICADORES")
# Primer seguimiento de actualización
with gr.Column():
output_ods_mass_mix_pmt.change(
fn=tab_inicio_mass,
inputs=[output_ods_mass_mix_pmt,output_meta_mass_mix_pmt,output_indicadores_mass_mix_pmt],
outputs=[html_inicio_mass_pmt]
)
output_meta_mass_mix_pmt.change(
fn=tab_inicio_mass,
inputs=[output_ods_mass_mix_pmt,output_meta_mass_mix_pmt,output_indicadores_mass_mix_pmt],
outputs=[html_inicio_mass_pmt]
)
output_indicadores_mass_mix_pmt.change(
fn=tab_inicio_mass,
inputs=[output_ods_mass_mix_pmt,output_meta_mass_mix_pmt,output_indicadores_mass_mix_pmt],
outputs=[html_inicio_mass_pmt]
)
with gr.Column():
output_ods_mass_mix_pmt.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_pmt, gr.State('ods')],
outputs=[plot_mass_ods_pmt, exp_mass_ods]
)
output_meta_mass_mix_pmt.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_pmt, gr.State('meta')],
outputs=[plot_mass_metas_pmt, exp_mass_metas]
)
output_indicadores_mass_mix_pmt.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_pmt, gr.State('indicador')],
outputs=[plot_mass_indicadores_pmt, exp_mass_indicadores]
)
## AJUSTE POR FILTROS DE CATEGORIA
# with gr.Column():
# bttn_filtro_categoria_pmt.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_pmt.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_pmt.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_pmt.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_pmt.change(
# fn=tab_viz19,
# inputs=[base_ods_mass_mix_cat_pmt, base_meta_mass_mix_cat_pmt, base_indicadores_mass_mix_cat_pmt],
# outputs=[html19_1_pmt, html19_2_pmt, html19_3_pmt, exp19]
# )
# base_indicadores_mass_mix_cat_pmt.change(
# fn=tab_viz19,
# inputs=[base_ods_mass_mix_cat_pmt, base_meta_mass_mix_cat_pmt, base_indicadores_mass_mix_cat_pmt],
# outputs=[html19_1_pmt, html19_2_pmt, html19_3_pmt, exp19]
# )
# 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
# )
btn_mass_emb_pmt.click(
fn=procesar_lote_llm,
inputs=[iniciativas_pmt],
outputs=[output_ods_mass_mix_pmt, output_meta_mass_mix_pmt, output_indicadores_mass_mix_pmt, msj_procesamiento_mass_pmt, n_ini_state],
show_progress=True
)
txt_fecha_mass = gr.Textbox(value="Fecha de consulta: ", interactive=False, show_label=False)
output_ods_mass_mix_pmt.change(fn= actualizar_fecha, inputs=[], outputs=txt_fecha_mass)
with gr.Tab("MASIVA CON CONTEXTO", visible=False):
# 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, show_label=False)
# PESTAÑA 19: RESUMEN EN TAGS
# with gr.Tab("🏷️ 19. Resumen Tags"):
with gr.Row():
with gr.Row():
with gr.Column(scale=2):
html19_1 = gr.HTML(label="Resumen en Tags")
with gr.Column(scale=4):
# PESTAÑA 23: NUBE DE PALABRAS - PALOMA
# with gr.Tab("23. Nube Palabras"):
# file_output = gr.File()
file_output.change(leer_tabla,
file_output,
df_tx_iniciativas)
with gr.Row():
with gr.Column(scale=2):
img23 = gr.Image(label="Nube de Palabras", type="filepath")
with gr.Column(scale=1, visible=False):
exp23 = gr.Markdown()
# btn23 = gr.Button("🔄 Generar Nube de Palabras", variant="primary")
df_tx_iniciativas.change(
fn=tab_viz23,
inputs=[df_tx_iniciativas, gr.State('iniciativa')],
outputs=[img23, exp23]
)
# gr.Markdown("""
# ### 📝 Nota:
# Esta visualización analiza el texto de ODS/Metas/Indicadores y muestra
# las palabras más frecuentes en forma de paloma de paz.
# **Requiere:** `pip install wordcloud pillow`
# """)
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():
def update_choices(listado):
return gr.update(choices=listado)
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():
with gr.Column(scale=3):
with gr.Tab("TOP ODS"):
html_inicio_mass = gr.HTML(label="Resultados del análisis masivo - vista inicial")
with gr.Tab("Resultados por ODS - Metas"):
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)
output_ods_mass_mix_cat = gr.Dataframe(label="Resultados del análisis masivo ODS - Categorización", visible=False)
output_meta_mass_mix_cat = gr.Dataframe(label="Resultados del análisis masivo METAS - Categorización", visible=False)
output_indicadores_mass_mix_cat = gr.Dataframe(label="Resultados del análisis masivo INDICADORES - Categorización", 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", 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_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(visible=False):
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=[])
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]
)
## Actualilzar resultados de análisis cada vez que se ajusta el filtro de categoría, para mantener todo sincronizado
def actualizar_analisis_con_filtro_categoria(df, categoria, elementos, nivel):
if df is None:
return None
df_filtrado = df[df[categoria].isin(elementos)]
if nivel == 'ods':
umbral_pareto = slider_pareto_ods.value
elif nivel == 'meta':
umbral_pareto = slider_pareto_meta.value
elif nivel == 'indicador':
umbral_pareto = slider_pareto_indicador.value
else:
umbral_pareto = 0.8 # valor por defecto
resultado_actualizado, base_actualizada = analisis_global_con_pareto(df_filtrado, nivel, umbral_pareto, metodo='mixto', sim_prop=slider_prop_sim.value, rank_prop=slider_prop_rank.value)
return resultado_actualizado, base_actualizada
bttn_filtro_categoria.click(
fn=actualizar_analisis_con_filtro_categoria,
inputs=[output_ods_mass, categorias_mass, filtro_categoria, gr.State('ods')],
outputs=[output_ods_mass_mix_cat, base_ods_mass_mix_cat]
)
bttn_filtro_categoria.click(
fn=actualizar_analisis_con_filtro_categoria,
inputs=[output_meta_mass, categorias_mass, filtro_categoria, gr.State('meta')],
outputs=[output_meta_mass_mix_cat, base_meta_mass_mix_cat]
)
bttn_filtro_categoria.click(
fn=actualizar_analisis_con_filtro_categoria,
inputs=[output_indicadores_mass, categorias_mass, filtro_categoria, gr.State('indicador')],
outputs=[output_indicadores_mass_mix_cat, base_indicadores_mass_mix_cat]
)
## Actuaizar visualizaciones niveles cada vez que se ajusta el filtro de categoría, para mantener todo sincronizado
output_ods_mass_mix_cat.change(
fn=tab_inicio_mass,
inputs=[output_ods_mass_mix_cat,output_meta_mass_mix_cat,output_indicadores_mass_mix_cat],
outputs=[html_inicio_mass]
)
output_meta_mass_mix_cat.change(
fn=tab_inicio_mass,
inputs=[output_ods_mass_mix_cat,output_meta_mass_mix_cat,output_indicadores_mass_mix_cat],
outputs=[html_inicio_mass]
)
output_indicadores_mass_mix_cat.change(
fn=tab_inicio_mass,
inputs=[output_ods_mass_mix_cat,output_meta_mass_mix_cat,output_indicadores_mass_mix_cat],
outputs=[html_inicio_mass]
)
# 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 - UNFPA | Marzo 2026 | Desarrollado con Python, Gradio y gemini-2.5-flash-lite*
""")
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
)