Comprender la dinámica de Twitter con R y Gephi: análisis de texto y centralidad
Publicado: 2022-07-22Este artículo amplía y profundiza el análisis presentado en la primera entrega de nuestra serie de análisis de redes sociales. Usamos el mismo conjunto de datos de Twitter y la misma red de interacción construida en el primer artículo. Sin embargo, esta vez, la idea es inferir mejor a los actores principales, identificar sus temas de discusión y entender cómo se propagan estos temas.
Centralidad de redes sociales
Para lograr nuestros objetivos, primero debemos introducir el concepto de centralidad . En ciencia de redes, la centralidad se refiere a los nodos que tienen una fuerte influencia en la red. La influencia es un concepto ambiguo; se puede entender de muchas maneras. ¿Es un nodo con muchos bordes más influyente que un nodo con menos pero más "importantes" bordes? ¿Qué constituye una ventaja importante en una red social?
Para abordar estas ambigüedades, los científicos de redes han desarrollado muchas medidas de centralidad. Aquí, discutimos cuatro medidas de uso común, aunque hay muchas más disponibles.
La licenciatura
La medida más común e intuitiva es la centralidad de grado. La idea detrás de la centralidad del grado es simple: medir la influencia por el grado del nodo. Puede tener variantes si la gráfica es dirigida; en ese caso, puede medir el grado de entrada y el de salida: el primero se conoce como puntaje central y el segundo como puntaje de autoridad.
En la primera entrega de esta serie, utilizamos el enfoque no dirigido. Esta vez, nos centramos en el enfoque de grado. Esto permite un análisis más preciso al enfatizar a los usuarios que son retuiteados por otros sobre los usuarios que simplemente retuitean con frecuencia.
Vector propio
La medida del vector propio se basa en la centralidad del grado. Cuanto más apuntan los nodos influyentes a un nodo determinado, mayor es su puntuación. Comenzamos con una matriz de adyacencia , donde las filas y columnas representan nodos, y usamos un 1 o un 0 para indicar si los nodos correspondientes de una fila y columna dadas están conectados. El cálculo principal estima los vectores propios de la matriz. El vector propio principal contendrá las medidas de centralidad que queremos, donde la posición i mantendrá la puntuación de centralidad del nodo i .
Rango de página
PageRank es la variación de la medida del vector propio en el núcleo de Google. Se desconoce el método exacto que usa Google, pero la idea general es que cada nodo comienza con una puntuación de 1, luego distribuye su puntuación en partes iguales a cada uno de sus bordes. Por ejemplo, si un nodo tiene tres bordes que se extienden desde él, "envía" un tercio de su puntuación a través de cada borde. Al mismo tiempo, el nodo se vuelve más importante por los bordes que apuntan hacia él. Esto da como resultado un sistema solucionable de N ecuaciones con N incógnitas.
intermediación
La cuarta medida, la intermediación , utiliza un enfoque muy diferente. Aquí, se dice que un nodo es influyente si está incluido en muchos caminos cortos entre otros nodos. Es decir, se encarga de comunicarse con muchos otros nodos, conectando “mundos diferentes”.
Por ejemplo, en el análisis de redes sociales, este tipo de nodos podrían entenderse como los tipos de personas que ayudan a otros a encontrar nuevos trabajos o establecer nuevas conexiones: son las puertas a círculos sociales previamente desconocidos.
¿Cuál debo usar?
La medida de centralidad adecuada depende del objetivo de su análisis. ¿Quieres saber qué usuarios son señalados con frecuencia por otros en términos de cantidad? La centralidad del grado probablemente sea su mejor opción. ¿O prefiere una medida de centralidad que considere la calidad? En ese caso, el vector propio o el PageRank darán mejores resultados. Si quieres saber qué usuarios funcionan más eficazmente como puentes entre diferentes comunidades, la intermediación es tu mejor opción.
Cuando utilice varias medidas similares, por ejemplo, vector propio y PageRank, puede estimarlas todas y ver si producen clasificaciones equivalentes. Si no, puede profundizar su análisis de las diferencias o generar una nueva medida combinando sus puntajes.
Otro enfoque utiliza el análisis de componentes principales para estimar qué medida le brinda más información sobre la influencia real de los nodos en su red.
Cálculo práctico de centralidad
Veamos cómo podemos calcular estas medidas usando R y RStudio. (También se pueden hacer con Gephi).
Primero, necesitamos cargar todas las bibliotecas que vamos a usar a lo largo de este artículo:
library("plyr") library(igraph) library(tidyverse) library(NLP) library("tm") library(RColorBrewer) library(wordcloud) library(topicmodels) library(SnowballC) library("textmineR")
A continuación, eliminaremos los nodos aislados de los datos que usamos antes, ya que no son útiles para este análisis. Luego, usaremos las funciones igraph
betweenness
, centr_eigen
, page_rank
y degree
para estimar las medidas de centralidad. Finalmente, almacenaremos las puntuaciones en el objeto igraph
y en un marco de datos para ver qué usuarios fueron los más centrales.
load("art1_tweets.RData") Isolated = which(degree(net)==0) net_clean = delete.vertices(net, Isolated) cent<-data.frame(bet=betweenness(net_clean),eig=centr_eigen(net_clean)$vector,prank=(page_rank(net_clean)$vector),degr=degree(net_clean, mode="in")) cent <- cbind(account = rownames(cent), cent)
Ahora podemos consultar los 10 usuarios más centrales por cada medida:
La licenciatura | |
Vector propio | |
Rango de página | |
intermediación | |
Los resultados:
La licenciatura | Vector propio | Rango de página | intermediación | ||||
---|---|---|---|---|---|---|---|
ESPNFC | 5892 | PSG_dentro | 1 | mundodabola | 0.037 | vistasdey | 77704 |
TrollFútbol | 5755 | TripulacionesMat19 | 0.51 | AleLiparoti | 0.026 | edmund oris | 76425 |
PSG_dentro | 5194 | eh01195991 | 0.4 | PSG_dentro | 0.017 | bala*****lla | 63799 |
TripulacionesMat19 | 4344 | mohammad135680 | 0.37 | Roy Nemer | 0.016 | FranciscoGaius | 63081 |
fútbol | 4054 | ActuFoot_ | 0.34 | TrollFútbol | 0.013 | Yemihazán | 62534 |
PSG_español | 3616 | marttvall | 0.34 | ESPNFC | 0.01 | hashtag2weet | 61123 |
IbaiFuera | 3258 | ESPNFC | 0.3 | PSG_español | 0.007 | Ángela_FCB | 60991 |
ActuFoot_ | 3175 | fútbol | 0.25 | pie instantáneo | 0.007 | Zyyon_ | 57269 |
FootyHumor | 2976 | SaylorMoonEjército | 0.22 | IbaiFuera | 0.006 | TripulacionesMat19 | 53758 |
mundodabola | 2778 | JohnsvillePat | 0.2 | 2010MisterChip | 0.006 | MdeenOlawale | 49572 |
Podemos ver que las primeras tres medidas comparten una cantidad de usuarios, como PSG_inside, ESPNFC, CrewsMat19 y TrollFootball. Podemos suponer que tuvieron una fuerte influencia en la discusión. La intermediación tiene un enfoque diferente para medir la centralidad y, por lo tanto, no muestra tanta superposición con las otras técnicas.
Nota: Las opiniones expresadas por las cuentas de Twitter mencionadas en este artículo no reflejan las de Toptal ni las del autor.
En las siguientes imágenes, puede ver nuestro gráfico de red en color original con dos superposiciones de etiquetas de usuario. En el primero, los nodos se destacan por sus puntajes de PageRank, y en el segundo, por sus puntajes de intermediación:
Gephi se puede utilizar para reproducir estas imágenes. Puede estimar las puntuaciones de intermediación o de PageRank mediante el botón Diámetro de la red en el panel de estadísticas. Luego, puede mostrar los nombres de los nodos usando atributos como se demostró en la primera entrega de esta serie.
Análisis de texto: R y LDA
También podemos analizar las discusiones de las redes sociales para identificar de qué han estado hablando los usuarios. Hay múltiples formas de abordar esto. Haremos un modelado de temas a través de Latent Dirichlet Allocation (LDA), una técnica de aprendizaje automático no supervisado que nos permite estimar qué conjunto de palabras tienden a aparecer juntas. Entonces, a través de ese conjunto de palabras, podemos inferir el tema que se está tratando.
El primer paso es desinfectar el texto. Para ello, definimos la siguiente función:
# This function normalizes text by removing Twitter-related terms and noisy characters sanitize_text <- function(text) { # Convert to ASCII to remove accented characters: text <- iconv(text, to = "ASCII", sub = " ") # Move to lower case and delete RT word (this is added by Twitter) text <- gsub("rt", " ", tolower(text)) # Delete links and user names: text <- gsub("@\\w+", " ", gsub("http.+ |http.+$", " ", text)) # Delete tabs and punctuation: text <- gsub("[ |\t]{2,}", " ", gsub("[[:punct:]]", " ", text)) text <- gsub("amp", " ", text) # Remove HTML special character # Delete leading and lagging blanks: text <- gsub("^ ", "", gsub(" $", "", text)) text <- gsub(" +", " ", text) # Delete extra spaces return(text) }
También debemos eliminar las palabras vacías, los duplicados y las entradas vacías. A continuación, tenemos que convertir nuestro texto en una matriz de término de documento para ser procesado por LDA.

