← Volver ao Perfil

Análisis de Red de Co-ocorrencia de Terminos

Este script de Python utiliza las bibliotecas Pandas, NetworkX y Matplotlib para construir, analizar y visualizar una red de coocurrencia de términos a partir de un archivo de texto. El proceso incluye el preprocesamiento del texto en español, el cálculo de métricas de red (PageRank, clústeres) y un flujo interactivo para corregir la acentuación antes de generar el gráfico final.

Galería de Gráficos

Diferentes visualizaciones generadas por el script, mostrando layouts alternativos (Circular vs. Kamada-Kawai) y estilos de color (Monocromático vs. Clusters).

Visita el Repositorio del Proyecto

Para descargar el script completo, ver la documentación `README.md` y explorar el código fuente, visita el repositorio oficial en GitHub.

Ir a GitHub →

Script Python Completo


import pandas as pd
import networkx as nx
import re
from itertools import combinations
import community as community_louvain
import matplotlib.pyplot as plt
import unicodedata # Para eliminar tildes
import numpy as np # Para normalizar

# --- Función para eliminar tildes ---
def _eliminar_tildes(texto):
    nfkd_form = unicodedata.normalize('NFD', texto)
    return "".join([c for c in nfkd_form if unicodedata.category(c) != 'Mn'])

# --- Función de Preprocesamiento (Español) ---
def preprocess_text(text, custom_stopwords=None):
    mapa_normalizacao = {
        'investigaciones': 'investigacion',
        'docentes': 'docente',
        'estudiantes': 'estudiante',
        'sociales': 'social',
        'educacionales': 'educacional',
        'nacionales': 'nacional',
        'regionales': 'regional',
        'locales': 'local',
        'institucionales': 'institucional',
        'profesionales': 'profesional',
        'populares': 'popular',
        'municipales': 'municipal',
        'politicas': 'politica', # 'políticas' se vuelve 'politica'
        'acciones': 'accion',    # 'acción' se vuelve 'accion'
        'redes': 'red',
        'universidades': 'universidad'
    }
    stop_words = set(['de', 'a', 'o', 'que', 'y', 'e', 'el', 'la', 'en', 'un', 'una', 'para',
        'con', 'no', 'los', 'las', 'por', 'mas', 'más', 'se', 'su', 'sus',
        'como', 'pero', 'al', 'del', 'le', 'lo', 'me', 'mi', 'sin', 'son',
        'tambien', 'también', 'este', 'esta', 'estos', 'estas', 'ser', 'es'])
    
    if custom_stopwords:
        # Normalizar también las stopwords personalizadas
        normalized_custom = [_eliminar_tildes(sw.lower()) for sw in custom_stopwords]
        stop_words.update(normalized_custom)

    text = text.lower()
    text = _eliminar_tildes(text) # <-- PASO CLAVE
    text = re.sub(r'[^\w\s]', '', text)
    text = re.sub(r'\d+', '', text)
    tokens = text.split()
    normalized_tokens = [mapa_normalizacao.get(token, token) for token in tokens]
    filtered_tokens = [word for word in normalized_tokens if word not in stop_words and len(word) > 2]
    return filtered_tokens

# --- Función de Creación de Matriz ---
def create_cooccurrence_matrix_from_file(filepath):
    with open(filepath, 'r', encoding='utf-8') as file:
        content = file.read()
    documents_raw = content.split('###')
    documents_raw = [doc.strip() for doc in documents_raw if doc.strip()]
    
    # Esta lista ahora se pasa desde el bloque principal, pero la dejamos aquí como ejemplo
    # custom_stopwords = ['programa', 'educacion', 'pdi', 'art', 'articulo'] 
    
    # Pasamos una lista vacía de stopwords personalizadas, ya que se definen en el bloque principal
    processed_docs = [preprocess_text(doc, []) for doc in documents_raw]
    
    vocabulary = sorted(list(set(term for doc in processed_docs for term in doc)))
    M = pd.DataFrame(0, index=vocabulary, columns=vocabulary, dtype=int)
    
    for doc in processed_docs:
        unique_terms_in_doc = sorted(list(set(doc)))
        for term in unique_terms_in_doc:
            M.loc[term, term] += 1
        for term1, term2 in combinations(unique_terms_in_doc, 2):
            M.loc[term1, term2] += 1
            M.loc[term2, term1] += 1
    return M

