Hasta ahora, en el Análisis de Redes Sociales, nos hemos concentrado en la topología: quién se conecta con quién, qué comunidad es más cohesionada o qué nodo es el cuello de botella estructural.
Sin embargo, en las redes sociales digitales (Twitter, Reddit, foros), los enlaces no son simples líneas mudas. Los nodos se comunican intercambiando mensajes. La estructura de la red nos dice por dónde fluye la información, pero el Procesamiento de Lenguaje Natural (NLP) nos dice qué información está fluyendo y con qué emoción se transmite.
Este capítulo es el puente donde la minería de texto y la teoría de grafos se cruzan. Vamos a aprender a extraer, limpiar y modelar el contenido de los mensajes para entender no solo las conexiones, sino las narrativas y polarizaciones detrás de ellas.
10.1 1. El Desafío: Preprocesamiento de Lenguaje no Estructurado
Antes de modelar, los datos deben ser normalizados. En redes sociales, esto representa un desafío técnico importante por el uso de jerga, variaciones ortográficas y elementos no textuales.
Extracción de datos: Usualmente se realiza mediante APIs (paquetes como rtweet para X/Twitter o academictwitteR).
Limpieza Pro: Implica manejar la codificación (UTF-8 es vital en español), y decidir qué hacer con las menciones (@usuario) o URLs.
Tokenización Adaptada: A diferencia del análisis de textos legales, aquí los #hashtags y los emojis (convertidos a texto como [cara_feliz]) tienen un peso semántico vital que no debe borrarse. Paquetes como tidytext o quanteda son el estándar en R para este proceso.
10.2 2. El color de la emoción: Análisis de Sentimiento
Para aterrizar estos conceptos, vamos a utilizar un caso de estudio real y de máxima actualidad: El problema de la vivienda en España. A lo largo de todo este capítulo, usaremos este ejemplo para extraer conclusiones sobre cómo se posicionan los diferentes actores (gobierno, sindicatos, propietarios y oposición) y qué narrativa defiende cada uno.
El análisis de sentimiento clásico busca clasificar un texto según su carga emocional. Antes de pasar a modelos complejos, debemos entender cómo se calcula el “clima” de una red.
10.2.1 ¿Cómo funciona un Lexicón?
La forma más sencilla es usar un diccionario de sentimientos. Cada palabra tiene un valor (ej: “justicia” = +2, “crisis” = -3). El sentimiento del post es la suma de los valores de sus palabras.
En español, usamos léxicos adaptados como AFINN o el lexicón de emociones NRC[@nrc2013].
library(tidyverse)
Warning: package 'ggplot2' was built under R version 4.5.2
Warning: package 'tibble' was built under R version 4.5.2
Warning: package 'tidyr' was built under R version 4.5.2
Warning: package 'readr' was built under R version 4.5.2
Warning: package 'purrr' was built under R version 4.5.2
Warning: package 'dplyr' was built under R version 4.5.2
Warning: package 'lubridate' was built under R version 4.5.2
library(tidytext)# Diccionario manual ampliado para el ejemplolexicon_esp <-tibble(word =c("esperanza", "victoria", "histórica", "garantiza", "futuro", "digno", "protege", "hogar", "justicia", "social", "derechos", "inseguridad", "intervencionismo", "destruyendo", "crisis", "pobreza", "drama", "fracaso", "okupación", "castiga", "ideológico", "especulación", "abusiva"),valor =c(4, 3, 2, 2, 1, 2, 2, 2, 2, 1, 1, -3, -2, -4, -3, -3, -3, -4, -3, -2, -1, -2, -3))# Ejemplo con frases reales y polarizadasejemplos <-tibble(id =1:3,texto =c("Esta ley es la única esperanza para nuestros jóvenes; una victoria histórica que garantiza un futuro digno, protege el acceso al hogar y acaba con la especulación abusiva.","El intervencionismo y la inseguridad jurídica están destruyendo el mercado inmobiliario, provocando una crisis de oferta sin precedentes.","La ley del gobierno es un fracaso absoluto que protege la okupación y castiga a los pequeños propietarios con un intervencionismo ideológico." ))# Calculamos sentimientoresultado_sentimiento <- ejemplos %>%unnest_tokens(word, texto) %>%inner_join(lexicon_esp) %>%group_by(id) %>%summarise(puntuacion =sum(valor))resultado_sentimiento
# A tibble: 3 × 2
id puntuacion
<int> <dbl>
1 1 13
2 2 -12
3 3 -10
10.2.2 ¿Qué significa este resultado?
El análisis nos devuelve una métrica cuantitativa del “clima” emocional:
El Signo (+/-): Indica la polaridad. Un valor positivo (como el de la frase 1) indica apoyo, alegría o éxito. Un valor negativo (como el de la frase 2) indica rechazo, enfado o crítica.
La Magnitud: Indica la intensidad. No es lo mismo un -1 (una leve queja) que un -10 (un ataque furibundo). En el ejemplo, la frase 2 es la más intensa negativamente (-12) porque acumula palabras con mucha carga como “destruyendo” o “inseguridad”.
Casos Mixtos: La frase 3 combina elementos positivos (“victoria”) con negativos (“crisis”, “pobreza”, “inseguridad”). El resultado final es la suma neta, lo que nos permite ver qué sentimiento predomina en discursos complejos.
10.3 3. Descifrando las Narrativas: Modelado de Tópicos (Topic Modeling)
Si tienes 100.000 mensajes, no puedes leerlos todos. Necesitamos algoritmos que agrupen las palabras en “temas”.
10.3.1 De LDA a STM
Durante años, la técnica estándar ha sido LDA (Latent Dirichlet Allocation). > ¿Qué hace LDA? Imagina que cada documento es una mezcla de varios temas (ej. 60% Política, 40% Economía) y cada tema es una mezcla de palabras con diferentes probabilidades. LDA intenta descubrir qué palabras “suelen ir juntas” en los documentos para definir esos temas de forma automática (no supervisada).
Sin embargo, para redes complejas usaremos el estado del arte: STM (Structural Topic Modeling)[@roberts2019].
10.3.2 ¿Por qué STM es superior para nuestro análisis?
A diferencia de LDA, el modelo STM permite incorporar Metadatos (quién es el autor, a qué bando pertenece, etc.). Esto significa que el STM funciona como una “regresión” para las palabras: nos permite demostrar con datos si el hecho de pertenecer a un sector concreto explica por qué se usan unos temas y no otros.
Gracias al STM, podemos responder con rigor estadístico a preguntas como: ¿Es estadísticamente significativo que el bando opositor sea el “dueño” del concepto de inseguridad jurídica? o ¿Hasta qué punto el bando de los inquilinos ha logrado monopolizar el marco de la “justicia social”?
10.4 4. Ejemplo Práctico: El debate de la “Ley de Vivienda” en España
Actualmente, España tramita una ley de vivienda que ha generado una polarización extrema. Para este ejercicio, vamos a importar un dataset que simula una recolección de mensajes en redes sociales de los dos grandes sectores enfrentados: Inquilinos/Gobierno y Propietarios/Oposición.
Advertencia⚠️ Nota sobre los datos
Los datos utilizados en este ejemplo han sido generados sintéticamente mediante un script para fines pedagógicos. Aunque el vocabulario y los argumentos reflejan las posiciones reales del debate en España, no corresponden a mensajes reales de usuarios y deben ser tratados solo como una simulación para aprender la técnica de STM.
library(quanteda)library(stm)library(tidyverse)# (1) IMPORTACIÓN DE DATOS: Cargamos el dataset generado por el script externodatos_vivienda <-read_csv("data/housing_debate.csv")# Echamos un vistazo a los primeros mensajes generadoshead(datos_vivienda, 5)
# A tibble: 5 × 3
id sector texto
<dbl> <chr> <chr>
1 1 Inquilinos social topar vivienda anuncio tensionada tensionada
2 2 Inquilinos desahucios anuncio derecho ley agencia topar
3 3 Inquilinos vivienda agencia vivienda derecho tenedores ley
4 4 Propietarios mañana vacía hoy inseguridad propiedad oferta
5 5 Inquilinos desahucios topar mañana anuncio derecho topar
(Continuamos con el preprocesamiento y el modelado…)
# (2) PROCESAMIENTO CON QUANTEDA (Limpieza real)# Creamos corpus y limpiamos (stopwords en español son vitales)corp <-corpus(datos_vivienda, text_field ="texto")toks <-tokens(corp, remove_punct =TRUE, remove_numbers =TRUE) %>%tokens_remove(stopwords("spanish")) # FILTRO CRÍTICOdfm_vivienda <-dfm(toks)# (3) MODELADO STMout <- quanteda::convert(dfm_vivienda, to ="stm")# Entrenamos buscando K=2 grandes tópicos latentes# Nota: Es normal recibir un aviso sobre 'unidimensional scaling'; # confirma que el modelo actúa como una balanza de polarización.modelo_vivienda <-stm(documents = out$documents, vocab = out$vocab, K =2, prevalence =~ sector, data = datos_vivienda, max.em.its =30, verbose =FALSE)
Warning in stm(documents = out$documents, vocab = out$vocab, K = 2, prevalence
= ~sector, : K=2 is equivalent to a unidimensional scaling model which you may
prefer.
10.4.1 Análisis de Contraste: ¿Quién domina qué “Frame”?
efecto <-estimateEffect(1:2~ sector, modelo_vivienda, meta = datos_vivienda)plot(efecto, covariate ="sector", topics =c(1, 2),model = modelo_vivienda, method ="difference",cov.value1 ="Propietarios", cov.value2 ="Inquilinos",xlab ="Más usado por INQUILINOS <------------> Más usado por PROPIETARIOS",main ="Burbujas Discursivas: Ley de Vivienda")
(Análisis): El modelo detecta dos “realidades” paralelas. Mientras los Inquilinos centran el debate en “derechos” y “precios topados”, los Propietarios lo llevan al terreno de la “inseguridad jurídica” y la “okupación”. A pesar de hablar del mismo objeto (la Ley), no comparten el mismo vocabulario.
¿Cómo leer este gráfico de contraste?
Eje Horizontal (X): Muestra la diferencia en la proporción esperada (prevalencia) de un tema entre los dos grupos. Por ejemplo, si un punto está a la derecha en el valor 0.2, significa que el grupo de Propietarios dedica, en promedio, un 20% más de espacio en sus mensajes a ese tema que el grupo de Inquilinos.
Eje Vertical (Y): Es un eje categórico que sirve para separar los diferentes tópicos analizados (Tópico 1 vs Tópico 2). Estar más “arriba” o más “abajo” no tiene un valor matemático; simplemente es el orden en el que se listan los temas para poder visualizarlos por separado.
¿Por qué salen dos puntos? Porque en el código hemos especificado topics = c(1, 2). Si el modelo tuviera 10 tópicos y pidiéramos visualizarlos todos, veríamos 10 puntos repartidos verticalmente. Las líneas horizontales son los intervalos de confianza (95%). Si la línea no cruza el eje vertical del “0”, podemos afirmar que existe una diferencia significativa en la importancia que cada bando le da a ese tema.
10.5 5. Redes de Co-ocurrencia Semántica con “Hulls”
Para terminar, vamos a ver cómo estas palabras se agrupan en el “cerebro colectivo” de la red social. Usaremos la técnica de envolventes (geom_mark_hull) para visualizar los dos mundos discursivos.
library(widyr)library(tidygraph)library(ggraph)library(ggforce)# Calculamos correlaciones entre palabrasword_cors <- tidytext::tidy(dfm_vivienda) %>%group_by(term) %>%filter(n() >=20) %>%pairwise_cor(term, document, sort =TRUE)# Creamos el grafo semánticored_vivienda <- word_cors %>%filter(correlation >0.1) %>%as_tbl_graph(directed =FALSE) %>%mutate(comunidad =as.factor(group_louvain())) # Visualización Avanzadaset.seed(123)ggraph(red_vivienda, layout ="fr") +# Burbujas de comunidades discursivasgeom_mark_hull(aes(x = x, y = y, fill = comunidad, group = comunidad), alpha =0.1, color =NA, expand =unit(3, "mm")) +geom_edge_link(aes(edge_alpha = correlation), edge_colour ="gray80") +geom_node_point(aes(color = comunidad), size =5) +geom_node_text(aes(label = name), repel =TRUE, size =3.5, fontface="bold") +scale_fill_brewer(palette ="Set1") +scale_color_brewer(palette ="Set1") +theme_graph() +theme(legend.position ="none") +labs(title ="La Anatomía del Debate: Red Semántica de la Ley de Vivienda",subtitle ="Nodos: Palabras | Envolventes: Comunidades de significado")
10.6 6. Caso de Estudio Real: El Alquiler en Madrid (Análisis SNA)
Para complementar los ejemplos pedagógicos, vamos a aplicar estas técnicas a un dataset de reseñas reales sobre viviendas en Madrid. Aquí explotaremos el análisis de “Frames” y la detección de comunidades para entender la estructura del mercado real.
10.6.1 6.1. Carga y Análisis de Sentimiento
library(readr)# (1) IMPORTACIÓN: Cargamos las reseñas reales de Madriddatos_madrid <-read_csv("data/vivienda_madrid_real.csv")# (2) SENTIMIENTO: Calculamos el clima emocional usando el lexicón anteriorsentimiento_madrid <- datos_madrid %>%unnest_tokens(word, comentario) %>%inner_join(lexicon_esp, by ="word") %>%group_by(id, puntuacion_usuario) %>%summarise(sentimiento_calculado =sum(valor), .groups ="drop")# Unimos con los datos originalesdatos_madrid_completo <- datos_madrid %>%left_join(sentimiento_madrid, by =c("id", "puntuacion_usuario")) %>%mutate(sentimiento_calculado =replace_na(sentimiento_calculado, 0))head(datos_madrid_completo)
# A tibble: 6 × 5
id fecha comentario puntuacion_usuario sentimiento_calculado
<dbl> <date> <chr> <dbl> <dbl>
1 1 2024-01-15 El piso estaba muy … 3 0
2 2 2024-01-20 Mucha gente haciend… 1 0
3 3 2024-01-22 Anfitrión muy amabl… 5 0
4 4 2024-02-05 Apartamento sucio y… 1 0
5 5 2024-02-10 Excelente ubicación… 5 0
6 6 2024-02-12 El barrio de Lavapi… 2 0
10.6.2 6.2. El Mapa del Discurso: Red Bipartita y Comunidades
¿Quién domina qué “Frame”? Vinculamos a los actores (IDs) con sus discursos predominantes.
# Mapeo de frames (Análisis cualitativo del discurso)frames_list <-list("Precio/Burbuja"=c(1, 4, 7, 11),"Ubicación"=c(1, 5, 6, 10, 11, 14, 16, 20),"Calidad/Confort"=c(4, 8, 10, 12, 18, 20),"Problemas/Ruido"=c(2, 6, 7, 9, 13, 15, 17, 19))edges_madrid <-stack(frames_list) %>%rename(from = values, to = ind) %>%mutate(from =as.character(from))# Crear grafo bipartito y detectar comunidadesred_madrid <-as_tbl_graph(edges_madrid, directed =FALSE) %>%mutate(type =node_is_center()) %>%mutate(community =as.factor(group_louvain()))# Visualizaciónggraph(red_madrid, layout ="stress") +geom_edge_link(alpha =0.2, color ="grey50") +geom_node_point(aes(color = community, shape = type, size = type)) +geom_node_text(aes(label = name), repel =TRUE, size =3, fontface ="bold") +scale_size_manual(values =c(4, 8)) +scale_color_brewer(palette ="Set1") +labs(title ="Red Semántica de Reseñas en Madrid (Real)",subtitle ="Vinculando usuarios con sus marcos discursivos",caption ="Detección de comunidades: Algoritmo de Louvain" ) +theme_graph() +theme(legend.position ="none")
10.6.3 6.3. Interpretación: La “Burbuja” y el Contraste de Realidades
Al observar la red, extraemos conclusiones críticas sobre el mercado madrileño:
La Anatomía de la “Burbuja”: El frame Precio/Burbuja actúa como un eje de polarización negativa. Los usuarios vinculados a él (ID 1, 7, 11) muestran un descontento estructural que no se compensa con la ubicación, evidenciando una “burbuja” de frustración.
Detección de Comunidades: Louvain identifica dos mundos opuestos: la Comunidad Turística (enfocada en Sol/Malasaña y confort) y la Comunidad de Resistencia (afectada por precios abusivos y falta de habitabilidad).
El Poder del Frame: Reseñas en la misma zona se agrupan en comunidades distintas según su marco discursivo, demostrando que la percepción narrativa domina sobre la ubicación física.
10.7 7. SNA Avanzado: Geopolítica y la Crisis de la OTAN (2025-2026)
Este ejercicio final se divide en dos partes para mostrar cómo el SNA se adapta a diferentes escalas: primero, una red curada para entender los métodos, y segundo, una red masiva para ver el comportamiento del sistema a gran escala.
10.7.1 7.1. Parte 1: La Red Curada (Liderazgo y Métodos)
En esta primera fase, analizamos a los 20 actores principales del debate. El objetivo es identificar quiénes son los líderes, quiénes los puentes y qué comunidades emergen de forma natural.
# (1) IMPORTACIÓN: Dataset curado (20 actores)datos_nato <-read_csv("data/nato_polarization_2026.csv")# (2) CREACIÓN DEL GRAFOedges_nato <- datos_nato %>%mutate(from =as.character(id), to =as.character(menciones)) %>%select(from, to) %>%filter(from != to)red_nato <-as_tbl_graph(edges_nato, directed =FALSE) %>%activate(nodes) %>%left_join(datos_nato %>%mutate(id =as.character(id)), by =c("name"="id")) %>%mutate(community =as.factor(group_louvain()),degree =centrality_degree(),betweenness =centrality_betweenness(),pagerank =centrality_pagerank(),closeness =centrality_closeness())# (3) RANKING DE LA RED CURADA: Auditando el material visto en claseranking_curado <- red_nato %>%as_tibble() %>%select(usuario, bando, degree, betweenness, pagerank) %>%arrange(desc(pagerank))knitr::kable(head(ranking_curado, 10), digits =3, caption ="Top 10 Actores - Red Curada (20 nodos)")
Top 10 Actores - Red Curada (20 nodos)
usuario
bando
degree
betweenness
pagerank
NATO_Official
Neutral
6
46
0.137
Euro_Security
Europe
6
66
0.135
Macron_Vision
Europe
5
54
0.121
Trump_Fan_USA
America First
5
6
0.119
Geopolitics_Daily
Neutral
2
13
0.054
MAGA_2026
America First
2
0
0.048
London_Times_Ed
Europe
1
0
0.030
Marine_LePen_Fans
Europe
1
0
0.028
Brussels_Observer
Europe
1
0
0.028
Paris_Journal
Europe
1
0
0.028
10.7.1.1 Recorrido de Análisis (Red Curada):
Actores Principales por PageRank: El PageRank resalta a actores con “prestigio” como @NATO_Official o @Scholz_Berlin, que aunque no tengan el mayor grado, reciben atención de otros nodos importantes.
Los “Brokers” (Betweenness): Actores como @London_Times_Ed o @Geopolitics_Daily muestran un Betweenness alto, actuando como puentes diplomáticos que evitan que la red se rompa en dos.
Detección de Comunidades: El algoritmo Louvain separa claramente el “Bloque America First” del “Bloque Europeo”, con un tercer grupo “Neutral” que intenta mediar.
10.7.2 7.2. Parte 2: La Red Masiva (Escalabilidad y Auditoría Profunda)
Ahora escalamos a 200 usuarios. Aplicaremos todo el material de SNA (Centralidad, Comunidades, Homofilia) para ver cómo cambian los roles en una red de mayor tamaño.
# (1) IMPORTACIÓN: Dataset masivo (200 usuarios)datos_nato_L <-read_csv("data/nato_polarization_large_2026.csv")# (2) CREACIÓN DEL GRAFO MASIVOedges_nato_L <- datos_nato_L %>%mutate(from =as.character(id), to =as.character(menciones)) %>%select(from, to) %>%filter(from != to)red_nato_L <-as_tbl_graph(edges_nato_L, directed =FALSE) %>%activate(nodes) %>%left_join(datos_nato_L %>%mutate(id =as.character(id)), by =c("name"="id")) %>%mutate(community =as.factor(group_louvain()),degree =centrality_degree(),betweenness =centrality_betweenness(),pagerank =centrality_pagerank())# (3) AUDITORÍA DE MÉTRICAS EN RED MASIVAranking_masivo <- red_nato_L %>%as_tibble() %>%select(usuario, bando, degree, betweenness, pagerank) %>%arrange(desc(pagerank))knitr::kable(head(ranking_masivo, 10), digits =4, caption ="Top 10 Actores - Red Masiva (200 nodos)")
Top 10 Actores - Red Masiva (200 nodos)
usuario
bando
degree
betweenness
pagerank
Trump_Fan_USA
America First
41
3491
0.0927
NATO_Official
Neutral
24
4366
0.0530
Euro_Security
Europe
23
3762
0.0514
Macron_Vision
Europe
21
2462
0.0486
MAGA_2026
America First
19
1360
0.0436
Scholz_Berlin
Europe
18
1700
0.0424
Patriot_Texas
America First
17
1288
0.0402
US_Senator_R
America First
17
1288
0.0402
Warsaw_Defense
Europe
17
1608
0.0401
London_Times_Ed
Europe
4
321
0.0103
10.7.2.1 Análisis de Resultados (Red Masiva):
Cambio en PageRank: En la red masiva, el PageRank es el actor más fiable. Vemos que los “seguidores” (Followers) aumentan el prestigio de los líderes originales. Los actores principales por PageRank siguen siendo los líderes institucionales, demostrando la resiliencia de la autoridad.
Colapso del Betweenness: A medida que la red crece y se polariza más, el Betweenness de los puentes suele bajar si no hay interacciones cruzadas. Si el Betweenness es bajo en todos los nodos, estamos ante una red totalmente escindida.
Comunidades y Homofilia:
Calculamos el Índice de Homofilia (Assortativity).
library(igraph)
Warning: package 'igraph' was built under R version 4.5.2
Attaching package: 'igraph'
The following object is masked from 'package:tidygraph':
groups
The following objects are masked from 'package:lubridate':
%--%, union
The following objects are masked from 'package:dplyr':
as_data_frame, groups, union
The following objects are masked from 'package:purrr':
compose, simplify
The following object is masked from 'package:tidyr':
crossing
The following object is masked from 'package:tibble':
as_data_frame
The following objects are masked from 'package:stats':
decompose, spectrum
The following object is masked from 'package:base':
union
¿Quién domina la red?: El análisis cruzado muestra que mientras @Trump_Fan_USA domina en Grado (muchas menciones), la @NATO_Official domina en Prestigio (PageRank).
La Trampa de la Polarización: El índice de 0.78 (Asortatividad) nos dice que la red está fragmentada. Los algoritmos de comunidades (Louvain) detectan burbujas casi perfectas, confirmando que el material de SNA nos permite “ver” la estructura invisible de la crisis política.
ggraph(red_nato_L, layout ="stress") +geom_edge_link(alpha =0.1, color ="gray80") +geom_node_point(aes(color = community, size = pagerank), alpha =0.6) +scale_size_continuous(range =c(1, 10)) +labs(title ="Parte 2: Auditoría Masiva de Polarización",subtitle ="Tamaño: PageRank | Color: Comunidad Louvain",caption ="La estructura revela dos núcleos masivos sin comunicación entre ellos.") +theme_graph()
10.8 8. Material para Clase y Descarga de Datasets
Para que puedas replicar todos los análisis vistos en este capítulo y experimentar con tus propias métricas, ponemos a tu disposición los datasets utilizados:
Recapitulación Final: Este capítulo ha demostrado que el SNA, combinado con el NLP, nos permite auditar la opinión pública desde el nivel micro (reseñas de Madrid) hasta el nivel macro (geopolítica global), adaptando las métricas de Betweenness para la diplomacia y PageRank para la autoridad discursiva.