Spaces:
Running
Running
actualización inicial y documentación ejercicio base
Browse files- docs/GUIA_APP_GRADIO.md +607 -0
- src/app.py +1254 -0
- src/app_graficas.ipynb +0 -0
- src/modelos_nlp_db.py +532 -0
- src/visualizaciones_ods.py +835 -0
docs/GUIA_APP_GRADIO.md
ADDED
|
@@ -0,0 +1,607 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🚀 Aplicación Web Gradio - Visualizaciones ODS
|
| 2 |
+
|
| 3 |
+
## 📋 Descripción
|
| 4 |
+
|
| 5 |
+
**App Gradio Interactiva** para explorar las 10 visualizaciones de análisis de similaridad ODS a través de una interfaz web profesional y amigable.
|
| 6 |
+
|
| 7 |
+
### ✨ Características Principales
|
| 8 |
+
|
| 9 |
+
- ✅ **Interfaz web interactiva** con diseño profesional
|
| 10 |
+
- ✅ **10 pestañas** con cada visualización completa
|
| 11 |
+
- ✅ **Explicaciones integradas** para público general
|
| 12 |
+
- ✅ **Visualizaciones dinámicas** (Plotly) y estáticas (PNG)
|
| 13 |
+
- ✅ **Estadísticas en tiempo real** con análisis detallado
|
| 14 |
+
- ✅ **Dashboard de inicio** con métricas clave
|
| 15 |
+
- ✅ **Responsive design** adaptable a cualquier pantalla
|
| 16 |
+
- ✅ **Sin necesidad de conocimientos técnicos** para usar
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## 🎯 ¿Para quién es esta aplicación?
|
| 21 |
+
|
| 22 |
+
### 👥 Público General
|
| 23 |
+
- Explorar visualizaciones de forma intuitiva
|
| 24 |
+
- Entender qué ODS son más relevantes
|
| 25 |
+
- Identificar indicadores clave sin código
|
| 26 |
+
|
| 27 |
+
### 👔 Ejecutivos y Tomadores de Decisión
|
| 28 |
+
- Presentaciones interactivas
|
| 29 |
+
- Análisis rápido de alineación ODS
|
| 30 |
+
- Métricas clave de un vistazo
|
| 31 |
+
|
| 32 |
+
### 🔬 Analistas e Investigadores
|
| 33 |
+
- Exploración profunda de datos
|
| 34 |
+
- Validación de correlaciones
|
| 35 |
+
- Exportación de visualizaciones
|
| 36 |
+
|
| 37 |
+
### 👨💻 Desarrolladores
|
| 38 |
+
- Referencia de implementación
|
| 39 |
+
- Base para personalización
|
| 40 |
+
- Integración con otros sistemas
|
| 41 |
+
|
| 42 |
+
---
|
| 43 |
+
|
| 44 |
+
## 🛠️ Instalación y Configuración
|
| 45 |
+
|
| 46 |
+
### Requisitos Previos
|
| 47 |
+
|
| 48 |
+
```bash
|
| 49 |
+
# Python 3.8 o superior
|
| 50 |
+
python --version
|
| 51 |
+
|
| 52 |
+
# Librerías necesarias
|
| 53 |
+
pip install pandas numpy matplotlib seaborn plotly gradio
|
| 54 |
+
```
|
| 55 |
+
|
| 56 |
+
### Instalación Completa
|
| 57 |
+
|
| 58 |
+
```bash
|
| 59 |
+
# 1. Instalar todas las dependencias
|
| 60 |
+
pip install pandas numpy matplotlib seaborn plotly gradio --break-system-packages
|
| 61 |
+
|
| 62 |
+
# 2. Verificar instalación
|
| 63 |
+
python -c "import gradio; print(f'Gradio {gradio.__version__} instalado correctamente')"
|
| 64 |
+
```
|
| 65 |
+
|
| 66 |
+
---
|
| 67 |
+
|
| 68 |
+
## 🚀 Ejecución de la Aplicación
|
| 69 |
+
|
| 70 |
+
### Método 1: Ejecución Directa
|
| 71 |
+
|
| 72 |
+
```bash
|
| 73 |
+
# Navegar al directorio
|
| 74 |
+
cd /ruta/donde/está/app_gradio_ods.py
|
| 75 |
+
|
| 76 |
+
# Ejecutar la aplicación
|
| 77 |
+
python app_gradio_ods.py
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
**Resultado esperado:**
|
| 81 |
+
```
|
| 82 |
+
======================================================================
|
| 83 |
+
INICIANDO APLICACIÓN GRADIO - VISUALIZACIONES ODS
|
| 84 |
+
======================================================================
|
| 85 |
+
|
| 86 |
+
✓ Datos cargados correctamente: 244 registros
|
| 87 |
+
✓ ODS únicos: 17
|
| 88 |
+
|
| 89 |
+
======================================================================
|
| 90 |
+
CREANDO APLICACIÓN...
|
| 91 |
+
======================================================================
|
| 92 |
+
|
| 93 |
+
✓ Aplicación creada exitosamente
|
| 94 |
+
|
| 95 |
+
======================================================================
|
| 96 |
+
INICIANDO SERVIDOR WEB...
|
| 97 |
+
======================================================================
|
| 98 |
+
|
| 99 |
+
🌐 La aplicación se abrirá en tu navegador automáticamente
|
| 100 |
+
📍 URL local: http://127.0.0.1:7860
|
| 101 |
+
🌍 URL pública: Se generará si share=True
|
| 102 |
+
|
| 103 |
+
💡 Presiona Ctrl+C para detener el servidor
|
| 104 |
+
|
| 105 |
+
Running on local URL: http://127.0.0.1:7860
|
| 106 |
+
```
|
| 107 |
+
|
| 108 |
+
### Método 2: Ejecución en Background
|
| 109 |
+
|
| 110 |
+
```bash
|
| 111 |
+
# Para mantener la app corriendo en segundo plano
|
| 112 |
+
nohup python app_gradio_ods.py > app.log 2>&1 &
|
| 113 |
+
|
| 114 |
+
# Ver los logs
|
| 115 |
+
tail -f app.log
|
| 116 |
+
|
| 117 |
+
# Detener la aplicación
|
| 118 |
+
pkill -f app_gradio_ods.py
|
| 119 |
+
```
|
| 120 |
+
|
| 121 |
+
### Método 3: Compartir Públicamente
|
| 122 |
+
|
| 123 |
+
Editar el archivo `app_gradio_ods.py` en la línea final:
|
| 124 |
+
|
| 125 |
+
```python
|
| 126 |
+
# Cambiar de:
|
| 127 |
+
app.launch(share=False)
|
| 128 |
+
|
| 129 |
+
# A:
|
| 130 |
+
app.launch(share=True)
|
| 131 |
+
```
|
| 132 |
+
|
| 133 |
+
Esto generará una URL pública accesible desde cualquier lugar por 72 horas.
|
| 134 |
+
|
| 135 |
+
---
|
| 136 |
+
|
| 137 |
+
## 📱 Uso de la Aplicación
|
| 138 |
+
|
| 139 |
+
### Pantalla de Inicio
|
| 140 |
+
|
| 141 |
+
Al abrir la aplicación, verás:
|
| 142 |
+
|
| 143 |
+
1. **Título principal** con descripción
|
| 144 |
+
2. **Estadísticas generales** en tarjeta destacada
|
| 145 |
+
3. **Top 3 ODS** más relevantes
|
| 146 |
+
4. **Top 5 indicadores** en tabla
|
| 147 |
+
5. **Guía de uso** paso a paso
|
| 148 |
+
|
| 149 |
+
### Navegación por Pestañas
|
| 150 |
+
|
| 151 |
+
#### 🏠 **Inicio**
|
| 152 |
+
- Dashboard con resumen ejecutivo
|
| 153 |
+
- Métricas clave del análisis
|
| 154 |
+
- Recomendaciones de exploración
|
| 155 |
+
|
| 156 |
+
#### 📦 **1. Box Plot**
|
| 157 |
+
- Distribución de similaridad por ODS
|
| 158 |
+
- Clic en "🔄 Generar Visualización"
|
| 159 |
+
- Explicación a la derecha
|
| 160 |
+
- Gráfico interactivo a la izquierda
|
| 161 |
+
|
| 162 |
+
#### 🔥 **2. Heatmap**
|
| 163 |
+
- Mapa de calor ODS × Ranking
|
| 164 |
+
- Imagen estática de alta resolución
|
| 165 |
+
- Interpretación de colores
|
| 166 |
+
|
| 167 |
+
#### 🌐 **3. Scatter 3D**
|
| 168 |
+
- Exploración tridimensional
|
| 169 |
+
- **Rotar**: Arrastra con el mouse
|
| 170 |
+
- **Zoom**: Scroll o rueda del mouse
|
| 171 |
+
- **Hover**: Ver detalles de cada punto
|
| 172 |
+
|
| 173 |
+
#### 🕸️ **4. Radar Chart**
|
| 174 |
+
- Perfil circular de ODS
|
| 175 |
+
- Dos polígonos superpuestos
|
| 176 |
+
- Ideal para presentaciones
|
| 177 |
+
|
| 178 |
+
#### ☀️ **5. Sunburst**
|
| 179 |
+
- Jerarquía ODS → Indicadores
|
| 180 |
+
- **Clic**: Zoom en segmento
|
| 181 |
+
- Tamaño proporcional a similaridad
|
| 182 |
+
|
| 183 |
+
#### 🏆 **6. Top Indicadores**
|
| 184 |
+
- Top 5 por cada ODS
|
| 185 |
+
- 17 paneles (uno por ODS)
|
| 186 |
+
- Scroll vertical para explorar todos
|
| 187 |
+
|
| 188 |
+
#### 🌊 **7. Stream Graph**
|
| 189 |
+
- Evolución de contribución
|
| 190 |
+
- Áreas apiladas al 100%
|
| 191 |
+
- Cambios de dominancia
|
| 192 |
+
|
| 193 |
+
#### 🎻 **8. Violin Plot**
|
| 194 |
+
- Distribución detallada
|
| 195 |
+
- Densidad de probabilidad
|
| 196 |
+
- Detecta patrones complejos
|
| 197 |
+
|
| 198 |
+
#### 📊 **9. Dashboard**
|
| 199 |
+
- 4 paneles integrados
|
| 200 |
+
- Vista 360° del análisis
|
| 201 |
+
- Validación del sistema
|
| 202 |
+
|
| 203 |
+
#### 🔀 **10. Matriz Transición**
|
| 204 |
+
- Presencia por cuartiles
|
| 205 |
+
- Consistencia de ODS
|
| 206 |
+
- Análisis de dominancia
|
| 207 |
+
|
| 208 |
+
#### 📈 **Estadísticas**
|
| 209 |
+
- Análisis estadístico completo
|
| 210 |
+
- Tablas detalladas por ODS
|
| 211 |
+
- Validación de correlaciones
|
| 212 |
+
|
| 213 |
+
---
|
| 214 |
+
|
| 215 |
+
## 🎨 Personalización
|
| 216 |
+
|
| 217 |
+
### Cambiar Colores
|
| 218 |
+
|
| 219 |
+
Editar en `app_gradio_ods.py`:
|
| 220 |
+
|
| 221 |
+
```python
|
| 222 |
+
# Línea ~35 - Tema de colores
|
| 223 |
+
theme=gr.themes.Soft(
|
| 224 |
+
primary_hue="blue", # Cambiar a: "green", "red", "purple", etc.
|
| 225 |
+
secondary_hue="cyan", # Cambiar a: "teal", "orange", "pink", etc.
|
| 226 |
+
neutral_hue="slate" # Cambiar a: "gray", "zinc", "stone", etc.
|
| 227 |
+
)
|
| 228 |
+
```
|
| 229 |
+
|
| 230 |
+
### Cambiar Puerto
|
| 231 |
+
|
| 232 |
+
```python
|
| 233 |
+
# Línea final - Configuración del servidor
|
| 234 |
+
app.launch(
|
| 235 |
+
server_port=7860, # Cambiar a: 8000, 8080, 3000, etc.
|
| 236 |
+
)
|
| 237 |
+
```
|
| 238 |
+
|
| 239 |
+
### Agregar Autenticación
|
| 240 |
+
|
| 241 |
+
```python
|
| 242 |
+
# Línea final - Añadir usuario/contraseña
|
| 243 |
+
app.launch(
|
| 244 |
+
auth=("usuario", "contraseña"), # Credenciales de acceso
|
| 245 |
+
auth_message="Ingrese sus credenciales para acceder"
|
| 246 |
+
)
|
| 247 |
+
```
|
| 248 |
+
|
| 249 |
+
### Modificar Explicaciones
|
| 250 |
+
|
| 251 |
+
Editar las funciones `tab_vizN()` en el archivo:
|
| 252 |
+
|
| 253 |
+
```python
|
| 254 |
+
def tab_viz1():
|
| 255 |
+
# ...
|
| 256 |
+
explicacion = """
|
| 257 |
+
## Tu título personalizado
|
| 258 |
+
|
| 259 |
+
Tu texto explicativo aquí...
|
| 260 |
+
"""
|
| 261 |
+
# ...
|
| 262 |
+
```
|
| 263 |
+
|
| 264 |
+
---
|
| 265 |
+
|
| 266 |
+
## 🔧 Solución de Problemas
|
| 267 |
+
|
| 268 |
+
### Problema 1: "ModuleNotFoundError: No module named 'gradio'"
|
| 269 |
+
|
| 270 |
+
**Solución:**
|
| 271 |
+
```bash
|
| 272 |
+
pip install gradio --break-system-packages
|
| 273 |
+
```
|
| 274 |
+
|
| 275 |
+
### Problema 2: "Address already in use"
|
| 276 |
+
|
| 277 |
+
**Causa:** El puerto 7860 ya está siendo usado
|
| 278 |
+
|
| 279 |
+
**Solución A - Cambiar puerto:**
|
| 280 |
+
```python
|
| 281 |
+
app.launch(server_port=8080) # Usar otro puerto
|
| 282 |
+
```
|
| 283 |
+
|
| 284 |
+
**Solución B - Cerrar proceso existente:**
|
| 285 |
+
```bash
|
| 286 |
+
lsof -ti:7860 | xargs kill -9
|
| 287 |
+
```
|
| 288 |
+
|
| 289 |
+
### Problema 3: "⚠️ Error: No se pudieron cargar los datos"
|
| 290 |
+
|
| 291 |
+
**Causa:** Ruta incorrecta del archivo de datos
|
| 292 |
+
|
| 293 |
+
**Solución:**
|
| 294 |
+
```python
|
| 295 |
+
# Editar línea ~49 en app_gradio_ods.py
|
| 296 |
+
RUTA_DATOS = '/ruta/correcta/a/indicadores_markdown.txt'
|
| 297 |
+
```
|
| 298 |
+
|
| 299 |
+
### Problema 4: Las visualizaciones no se cargan
|
| 300 |
+
|
| 301 |
+
**Causa:** Falta el archivo `visualizaciones_ods.py`
|
| 302 |
+
|
| 303 |
+
**Solución:**
|
| 304 |
+
```bash
|
| 305 |
+
# Asegurarse de tener ambos archivos en el mismo directorio
|
| 306 |
+
ls -la app_gradio_ods.py visualizaciones_ods.py
|
| 307 |
+
```
|
| 308 |
+
|
| 309 |
+
### Problema 5: Error de memoria con muchos datos
|
| 310 |
+
|
| 311 |
+
**Solución - Limitar datos:**
|
| 312 |
+
```python
|
| 313 |
+
# Editar en cargar_datos()
|
| 314 |
+
df = df.sample(n=1000) # Muestra de 1000 registros
|
| 315 |
+
```
|
| 316 |
+
|
| 317 |
+
### Problema 6: La app no se abre automáticamente
|
| 318 |
+
|
| 319 |
+
**Solución:**
|
| 320 |
+
```bash
|
| 321 |
+
# Abrir manualmente en navegador
|
| 322 |
+
google-chrome http://127.0.0.1:7860 # Chrome
|
| 323 |
+
firefox http://127.0.0.1:7860 # Firefox
|
| 324 |
+
open http://127.0.0.1:7860 # macOS
|
| 325 |
+
```
|
| 326 |
+
|
| 327 |
+
---
|
| 328 |
+
|
| 329 |
+
## 📊 Capturas de Pantalla
|
| 330 |
+
|
| 331 |
+
### Vista del Dashboard de Inicio
|
| 332 |
+
```
|
| 333 |
+
┌─────────────────────────────────────────────────────┐
|
| 334 |
+
│ 📊 Sistema de Visualización ODS │
|
| 335 |
+
│ Análisis de Similaridad de Indicadores │
|
| 336 |
+
│ │
|
| 337 |
+
│ 📈 Estadísticas Generales │
|
| 338 |
+
│ ┌──────────────────────────────────────────────┐ │
|
| 339 |
+
│ │ Total indicadores: 244 │ │
|
| 340 |
+
│ │ ODS cubiertos: 17/17 (100%) │ │
|
| 341 |
+
│ │ Similaridad promedio: 0.9050 │ │
|
| 342 |
+
│ │ Correlación: -0.9837 ✅ │ │
|
| 343 |
+
│ └──────────────────────────────────────────────┘ │
|
| 344 |
+
│ │
|
| 345 |
+
│ 🏆 Top 3 ODS Más Relevantes │
|
| 346 |
+
│ 1. ODS 17: 0.9223 │
|
| 347 |
+
│ 2. ODS 16: 0.9183 │
|
| 348 |
+
│ 3. ODS 9: 0.9199 │
|
| 349 |
+
└─────────────────────────────────────────────────────┘
|
| 350 |
+
```
|
| 351 |
+
|
| 352 |
+
### Vista de Visualización Individual
|
| 353 |
+
```
|
| 354 |
+
┌─────────────────────────────────────────────────────┐
|
| 355 |
+
│ [Pestaña] 📦 1. Box Plot │
|
| 356 |
+
├─────────────────────┬───────────────────────────────┤
|
| 357 |
+
│ │ ## 📦 Diagrama de Caja │
|
| 358 |
+
│ [Gráfico │ │
|
| 359 |
+
│ Interactivo │ ### ¿Qué muestra? │
|
| 360 |
+
│ Plotly] │ Esta visualización... │
|
| 361 |
+
│ │ │
|
| 362 |
+
│ │ ### ¿Cómo leerlo? │
|
| 363 |
+
│ │ - Línea central: Mediana │
|
| 364 |
+
│ [🔄 Generar] │ - Caja: Rango IQR │
|
| 365 |
+
└─────────────────────┴───────────────────────────────┘
|
| 366 |
+
```
|
| 367 |
+
|
| 368 |
+
---
|
| 369 |
+
|
| 370 |
+
## 🌐 Compartir y Colaborar
|
| 371 |
+
|
| 372 |
+
### Opción 1: Compartir en Red Local
|
| 373 |
+
|
| 374 |
+
```python
|
| 375 |
+
# Permite acceso desde otras computadoras en la misma red
|
| 376 |
+
app.launch(
|
| 377 |
+
server_name="0.0.0.0", # Ya está configurado por defecto
|
| 378 |
+
)
|
| 379 |
+
```
|
| 380 |
+
|
| 381 |
+
**Acceso desde otra computadora:**
|
| 382 |
+
```
|
| 383 |
+
http://[IP-DEL-SERVIDOR]:7860
|
| 384 |
+
```
|
| 385 |
+
|
| 386 |
+
### Opción 2: Compartir Públicamente (72 horas)
|
| 387 |
+
|
| 388 |
+
```python
|
| 389 |
+
app.launch(share=True)
|
| 390 |
+
```
|
| 391 |
+
|
| 392 |
+
**Resultado:**
|
| 393 |
+
```
|
| 394 |
+
Running on local URL: http://127.0.0.1:7860
|
| 395 |
+
Running on public URL: https://abc123xyz.gradio.live
|
| 396 |
+
|
| 397 |
+
This share link expires in 72 hours.
|
| 398 |
+
```
|
| 399 |
+
|
| 400 |
+
### Opción 3: Deployar en la Nube
|
| 401 |
+
|
| 402 |
+
#### **Hugging Face Spaces** (Gratis)
|
| 403 |
+
|
| 404 |
+
1. Crear cuenta en huggingface.co
|
| 405 |
+
2. Crear nuevo Space
|
| 406 |
+
3. Subir archivos:
|
| 407 |
+
- `app_gradio_ods.py`
|
| 408 |
+
- `visualizaciones_ods.py`
|
| 409 |
+
- `indicadores_markdown.txt`
|
| 410 |
+
- `requirements.txt`
|
| 411 |
+
|
| 412 |
+
**requirements.txt:**
|
| 413 |
+
```
|
| 414 |
+
gradio==5.49.1
|
| 415 |
+
pandas
|
| 416 |
+
numpy
|
| 417 |
+
matplotlib
|
| 418 |
+
seaborn
|
| 419 |
+
plotly
|
| 420 |
+
```
|
| 421 |
+
|
| 422 |
+
4. Tu app estará en: `https://huggingface.co/spaces/[tu-usuario]/[nombre-app]`
|
| 423 |
+
|
| 424 |
+
---
|
| 425 |
+
|
| 426 |
+
## 📚 Estructura de Archivos
|
| 427 |
+
|
| 428 |
+
```
|
| 429 |
+
proyecto/
|
| 430 |
+
│
|
| 431 |
+
├── app_gradio_ods.py # ⭐ Aplicación principal
|
| 432 |
+
├── visualizaciones_ods.py # Funciones de visualización
|
| 433 |
+
├── indicadores_markdown.txt # Datos de entrada
|
| 434 |
+
│
|
| 435 |
+
├── GUIA_APP_GRADIO.md # 📖 Esta guía
|
| 436 |
+
├── README.md # Índice general
|
| 437 |
+
│
|
| 438 |
+
└── outputs/ # Visualizaciones generadas
|
| 439 |
+
├── viz1_boxplot_ods.html
|
| 440 |
+
├── viz2_heatmap.png
|
| 441 |
+
└── ...
|
| 442 |
+
```
|
| 443 |
+
|
| 444 |
+
---
|
| 445 |
+
|
| 446 |
+
## 🎓 Casos de Uso Avanzados
|
| 447 |
+
|
| 448 |
+
### Caso 1: Integrar con otros datos
|
| 449 |
+
|
| 450 |
+
```python
|
| 451 |
+
# En app_gradio_ods.py, añadir nuevo tab
|
| 452 |
+
with gr.Tab("📁 Cargar Datos"):
|
| 453 |
+
file_upload = gr.File(label="Subir archivo CSV/TXT")
|
| 454 |
+
btn_load = gr.Button("Cargar")
|
| 455 |
+
|
| 456 |
+
def cargar_nuevos_datos(file):
|
| 457 |
+
df = pd.read_csv(file.name)
|
| 458 |
+
# Procesar y visualizar
|
| 459 |
+
return "✓ Datos cargados"
|
| 460 |
+
|
| 461 |
+
btn_load.click(cargar_nuevos_datos, file_upload, output_text)
|
| 462 |
+
```
|
| 463 |
+
|
| 464 |
+
### Caso 2: Exportar visualizaciones
|
| 465 |
+
|
| 466 |
+
```python
|
| 467 |
+
# Añadir botones de descarga
|
| 468 |
+
with gr.Row():
|
| 469 |
+
btn_download_html = gr.Button("📥 Descargar HTML")
|
| 470 |
+
btn_download_png = gr.Button("📥 Descargar PNG")
|
| 471 |
+
|
| 472 |
+
def exportar_viz(fig, formato):
|
| 473 |
+
if formato == "html":
|
| 474 |
+
fig.write_html("visualizacion.html")
|
| 475 |
+
return "visualizacion.html"
|
| 476 |
+
else:
|
| 477 |
+
fig.write_image("visualizacion.png")
|
| 478 |
+
return "visualizacion.png"
|
| 479 |
+
```
|
| 480 |
+
|
| 481 |
+
### Caso 3: Filtros dinámicos
|
| 482 |
+
|
| 483 |
+
```python
|
| 484 |
+
# Añadir controles interactivos
|
| 485 |
+
with gr.Row():
|
| 486 |
+
ods_select = gr.Dropdown(
|
| 487 |
+
choices=list(range(1, 18)),
|
| 488 |
+
label="Filtrar por ODS",
|
| 489 |
+
multiselect=True
|
| 490 |
+
)
|
| 491 |
+
slider_sim = gr.Slider(
|
| 492 |
+
minimum=0.85,
|
| 493 |
+
maximum=0.95,
|
| 494 |
+
value=0.90,
|
| 495 |
+
label="Umbral de similaridad"
|
| 496 |
+
)
|
| 497 |
+
|
| 498 |
+
def filtrar_datos(ods_list, umbral):
|
| 499 |
+
df_filtrado = df_global[
|
| 500 |
+
(df_global['ods_id'].isin(ods_list)) &
|
| 501 |
+
(df_global['similaridad_cos'] >= umbral)
|
| 502 |
+
]
|
| 503 |
+
return generar_visualizacion(df_filtrado)
|
| 504 |
+
```
|
| 505 |
+
|
| 506 |
+
---
|
| 507 |
+
|
| 508 |
+
## 🔐 Seguridad y Buenas Prácticas
|
| 509 |
+
|
| 510 |
+
### Recomendaciones
|
| 511 |
+
|
| 512 |
+
1. ✅ **No exponer datos sensibles** en la app pública
|
| 513 |
+
2. ✅ **Usar autenticación** si compartes públicamente
|
| 514 |
+
3. ✅ **Limitar acceso** a redes confiables
|
| 515 |
+
4. ✅ **Validar inputs** del usuario
|
| 516 |
+
5. ✅ **Mantener actualizado** Gradio y dependencias
|
| 517 |
+
|
| 518 |
+
### Autenticación Básica
|
| 519 |
+
|
| 520 |
+
```python
|
| 521 |
+
app.launch(
|
| 522 |
+
auth=[("usuario1", "pass1"), ("usuario2", "pass2")],
|
| 523 |
+
auth_message="Acceso restringido - Ingrese credenciales"
|
| 524 |
+
)
|
| 525 |
+
```
|
| 526 |
+
|
| 527 |
+
### Variables de Entorno
|
| 528 |
+
|
| 529 |
+
```python
|
| 530 |
+
import os
|
| 531 |
+
|
| 532 |
+
# Usar variables de entorno para credenciales
|
| 533 |
+
usuario = os.getenv("APP_USERNAME", "admin")
|
| 534 |
+
password = os.getenv("APP_PASSWORD", "secret")
|
| 535 |
+
|
| 536 |
+
app.launch(auth=(usuario, password))
|
| 537 |
+
```
|
| 538 |
+
|
| 539 |
+
---
|
| 540 |
+
|
| 541 |
+
## 📞 Soporte y Recursos
|
| 542 |
+
|
| 543 |
+
### Documentación Oficial
|
| 544 |
+
- **Gradio**: https://www.gradio.app/docs
|
| 545 |
+
- **Plotly**: https://plotly.com/python/
|
| 546 |
+
- **Pandas**: https://pandas.pydata.org/docs/
|
| 547 |
+
|
| 548 |
+
### Comunidad
|
| 549 |
+
- **Gradio Discord**: https://discord.gg/gradio
|
| 550 |
+
- **Hugging Face Forums**: https://discuss.huggingface.co/
|
| 551 |
+
|
| 552 |
+
### Archivos Relacionados
|
| 553 |
+
- `README.md` - Índice general del proyecto
|
| 554 |
+
- `GUIA_VISUALIZACIONES_ODS.md` - Explicación de visualizaciones
|
| 555 |
+
- `DOCUMENTACION_TECNICA_CODIGO.md` - Código técnico explicado
|
| 556 |
+
- `GUIA_USO_RAPIDO.md` - Casos prácticos
|
| 557 |
+
|
| 558 |
+
---
|
| 559 |
+
|
| 560 |
+
## 🎉 Características Futuras (Roadmap)
|
| 561 |
+
|
| 562 |
+
### En Desarrollo
|
| 563 |
+
- [ ] Comparación de múltiples iniciativas
|
| 564 |
+
- [ ] Exportación de reportes en PDF
|
| 565 |
+
- [ ] Análisis de series temporales
|
| 566 |
+
- [ ] Integración con APIs de ODS oficiales
|
| 567 |
+
|
| 568 |
+
### Planeado
|
| 569 |
+
- [ ] Modo oscuro / claro
|
| 570 |
+
- [ ] Internacionalización (ES, EN, FR)
|
| 571 |
+
- [ ] Chat con IA para interpretación
|
| 572 |
+
- [ ] Dashboard personalizable
|
| 573 |
+
|
| 574 |
+
---
|
| 575 |
+
|
| 576 |
+
## 📄 Licencia
|
| 577 |
+
|
| 578 |
+
Este proyecto es de código abierto. Consulta el archivo LICENSE para más detalles.
|
| 579 |
+
|
| 580 |
+
---
|
| 581 |
+
|
| 582 |
+
## 🙏 Agradecimientos
|
| 583 |
+
|
| 584 |
+
Desarrollado con:
|
| 585 |
+
- **Python** - Lenguaje de programación
|
| 586 |
+
- **Gradio** - Framework de aplicaciones web
|
| 587 |
+
- **Plotly** - Visualizaciones interactivas
|
| 588 |
+
- **Pandas** - Análisis de datos
|
| 589 |
+
- **Matplotlib/Seaborn** - Gráficos estáticos
|
| 590 |
+
|
| 591 |
+
---
|
| 592 |
+
|
| 593 |
+
## 📬 Contacto
|
| 594 |
+
|
| 595 |
+
¿Preguntas? ¿Sugerencias? ¿Encontraste un bug?
|
| 596 |
+
|
| 597 |
+
- 📧 Email: [tu-email@ejemplo.com]
|
| 598 |
+
- 💬 Issues: [URL del repositorio]
|
| 599 |
+
- 📖 Wiki: [URL de la wiki]
|
| 600 |
+
|
| 601 |
+
---
|
| 602 |
+
|
| 603 |
+
**¡Disfruta explorando las visualizaciones ODS! 📊🌍✨**
|
| 604 |
+
|
| 605 |
+
---
|
| 606 |
+
|
| 607 |
+
*Última actualización: Noviembre 2025*
|
src/app.py
ADDED
|
@@ -0,0 +1,1254 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
APLICACIÓN WEB GRADIO - VISUALIZACIONES ODS
|
| 3 |
+
============================================
|
| 4 |
+
|
| 5 |
+
Aplicación interactiva que permite explorar las 10 visualizaciones
|
| 6 |
+
de análisis de similaridad ODS a través de una interfaz web amigable.
|
| 7 |
+
|
| 8 |
+
Características:
|
| 9 |
+
- Interfaz con pestañas para cada visualización
|
| 10 |
+
- Explicaciones integradas para público general
|
| 11 |
+
- Visualizaciones interactivas (HTML) y estáticas (PNG)
|
| 12 |
+
- Estadísticas en tiempo real
|
| 13 |
+
- Diseño responsivo y profesional
|
| 14 |
+
|
| 15 |
+
Autor: Sistema de Visualización ODS
|
| 16 |
+
Fecha: Noviembre 2025
|
| 17 |
+
"""
|
| 18 |
+
|
| 19 |
+
import gradio as gr
|
| 20 |
+
import pandas as pd
|
| 21 |
+
import numpy as np
|
| 22 |
+
from pathlib import Path
|
| 23 |
+
import plotly.graph_objects as go
|
| 24 |
+
import plotly.express as px
|
| 25 |
+
from plotly.subplots import make_subplots
|
| 26 |
+
import matplotlib.pyplot as plt
|
| 27 |
+
import seaborn as sns
|
| 28 |
+
from modelos_nlp_db import search
|
| 29 |
+
|
| 30 |
+
# Importar funciones de visualización
|
| 31 |
+
import sys
|
| 32 |
+
# sys.path.insert(0, '/home/claude')
|
| 33 |
+
from visualizaciones_ods import (
|
| 34 |
+
cargar_datos,
|
| 35 |
+
viz_1_distribucion_por_ods,
|
| 36 |
+
viz_2_heatmap_ods_ranking,
|
| 37 |
+
viz_3_scatter_3d_interactivo,
|
| 38 |
+
viz_4_radar_chart_ods,
|
| 39 |
+
viz_5_sunburst_jerarquia,
|
| 40 |
+
viz_6_top_indicadores_por_ods,
|
| 41 |
+
viz_7_streamgraph_similaridad,
|
| 42 |
+
viz_8_violin_plot_ods,
|
| 43 |
+
viz_9_dashboard_metricas,
|
| 44 |
+
viz_10_matriz_transicion,
|
| 45 |
+
analisis_estadistico
|
| 46 |
+
)
|
| 47 |
+
|
| 48 |
+
# ============================================================================
|
| 49 |
+
# CONFIGURACIÓN GLOBAL
|
| 50 |
+
# ============================================================================
|
| 51 |
+
import os
|
| 52 |
+
import base64
|
| 53 |
+
|
| 54 |
+
levels = ['ODS_ID','META_ID','INDICADOR_ID']
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
def convertir_logo_a_base64(logo_path):
|
| 58 |
+
"""Convierte un logo a base64 para incrustar en HTML"""
|
| 59 |
+
# try:
|
| 60 |
+
# rutas_posibles = [
|
| 61 |
+
# logo_path,
|
| 62 |
+
# os.path.join(os.path.dirname(__file__), logo_path),
|
| 63 |
+
# os.path.join('/mnt/user-data/outputs', logo_path),
|
| 64 |
+
# ]
|
| 65 |
+
|
| 66 |
+
# for ruta in rutas_posibles:
|
| 67 |
+
# if os.path.exists(ruta):
|
| 68 |
+
# with open(ruta, "rb") as image_file:
|
| 69 |
+
# encoded = base64.b64encode(image_file.read()).decode()
|
| 70 |
+
# return f"data:image/png;base64,{encoded}"
|
| 71 |
+
|
| 72 |
+
# print(f"⚠️ Logo no encontrado: {logo_path}")
|
| 73 |
+
# return ""
|
| 74 |
+
# except Exception as e:
|
| 75 |
+
# print(f"⚠️ Error al cargar logo: {e}")
|
| 76 |
+
# return ""
|
| 77 |
+
ruta = '/content/drive/MyDrive/Compartida/06_Desarrollo de la herramienta IA/01_MPTF /archivos_trabajo/app_visualizaciones/inputs/img'
|
| 78 |
+
with open(f'{ruta}/{logo_path}', "rb") as image_file:
|
| 79 |
+
encoded = base64.b64encode(image_file.read()).decode()
|
| 80 |
+
return f"data:image/png;base64,{encoded}"
|
| 81 |
+
|
| 82 |
+
|
| 83 |
+
|
| 84 |
+
# Cargar logos una sola vez al iniciar
|
| 85 |
+
print("Cargando logos institucionales...")
|
| 86 |
+
LOGO_GOBIERNO = convertir_logo_a_base64("GOBIERNO-DE-COLOMBIA_HORIZONTAL.png")
|
| 87 |
+
LOGO_FONDO = convertir_logo_a_base64("LOGO MPTF (ESP).png")
|
| 88 |
+
|
| 89 |
+
|
| 90 |
+
if LOGO_GOBIERNO and LOGO_FONDO:
|
| 91 |
+
print("✅ Logos cargados correctamente")
|
| 92 |
+
else:
|
| 93 |
+
print("⚠️ Algunos logos no se pudieron cargar")
|
| 94 |
+
|
| 95 |
+
dict_logos = {
|
| 96 |
+
'gobierno': convertir_logo_a_base64("GOBIERNO-DE-COLOMBIA_HORIZONTAL.png"),
|
| 97 |
+
'fondo_un': convertir_logo_a_base64("LOGO MPTF (ESP).png"),
|
| 98 |
+
'ods_1': convertir_logo_a_base64("S-WEB-Goal-01.png"),
|
| 99 |
+
'ods_2': convertir_logo_a_base64("S-WEB-Goal-02.png"),
|
| 100 |
+
'ods_3': convertir_logo_a_base64("S-WEB-Goal-03.png"),
|
| 101 |
+
'ods_4': convertir_logo_a_base64("S-WEB-Goal-04.png"),
|
| 102 |
+
'ods_5': convertir_logo_a_base64("S-WEB-Goal-05.png"),
|
| 103 |
+
'ods_6': convertir_logo_a_base64("S-WEB-Goal-06.png"),
|
| 104 |
+
'ods_7': convertir_logo_a_base64("S-WEB-Goal-07.png"),
|
| 105 |
+
'ods_8': convertir_logo_a_base64("S-WEB-Goal-08.png"),
|
| 106 |
+
'ods_9': convertir_logo_a_base64("S-WEB-Goal-09.png"),
|
| 107 |
+
'ods_10': convertir_logo_a_base64("S-WEB-Goal-10.png"),
|
| 108 |
+
'ods_11': convertir_logo_a_base64("S-WEB-Goal-11.png"),
|
| 109 |
+
'ods_12': convertir_logo_a_base64("S-WEB-Goal-12.png"),
|
| 110 |
+
'ods_13': convertir_logo_a_base64("S-WEB-Goal-13.png"),
|
| 111 |
+
'ods_14': convertir_logo_a_base64("S-WEB-Goal-14.png"),
|
| 112 |
+
'ods_15': convertir_logo_a_base64("S-WEB-Goal-15.png"),
|
| 113 |
+
'ods_16': convertir_logo_a_base64("S-WEB-Goal-16.png"),
|
| 114 |
+
'ods_17': convertir_logo_a_base64("S-WEB-Goal-17.png"),
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
# Ruta al archivo de datos
|
| 118 |
+
# # RUTA_DATOS = '/mnt/user-data/uploads/indicadores_markdown.txt'
|
| 119 |
+
# RUTA_DATOS = '/content/drive/MyDrive/Compartida/06_Desarrollo de la herramienta IA/01_MPTF /archivos_trabajo/app_visualizaciones/indicadores_markdown.txt'
|
| 120 |
+
|
| 121 |
+
# # Cargar datos globalmente para toda la app
|
| 122 |
+
# try:
|
| 123 |
+
# df_global = cargar_datos(RUTA_DATOS)
|
| 124 |
+
DATOS_CARGADOS = True
|
| 125 |
+
# print(f"✓ Datos cargados: {len(df_global)} registros")
|
| 126 |
+
# except Exception as e:
|
| 127 |
+
# df_global = None
|
| 128 |
+
# DATOS_CARGADOS = False
|
| 129 |
+
# print(f"✗ Error al cargar datos: {e}")
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
|
| 133 |
+
# Estilos CSS personalizados
|
| 134 |
+
CUSTOM_CSS = """
|
| 135 |
+
.gradio-container {
|
| 136 |
+
font-family: 'Arial', sans-serif;
|
| 137 |
+
}
|
| 138 |
+
.explanation-box {
|
| 139 |
+
background-color: #E8F4F8;
|
| 140 |
+
padding: 20px;
|
| 141 |
+
border-radius: 10px;
|
| 142 |
+
border-left: 5px solid #2E5090;
|
| 143 |
+
margin: 10px 0;
|
| 144 |
+
}
|
| 145 |
+
.stats-box {
|
| 146 |
+
background-color: #FFF9E6;
|
| 147 |
+
padding: 15px;
|
| 148 |
+
border-radius: 8px;
|
| 149 |
+
border: 2px solid #FFD700;
|
| 150 |
+
margin: 10px 0;
|
| 151 |
+
}
|
| 152 |
+
.important-box {
|
| 153 |
+
background-color: #FFE6E6;
|
| 154 |
+
padding: 15px;
|
| 155 |
+
border-radius: 8px;
|
| 156 |
+
border-left: 5px solid #C00000;
|
| 157 |
+
margin: 10px 0;
|
| 158 |
+
}
|
| 159 |
+
h1, h2, h3 {
|
| 160 |
+
color: #2E5090;
|
| 161 |
+
}
|
| 162 |
+
.tab-nav button {
|
| 163 |
+
font-size: 16px;
|
| 164 |
+
padding: 10px 20px;
|
| 165 |
+
}
|
| 166 |
+
|
| 167 |
+
/* ESTILOS PARA HEADER CON LOGOS INSTITUCIONALES */
|
| 168 |
+
.header-institucional {
|
| 169 |
+
display: flex;
|
| 170 |
+
justify-content: space-between;
|
| 171 |
+
align-items: center;
|
| 172 |
+
padding: 20px 40px;
|
| 173 |
+
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 50%, #f8f9fa 100%);
|
| 174 |
+
border-bottom: 4px solid #003DA5;
|
| 175 |
+
margin-bottom: 25px;
|
| 176 |
+
box-shadow: 0 3px 10px rgba(0,0,0,0.08);
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
.logo-institucional {
|
| 180 |
+
height: 40px;
|
| 181 |
+
width: auto;
|
| 182 |
+
object-fit: contain;
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
.titulo-institucional {
|
| 186 |
+
flex: 1;
|
| 187 |
+
text-align: center;
|
| 188 |
+
padding: 0 30px;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
.titulo-institucional h1 {
|
| 192 |
+
margin: 0;
|
| 193 |
+
color: #003DA5 !important;
|
| 194 |
+
font-size: 28px;
|
| 195 |
+
font-weight: 700;
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
.logo-ods-tbl {
|
| 199 |
+
height: 60px;
|
| 200 |
+
width: auto;
|
| 201 |
+
object-fit: contain;
|
| 202 |
+
}
|
| 203 |
+
|
| 204 |
+
@media (max-width: 768px) {
|
| 205 |
+
.header-institucional {
|
| 206 |
+
padding: 15px 20px;
|
| 207 |
+
flex-direction: column;
|
| 208 |
+
gap: 15px;
|
| 209 |
+
}
|
| 210 |
+
.logo-institucional {
|
| 211 |
+
height: 50px;
|
| 212 |
+
}
|
| 213 |
+
}
|
| 214 |
+
"""
|
| 215 |
+
|
| 216 |
+
# ============================================================================
|
| 217 |
+
# FUNCIONES DE CONVERSIÓN DE FIGURAS
|
| 218 |
+
# ============================================================================
|
| 219 |
+
|
| 220 |
+
def plotly_to_html(fig):
|
| 221 |
+
"""Convierte figura Plotly a HTML para mostrar en Gradio"""
|
| 222 |
+
return fig.to_html(include_plotlyjs='cdn', full_html=False)
|
| 223 |
+
|
| 224 |
+
def matplotlib_to_file(fig, filename):
|
| 225 |
+
"""Convierte figura Matplotlib a archivo temporal"""
|
| 226 |
+
import tempfile
|
| 227 |
+
import os
|
| 228 |
+
|
| 229 |
+
# Crear directorio temporal si no existe
|
| 230 |
+
temp_dir = tempfile.gettempdir()
|
| 231 |
+
filepath = os.path.join(temp_dir, filename)
|
| 232 |
+
|
| 233 |
+
# Guardar la figura
|
| 234 |
+
fig.savefig(filepath, format='png', dpi=150, bbox_inches='tight')
|
| 235 |
+
plt.close(fig)
|
| 236 |
+
|
| 237 |
+
return filepath
|
| 238 |
+
|
| 239 |
+
# ============================================================================
|
| 240 |
+
# FUNCIONES PARA CADA PESTAÑA
|
| 241 |
+
# ============================================================================
|
| 242 |
+
|
| 243 |
+
def tab_inicio(df_ods, df_metas, df_indicador):
|
| 244 |
+
# def tab_inicio():
|
| 245 |
+
"""Pestaña de inicio con resumen general"""
|
| 246 |
+
if not DATOS_CARGADOS:
|
| 247 |
+
return "⚠️ Error: No se pudieron cargar los datos."
|
| 248 |
+
|
| 249 |
+
# Estadísticas básicas
|
| 250 |
+
|
| 251 |
+
total_ods = df_ods['ODS_ID'].nunique()
|
| 252 |
+
total_metas = df_metas['META_ID'].nunique()
|
| 253 |
+
total_indicadores = df_indicador['INDICADOR_ID'].nunique()
|
| 254 |
+
sim_media = df_ods['ods_similaridad_cos_normalized'].mean()
|
| 255 |
+
sim_max = df_ods['ods_similaridad_cos_normalized'].max()
|
| 256 |
+
sim_min = df_ods['ods_similaridad_cos_normalized'].min()
|
| 257 |
+
correlacion = df_ods['ods_rank'].corr(df_ods['ods_similaridad_cos_normalized'])
|
| 258 |
+
|
| 259 |
+
# Top 4 ODS
|
| 260 |
+
top_ods = df_ods.nsmallest(4, 'ods_rank')[['ODS_ID','ods_rank','OBJETIVO','ods_similaridad_cos_normalized']]
|
| 261 |
+
top_ods['logo_id'] = top_ods['ODS_ID'].apply(lambda _: f"ods_{_}")
|
| 262 |
+
# top_ods = df_ods.groupby('ODS_ID').agg({
|
| 263 |
+
# 'ods_similaridad_cos_normalized': 'mean'
|
| 264 |
+
# }).sort_values('ods_similaridad_cos_normalized', ascending=False).head(3)[['ods_similaridad_cos_normalized']]
|
| 265 |
+
|
| 266 |
+
# Top ODS referencia
|
| 267 |
+
ods_ref = top_ods.ODS_ID
|
| 268 |
+
|
| 269 |
+
# Top 3 METAS
|
| 270 |
+
|
| 271 |
+
top_metas = pd.DataFrame()
|
| 272 |
+
for i in ods_ref:
|
| 273 |
+
top_metas_lcl = df_metas[df_metas.ODS_ID == i]
|
| 274 |
+
top_metas_lcl = top_metas_lcl.nsmallest(2, 'meta_rank')[['META_ID','meta_rank','META','meta_similaridad_cos_normalized', 'ODS_ID']]
|
| 275 |
+
top_metas = pd.concat([top_metas, top_metas_lcl], axis=0)
|
| 276 |
+
top_metas['logo_id'] = top_metas['ODS_ID'].apply(lambda _: f"ods_{_}")
|
| 277 |
+
# top_metas = df_metas.groupby('META_ID').agg({
|
| 278 |
+
# 'meta_similaridad_cos_normalized': 'mean'
|
| 279 |
+
# }).sort_values('meta_similaridad_cos_normalized', ascending=False).head(5)[['META_ID','META','meta_similaridad_cos_normalized']]
|
| 280 |
+
|
| 281 |
+
# Top 5 indicadores
|
| 282 |
+
top_indicador = pd.DataFrame()
|
| 283 |
+
for i in ods_ref:
|
| 284 |
+
top_indicador_lcl = df_indicador[df_indicador.ODS_ID == i]
|
| 285 |
+
top_indicador_lcl = top_indicador_lcl.nsmallest(2, 'indicador_rank')[['INDICADOR_ID', 'indicador_rank', 'INDICADOR', 'indicador_similaridad_cos_normalized', 'ODS_ID']]
|
| 286 |
+
top_indicador = pd.concat([top_indicador, top_indicador_lcl], axis=0)
|
| 287 |
+
top_indicador['logo_id'] = top_indicador['ODS_ID'].apply(lambda _: f"ods_{_}")
|
| 288 |
+
|
| 289 |
+
|
| 290 |
+
html = f"""
|
| 291 |
+
<div style="font-family: Arial, sans-serif; padding: 20px;">
|
| 292 |
+
<h1 style="color: #2E5090; text-align: center;">
|
| 293 |
+
📊 Sistema de Visualización ODS
|
| 294 |
+
</h1>
|
| 295 |
+
<h2 style="color: #4472C4; text-align: center;">
|
| 296 |
+
Análisis de Similaridad de Indicadores
|
| 297 |
+
</h2>
|
| 298 |
+
|
| 299 |
+
<div class="stats-box" style="background-color: #E8F4F8; padding: 20px; border-radius: 10px; margin: 20px 0;">
|
| 300 |
+
<h3 style="color: #2E5090;">📈 Estadísticas Generales</h3>
|
| 301 |
+
<table style="width: 100%; border-collapse: collapse;">
|
| 302 |
+
<tr>
|
| 303 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Total de indicadores analizados:</strong></td>
|
| 304 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right;">{total_indicadores}</td>
|
| 305 |
+
</tr>
|
| 306 |
+
<tr>
|
| 307 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>ODS cubiertos:</strong></td>
|
| 308 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right;">{total_ods}/17 (100%)</td>
|
| 309 |
+
</tr>
|
| 310 |
+
<tr>
|
| 311 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Similaridad promedio:</strong></td>
|
| 312 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right;">{sim_media:.4f}</td>
|
| 313 |
+
</tr>
|
| 314 |
+
<tr>
|
| 315 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Rango de similaridad:</strong></td>
|
| 316 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right;">{sim_min:.4f} - {sim_max:.4f}</td>
|
| 317 |
+
</tr>
|
| 318 |
+
<tr>
|
| 319 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Correlación Rank-Similaridad:</strong></td>
|
| 320 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right; color: {'green' if correlacion < -0.7 else 'orange'};">
|
| 321 |
+
{correlacion:.4f} {'✅' if correlacion < -0.7 else '⚠️'}
|
| 322 |
+
</td>
|
| 323 |
+
</tr>
|
| 324 |
+
</table>
|
| 325 |
+
</div>
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
<div class="important-box" style="background-color: #E6F7E6; padding: 20px; border-radius: 10px; margin: 20px 0; border-left: 5px solid #28A745;">
|
| 330 |
+
<h3 style="color: #28A745;">🏆 Top 4 ODS Más Relevantes</h3>
|
| 331 |
+
<table style="width: 100%; border-collapse: collapse; margin-top: 10px;">
|
| 332 |
+
<thead>
|
| 333 |
+
<tr style="background-color: #FFD700;">
|
| 334 |
+
<th style="padding: 10px; text-align: left;">Rank</th>
|
| 335 |
+
<th style="padding: 10px; text-align: left;"> </th>
|
| 336 |
+
<th style="padding: 10px; text-align: left;">ID</th>
|
| 337 |
+
<th style="padding: 10px; text-align: left;">ODS</th>
|
| 338 |
+
<!-- <th style="padding: 10px; text-align: center;">ODS</th> -->
|
| 339 |
+
<th style="padding: 10px; text-align: right;">Similaridad</th>
|
| 340 |
+
</tr>
|
| 341 |
+
</thead>
|
| 342 |
+
<tbody>
|
| 343 |
+
{''.join([f'''<tr>
|
| 344 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;">{row['ods_rank']}</td>
|
| 345 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><img src="{dict_logos[row['logo_id']]}"
|
| 346 |
+
alt="ODS {row['ODS_ID']}"
|
| 347 |
+
class="logo-ods-tbl"></td>
|
| 348 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>{row['ODS_ID']}</strong></td>
|
| 349 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: center;">{row['OBJETIVO']}</td>
|
| 350 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right;">{row['ods_similaridad_cos_normalized']:.4f}</td>
|
| 351 |
+
</tr>''' for _, row in top_ods.iterrows()])}
|
| 352 |
+
</tbody>
|
| 353 |
+
</table>
|
| 354 |
+
</div>
|
| 355 |
+
|
| 356 |
+
<div class="important-box" style="background-color: #E6F7E6; padding: 20px; border-radius: 10px; margin: 20px 0; border-left: 5px solid #FFD700;">
|
| 357 |
+
<h3 style="color: #FF8C00;">🎯 Top 5 Metas Más Relevantes</h3>
|
| 358 |
+
<table style="width: 100%; border-collapse: collapse; margin-top: 10px;">
|
| 359 |
+
<thead>
|
| 360 |
+
<tr style="background-color: #FFD700;">
|
| 361 |
+
<th style="padding: 10px; text-align: left;">Rank</th>
|
| 362 |
+
<th style="padding: 10px; text-align: left;"> </th>
|
| 363 |
+
<th style="padding: 10px; text-align: left;">ID </th>
|
| 364 |
+
<th style="padding: 10px; text-align: left;">Meta</th>
|
| 365 |
+
<!-- <th style="padding: 10px; text-align: center;">ODS</th> -->
|
| 366 |
+
<th style="padding: 10px; text-align: right;">Similaridad</th>
|
| 367 |
+
</tr>
|
| 368 |
+
</thead>
|
| 369 |
+
<tbody>
|
| 370 |
+
{''.join([f'''<tr>
|
| 371 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;">{row['meta_rank']}</td>
|
| 372 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><img src="{dict_logos[row['logo_id']]}"
|
| 373 |
+
alt="ODS {row['ODS_ID']}"
|
| 374 |
+
class="logo-ods-tbl"></td>
|
| 375 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>{row['META_ID']}</strong></td>
|
| 376 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: center;">{row['META']}</td>
|
| 377 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right;">{row['meta_similaridad_cos_normalized']:.4f}</td>
|
| 378 |
+
</tr>''' for _, row in top_metas.iterrows()])}
|
| 379 |
+
</tbody>
|
| 380 |
+
</table>
|
| 381 |
+
</div>
|
| 382 |
+
|
| 383 |
+
<div class="important-box" style="background-color: #FFF9E6; padding: 20px; border-radius: 10px; margin: 20px 0; border-left: 5px solid #FFD700;">
|
| 384 |
+
<h3 style="color: #FF8C00;">🎯 Top 5 Indicadores Más Relevantes</h3>
|
| 385 |
+
<table style="width: 100%; border-collapse: collapse; margin-top: 10px;">
|
| 386 |
+
<thead>
|
| 387 |
+
<tr style="background-color: #FFD700;">
|
| 388 |
+
<th style="padding: 10px; text-align: left;">Rank</th>
|
| 389 |
+
<th style="padding: 10px; text-align: left;"> </th>
|
| 390 |
+
<th style="padding: 10px; text-align: left;">ID </th>
|
| 391 |
+
<th style="padding: 10px; text-align: left;">Indicador</th>
|
| 392 |
+
<!-- <th style="padding: 10px; text-align: center;">ODS</th> -->
|
| 393 |
+
<th style="padding: 10px; text-align: right;">Similaridad</th>
|
| 394 |
+
</tr>
|
| 395 |
+
</thead>
|
| 396 |
+
<tbody>
|
| 397 |
+
{''.join([f'''<tr>
|
| 398 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;">{row['indicador_rank']}</td>
|
| 399 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><img src="{dict_logos[row['logo_id']]}"
|
| 400 |
+
alt="ODS {row['ODS_ID']}"
|
| 401 |
+
class="logo-ods-tbl"></td>
|
| 402 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>{row['INDICADOR_ID']}</strong></td>
|
| 403 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: center;">{row['INDICADOR']}</td>
|
| 404 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right;">{row['indicador_similaridad_cos_normalized']:.4f}</td>
|
| 405 |
+
</tr>''' for _, row in top_indicador.iterrows()])}
|
| 406 |
+
</tbody>
|
| 407 |
+
</table>
|
| 408 |
+
</div>
|
| 409 |
+
|
| 410 |
+
<div style="background-color: #F0F0F0; padding: 20px; border-radius: 10px; margin: 20px 0;">
|
| 411 |
+
<h3 style="color: #2E5090;">📚 Cómo usar esta aplicación</h3>
|
| 412 |
+
<ol style="line-height: 1.8;">
|
| 413 |
+
<li><strong>Explora las pestañas:</strong> Cada pestaña contiene una visualización diferente</li>
|
| 414 |
+
<li><strong>Lee las explicaciones:</strong> Cada gráfica incluye una guía de interpretación</li>
|
| 415 |
+
<li><strong>Interactúa:</strong> Las visualizaciones HTML permiten zoom, hover y exploración</li>
|
| 416 |
+
<li><strong>Descarga:</strong> Puedes descargar las imágenes desde las pestañas</li>
|
| 417 |
+
</ol>
|
| 418 |
+
</div>
|
| 419 |
+
|
| 420 |
+
<div style="text-align: center; margin-top: 30px; padding: 20px; background-color: #E8F4F8; border-radius: 10px;">
|
| 421 |
+
<p style="font-size: 18px; color: #2E5090;">
|
| 422 |
+
<strong>¡Comienza explorando las visualizaciones en las pestañas superiores!</strong>
|
| 423 |
+
</p>
|
| 424 |
+
<p style="color: #666;">
|
| 425 |
+
Recomendación: Empieza con el "Dashboard Integrado" para una vista general
|
| 426 |
+
</p>
|
| 427 |
+
</div>
|
| 428 |
+
</div>
|
| 429 |
+
"""
|
| 430 |
+
return html
|
| 431 |
+
|
| 432 |
+
def tab_viz1(df_ods, df_metas, df_indicador):
|
| 433 |
+
# def tab_viz1():
|
| 434 |
+
"""Visualización 1: Box Plot por ODS"""
|
| 435 |
+
if not DATOS_CARGADOS:
|
| 436 |
+
return None, "⚠️ Error: No se pudieron cargar los datos."
|
| 437 |
+
|
| 438 |
+
fig1 = viz_1_distribucion_por_ods(df_ods, 'ODS_ID', 'ods_similaridad_cos_normalized', 'ODS')
|
| 439 |
+
fig2 = viz_1_distribucion_por_ods(df_metas, 'META_ID', 'meta_similaridad_cos_normalized', 'META')
|
| 440 |
+
fig3 = viz_1_distribucion_por_ods(df_indicador, 'INDICADOR_ID', 'indicador_similaridad_cos_normalized', 'INDICADOR')
|
| 441 |
+
|
| 442 |
+
explicacion = """
|
| 443 |
+
## 📦 Diagrama de Caja por ODS
|
| 444 |
+
|
| 445 |
+
### ¿Qué muestra?
|
| 446 |
+
Esta visualización muestra cómo se distribuyen los valores de similaridad para cada uno de los 17 ODS.
|
| 447 |
+
|
| 448 |
+
### ¿Cómo leerlo?
|
| 449 |
+
- **Línea central**: Mediana (valor del medio)
|
| 450 |
+
- **Caja**: Rango intercuartílico (Q1 a Q3)
|
| 451 |
+
- **Líneas extendidas**: Valores mínimos y máximos normales
|
| 452 |
+
- **Puntos fuera**: Valores atípicos (outliers)
|
| 453 |
+
|
| 454 |
+
### Interpretación:
|
| 455 |
+
- ✅ **Cajas altas**: Mucha variación entre indicadores del ODS
|
| 456 |
+
- ✅ **Cajas pequeñas**: Indicadores consistentes
|
| 457 |
+
- ✅ **Mediana alta**: ODS muy relacionado con la iniciativa
|
| 458 |
+
- ✅ **Puntos aislados**: Indicadores especialmente relevantes
|
| 459 |
+
|
| 460 |
+
### 💡 Consejo:
|
| 461 |
+
Busca ODS con medianas altas y cajas pequeñas para identificar objetivos con indicadores consistentemente relevantes.
|
| 462 |
+
"""
|
| 463 |
+
|
| 464 |
+
return fig1, fig2, fig3, explicacion
|
| 465 |
+
|
| 466 |
+
def tab_viz2(df_global):
|
| 467 |
+
# def tab_viz2():
|
| 468 |
+
"""Visualización 2: Heatmap ODS × Ranking"""
|
| 469 |
+
if not DATOS_CARGADOS:
|
| 470 |
+
return None, "⚠️ Error: No se pudieron cargar los datos."
|
| 471 |
+
|
| 472 |
+
fig = viz_2_heatmap_ods_ranking(df_global)
|
| 473 |
+
filepath = matplotlib_to_file(fig, 'viz2_heatmap.png')
|
| 474 |
+
|
| 475 |
+
explicacion = """
|
| 476 |
+
## 🔥 Mapa de Calor: ODS × Ranking
|
| 477 |
+
|
| 478 |
+
### ¿Qué muestra?
|
| 479 |
+
Matriz bidimensional que cruza los 17 ODS (filas) con deciles de ranking (columnas),
|
| 480 |
+
mostrando la similaridad promedio en cada celda.
|
| 481 |
+
|
| 482 |
+
### ¿Cómo leerlo?
|
| 483 |
+
- 🔴 **Colores cálidos** (rojo/naranja): Alta similaridad
|
| 484 |
+
- 🔵 **Colores fríos** (verde/azul): Baja similaridad
|
| 485 |
+
- **D1 a D10**: Desde los más relevantes (D1) hasta los menos (D10)
|
| 486 |
+
|
| 487 |
+
### Interpretación:
|
| 488 |
+
- ✅ **Fila roja completa**: ODS relevante en todos los rangos
|
| 489 |
+
- ✅ **Columna roja**: Varios ODS relevantes en esa posición
|
| 490 |
+
- ✅ **Diagonal descendente**: Patrón esperado (a mayor rank, menor similaridad)
|
| 491 |
+
- ✅ **Rojo en D1-D2**: Los ODS más críticos
|
| 492 |
+
|
| 493 |
+
### 💡 Consejo:
|
| 494 |
+
Identifica rápidamente qué ODS dominan en las posiciones altas del ranking.
|
| 495 |
+
"""
|
| 496 |
+
|
| 497 |
+
return filepath, explicacion
|
| 498 |
+
|
| 499 |
+
def tab_viz3(df_global):
|
| 500 |
+
# def tab_viz3():
|
| 501 |
+
"""Visualización 3: Scatter 3D Interactivo"""
|
| 502 |
+
if not DATOS_CARGADOS:
|
| 503 |
+
return None, "⚠️ Error: No se pudieron cargar los datos."
|
| 504 |
+
|
| 505 |
+
fig = viz_3_scatter_3d_interactivo(df_global)
|
| 506 |
+
|
| 507 |
+
explicacion = """
|
| 508 |
+
## 🌐 Gráfico 3D Interactivo
|
| 509 |
+
|
| 510 |
+
### ¿Qué muestra?
|
| 511 |
+
Visualización tridimensional donde cada punto representa un indicador.
|
| 512 |
+
|
| 513 |
+
### Las tres dimensiones:
|
| 514 |
+
- **Eje X**: ODS ID (1-17)
|
| 515 |
+
- **Eje Y**: Número de sub-indicador
|
| 516 |
+
- **Eje Z**: Similaridad (altura del punto)
|
| 517 |
+
- **Tamaño**: Los más grandes = más relevantes
|
| 518 |
+
- **Color**: Cada ODS tiene su color
|
| 519 |
+
|
| 520 |
+
### Interactividad:
|
| 521 |
+
- 🔄 **Rotar**: Arrastra con el mouse
|
| 522 |
+
- 🔍 **Zoom**: Scroll o pinch
|
| 523 |
+
- 👆 **Hover**: Pasa el mouse sobre puntos
|
| 524 |
+
|
| 525 |
+
### Interpretación:
|
| 526 |
+
- ✅ **Puntos altos**: Alta similaridad
|
| 527 |
+
- ✅ **Clusters de color**: Grupo de indicadores relacionados
|
| 528 |
+
- ✅ **Puntos grandes y altos**: Los más importantes
|
| 529 |
+
|
| 530 |
+
### 💡 Consejo:
|
| 531 |
+
Rota el gráfico para descubrir patrones ocultos y agrupaciones de indicadores.
|
| 532 |
+
"""
|
| 533 |
+
|
| 534 |
+
return fig, explicacion
|
| 535 |
+
|
| 536 |
+
def tab_viz4(df_global):
|
| 537 |
+
# def tab_viz4():
|
| 538 |
+
"""Visualización 4: Radar Chart"""
|
| 539 |
+
if not DATOS_CARGADOS:
|
| 540 |
+
return None, "⚠️ Error: No se pudieron cargar los datos."
|
| 541 |
+
|
| 542 |
+
fig = viz_4_radar_chart_ods(df_global)
|
| 543 |
+
|
| 544 |
+
explicacion = """
|
| 545 |
+
## 🕸️ Gráfico de Radar (Perfil ODS)
|
| 546 |
+
|
| 547 |
+
### ¿Qué muestra?
|
| 548 |
+
Gráfico circular que muestra el 'perfil ODS' de tu iniciativa con dos métricas.
|
| 549 |
+
|
| 550 |
+
### Cómo leerlo:
|
| 551 |
+
- 🔵 **Polígono azul**: Similaridad promedio por ODS
|
| 552 |
+
- 🔴 **Polígono rojo**: Similaridad máxima (mejor indicador)
|
| 553 |
+
- **Distancia del centro**: Mayor distancia = mayor similaridad
|
| 554 |
+
|
| 555 |
+
### Interpretación:
|
| 556 |
+
- ✅ **Picos hacia afuera**: ODS muy relevantes
|
| 557 |
+
- ✅ **Valles hacia dentro**: ODS menos relacionados
|
| 558 |
+
- ✅ **Forma circular**: Iniciativa equilibrada
|
| 559 |
+
- ✅ **Forma irregular**: Especialización en ODS específicos
|
| 560 |
+
- ✅ **Gap azul-rojo grande**: Indicador estrella en ese ODS
|
| 561 |
+
|
| 562 |
+
### 💡 Consejo:
|
| 563 |
+
Ideal para presentaciones ejecutivas. Muestra de un vistazo el perfil completo de alineación ODS.
|
| 564 |
+
"""
|
| 565 |
+
|
| 566 |
+
return fig, explicacion
|
| 567 |
+
|
| 568 |
+
def tab_viz5(df_global):
|
| 569 |
+
# def tab_viz5():
|
| 570 |
+
"""Visualización 5: Sunburst"""
|
| 571 |
+
if not DATOS_CARGADOS:
|
| 572 |
+
return None, "⚠️ Error: No se pudieron cargar los datos."
|
| 573 |
+
|
| 574 |
+
fig = viz_5_sunburst_jerarquia(df_global)
|
| 575 |
+
|
| 576 |
+
explicacion = """
|
| 577 |
+
## ☀️ Diagrama de Sol (Sunburst)
|
| 578 |
+
|
| 579 |
+
### ¿Qué muestra?
|
| 580 |
+
Diagrama circular jerárquico mostrando ODS (centro) → Indicadores (anillo exterior).
|
| 581 |
+
|
| 582 |
+
### Cómo leerlo:
|
| 583 |
+
- **Tamaño del segmento**: Proporcional a la similaridad
|
| 584 |
+
- **Color**: Gradiente (más oscuro = mayor similaridad)
|
| 585 |
+
- **Nivel 1 (centro)**: Los 17 ODS
|
| 586 |
+
- **Nivel 2 (exterior)**: Indicadores individuales
|
| 587 |
+
|
| 588 |
+
### Interactividad:
|
| 589 |
+
- 👆 **Click**: Zoom en un ODS específico
|
| 590 |
+
- 🔍 **Hover**: Ver código y valor del indicador
|
| 591 |
+
|
| 592 |
+
### Interpretación:
|
| 593 |
+
- ✅ **Segmentos grandes**: Indicadores muy relevantes
|
| 594 |
+
- ✅ **ODS ocupa mucho espacio**: Muchos indicadores relevantes
|
| 595 |
+
- ✅ **Colores oscuros**: Alta similaridad
|
| 596 |
+
|
| 597 |
+
### 💡 Consejo:
|
| 598 |
+
Excelente para visualizar la contribución relativa de cada indicador al total.
|
| 599 |
+
"""
|
| 600 |
+
|
| 601 |
+
return fig, explicacion
|
| 602 |
+
|
| 603 |
+
def tab_viz6(df_global):
|
| 604 |
+
# def tab_viz6():
|
| 605 |
+
"""Visualización 6: Top Indicadores por ODS"""
|
| 606 |
+
if not DATOS_CARGADOS:
|
| 607 |
+
return None, "⚠️ Error: No se pudieron cargar los datos."
|
| 608 |
+
|
| 609 |
+
fig = viz_6_top_indicadores_por_ods(df_global, top_n=5)
|
| 610 |
+
|
| 611 |
+
explicacion = """
|
| 612 |
+
## 🏆 Top 5 Indicadores por ODS
|
| 613 |
+
|
| 614 |
+
### ¿Qué muestra?
|
| 615 |
+
Barras horizontales con los 5 indicadores más relevantes de cada ODS.
|
| 616 |
+
|
| 617 |
+
### Cómo leerlo:
|
| 618 |
+
- **Longitud de barra**: Valor de similaridad
|
| 619 |
+
- **Primera barra**: El indicador más relevante
|
| 620 |
+
- **Color**: Gradiente por similaridad
|
| 621 |
+
- **Cada panel**: Un ODS diferente
|
| 622 |
+
|
| 623 |
+
### Interpretación:
|
| 624 |
+
- ✅ **Barra mucho más larga**: Indicador campeón
|
| 625 |
+
- ✅ **Barras parejas**: Varios indicadores igualmente relevantes
|
| 626 |
+
- ✅ **Comparación entre ODS**: Qué objetivo tiene mejores indicadores
|
| 627 |
+
|
| 628 |
+
### 💡 Consejo:
|
| 629 |
+
Perfecta para planificación estratégica. Te dice exactamente en qué indicadores enfocarte por cada ODS.
|
| 630 |
+
"""
|
| 631 |
+
|
| 632 |
+
return fig, explicacion
|
| 633 |
+
|
| 634 |
+
def tab_viz7(df_global):
|
| 635 |
+
# def tab_viz7():
|
| 636 |
+
"""Visualización 7: Stream Graph"""
|
| 637 |
+
if not DATOS_CARGADOS:
|
| 638 |
+
return None, "⚠️ Error: No se pudieron cargar los datos."
|
| 639 |
+
|
| 640 |
+
fig = viz_7_streamgraph_similaridad(df_global)
|
| 641 |
+
|
| 642 |
+
explicacion = """
|
| 643 |
+
## 🌊 Gráfico de Flujo (Stream Graph)
|
| 644 |
+
|
| 645 |
+
### ¿Qué muestra?
|
| 646 |
+
Áreas apiladas que muestran cómo cambia la contribución porcentual de cada ODS
|
| 647 |
+
a lo largo del ranking.
|
| 648 |
+
|
| 649 |
+
### Cómo leerlo:
|
| 650 |
+
- **Eje horizontal**: Ranking agrupado (izq. = más relevante)
|
| 651 |
+
- **Eje vertical**: Porcentaje de contribución (suma 100%)
|
| 652 |
+
- **Ancho del color**: Porcentaje del ODS en ese rango
|
| 653 |
+
|
| 654 |
+
### Interpretación:
|
| 655 |
+
- ✅ **Color dominante izquierda**: ODS líder en indicadores relevantes
|
| 656 |
+
- ✅ **Cambio de color**: Transición de relevancia
|
| 657 |
+
- ✅ **Área ancha constante**: ODS presente en todo el ranking
|
| 658 |
+
- ✅ **Área que crece/decrece**: ODS relevante en ciertos rangos
|
| 659 |
+
|
| 660 |
+
### 💡 Consejo:
|
| 661 |
+
Si un ODS ocupa mucho espacio a la izquierda, domina entre los indicadores más relevantes.
|
| 662 |
+
"""
|
| 663 |
+
|
| 664 |
+
return fig, explicacion
|
| 665 |
+
|
| 666 |
+
def tab_viz8(df_global):
|
| 667 |
+
# def tab_viz8():
|
| 668 |
+
"""Visualización 8: Violin Plot"""
|
| 669 |
+
if not DATOS_CARGADOS:
|
| 670 |
+
return None, "⚠️ Error: No se pudieron cargar los datos."
|
| 671 |
+
|
| 672 |
+
fig = viz_8_violin_plot_ods(df_global)
|
| 673 |
+
|
| 674 |
+
explicacion = """
|
| 675 |
+
## 🎻 Gráfico de Violín
|
| 676 |
+
|
| 677 |
+
### ¿Qué muestra?
|
| 678 |
+
Similar al diagrama de caja pero con más detalle. Muestra la 'forma' completa
|
| 679 |
+
de la distribución de similaridad por ODS.
|
| 680 |
+
|
| 681 |
+
### Cómo leerlo:
|
| 682 |
+
- **Ancho del violín**: Concentración de valores
|
| 683 |
+
- **Caja interior**: Mediana y cuartiles
|
| 684 |
+
- **Línea horizontal**: Media (promedio)
|
| 685 |
+
|
| 686 |
+
### Concepto clave:
|
| 687 |
+
El ancho representa la **densidad de probabilidad**: donde el violín es más ancho,
|
| 688 |
+
es más probable encontrar indicadores con esos valores.
|
| 689 |
+
|
| 690 |
+
### Interpretación:
|
| 691 |
+
- ✅ **Violín ancho en un punto**: Muchos indicadores similares
|
| 692 |
+
- ✅ **Dos ensanchamientos**: Dos grupos distintos
|
| 693 |
+
- ✅ **Violín delgado**: Pocos indicadores en ese rango
|
| 694 |
+
- ✅ **Forma simétrica**: Distribución equilibrada
|
| 695 |
+
|
| 696 |
+
### 💡 Consejo:
|
| 697 |
+
Detecta distribuciones complejas que el diagrama de caja no puede mostrar.
|
| 698 |
+
"""
|
| 699 |
+
|
| 700 |
+
return fig, explicacion
|
| 701 |
+
|
| 702 |
+
def tab_viz9(df_global):
|
| 703 |
+
# def tab_viz9():
|
| 704 |
+
"""Visualización 9: Dashboard Integrado"""
|
| 705 |
+
if not DATOS_CARGADOS:
|
| 706 |
+
return None, "⚠️ Error: No se pudieron cargar los datos."
|
| 707 |
+
|
| 708 |
+
fig = viz_9_dashboard_metricas(df_global)
|
| 709 |
+
|
| 710 |
+
explicacion = """
|
| 711 |
+
## 📊 Dashboard Integrado (4 Paneles)
|
| 712 |
+
|
| 713 |
+
### Panel 1 (Superior Izquierdo): Top 10 Indicadores
|
| 714 |
+
Barras con los 10 indicadores más relevantes del análisis completo.
|
| 715 |
+
|
| 716 |
+
### Panel 2 (Superior Derecho): Estadísticas por ODS
|
| 717 |
+
Tabla con media, desviación estándar, mínimo, máximo y cantidad por ODS.
|
| 718 |
+
|
| 719 |
+
### Panel 3 (Inferior Izquierdo): Histograma Global
|
| 720 |
+
Distribución de frecuencias de todos los valores de similaridad.
|
| 721 |
+
|
| 722 |
+
### Panel 4 (Inferior Derecho): Correlación Rank-Similaridad
|
| 723 |
+
Scatter plot con línea de tendencia. **CRÍTICO para validación del sistema**.
|
| 724 |
+
|
| 725 |
+
### Validación:
|
| 726 |
+
- ✅ **Línea descendente**: Sistema funcionando correctamente
|
| 727 |
+
- ✅ **Correlación < -0.7**: Excelente
|
| 728 |
+
- ⚠️ **Correlación > -0.4**: Revisar sistema
|
| 729 |
+
|
| 730 |
+
### 💡 Consejo:
|
| 731 |
+
Este debe ser tu punto de partida. Vista 360° del análisis completo.
|
| 732 |
+
"""
|
| 733 |
+
|
| 734 |
+
return fig, explicacion
|
| 735 |
+
|
| 736 |
+
def tab_viz10(df_global):
|
| 737 |
+
# def tab_viz10():
|
| 738 |
+
"""Visualización 10: Matriz de Transición"""
|
| 739 |
+
if not DATOS_CARGADOS:
|
| 740 |
+
return None, "⚠️ Error: No se pudieron cargar los datos."
|
| 741 |
+
|
| 742 |
+
fig = viz_10_matriz_transicion(df_global)
|
| 743 |
+
filepath = matplotlib_to_file(fig, 'viz10_matriz_transicion.png')
|
| 744 |
+
|
| 745 |
+
explicacion = """
|
| 746 |
+
## 🔀 Matriz de Transición por Cuartiles
|
| 747 |
+
|
| 748 |
+
### ¿Qué muestra?
|
| 749 |
+
Mapa de calor que muestra el porcentaje de cada ODS presente en los 4 cuartiles del ranking.
|
| 750 |
+
|
| 751 |
+
### Cómo leerlo:
|
| 752 |
+
- **Filas**: Los 17 ODS
|
| 753 |
+
- **Columnas**: Q1 (Top 25%), Q2, Q3, Q4 (Bottom 25%)
|
| 754 |
+
- **Valores**: Porcentaje de presencia del ODS
|
| 755 |
+
- **Colores**: Naranja/rojo = alta presencia
|
| 756 |
+
|
| 757 |
+
### Interpretación:
|
| 758 |
+
- ✅ **Rojo intenso en Q1**: ODS crítico (domina rankings altos)
|
| 759 |
+
- ✅ **Colores uniformes**: ODS consistente en todo el ranking
|
| 760 |
+
- ✅ **Concentración en un cuartil**: ODS especializado
|
| 761 |
+
- ✅ **Claro en Q1, oscuro en Q4**: Más relevante en posiciones bajas
|
| 762 |
+
|
| 763 |
+
### 💡 Consejo:
|
| 764 |
+
Analiza la consistencia de relevancia por ODS. Alta presencia en Q1 = crítico para la iniciativa.
|
| 765 |
+
"""
|
| 766 |
+
|
| 767 |
+
return filepath, explicacion
|
| 768 |
+
|
| 769 |
+
def tab_estadisticas(df_global):
|
| 770 |
+
# def tab_estadisticas():
|
| 771 |
+
"""Pestaña con análisis estadístico detallado"""
|
| 772 |
+
if not DATOS_CARGADOS:
|
| 773 |
+
return "⚠️ Error: No se pudieron cargar los datos."
|
| 774 |
+
|
| 775 |
+
# Estadísticas globales
|
| 776 |
+
stats = df_global['similaridad_cos'].describe()
|
| 777 |
+
correlacion = df_global['rank'].corr(df_global['similaridad_cos'])
|
| 778 |
+
|
| 779 |
+
# Por ODS
|
| 780 |
+
stats_ods = df_global.groupby('ods_id')['similaridad_cos'].agg([
|
| 781 |
+
('count', 'count'),
|
| 782 |
+
('mean', 'mean'),
|
| 783 |
+
('std', 'std'),
|
| 784 |
+
('min', 'min'),
|
| 785 |
+
('max', 'max')
|
| 786 |
+
]).round(4)
|
| 787 |
+
|
| 788 |
+
# Top 50
|
| 789 |
+
top_50_ods = df_global.nsmallest(50, 'rank')['ods_id'].value_counts()
|
| 790 |
+
|
| 791 |
+
html = f"""
|
| 792 |
+
<div style="font-family: Arial, sans-serif; padding: 20px;">
|
| 793 |
+
<h1 style="color: #2E5090;">📈 Análisis Estadístico Detallado</h1>
|
| 794 |
+
|
| 795 |
+
<div class="stats-box" style="background-color: #E8F4F8; padding: 20px; border-radius: 10px; margin: 20px 0;">
|
| 796 |
+
<h2 style="color: #2E5090;">1. Estadísticas Globales</h2>
|
| 797 |
+
<table style="width: 100%; border-collapse: collapse;">
|
| 798 |
+
<tr>
|
| 799 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Cantidad de datos:</strong></td>
|
| 800 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right;">{stats['count']:.0f}</td>
|
| 801 |
+
</tr>
|
| 802 |
+
<tr>
|
| 803 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Media:</strong></td>
|
| 804 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right;">{stats['mean']:.4f}</td>
|
| 805 |
+
</tr>
|
| 806 |
+
<tr>
|
| 807 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Desviación Estándar:</strong></td>
|
| 808 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right;">{stats['std']:.4f}</td>
|
| 809 |
+
</tr>
|
| 810 |
+
<tr>
|
| 811 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Mínimo:</strong></td>
|
| 812 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right;">{stats['min']:.4f}</td>
|
| 813 |
+
</tr>
|
| 814 |
+
<tr>
|
| 815 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Q1 (Percentil 25):</strong></td>
|
| 816 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right;">{stats['25%']:.4f}</td>
|
| 817 |
+
</tr>
|
| 818 |
+
<tr>
|
| 819 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Mediana (Q2):</strong></td>
|
| 820 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right;">{stats['50%']:.4f}</td>
|
| 821 |
+
</tr>
|
| 822 |
+
<tr>
|
| 823 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Q3 (Percentil 75):</strong></td>
|
| 824 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right;">{stats['75%']:.4f}</td>
|
| 825 |
+
</tr>
|
| 826 |
+
<tr>
|
| 827 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>Máximo:</strong></td>
|
| 828 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right;">{stats['max']:.4f}</td>
|
| 829 |
+
</tr>
|
| 830 |
+
</table>
|
| 831 |
+
</div>
|
| 832 |
+
|
| 833 |
+
<div class="explanation-box" style="background-color: #{'E6F7E6' if correlacion < -0.7 else 'FFF9E6'}; padding: 20px; border-radius: 10px; margin: 20px 0; border-left: 5px solid #{'28A745' if correlacion < -0.7 else 'FFD700'};">
|
| 834 |
+
<h2 style="color: #{'28A745' if correlacion < -0.7 else 'FF8C00'};">2. Validación del Sistema</h2>
|
| 835 |
+
<p style="font-size: 18px;"><strong>Correlación Rank vs Similaridad:</strong> {correlacion:.4f}</p>
|
| 836 |
+
<p><strong>Interpretación:</strong>
|
| 837 |
+
{
|
| 838 |
+
"✅ Excelente - Sistema de ranking muy confiable" if correlacion < -0.9 else
|
| 839 |
+
"✅ Muy bueno - Sistema de ranking confiable" if correlacion < -0.7 else
|
| 840 |
+
"⚠️ Aceptable - Sistema funciona pero puede mejorarse" if correlacion < -0.4 else
|
| 841 |
+
"❌ Problema - Revisar cálculo de similaridad o ranking"
|
| 842 |
+
}
|
| 843 |
+
</p>
|
| 844 |
+
<p><em>Una correlación negativa fuerte indica que a mayor ranking (menos relevante), menor es la similaridad, lo cual es el comportamiento esperado.</em></p>
|
| 845 |
+
</div>
|
| 846 |
+
|
| 847 |
+
<div class="stats-box" style="background-color: #FFF9E6; padding: 20px; border-radius: 10px; margin: 20px 0;">
|
| 848 |
+
<h2 style="color: #2E5090;">3. Estadísticas por ODS</h2>
|
| 849 |
+
<table style="width: 100%; border-collapse: collapse; font-size: 14px;">
|
| 850 |
+
<thead>
|
| 851 |
+
<tr style="background-color: #FFD700;">
|
| 852 |
+
<th style="padding: 10px; text-align: left;">ODS</th>
|
| 853 |
+
<th style="padding: 10px; text-align: right;">Count</th>
|
| 854 |
+
<th style="padding: 10px; text-align: right;">Media</th>
|
| 855 |
+
<th style="padding: 10px; text-align: right;">Std</th>
|
| 856 |
+
<th style="padding: 10px; text-align: right;">Min</th>
|
| 857 |
+
<th style="padding: 10px; text-align: right;">Max</th>
|
| 858 |
+
</tr>
|
| 859 |
+
</thead>
|
| 860 |
+
<tbody>
|
| 861 |
+
{''.join([f'''<tr>
|
| 862 |
+
<td style="padding: 8px; border-bottom: 1px solid #ddd;"><strong>ODS {idx}</strong></td>
|
| 863 |
+
<td style="padding: 8px; border-bottom: 1px solid #ddd; text-align: right;">{int(row['count'])}</td>
|
| 864 |
+
<td style="padding: 8px; border-bottom: 1px solid #ddd; text-align: right;">{row['mean']:.4f}</td>
|
| 865 |
+
<td style="padding: 8px; border-bottom: 1px solid #ddd; text-align: right;">{row['std']:.4f}</td>
|
| 866 |
+
<td style="padding: 8px; border-bottom: 1px solid #ddd; text-align: right;">{row['min']:.4f}</td>
|
| 867 |
+
<td style="padding: 8px; border-bottom: 1px solid #ddd; text-align: right;">{row['max']:.4f}</td>
|
| 868 |
+
</tr>''' for idx, row in stats_ods.iterrows()])}
|
| 869 |
+
</tbody>
|
| 870 |
+
</table>
|
| 871 |
+
</div>
|
| 872 |
+
|
| 873 |
+
<div class="explanation-box" style="background-color: #E8F4F8; padding: 20px; border-radius: 10px; margin: 20px 0; border-left: 5px solid #2E5090;">
|
| 874 |
+
<h2 style="color: #2E5090;">4. ODS Más Representados en Top 50</h2>
|
| 875 |
+
<table style="width: 100%; border-collapse: collapse;">
|
| 876 |
+
<thead>
|
| 877 |
+
<tr style="background-color: #4472C4; color: white;">
|
| 878 |
+
<th style="padding: 10px; text-align: left;">ODS</th>
|
| 879 |
+
<th style="padding: 10px; text-align: right;">Cantidad</th>
|
| 880 |
+
<th style="padding: 10px; text-align: right;">Porcentaje</th>
|
| 881 |
+
</tr>
|
| 882 |
+
</thead>
|
| 883 |
+
<tbody>
|
| 884 |
+
{''.join([f'''<tr>
|
| 885 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd;"><strong>ODS {idx}</strong></td>
|
| 886 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right;">{count}</td>
|
| 887 |
+
<td style="padding: 10px; border-bottom: 1px solid #ddd; text-align: right;">{count/50*100:.1f}%</td>
|
| 888 |
+
</tr>''' for idx, count in top_50_ods.head(10).items()])}
|
| 889 |
+
</tbody>
|
| 890 |
+
</table>
|
| 891 |
+
</div>
|
| 892 |
+
</div>
|
| 893 |
+
"""
|
| 894 |
+
|
| 895 |
+
return html
|
| 896 |
+
|
| 897 |
+
# ============================================================================
|
| 898 |
+
# CONSTRUCCIÓN DE LA APLICACIÓN GRADIO
|
| 899 |
+
# ============================================================================
|
| 900 |
+
|
| 901 |
+
def crear_app():
|
| 902 |
+
"""Crea y configura la aplicación Gradio completa"""
|
| 903 |
+
|
| 904 |
+
with gr.Blocks(
|
| 905 |
+
title="Sistema de Visualización ODS",
|
| 906 |
+
# theme=gr.themes.Soft(
|
| 907 |
+
# primary_hue="indigo",
|
| 908 |
+
# secondary_hue="orange",
|
| 909 |
+
# neutral_hue="slate"
|
| 910 |
+
# ),
|
| 911 |
+
theme="light",
|
| 912 |
+
css=CUSTOM_CSS
|
| 913 |
+
) as app:
|
| 914 |
+
|
| 915 |
+
gr.HTML(f"""
|
| 916 |
+
<div class="header-institucional">
|
| 917 |
+
<div style="flex: 0 0 auto;">
|
| 918 |
+
<img src="{dict_logos['gobierno']}"
|
| 919 |
+
alt="Gobierno de Colombia"
|
| 920 |
+
class="logo-institucional">
|
| 921 |
+
</div>
|
| 922 |
+
<div class="titulo-institucional">
|
| 923 |
+
<h1></h1>
|
| 924 |
+
<p> </p>
|
| 925 |
+
</div>
|
| 926 |
+
<div style="flex: 0 0 auto;">
|
| 927 |
+
<img src="{dict_logos['fondo_un']}"
|
| 928 |
+
alt="Fondo Multidonante de las Naciones Unidas"
|
| 929 |
+
class="logo-institucional">
|
| 930 |
+
</div>
|
| 931 |
+
</div>
|
| 932 |
+
""")
|
| 933 |
+
|
| 934 |
+
# Encabezado principal
|
| 935 |
+
gr.Markdown("""
|
| 936 |
+
# 📊 Voces ODS: Explora cómo tu voz conecta con los ODS
|
| 937 |
+
### Explorador Interactivo
|
| 938 |
+
|
| 939 |
+
*Voces ODS es una herramienta innovadora que traduce las narrativas de las comunidades en lenguaje de los Objetivos de Desarrollo Sostenible (ODS). Su propósito es visibilizar cómo las voces locales, las memorias colectivas como las iniciativas PATR y las experiencias territoriales se vinculan con las metas globales, facilitando el análisis e incidencia para la toma de decisiones.A través de un sistema de visualización y análisis de similitud, la herramienta permite identificar líneas estratégicas asociadas a las narrativas de las comunidades, transformando relatos en insumos estratégicos para políticas públicas, proyectos de desarrollo y procesos de incidencia*
|
| 940 |
+
""")
|
| 941 |
+
|
| 942 |
+
# Pestañas principales
|
| 943 |
+
with gr.Tabs():
|
| 944 |
+
|
| 945 |
+
# PESTAÑA: CONSULTA
|
| 946 |
+
with gr.Tab("CONSULTA BASICA"):
|
| 947 |
+
with gr.Column():
|
| 948 |
+
query_in = gr.Textbox(lines=5, placeholder="Escribe aquí tu consulta...", label="")
|
| 949 |
+
query_out = gr.Textbox(lines=5, label="Texto ajustado para lenguaje natural")
|
| 950 |
+
|
| 951 |
+
btn = gr.Button(value="Analizar mi iniciativa")
|
| 952 |
+
|
| 953 |
+
with gr.Row():
|
| 954 |
+
ods = gr.Dataframe(type="pandas", label="ODS")
|
| 955 |
+
meta = gr.Dataframe(type="pandas", label="METAS")
|
| 956 |
+
indicador = gr.Dataframe(type="pandas", label="INDICADORES")
|
| 957 |
+
|
| 958 |
+
with gr.Row():
|
| 959 |
+
genero = gr.Dataframe(type="pandas", label="Enfoque de genero")
|
| 960 |
+
poblacional = gr.Dataframe(type="pandas", label="Enfoque poblacional")
|
| 961 |
+
etnico = gr.Dataframe(type="pandas", label="Enfoque étnico")
|
| 962 |
+
|
| 963 |
+
with gr.Row():
|
| 964 |
+
pilar = gr.Dataframe(type="pandas", label="Pilares")
|
| 965 |
+
estrategia = gr.Dataframe(type="pandas", label="Estrategias")
|
| 966 |
+
categoria = gr.Dataframe(type="pandas", label="Categorias")
|
| 967 |
+
|
| 968 |
+
with gr.Row():
|
| 969 |
+
bdl_ods = gr.Dataframe(value=pd.DataFrame(),type="pandas", label="BDL_ODS")
|
| 970 |
+
|
| 971 |
+
# query_in.render()
|
| 972 |
+
# indicador, indicador_norm, query, pilares, estrategias, categorias = search()
|
| 973 |
+
|
| 974 |
+
|
| 975 |
+
btn.click(search, query_in, [query_out,ods,meta,indicador,genero,poblacional,etnico,pilar,estrategia,categoria,bdl_ods])
|
| 976 |
+
# btn.click(cara_utility, [a_valu, trials], cara_output)
|
| 977 |
+
|
| 978 |
+
with gr.Tab('CONSULTA ESPECIALIZADA'):
|
| 979 |
+
|
| 980 |
+
# with gr.Tab("CONSULTA"):
|
| 981 |
+
|
| 982 |
+
with gr.Column():
|
| 983 |
+
query_in_esp = gr.Textbox(lines=5, placeholder="Escribe aquí tu consulta...", label="")
|
| 984 |
+
query_out_esp = gr.Textbox(lines=5, label="Texto ajustado para lenguaje natural")
|
| 985 |
+
|
| 986 |
+
btn_esp = gr.Button(value="Analizar mi iniciativa")
|
| 987 |
+
|
| 988 |
+
|
| 989 |
+
# lvl = gr.Dropdown([col for col in bdl_ods_esp.value.columns if 'ID' in col], label='Nivel de análisis')
|
| 990 |
+
# score = gr.Dropdown([col for col in bdl_ods_esp.value.columns if 'similaridad' in col], label='Score de medida')
|
| 991 |
+
# rank = gr.Dropdown([col for col in bdl_ods_esp.value.columns if 'rank' in col], label='Score de medida')
|
| 992 |
+
|
| 993 |
+
with gr.Tab("Clasificaciones"):
|
| 994 |
+
with gr.Row():
|
| 995 |
+
ods_esp = gr.Dataframe(value=pd.DataFrame(),type="pandas", label="ODS")
|
| 996 |
+
meta_esp = gr.Dataframe(value=pd.DataFrame(),type="pandas", label="METAS")
|
| 997 |
+
indicador_esp = gr.Dataframe(value=pd.DataFrame(),type="pandas", label="INDICADORES")
|
| 998 |
+
|
| 999 |
+
with gr.Row():
|
| 1000 |
+
genero_esp = gr.Dataframe(value=pd.DataFrame(),type="pandas", label="Enfoque de genero")
|
| 1001 |
+
poblacional_esp = gr.Dataframe(value=pd.DataFrame(),type="pandas", label="Enfoque poblacional")
|
| 1002 |
+
etnico_esp = gr.Dataframe(value=pd.DataFrame(),type="pandas", label="Enfoque étnico")
|
| 1003 |
+
|
| 1004 |
+
with gr.Row():
|
| 1005 |
+
pilar_esp = gr.Dataframe(value=pd.DataFrame(),type="pandas", label="Pilares")
|
| 1006 |
+
estrategia_esp = gr.Dataframe(value=pd.DataFrame(),type="pandas", label="Estrategias")
|
| 1007 |
+
categoria_esp = gr.Dataframe(value=pd.DataFrame(),type="pandas", label="Categorias")
|
| 1008 |
+
|
| 1009 |
+
with gr.Row():
|
| 1010 |
+
bdl_ods_esp = gr.Dataframe(value=pd.DataFrame(),type="pandas", label="ODS")
|
| 1011 |
+
|
| 1012 |
+
# PESTAÑA: INICIO
|
| 1013 |
+
with gr.Tab("🏠 Inicio"):
|
| 1014 |
+
html_inicio_ods = gr.HTML() #tab_inicio(ods.value)
|
| 1015 |
+
|
| 1016 |
+
|
| 1017 |
+
btn0 = gr.Button("🔄 Generar Metricas Iniciales", variant="primary")
|
| 1018 |
+
btn0.click(
|
| 1019 |
+
fn=tab_inicio,
|
| 1020 |
+
inputs=[ods_esp,meta_esp,indicador_esp],
|
| 1021 |
+
outputs=[html_inicio_ods]
|
| 1022 |
+
)
|
| 1023 |
+
|
| 1024 |
+
|
| 1025 |
+
# PESTAÑA 1: BOX PLOT
|
| 1026 |
+
with gr.Tab("📦 1. Box Plot"):
|
| 1027 |
+
btn1 = gr.Button("🔄 Generar Visualización", variant="primary")
|
| 1028 |
+
with gr.Row():
|
| 1029 |
+
with gr.Column(scale=1):
|
| 1030 |
+
exp1 = gr.Markdown()
|
| 1031 |
+
with gr.Row(visible=False):
|
| 1032 |
+
with gr.Column(scale=2):
|
| 1033 |
+
plot1_1 = gr.Plot(label="Diagrama de Caja por ODS")
|
| 1034 |
+
|
| 1035 |
+
with gr.Row():
|
| 1036 |
+
with gr.Column(scale=2):
|
| 1037 |
+
plot1_2 = gr.Plot(label="Diagrama de Caja por META")
|
| 1038 |
+
|
| 1039 |
+
with gr.Row():
|
| 1040 |
+
with gr.Column(scale=2):
|
| 1041 |
+
plot1_3 = gr.Plot(label="Diagrama de Caja por INDICADOR")
|
| 1042 |
+
|
| 1043 |
+
|
| 1044 |
+
|
| 1045 |
+
btn1.click(
|
| 1046 |
+
fn=tab_viz1,
|
| 1047 |
+
inputs=[ods_esp, meta_esp, indicador_esp],
|
| 1048 |
+
outputs=[plot1_1, plot1_2, plot1_3, exp1]
|
| 1049 |
+
)
|
| 1050 |
+
|
| 1051 |
+
# PESTAÑA 2: HEATMAP
|
| 1052 |
+
with gr.Tab("🔥 2. Heatmap"):
|
| 1053 |
+
with gr.Row():
|
| 1054 |
+
with gr.Column(scale=2):
|
| 1055 |
+
img2 = gr.Image(label="Mapa de Calor ODS × Ranking", type="filepath")
|
| 1056 |
+
with gr.Column(scale=1):
|
| 1057 |
+
exp2 = gr.Markdown()
|
| 1058 |
+
|
| 1059 |
+
btn2 = gr.Button("🔄 Generar Visualización", variant="primary")
|
| 1060 |
+
btn2.click(
|
| 1061 |
+
fn=tab_viz2,
|
| 1062 |
+
inputs=[ods],
|
| 1063 |
+
outputs=[img2, exp2]
|
| 1064 |
+
)
|
| 1065 |
+
|
| 1066 |
+
# PESTAÑA 3: SCATTER 3D
|
| 1067 |
+
with gr.Tab("🌐 3. Scatter 3D"):
|
| 1068 |
+
with gr.Row():
|
| 1069 |
+
with gr.Column(scale=2):
|
| 1070 |
+
plot3 = gr.Plot(label="Gráfico 3D Interactivo")
|
| 1071 |
+
with gr.Column(scale=1):
|
| 1072 |
+
exp3 = gr.Markdown()
|
| 1073 |
+
|
| 1074 |
+
btn3 = gr.Button("🔄 Generar Visualización", variant="primary")
|
| 1075 |
+
btn3.click(
|
| 1076 |
+
fn=tab_viz3,
|
| 1077 |
+
inputs=[ods],
|
| 1078 |
+
outputs=[plot3, exp3]
|
| 1079 |
+
)
|
| 1080 |
+
|
| 1081 |
+
# PESTAÑA 4: RADAR
|
| 1082 |
+
with gr.Tab("🕸️ 4. Radar Chart"):
|
| 1083 |
+
with gr.Row():
|
| 1084 |
+
with gr.Column(scale=2):
|
| 1085 |
+
plot4 = gr.Plot(label="Gráfico de Radar")
|
| 1086 |
+
with gr.Column(scale=1):
|
| 1087 |
+
exp4 = gr.Markdown()
|
| 1088 |
+
|
| 1089 |
+
btn4 = gr.Button("🔄 Generar Visualización", variant="primary")
|
| 1090 |
+
btn4.click(
|
| 1091 |
+
fn=tab_viz4,
|
| 1092 |
+
inputs=[ods],
|
| 1093 |
+
outputs=[plot4, exp4]
|
| 1094 |
+
)
|
| 1095 |
+
|
| 1096 |
+
# PESTAÑA 5: SUNBURST
|
| 1097 |
+
with gr.Tab("☀️ 5. Sunburst"):
|
| 1098 |
+
with gr.Row():
|
| 1099 |
+
with gr.Column(scale=2):
|
| 1100 |
+
plot5 = gr.Plot(label="Diagrama de Sol")
|
| 1101 |
+
with gr.Column(scale=1):
|
| 1102 |
+
exp5 = gr.Markdown()
|
| 1103 |
+
|
| 1104 |
+
btn5 = gr.Button("🔄 Generar Visualización", variant="primary")
|
| 1105 |
+
btn5.click(
|
| 1106 |
+
fn=tab_viz5,
|
| 1107 |
+
inputs=[ods],
|
| 1108 |
+
outputs=[plot5, exp5]
|
| 1109 |
+
)
|
| 1110 |
+
|
| 1111 |
+
# PESTAÑA 6: TOP INDICADORES
|
| 1112 |
+
with gr.Tab("🏆 6. Top Indicadores"):
|
| 1113 |
+
with gr.Row():
|
| 1114 |
+
with gr.Column(scale=2):
|
| 1115 |
+
plot6 = gr.Plot(label="Top 5 Indicadores por ODS")
|
| 1116 |
+
with gr.Column(scale=1):
|
| 1117 |
+
exp6 = gr.Markdown()
|
| 1118 |
+
|
| 1119 |
+
btn6 = gr.Button("🔄 Generar Visualización", variant="primary")
|
| 1120 |
+
btn6.click(
|
| 1121 |
+
fn=tab_viz6,
|
| 1122 |
+
inputs=[ods],
|
| 1123 |
+
outputs=[plot6, exp6]
|
| 1124 |
+
)
|
| 1125 |
+
|
| 1126 |
+
# PESTAÑA 7: STREAM GRAPH
|
| 1127 |
+
with gr.Tab("🌊 7. Stream Graph"):
|
| 1128 |
+
with gr.Row():
|
| 1129 |
+
with gr.Column(scale=2):
|
| 1130 |
+
plot7 = gr.Plot(label="Gráfico de Flujo")
|
| 1131 |
+
with gr.Column(scale=1):
|
| 1132 |
+
exp7 = gr.Markdown()
|
| 1133 |
+
|
| 1134 |
+
btn7 = gr.Button("🔄 Generar Visualización", variant="primary")
|
| 1135 |
+
btn7.click(
|
| 1136 |
+
fn=tab_viz7,
|
| 1137 |
+
inputs=[ods],
|
| 1138 |
+
outputs=[plot7, exp7]
|
| 1139 |
+
)
|
| 1140 |
+
|
| 1141 |
+
# PESTAÑA 8: VIOLIN PLOT
|
| 1142 |
+
with gr.Tab("🎻 8. Violin Plot"):
|
| 1143 |
+
with gr.Row():
|
| 1144 |
+
with gr.Column(scale=2):
|
| 1145 |
+
plot8 = gr.Plot(label="Gráfico de Violín")
|
| 1146 |
+
with gr.Column(scale=1):
|
| 1147 |
+
exp8 = gr.Markdown()
|
| 1148 |
+
|
| 1149 |
+
btn8 = gr.Button("🔄 Generar Visualización", variant="primary")
|
| 1150 |
+
btn8.click(
|
| 1151 |
+
fn=tab_viz8,
|
| 1152 |
+
inputs=[ods],
|
| 1153 |
+
outputs=[plot8, exp8]
|
| 1154 |
+
)
|
| 1155 |
+
|
| 1156 |
+
# PESTAÑA 9: DASHBOARD
|
| 1157 |
+
with gr.Tab("📊 9. Dashboard"):
|
| 1158 |
+
with gr.Row():
|
| 1159 |
+
with gr.Column(scale=2):
|
| 1160 |
+
plot9 = gr.Plot(label="Dashboard Integrado")
|
| 1161 |
+
with gr.Column(scale=1):
|
| 1162 |
+
exp9 = gr.Markdown()
|
| 1163 |
+
|
| 1164 |
+
btn9 = gr.Button("🔄 Generar Visualización", variant="primary")
|
| 1165 |
+
btn9.click(
|
| 1166 |
+
fn=tab_viz9,
|
| 1167 |
+
inputs=[ods],
|
| 1168 |
+
outputs=[plot9, exp9]
|
| 1169 |
+
)
|
| 1170 |
+
|
| 1171 |
+
# PESTAÑA 10: MATRIZ TRANSICIÓN
|
| 1172 |
+
with gr.Tab("🔀 10. Matriz Transición"):
|
| 1173 |
+
with gr.Row():
|
| 1174 |
+
with gr.Column(scale=2):
|
| 1175 |
+
img10 = gr.Image(label="Matriz de Transición", type="filepath")
|
| 1176 |
+
with gr.Column(scale=1):
|
| 1177 |
+
exp10 = gr.Markdown()
|
| 1178 |
+
|
| 1179 |
+
btn10 = gr.Button("🔄 Generar Visualización", variant="primary")
|
| 1180 |
+
btn10.click(
|
| 1181 |
+
fn=tab_viz10,
|
| 1182 |
+
inputs=[ods],
|
| 1183 |
+
outputs=[img10, exp10]
|
| 1184 |
+
)
|
| 1185 |
+
|
| 1186 |
+
# PESTAÑA: ESTADÍSTICAS
|
| 1187 |
+
with gr.Tab("📈 Estadísticas"):
|
| 1188 |
+
html_stats = gr.HTML() #tab_estadisticas(ods)
|
| 1189 |
+
|
| 1190 |
+
btn11 = gr.Button("🔄 Generar Estadísticas", variant="primary")
|
| 1191 |
+
btn11.click(
|
| 1192 |
+
fn=tab_estadisticas,
|
| 1193 |
+
inputs=[ods],
|
| 1194 |
+
outputs=[html_stats]
|
| 1195 |
+
)
|
| 1196 |
+
|
| 1197 |
+
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])
|
| 1198 |
+
|
| 1199 |
+
|
| 1200 |
+
|
| 1201 |
+
# Pie de página
|
| 1202 |
+
gr.Markdown("""
|
| 1203 |
+
---
|
| 1204 |
+
### 📚 Recursos Adicionales
|
| 1205 |
+
- **Documentación completa**: Consulta los archivos `.md` incluidos
|
| 1206 |
+
- **Código fuente**: `visualizaciones_ods.py`
|
| 1207 |
+
- **Documento Word**: `GUIA_VISUALIZACIONES_PUBLICO_GENERAL.docx`
|
| 1208 |
+
|
| 1209 |
+
---
|
| 1210 |
+
*Sistema de Visualización ODS | Octubre 2025 | Desarrollado con Python, Plotly, Matplotlib y Gradio*
|
| 1211 |
+
""")
|
| 1212 |
+
|
| 1213 |
+
return app
|
| 1214 |
+
|
| 1215 |
+
# ============================================================================
|
| 1216 |
+
# EJECUCIÓN DE LA APLICACIÓN
|
| 1217 |
+
# ============================================================================
|
| 1218 |
+
|
| 1219 |
+
if __name__ == "__main__":
|
| 1220 |
+
print("\n" + "="*70)
|
| 1221 |
+
print("INICIANDO APLICACIÓN GRADIO - VISUALIZACIONES ODS")
|
| 1222 |
+
print("="*70)
|
| 1223 |
+
|
| 1224 |
+
# if not DATOS_CARGADOS:
|
| 1225 |
+
# print("\n⚠️ ADVERTENCIA: No se pudieron cargar los datos.")
|
| 1226 |
+
# print(" Verifica que el archivo existe en:", RUTA_DATOS)
|
| 1227 |
+
# print(" La aplicación se iniciará pero mostrará errores.")
|
| 1228 |
+
# else:
|
| 1229 |
+
# print(f"\n✓ Datos cargados correctamente: {len(df_global)} registros")
|
| 1230 |
+
# print(f"✓ ODS únicos: {df_global['ods_id'].nunique()}")
|
| 1231 |
+
|
| 1232 |
+
print("\n" + "="*70)
|
| 1233 |
+
print("CREANDO APLICACIÓN...")
|
| 1234 |
+
print("="*70)
|
| 1235 |
+
|
| 1236 |
+
app = crear_app()
|
| 1237 |
+
|
| 1238 |
+
print("\n✓ Aplicación creada exitosamente")
|
| 1239 |
+
print("\n" + "="*70)
|
| 1240 |
+
print("INICIANDO SERVIDOR WEB...")
|
| 1241 |
+
print("="*70)
|
| 1242 |
+
print("\n🌐 La aplicación se abrirá en tu navegador automáticamente")
|
| 1243 |
+
print("📍 URL local: http://127.0.0.1:7860")
|
| 1244 |
+
print("🌍 URL pública: Se generará si share=True\n")
|
| 1245 |
+
print("💡 Presiona Ctrl+C para detener el servidor\n")
|
| 1246 |
+
|
| 1247 |
+
# Lanzar la aplicación
|
| 1248 |
+
app.launch(
|
| 1249 |
+
# server_name="0.0.0.0", # Permite acceso desde cualquier IP
|
| 1250 |
+
# server_port=7860, # Puerto por defecto
|
| 1251 |
+
# share=False, # Cambiar a True para URL pública
|
| 1252 |
+
# show_error=True, # Mostrar errores en la interfaz
|
| 1253 |
+
# quiet=False # Mostrar logs en consola
|
| 1254 |
+
)
|
src/app_graficas.ipynb
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
src/modelos_nlp_db.py
ADDED
|
@@ -0,0 +1,532 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
|
| 2 |
+
# ============================================================================
|
| 3 |
+
# Funciones generales PLN
|
| 4 |
+
# ============================================================================
|
| 5 |
+
|
| 6 |
+
import argparse, os, json, hashlib, pandas as pd, numpy as np
|
| 7 |
+
from pathlib import Path
|
| 8 |
+
import re
|
| 9 |
+
|
| 10 |
+
def md5_text(s: str) -> str:
|
| 11 |
+
return hashlib.md5(s.encode('utf-8')).hexdigest()
|
| 12 |
+
|
| 13 |
+
def build_ods_fingerprint(model_name: str, instruction: str, ods_texts: list) -> str:
|
| 14 |
+
concat = model_name + "\n" + instruction + "\n" + "\n".join(ods_texts)
|
| 15 |
+
return md5_text(concat)
|
| 16 |
+
|
| 17 |
+
def ensure_out_dir(p: str):
|
| 18 |
+
Path(p).mkdir(parents=True, exist_ok=True)
|
| 19 |
+
|
| 20 |
+
def load_data(patr_tblinput: str, ods_tblinput: str):
|
| 21 |
+
# patr = pd.read_tblinput(patr_tblinput)
|
| 22 |
+
# ods = pd.read_tblinput(ods_tblinput)
|
| 23 |
+
patr = pd.read_excel(patr_tblinput)#, encoding='cp1252')
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
ods = pd.read_excel(ods_tblinput)#.iloc[:32,:]
|
| 27 |
+
# Basic validations
|
| 28 |
+
assert {"ID", "INICIATIVAS", "MUNICIPIO"}.issubset(patr.columns), "PATR CSV must include columns: ID, INICIATIVAS"
|
| 29 |
+
assert {'OBJETIVO', 'OBJETIVO_META', 'INDICADORES', 'CODIGO_UNSD',
|
| 30 |
+
'ID_OBJETIVO', 'ID_META', 'ID_INDICADORES'}.issubset(ods.columns), "ODS CSV must include columns: OBJETIVO, OBJETIVO_META, INDICADORES, CODIGO_UNSD,ID_OBJETIVO, ID_META, ID_INDICADORES"
|
| 31 |
+
return patr, ods
|
| 32 |
+
|
| 33 |
+
def make_text_pairs(instruction: str, texts: list):
|
| 34 |
+
return [[instruction, t if isinstance(t,str) else ""] for t in texts]
|
| 35 |
+
|
| 36 |
+
def compute_embeddings(model, pairs, batch_size: int, normalize: bool):
|
| 37 |
+
# SentenceTransformer.encode has normalize_embeddings parameter
|
| 38 |
+
return model.encode(
|
| 39 |
+
pairs,
|
| 40 |
+
batch_size=batch_size,
|
| 41 |
+
convert_to_tensor=True,
|
| 42 |
+
show_progress_bar=True,
|
| 43 |
+
normalize_embeddings=normalize
|
| 44 |
+
)
|
| 45 |
+
|
| 46 |
+
def cosine_sim_matrix(a, b):
|
| 47 |
+
# a: (N,d) tensor, b: (M,d) tensor
|
| 48 |
+
from sentence_transformers import util
|
| 49 |
+
sims = util.cos_sim(a, b).cpu().numpy()
|
| 50 |
+
return sims
|
| 51 |
+
|
| 52 |
+
# def save_cache(cache_path: str, meta: dict, emb_np: np.ndarray):
|
| 53 |
+
# np.savez(cache_path, embeddings=emb_np, meta=json.dumps(meta, ensure_ascii=False))
|
| 54 |
+
|
| 55 |
+
# def load_cache(cache_path: str):
|
| 56 |
+
# data = np.load(cache_path, allow_pickle=True)
|
| 57 |
+
# emb = data["embeddings"]
|
| 58 |
+
# meta = json.loads(str(data["meta"]))
|
| 59 |
+
# return emb, meta
|
| 60 |
+
|
| 61 |
+
def save_cache(cache_path: str, meta: dict, emb_np: np.ndarray):
|
| 62 |
+
np.savez(cache_path, embeddings=emb_np) # solo arrays
|
| 63 |
+
with open(cache_path + ".json", "w", encoding="utf-8") as f:
|
| 64 |
+
json.dump(meta, f, ensure_ascii=False) # meta en JSON sidecar
|
| 65 |
+
|
| 66 |
+
def load_cache(cache_path: str):
|
| 67 |
+
emb = np.load(cache_path)["embeddings"]
|
| 68 |
+
with open(cache_path + ".json", "r", encoding="utf-8") as f:
|
| 69 |
+
meta = json.load(f)
|
| 70 |
+
return emb, meta
|
| 71 |
+
|
| 72 |
+
import spacy
|
| 73 |
+
|
| 74 |
+
def limpiar_texto(texto, nlp):
|
| 75 |
+
"""
|
| 76 |
+
Limpia nombres propios, entidades y caracteres especiales del texto.
|
| 77 |
+
Conserva la primera palabra de cada oración (aunque esté en mayúscula).
|
| 78 |
+
"""
|
| 79 |
+
if not texto or not isinstance(texto, str):
|
| 80 |
+
return ""
|
| 81 |
+
|
| 82 |
+
# 1️⃣ Remover caracteres especiales innecesarios (antes del análisis)
|
| 83 |
+
# Mantiene letras, números, espacios y signos básicos de puntuación.
|
| 84 |
+
texto = re.sub(r"[^A-Za-zÁÉÍÓÚÜÑáéíóúüñ0-9\s.,;:!?()\-]", " ", texto)
|
| 85 |
+
|
| 86 |
+
# 2️⃣ Procesamiento lingüístico
|
| 87 |
+
doc = nlp(texto)
|
| 88 |
+
resultado = []
|
| 89 |
+
|
| 90 |
+
for sent in doc.sents:
|
| 91 |
+
tokens = []
|
| 92 |
+
for i, token in enumerate(sent):
|
| 93 |
+
# eliminar puntuación y símbolos
|
| 94 |
+
if token.is_punct or token.is_space or token.is_digit:
|
| 95 |
+
continue
|
| 96 |
+
# Mantiene primera palabra de cada oración
|
| 97 |
+
if i == 0:
|
| 98 |
+
tokens.append(token.text)
|
| 99 |
+
# Elimina nombres propios o entidades nombradas
|
| 100 |
+
elif token.pos_ == "PROPN" or token.ent_type_ in ["PER", "ORG", "LOC", "GPE"]:
|
| 101 |
+
continue
|
| 102 |
+
else:
|
| 103 |
+
tokens.append(token.text)
|
| 104 |
+
resultado.append(" ".join(tokens))
|
| 105 |
+
|
| 106 |
+
# 3️⃣ Limpiar puntuación repetida y espacios múltiples
|
| 107 |
+
texto_limpio = " ".join(resultado)
|
| 108 |
+
texto_limpio = re.sub(r"\s{2,}", " ", texto_limpio).strip()
|
| 109 |
+
|
| 110 |
+
# 4️⃣ Opcional: eliminar espacios antes de comas o puntos
|
| 111 |
+
texto_limpio = re.sub(r"\s+([,.!?;:])", r"\1", texto_limpio)
|
| 112 |
+
|
| 113 |
+
return texto_limpio
|
| 114 |
+
|
| 115 |
+
|
| 116 |
+
# ============================================================================
|
| 117 |
+
# Generador de cache para generar embeddings nuevas tablas
|
| 118 |
+
# ============================================================================
|
| 119 |
+
|
| 120 |
+
def genCache(cache_name:str, tbl_input_dir:str, out_dir:str, instruction:str, batch_size = 32, normalize = True, cache_path = None, force_recompute = False):
|
| 121 |
+
|
| 122 |
+
model_name = "hkunlp/instructor-large" #help="HF model name for embeddings.")
|
| 123 |
+
# instruction = "Representa el tema central del siguiente objetivo de desarrollo sostenible" #"Instruction for ODS texts.")
|
| 124 |
+
ensure_out_dir(out_dir)
|
| 125 |
+
|
| 126 |
+
# Load data
|
| 127 |
+
input_df = pd.read_excel(tbl_input_dir)
|
| 128 |
+
input_texts = (input_df["ods"].fillna("") + ". " + input_df["descripcion"].fillna("")).tolist()
|
| 129 |
+
|
| 130 |
+
# Compute fingerprint and cache path
|
| 131 |
+
fingerprint = build_ods_fingerprint(model_name, instruction, input_texts)
|
| 132 |
+
cache_path = cache_path or os.path.join(out_dir, f"{cache_name}_{fingerprint}.npz")
|
| 133 |
+
|
| 134 |
+
# Lazy import model to allow quick --help
|
| 135 |
+
from sentence_transformers import SentenceTransformer
|
| 136 |
+
|
| 137 |
+
model = SentenceTransformer(model_name)
|
| 138 |
+
input_pairs = make_text_pairs(instruction, input_texts)
|
| 139 |
+
emb_input = compute_embeddings(model, input_pairs, batch_size=batch_size, normalize=normalize)
|
| 140 |
+
emb_input_np = emb_input.cpu().numpy()
|
| 141 |
+
save_cache(cache_path, {"model": model_name, "instr": instruction, "count": len(input_texts)}, emb_input_np)
|
| 142 |
+
|
| 143 |
+
# ============================================================================
|
| 144 |
+
# Función generadora tablas
|
| 145 |
+
# ============================================================================
|
| 146 |
+
|
| 147 |
+
import torch
|
| 148 |
+
import pandas as pd
|
| 149 |
+
import numpy as np
|
| 150 |
+
|
| 151 |
+
|
| 152 |
+
def search(query):
|
| 153 |
+
patr_tblinput = '/content/drive/MyDrive/Compartida/06_Desarrollo de la herramienta IA/01_MPTF /insumos/Copy of Iniciativas priorizadas PATR 385.xlsx' #"CSV with PATR projects (columns: id, descripcion, ...).")
|
| 154 |
+
ods_tblinput = '/content/drive/MyDrive/Compartida/06_Desarrollo de la herramienta IA/01_MPTF /documentos_para_revision/tabla_odsDescripcion.xlsx'
|
| 155 |
+
meta_tblinput = '/content/drive/MyDrive/Compartida/06_Desarrollo de la herramienta IA/01_MPTF /documentos_para_revision/tabla_lvlMetaOds.xlsx'
|
| 156 |
+
indicador_tblinput = '/content/drive/MyDrive/Compartida/06_Desarrollo de la herramienta IA/01_MPTF /documentos_para_revision/marco_ods_ids.xlsx'
|
| 157 |
+
genero_tblinput = '/content/drive/MyDrive/Compartida/06_Desarrollo de la herramienta IA/01_MPTF /documentos_para_revision/genero.xlsx'
|
| 158 |
+
poblacional_tblinput = '/content/drive/MyDrive/Compartida/06_Desarrollo de la herramienta IA/01_MPTF /documentos_para_revision/poblacional.xlsx'
|
| 159 |
+
etnico_tblinput = '/content/drive/MyDrive/Compartida/06_Desarrollo de la herramienta IA/01_MPTF /documentos_para_revision/etnico.xlsx'
|
| 160 |
+
pilares_tblinput = '/content/drive/MyDrive/Compartida/06_Desarrollo de la herramienta IA/01_MPTF /documentos_para_revision/pilares.xlsx' #"CSV with ODS list (columns: ods_id, titulo, descripcion).")
|
| 161 |
+
categorias_tblinput = '/content/drive/MyDrive/Compartida/06_Desarrollo de la herramienta IA/01_MPTF /documentos_para_revision/categorias.xlsx'
|
| 162 |
+
estrategias_tblinput = '/content/drive/MyDrive/Compartida/06_Desarrollo de la herramienta IA/01_MPTF /documentos_para_revision/estrategias.xlsx'
|
| 163 |
+
out_dir = '/content/drive/MyDrive/Compartida/06_Desarrollo de la herramienta IA/01_MPTF /archivos_trabajo/salidas/modelo_instructor/data/out' #"Output directory.")
|
| 164 |
+
model_name = "hkunlp/instructor-large" #help="HF model name for embeddings.")
|
| 165 |
+
instr_proj = "Representa el propósito de desarrollo sostenible del siguiente proyecto territorial" #"Instruction for PATR projects.")
|
| 166 |
+
instr_ods = "Representa el tema central del siguiente ODS" #"Instruction for ODS texts.")
|
| 167 |
+
batch_size = 32 #"Batch size for encoding.")
|
| 168 |
+
top_k = 5 #"Top-K ODS to retrieve.")
|
| 169 |
+
normalize = True #"L2-normalize embeddings during encoding.") # Changed from "store_true" to boolean
|
| 170 |
+
cache_path = None #"Path to cache npz for ODS embeddings (auto if not set).")
|
| 171 |
+
force_recompute = False #"Ignore cache and recompute ODS embeddings.") # Changed from "store_true" to boolean
|
| 172 |
+
|
| 173 |
+
|
| 174 |
+
ensure_out_dir(out_dir)
|
| 175 |
+
|
| 176 |
+
#"OBJETIVO","OBJETIVO_META","INDICADORES","CODIGO_UNSD"
|
| 177 |
+
|
| 178 |
+
# Load data
|
| 179 |
+
# patr_df, ods_df = load_data(patr_tblinput, ods_tblinput)
|
| 180 |
+
# patr_df = patr_df[['ID', 'INICIATIVAS']].drop_duplicates().reset_index(drop=True) # Reset index
|
| 181 |
+
# patr_texts = patr_df["INICIATIVAS"].fillna("").tolist()
|
| 182 |
+
# patr_df = pd.read_excel(patr_tblinput)
|
| 183 |
+
ods_df = pd.read_excel(ods_tblinput)
|
| 184 |
+
meta_df = pd.read_excel(meta_tblinput)
|
| 185 |
+
inidicador_df = pd.read_excel(indicador_tblinput)
|
| 186 |
+
genero_df = pd.read_excel(genero_tblinput)
|
| 187 |
+
poblacional_df = pd.read_excel(poblacional_tblinput)
|
| 188 |
+
etnico_df = pd.read_excel(etnico_tblinput)
|
| 189 |
+
pilares_df = pd.read_excel(pilares_tblinput)
|
| 190 |
+
estrategias_df = pd.read_excel(estrategias_tblinput)
|
| 191 |
+
categorias_df = pd.read_excel(categorias_tblinput)
|
| 192 |
+
|
| 193 |
+
nlp = spacy.load("es_core_news_md")
|
| 194 |
+
query = limpiar_texto(query, nlp)
|
| 195 |
+
patr_texts = list([query])
|
| 196 |
+
# print(len(patr_texts))
|
| 197 |
+
ods_texts = (ods_df["ods"].fillna("") + ". " + ods_df["descripcion"].fillna("")).tolist()
|
| 198 |
+
meta_texts = (meta_df["OBJETIVO"].fillna("") + ". " + meta_df["META"].fillna("")).tolist()
|
| 199 |
+
indicadores_texts = (inidicador_df["OBJETIVO"].fillna("") + ". " + inidicador_df["INDICADORES"].fillna("")).tolist()
|
| 200 |
+
genero_texts = (genero_df["DESCRIPCION"].fillna("")).tolist()
|
| 201 |
+
poblacional_texts = (poblacional_df["DESCRIPCION"].fillna("")).tolist()
|
| 202 |
+
etnico_texts = (etnico_df["DESCRIPCION"].fillna("")).tolist()
|
| 203 |
+
# ods_texts = (ods_df["OBJETIVO"].fillna("") + ". " + ods_df["INDICADORES"].fillna("")).tolist()
|
| 204 |
+
pilares_texts = (pilares_df["PILAR"].fillna("") + ". " + pilares_df["DESCRIPCION"].fillna("") + ". " + pilares_df["SUSTENTO"].fillna("")).tolist()
|
| 205 |
+
estrategias_texts = (estrategias_df["ESTRATEGIA"].fillna("") + ". " + estrategias_df["DESCRIPCION"].fillna("")).tolist()
|
| 206 |
+
categorias_texts = (categorias_df["CATEGORIA"].fillna("") + ". " + categorias_df["DESCRIPCION"].fillna("")).tolist()
|
| 207 |
+
# print(len(ods_texts))
|
| 208 |
+
|
| 209 |
+
texts = [ods_texts, meta_texts, indicadores_texts, genero_texts, poblacional_texts, etnico_texts, pilares_texts, estrategias_texts, categorias_texts]
|
| 210 |
+
|
| 211 |
+
# print('texts')
|
| 212 |
+
# print([len(x) for x in texts])
|
| 213 |
+
|
| 214 |
+
instruc_bases = ["Representa el tema central del siguiente objetivo de desarrollo sostenible", "Representa el tema central de la siguiente meta de desarrollo sostenible",
|
| 215 |
+
"Representa el tema central del siguiente ODS", "Representa el tema central del siguiente de enfoque", "Representa el tema central del siguiente de enfoque poblacional",
|
| 216 |
+
"Representa el tema central del siguiente de enfoque etnico",
|
| 217 |
+
"Representa el tema de los siguiente ejes temáticos y estratégicos", "Representa el tema de las siguiente estrategias","Representa el tema de las siguientes categorias"]
|
| 218 |
+
|
| 219 |
+
instruc_iniciativas = ["Representa el siguiente proyecto territorial en terminos de los objetivos de desarrollo sostenible ", "Representa el siguiente proyecto territorial en terminos de las metas de desarrollo sostenible",
|
| 220 |
+
"Representa el siguiente proyecto territorial en terminos de los indicadores de desarrollo sostenible", "Representa el siguiente proyecto territorial en terminos del enfoque de genero", "Representa el siguiente proyecto territorial en terminos del enfoque poblacional",
|
| 221 |
+
"Representa el siguiente proyecto territorial en terminos del enfoque etnico",
|
| 222 |
+
"Representa el siguiente proyecto territorial en terminos de ejes temáticos y estratégicos", "Representa el siguiente proyecto territorial en terminos de la estrategia", "Representa el siguiente proyecto territorial en terminos de la categoria"]
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
# Compute fingerprint and cache path
|
| 227 |
+
# fingerprint = build_ods_fingerprint(model_name, instr_ods, ods_texts)
|
| 228 |
+
# fingerprint = [build_ods_fingerprint(model_name, instr, texts[idx]) for idx, instr in enumerate(instruc_bases)]
|
| 229 |
+
fingerprint = ['7cb4c79002a04c14d92c9e1e4e9b251a','fe327349acadb19200187b58a565304b','07948e6beafe34049ca8a7309363eee2','9a4c52cf18e95c52566c0b657a25c44f','5a8b0dd04b865e8f1c356a64795b3b67',
|
| 230 |
+
'c0973f650cac27181b3751aa9666819b','0a475def7da8551abdd502e1d042dc00','42e4e8bfb28dc47602e662a27d8b4e76','e0338741fd4e7b08ab7f92a32e08919b']
|
| 231 |
+
|
| 232 |
+
|
| 233 |
+
ods_cache_path = cache_path or os.path.join(out_dir, f"tabla_odsDescripcion_{fingerprint[0]}.npz")
|
| 234 |
+
meta_cache_path = cache_path or os.path.join(out_dir, f"tabla_lvlMetaOds_{fingerprint[1]}.npz")
|
| 235 |
+
indicadores_cache_path = cache_path or os.path.join(out_dir, f"ods_embeddings_{fingerprint[2]}.npz")
|
| 236 |
+
genero_cache_path = cache_path or os.path.join(out_dir, f"tabla_genero_{fingerprint[3]}.npz")
|
| 237 |
+
poblacional_cache_path = cache_path or os.path.join(out_dir, f"tabla_poblacional_{fingerprint[4]}.npz")
|
| 238 |
+
etnico_cache_path = cache_path or os.path.join(out_dir, f"tabla_etnico_{fingerprint[5]}.npz")
|
| 239 |
+
pilaresPdet_cache_path = cache_path or os.path.join(out_dir, f"pilaresPdet_embeddings_{fingerprint[6]}.npz")
|
| 240 |
+
estrategiasPdet_cache_path = cache_path or os.path.join(out_dir, f"estrategiasPdet_embeddings_{fingerprint[7]}.npz")
|
| 241 |
+
categoriasPdet_cache_path = cache_path or os.path.join(out_dir, f"categoriasPdet_embeddings_{fingerprint[8]}.npz")
|
| 242 |
+
|
| 243 |
+
cache_paths = [ods_cache_path, meta_cache_path, indicadores_cache_path, genero_cache_path, poblacional_cache_path, etnico_cache_path, pilaresPdet_cache_path, estrategiasPdet_cache_path, categoriasPdet_cache_path]
|
| 244 |
+
|
| 245 |
+
print('cache_paths')
|
| 246 |
+
print([x for x in cache_paths])
|
| 247 |
+
|
| 248 |
+
# Lazy import model to allow quick --help
|
| 249 |
+
from sentence_transformers import SentenceTransformer
|
| 250 |
+
|
| 251 |
+
# Load / compute ODS embeddings with cache
|
| 252 |
+
ods_use_cache = (not force_recompute) and os.path.exists(ods_cache_path)
|
| 253 |
+
meta_use_cache = (not force_recompute) and os.path.exists(meta_cache_path)
|
| 254 |
+
indicadores_use_cache = (not force_recompute) and os.path.exists(indicadores_cache_path)
|
| 255 |
+
genero_use_cache = (not force_recompute) and os.path.exists(genero_cache_path)
|
| 256 |
+
poblacional_use_cache = (not force_recompute) and os.path.exists(poblacional_cache_path)
|
| 257 |
+
etnico_use_cache = (not force_recompute) and os.path.exists(etnico_cache_path)
|
| 258 |
+
pilaresPdet_use_cache = (not force_recompute) and os.path.exists(pilaresPdet_cache_path)
|
| 259 |
+
estrategiasPdet_use_cache = (not force_recompute) and os.path.exists(estrategiasPdet_cache_path)
|
| 260 |
+
categoriasPdet_use_cache = (not force_recompute) and os.path.exists(categoriasPdet_cache_path)
|
| 261 |
+
|
| 262 |
+
matrix_unfpa = []
|
| 263 |
+
caches = [ods_use_cache, meta_use_cache, indicadores_use_cache, genero_use_cache, poblacional_use_cache, etnico_use_cache,
|
| 264 |
+
pilaresPdet_use_cache, estrategiasPdet_use_cache, categoriasPdet_use_cache]
|
| 265 |
+
|
| 266 |
+
for idx, i_cache in enumerate(caches):
|
| 267 |
+
# print(cache_paths[idx])
|
| 268 |
+
|
| 269 |
+
if i_cache:
|
| 270 |
+
emb_unfpa_np, meta = load_cache(cache_paths[idx])
|
| 271 |
+
# Minimal safety check: same model/instruction length
|
| 272 |
+
if meta.get("model_name") != model_name or meta.get("instr") != instruc_bases[idx] or meta.get("count") != len(texts[idx]):
|
| 273 |
+
print(f'Diferencias en carga de metadata nlp cache {cache_paths[idx]}:')
|
| 274 |
+
print(meta.get("model_name"), model_name)
|
| 275 |
+
print(meta.get("instr"), instruc_bases[idx])
|
| 276 |
+
print(meta.get("count"),len(texts[idx]))
|
| 277 |
+
# i_cache = False
|
| 278 |
+
|
| 279 |
+
if not i_cache:
|
| 280 |
+
print(f'no se encontro cache de id : {idx}')
|
| 281 |
+
# model = SentenceTransformer(model_name)
|
| 282 |
+
# ods_pairs = make_text_pairs(instruc_bases[idx], texts[idx])
|
| 283 |
+
# emb_ods = compute_embeddings(model, ods_pairs, batch_size=batch_size, normalize=normalize)
|
| 284 |
+
# emb_unfpa_np = emb_ods.cpu().numpy()
|
| 285 |
+
# save_cache(cache_paths[idx], {"model_name": model_name, "instr": instruc_bases[idx], "count": len(texts[idx])}, emb_unfpa_np)
|
| 286 |
+
else:
|
| 287 |
+
model = SentenceTransformer(model_name) # still needed for project embeddings
|
| 288 |
+
|
| 289 |
+
# Compute PATR embeddings
|
| 290 |
+
patr_pairs = make_text_pairs(instruc_iniciativas[idx], patr_texts)
|
| 291 |
+
emb_patr = compute_embeddings(model, patr_pairs, batch_size=batch_size, normalize=normalize)
|
| 292 |
+
|
| 293 |
+
# Convert ODS (np.ndarray) to torch.Tensor and move it to the same device as emb_patr
|
| 294 |
+
emb_unfpa_t = torch.from_numpy(emb_unfpa_np).to(emb_patr.device)
|
| 295 |
+
|
| 296 |
+
# Similarity
|
| 297 |
+
from sentence_transformers import util
|
| 298 |
+
sim_matrix_ = util.cos_sim(emb_patr, emb_unfpa_t).cpu().numpy()
|
| 299 |
+
|
| 300 |
+
matrix_unfpa.append(sim_matrix_)
|
| 301 |
+
|
| 302 |
+
print([len(x) for x in matrix_unfpa])
|
| 303 |
+
|
| 304 |
+
# tops_k = [5,1,1,1] # ods_use_cache, pilaresPdet_use_cache, estrategiasPdet_use_cache, categoriasPdet_use_cache
|
| 305 |
+
tops_k = [len(ods_texts),len(meta_texts),len(indicadores_texts),1,1,1,1,1,1]
|
| 306 |
+
res_dfs = []
|
| 307 |
+
|
| 308 |
+
for idx, top in enumerate(tops_k):
|
| 309 |
+
sim_matrix = matrix_unfpa[idx]
|
| 310 |
+
# Top-K per project
|
| 311 |
+
# K = min(top_k, sim_matrix.shape[1])
|
| 312 |
+
K = min(top, sim_matrix.shape[1])
|
| 313 |
+
top_rows = []
|
| 314 |
+
for i in range(sim_matrix.shape[0]):
|
| 315 |
+
sims = sim_matrix[i]
|
| 316 |
+
# rt descending and take first K
|
| 317 |
+
top_idx = np.argsort(-sims)[:K]
|
| 318 |
+
# ods_df
|
| 319 |
+
# meta_df
|
| 320 |
+
# inidicador_df
|
| 321 |
+
# genero_df
|
| 322 |
+
# poblacional_df
|
| 323 |
+
# etnico_df
|
| 324 |
+
# pilares_df
|
| 325 |
+
# estrategias_df
|
| 326 |
+
# categorias_df
|
| 327 |
+
|
| 328 |
+
#### RESULTADOS PARA DESCRIPCION ODS
|
| 329 |
+
if idx == 0:
|
| 330 |
+
for rank, j in enumerate(top_idx, start=1):
|
| 331 |
+
row = {
|
| 332 |
+
# "project_id": patr_df.iloc[i, patr_df.columns.get_loc("ID")], # Use iloc with positional index
|
| 333 |
+
# "project_text": patr_df.iloc[i, patr_df.columns.get_loc("INICIATIVAS")], # Use iloc with positional index
|
| 334 |
+
"ODS_ID": ods_df.iloc[j, ods_df.columns.get_loc("id_ods")], # Use iloc with positional index
|
| 335 |
+
"OBJETIVO": ods_df.iloc[j, ods_df.columns.get_loc("ods")], # Use iloc with positional index
|
| 336 |
+
|
| 337 |
+
# "ods_texto": ods_texts[j],
|
| 338 |
+
"ods_rank": rank,
|
| 339 |
+
"ods_similaridad_cos": float(sims[j]),
|
| 340 |
+
# "ods_titulo": ods_df.iloc[j, ods_df.columns.get_loc("INDICADORES")], # Use iloc with positional index
|
| 341 |
+
# "ods_texto": ods_texts[j]
|
| 342 |
+
}
|
| 343 |
+
top_rows.append(row)
|
| 344 |
+
|
| 345 |
+
#### RESULTADOS PARA METAS ODS
|
| 346 |
+
if idx == 1:
|
| 347 |
+
for rank, j in enumerate(top_idx, start=1):
|
| 348 |
+
row = {
|
| 349 |
+
# "project_id": patr_df.iloc[i, patr_df.columns.get_loc("ID")], # Use iloc with positional index
|
| 350 |
+
# "project_text": patr_df.iloc[i, patr_df.columns.get_loc("INICIATIVAS")], # Use iloc with positional index
|
| 351 |
+
"META_ID": meta_df.iloc[j, meta_df.columns.get_loc("ID_META")], # Use iloc with positional index
|
| 352 |
+
"META": meta_df.iloc[j, meta_df.columns.get_loc("META")], # Use iloc with positional index
|
| 353 |
+
"ODS_ID": meta_df.iloc[j, meta_df.columns.get_loc("ID_OBJETIVO")],
|
| 354 |
+
|
| 355 |
+
# "ods_texto": ods_texts[j],
|
| 356 |
+
"meta_rank": rank,
|
| 357 |
+
"meta_similaridad_cos": float(sims[j]),
|
| 358 |
+
# "ods_titulo": ods_df.iloc[j, ods_df.columns.get_loc("INDICADORES")], # Use iloc with positional index
|
| 359 |
+
# "ods_texto": ods_texts[j]
|
| 360 |
+
}
|
| 361 |
+
top_rows.append(row)
|
| 362 |
+
|
| 363 |
+
#### RESULTADOS PARA INDICADORES ODS
|
| 364 |
+
if idx == 2:
|
| 365 |
+
for rank, j in enumerate(top_idx, start=1):
|
| 366 |
+
row = {
|
| 367 |
+
# "project_id": patr_df.iloc[i, patr_df.columns.get_loc("ID")], # Use iloc with positional index
|
| 368 |
+
# "project_text": patr_df.iloc[i, patr_df.columns.get_loc("INICIATIVAS")], # Use iloc with positional index
|
| 369 |
+
"INDICADOR_ID": inidicador_df.iloc[j, inidicador_df.columns.get_loc("ID_INDICADORES")], # Use iloc with positional index
|
| 370 |
+
"INDICADOR": inidicador_df.iloc[j, inidicador_df.columns.get_loc("INDICADORES")], # Use iloc with positional index
|
| 371 |
+
"ODS_ID": inidicador_df.iloc[j, inidicador_df.columns.get_loc("ID_ODS")],
|
| 372 |
+
"META_ID": inidicador_df.iloc[j, inidicador_df.columns.get_loc("ID_META")],
|
| 373 |
+
|
| 374 |
+
# "ods_texto": ods_texts[j],
|
| 375 |
+
"indicador_rank": rank,
|
| 376 |
+
"indicador_similaridad_cos": float(sims[j]),
|
| 377 |
+
# "ods_titulo": ods_df.iloc[j, ods_df.columns.get_loc("INDICADORES")], # Use iloc with positional index
|
| 378 |
+
# "ods_texto": ods_texts[j]
|
| 379 |
+
}
|
| 380 |
+
top_rows.append(row)
|
| 381 |
+
|
| 382 |
+
#### RESULTADOS PARA ENFOQUE GENERO
|
| 383 |
+
if idx == 3:
|
| 384 |
+
for rank, j in enumerate(top_idx, start=1):
|
| 385 |
+
row = {
|
| 386 |
+
# "project_id": patr_df.iloc[i, patr_df.columns.get_loc("ID")], # Use iloc with positional index
|
| 387 |
+
# "project_text": patr_df.iloc[i, patr_df.columns.get_loc("INICIATIVAS")], # Use iloc with positional index
|
| 388 |
+
"ENFOQUE_GENERO": genero_df.iloc[j, genero_df.columns.get_loc("CATEGORIA")], # Use iloc with positional index
|
| 389 |
+
# "INDICADOR": genero_df.iloc[j, genero_df.columns.get_loc("INDICADORES")], # Use iloc with positional index
|
| 390 |
+
|
| 391 |
+
# "ods_texto": ods_texts[j],
|
| 392 |
+
"rank": rank,
|
| 393 |
+
"similaridad_cos": float(sims[j]),
|
| 394 |
+
# "ods_titulo": ods_df.iloc[j, ods_df.columns.get_loc("INDICADORES")], # Use iloc with positional index
|
| 395 |
+
# "ods_texto": ods_texts[j]
|
| 396 |
+
}
|
| 397 |
+
top_rows.append(row)
|
| 398 |
+
|
| 399 |
+
#### RESULTADOS PARA ENFOQUE POBLACIONAL
|
| 400 |
+
if idx == 4:
|
| 401 |
+
for rank, j in enumerate(top_idx, start=1):
|
| 402 |
+
row = {
|
| 403 |
+
# "project_id": patr_df.iloc[i, patr_df.columns.get_loc("ID")], # Use iloc with positional index
|
| 404 |
+
# "project_text": patr_df.iloc[i, patr_df.columns.get_loc("INICIATIVAS")], # Use iloc with positional index
|
| 405 |
+
"ENFOQUE_POBLACIONAL": poblacional_df.iloc[j, poblacional_df.columns.get_loc("CATEGORIA")], # Use iloc with positional index
|
| 406 |
+
# "INDICADOR": poblacional_df.iloc[j, poblacional_df.columns.get_loc("INDICADORES")], # Use iloc with positional index
|
| 407 |
+
|
| 408 |
+
# "ods_texto": ods_texts[j],
|
| 409 |
+
"rank": rank,
|
| 410 |
+
"similaridad_cos": float(sims[j]),
|
| 411 |
+
# "ods_titulo": ods_df.iloc[j, ods_df.columns.get_loc("INDICADORES")], # Use iloc with positional index
|
| 412 |
+
# "ods_texto": ods_texts[j]
|
| 413 |
+
}
|
| 414 |
+
top_rows.append(row)
|
| 415 |
+
|
| 416 |
+
#### RESULTADOS PARA ENFOQUE ETNICO
|
| 417 |
+
if idx == 5:
|
| 418 |
+
for rank, j in enumerate(top_idx, start=1):
|
| 419 |
+
row = {
|
| 420 |
+
# "project_id": patr_df.iloc[i, patr_df.columns.get_loc("ID")], # Use iloc with positional index
|
| 421 |
+
# "project_text": patr_df.iloc[i, patr_df.columns.get_loc("INICIATIVAS")], # Use iloc with positional index
|
| 422 |
+
"ENFOQUE_POBLACIONAL": etnico_df.iloc[j, etnico_df.columns.get_loc("CATEGORIA")], # Use iloc with positional index
|
| 423 |
+
# "INDICADOR": etnico_df.iloc[j, etnico_df.columns.get_loc("INDICADORES")], # Use iloc with positional index
|
| 424 |
+
|
| 425 |
+
# "ods_texto": ods_texts[j],
|
| 426 |
+
"rank": rank,
|
| 427 |
+
"similaridad_cos": float(sims[j]),
|
| 428 |
+
# "ods_titulo": ods_df.iloc[j, ods_df.columns.get_loc("INDICADORES")], # Use iloc with positional index
|
| 429 |
+
# "ods_texto": ods_texts[j]
|
| 430 |
+
}
|
| 431 |
+
top_rows.append(row)
|
| 432 |
+
|
| 433 |
+
#### RESULTADOS PARA PILARES
|
| 434 |
+
if idx == 6:
|
| 435 |
+
for rank, j in enumerate(top_idx, start=1):
|
| 436 |
+
row = {
|
| 437 |
+
"rank": rank,
|
| 438 |
+
"similaridad_cos": float(sims[j]),
|
| 439 |
+
"pilar_texto": pilares_texts[j]
|
| 440 |
+
}
|
| 441 |
+
top_rows.append(row)
|
| 442 |
+
|
| 443 |
+
#### RESULTADOS PARA ESTRATEGIAS
|
| 444 |
+
if idx == 7:
|
| 445 |
+
for rank, j in enumerate(top_idx, start=1):
|
| 446 |
+
row = {
|
| 447 |
+
"rank": rank,
|
| 448 |
+
"similaridad_cos": float(sims[j]),
|
| 449 |
+
"estrategia_texto": estrategias_texts[j]
|
| 450 |
+
}
|
| 451 |
+
top_rows.append(row)
|
| 452 |
+
|
| 453 |
+
#### RESULTADOS PARA CATEGORIAS
|
| 454 |
+
if idx == 8:
|
| 455 |
+
for rank, j in enumerate(top_idx, start=1):
|
| 456 |
+
row = {
|
| 457 |
+
"rank": rank,
|
| 458 |
+
"similaridad_cos": float(sims[j]),
|
| 459 |
+
"categoria_texto": categorias_texts[j]
|
| 460 |
+
}
|
| 461 |
+
top_rows.append(row)
|
| 462 |
+
|
| 463 |
+
|
| 464 |
+
res_df = pd.DataFrame(top_rows).drop_duplicates()
|
| 465 |
+
res_dfs.append(res_df)
|
| 466 |
+
|
| 467 |
+
# Additionally, export a simple edges file (Top-1) for graph visualizations
|
| 468 |
+
# edges = []
|
| 469 |
+
# df_edges = pd.DataFrame()
|
| 470 |
+
# df_edges['source'] = res_dfs[0]['ods_id']
|
| 471 |
+
# df_edges['target'] = res_dfs[0]['indicador_id']
|
| 472 |
+
# df_edges['weight'] = res_dfs[0]['similaridad_cos']
|
| 473 |
+
|
| 474 |
+
# for pid, group in res_df.groupby("project_id"):
|
| 475 |
+
# best = group.sort_values("rank").iloc[0]
|
| 476 |
+
# edges.append({"source": group["project_id"], "target": group["ods_id"], "weight": group["similaridad_cos"]})
|
| 477 |
+
# df_edges = pd.DataFrame(edges)#.to_tblinput(out_edges, index=False, encoding="utf-8
|
| 478 |
+
|
| 479 |
+
# html = build_graph(df_edges)
|
| 480 |
+
from sklearn.preprocessing import MinMaxScaler
|
| 481 |
+
|
| 482 |
+
|
| 483 |
+
# dfs_norm = []
|
| 484 |
+
# Initialize the MinMaxScaler
|
| 485 |
+
scaler = MinMaxScaler()
|
| 486 |
+
|
| 487 |
+
for i in range(0,3):
|
| 488 |
+
|
| 489 |
+
if i == 0:
|
| 490 |
+
# Reshape the 'similaridad_cos' column as it needs to be 2D for the scaler
|
| 491 |
+
similarity_scores = res_dfs[i]['ods_similaridad_cos'].values.reshape(-1, 1)
|
| 492 |
+
# Fit and transform the data
|
| 493 |
+
res_dfs[i]['ods_similaridad_cos_normalized'] = scaler.fit_transform(similarity_scores)
|
| 494 |
+
# df_sim = res_dfs[i][['ODS_ID', 'OBJETIVO', 'rank', 'similaridad_cos']]
|
| 495 |
+
# df_simnorm = res_dfs[i][['ODS_ID', 'OBJETIVO', 'ods_rank', 'ods_similaridad_cos_normalized']]
|
| 496 |
+
# df_simnorm.columns = ['ODS_ID', 'OBJETIVO', 'rank', 'similaridad_cos']
|
| 497 |
+
# dfs_norm.append(df_simnorm)
|
| 498 |
+
if i == 1:
|
| 499 |
+
# Reshape the 'similaridad_cos' column as it needs to be 2D for the scaler
|
| 500 |
+
similarity_scores = res_dfs[i]['meta_similaridad_cos'].values.reshape(-1, 1)
|
| 501 |
+
# Fit and transform the data
|
| 502 |
+
res_dfs[i]['meta_similaridad_cos_normalized'] = scaler.fit_transform(similarity_scores)
|
| 503 |
+
# df_sim = res_dfs[i][['META_ID', 'META', 'rank', 'similaridad_cos']]
|
| 504 |
+
# df_simnorm = res_dfs[i][['META_ID', 'META', 'rank', 'similaridad_cos_normalized']]
|
| 505 |
+
# df_simnorm.columns = ['META_ID', 'META', 'rank', 'similaridad_cos']
|
| 506 |
+
# dfs_norm.append(df_simnorm)
|
| 507 |
+
if i == 2:
|
| 508 |
+
# Reshape the 'similaridad_cos' column as it needs to be 2D for the scaler
|
| 509 |
+
similarity_scores = res_dfs[i]['indicador_similaridad_cos'].values.reshape(-1, 1)
|
| 510 |
+
# Fit and transform the data
|
| 511 |
+
res_dfs[i]['indicador_similaridad_cos_normalized'] = scaler.fit_transform(similarity_scores)
|
| 512 |
+
# # df_sim = res_dfs[i][['INDICADOR_ID', 'INDICADOR', 'rank', 'similaridad_cos']]
|
| 513 |
+
# df_simnorm = res_dfs[i][['INDICADOR_ID', 'INDICADOR', 'rank', 'similaridad_cos_normalized']]
|
| 514 |
+
# df_simnorm.columns = ['INDICADOR_ID', 'INDICADOR', 'rank', 'similaridad_cos']
|
| 515 |
+
# dfs_norm.append(df_simnorm)
|
| 516 |
+
|
| 517 |
+
bdl_ods = res_dfs[0].merge(res_dfs[1], 'inner', left_on='ODS_ID', right_on='ODS_ID')
|
| 518 |
+
bdl_ods = bdl_ods.merge(res_dfs[2],'inner', left_on=['ODS_ID','META_ID'], right_on=['ODS_ID','META_ID'])
|
| 519 |
+
print(f'Tamaño BDL: {len(bdl_ods)}')
|
| 520 |
+
|
| 521 |
+
|
| 522 |
+
|
| 523 |
+
return (query, res_dfs[0], res_dfs[1], res_dfs[2], res_dfs[3], res_dfs[4], res_dfs[5], res_dfs[6], res_dfs[7], res_dfs[8], bdl_ods)
|
| 524 |
+
|
| 525 |
+
|
| 526 |
+
|
| 527 |
+
# ============================================================================
|
| 528 |
+
# Función para normalizar
|
| 529 |
+
# ============================================================================
|
| 530 |
+
|
| 531 |
+
|
| 532 |
+
|
src/visualizaciones_ods.py
ADDED
|
@@ -0,0 +1,835 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
VISUALIZACIONES PARA ANÁLISIS DE SIMILARIDAD COSENO - INDICADORES ODS
|
| 3 |
+
========================================================================
|
| 4 |
+
|
| 5 |
+
Este script genera visualizaciones interactivas y estáticas para ponderar
|
| 6 |
+
el valor de similaridad_cos como proxy de similaridad al consultar una
|
| 7 |
+
iniciativa ciudadana con una base de indicadores ODS.
|
| 8 |
+
|
| 9 |
+
Autor: Análisis ODS
|
| 10 |
+
Fecha: Octubre 2025
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
import pandas as pd
|
| 14 |
+
import numpy as np
|
| 15 |
+
import matplotlib.pyplot as plt
|
| 16 |
+
import seaborn as sns
|
| 17 |
+
from matplotlib.gridspec import GridSpec
|
| 18 |
+
import plotly.graph_objects as go
|
| 19 |
+
import plotly.express as px
|
| 20 |
+
from plotly.subplots import make_subplots
|
| 21 |
+
import warnings
|
| 22 |
+
|
| 23 |
+
warnings.filterwarnings('ignore')
|
| 24 |
+
|
| 25 |
+
# Configuración estética
|
| 26 |
+
plt.style.use('seaborn-v0_8-darkgrid')
|
| 27 |
+
sns.set_palette("husl")
|
| 28 |
+
|
| 29 |
+
# ============================================================================
|
| 30 |
+
# 1. CARGA Y PREPARACIÓN DE DATOS
|
| 31 |
+
# ============================================================================
|
| 32 |
+
|
| 33 |
+
def cargar_datos(ruta_archivo):
|
| 34 |
+
"""
|
| 35 |
+
Carga los datos desde el archivo markdown y los convierte a DataFrame
|
| 36 |
+
"""
|
| 37 |
+
# Leer el archivo saltando la línea de separación
|
| 38 |
+
df = pd.read_csv(ruta_archivo, sep='|', skiprows=[1])
|
| 39 |
+
|
| 40 |
+
# Limpiar columnas (eliminar espacios)
|
| 41 |
+
df.columns = df.columns.str.strip()
|
| 42 |
+
|
| 43 |
+
# Eliminar columnas vacías (primera y última por el formato markdown)
|
| 44 |
+
df = df.drop(df.columns[[0, -1]], axis=1)
|
| 45 |
+
|
| 46 |
+
# Limpiar espacios en valores de texto
|
| 47 |
+
for col in df.select_dtypes(include=['object']).columns:
|
| 48 |
+
df[col] = df[col].str.strip()
|
| 49 |
+
|
| 50 |
+
return df
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# ============================================================================
|
| 54 |
+
# 2. GRÁFICA 1: DISTRIBUCIÓN DE SIMILARIDAD POR ODS (Box Plot Interactivo)
|
| 55 |
+
# ============================================================================
|
| 56 |
+
|
| 57 |
+
def viz_1_distribucion_por_ods(df, id_lvl, score, titulo):
|
| 58 |
+
"""
|
| 59 |
+
LÓGICA: Esta visualización muestra la distribución de valores de similaridad
|
| 60 |
+
coseno agrupados por cada ODS. Permite identificar:
|
| 61 |
+
- Qué ODS tienen mayor rango de similaridad
|
| 62 |
+
- La mediana de similaridad por ODS
|
| 63 |
+
- Outliers o valores atípicos
|
| 64 |
+
- Consistencia interna de cada ODS
|
| 65 |
+
|
| 66 |
+
INTERPRETACIÓN:
|
| 67 |
+
- Cajas más altas → Mayor variabilidad en la similaridad dentro del ODS
|
| 68 |
+
- Medianas altas → El ODS tiene indicadores más similares a la consulta
|
| 69 |
+
- Outliers superiores → Indicadores específicos muy relevantes
|
| 70 |
+
"""
|
| 71 |
+
|
| 72 |
+
fig = go.Figure()
|
| 73 |
+
|
| 74 |
+
for idx, ods in enumerate(sorted(df['ODS_ID'].unique())):
|
| 75 |
+
datos_ods = df[df['ODS_ID'] == ods][score]
|
| 76 |
+
|
| 77 |
+
fig.add_trace(go.Box(
|
| 78 |
+
y=datos_ods,
|
| 79 |
+
name=f'ODS {ods}',
|
| 80 |
+
boxmean='sd', # Mostrar media y desviación estándar
|
| 81 |
+
marker_color=px.colors.qualitative.Plotly[int(ods) % len(px.colors.qualitative.Plotly)]
|
| 82 |
+
))
|
| 83 |
+
|
| 84 |
+
fig.update_layout(
|
| 85 |
+
title={
|
| 86 |
+
'text': f'Distribución de Similaridad Coseno por {titulo}<br><sub>Análisis de dispersión y tendencia central por objetivo</sub>',
|
| 87 |
+
'x': 0.5,
|
| 88 |
+
'xanchor': 'center'
|
| 89 |
+
},
|
| 90 |
+
# xaxis_title='Objetivo de Desarrollo Sostenible',
|
| 91 |
+
xaxis_title=id_lvl,
|
| 92 |
+
yaxis_title='Similaridad Coseno',
|
| 93 |
+
height=600,
|
| 94 |
+
showlegend=False,
|
| 95 |
+
hovermode='x unified'
|
| 96 |
+
)
|
| 97 |
+
|
| 98 |
+
return fig
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
# ============================================================================
|
| 102 |
+
# 3. GRÁFICA 2: HEATMAP DE SIMILARIDAD (ODS vs Rango de Ranking)
|
| 103 |
+
# ============================================================================
|
| 104 |
+
|
| 105 |
+
def viz_2_heatmap_ods_ranking(df, id_lvl, score, rank, titulo):
|
| 106 |
+
"""
|
| 107 |
+
LÓGICA: Matriz de calor que muestra la intensidad de similaridad en función
|
| 108 |
+
de dos dimensiones: ODS (eje Y) y posición en el ranking (eje X agrupado).
|
| 109 |
+
|
| 110 |
+
Se divide el ranking en deciles (10 grupos) para visualizar cómo se
|
| 111 |
+
distribuye la similaridad a lo largo de la relevancia ordenada.
|
| 112 |
+
|
| 113 |
+
INTERPRETACIÓN:
|
| 114 |
+
- Colores cálidos (rojo/naranja) → Alta similaridad
|
| 115 |
+
- Colores fríos (azul) → Baja similaridad
|
| 116 |
+
- Patrón horizontal → Un ODS domina en ciertas posiciones
|
| 117 |
+
- Patrón vertical → Ciertas posiciones tienen alta similaridad en varios ODS
|
| 118 |
+
- Diagonal descendente → Comportamiento esperado (mayor rank → menor similaridad)
|
| 119 |
+
"""
|
| 120 |
+
|
| 121 |
+
# Crear deciles de ranking
|
| 122 |
+
df['rank_decil'] = pd.qcut(df[rank], q=10, labels=[f'D{i+1}' for i in range(10)])
|
| 123 |
+
|
| 124 |
+
# Crear matriz pivote
|
| 125 |
+
pivot_table = df.pivot_table(
|
| 126 |
+
values=score,
|
| 127 |
+
index=id_lvl,
|
| 128 |
+
columns='rank_decil',
|
| 129 |
+
aggfunc='mean'
|
| 130 |
+
)
|
| 131 |
+
|
| 132 |
+
fig, ax = plt.subplots(figsize=(14, 8))
|
| 133 |
+
|
| 134 |
+
sns.heatmap(
|
| 135 |
+
pivot_table,
|
| 136 |
+
annot=True,
|
| 137 |
+
fmt='.3f',
|
| 138 |
+
cmap='RdYlGn',
|
| 139 |
+
center=df[score].median(),
|
| 140 |
+
cbar_kws={'label': 'Similaridad Coseno Promedio'},
|
| 141 |
+
linewidths=0.5,
|
| 142 |
+
ax=ax
|
| 143 |
+
)
|
| 144 |
+
|
| 145 |
+
ax.set_title(
|
| 146 |
+
f'Heatmap: Similaridad Coseno por {id_lvl} y Decil de Ranking\n'
|
| 147 |
+
'Visualización de patrones de relevancia en función del orden',
|
| 148 |
+
fontsize=14,
|
| 149 |
+
pad=20
|
| 150 |
+
)
|
| 151 |
+
ax.set_xlabel('Decil de Ranking (D1=Top 10%, D10=Bottom 10%)', fontsize=12)
|
| 152 |
+
ax.set_ylabel(id_lvl, fontsize=12)
|
| 153 |
+
|
| 154 |
+
plt.tight_layout()
|
| 155 |
+
return fig
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
# ============================================================================
|
| 159 |
+
# 4. GRÁFICA 3: SCATTER PLOT 3D (ODS, Indicador, Similaridad)
|
| 160 |
+
# ============================================================================
|
| 161 |
+
|
| 162 |
+
def viz_3_scatter_3d_interactivo(df, id_lvl, score, rank, titulo):
|
| 163 |
+
"""
|
| 164 |
+
LÓGICA: Visualización tridimensional que permite explorar la relación
|
| 165 |
+
entre tres variables:
|
| 166 |
+
- Eje X: ODS ID
|
| 167 |
+
- Eje Y: Número de indicador dentro del ODS (extraído del indicador_id)
|
| 168 |
+
- Eje Z: Similaridad coseno
|
| 169 |
+
- Tamaño: Inversamente proporcional al ranking (más relevantes = más grandes)
|
| 170 |
+
- Color: Por ODS
|
| 171 |
+
|
| 172 |
+
INTERPRETACIÓN:
|
| 173 |
+
- Puntos altos (eje Z) → Alta similaridad
|
| 174 |
+
- Clusters verticales → Varios indicadores de un ODS son similares
|
| 175 |
+
- Puntos grandes en altura → Indicadores relevantes y bien posicionados
|
| 176 |
+
- Permite rotar e interactuar para descubrir patrones espaciales
|
| 177 |
+
"""
|
| 178 |
+
|
| 179 |
+
# Extraer número de indicador
|
| 180 |
+
df['indicador_num'] = df[id_lvl].str.extract(r'\.(\d+)\.').astype(float)
|
| 181 |
+
|
| 182 |
+
fig = go.Figure()
|
| 183 |
+
|
| 184 |
+
for ods in sorted(df['ODS_ID'].unique()):
|
| 185 |
+
datos_ods = df[df['ODS_ID'] == ods]
|
| 186 |
+
|
| 187 |
+
fig.add_trace(go.Scatter3d(
|
| 188 |
+
x=datos_ods['ODS_ID'],
|
| 189 |
+
y=datos_ods['indicador_num'],
|
| 190 |
+
z=datos_ods[score],
|
| 191 |
+
mode='markers',
|
| 192 |
+
name=f'ODS {ods}',
|
| 193 |
+
marker=dict(
|
| 194 |
+
size=10 - (datos_ods[rank] / len(df) * 8), # Tamaño inversamente proporcional al rank
|
| 195 |
+
opacity=0.7,
|
| 196 |
+
line=dict(width=0.5, color='white')
|
| 197 |
+
),
|
| 198 |
+
text=datos_ods[id_lvl],
|
| 199 |
+
hovertemplate='<b>%{text}</b><br>' +
|
| 200 |
+
'ODS: %{x}<br>' +
|
| 201 |
+
'Similaridad: %{z:.4f}<br>' +
|
| 202 |
+
'<extra></extra>'
|
| 203 |
+
))
|
| 204 |
+
|
| 205 |
+
fig.update_layout(
|
| 206 |
+
title='Visualización 3D: ODS × Indicador × Similaridad<br><sub>Exploración espacial de patrones de relevancia</sub>',
|
| 207 |
+
scene=dict(
|
| 208 |
+
xaxis_title='ODS ID',
|
| 209 |
+
yaxis_title='Número de Indicador',
|
| 210 |
+
zaxis_title='Similaridad Coseno',
|
| 211 |
+
camera=dict(eye=dict(x=1.5, y=1.5, z=1.3))
|
| 212 |
+
),
|
| 213 |
+
height=700,
|
| 214 |
+
showlegend=True
|
| 215 |
+
)
|
| 216 |
+
|
| 217 |
+
return fig
|
| 218 |
+
|
| 219 |
+
|
| 220 |
+
# ============================================================================
|
| 221 |
+
# 5. GRÁFICA 4: RADAR CHART - Similaridad Promedio por ODS
|
| 222 |
+
# ============================================================================
|
| 223 |
+
|
| 224 |
+
def viz_4_radar_chart_ods(df, id_lvl, score, rank, titulo):
|
| 225 |
+
"""
|
| 226 |
+
LÓGICA: Gráfico de radar (spider chart) que muestra la similaridad promedio
|
| 227 |
+
de cada ODS en forma circular. Útil para comparar rápidamente el perfil
|
| 228 |
+
de relevancia de todos los ODS.
|
| 229 |
+
|
| 230 |
+
INTERPRETACIÓN:
|
| 231 |
+
- Áreas más grandes → Mayor similaridad promedio con la consulta
|
| 232 |
+
- Forma del polígono → Perfil de cobertura de la iniciativa
|
| 233 |
+
- Picos → ODS altamente relevantes
|
| 234 |
+
- Valles → ODS menos relacionados
|
| 235 |
+
- Simetría → Iniciativa balanceada entre ODS vs. especializada
|
| 236 |
+
"""
|
| 237 |
+
|
| 238 |
+
# Calcular promedios por ODS
|
| 239 |
+
ods_stats = df.groupby(id_lvl).agg({
|
| 240 |
+
score: ['mean', 'max', 'count']
|
| 241 |
+
}).reset_index()
|
| 242 |
+
|
| 243 |
+
ods_stats.columns = [id_lvl, 'sim_promedio', 'sim_max', 'count_indicadores']
|
| 244 |
+
ods_stats = ods_stats.sort_values(id_lvl)
|
| 245 |
+
|
| 246 |
+
fig = go.Figure()
|
| 247 |
+
|
| 248 |
+
# Similaridad promedio
|
| 249 |
+
fig.add_trace(go.Scatterpolar(
|
| 250 |
+
r=ods_stats['sim_promedio'],
|
| 251 |
+
theta=['ODS ' + str(x) for x in ods_stats[id_lvl]],
|
| 252 |
+
fill='toself',
|
| 253 |
+
name='Similaridad Promedio',
|
| 254 |
+
line_color='blue',
|
| 255 |
+
fillcolor='rgba(0, 0, 255, 0.2)'
|
| 256 |
+
))
|
| 257 |
+
|
| 258 |
+
# Similaridad máxima
|
| 259 |
+
fig.add_trace(go.Scatterpolar(
|
| 260 |
+
r=ods_stats['sim_max'],
|
| 261 |
+
theta=['ODS ' + str(x) for x in ods_stats[id_lvl]],
|
| 262 |
+
fill='toself',
|
| 263 |
+
name='Similaridad Máxima',
|
| 264 |
+
line_color='red',
|
| 265 |
+
fillcolor='rgba(255, 0, 0, 0.1)'
|
| 266 |
+
))
|
| 267 |
+
|
| 268 |
+
fig.update_layout(
|
| 269 |
+
polar=dict(
|
| 270 |
+
radialaxis=dict(
|
| 271 |
+
visible=True,
|
| 272 |
+
range=[0.85, 0.95] # Ajustar según datos reales
|
| 273 |
+
)
|
| 274 |
+
),
|
| 275 |
+
title=f'Radar Chart: Perfil de Similaridad por {titulo}<br><sub>Comparación de promedios y máximos</sub>',
|
| 276 |
+
showlegend=True,
|
| 277 |
+
height=600
|
| 278 |
+
)
|
| 279 |
+
|
| 280 |
+
return fig
|
| 281 |
+
|
| 282 |
+
|
| 283 |
+
# ============================================================================
|
| 284 |
+
# 6. GRÁFICA 5: SUNBURST - Jerarquía ODS → Indicadores
|
| 285 |
+
# ============================================================================
|
| 286 |
+
|
| 287 |
+
def viz_5_sunburst_jerarquia(df, id_lvl, score, rank, titulo):
|
| 288 |
+
"""
|
| 289 |
+
LÓGICA: Diagrama de sunburst (sol radiante) que muestra la jerarquía
|
| 290 |
+
ODS → Indicadores con el tamaño proporcional a la similaridad.
|
| 291 |
+
|
| 292 |
+
El círculo interior representa los ODS y los anillos exteriores los
|
| 293 |
+
indicadores dentro de cada ODS.
|
| 294 |
+
|
| 295 |
+
INTERPRETACIÓN:
|
| 296 |
+
- Segmentos grandes → Indicadores o grupos de indicadores muy similares
|
| 297 |
+
- Colores → Gradiente de similaridad (más oscuro = mayor similaridad)
|
| 298 |
+
- Permite drill-down interactivo
|
| 299 |
+
- Visualiza la contribución relativa de cada indicador al ODS
|
| 300 |
+
"""
|
| 301 |
+
|
| 302 |
+
# Preparar datos para sunburst
|
| 303 |
+
df_sun = df.copy()
|
| 304 |
+
df_sun['ods_label'] = 'ODS ' + df_sun['ODS_ID'].astype(str)
|
| 305 |
+
df_sun['path'] = df_sun['ods_label'] + ' / ' + df_sun[id_lvl]
|
| 306 |
+
|
| 307 |
+
# Limitar a top 100 para mejor visualización
|
| 308 |
+
df_sun_top = df_sun.nsmallest(100, rank)
|
| 309 |
+
|
| 310 |
+
fig = px.sunburst(
|
| 311 |
+
df_sun_top,
|
| 312 |
+
path=['ods_label', id_lvl],
|
| 313 |
+
values=score,
|
| 314 |
+
color=score,
|
| 315 |
+
color_continuous_scale='Viridis',
|
| 316 |
+
hover_data=[rank],
|
| 317 |
+
title=f'Sunburst: Jerarquía {titulo} → Indicadores (Top 100)<br><sub>Tamaño proporcional a similaridad</sub>'
|
| 318 |
+
)
|
| 319 |
+
|
| 320 |
+
fig.update_layout(
|
| 321 |
+
height=700,
|
| 322 |
+
coloraxis_colorbar=dict(title="Similaridad")
|
| 323 |
+
)
|
| 324 |
+
|
| 325 |
+
return fig
|
| 326 |
+
|
| 327 |
+
|
| 328 |
+
# ============================================================================
|
| 329 |
+
# 7. GRÁFICA 6: CASCADA - Top Indicadores por ODS
|
| 330 |
+
# ============================================================================
|
| 331 |
+
|
| 332 |
+
def viz_6_top_indicadores_por_ods(df, id_lvl, score, rank, titulo, top_n=3):
|
| 333 |
+
"""
|
| 334 |
+
LÓGICA: Para cada ODS, muestra los top N indicadores con mayor similaridad
|
| 335 |
+
en un formato de barras horizontales agrupadas.
|
| 336 |
+
|
| 337 |
+
Permite comparar:
|
| 338 |
+
- Cuál es el mejor indicador de cada ODS
|
| 339 |
+
- La brecha entre el mejor y los siguientes
|
| 340 |
+
- Qué ODS tiene los indicadores más relevantes en general
|
| 341 |
+
|
| 342 |
+
INTERPRETACIÓN:
|
| 343 |
+
- Barras más largas → Mayor similaridad
|
| 344 |
+
- Agrupación densa → Varios indicadores igualmente relevantes
|
| 345 |
+
- Gaps grandes → Un indicador destaca sobre el resto en ese ODS
|
| 346 |
+
"""
|
| 347 |
+
|
| 348 |
+
# Obtener top N por ODS
|
| 349 |
+
top_indicadores = df.groupby('ODS_ID').apply(
|
| 350 |
+
lambda x: x.nsmallest(top_n, rank)
|
| 351 |
+
).reset_index(drop=True)
|
| 352 |
+
|
| 353 |
+
fig = px.bar(
|
| 354 |
+
top_indicadores,
|
| 355 |
+
x=score,
|
| 356 |
+
y=id_lvl,
|
| 357 |
+
color=id_lvl,
|
| 358 |
+
orientation='h',
|
| 359 |
+
facet_row=id_lvl,
|
| 360 |
+
height=300 * len(df[id_lvl].unique()) // 3,
|
| 361 |
+
title=f'Top {top_n} Indicadores con Mayor Similaridad por ODS<br><sub>Análisis de relevancia por objetivo</sub>',
|
| 362 |
+
labels={score: 'Similaridad Coseno', id_lvl: 'Indicador'},
|
| 363 |
+
color_continuous_scale='Plasma'
|
| 364 |
+
)
|
| 365 |
+
|
| 366 |
+
fig.update_yaxes(showticklabels=True, matches=None)
|
| 367 |
+
fig.update_xaxes(matches='x')
|
| 368 |
+
|
| 369 |
+
return fig
|
| 370 |
+
|
| 371 |
+
|
| 372 |
+
# ============================================================================
|
| 373 |
+
# 8. GRÁFICA 7: STREAM GRAPH - Evolución de Similaridad
|
| 374 |
+
# ============================================================================
|
| 375 |
+
|
| 376 |
+
def viz_7_streamgraph_similaridad(df, id_lvl, score, rank, titulo):
|
| 377 |
+
"""
|
| 378 |
+
LÓGICA: Gráfico de área apilada que muestra cómo contribuye cada ODS
|
| 379 |
+
a la similaridad acumulada a lo largo del ranking.
|
| 380 |
+
|
| 381 |
+
El eje X es el ranking (ordenado) y el eje Y muestra el área acumulada
|
| 382 |
+
de similaridad por ODS.
|
| 383 |
+
|
| 384 |
+
INTERPRETACIÓN:
|
| 385 |
+
- Áreas más anchas → ODS con mayor presencia en ese rango de ranking
|
| 386 |
+
- Cambios de color dominante → Transición de relevancia entre ODS
|
| 387 |
+
- Posición en ranking bajo → Indicadores más relevantes
|
| 388 |
+
- Permite ver qué ODS domina en qué rangos de relevancia
|
| 389 |
+
"""
|
| 390 |
+
|
| 391 |
+
# Crear bins de ranking
|
| 392 |
+
df['rank_bin'] = pd.cut(df[rank], bins=20, labels=False)
|
| 393 |
+
|
| 394 |
+
# Agrupar por rank_bin y ODS
|
| 395 |
+
stream_data = df.groupby(['rank_bin', id_lvl])[score].sum().reset_index()
|
| 396 |
+
|
| 397 |
+
# Pivotar para streamgraph
|
| 398 |
+
stream_pivot = stream_data.pivot(index='rank_bin', columns=id_lvl, values=score).fillna(0)
|
| 399 |
+
|
| 400 |
+
fig = go.Figure()
|
| 401 |
+
|
| 402 |
+
for ods in stream_pivot.columns:
|
| 403 |
+
fig.add_trace(go.Scatter(
|
| 404 |
+
x=stream_pivot.index,
|
| 405 |
+
y=stream_pivot[ods],
|
| 406 |
+
mode='lines',
|
| 407 |
+
name=f'ODS {ods}',
|
| 408 |
+
stackgroup='one',
|
| 409 |
+
groupnorm='percent', # Normalizar a porcentaje
|
| 410 |
+
hovertemplate='ODS %{fullData.name}<br>Contribución: %{y:.1f}%<extra></extra>'
|
| 411 |
+
))
|
| 412 |
+
|
| 413 |
+
fig.update_layout(
|
| 414 |
+
title='Stream Graph: Contribución de cada ODS por Rango de Ranking<br><sub>Evolución de relevancia normalizada</sub>',
|
| 415 |
+
xaxis_title='Rango de Ranking (agrupado)',
|
| 416 |
+
yaxis_title='Contribución Porcentual',
|
| 417 |
+
height=600,
|
| 418 |
+
hovermode='x unified'
|
| 419 |
+
)
|
| 420 |
+
|
| 421 |
+
return fig
|
| 422 |
+
|
| 423 |
+
|
| 424 |
+
# ============================================================================
|
| 425 |
+
# 9. GRÁFICA 8: VIOLIN PLOT - Comparación Detallada de Distribuciones
|
| 426 |
+
# ============================================================================
|
| 427 |
+
|
| 428 |
+
def viz_8_violin_plot_ods(df, id_lvl, score, rank, titulo):
|
| 429 |
+
"""
|
| 430 |
+
LÓGICA: Similar al box plot pero muestra la distribución completa de
|
| 431 |
+
densidad de probabilidad de la similaridad para cada ODS.
|
| 432 |
+
|
| 433 |
+
El ancho del "violín" representa la concentración de valores en ese rango.
|
| 434 |
+
|
| 435 |
+
INTERPRETACIÓN:
|
| 436 |
+
- Violines anchos → Muchos valores en ese rango de similaridad
|
| 437 |
+
- Violines angostos → Pocos valores en ese rango
|
| 438 |
+
- Forma bimodal → Dos grupos de indicadores con diferente similaridad
|
| 439 |
+
- Forma unimodal → Indicadores homogéneos en similaridad
|
| 440 |
+
- Permite ver distribuciones no normales que el box plot no captura
|
| 441 |
+
"""
|
| 442 |
+
|
| 443 |
+
fig = go.Figure()
|
| 444 |
+
|
| 445 |
+
for ods in sorted(df[id_lvl].unique()):
|
| 446 |
+
datos_ods = df[df[id_lvl] == ods][score]
|
| 447 |
+
|
| 448 |
+
fig.add_trace(go.Violin(
|
| 449 |
+
y=datos_ods,
|
| 450 |
+
name=f'ODS {ods}',
|
| 451 |
+
box_visible=True,
|
| 452 |
+
meanline_visible=True,
|
| 453 |
+
fillcolor=px.colors.qualitative.Plotly[int(ods) % len(px.colors.qualitative.Plotly)],
|
| 454 |
+
opacity=0.6,
|
| 455 |
+
x0=f'ODS {ods}'
|
| 456 |
+
))
|
| 457 |
+
|
| 458 |
+
fig.update_layout(
|
| 459 |
+
title='Violin Plot: Distribución de Densidad de Similaridad por ODS<br><sub>Análisis detallado de concentración de valores</sub>',
|
| 460 |
+
yaxis_title='Similaridad Coseno',
|
| 461 |
+
xaxis_title='Objetivo de Desarrollo Sostenible',
|
| 462 |
+
height=600,
|
| 463 |
+
showlegend=False
|
| 464 |
+
)
|
| 465 |
+
|
| 466 |
+
return fig
|
| 467 |
+
|
| 468 |
+
|
| 469 |
+
# ============================================================================
|
| 470 |
+
# 10. GRÁFICA 9: DASHBOARD INTEGRADO - Métricas Clave
|
| 471 |
+
# ============================================================================
|
| 472 |
+
|
| 473 |
+
def viz_9_dashboard_metricas(df, id_lvl, score, rank, titulo):
|
| 474 |
+
"""
|
| 475 |
+
LÓGICA: Dashboard con múltiples paneles que resume las métricas clave:
|
| 476 |
+
- Panel 1: Top 10 indicadores con mayor similaridad
|
| 477 |
+
- Panel 2: Estadísticas por ODS (media, std, max, min)
|
| 478 |
+
- Panel 3: Distribución global de similaridad (histograma)
|
| 479 |
+
- Panel 4: Correlación entre rank y similaridad
|
| 480 |
+
|
| 481 |
+
INTERPRETACIÓN:
|
| 482 |
+
- Vista holística de la calidad del matching
|
| 483 |
+
- Permite validar que el ranking está bien correlacionado con similaridad
|
| 484 |
+
- Identifica outliers o problemas en el cálculo
|
| 485 |
+
- Facilita comunicación de resultados a stakeholders
|
| 486 |
+
"""
|
| 487 |
+
|
| 488 |
+
fig = make_subplots(
|
| 489 |
+
rows=2, cols=2,
|
| 490 |
+
subplot_titles=(
|
| 491 |
+
'Top 10 Indicadores por Similaridad',
|
| 492 |
+
'Estadísticas por ODS',
|
| 493 |
+
'Distribución Global de Similaridad',
|
| 494 |
+
'Correlación: Rank vs Similaridad'
|
| 495 |
+
),
|
| 496 |
+
specs=[
|
| 497 |
+
[{"type": "bar"}, {"type": "table"}],
|
| 498 |
+
[{"type": "histogram"}, {"type": "scatter"}]
|
| 499 |
+
]
|
| 500 |
+
)
|
| 501 |
+
|
| 502 |
+
# Panel 1: Top 10
|
| 503 |
+
top_10 = df.nsmallest(10, rank)
|
| 504 |
+
fig.add_trace(
|
| 505 |
+
go.Bar(
|
| 506 |
+
x=top_10[score],
|
| 507 |
+
y=top_10['indicador_id'],
|
| 508 |
+
orientation='h',
|
| 509 |
+
marker_color='lightblue',
|
| 510 |
+
text=top_10[score].round(4),
|
| 511 |
+
textposition='auto'
|
| 512 |
+
),
|
| 513 |
+
row=1, col=1
|
| 514 |
+
)
|
| 515 |
+
|
| 516 |
+
# Panel 2: Tabla de estadísticas
|
| 517 |
+
stats_ods = df.groupby(id_lvl)[score].agg(['mean', 'std', 'min', 'max', 'count']).reset_index()
|
| 518 |
+
stats_ods.columns = ['ODS', 'Media', 'Std', 'Min', 'Max', 'Count']
|
| 519 |
+
stats_ods = stats_ods.round(4)
|
| 520 |
+
|
| 521 |
+
fig.add_trace(
|
| 522 |
+
go.Table(
|
| 523 |
+
header=dict(values=list(stats_ods.columns),
|
| 524 |
+
fill_color='paleturquoise',
|
| 525 |
+
align='left'),
|
| 526 |
+
cells=dict(values=[stats_ods[col] for col in stats_ods.columns],
|
| 527 |
+
fill_color='lavender',
|
| 528 |
+
align='left')
|
| 529 |
+
),
|
| 530 |
+
row=1, col=2
|
| 531 |
+
)
|
| 532 |
+
|
| 533 |
+
# Panel 3: Histograma
|
| 534 |
+
fig.add_trace(
|
| 535 |
+
go.Histogram(
|
| 536 |
+
x=df[score],
|
| 537 |
+
nbinsx=30,
|
| 538 |
+
marker_color='indianred',
|
| 539 |
+
name='Distribución'
|
| 540 |
+
),
|
| 541 |
+
row=2, col=1
|
| 542 |
+
)
|
| 543 |
+
|
| 544 |
+
# Panel 4: Scatter rank vs similaridad
|
| 545 |
+
fig.add_trace(
|
| 546 |
+
go.Scatter(
|
| 547 |
+
x=df[rank],
|
| 548 |
+
y=df[score],
|
| 549 |
+
mode='markers',
|
| 550 |
+
marker=dict(
|
| 551 |
+
size=5,
|
| 552 |
+
color=df[id_lvl],
|
| 553 |
+
colorscale='Viridis',
|
| 554 |
+
showscale=True,
|
| 555 |
+
colorbar=dict(title="ODS", x=1.15)
|
| 556 |
+
),
|
| 557 |
+
text=df['indicador_id']
|
| 558 |
+
),
|
| 559 |
+
row=2, col=2
|
| 560 |
+
)
|
| 561 |
+
|
| 562 |
+
# Añadir línea de tendencia
|
| 563 |
+
z = np.polyfit(df[rank], df[score], 1)
|
| 564 |
+
p = np.poly1d(z)
|
| 565 |
+
fig.add_trace(
|
| 566 |
+
go.Scatter(
|
| 567 |
+
x=df[rank],
|
| 568 |
+
y=p(df[rank]),
|
| 569 |
+
mode='lines',
|
| 570 |
+
line=dict(color='red', dash='dash'),
|
| 571 |
+
name='Tendencia'
|
| 572 |
+
),
|
| 573 |
+
row=2, col=2
|
| 574 |
+
)
|
| 575 |
+
|
| 576 |
+
fig.update_xaxes(title_text="Similaridad", row=1, col=1)
|
| 577 |
+
fig.update_xaxes(title_text="Similaridad", row=2, col=1)
|
| 578 |
+
fig.update_xaxes(title_text="Rank", row=2, col=2)
|
| 579 |
+
fig.update_yaxes(title_text="Indicador", row=1, col=1)
|
| 580 |
+
fig.update_yaxes(title_text="Frecuencia", row=2, col=1)
|
| 581 |
+
fig.update_yaxes(title_text="Similaridad", row=2, col=2)
|
| 582 |
+
|
| 583 |
+
fig.update_layout(
|
| 584 |
+
height=900,
|
| 585 |
+
showlegend=False,
|
| 586 |
+
title_text="Dashboard Integrado: Métricas Clave de Similaridad ODS",
|
| 587 |
+
title_x=0.5
|
| 588 |
+
)
|
| 589 |
+
|
| 590 |
+
return fig
|
| 591 |
+
|
| 592 |
+
|
| 593 |
+
# ============================================================================
|
| 594 |
+
# 11. GRÁFICA 10: MATRIZ DE TRANSICIÓN - Cambios de ODS por Ranking
|
| 595 |
+
# ============================================================================
|
| 596 |
+
|
| 597 |
+
def viz_10_matriz_transicion(df, id_lvl, score, rank, titulo):
|
| 598 |
+
"""
|
| 599 |
+
LÓGICA: Muestra cómo cambia el ODS dominante a medida que avanzamos
|
| 600 |
+
en el ranking. Divide el ranking en cuartiles y muestra qué ODS
|
| 601 |
+
tiene más presencia en cada cuartil.
|
| 602 |
+
|
| 603 |
+
INTERPRETACIÓN:
|
| 604 |
+
- Permite ver si un ODS domina consistentemente
|
| 605 |
+
- Identifica cambios de dominancia (ej: ODS 5 domina top rankings,
|
| 606 |
+
luego ODS 17)
|
| 607 |
+
- Útil para entender si la iniciativa es más afín a ciertos ODS
|
| 608 |
+
- Ayuda a explicar por qué ciertos ODS aparecen más arriba
|
| 609 |
+
"""
|
| 610 |
+
|
| 611 |
+
# Crear cuartiles
|
| 612 |
+
df['cuartil'] = pd.qcut(df[rank], q=4, labels=['Q1 (Top)', 'Q2', 'Q3', 'Q4 (Bottom)'])
|
| 613 |
+
|
| 614 |
+
# Contar presencia de ODS por cuartil
|
| 615 |
+
matriz = pd.crosstab(df[id_lvl], df['cuartil'], normalize='columns') * 100
|
| 616 |
+
|
| 617 |
+
fig, ax = plt.subplots(figsize=(12, 8))
|
| 618 |
+
|
| 619 |
+
sns.heatmap(
|
| 620 |
+
matriz,
|
| 621 |
+
annot=True,
|
| 622 |
+
fmt='.1f',
|
| 623 |
+
cmap='YlOrRd',
|
| 624 |
+
cbar_kws={'label': '% de Presencia en Cuartil'},
|
| 625 |
+
linewidths=0.5,
|
| 626 |
+
ax=ax
|
| 627 |
+
)
|
| 628 |
+
|
| 629 |
+
ax.set_title(
|
| 630 |
+
'Matriz de Transición: Presencia de ODS por Cuartil de Ranking\n'
|
| 631 |
+
'Análisis de dominancia y evolución',
|
| 632 |
+
fontsize=14,
|
| 633 |
+
pad=20
|
| 634 |
+
)
|
| 635 |
+
ax.set_xlabel('Cuartil de Ranking', fontsize=12)
|
| 636 |
+
ax.set_ylabel('ODS ID', fontsize=12)
|
| 637 |
+
|
| 638 |
+
plt.tight_layout()
|
| 639 |
+
return fig
|
| 640 |
+
|
| 641 |
+
|
| 642 |
+
# ============================================================================
|
| 643 |
+
# 12. FUNCIÓN PRINCIPAL - GENERAR TODAS LAS VISUALIZACIONES
|
| 644 |
+
# ============================================================================
|
| 645 |
+
|
| 646 |
+
def generar_todas_visualizaciones(ruta_archivo, guardar=True, formato='html'):
|
| 647 |
+
"""
|
| 648 |
+
Función principal que genera todas las visualizaciones.
|
| 649 |
+
|
| 650 |
+
Parámetros:
|
| 651 |
+
-----------
|
| 652 |
+
ruta_archivo : str
|
| 653 |
+
Ruta al archivo markdown con los datos
|
| 654 |
+
guardar : bool
|
| 655 |
+
Si True, guarda las visualizaciones en archivos
|
| 656 |
+
formato : str
|
| 657 |
+
Formato de salida: 'html' para interactivas, 'png' para estáticas
|
| 658 |
+
|
| 659 |
+
Retorna:
|
| 660 |
+
--------
|
| 661 |
+
dict : Diccionario con todas las figuras generadas
|
| 662 |
+
"""
|
| 663 |
+
|
| 664 |
+
print("Cargando datos...")
|
| 665 |
+
df = cargar_datos(ruta_archivo)
|
| 666 |
+
print(f"Datos cargados: {len(df)} registros, {df[id_lvl].nunique()} ODS únicos")
|
| 667 |
+
|
| 668 |
+
figuras = {}
|
| 669 |
+
|
| 670 |
+
print("\n" + "="*70)
|
| 671 |
+
print("GENERANDO VISUALIZACIONES")
|
| 672 |
+
print("="*70)
|
| 673 |
+
|
| 674 |
+
# Visualización 1
|
| 675 |
+
print("\n[1/10] Generando distribución por ODS (Box Plot)...")
|
| 676 |
+
figuras['viz1_boxplot'] = viz_1_distribucion_por_ods(df)
|
| 677 |
+
if guardar:
|
| 678 |
+
figuras['viz1_boxplot'].write_html('viz1_boxplot_ods.html')
|
| 679 |
+
|
| 680 |
+
# Visualización 2
|
| 681 |
+
print("[2/10] Generando heatmap ODS vs Ranking...")
|
| 682 |
+
figuras['viz2_heatmap'] = viz_2_heatmap_ods_ranking(df)
|
| 683 |
+
if guardar:
|
| 684 |
+
figuras['viz2_heatmap'].savefig('viz2_heatmap.png', dpi=300, bbox_inches='tight')
|
| 685 |
+
plt.close()
|
| 686 |
+
|
| 687 |
+
# Visualización 3
|
| 688 |
+
print("[3/10] Generando scatter 3D interactivo...")
|
| 689 |
+
figuras['viz3_scatter3d'] = viz_3_scatter_3d_interactivo(df)
|
| 690 |
+
if guardar:
|
| 691 |
+
figuras['viz3_scatter3d'].write_html('viz3_scatter3d.html')
|
| 692 |
+
|
| 693 |
+
# Visualización 4
|
| 694 |
+
print("[4/10] Generando radar chart por ODS...")
|
| 695 |
+
figuras['viz4_radar'] = viz_4_radar_chart_ods(df)
|
| 696 |
+
if guardar:
|
| 697 |
+
figuras['viz4_radar'].write_html('viz4_radar_ods.html')
|
| 698 |
+
|
| 699 |
+
# Visualización 5
|
| 700 |
+
print("[5/10] Generando sunburst jerárquico...")
|
| 701 |
+
figuras['viz5_sunburst'] = viz_5_sunburst_jerarquia(df)
|
| 702 |
+
if guardar:
|
| 703 |
+
figuras['viz5_sunburst'].write_html('viz5_sunburst.html')
|
| 704 |
+
|
| 705 |
+
# Visualización 6
|
| 706 |
+
print("[6/10] Generando top indicadores por ODS...")
|
| 707 |
+
figuras['viz6_topn'] = viz_6_top_indicadores_por_ods(df, top_n=5)
|
| 708 |
+
if guardar:
|
| 709 |
+
figuras['viz6_topn'].write_html('viz6_top_indicadores.html')
|
| 710 |
+
|
| 711 |
+
# Visualización 7
|
| 712 |
+
print("[7/10] Generando stream graph...")
|
| 713 |
+
figuras['viz7_stream'] = viz_7_streamgraph_similaridad(df)
|
| 714 |
+
if guardar:
|
| 715 |
+
figuras['viz7_stream'].write_html('viz7_streamgraph.html')
|
| 716 |
+
|
| 717 |
+
# Visualización 8
|
| 718 |
+
print("[8/10] Generando violin plot...")
|
| 719 |
+
figuras['viz8_violin'] = viz_8_violin_plot_ods(df)
|
| 720 |
+
if guardar:
|
| 721 |
+
figuras['viz8_violin'].write_html('viz8_violin_plot.html')
|
| 722 |
+
|
| 723 |
+
# Visualización 9
|
| 724 |
+
print("[9/10] Generando dashboard integrado...")
|
| 725 |
+
figuras['viz9_dashboard'] = viz_9_dashboard_metricas(df)
|
| 726 |
+
if guardar:
|
| 727 |
+
figuras['viz9_dashboard'].write_html('viz9_dashboard.html')
|
| 728 |
+
|
| 729 |
+
# Visualización 10
|
| 730 |
+
print("[10/10] Generando matriz de transición...")
|
| 731 |
+
figuras['viz10_matriz'] = viz_10_matriz_transicion(df)
|
| 732 |
+
if guardar:
|
| 733 |
+
figuras['viz10_matriz'].savefig('viz10_matriz_transicion.png', dpi=300, bbox_inches='tight')
|
| 734 |
+
plt.close()
|
| 735 |
+
|
| 736 |
+
print("\n" + "="*70)
|
| 737 |
+
print("GENERACIÓN COMPLETADA")
|
| 738 |
+
print("="*70)
|
| 739 |
+
print(f"\nTotal de visualizaciones generadas: {len(figuras)}")
|
| 740 |
+
|
| 741 |
+
if guardar:
|
| 742 |
+
print("\nArchivos guardados:")
|
| 743 |
+
print(" - Visualizaciones interactivas (HTML): 8 archivos")
|
| 744 |
+
print(" - Visualizaciones estáticas (PNG): 2 archivos")
|
| 745 |
+
|
| 746 |
+
return figuras, df
|
| 747 |
+
|
| 748 |
+
|
| 749 |
+
# ============================================================================
|
| 750 |
+
# 13. ANÁLISIS ESTADÍSTICO COMPLEMENTARIO
|
| 751 |
+
# ============================================================================
|
| 752 |
+
|
| 753 |
+
def analisis_estadistico(df):
|
| 754 |
+
"""
|
| 755 |
+
Genera estadísticas descriptivas complementarias para el análisis
|
| 756 |
+
"""
|
| 757 |
+
print("\n" + "="*70)
|
| 758 |
+
print("ANÁLISIS ESTADÍSTICO COMPLEMENTARIO")
|
| 759 |
+
print("="*70)
|
| 760 |
+
|
| 761 |
+
print("\n1. ESTADÍSTICAS GLOBALES")
|
| 762 |
+
print("-" * 70)
|
| 763 |
+
print(f" Similaridad media: {df[score].mean():.4f}")
|
| 764 |
+
print(f" Desviación estándar: {df[score].std():.4f}")
|
| 765 |
+
print(f" Similaridad mínima: {df[score].min():.4f}")
|
| 766 |
+
print(f" Similaridad máxima: {df[score].max():.4f}")
|
| 767 |
+
print(f" Mediana: {df[score].median():.4f}")
|
| 768 |
+
|
| 769 |
+
print("\n2. ESTADÍSTICAS POR ODS")
|
| 770 |
+
print("-" * 70)
|
| 771 |
+
stats_ods = df.groupby(id_lvl)[score].agg([
|
| 772 |
+
('count', 'count'),
|
| 773 |
+
('mean', 'mean'),
|
| 774 |
+
('std', 'std'),
|
| 775 |
+
('min', 'min'),
|
| 776 |
+
('max', 'max')
|
| 777 |
+
]).round(4)
|
| 778 |
+
print(stats_ods.to_string())
|
| 779 |
+
|
| 780 |
+
print("\n3. ODS MÁS REPRESENTADOS EN TOP 50")
|
| 781 |
+
print("-" * 70)
|
| 782 |
+
top_50_ods = df.nsmallest(50, rank)[id_lvl].value_counts()
|
| 783 |
+
print(top_50_ods.to_string())
|
| 784 |
+
|
| 785 |
+
print("\n4. CORRELACIÓN RANK vs SIMILARIDAD")
|
| 786 |
+
print("-" * 70)
|
| 787 |
+
correlacion = df[rank].corr(df[score])
|
| 788 |
+
print(f" Correlación de Pearson: {correlacion:.4f}")
|
| 789 |
+
print(f" Interpretación: {'Negativa fuerte' if correlacion < -0.7 else 'Negativa moderada' if correlacion < -0.4 else 'Negativa débil'}")
|
| 790 |
+
print(f" (Esperado: correlación negativa, a mayor rank → menor similaridad)")
|
| 791 |
+
|
| 792 |
+
return stats_ods
|
| 793 |
+
|
| 794 |
+
|
| 795 |
+
# ============================================================================
|
| 796 |
+
# EJECUCIÓN DEL SCRIPT
|
| 797 |
+
# ============================================================================
|
| 798 |
+
|
| 799 |
+
if __name__ == "__main__":
|
| 800 |
+
# Configurar ruta del archivo
|
| 801 |
+
RUTA_ARCHIVO = '/mnt/user-data/uploads/indicadores_markdown.txt'
|
| 802 |
+
|
| 803 |
+
print("\n" + "="*70)
|
| 804 |
+
print("SISTEMA DE VISUALIZACIÓN - ANÁLISIS DE SIMILARIDAD ODS")
|
| 805 |
+
print("="*70)
|
| 806 |
+
print("\nEste script genera 10 visualizaciones avanzadas para analizar")
|
| 807 |
+
print("la similaridad coseno como proxy de relevancia entre una iniciativa")
|
| 808 |
+
print("ciudadana y los indicadores ODS.")
|
| 809 |
+
|
| 810 |
+
# Generar todas las visualizaciones
|
| 811 |
+
figuras, df = generar_todas_visualizaciones(
|
| 812 |
+
RUTA_ARCHIVO,
|
| 813 |
+
guardar=True,
|
| 814 |
+
formato='html'
|
| 815 |
+
)
|
| 816 |
+
|
| 817 |
+
# Análisis estadístico
|
| 818 |
+
stats = analisis_estadistico(df)
|
| 819 |
+
|
| 820 |
+
print("\n" + "="*70)
|
| 821 |
+
print("RECOMENDACIONES DE USO")
|
| 822 |
+
print("="*70)
|
| 823 |
+
print("""
|
| 824 |
+
1. Use el Dashboard (viz9) como punto de partida para exploración general
|
| 825 |
+
2. Use el Heatmap (viz2) para identificar patrones temporales de relevancia
|
| 826 |
+
3. Use el Radar Chart (viz4) para comunicar el perfil ODS de la iniciativa
|
| 827 |
+
4. Use el Scatter 3D (viz3) para exploración detallada e interactiva
|
| 828 |
+
5. Use el Violin Plot (viz8) para análisis estadístico profundo
|
| 829 |
+
6. Use el Sunburst (viz5) para presentaciones ejecutivas
|
| 830 |
+
7. Use la Matriz de Transición (viz10) para análisis de consistencia
|
| 831 |
+
|
| 832 |
+
NOTA: Los archivos HTML son interactivos - ábralos en un navegador
|
| 833 |
+
""")
|
| 834 |
+
|
| 835 |
+
print("\n¡Proceso completado exitosamente!")
|