Entendendo a dinâmica do Twitter com R e Gephi: análise de texto e centralidade
Publicados: 2022-07-22Este artigo expande e aprofunda a análise apresentada na primeira parte de nossa série de análise de redes sociais. Usamos o mesmo conjunto de dados do Twitter e rede de interação construídos no primeiro artigo. Porém, desta vez, a ideia é inferir melhor os atores principais, identificar seus tópicos de discussão e entender como esses tópicos se espalham.
Centralidade da Rede Social
Para atingir nossos objetivos, primeiro precisamos introduzir o conceito de centralidade . Na ciência da rede, a centralidade refere-se a nós que têm uma forte influência na rede. A influência é um conceito ambíguo; pode ser entendido de muitas maneiras. Um nó com muitas arestas é mais influente do que um nó com menos arestas, porém mais “importantes”? O que constitui uma vantagem importante em uma rede social?
Para lidar com essas ambiguidades, os cientistas de rede desenvolveram muitas medidas de centralidade. Aqui, discutimos quatro medidas comumente usadas, embora muitas outras estejam disponíveis.
Grau
A medida mais comum e intuitiva é a centralidade de grau. A ideia por trás da centralidade de grau é simples: meça a influência pelo grau do nó. Pode ter variantes se o gráfico for direcionado; nesse caso, você pode medir o grau de entrada e o grau de saída — o primeiro é conhecido como pontuação de hub e o segundo como pontuação de autoridade.
Na primeira parte desta série, usamos a abordagem não direcionada. Desta vez, focamos na abordagem indegree. Isso permite uma análise mais precisa, enfatizando os usuários que são retweetados por outras pessoas sobre os usuários que apenas retuítam com frequência.
Vetor próprio
A medida de autovetor baseia-se na centralidade de grau. Quanto mais nós influentes apontarem para um determinado nó, maior será sua pontuação. Começamos com uma matriz de adjacência , onde linhas e colunas representam nós, e usamos 1 ou 0 para indicar se os nós correspondentes de uma determinada linha e coluna estão conectados. O cálculo principal estima os autovetores da matriz. O autovetor principal conterá as medidas de centralidade que queremos, onde a posição i conterá a pontuação de centralidade do nó i .
Ranking da página
PageRank é a variação da medida de autovetor no núcleo do Google. O método exato que o Google usa é desconhecido, mas a ideia geral é que cada nó comece com uma pontuação de 1 e depois distribua sua pontuação em partes iguais para cada uma de suas arestas. Por exemplo, se um nó tem três arestas que se estendem a partir dele, ele “envia” um terço de sua pontuação através de cada aresta. Ao mesmo tempo, o nó se torna mais importante pelas arestas que apontam para ele. Isso resulta em um sistema solucionável de N equações com N incógnitas.
Intermediação
A quarta medida, intermediação , usa uma abordagem muito diferente. Aqui, um nó é considerado influente se estiver incluído em muitos caminhos curtos entre outros nós. Ou seja, é responsável por se comunicar com muitos outros nós, conectando “diferentes mundos”.
Por exemplo, na análise de redes sociais, esses tipos de nós podem ser entendidos como os tipos de pessoas que ajudam os outros a encontrar novos empregos ou fazer novas conexões – eles são as portas para círculos sociais anteriormente desconhecidos.
Qual Devo Usar?
A medida de centralidade apropriada depende do objetivo de sua análise. Você quer saber quais usuários são frequentemente apontados por outros em termos de quantidade? A centralidade de grau provavelmente seria sua melhor opção. Ou você prefere uma medida de centralidade que considere a qualidade? Nesse caso, o eigenvector ou o PageRank produzirão melhores resultados. Se você quiser saber quais usuários funcionam de maneira mais eficaz como pontes entre diferentes comunidades, a intermediação é sua melhor opção.
Ao usar várias medidas semelhantes, por exemplo, autovetor e PageRank, você pode estimar todas elas e ver se elas geram classificações equivalentes. Caso contrário, você pode aprofundar sua análise das diferenças ou gerar uma nova medida combinando suas pontuações.
Outra abordagem usa a análise de componentes principais para estimar qual medida fornece mais informações sobre a influência real dos nós em sua rede.
Cálculo prático de centralidade
Vamos ver como podemos calcular essas medidas usando R e RStudio. (Eles também podem ser feitos com Gephi.)
Primeiro, precisamos carregar todas as bibliotecas que usaremos ao longo deste artigo:
library("plyr") library(igraph) library(tidyverse) library(NLP) library("tm") library(RColorBrewer) library(wordcloud) library(topicmodels) library(SnowballC) library("textmineR")
Em seguida, removeremos nós isolados dos dados que usamos anteriormente, pois eles não são úteis para esta análise. Em seguida, usaremos as funções igraph
betweenness
, centr_eigen
, page_rank
e degree
para estimar as medidas de centralidade. Por fim, armazenaremos as pontuações no objeto igraph
e em um data frame para ver quais usuários foram os mais centrais.
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)
Agora podemos verificar os 10 usuários mais centrais por medida:
Grau | |
Vetor próprio | |
Ranking da página | |
Intermediação | |
Os resultados:
Grau | Vetor próprio | Ranking da página | Intermediação | ||||
---|---|---|---|---|---|---|---|
ESPNFC | 5892 | PSG_dentro | 1 | mundodabola | 0,037 | vista | 77704 |
Futebol Troll | 5755 | CrewsMat19 | 0,51 | Ale Liparoti | 0,026 | Edmundo Oris | 76425 |
PSG_dentro | 5194 | eh01195991 | 0,4 | PSG_dentro | 0,017 | ba*****lla | 63799 |
CrewsMat19 | 4344 | mohammad135680 | 0,37 | Roy Nemer | 0,016 | Francisco Caio | 63081 |
futebol | 4054 | ActuFoot_ | 0,34 | Futebol Troll | 0,013 | Yemihazan | 62534 |
PSG_espanol | 3616 | marttvall | 0,34 | ESPNFC | 0,01 | hashtag2weet | 61123 |
IbaiOut | 3258 | ESPNFC | 0,3 | PSG_espanol | 0,007 | Ângela_FCB | 60991 |
ActuFoot_ | 3175 | futebol | 0,25 | Pé Instantâneo | 0,007 | Zyyon_ | 57269 |
FootyHumour | 2976 | Saylor Moon Army | 0,22 | IbaiOut | 0,006 | CrewsMat19 | 53758 |
mundodabola | 2778 | Johnsvill Pat | 0,2 | 2010Mister Chip | 0,006 | Mdeen OlawaleGenericName | 49572 |
Podemos ver que as três primeiras medidas compartilham vários usuários, como PSG_inside, ESPNFC, CrewsMat19 e TrollFootball. Podemos supor que eles tiveram uma forte influência sobre a discussão. A intermediação tem uma abordagem diferente para medir a centralidade e, portanto, não mostra tanta sobreposição com as outras técnicas.
Nota: As opiniões expressas pelas contas do Twitter mencionadas neste artigo não refletem as da Toptal ou do autor.
Nas imagens a seguir, você pode ver nosso gráfico de rede colorido original com duas sobreposições de rótulos de usuário. No primeiro, os nós são destacados por suas pontuações de PageRank e, no segundo, por suas pontuações de intermediação:
Gephi pode ser usado para reproduzir essas imagens. Você pode estimar as pontuações de intermediação ou PageRank usando o botão Network Diameter no painel de estatísticas. Em seguida, você pode mostrar nomes de nós usando atributos conforme demonstrado na primeira parte desta série.
Análise de Texto: R e LDA
Também podemos analisar as discussões nas redes sociais para identificar sobre o que os usuários estão falando. Existem várias maneiras de abordar isso. Faremos a modelagem de tópicos por meio da Alocação de Dirichlet Latente (LDA), uma técnica de aprendizado de máquina não supervisionada que nos permite estimar qual conjunto de palavras tende a aparecer juntas. Então, através desse conjunto de palavras, podemos inferir o tema que está sendo discutido.
O primeiro passo é higienizar o texto. Para isso, definimos a seguinte função:
# 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) }
Também precisamos remover palavras de parada, duplicatas e entradas vazias. Em seguida, temos que converter nosso texto em uma matriz documento-termo para ser processado pelo LDA.
Neste conjunto de dados, temos usuários falando em vários idiomas (inglês, espanhol, francês etc.). O LDA funciona melhor se nos concentrarmos em um único idioma. Vamos aplicá-lo sobre os usuários da maior comunidade detectada na primeira parte desta série, composta principalmente por contas com usuários de língua 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))
Contagens de tópicos e pontuações de coerência
O principal hiperparâmetro que precisamos definir no LDA é o número (k) de tópicos que queremos estimar. No entanto, como podemos sabê-lo de antemão? Uma abordagem comum é treinar modelos LDA sobre diferentes valores de k e medir a coerência de cada um. Faremos isso para valores k de 3 a 20, pois valores fora desse intervalo não valem a pena verificar, na minha experiência:
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"))
Em seguida, fazemos o gráfico do valor de coerência de cada um:
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")
Um alto valor de coerência mostra uma melhor segmentação do texto em tópicos:
Atingimos nossa pontuação máxima de coerência com k = 13, então usaremos o modelo LDA treinado com 13 tópicos. Através da função GetTopTerms, podemos ver as 10 palavras principais de cada tópico e estimar a semântica do tópico através delas:
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
A tabela a seguir detalha os cinco tópicos mais importantes detectados e as 10 principais palavras que os exemplificam:
t_1 | t_2 | t_3 | t_4 | t_5 | |
---|---|---|---|---|---|
1 | messi | messi | messi | messi | messi |
2 | leão | liga | Husa | PSG | |
3 | Lionel Messi | publicar | ganhar | il | leão |
4 | PSG | milhão | metas | au | leo_messi |
5 | madri | gosta | CH | derramar | agora |
6 | real | spo | íons | pas | compa |
7 | barcelona | cabra | ch_ions | avec | vai |
8 | Paris | PSG | ucl | du | Ser |
9 | real Madrid | bar | balão | qui | jugador |
10 | mbapp | Maior | mundo | je | melhor |
Embora a maioria dos usuários desta comunidade seja falante de inglês, ainda há vários falantes de francês e espanhol (t_4 e t_5 na tabela). Podemos inferir que o primeiro tópico se refere ao time anterior de Messi (FC Barcelona), o segundo tópico é sobre a postagem de Messi no Instagram e o terceiro tópico se concentra nas conquistas de Messi.
Agora que temos os tópicos, podemos prever qual deles foi o mais discutido. Para fazer isso, primeiro vamos concatenar os tweets dos usuários (mais uma vez, da maior comunidade):
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 = " "))
Em seguida, higienizamos o texto como antes e criamos o DTM. Depois disso, chamamos a função de predict
usando nosso modelo LDA e o DTM como argumentos. Além disso, definimos o método para Gibbs para melhorar o tempo de computação porque temos muito texto para analisar:
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)
Agora, no data frame com1.users.topics
, vemos o quanto cada usuário falou sobre cada tópico:
Conta | t_1 | t_2 | t_3 | t_4 | t_5 | […] |
---|---|---|---|---|---|---|
___99º | 0,02716049 | 0,86666666 | 0,00246913 | 0,00246913 | 0,00246913 | |
Chefe__ | 0,05185185 | 0,84197530 | 0,00246913 | 0,00246913 | 0,00246913 | |
Memphis | 0,00327868 | 0,00327868 | 0,03606557 | 0,00327868 | 0,00327868 | |
___Alex1 | 0,00952380 | 0,00952380 | 0,00952380 | 0,00952380 | 0,00952380 | |
[…] |
Por fim, com essas informações, podemos criar um novo atributo no gráfico de nós para definir qual tópico foi mais falado por qual usuário. Então podemos criar um novo arquivo GML para visualizá-lo no 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")
Inferindo tópicos importantes e aplicando a centralidade da rede social
Na primeira parte desta série, aprendemos como obter dados do Twitter, criar o gráfico de interação, plotá-lo através do Gephi e detectar comunidades e usuários importantes. Nesta edição, expandimos essa análise demonstrando o uso de critérios adicionais para detectar usuários influentes. Também demonstramos como detectar e inferir sobre o que os usuários estavam falando e plotar isso na rede.
Em nosso próximo artigo, continuaremos aprofundando essa análise mostrando como os usuários podem detectar o flagelo das mídias sociais: spambots e trolls.
Leitura adicional no Blog da Toptal Engineering:
- Análise de rede social usando Power BI e R: um guia de recursos visuais personalizados