danpa01 commited on
Commit
dc95ecb
·
1 Parent(s): b6de7b9

actualización inicial y documentación ejercicio base

Browse files
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!")