cucumber flesh

Rを中心としたデータ分析・統計解析らへんの話題をしていくだけ

🍭ホクソエムのつながりをNeo4Jを使って確かめる

先日、Neo4Jというオープンソースで開発されるグラフデータベースの存在を知りました。恥ずかしながら、グラフデータベース?なにそれ美味しいの?という知識しかありませんでしたが、どうやらNoSQL(リレーショナルデータベースだけがデータベースではない)の流れを組んで誕生したものらしいです。グラフデータベースは、表形式で表現されるリレーショナルデータベースに対して、データ間の関係性を表現するのに向いています。

気になるものがあるとすぐにRパッケージを探し出してしまう性分なので調べてみると、すぐに {RNeo4j}というパッケージが見つかりました。というわけでこの{RNeo4j}パッケージを使ってNeo4Jの扱いについて慣れていこうというメモです。

今回は適当な例として来月に結成1周年を迎える匿名知的集団「ホクソエム」のメンバー間のTwitter上でのフォロー状況について整理してみることにします。

🔨 データの用意

ホクソエムメンバーデータの取得

「鍵付きアカウント」を除いたメンバーのデータを取得します。ホクソエムメンバーについては https://twitter.com/teramonagi/lists/list/members にまとめてあるので、こちらのリストに登録されているユーザー名を{rvest}パッケージを使ってスクレイピングしてきます。その後、各メンバーの詳細な情報を{twitteR}で求めます。

library(magrittr)
library(rvest)
library(twitteR)
library(dplyr)
df.hoxom <- read_html("https://twitter.com/teramonagi/lists/list/members") %>% 
    html_nodes(xpath = "//div/div[3]/div/div/a/span") %>% 
    html_text() %>% grep("非公開ツイート", 
    x = ., value = TRUE, invert = TRUE) %>% 
    gsub("@", "", .) %>% sort() %>% as.data.frame(stringsAsFactors = FALSE)
colnames(df.hoxom) <- "screenName"
setup_twitter_oauth(consumer_key = Sys.getenv("TWITTER_KEY"), 
    consumer_secret = Sys.getenv("TWITTER_SECRET"))
info.hoxom <- df.hoxom %>% group_by(screenName) %>% 
    do(user = getUser(.) %>% {
        data_frame(screenName = screenName(.), 
            created = created(.), description = description(.))
    })

19名分のデータを取得しました。各ユーザーのデータは次のようになっています。

info.hoxom$user[[18]]
## Source: local data frame [1 x 3]
## 
##   screenName             created
##        (chr)              (time)
## 1     u_ribo 2009-03-11 22:40:25
## Variables not shown: description (chr)

メンバー間のフォロー関係を取得

ユーザーのフォローリストを取得して、操作しやすいようにリストからデータフレームに変換したものから必要な列だけを選択しホクソエムメンバーのみを抽出します。一度に実行するとAPIの利用制限に引っかかるので分割して実行するようにします。

hoxom.flw1 <- df.hoxom %>% tbl_df() %>% .[1:(nrow(.)/2), 
    ] %>% group_by(screenName) %>% do(res = getUser(.)$getFriends() %>% 
    twListToDF() %>% dplyr::select(screenName) %>% 
    dplyr::filter(screenName %in% df.hoxom$screenName))
hoxom.flw2 <- df.hoxom %>% tbl_df() %>% .[(nrow(.)/2):nrow(.) + 
    1, ] %>% group_by(screenName) %>% do(res = getUser(.)$getFriends() %>% 
    twListToDF() %>% dplyr::select(screenName) %>% 
    dplyr::filter(screenName %in% df.hoxom$screenName))

この情報がノード間の関係を示すものになります。

🔗 ノードと関係の構築

本題の{RNeo4j}パッケージを利用します。あらかじめNeo4Jを起動しておきます。

library(RNeo4j)
# Neo4Jへの接続。ユーザー名とパスワードは適宜変更
graph <- startGraph("http://localhost:7474/db/data/", 
    username = "<username>", password = "<PW>")
graph %>% class()
## [1] "graph"

こちらのgraphクラスオブジェクトに最初に取得したホクソエムデータを元にノードを作成し、メンバー間のフォロー状況を関係に現していきます。{RNeo4j}では、createNode()createRel()によって簡単にRからノードの操作を行えます。

u_ribo <- createNode(graph, .label = "Member", 
    name = info.hoxom$user[[18]]$screenName, 
    role = "清掃屋", created = info.hoxom$user[[18]]$created %>% 
        as.character())
u_ribo
## < Node > 
## Member
## 
## $role
## [1] "清掃屋"
## 
## $created
## [1] 2009-03-11 22:40:25
## 
## $name
## [1] "u_ribo"

