visualizar-ods / src /visualization /visualizaciones_ods.py
ConectaODSco's picture
Update src/visualization/visualizaciones_ods.py
439fcf9 verified
"""
VISUALIZACIONES PARA ANÁLISIS DE SIMILARIDAD COSENO - INDICADORES ODS
========================================================================
Este script genera visualizaciones interactivas y estáticas para ponderar
el valor de similaridad_cos como proxy de similaridad al consultar una
iniciativa ciudadana con una base de indicadores ODS.
Autor: Análisis ODS
Fecha: Octubre 2025
"""
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from matplotlib.gridspec import GridSpec
import plotly.graph_objects as go
import plotly.express as px
from plotly.subplots import make_subplots
import warnings
warnings.filterwarnings('ignore')
# Configuración estética
plt.style.use('seaborn-v0_8-darkgrid')
sns.set_palette("husl")
# ============================================================================
# 1. CARGA Y PREPARACIÓN DE DATOS
# ============================================================================
def cargar_datos(ruta_archivo):
"""
Carga los datos desde el archivo markdown y los convierte a DataFrame
"""
# Leer el archivo saltando la línea de separación
df = pd.read_csv(ruta_archivo, sep='|', skiprows=[1])
# Limpiar columnas (eliminar espacios)
df.columns = df.columns.str.strip()
# Eliminar columnas vacías (primera y última por el formato markdown)
df = df.drop(df.columns[[0, -1]], axis=1)
# Limpiar espacios en valores de texto
for col in df.select_dtypes(include=['object']).columns:
df[col] = df[col].str.strip()
return df
# Diccionario de colores oficiales de los ODS (Fuente: Guías de la ONU)
colores_ods = {
"1": "#E5243B", # Red - ODS 1: Fin de la Pobreza
"2": "#DDA63A", # Mustard - ODS 2: Hambre Cero
"3": "#4C9F38", # Kelly Green - ODS 3: Salud y Bienestar
"4": "#C5192D", # Dark Red - ODS 4: Educación de Calidad
"5": "#FF3A21", # Red Orange - ODS 5: Igualdad de Género
"6": "#26BDE2", # Bright Blue - ODS 6: Agua Limpia y Saneamiento
"7": "#FCC30B", # Yellow - ODS 7: Energía Asequible y No Contaminante
"8": "#A21942", # Burgundy Red - ODS 8: Trabajo Decente y Crecimiento Económico
"9": "#FD6925", # Orange - ODS 9: Industria, Innovación e Infraestructura
"10": "#DD1367", # Magenta - ODS 10: Reducción de las Desigualdades
"11": "#FD9D24", # Golden Yellow - ODS 11: Ciudades y Comunidades Sostenibles
"12": "#BF8B2E", # Dark Mustard - ODS 12: Producción y Consumo Responsables
"13": "#3F7E44", # Dark Green - ODS 13: Acción por el Clima
"14": "#0A97D9", # Blue - ODS 14: Vida Submarina
"15": "#56C02B", # Lime Green - ODS 15: Vida de Ecosistemas Terrestres
"16": "#00689D", # Royal Blue - ODS 16: Paz, Justicia e Instituciones Sólidas
"17": "#19486A", # Navy Blue - ODS 17: Alianzas para Lograr los Objetivos
}
# ============================================================================
# 2. GRÁFICA 1: DISTRIBUCIÓN DE SIMILARIDAD POR ODS (Box Plot Interactivo)
# ============================================================================
def viz_1_distribucion_por_ods(df, id_lvl, score, titulo):
"""
LÓGICA: Esta visualización muestra la distribución de valores de similaridad
coseno agrupados por cada ODS. Permite identificar:
- Qué ODS tienen mayor rango de similaridad
- La mediana de similaridad por ODS
- Outliers o valores atípicos
- Consistencia interna de cada ODS
INTERPRETACIÓN:
- Cajas más altas → Mayor variabilidad en la similaridad dentro del ODS
- Medianas altas → El ODS tiene indicadores más similares a la consulta
- Outliers superiores → Indicadores específicos muy relevantes
"""
fig = go.Figure()
for idx, ods in enumerate(sorted(df['ODS_ID'].unique())):
datos_ods = df[df['ODS_ID'] == ods][score]
fig.add_trace(go.Box(
y=datos_ods,
name=f'ODS {ods}',
boxmean='sd', # Mostrar media y desviación estándar
marker_color=px.colors.qualitative.Plotly[int(ods) % len(px.colors.qualitative.Plotly)]
))
fig.update_layout(
title={
'text': f'Distribución de Similaridad Coseno por {titulo}<br><sub>Análisis de dispersión y tendencia central por objetivo</sub>',
'x': 0.5,
'xanchor': 'center'
},
# xaxis_title='Objetivo de Desarrollo Sostenible',
xaxis_title=id_lvl,
yaxis_title='Similaridad Coseno',
height=600,
showlegend=False,
hovermode='x unified'
)
return fig
# ============================================================================
# 3. GRÁFICA 2: HEATMAP DE SIMILARIDAD (ODS vs Rango de Ranking)
# ============================================================================
def viz_2_heatmap_ods_ranking(df, id_lvl, score, rank, titulo):
"""
LÓGICA: Matriz de calor que muestra la intensidad de similaridad en función
de dos dimensiones: ODS (eje Y) y posición en el ranking (eje X agrupado).
Se divide el ranking en deciles (10 grupos) para visualizar cómo se
distribuye la similaridad a lo largo de la relevancia ordenada.
INTERPRETACIÓN:
- Colores cálidos (rojo/naranja) → Alta similaridad
- Colores fríos (azul) → Baja similaridad
- Patrón horizontal → Un ODS domina en ciertas posiciones
- Patrón vertical → Ciertas posiciones tienen alta similaridad en varios ODS
- Diagonal descendente → Comportamiento esperado (mayor rank → menor similaridad)
"""
# Crear deciles de ranking
df['rank_decil'] = pd.qcut(df[rank], q=10, labels=[f'D{i+1}' for i in range(10)])
# Crear matriz pivote
pivot_table = df.pivot_table(
values=score,
index=id_lvl,
columns='rank_decil',
aggfunc='mean'
)
fig, ax = plt.subplots(figsize=(14, 8))
sns.heatmap(
pivot_table,
annot=True,
fmt='.3f',
cmap='RdYlGn',
center=df[score].median(),
cbar_kws={'label': 'Similaridad Coseno Promedio'},
linewidths=0.5,
ax=ax
)
ax.set_title(
f'Heatmap: Similaridad Coseno por {id_lvl} y Decil de Ranking\n'
'Visualización de patrones de relevancia en función del orden',
fontsize=14,
pad=20
)
ax.set_xlabel('Decil de Ranking (D1=Top 10%, D10=Bottom 10%)', fontsize=12)
ax.set_ylabel(id_lvl, fontsize=12)
plt.tight_layout()
return fig
# ============================================================================
# 4. GRÁFICA 3: SCATTER PLOT 3D (ODS, Indicador, Similaridad)
# ============================================================================
def viz_3_scatter_3d_interactivo(df, id_lvl, score, rank, titulo):
"""
LÓGICA: Visualización tridimensional que permite explorar la relación
entre tres variables:
- Eje X: ODS ID
- Eje Y: Número de indicador dentro del ODS (extraído del indicador_id)
- Eje Z: Similaridad coseno
- Tamaño: Inversamente proporcional al ranking (más relevantes = más grandes)
- Color: Por ODS
INTERPRETACIÓN:
- Puntos altos (eje Z) → Alta similaridad
- Clusters verticales → Varios indicadores de un ODS son similares
- Puntos grandes en altura → Indicadores relevantes y bien posicionados
- Permite rotar e interactuar para descubrir patrones espaciales
"""
# Extraer número de indicador
df['indicador_num'] = df[id_lvl].str.extract(r'\.(\d+)\.').astype(float)
fig = go.Figure()
for ods in sorted(df['ODS_ID'].unique()):
datos_ods = df[df['ODS_ID'] == ods]
fig.add_trace(go.Scatter3d(
x=datos_ods['ODS_ID'],
y=datos_ods['indicador_num'],
z=datos_ods[score],
mode='markers',
name=f'ODS {ods}',
marker=dict(
size=10 - (datos_ods[rank] / len(df) * 8), # Tamaño inversamente proporcional al rank
opacity=0.7,
line=dict(width=0.5, color='white')
),
text=datos_ods[id_lvl],
hovertemplate='<b>%{text}</b><br>' +
'ODS: %{x}<br>' +
'Similaridad: %{z:.4f}<br>' +
'<extra></extra>'
))
fig.update_layout(
title='Visualización 3D: ODS × Indicador × Similaridad<br><sub>Exploración espacial de patrones de relevancia</sub>',
scene=dict(
xaxis_title='ODS ID',
yaxis_title='Número de Indicador',
zaxis_title='Similaridad Coseno',
camera=dict(eye=dict(x=1.5, y=1.5, z=1.3))
),
height=700,
showlegend=True
)
return fig
# ============================================================================
# 5. GRÁFICA 4: RADAR CHART - Similaridad Promedio por ODS
# ============================================================================
def viz_4_radar_chart_ods(df, id_lvl, score, rank, titulo):
"""
LÓGICA: Gráfico de radar (spider chart) que muestra la similaridad promedio
de cada ODS en forma circular. Útil para comparar rápidamente el perfil
de relevancia de todos los ODS.
INTERPRETACIÓN:
- Áreas más grandes → Mayor similaridad promedio con la consulta
- Forma del polígono → Perfil de cobertura de la iniciativa
- Picos → ODS altamente relevantes
- Valles → ODS menos relacionados
- Simetría → Iniciativa balanceada entre ODS vs. especializada
"""
# Calcular promedios por ODS
ods_stats = df.groupby(id_lvl).agg({
score: ['mean', 'max', 'count']
}).reset_index()
ods_stats.columns = [id_lvl, 'sim_promedio', 'sim_max', 'count_indicadores']
ods_stats = ods_stats.sort_values(id_lvl)
fig = go.Figure()
# Similaridad promedio
fig.add_trace(go.Scatterpolar(
r=ods_stats['sim_promedio'],
theta=['ODS ' + str(x) for x in ods_stats[id_lvl]],
fill='toself',
name='Similaridad Promedio',
line_color='blue',
fillcolor='rgba(0, 0, 255, 0.2)'
))
# Similaridad máxima
fig.add_trace(go.Scatterpolar(
r=ods_stats['sim_max'],
theta=['ODS ' + str(x) for x in ods_stats[id_lvl]],
fill='toself',
name='Similaridad Máxima',
line_color='red',
fillcolor='rgba(255, 0, 0, 0.1)'
))
# Calcular rango automático con margen del 5%
valor_minimo = min(ods_stats['sim_promedio'].min(), ods_stats['sim_max'].min())
valor_maximo = max(ods_stats['sim_promedio'].max(), ods_stats['sim_max'].max())
margen = (valor_maximo - valor_minimo) * 0.05
rango_automatico = [max(0, valor_minimo - margen), min(1, valor_maximo + margen)]
fig.update_layout(
polar=dict(
radialaxis=dict(
visible=True,
range=rango_automatico
)
),
title=f'Radar Chart: Perfil de Similaridad por {titulo}<br><sub>Comparación de promedios y máximos</sub>',
showlegend=True,
height=600
)
return fig
# ============================================================================
# 6. GRÁFICA 5: SUNBURST - Jerarquía ODS → Indicadores
# ============================================================================
def viz_5_sunburst_jerarquia(df, id_lvl, score, rank, titulo):
"""
LÓGICA: Diagrama de sunburst (sol radiante) que muestra la jerarquía
ODS → Indicadores con el tamaño proporcional a la similaridad.
El círculo interior representa los ODS y los anillos exteriores los
indicadores dentro de cada ODS.
INTERPRETACIÓN:
- Segmentos grandes → Indicadores o grupos de indicadores muy similares
- Colores → Gradiente de similaridad (más oscuro = mayor similaridad)
- Permite drill-down interactivo
- Visualiza la contribución relativa de cada indicador al ODS
"""
# Preparar datos para sunburst
df_sun = df.copy()
df_sun['ods_label'] = 'ODS ' + df_sun['ODS_ID'].astype(str)
df_sun['path'] = df_sun['ods_label'] + ' / ' + df_sun[id_lvl]
# Limitar a top 100 para mejor visualización
df_sun_top = df_sun.nsmallest(100, rank)
fig = px.sunburst(
df_sun_top,
path=['ods_label', id_lvl],
values=score,
color=score,
color_continuous_scale='Viridis',
hover_data=[rank],
title=f'Sunburst: Jerarquía {titulo} → Indicadores (Top 100)<br><sub>Tamaño proporcional a similaridad</sub>'
)
fig.update_layout(
height=700,
coloraxis_colorbar=dict(title="Similaridad")
)
return fig
# ============================================================================
# 7. GRÁFICA 6: CASCADA - Top Indicadores por ODS
# ============================================================================
def viz_6_top_indicadores_por_ods(df, id_lvl, score, rank, titulo, top_n=3):
"""
LÓGICA: Para cada ODS, muestra los top N indicadores con mayor similaridad
en un formato de barras horizontales agrupadas.
Permite comparar:
- Cuál es el mejor indicador de cada ODS
- La brecha entre el mejor y los siguientes
- Qué ODS tiene los indicadores más relevantes en general
INTERPRETACIÓN:
- Barras más largas → Mayor similaridad
- Agrupación densa → Varios indicadores igualmente relevantes
- Gaps grandes → Un indicador destaca sobre el resto en ese ODS
"""
# Obtener top N por ODS
top_indicadores = df.groupby('ODS_ID').apply(
lambda x: x.nsmallest(top_n, rank)
).reset_index(drop=True)
fig = px.bar(
top_indicadores,
x=score,
y=id_lvl,
color=id_lvl,
orientation='h',
facet_row=id_lvl,
height=300 * len(df[id_lvl].unique()) // 3,
title=f'Top {top_n} Indicadores con Mayor Similaridad por ODS<br><sub>Análisis de relevancia por objetivo</sub>',
labels={score: 'Similaridad Coseno', id_lvl: 'Indicador'},
color_continuous_scale='Plasma'
)
fig.update_yaxes(showticklabels=True, matches=None)
fig.update_xaxes(matches='x')
return fig
# ============================================================================
# 8. GRÁFICA 7: STREAM GRAPH - Evolución de Similaridad
# ============================================================================
def viz_7_streamgraph_similaridad(df, id_lvl, score, rank, titulo):
"""
LÓGICA: Gráfico de área apilada que muestra cómo contribuye cada ODS
a la similaridad acumulada a lo largo del ranking.
El eje X es el ranking (ordenado) y el eje Y muestra el área acumulada
de similaridad por ODS.
INTERPRETACIÓN:
- Áreas más anchas → ODS con mayor presencia en ese rango de ranking
- Cambios de color dominante → Transición de relevancia entre ODS
- Posición en ranking bajo → Indicadores más relevantes
- Permite ver qué ODS domina en qué rangos de relevancia
"""
# Crear bins de ranking
df['rank_bin'] = pd.cut(df[rank], bins=20, labels=False)
# Agrupar por rank_bin y ODS
stream_data = df.groupby(['rank_bin', id_lvl])[score].sum().reset_index()
# Pivotar para streamgraph
stream_pivot = stream_data.pivot(index='rank_bin', columns=id_lvl, values=score).fillna(0)
fig = go.Figure()
for ods in stream_pivot.columns:
fig.add_trace(go.Scatter(
x=stream_pivot.index,
y=stream_pivot[ods],
mode='lines',
name=f'ODS {ods}',
stackgroup='one',
groupnorm='percent', # Normalizar a porcentaje
hovertemplate='ODS %{fullData.name}<br>Contribución: %{y:.1f}%<extra></extra>'
))
fig.update_layout(
title='Stream Graph: Contribución de cada ODS por Rango de Ranking<br><sub>Evolución de relevancia normalizada</sub>',
xaxis_title='Rango de Ranking (agrupado)',
yaxis_title='Contribución Porcentual',
height=600,
hovermode='x unified'
)
return fig
# ============================================================================
# 9. GRÁFICA 8: VIOLIN PLOT - Comparación Detallada de Distribuciones
# ============================================================================
def viz_8_violin_plot_ods(df, id_lvl, score, rank, titulo):
"""
LÓGICA: Similar al box plot pero muestra la distribución completa de
densidad de probabilidad de la similaridad para cada ODS.
El ancho del "violín" representa la concentración de valores en ese rango.
INTERPRETACIÓN:
- Violines anchos → Muchos valores en ese rango de similaridad
- Violines angostos → Pocos valores en ese rango
- Forma bimodal → Dos grupos de indicadores con diferente similaridad
- Forma unimodal → Indicadores homogéneos en similaridad
- Permite ver distribuciones no normales que el box plot no captura
"""
fig = go.Figure()
for ods in sorted(df[id_lvl].unique()):
datos_ods = df[df[id_lvl] == ods][score]
fig.add_trace(go.Violin(
y=datos_ods,
name=f'ODS {ods}',
box_visible=True,
meanline_visible=True,
fillcolor=px.colors.qualitative.Plotly[int(ods) % len(px.colors.qualitative.Plotly)],
opacity=0.6,
x0=f'ODS {ods}'
))
fig.update_layout(
title='Violin Plot: Distribución de Densidad de Similaridad por ODS<br><sub>Análisis detallado de concentración de valores</sub>',
yaxis_title='Similaridad Coseno',
xaxis_title='Objetivo de Desarrollo Sostenible',
height=600,
showlegend=False
)
return fig
# ============================================================================
# 10. GRÁFICA 9: DASHBOARD INTEGRADO - Métricas Clave
# ============================================================================
def viz_9_dashboard_metricas(df, id_lvl, score, rank, titulo):
"""
LÓGICA: Dashboard con múltiples paneles que resume las métricas clave:
- Panel 1: Top 10 indicadores con mayor similaridad
- Panel 2: Estadísticas por ODS (media, std, max, min)
- Panel 3: Distribución global de similaridad (histograma)
- Panel 4: Correlación entre rank y similaridad
INTERPRETACIÓN:
- Vista holística de la calidad del matching
- Permite validar que el ranking está bien correlacionado con similaridad
- Identifica outliers o problemas en el cálculo
- Facilita comunicación de resultados a stakeholders
"""
fig = make_subplots(
rows=2, cols=2,
subplot_titles=(
'Top 10 Indicadores por Similaridad',
'Estadísticas por ODS',
'Distribución Global de Similaridad',
'Correlación: Rank vs Similaridad'
),
specs=[
[{"type": "bar"}, {"type": "table"}],
[{"type": "histogram"}, {"type": "scatter"}]
]
)
# Panel 1: Top 10
top_10 = df.nsmallest(10, rank)
fig.add_trace(
go.Bar(
x=top_10[score],
y=top_10['indicador_id'],
orientation='h',
marker_color='lightblue',
text=top_10[score].round(4),
textposition='auto'
),
row=1, col=1
)
# Panel 2: Tabla de estadísticas
stats_ods = df.groupby(id_lvl)[score].agg(['mean', 'std', 'min', 'max', 'count']).reset_index()
stats_ods.columns = ['ODS', 'Media', 'Std', 'Min', 'Max', 'Count']
stats_ods = stats_ods.round(4)
fig.add_trace(
go.Table(
header=dict(values=list(stats_ods.columns),
fill_color='paleturquoise',
align='left'),
cells=dict(values=[stats_ods[col] for col in stats_ods.columns],
fill_color='lavender',
align='left')
),
row=1, col=2
)
# Panel 3: Histograma
fig.add_trace(
go.Histogram(
x=df[score],
nbinsx=30,
marker_color='indianred',
name='Distribución'
),
row=2, col=1
)
# Panel 4: Scatter rank vs similaridad
fig.add_trace(
go.Scatter(
x=df[rank],
y=df[score],
mode='markers',
marker=dict(
size=5,
color=df[id_lvl],
colorscale='Viridis',
showscale=True,
colorbar=dict(title="ODS", x=1.15)
),
text=df['indicador_id']
),
row=2, col=2
)
# Añadir línea de tendencia
z = np.polyfit(df[rank], df[score], 1)
p = np.poly1d(z)
fig.add_trace(
go.Scatter(
x=df[rank],
y=p(df[rank]),
mode='lines',
line=dict(color='red', dash='dash'),
name='Tendencia'
),
row=2, col=2
)
fig.update_xaxes(title_text="Similaridad", row=1, col=1)
fig.update_xaxes(title_text="Similaridad", row=2, col=1)
fig.update_xaxes(title_text="Rank", row=2, col=2)
fig.update_yaxes(title_text="Indicador", row=1, col=1)
fig.update_yaxes(title_text="Frecuencia", row=2, col=1)
fig.update_yaxes(title_text="Similaridad", row=2, col=2)
fig.update_layout(
height=900,
showlegend=False,
title_text="Dashboard Integrado: Métricas Clave de Similaridad ODS",
title_x=0.5
)
return fig
# ============================================================================
# 11. GRÁFICA 10: MATRIZ DE TRANSICIÓN - Cambios de ODS por Ranking
# ============================================================================
def viz_10_matriz_transicion(df, id_lvl, score, rank, titulo):
"""
LÓGICA: Muestra cómo cambia el ODS dominante a medida que avanzamos
en el ranking. Divide el ranking en cuartiles y muestra qué ODS
tiene más presencia en cada cuartil.
INTERPRETACIÓN:
- Permite ver si un ODS domina consistentemente
- Identifica cambios de dominancia (ej: ODS 5 domina top rankings,
luego ODS 17)
- Útil para entender si la iniciativa es más afín a ciertos ODS
- Ayuda a explicar por qué ciertos ODS aparecen más arriba
"""
# Crear cuartiles
df['cuartil'] = pd.qcut(df[rank], q=4, labels=['Q1 (Top)', 'Q2', 'Q3', 'Q4 (Bottom)'])
# Contar presencia de ODS por cuartil
matriz = pd.crosstab(df[id_lvl], df['cuartil'], normalize='columns') * 100
fig, ax = plt.subplots(figsize=(12, 8))
sns.heatmap(
matriz,
annot=True,
fmt='.1f',
cmap='YlOrRd',
cbar_kws={'label': '% de Presencia en Cuartil'},
linewidths=0.5,
ax=ax
)
ax.set_title(
'Matriz de Transición: Presencia de ODS por Cuartil de Ranking\n'
'Análisis de dominancia y evolución',
fontsize=14,
pad=20
)
ax.set_xlabel('Cuartil de Ranking', fontsize=12)
ax.set_ylabel('ODS ID', fontsize=12)
plt.tight_layout()
return fig
def viz_19_resumen_tags(df_ods, df_metas, df_indicadores):
"""
Visualización 19: Resumen en Tags (Métricas Clave)
Muestra métricas agregadas en formato de tags/badges:
- Cantidad de iniciativas
- Promedios por iniciativa (ODS, Metas, Indicadores)
- Elementos más frecuentes
"""
# Calcular métricas
n_iniciativas = df_ods['INICIATIVA_ID'].nunique() if 'INICIATIVA_ID' in df_ods.columns else 1
# Promedios por iniciativa
try:
ods_por_iniciativa = df_ods.groupby('INICIATIVA_ID')['ODS_ID'].nunique().mean()
metas_por_iniciativa = df_metas.groupby('INICIATIVA_ID')['META_ID'].nunique().mean()
ind_por_iniciativa = df_indicadores.groupby('INICIATIVA_ID')['INDICADOR_ID'].nunique().mean()
except:
ods_por_iniciativa = df_ods.groupby('id_unico')['ODS_ID'].nunique().mean()
metas_por_iniciativa = df_metas.groupby('id_unico')['META_ID'].nunique().mean()
ind_por_iniciativa = df_indicadores.groupby('id_unico')['INDICADOR_ID'].nunique().mean()
# Más frecuentes
ods_mas_frecuente = df_ods['ODS_ID'].mode()[0] if len(df_ods) > 0 else 0
meta_mas_frecuente = df_metas['META_ID'].mode()[0] if len(df_metas) > 0 else 'N/A'
ind_mas_frecuente = df_indicadores['INDICADOR_ID'].mode()[0] if len(df_indicadores) > 0 else 'N/A'
# Frecuencias
ods_freq = (df_ods['ODS_ID'] == ods_mas_frecuente).sum()
meta_freq = (df_metas['META_ID'] == meta_mas_frecuente).sum()
ind_freq = (df_indicadores['INDICADOR_ID'] == ind_mas_frecuente).sum()
# Crear HTML con tags
html1 = f"""
<div style="font-family: Arial, sans-serif; padding: 20px;">
<!-- <h2 style="color: #2E5090; margin-bottom: 30px;">📊 Resumen de Análisis</h2> -->
<!-- Métricas Generales -->
<div style="margin-bottom: 40px;">
<h3 style="color: #4472C4; margin-bottom: 15px;">Métricas Generales</h3>
<div style="display: flex; flex-wrap: wrap; gap: 15px;">
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 20px 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1);">
<div style="font-size: 14px; opacity: 0.9;">Iniciativas Analizadas</div>
<div style="font-size: 36px; font-weight: bold; margin-top: 5px;">{n_iniciativas}</div>
</div>
</div>
</div>
<!-- Promedios por Iniciativa
<div style="margin-bottom: 40px;">
<h3 style="color: #4472C4; margin-bottom: 15px;">Promedio por Iniciativa</h3>
<div style="display: flex; flex-wrap: wrap; gap: 15px;">
<div style="background: linear-gradient(135deg, #2E5090 0%, #4472C4 100%); color: white; padding: 20px 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); min-width: 180px;">
<div style="font-size: 14px; opacity: 0.9;">ODS</div>
<div style="font-size: 36px; font-weight: bold; margin-top: 5px;">{ods_por_iniciativa:.1f}</div>
</div>
<div style="background: linear-gradient(135deg, #4472C4 0%, #70AD47 100%); color: white; padding: 20px 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); min-width: 180px;">
<div style="font-size: 14px; opacity: 0.9;">Metas</div>
<div style="font-size: 36px; font-weight: bold; margin-top: 5px;">{metas_por_iniciativa:.1f}</div>
</div>
<div style="background: linear-gradient(135deg, #70AD47 0%, #A5D6A7 100%); color: white; padding: 20px 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); min-width: 180px;">
<div style="font-size: 14px; opacity: 0.9;">Indicadores</div>
<div style="font-size: 36px; font-weight: bold; margin-top: 5px;">{ind_por_iniciativa:.1f}</div>
</div>
</div>
</div>
-->
<!-- Más Frecuentes
<div style="margin-bottom: 20px;">
<h3 style="color: #4472C4; margin-bottom: 15px;">Elementos Más Frecuentes</h3>
<div style="display: flex; flex-wrap: wrap; gap: 15px;">
<div style="background: white; border: 3px solid #2E5090; padding: 20px 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); min-width: 180px;">
<div style="font-size: 14px; color: #666; margin-bottom: 5px;">ODS Más Frecuente</div>
<div style="font-size: 32px; font-weight: bold; color: #2E5090;">ODS {ods_mas_frecuente}</div>
<div style="font-size: 13px; color: #888; margin-top: 5px;">{ods_freq} apariciones</div>
</div>
<div style="background: white; border: 3px solid #4472C4; padding: 20px 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); min-width: 180px;">
<div style="font-size: 14px; color: #666; margin-bottom: 5px;">Meta Más Frecuente</div>
<div style="font-size: 32px; font-weight: bold; color: #4472C4;">Meta {meta_mas_frecuente}</div>
<div style="font-size: 13px; color: #888; margin-top: 5px;">{meta_freq} apariciones</div>
</div>
<div style="background: white; border: 3px solid #70AD47; padding: 20px 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); min-width: 220px;">
<div style="font-size: 14px; color: #666; margin-bottom: 5px;">Indicador Más Frecuente</div>
<div style="font-size: 24px; font-weight: bold; color: #70AD47;">{str(ind_mas_frecuente)[:30]}</div>
<div style="font-size: 13px; color: #888; margin-top: 5px;">{ind_freq} apariciones</div>
</div>
</div>
</div>
-->
</div>
"""
html2 = f"""
<div style="font-family: Arial, sans-serif; padding: 20px;">
<!-- Promedios por Iniciativa -->
<div style="margin-bottom: 40px;">
<h3 style="color: #4472C4; margin-bottom: 15px;">Promedio por Iniciativa</h3>
<div style="display: flex; flex-wrap: wrap; gap: 15px;">
<div style="background: linear-gradient(135deg, #2E5090 0%, #4472C4 100%); color: white; padding: 20px 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); min-width: 180px;">
<div style="font-size: 14px; opacity: 0.9;">ODS</div>
<div style="font-size: 36px; font-weight: bold; margin-top: 5px;">{ods_por_iniciativa:.1f}</div>
</div>
<div style="background: linear-gradient(135deg, #4472C4 0%, #70AD47 100%); color: white; padding: 20px 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); min-width: 180px;">
<div style="font-size: 14px; opacity: 0.9;">Metas</div>
<div style="font-size: 36px; font-weight: bold; margin-top: 5px;">{metas_por_iniciativa:.1f}</div>
</div>
<div style="background: linear-gradient(135deg, #70AD47 0%, #A5D6A7 100%); color: white; padding: 20px 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); min-width: 180px;">
<div style="font-size: 14px; opacity: 0.9;">Indicadores</div>
<div style="font-size: 36px; font-weight: bold; margin-top: 5px;">{ind_por_iniciativa:.1f}</div>
</div>
</div>
</div>
</div>
"""
html3 = f"""
<div style="font-family: Arial, sans-serif; padding: 20px;">
<!-- Más Frecuentes -->
<div style="margin-bottom: 20px;">
<h3 style="color: #4472C4; margin-bottom: 15px;">Elementos Más Frecuentes</h3>
<div style="display: flex; flex-wrap: wrap; gap: 15px;">
<div style="background: white; border: 3px solid #2E5090; padding: 20px 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); min-width: 180px;">
<div style="font-size: 14px; color: #666; margin-bottom: 5px;">ODS Más Frecuente</div>
<div style="font-size: 32px; font-weight: bold; color: #2E5090;">ODS {ods_mas_frecuente}</div>
<div style="font-size: 13px; color: #888; margin-top: 5px;">{ods_freq} apariciones</div>
</div>
<div style="background: white; border: 3px solid #4472C4; padding: 20px 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); min-width: 180px;">
<div style="font-size: 14px; color: #666; margin-bottom: 5px;">Meta Más Frecuente</div>
<div style="font-size: 32px; font-weight: bold; color: #4472C4;">Meta {meta_mas_frecuente}</div>
<div style="font-size: 13px; color: #888; margin-top: 5px;">{meta_freq} apariciones</div>
</div>
<div style="background: white; border: 3px solid #70AD47; padding: 20px 30px; border-radius: 10px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); min-width: 220px;">
<div style="font-size: 14px; color: #666; margin-bottom: 5px;">Indicador Más Frecuente</div>
<div style="font-size: 24px; font-weight: bold; color: #70AD47;">{str(ind_mas_frecuente)[:30]}</div>
<div style="font-size: 13px; color: #888; margin-top: 5px;">{ind_freq} apariciones</div>
</div>
</div>
</div>
</div>
"""
return (html1, html2, html3)
import plotly.graph_objects as go
# Para visualizaciones_relaciones.py
def viz_20_pareto_ods(df, nivel, nivel_pareto, mass = False, iniciativa_id=None):
"""
Visualización 20: Diagrama de Pareto por Iniciativa
Muestra la curva 80/20 para identificar ODS críticos
"""
if mass:
df_init = df.copy()
# Ordenar por similaridad
df_sorted = df_init.sort_values(f'score', ascending=False).reset_index(drop=True)
# Se aplica calculo exponencial para acentuar y diferenciar los pesos
df_sorted[f'{nivel}_similaridad_cos_exp3'] = df_sorted[f'score'] ** 3
else:
# Si no se especifica iniciativa, usar la primera
if iniciativa_id == 'individual':
iniciativa_id = 'consulta_individual'
df_init = df.copy()
elif iniciativa_id is None:
iniciativa_id = df['INICIATIVA_ID'].iloc[0]
# Filtrar iniciativa
df_init = df[df['INICIATIVA_ID'] == iniciativa_id].copy()
# Ordenar por similaridad
df_sorted = df_init.sort_values(f'{nivel}_similaridad_cos', ascending=False).reset_index(drop=True)
# Se aplica calculo exponencial para acentuar y diferenciar los pesos
df_sorted[f'{nivel}_similaridad_cos_exp3'] = df_sorted[f'{nivel}_similaridad_cos'] ** 3
# Normalizar la columna ods_similaridad_cos_exp3
min_val = df_sorted[f'{nivel}_similaridad_cos_exp3'].min()
max_val = df_sorted[f'{nivel}_similaridad_cos_exp3'].max()
if max_val - min_val > 0:
df_sorted[f'{nivel}_similaridad_cos_exp3_norm'] = (df_sorted[f'{nivel}_similaridad_cos_exp3'] - min_val) / (max_val - min_val)
else:
df_sorted[f'{nivel}_similaridad_cos_exp3_norm'] = 0.0 # Handle case where all values are the same
# Calcular Pareto
total = df_sorted[f'{nivel}_similaridad_cos_exp3_norm'].sum()
df_sorted['acumulado'] = df_sorted[f'{nivel}_similaridad_cos_exp3_norm'].cumsum()
df_sorted['porcentaje_acumulado'] = (df_sorted['acumulado'] / total) * 100
# Punto de corte 80%
corte_80 = (df_sorted['porcentaje_acumulado'] <= (nivel_pareto * 100)).sum()
# Crear figura con doble eje Y
fig = go.Figure()
# Barras: Similaridad individual
fig.add_trace(go.Bar(
name='Similaridad',
x=[f"{nivel.upper()} {ods}" for ods in df_sorted[f'{nivel.upper()}_ID']],
y=df_sorted[f'{nivel}_similaridad_cos_exp3_norm'],
marker_color='#4472C4',
yaxis='y',
hovertemplate='<b>%{x}</b><br>Similaridad: %{y:.4f}<extra></extra>'
))
# Línea: Porcentaje acumulado
fig.add_trace(go.Scatter(
name='% Acumulado',
x=[f"{nivel.upper()} {ods}" for ods in df_sorted[f'{nivel.upper()}_ID']],
y=df_sorted['porcentaje_acumulado'],
mode='lines+markers',
line=dict(color='#FF6B6B', width=3),
marker=dict(size=8),
yaxis='y2',
hovertemplate='<b>%{x}</b><br>Acumulado: %{y:.1f}%<extra></extra>'
))
# Línea de corte 80%
fig.add_hline(
y=80,
line_dash="dash",
line_color="red",
annotation_text="80% (Regla de Pareto)",
annotation_position="right",
yref='y2'
)
fig.add_hline(
y=( nivel_pareto * 100 ),
line_dash="dash",
line_color="red",
annotation_text=f"{nivel_pareto*100:.0f}% (Regla de Pareto)",
annotation_position="right",
yref='y2'
)
# Sombra del área Pareto (top críticos)
fig.add_vrect(
x0=-0.5,
x1=corte_80 - 0.5,
fillcolor="green",
opacity=0.1,
layer="below",
line_width=0,
annotation_text=f"Top {corte_80} {nivel.upper()}<br>({nivel_pareto*100:.0f}% del valor)",
annotation_position="top left"
)
# Layout con doble eje Y
fig.update_layout(
title=dict(
text=f"Diagrama de Pareto - Iniciativa {str(iniciativa_id)[:15]}<br><sub>Top {corte_80} {nivel.upper()} representan el {nivel_pareto*100:.0f}% de la similaridad total</sub>",
font=dict(size=18, color='#2E5090')
),
xaxis=dict(title=f'{nivel.upper()} ordenados por Similaridad'),
yaxis=dict(
title='Similaridad Coseno',
side='left',
showgrid=True
),
yaxis2=dict(
title='Porcentaje Acumulado (%)',
side='right',
overlaying='y',
range=[0, 105],
showgrid=False
),
hovermode='x unified',
height=600,
legend=dict(x=0.7, y=0.95),
plot_bgcolor='white'
)
return fig, corte_80
def viz_21_ranking_mixto_masivo(df_resultado, nivel):
"""
Visualización 21: Ranking de ODS - Análisis Masivo
Muestra el resultado de la estrategia mixta con barras horizontales
coloreadas por score y tamaño por frecuencia
"""
import plotly.graph_objects as go
from plotly.subplots import make_subplots
# Ordenar por score descendente
df_plot = df_resultado.sort_values('score', ascending=True).tail(15) # Top 15
# # Normalizar score para colores
# score_norm = (df_plot['score'] - df_plot['score'].min()) / (df_plot['score'].max() - df_plot['score'].min())
# Obtener colores basados en ODS_ID del diccionario colores_ods
colors = [colores_ods[ods_id] for ods_id in df_plot['ODS_ID']]
# Normalizar frecuencia al rango [5, 25] para tamaños de círculos proporcionales
freq_min = df_plot['frecuencia'].min()
freq_max = df_plot['frecuencia'].max()
if freq_max > freq_min:
freq_norm = (df_plot['frecuencia'] - freq_min) / (freq_max - freq_min) * 20 + 5
else:
freq_norm = pd.Series([15] * len(df_plot), index=df_plot.index)
# # Crear colores del azul al verde según score
# colors = [f'rgb({int(46 + (112-46)*s)}, {int(80 + (173-80)*s)}, {int(144 + (71-144)*s)})'
# for s in score_norm]
fig = go.Figure()
# if nivel == 'meta':
# Barras horizontales META
fig.add_trace(go.Bar(
y=[f"{nivel.upper()} {ods}" for ods in df_plot[f'{nivel.upper()}_ID']],
x=df_plot['score'],
orientation='h',
marker=dict(
color=colors,
line=dict(color='white', width=1)
),
text=[f"{score:.4f}" for score in df_plot['score']],
textposition='inside',
textfont=dict(color='white', size=11, family='Arial Black'),
# hovertemplate=f'<b>{nivel.upper()} %{{y}}</b><br>Score: %{{x:.4f}}<br>Frecuencia: %{{customdata}}<extra></extra>',
# customdata=df_plot['frecuencia']
#version funcional
# hovertemplate=f'<b>{nivel.upper()} %{{y}}</b><br>Score: %{{x:.4f}}<br>ODS: %{{customdata[0]}}<br>Frecuencia: %{{customdata[1]}}<extra></extra>',
# customdata=df_plot[['ODS_ID', 'frecuencia']].values
# hovertemplate=f'<b>{nivel.upper()} %{{y}}</b><br>Score: %{{x:.4f}}<br>ODS: %{{customdata[0]}}<br>Frecuencia: %{{customdata[1]}}<extra></extra>',
hovertemplate=f'<b> %{{y}}</b><br>Score: %{{x:.4f}}<br>ODS: %{{customdata[0]}}<br>Frecuencia: %{{customdata[1]}}<extra></extra>',
customdata=df_plot[['ODS_ID', 'frecuencia']].values
))
# elif nivel == 'ods':
# # Barras horizontales ODS
# fig.add_trace(go.Bar(
# y=[f"{nivel.upper()} {ods}" for ods in df_plot[f'{nivel.upper()}_ID']],
# x=df_plot['score'],
# orientation='h',
# marker=dict(
# color=colors,
# line=dict(color='white', width=1)
# ),
# text=[f"{score:.4f}" for score in df_plot['score']],
# textposition='inside',
# textfont=dict(color='white', size=11, family='Arial Black'),
# # hovertemplate=f'<b>{nivel.upper()} %{{y}}</b><br>Score: %{{x:.4f}}<br>Frecuencia: %{{customdata}}<extra></extra>',
# # customdata=df_plot['frecuencia']
# #version funcional
# # hovertemplate=f'<b>{nivel.upper()} %{{y}}</b><br>Score: %{{x:.4f}}<br>ODS: %{{customdata[0]}}<br>Frecuencia: %{{customdata[1]}}<extra></extra>',
# # customdata=df_plot[['ODS_ID', 'frecuencia']].values
# hovertemplate=f'<b> %{{y}}</b><br>Score: %{{x:.4f}}<br>ODS: %{{customdata[0]}}<br>Frecuencia: %{{customdata[1]}}<b> %{{customdata[2]}}</b><extra></extra>',
# customdata=df_plot[['ODS_ID', 'frecuencia', 'OBJETIVO']].values
# ))
# Agregar marcadores de frecuencia (normalizados)
fig.add_trace(go.Scatter(
y=[f"{nivel.upper()} {ods}" for ods in df_plot[f'{nivel.upper()}_ID']],
x=df_plot['score'],
mode='markers',
marker=dict(
size=freq_norm, # Tamaño normalizado proporcional a frecuencia [5-25]
color='rgba(255, 255, 255, 0.6)',
line=dict(color='#2E5090', width=2)
),
showlegend=False,
hoverinfo='skip'
))
fig.update_layout(
title=dict(
# text=f"Ranking Global de {nivel.upper()} - Score <br><sub>Tamaño de círculos = Frecuencia de aparición</sub>",
text=f"Ranking Global de {nivel.upper()} - Estrategia Mixta<br><sub>Colores = ODS asociado | Tamaño de círculos = Frecuencia de aparición</sub>",
font=dict(size=20, color='#2E5090'),
x=0.5,
xanchor='center'
),
xaxis=dict(
title='Score Mixto Ponderado',
showgrid=True,
gridcolor='rgba(0,0,0,0.1)'
),
yaxis=dict(
title='',
showgrid=False
),
height=600,
plot_bgcolor='white',
paper_bgcolor='white',
margin=dict(l=80, r=20, t=100, b=60)
)
return fig
def viz_22_composicion_score_mixto(w_sim=0.5, w_rank=0.3, w_freq=0.2):
"""
Visualización 22: Composición del Score Mixto
Muestra los pesos/porcentajes usados en la estrategia mixta
"""
import plotly.graph_objects as go
# Datos de composición
componentes = ['Similaridad', 'Ranking', 'Frecuencia']
pesos = [w_sim, w_rank, w_freq]
porcentajes = [w * 100 for w in pesos]
colores = ['#2E5090', '#4472C4', '#70AD47']
# Crear figura con solo gráfico de pastel
fig = go.Figure()
# Gráfico de pastel
fig.add_trace(
go.Pie(
labels=componentes,
values=porcentajes,
marker=dict(colors=colores, line=dict(color='white', width=2)),
textinfo='label+percent',
textfont=dict(size=14, color='white', family='Arial Black'),
hovertemplate='<b>%{label}</b><br>Peso: %{value:.0f}%<extra></extra>',
hole=0.4
)
)
# Layout
fig.update_layout(
title=dict(
text=f"Composición del Score Mixto<br><sub>Total: {sum(porcentajes):.0f}%</sub>",
font=dict(size=20, color='#2E5090'),
x=0.5,
xanchor='center'
),
height=600*0.7,
width=700*0.6,
showlegend=True,
plot_bgcolor='white',
paper_bgcolor='white',
legend=dict(
orientation='h',
x=0.5,
y=-0.15,
xanchor='center',
yanchor='top'
)
)
# Agregar anotación central en el donut
fig.add_annotation(
text=f"Score<br>Mixto",
x=0.5, y=0.5,
xref='paper', yref='paper',
showarrow=False,
font=dict(size=18, color='#2E5090', family='Arial Black')
)
return fig
def viz_23_nube_palabras_paloma(df, columna_texto, max_palabras=100):
"""
Visualización 23: Nube de Palabras en Forma de Paloma de Paz
Genera una nube de palabras con la forma de una paloma
mostrando las palabras más frecuentes de una columna de texto
Args:
df: DataFrame con los datos
columna_texto: Nombre de la columna con texto
max_palabras: Máximo de palabras a mostrar
"""
from wordcloud import WordCloud
import matplotlib.pyplot as plt
import numpy as np
from PIL import Image
import os
import tempfile
# Combinar todo el texto de la columna
texto_completo = ' '.join(df[columna_texto].dropna().astype(str))
# Palabras a excluir (stopwords en español)
stopwords_es = {
'el', 'la', 'de', 'que', 'y', 'a', 'en', 'un', 'ser', 'se', 'no', 'haber',
'por', 'con', 'su', 'para', 'como', 'estar', 'tener', 'le', 'lo', 'todo',
'pero', 'más', 'hacer', 'o', 'poder', 'decir', 'este', 'ir', 'otro', 'ese',
'la', 'si', 'me', 'ya', 'ver', 'porque', 'dar', 'cuando', 'él', 'muy',
'sin', 'vez', 'mucho', 'saber', 'qué', 'sobre', 'mi', 'alguno', 'mismo',
'yo', 'también', 'hasta', 'año', 'dos', 'querer', 'entre', 'así', 'primero',
'desde', 'grande', 'eso', 'ni', 'nos', 'llegar', 'pasar', 'tiempo', 'ella',
'del', 'al', 'los', 'las', 'una', 'unos', 'unas', 'ante', 'bajo', 'cabe',
'donde', 'durante', 'mediante', 'salvo', 'según', 'excepto', 'hacia', 'mediante',
'sus'
}
# Crear máscara en forma de paloma de paz
# Imagen SVG de paloma simplificada como array numpy
def crear_mascara_paloma(width=800, height=600):
"""Crea una máscara en forma de paloma de paz"""
from PIL import Image, ImageDraw
# Crear imagen en blanco
img = Image.new('RGB', (width, height), 'white')
draw = ImageDraw.Draw(img)
# Coordenadas de la paloma (simplificada)
# Cuerpo
cuerpo = [(width//2, height//2 + 50),
(width//2 + 100, height//2 + 80),
(width//2 + 80, height//2 + 120),
(width//2 - 20, height//2 + 100)]
# Cabeza
cabeza_center = (width//2 - 30, height//2 - 20)
cabeza_radius = 40
# Ala izquierda (extendida)
ala_izq = [(width//2, height//2 + 50),
(width//2 - 150, height//2 - 80),
(width//2 - 200, height//2 - 100),
(width//2 - 180, height//2),
(width//2 - 100, height//2 + 20)]
# Ala derecha (extendida)
ala_der = [(width//2, height//2 + 50),
(width//2 + 150, height//2 - 50),
(width//2 + 220, height//2 - 80),
(width//2 + 200, height//2 + 10),
(width//2 + 120, height//2 + 30)]
# Cola
cola = [(width//2 + 80, height//2 + 120),
(width//2 + 120, height//2 + 180),
(width//2 + 100, height//2 + 200),
(width//2 + 60, height//2 + 170)]
# Rama de olivo (en el pico)
rama = [(width//2 - 70, height//2 - 10),
(width//2 - 120, height//2 - 30),
(width//2 - 140, height//2 - 25)]
# Dibujar formas en negro (área donde irán las palabras)
draw.ellipse([cabeza_center[0]-cabeza_radius, cabeza_center[1]-cabeza_radius,
cabeza_center[0]+cabeza_radius, cabeza_center[1]+cabeza_radius],
fill='black')
draw.polygon(cuerpo, fill='black')
draw.polygon(ala_izq, fill='black')
draw.polygon(ala_der, fill='black')
draw.polygon(cola, fill='black')
draw.line(rama, fill='black', width=8)
# Ojo (punto blanco)
draw.ellipse([cabeza_center[0]-5, cabeza_center[1]-5,
cabeza_center[0]+5, cabeza_center[1]+5],
fill='white')
return np.array(img)
# Crear máscara
mascara_paloma = crear_mascara_paloma()
# Generar nube de palabras
wordcloud = WordCloud(
width=800,
height=600,
background_color='white',
stopwords=stopwords_es,
max_words=max_palabras,
mask=mascara_paloma,
contour_width=2,
contour_color='#2E5090',
colormap='Blues',
relative_scaling=0.5,
min_font_size=8
).generate(texto_completo)
# Crear figura
fig, ax = plt.subplots(figsize=(12, 9), facecolor='white')
ax.imshow(wordcloud, interpolation='bilinear')
ax.axis('off')
ax.set_title('Nube de Palabras - Paloma de Paz\nPalabras más frecuentes en análisis ODS',
fontsize=18, color='#2E5090', fontweight='bold', pad=20)
plt.tight_layout()
# Guardar en archivo temporal
temp_dir = tempfile.gettempdir()
filepath = os.path.join(temp_dir, 'nube_palabras_paloma.png')
fig.savefig(filepath, format='png', dpi=150, bbox_inches='tight', facecolor='white')
plt.close(fig)
return filepath
def viz_23_nube_palabras_simple(df, columna_texto, max_palabras=100, forma='paloma'):
"""
Versión alternativa con forma circular/elíptica
(más simple, no requiere crear máscara compleja)
"""
from wordcloud import WordCloud
import matplotlib.pyplot as plt
import tempfile
import os
# Combinar texto
texto_completo = ' '.join(df[columna_texto].dropna().astype(str))
# Stopwords español
stopwords_es = {
'el', 'la', 'de', 'que', 'y', 'a', 'en', 'un', 'ser', 'se', 'no', 'haber',
'por', 'con', 'su', 'para', 'como', 'estar', 'tener', 'le', 'lo', 'todo',
'pero', 'más', 'hacer', 'o', 'poder', 'decir', 'este', 'ir', 'otro', 'ese',
'si', 'me', 'ya', 'ver', 'porque', 'dar', 'cuando', 'muy', 'sin', 'vez',
'mucho', 'saber', 'sobre', 'mi', 'también', 'hasta', 'año', 'dos', 'entre',
'del', 'al', 'los', 'las', 'una', 'unos', 'unas', 'donde', 'cuando', 'sus', 'según',
}
# Generar nube con forma elíptica (simula ala de paloma)
wordcloud = WordCloud(
width=800,
height=600,
background_color='white',
stopwords=stopwords_es,
max_words=max_palabras,
colormap='Blues',
relative_scaling=0.5,
min_font_size=10,
prefer_horizontal=0.7,
collocations=False
).generate(texto_completo)
# Crear figura con diseño de paloma sugerido
fig, ax = plt.subplots(figsize=(14, 8), facecolor='white')
ax.imshow(wordcloud, interpolation='bilinear')
ax.axis('off')
# Título decorativo
# ax.text(0.5, 0.98, '🕊️ Nube de Palabras - Análisis ODS',
ax.text(0.5, 0.98, ' ',
transform=ax.transAxes,
fontsize=20, color='#2E5090', fontweight='bold',
ha='center', va='top')
ax.text(0.5, 0.02, 'Palabras más frecuentes en descripciones de ODS',
transform=ax.transAxes,
fontsize=12, color='#666',
ha='center', va='bottom')
plt.tight_layout()
# Guardar
temp_dir = tempfile.gettempdir()
filepath = os.path.join(temp_dir, 'nube_palabras_ods.png')
fig.savefig(filepath, format='png', dpi=150, bbox_inches='tight', facecolor='white')
plt.close(fig)
return filepath
def viz_24_header_conecta_ods(logo_path=None):
"""
Visualización 24: Header ConectaODS
Componente HTML llamativo con logo y descripción
Args:
logo_path: Ruta al logo (opcional). Si no se provee, usa placeholder
"""
import base64
import os
# Convertir logo a base64 si existe
logo_base64 = ""
if logo_path and os.path.exists(logo_path):
try:
with open(logo_path, "rb") as img_file:
logo_base64 = base64.b64encode(img_file.read()).decode()
logo_src = f"data:image/png;base64,{logo_base64}"
except:
logo_src = ""
else:
# Placeholder SVG si no hay logo
logo_src = """data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='120' height='120' viewBox='0 0 120 120'%3E%3Ccircle cx='60' cy='60' r='55' fill='%232E5090'/%3E%3Ctext x='60' y='75' font-family='Arial' font-size='48' font-weight='bold' fill='white' text-anchor='middle'%3EODS%3C/text%3E%3C/svg%3E"""
html = f"""
<div style="
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 20px;
padding: 40px;
box-shadow: 0 10px 40px rgba(0,0,0,0.2);
margin: 20px 0;
position: relative;
overflow: hidden;
">
<!-- Decoración de fondo -->
<div style="
position: absolute;
top: -50px;
right: -50px;
width: 200px;
height: 200px;
background: rgba(255,255,255,0.1);
border-radius: 50%;
"></div>
<div style="
position: absolute;
bottom: -30px;
left: -30px;
width: 150px;
height: 150px;
background: rgba(255,255,255,0.1);
border-radius: 50%;
"></div>
<!-- Contenido principal -->
<div style="
display: flex;
align-items: center;
gap: 30px;
position: relative;
z-index: 1;
flex-wrap: wrap;
">
<!-- Logo -->
<div style="
background: white;
border-radius: 20px;
padding: 20px;
box-shadow: 0 5px 20px rgba(0,0,0,0.15);
flex-shrink: 0;
">
<img src="{logo_src}"
alt="ConectaODS Logo"
style="
width: 120px;
height: 120px;
display: block;
object-fit: contain;
">
</div>
<!-- Contenido de texto -->
<div style="flex: 1; min-width: 300px;">
<!-- Título principal -->
<h1 style="
color: white;
font-size: 42px;
font-weight: 900;
margin: 0 0 15px 0;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
letter-spacing: -1px;
">
ConectaODS
<span style="
display: inline-block;
background: rgba(255,255,255,0.2);
padding: 5px 15px;
border-radius: 20px;
font-size: 18px;
font-weight: 600;
margin-left: 10px;
vertical-align: middle;
">v1.0</span>
</h1>
<!-- Subtítulo -->
<p style="
color: rgba(255,255,255,0.95);
font-size: 22px;
font-weight: 600;
margin: 0 0 20px 0;
font-style: italic;
">
Tu voz en clave de desarrollo sostenible
</p>
<!-- Descripción -->
<p style="
color: rgba(255,255,255,0.9);
font-size: 16px;
line-height: 1.6;
margin: 0;
max-width: 600px;
">
ConectaODS es una herramienta que convierte
<strong style="color: #FFE66D;">relatos, ideas o iniciativas del territorio</strong>
en conexiones claras con los
<strong style="color: #4ECDC4;">Objetivos de Desarrollo Sostenible (ODS)</strong>
</p>
<!-- Badges informativos -->
<div style="
display: flex;
gap: 10px;
margin-top: 20px;
flex-wrap: wrap;
">
<span style="
background: rgba(255,255,255,0.25);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 6px;
">
<span style="font-size: 16px;">🎯</span> 17 ODS
</span>
<span style="
background: rgba(255,255,255,0.25);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 6px;
">
<span style="font-size: 16px;">🎯</span> 169 Metas
</span>
<span style="
background: rgba(255,255,255,0.25);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 6px;
">
<span style="font-size: 16px;">📊</span> 244+ Indicadores
</span>
<span style="
background: rgba(255,230,109,0.3);
color: white;
padding: 8px 16px;
border-radius: 20px;
font-size: 13px;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 6px;
">
<span style="font-size: 16px;">🤖</span> IA + NLP
</span>
</div>
</div>
</div>
<!-- Barra decorativa inferior -->
<div style="
margin-top: 30px;
padding-top: 20px;
border-top: 2px solid rgba(255,255,255,0.2);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 15px;
">
<div style="color: rgba(255,255,255,0.8); font-size: 13px;">
<strong>Desarrollado por:</strong> UNFPA Colombia & Gobierno de Colombia
</div>
<div style="
display: flex;
gap: 10px;
">
<span style="
background: rgba(255,255,255,0.15);
color: white;
padding: 6px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
">
🌍 Análisis Territorial
</span>
<span style="
background: rgba(255,255,255,0.15);
color: white;
padding: 6px 12px;
border-radius: 12px;
font-size: 12px;
font-weight: 600;
">
📈 Visualización Interactiva
</span>
</div>
</div>
</div>
<!-- CSS para animación hover (opcional) -->
<style>
@keyframes float {{
0%, 100% {{ transform: translateY(0px); }}
50% {{ transform: translateY(-10px); }}
}}
div[style*="border-radius: 20px"] img:hover {{
animation: float 2s ease-in-out infinite;
}}
</style>
"""
return html
# ============================================================================
# 12. FUNCIÓN PRINCIPAL - GENERAR TODAS LAS VISUALIZACIONES
# ============================================================================
def generar_todas_visualizaciones(ruta_archivo, guardar=True, formato='html'):
"""
Función principal que genera todas las visualizaciones.
Parámetros:
-----------
ruta_archivo : str
Ruta al archivo markdown con los datos
guardar : bool
Si True, guarda las visualizaciones en archivos
formato : str
Formato de salida: 'html' para interactivas, 'png' para estáticas
Retorna:
--------
dict : Diccionario con todas las figuras generadas
"""
print("Cargando datos...")
df = cargar_datos(ruta_archivo)
print(f"Datos cargados: {len(df)} registros, {df[id_lvl].nunique()} ODS únicos")
figuras = {}
print("\n" + "="*70)
print("GENERANDO VISUALIZACIONES")
print("="*70)
# Visualización 1
print("\n[1/10] Generando distribución por ODS (Box Plot)...")
figuras['viz1_boxplot'] = viz_1_distribucion_por_ods(df)
if guardar:
figuras['viz1_boxplot'].write_html('viz1_boxplot_ods.html')
# Visualización 2
print("[2/10] Generando heatmap ODS vs Ranking...")
figuras['viz2_heatmap'] = viz_2_heatmap_ods_ranking(df)
if guardar:
figuras['viz2_heatmap'].savefig('viz2_heatmap.png', dpi=300, bbox_inches='tight')
plt.close()
# Visualización 3
print("[3/10] Generando scatter 3D interactivo...")
figuras['viz3_scatter3d'] = viz_3_scatter_3d_interactivo(df)
if guardar:
figuras['viz3_scatter3d'].write_html('viz3_scatter3d.html')
# Visualización 4
print("[4/10] Generando radar chart por ODS...")
figuras['viz4_radar'] = viz_4_radar_chart_ods(df)
if guardar:
figuras['viz4_radar'].write_html('viz4_radar_ods.html')
# Visualización 5
print("[5/10] Generando sunburst jerárquico...")
figuras['viz5_sunburst'] = viz_5_sunburst_jerarquia(df)
if guardar:
figuras['viz5_sunburst'].write_html('viz5_sunburst.html')
# Visualización 6
print("[6/10] Generando top indicadores por ODS...")
figuras['viz6_topn'] = viz_6_top_indicadores_por_ods(df, top_n=5)
if guardar:
figuras['viz6_topn'].write_html('viz6_top_indicadores.html')
# Visualización 7
print("[7/10] Generando stream graph...")
figuras['viz7_stream'] = viz_7_streamgraph_similaridad(df)
if guardar:
figuras['viz7_stream'].write_html('viz7_streamgraph.html')
# Visualización 8
print("[8/10] Generando violin plot...")
figuras['viz8_violin'] = viz_8_violin_plot_ods(df)
if guardar:
figuras['viz8_violin'].write_html('viz8_violin_plot.html')
# Visualización 9
print("[9/10] Generando dashboard integrado...")
figuras['viz9_dashboard'] = viz_9_dashboard_metricas(df)
if guardar:
figuras['viz9_dashboard'].write_html('viz9_dashboard.html')
# Visualización 10
print("[10/10] Generando matriz de transición...")
figuras['viz10_matriz'] = viz_10_matriz_transicion(df)
if guardar:
figuras['viz10_matriz'].savefig('viz10_matriz_transicion.png', dpi=300, bbox_inches='tight')
plt.close()
print("\n" + "="*70)
print("GENERACIÓN COMPLETADA")
print("="*70)
print(f"\nTotal de visualizaciones generadas: {len(figuras)}")
if guardar:
print("\nArchivos guardados:")
print(" - Visualizaciones interactivas (HTML): 8 archivos")
print(" - Visualizaciones estáticas (PNG): 2 archivos")
return figuras, df
# ============================================================================
# 13. ANÁLISIS ESTADÍSTICO COMPLEMENTARIO
# ============================================================================
def analisis_estadistico(df):
"""
Genera estadísticas descriptivas complementarias para el análisis
"""
print("\n" + "="*70)
print("ANÁLISIS ESTADÍSTICO COMPLEMENTARIO")
print("="*70)
print("\n1. ESTADÍSTICAS GLOBALES")
print("-" * 70)
print(f" Similaridad media: {df[score].mean():.4f}")
print(f" Desviación estándar: {df[score].std():.4f}")
print(f" Similaridad mínima: {df[score].min():.4f}")
print(f" Similaridad máxima: {df[score].max():.4f}")
print(f" Mediana: {df[score].median():.4f}")
print("\n2. ESTADÍSTICAS POR ODS")
print("-" * 70)
stats_ods = df.groupby(id_lvl)[score].agg([
('count', 'count'),
('mean', 'mean'),
('std', 'std'),
('min', 'min'),
('max', 'max')
]).round(4)
print(stats_ods.to_string())
print("\n3. ODS MÁS REPRESENTADOS EN TOP 50")
print("-" * 70)
top_50_ods = df.nsmallest(50, rank)[id_lvl].value_counts()
print(top_50_ods.to_string())
print("\n4. CORRELACIÓN RANK vs SIMILARIDAD")
print("-" * 70)
correlacion = df[rank].corr(df[score])
print(f" Correlación de Pearson: {correlacion:.4f}")
print(f" Interpretación: {'Negativa fuerte' if correlacion < -0.7 else 'Negativa moderada' if correlacion < -0.4 else 'Negativa débil'}")
print(f" (Esperado: correlación negativa, a mayor rank → menor similaridad)")
return stats_ods
# ============================================================================
# EJECUCIÓN DEL SCRIPT
# ============================================================================
if __name__ == "__main__":
# Configurar ruta del archivo
RUTA_ARCHIVO = '/mnt/user-data/uploads/indicadores_markdown.txt'
print("\n" + "="*70)
print("SISTEMA DE VISUALIZACIÓN - ANÁLISIS DE SIMILARIDAD ODS")
print("="*70)
print("\nEste script genera 10 visualizaciones avanzadas para analizar")
print("la similaridad coseno como proxy de relevancia entre una iniciativa")
print("ciudadana y los indicadores ODS.")
# Generar todas las visualizaciones
figuras, df = generar_todas_visualizaciones(
RUTA_ARCHIVO,
guardar=True,
formato='html'
)
# Análisis estadístico
stats = analisis_estadistico(df)
print("\n" + "="*70)
print("RECOMENDACIONES DE USO")
print("="*70)
print("""
1. Use el Dashboard (viz9) como punto de partida para exploración general
2. Use el Heatmap (viz2) para identificar patrones temporales de relevancia
3. Use el Radar Chart (viz4) para comunicar el perfil ODS de la iniciativa
4. Use el Scatter 3D (viz3) para exploración detallada e interactiva
5. Use el Violin Plot (viz8) para análisis estadístico profundo
6. Use el Sunburst (viz5) para presentaciones ejecutivas
7. Use la Matriz de Transición (viz10) para análisis de consistencia
NOTA: Los archivos HTML son interactivos - ábralos en un navegador
""")
print("\n¡Proceso completado exitosamente!")