# --- Función de Métricas ---
def calcular_e_associar_metricas(G, M):
    partition = community_louvain.best_partition(G, weight='weight')
    pagerank = nx.pagerank(G, weight='weight')
    occurrences = {term: M.loc[term, term] for term in G.nodes()}
    clusters_ajustados = {node: cluster_id + 1 for node, cluster_id in partition.items()}
    nx.set_node_attributes(G, clusters_ajustados, 'cluster')
    nx.set_node_attributes(G, pagerank, 'pagerank')
    nx.set_node_attributes(G, occurrences, 'occurrences')
    print("Métricas (Cluster, PageRank, Ocorrencias) calculadas y asociadas a los nodos.")
    return G

# --- Función de Filtrado ---
def filtrar_rede(G, top_n, min_edge_weight_for_viz):
    if G.number_of_nodes() <= top_n: # <-- HTML escape para <
        top_nodes = list(G.nodes())
    else:
        pagerank_dict = nx.get_node_attributes(G, 'pagerank')
        sorted_nodes = sorted(pagerank_dict, key=pagerank_dict.get, reverse=True)
        top_nodes = sorted_nodes[:top_n]
        
    G_sub = G.subgraph(top_nodes).copy()
    G_final = nx.Graph()
    G_final.add_nodes_from(G_sub.nodes(data=True))
    
    for u, v, data in G_sub.edges(data=True):
        if data['weight'] >= min_edge_weight_for_viz: # <-- HTML escape para >
            G_final.add_edge(u, v, weight=data['weight'])
            
    G_final.remove_nodes_from(list(nx.isolates(G_final)))
    print(f"Red final (Top {top_n} nodos, Bordes >= {min_edge_weight_for_viz}): {G_final.number_of_nodes()} nodos, {G_final.number_of_edges()} bordes.")
    return G_final

# --- Función de Visualización (Monocromática) ---
def visualizar_rede(G, title, output_filename):
    if G.number_of_nodes() == 0:
        print("La red está vacía. No es posible generar el gráfico.")
        return

    plt.figure(figsize=(16, 9))
    pos = nx.kamada_kawai_layout(G)

    pagerank_values = [data.get('pagerank', 0) for _, data in G.nodes(data=True)]

    # Lógica de Tamaño
    min_size = 1500
    max_size = 16000
    node_sizes = []
    if pagerank_values:
        min_pr = min(pagerank_values)
        max_pr = max(pagerank_values)
        if max_pr == min_pr:
            node_sizes = [min_size] * G.number_of_nodes()
        else:
            node_sizes = [
                min_size + ((p - min_pr) / (max_pr - min_pr)) * (max_size - min_size) 
                for p in pagerank_values
            ]
    else:
        node_sizes = [min_size] * G.number_of_nodes()

    # Lógica de Color (Monocromático)
    try:
        cmap_color = plt.colormaps.get_cmap('Blues_r')
    except AttributeError:
        cmap_color = plt.cm.get_cmap('Blues_r')
    
    # Lógica de Grosor de Línea
    edge_weights = [data['weight'] for u, v, data in G.edges(data=True)]
    base_width = 1.0
    max_extra_width = 6.0
    scaled_widths = []
    if edge_weights:
        min_w = min(edge_weights)
        max_w = max(edge_weights)
        if max_w == min_w:
            scaled_widths = [base_width] * len(edge_weights)
        else:
            scaled_widths = [
                base_width + (((w - min_w) / (max_w - min_w)) * max_extra_width) 
                for w in edge_weights
            ]
    else:
        scaled_widths = [base_width] * len(edge_weights)

    # Lógica de Etiquetas
    custom_labels = {
        node: f"{node.capitalize()}\n({data.get('occurrences', '?')})"
        for node, data in G.nodes(data=True)
    }

    # Dibujar Nodos
    nx.draw_networkx_nodes(
        G, pos,
        node_color=pagerank_values,
        cmap=cmap_color,
        node_size=node_sizes,
        alpha=0.8,
        edgecolors='black',
        linewidths=1.5
    )

    # Dibujar Aristas
    nx.draw_networkx_edges(
        G, pos, 
        alpha=0.3, 
        width=scaled_widths,
        edge_color='grey'
    )
    
    # Dibujar Etiquetas
    nx.draw_networkx_labels(G, pos, labels=custom_labels, font_size=12, font_color='black', font_weight='bold')

    plt.title(title, size=20)
    plt.tight_layout()
    plt.savefig(output_filename, dpi=300, bbox_inches='tight')
    plt.close()
    print(f"\nGráfico '{output_filename}' guardado con éxito! (Monocromático y grosor dinámico)")