En este conjunto de datos, tenemos usuarios que hablan muchos idiomas (inglés, español, francés, etc.). LDA funciona mejor si nos enfocamos en un solo idioma. Lo vamos a aplicar sobre los usuarios de la mayor comunidad detectada en la primera entrega de esta serie, que se compone principalmente de cuentas con usuarios de habla inglesa.
# Detect communities: my.com.fast <-cluster_louvain(as.undirected(simplify(net))) largestCommunities <- order(sizes(my.com.fast), decreasing=TRUE)[1:3] # Save the usernames of the biggest community: community1 <- names(which(membership(my.com.fast) == largestCommunities[1])) # Sanitize the text of the users of the biggest community: text <- unique(sanitize_text(tweets.df[which(tweets.df$screen_name %in% community1),]$text)) text = text[text!=''] # Delete empty entries stopwords_regex = paste(stopwords('es'), collapse = '\\b|\\b') stopwords_regex = paste0('\\b', stopwords_regex, '\\b') # Remove English stopwords: text = stringr::str_replace_all(text, stopwords_regex, '') # Create the document term matrix: dtm <- CreateDtm(text, doc_names = seq(1:length(text)), ngram_window = c(1, 2))
Recuentos de temas y puntuaciones de coherencia
El principal hiperparámetro que necesitamos definir en LDA es el número (k) de temas que queremos estimar. Sin embargo, ¿cómo podemos saberlo de antemano? Un enfoque común es entrenar modelos LDA sobre diferentes valores de k y medir la coherencia de cada uno. Haremos esto para valores de k de 3 a 20, ya que no vale la pena verificar los valores fuera de este rango, en mi experiencia:
tf <- TermDocFreq(dtm = dtm) # Remove infrequent words: tf_trimmed = tf$term[ tf$term_freq > 1 & tf$doc_freq < nrow(dtm) / 2 ] # Create a folder to store trained models: model_dir <- paste0("models_", digest::digest(tf_trimmed, algo = "sha1")) if (!dir.exists(model_dir)) dir.create(model_dir) # Define a function to infer LDA topics: train_lda_model <- function(number_of_topics){ filename = file.path(model_dir, paste0(number_of_topics, "_topics.rda")) # Check if the model already exists: if (!file.exists(filename)) { # To get exactly the same output on each run, use a constant seed: set.seed(12345) lda_model = FitLdaModel(dtm = dtm, k = number_of_topics, iterations = 500) lda_model$k = number_of_topics lda_model$coherence = CalcProbCoherence(phi = lda_model$phi, dtm = dtm, M = 5) save(lda_model, file = filename) } else { load(filename) } lda_model } # The number of topics that we are going to infer in each LDA training run: topic_count = seq(3, 20, by = 1) # Train through the TmParallelApply function models = TmParallelApply(X = topic_count, FUN = train_lda_model, export = c("dtm", "model_dir"))
A continuación, graficamos el valor de coherencia de cada uno:
coherence_by_topics_quantity = data.frame( topic_number = sapply(models, function(model_instance) nrow(model_instance$phi)), score_coherence = sapply(models, function(model_instance) mean(model_instance$coherence)), stringsAsFactors = FALSE) ggplot(coherence_by_topics_quantity, aes(x = topic_number, y = score_coherence)) + geom_point() + geom_line(group = 1) + ggtitle("Coherence by Topic") + theme_minimal() + scale_x_continuous(breaks = seq(1,20,1)) + ylab("Coherence Score") + xlab("Number of topics")
Un alto valor de coherencia muestra una mejor segmentación del texto en temas:
Alcanzamos nuestro puntaje máximo de coherencia con k = 13, por lo que usaremos el modelo LDA entrenado con 13 temas. A través de la función GetTopTerms, podemos ver las 10 palabras principales de cada tema y estimar la semántica del tema a través de ellas:
best_model <- models[which.max(coherence_by_topics_quantity$score_coherence)][[ 1 ]] # Most important terms by topic: best_model$top_terms <- GetTopTerms(phi = best_model$phi, M = 20) top10 <- as.data.frame(best_model$top_terms) top10
La siguiente tabla detalla los cinco temas más importantes detectados y las 10 palabras principales que los ejemplifican:
t_1 | t_2 | t_3 | t_4 | t_5 | |
---|---|---|---|---|---|
1 | messi | messi | messi | messi | messi |
2 | leonel | liga | est | psg | |
3 | Lionel Messi | correo | victoria | Illinois | León |
4 | psg | millón | metas | es | Leo Messi |
5 | Madrid | gustos | ch | verter | ahora |
6 | real | spo | iones | paso | compa |
7 | Barcelona | cabra | ch_iones | avec | Virginia |
8 | París | psg | ucl | du | ser |
9 | Real Madrid | bar | globo | qui | jugador |
10 | mbapp | más grande | mundo | jeje | mejor |
Aunque la mayoría de los usuarios de esta comunidad son angloparlantes, todavía hay algunos que hablan francés y español (t_4 y t_5 en la tabla). Podemos inferir que el primer tema se relaciona con el equipo anterior de Messi (FC Barcelona), el segundo tema se trata de la publicación de Messi en Instagram y el tercer tema se centra en los logros de Messi.
Ahora que tenemos los temas, podemos predecir cuál de ellos fue el más discutido. Para hacer eso, primero concatenaremos los tweets de los usuarios (nuevamente, de la comunidad más grande):
tweets.df.com1 = tweets.df[which(tweets.df$screen_name %in% community1),] users_text <- ddply(tweets.df.com1, ~screen_name, summarise, text = paste(text, collapse = " "))
Luego, desinfectamos el texto como antes y creamos el DTM. Después de eso, llamamos a la función de predict
utilizando nuestro modelo LDA y el DTM como argumentos. Además, configuramos el método en Gibbs para mejorar el tiempo de cálculo porque tenemos mucho texto para analizar:
users_text$text <- sanitize_text(users_text$text) # Get rid of duplicates stopwords_regex = paste(stopwords('en'), collapse = '\\b|\\b') stopwords_regex = paste0('\\b', stopwords_regex, '\\b') users_text$text = stringr::str_replace_all(users_text$text, stopwords_regex, '') dtm.users.com1 <- CreateDtm(users_text$text, doc_names = users_text$screen_name, ngram_window = c(1, 2)) com1.users.topics = predict(best_model, dtm.users.com1, method="gibbs", iterations=100)
Ahora, en el marco de datos com1.users.topics
, vemos cuánto habló cada usuario sobre cada tema:
Cuenta | t_1 | t_2 | t_3 | t_4 | t_5 | […] |
---|---|---|---|---|---|---|
___99 | 0.02716049 | 0.86666666 | 0.00246913 | 0.00246913 | 0.00246913 | |
Jefe__ | 0.05185185 | 0.84197530 | 0.00246913 | 0.00246913 | 0.00246913 | |
Menfis | 0.00327868 | 0.00327868 | 0.03606557 | 0.00327868 | 0.00327868 | |
___Alex1 | 0.00952380 | 0.00952380 | 0.00952380 | 0.00952380 | 0.00952380 | |
[…] |
Finalmente, con esta información, podemos crear un nuevo atributo en el gráfico de nodos para definir qué tema fue más hablado por qué usuario. Luego podemos crear un nuevo archivo GML para visualizarlo en Gephi:
# Get the subgraph of the first community: net.com1 = induced_subgraph(net,community1) # Estimate the topic with the max score for each user: com1.users.maxtopic = cbind(users_text$screen_name, colnames(com1.users.topics)[apply(com1.users.topics, 1, which.max)]) # Order the users topic data frame by the users' order in the graph: com1.users.maxtopic = com1.users.maxtopic[match(V(net.com1)$name, com1.users.maxtopic[,1]),] # Create a new attr of the graph by the topic most discussed by each user: V(net.com1)$topic = com1.users.maxtopic[,2] # Create a new graph: write_graph(simplify(net.com1), "messi_graph_topics.gml", format = "gml")
Inferir temas importantes y aplicar la centralidad de la red social
En la primera entrega de esta serie, aprendimos cómo obtener datos de Twitter, crear el gráfico de interacción, trazarlo a través de Gephi y detectar comunidades y usuarios importantes. En esta entrega, ampliamos este análisis demostrando el uso de criterios adicionales para detectar usuarios influyentes. También demostramos cómo detectar e inferir de qué estaban hablando los usuarios y representarlo en la red.
En nuestro próximo artículo seguiremos profundizando en este análisis mostrando cómo los usuarios pueden detectar el flagelo de las redes sociales: los spambots y los trolls.
Lecturas adicionales en el blog de ingeniería de Toptal:
- Análisis de redes sociales con Power BI y R: una guía de elementos visuales personalizados