ノードがもつ属性はlist()を使って渡すこともできます。

hoxo_m <- createNode(graph, .label = "Member", 
    list(name = info.hoxom$user[[7]]$screenName, 
        role = "Qiita", created = info.hoxom$user[[7]]$created %>% 
            as.character()))

このようにして全員分のノードを作成します。

次にノード間の関係を与えます。今回は、各メンバー(ノード)に対してフォローしている場合に値1を与えるようにします。例えば、私(u_irbo)はホクソエムの親分(hoxo_m)をフォローしているのでweight = 1とします。その関係はFOLLOWです。同様の処理を全メンバーについて行っていきます。

createRel(u_ribo, "FOLLOW", hoxo_m, weight = 1)
## < Relationship > 
## FOLLOW
## 
## $weight
## [1] 1

ユーザー名とフォロー状況については先に取得したデータを参照します。

# 対象のユーザー
hoxom.flw2$screenName[8]
## "u_ribo"
## フォローしているユーザー名
## hoxom.flw2$res[[8]]$screenName

🚀 クエリーを実行する

Neo4Jでのクエリーを記述するのには「Cypher」というものを使うそうです(MySQLでいうところのSQL)。cypher()関数にクエリーを文字列で渡して実行します。

query = "MATCH n RETURN n.name, n.role LIMIT 1"
(f <- cypher(graph, query))
##   n.name n.role
## 1 u_ribo 清掃屋

フォロー関係を図で表現する

いくつかの方法でグラフを作成できますが、{visNetwork}パッケージを使うとモダンな感じでデータ間の関係性を示すことができます。

library(visNetwork)

すべての関係を図示するとカオスな感じになったので、現在もホクソエムを名乗り続けているメンバーに絞ります。

# ベクトルを作成する true.hoxom <- c('')
query = "\nMATCH (n)-[:FOLLOW]->(n2)\nRETURN n.name AS from, n2.name AS to, COUNT(*) AS weight\n"
# (今更感があるけど)ユーザー名を隠すために符号化しておく
flw <- cypher(graph, query) %>% dplyr::filter(from %in% 
    true.hoxom, to %in% true.hoxom) %>% dplyr::mutate(from = from %>% 
    as.factor() %>% as.numeric(), to = to %>% 
    as.factor() %>% as.numeric())
nodes <- data_frame(id = unique(c(flw$from, 
    flw$to)), label = id)
visNetwork(nodes, flw) %>% visEdges(color = list(hover = "deeppink")) %>% 
    visInteraction(hover = TRUE) %>% visOptions(highlightNearest = TRUE) %>% 
    visNodes(shape = "icon", icon = list(face = "FontAwesome", 
        code = "f007", color = "forestgreen")) %>% 
    addFontAwesome() %>% visSave(file = "hoxom_follows_out.html")

グニャグニャと動いている場合、しばらく待つと止まります...。適当に選択したり引っ張ったりすると図が動きますが意味はありません。遊び心です。

🍵 所感

  • ノード間の関係を構築していく作業が結構面倒。
  • SQLよりもCypherの方が扱いやすい印象
  • 結局関係が複雑すぎて当初の目標を達成できていない感。
  • すべてのメンバーが相互フォローしているわけではないので、ホクソエムも一枚岩ではないなと思うなど。
  • 直接は関係ないけど、ちょうど良いタイミングでUniversal resilience patterns in complex networks (Jianxi Gao et al. 2015)というのを読んで、この論文の内容に沿った動画が、植物と動物の相互作用とかレジリアンス、気候変動が起こるとどうなることが予想されるかについてわかりやすいく導入しているし生態学とか興味ない人にもオススメ。

🔖 参考

💻 実行環境

devtools::session_info() %>% {
    print(.$platform)
    .$packages %>% dplyr::filter(`*` == "*") %>% 
        knitr::kable(format = "markdown")
}
##  setting  value                       
##  version  R version 3.2.3 (2015-12-10)
##  system   x86_64, darwin13.4.0        
##  ui       X11                         
##  language En                          
##  collate  en_US.UTF-8                 
##  tz       Asia/Tokyo                  
##  date     2016-02-23
package * version date source
dplyr * 0.4.3.9000 2015-10-28 Github ()
magrittr * 1.5 2016-01-13 Github ()
remoji * 0.1.0 2016-01-19 Github ()
RNeo4j * 1.6.3 2016-01-29 CRAN (R 3.2.3)
rvest * 0.3.1 2015-11-11 CRAN (R 3.2.2)
twitteR * 1.1.9 2016-02-14 Github ()
visNetwork * 0.2.1 2016-01-31 CRAN (R 3.2.3)
xml2 * 0.1.2 2015-09-01 CRAN (R 3.2.0)