使用 R 和 Gephi 理解 Twitter 動態:文本分析和中心性
已發表: 2022-07-22本文擴展並深化了我們社交網絡分析系列的第一部分中提出的分析。 我們使用第一篇文章中構建的相同 Twitter 數據集和交互網絡。 但是,這一次的想法是更好地推斷主要參與者,確定他們的討論主題,並了解這些主題是如何傳播的。
社交網絡中心性
為了實現我們的目標,首先我們需要引入中心性的概念。 在網絡科學中,中心性是指對網絡有強烈影響的節點。 影響是一個模棱兩可的概念; 它可以從很多方面來理解。 具有許多邊的節點是否比具有較少但更“重要”邊的節點更有影響力? 什麼構成了社交網絡的重要優勢?
為了解決這些模糊性,網絡科學家開發了許多中心性度量。 在這裡,我們討論了四種常用的測量方法,但還有更多可用的測量方法。
程度
最常見和最直觀的衡量標準是度中心性。 度中心性背後的想法很簡單:通過節點的度來衡量影響。 如果圖是有向的,它可以有變體; 在這種情況下,您可以測量入度和出度——第一個稱為中心分數,第二個稱為權威分數。
在本系列的第一部分中,我們使用了無向方法。 這一次,我們專注於入度方法。 這允許通過強調被其他人轉發的用戶而不是僅頻繁轉發的用戶來進行更準確的分析。
特徵向量
特徵向量測量建立在度中心性之上。 有影響力的節點越多地指向給定節點,其得分就越高。 我們從鄰接矩陣開始,其中行和列代表節點,我們使用 1 或 0 來表示給定行和列的對應節點是否連接。 主要計算估計矩陣的特徵向量。 主特徵向量將包含我們想要的中心度度量,其中位置i將保持節點i的中心度得分。
網頁排名
PageRank 是谷歌核心特徵向量度量的變體。 谷歌使用的確切方法是未知的,但一般的想法是每個節點從分數 1 開始,然後將其分數等分分配到它的每條邊上。 例如,如果一個節點有三個從它延伸出來的邊,它會通過每條邊“發送”三分之一的分數。 同時,指向它的邊使節點變得更加重要。 這導致了一個具有N個未知數的N個方程的可解系統。
介數
第四個度量,介數,使用了一種非常不同的方法。 在這裡,如果一個節點包含在其他節點之間的許多短路徑中,則稱該節點具有影響力。 也就是說,它負責與許多其他節點進行通信,連接“不同的世界”。
例如,在社交網絡分析中,這類節點可以理解為幫助他人找到新工作或建立新聯繫的人——它們是通往未知社交圈的大門。
我應該使用哪個?
適當的中心性度量取決於您的分析目標。 你想知道哪些用戶在數量上經常被別人挑出來嗎? 學位中心性可能是您的最佳選擇。 還是您更喜歡考慮質量的中心性度量? 在這種情況下,特徵向量或 PageRank 會產生更好的結果。 如果您想知道哪些用戶最有效地充當不同社區之間的橋樑,那麼介數是您的最佳選擇。
當使用多個相似的度量時,例如,特徵向量和 PageRank,您可以估計所有這些並查看它們是否產生等效的排名。 如果沒有,您可以加深對差異的分析或通過組合它們的分數來生成新的度量。
另一種方法使用主成分分析來估計哪個度量可以為您提供有關節點對網絡的實際影響的更多信息。
動手中心性計算
讓我們看看如何使用 R 和 RStudio 計算這些度量。 (它們也可以用 Gephi 完成。)
首先,我們需要加載我們將在本文中使用的所有庫:
library("plyr") library(igraph) library(tidyverse) library(NLP) library("tm") library(RColorBrewer) library(wordcloud) library(topicmodels) library(SnowballC) library("textmineR")
接下來,我們將從之前使用的數據中刪除孤立的節點,因為它們對分析沒有用處。 然後,我們將使用igraph
函數betweenness
、 centr_eigen
、 page_rank
和degree
來估計中心性度量。 最後,我們將分數存儲在igraph
對象和數據框中,以查看哪些用戶是最中心的。
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)
現在我們可以通過每個度量來檢查 10 個最中心的用戶:
程度 |
|
特徵向量 |
|
網頁排名 |
|
介數 |
|
結果:
程度 | 特徵向量 | 網頁排名 | 介數 | ||||
---|---|---|---|---|---|---|---|
ESPNFC | 5892 | PSG_inside | 1 | 蒙多達博拉 | 0.037 | 視點 | 77704 |
巨魔足球 | 5755 | 船員墊19 | 0.51 | 阿萊利帕羅蒂 | 0.026 | 愛德蒙奧里斯 | 76425 |
PSG_inside | 5194 | eh01195991 | 0.4 | PSG_inside | 0.017 | 巴*****拉 | 63799 |
船員墊19 | 4344 | 穆罕默德135680 | 0.37 | 羅伊內默 | 0.016 | 弗朗西斯科蓋烏斯 | 63081 |
br足球 | 4054 | ActuFoot_ | 0.34 | 巨魔足球 | 0.013 | 耶米哈贊 | 62534 |
PSG_espanol | 3616 | 馬特瓦爾 | 0.34 | ESPNFC | 0.01 | hashtag2weet | 61123 |
IbaiOut | 3258 | ESPNFC | 0.3 | PSG_espanol | 0.007 | 安吉拉_FCB | 60991 |
ActuFoot_ | 3175 | br足球 | 0.25 | 瞬間足 | 0.007 | Zyyon_ | 57269 |
足球幽默 | 2976 | 賽勒月軍 | 0.22 | IbaiOut | 0.006 | 船員墊19 | 53758 |
蒙多達博拉 | 2778 | 約翰斯維爾帕特 | 0.2 | 2010芯片先生 | 0.006 | 奧拉瓦萊 | 49572 |
我們可以看到前三個度量共享多個用戶,例如 PSG_inside、ESPNFC、CrewsMat19 和 TrollFootball。 我們可以假設他們對討論有很大的影響。 中介性有一種不同的方法來衡量中心性,因此與其他技術沒有太多重疊。
注:本文提及的推特賬號所表達的觀點不代表Toptal或作者的觀點。
在下圖中,您可以看到我們原始的帶有兩個用戶標籤疊加層的彩色網絡圖。 在第一個中,節點通過它們的 PageRank 分數突出顯示,在第二個中,通過它們的中介分數突出顯示:
Gephi 可用於重現這些圖像。 您可以使用統計面板中的 Network Diameter 按鈕估計介數或 PageRank 分數。 然後,您可以使用屬性顯示節點名稱,如本系列第一部分中所演示的。
文本分析:R 和 LDA
我們還可以分析社交網絡討論,以確定用戶一直在談論什麼。 有多種方法可以解決這個問題。 我們將通過潛在狄利克雷分配 (LDA) 進行主題建模,這是一種無監督機器學習技術,可以讓我們估計哪一組詞傾向於一起出現。 然後,通過這組詞,我們可以推斷出正在討論的話題。
第一步是淨化文本。 為此,我們定義了以下函數:
# 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) }
我們還需要刪除停用詞、重複項和空條目。 接下來,我們必須將我們的文本轉換為要由 LDA 處理的文檔術語矩陣。
在這個數據集中,我們有使用多種語言(英語、西班牙語、法語等)的用戶。 如果我們專注於單一語言,LDA 效果最好。 我們將把它應用於本系列第一部分中檢測到的最大社區的用戶,該社區主要由講英語的用戶的帳戶組成。
# 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))
主題計數和連貫性分數
我們需要在 LDA 中定義的主要超參數是我們想要估計的主題的數量(k) 。 但是,我們怎麼能事先知道呢? 一種常見的方法是在不同的k值上訓練 LDA 模型並測量每個模型的一致性。 我們將對從 3 到 20 的k值執行此操作,因為根據我的經驗,超出此範圍的值不值得檢查:
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"))
接下來,我們繪製每個的相干值:
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")
高連貫性值表明將文本更好地分割為主題:
我們在k = 13 時達到了最高一致性分數,因此我們將使用經過 13 個主題訓練的 LDA 模型。 通過 GetTopTerms 函數,我們可以看到每個主題的 10 個主詞,並通過它們估計主題語義:
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
下表詳細列出了檢測到的五個最重要的主題以及舉例說明它們的 10 個主要詞:
t_1 | t_2 | t_3 | t_4 | t_5 | |
---|---|---|---|---|---|
1 | 梅西 | 梅西 | 梅西 | 梅西 | 梅西 |
2 | 萊昂內爾 | 聯盟 | 美東時間 | psg | |
3 | 萊昂內爾·梅西 | 郵政 | 贏 | 我 | 獅子座 |
4 | psg | 百萬 | 目標 | 澳大利亞 | leo_messi |
5 | 馬德里 | 喜歡 | ch | 倒 | 阿霍拉 |
6 | 真實的 | 斯波 | 離子 | 帕斯 | 康帕 |
7 | 巴塞羅那 | 山羊 | ch_ions | avec | va |
8 | 巴黎 | psg | 倫敦大學學院 | 杜 | SER |
9 | 皇家馬德里 | 酒吧 | 氣球 | 奎 | 鬥牛士 |
10 | mbapp | 大 | 世界 | 傑 | 主要 |
儘管該社區中的大多數用戶都是說英語的,但仍然有一些說法語和西班牙語的用戶(表中的 t_4 和 t_5)。 我們可以推斷,第一個話題與梅西之前的球隊(巴塞羅那足球俱樂部)有關,第二個話題是關於梅西在 Instagram 上的帖子,第三個話題關注的是梅西的成就。
現在我們有了主題,我們可以預測其中哪些是討論最多的。 為此,我們將首先連接用戶的推文(同樣,來自最大的社區):
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 = " "))
然後,我們像以前一樣清理文本並創建 DTM。 之後,我們使用我們的 LDA 模型和 DTM 作為參數調用predict
函數。 此外,我們將方法設置為 Gibbs 以縮短計算時間,因為我們有很多文本要分析:
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)
現在,在com1.users.topics
數據框中,我們可以看到每個用戶對每個主題的討論程度:
帳戶 | t_1 | t_2 | t_3 | t_4 | t_5 | […] |
---|---|---|---|---|---|---|
___第 99 | 0.02716049 | 0.86666666 | 0.00246913 | 0.00246913 | 0.00246913 | |
老闆__ | 0.05185185 | 0.84197530 | 0.00246913 | 0.00246913 | 0.00246913 | |
孟菲斯 | 0.00327868 | 0.00327868 | 0.03606557 | 0.00327868 | 0.00327868 | |
___亞歷克斯1 | 0.00952380 | 0.00952380 | 0.00952380 | 0.00952380 | 0.00952380 | |
[…] |
最後,利用這些信息,我們可以在節點圖上創建一個新屬性,以定義哪個用戶談論最多的主題。 然後我們可以創建一個新的 GML 文件在 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")
推斷重要主題和應用社交網絡中心性
在本系列的第一部分中,我們學習瞭如何從 Twitter 獲取數據,創建交互圖,通過 Gephi 繪製它,以及檢測社區和重要用戶。 在本期中,我們通過演示使用其他標準來檢測有影響力的用戶來擴展此分析。 我們還演示瞭如何檢測和推斷用戶正在談論的內容並在網絡中繪製出來。
在我們的下一篇文章中,我們將通過展示用戶如何檢測社交媒體的禍害:垃圾郵件機器人和巨魔來繼續深化這一分析。
進一步閱讀 Toptal 工程博客:
- 使用 Power BI 和 R 進行社交網絡分析:自定義視覺效果指南