## ----------------------------------------------------------------
## BLOQUE DE EJECUCIÓN PRINCIPAL (CON PASO DE EDICIÓN)
## ----------------------------------------------------------------
if __name__ == "__main__":

    # --- Parámetros de Entrada ---
    FILE_PATH = 'extractos.txt'
    TOP_N_NODES = 20
    MIN_EDGE_WEIGHT_VIZ = 5 
    GRAFICO_TITULO = "Análisis de Red (Monocromático)"
    GRAFICO_OUTPUT_FILE = "analisis_red_monocromatica.png"
    MAPEO_ETIQUETAS_FILE = 'mapeo_terminos.csv'

    # --- 1. Flujo de trabajo estándar (hasta el filtrado) ---
    print("Iniciando análisis...")
    matriz_M = create_cooccurrence_matrix_from_file(FILE_PATH)
    grafo_base = nx.from_pandas_adjacency(matriz_M)
    grafo_com_metricas = calcular_e_associar_metricas(grafo_base, matriz_M)
    grafo_final = filtrar_rede(grafo_com_metricas, top_n=TOP_N_NODES, min_edge_weight_for_viz=MIN_EDGE_WEIGHT_VIZ)

    # --- 2. Exportar nodos para corrección de tildes ---
    nodos_sin_tildes = list(grafo_final.nodes())
    df_mapa = pd.DataFrame({
        'original_sin_tilde': nodos_sin_tildes,
        'corregido_con_tilde': nodos_sin_tildes
    })
    df_mapa.to_csv(MAPEO_ETIQUETAS_FILE, index=False, encoding='utf-8-sig')

    # --- 3. Pausa para la edición manual ---
    print("-" * 70)
    print(f"ARCHIVO CREADO: '{MAPEO_ETIQUETAS_FILE}'")
    print("Por favor, abre este archivo CSV (con Excel, Google Sheets, o un editor de texto).")
    print("Modifica la columna 'corregido_con_tilde' para añadir las tildes necesarias.")
    print("-" * 70)
    input(">>> PRESIONA ENTER para continuar después de guardar tus cambios... ")

    # --- 4. Importar mapa corregido y re-etiquetar ---
    print("Leyendo el archivo de etiquetas corregido...")
    try:
        df_mapa_editado = pd.read_csv(MAPEO_ETIQUETAS_FILE, encoding='utf-8-sig')
    except Exception as e:
        print(f"Error leyendo el archivo {MAPEO_ETIQUETAS_FILE}: {e}")
        print("Asegúrate de que el archivo esté guardado correctamente.")
        # exit() # Comentado para que no detenga el script si se usa en un entorno no interactivo

    mapa_de_etiquetas = pd.Series(
        df_mapa_editado.corregido_con_tilde.values,
        index=df_mapa_editado.original_sin_tilde
    ).to_dict()

    grafo_etiquetado = nx.relabel_nodes(grafo_final, mapa_de_etiquetas, copy=True)
    print("Nodos re-etiquetados con éxito.")

    # --- 5. Visualización ---
    visualizar_rede(
        grafo_etiquetado,
        GRAFICO_TITULO, 
        GRAFICO_OUTPUT_FILE
    )