Pajekでtwitterのソーシャルグラフを描く

twitterソーシャルグラフをとっていろいろいじりたくなったので、まず手始めにグラフを作ってpajekで描いてみた。

※ screen_idにコンバートする関数がまちがってる


<まとめ>

  • 以下の要領でtwitterのエゴネットワークをつくった。
    • ある人物の50ツイート以内での返信(reply)相手を1ホップとして、
    • 特定のアカウントから2ホップまでの人を対象とした
    • 対象メンバー内でのネットワークを、100ツイート内の返信(reply)回数で重み付けした有向グラフで表現した
    • 対象メンバー外に対しての返信は無視した
  • 生成されたネットワークを.netファイルに変換してpajek上で表示させてみた。

0.graphライブラリの使い方を覚える
http://gimite.net/gimite/rubymess/graph/
このサイトにあるグラフ理論のライブラリをつかった。
ハッシュをベースに実装してあって、例えば有向グラフは
{:member1 => {:member2 =>weight}, ... }みたいなデータ構造になっている。

<よく使う関数のまとめ>

graph = DirectedHashGraph.new()
graph[v,w] = weight
graph.each_edge # 各エッジに関してeach
graph.vertices # ノードをリストで返す

1.ソーシャルグラフをとる
1-1.ソーシャルグラフに含まれるメンバーをとる

任意のアカウントとホップ数を引数にして、グラフの対象とするメンバーを返す関数つくった(※正確には、再帰twitter APIを叩く回数の節約のために、1少ないホップの時に新しく取れたメンバーも返すようにした(配列を用いた多値関数にした))。
対象とするツイートはデフォルトで50ツイートにしたが、これも引数にいれてもいいかも。

Example;
ego_network_members("",4)
=> [ [member1,member2...],[member1,member2,... ] ]

def ego_network_members(account,hop) #[0]=members [1]=newmembers
account = Twitter.users(account)[0].id # converting from alphabet id to numerical id
members = Array.new()
new_members = Array.new()
members[0]=account


# hopが1の時は
if hop==1
error_count = 0
while error_count < 5 # errorは4回まで
begin
tweets = Twitter.user_timeline(account,{:count =>50})
rescue
error_count = error_count +1
STDERR.puts "Warning: #$!"
puts "error:#{account}" # errorを吐いたアカウントを表示
# 以降正常な場合の処理
# in_reply_to_user_idでリプライ相手をとる
else
tweets.each do |tweet|
members << tweet.in_reply_to_user_id
end
error_count = 5
puts "success"
end
end
members.delete(nil) # in_reply_to_user_idでnilが返されるので除去
members.uniq! # ダブりを消す
new_members=members
new_members.delete(account) # account は唯一新しく取れたアカウントでないので消す
p members
else
hoge = ego_network_members(account,hop-1)
old_members = hoge[0]
members = old_members
step_old_members = hoge[1]
step_old_members.each do |member| # 前のステップで初出のアカウントだけを対象にhop=1でまわす
new_members = new_members + ego_network_members(member,1)[0]
end
members = members + new_members
members.uniq!
new_members = members - old_members
new_members.uniq!
end
[members,new_members]
end

1-2.1-1でとってきたメンバー同士のネットワークをとる(有向グラフ)
対象となるメンバーが確定したので、そのメンバーひとりひとりに対してtweetをとって(100ツイート)、リプライ相手に関してエッジにリプライ回数だけ重みをつけていく。

def creating_twitter_graph(members)
# return network
network = DirectedHashGraph.new()
count_member = 0
# 対象メンバー
members.each do |member|
count = 0
# エラーをカウントする
while count < 6
begin
tweets = Twitter.user_timeline(member,{:count => 100}) # 100tweetをとる
rescue
puts "fail : #{member}" if count ==5
count = count+1
else
unless tweets ==nil
p member
tweets.each do |tweet|
replying_person = tweet.in_reply_to_user_id # reply相手をとる
unless network[replying_person,member] == nil # edge already exist?
network[replying_person,member]=network[replying_person,member]+1 # edgeの重みを加える
else
network[replying_person,member]=1 # edgeをつくる
end
end
count = 6

end
end
end
count_member = count_member+1
puts "count member: #{count_member}"
end
network
end

1-3.メンバー外の人、あるいは片思いの人を除く
片思いの人は邪魔なので除く。
ryouomoi(graph)はgraphをいれると片思いのノードを消し去る。
each_edgeメソッドをつかうと存在するエッジにしか適用されないので、任意の2ノード間に相互のアローが存在するかを二重のeachで検証した。

def ryouomoi(graph)
graph.vertices.each do |v|
graph.vertices.each do |w|
if (graph[v,w]==nil or graph[w,v]==nil)
graph.delete_edge(v,w)
graph.delete_edge(w,v)
end
end
end
graph.vertices.each do |v|
graph.delete_vertex(v) if graph.out_degree(v)==0
end
graph
end

1-4.ユーザーネームをidからscreen_nameに変換する

in_reply_to_user_idが数字のアカウントでかえってくるのでscreen_nameに再変換する。eachとかつかって回せると楽なところだが、Twitter APIの制約があるので100アカウントずつわたして2〜3回しか叩かないで済ますという方法をとる。

def convert_numerical_alphabet(graph)
new_network = DirectedHashGraph.new()
node_name = Hash.new()
numerical_ids = graph.vertices
screen_names = Array.new()
# 100アカウントに制限されているので100ずつ通す
for i in 0..numerical_ids.size/100
# max < 100
unless i == numerical_ids.size/100
max = (i+1)*100-1
else
max = numerical_ids.size-1
end

Twitter.users(numerical_ids[i*100..max]).each do
|u| screen_names << u.screen_name

end
end
ids = [numerical_ids,screen_names]
for i in 0..ids[0].size-1
node_name[numerical_ids[i]] = screen_names[i]
end
# node name をscreen_nameに更新する
graph.each_edge(uniq= false) do |v,w|
new_network[node_name[v],node_name[w]]=graph[v,w]
end
new_network
end
graph = convert_numerical_alphabet(graph)
p graph
FasterCSV.open("christopher_ego_graph_alphabet.csv","w") do |writer|
graph.each_edge do |v,w|
writer << [v,w,graph[v,w]]
end
end

で、ここまででグラフが完成。長い道のり。(Twitter APIが何百回と叩かれるのを待つのほうが"長い"が・・・)

2.Pajek用にファイルを描く

http://vlado.fmf.uni-lj.si/pub/networks/pajek/howto/jp/pajek-amr4-6-2.pdf などを参考に .netファイルをつくる。全部スペース区切り。
writer に << しても自動では改行されないとはじめて知った。

File.open("pajek_***_egonetwork.net","w") do |writer|
writer << "*Vertices #{graph.vertices.size}\n"
id_num_table = Hash.new()
for i in 0..graph.vertices.size-1
writer << "#{i+1} #{graph.vertices[i]}\n"
id_num_table[graph.vertices[i]]=i+1
end
writer << "*Arcs\n"
graph.each_edge do |v,w|
writer << "#{id_num_table[v]} #{id_num_table[w]} #{graph[v,w]}\n"
end
end

・・・ということで完成!!!