Spaces:
Sleeping
Sleeping
| """ | |
| 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!") | |