Статьи

Визуализация графика neo4j с использованием gephi

В ThoughtWorks у нас нет линейных менеджеров, но люди могут выбрать спонсора — обычно это кто-то, кто работал в компании дольше / имеет больший опыт работы в отрасли, чем они, — которые могут помочь им лучше ориентироваться в организации.

Из слухов, что люди говорили о спонсорах в течение последних 6 лет, казалось, что большинство спонсировало большинство, и, вероятно, было несколько человек, у которых не было спонсора.

Казалось, это довольно хорошая проблема для визуализации в графике, поэтому я получил доступ к данным, провел несколько часов, приводя их в порядок, чтобы все имена соответствовали именам, которые есть в нашем приложении для укомплектования персоналом, а затем загрузил их в neo4j.

Сначала я пытался визуализировать данные в sigma.js, но здесь это не сработало — я думаю, что гораздо лучше, когда мы действительно хотим просмотреть график, тогда как здесь меня просто интересует общий снимок.

Поэтому я решил загрузить данные в Gephi и найти способ визуализации их с помощью этого.

Отношения на графике таковы:

Спонсоры графвиз

Я создал это, используя следующее определение graphviz :

graph effectgraph {
	size="8,8"; 
	rankdir=LR;
 
	person1[label="Person 1"];
	person2[label="Person 2"];	
	person3[label="Person 3"];	
	officeA[label="Office A"];
 
	officeA -- person1 [label="member_of"];
	officeA -- person2 [label="member_of"];
	officeA -- person3 [label="member_of"];
	person1 -- person2 [label="sponsor_of"];
	person2 -- person3 [label="sponsor_of"];	
}
dot -Tpng v3.dot >> sponsors.png

Я написал скрипт на основе поста Макса де Марци в блоге, чтобы перевести данные в формат gexf, чтобы я мог загрузить их в gephi:

Сначала я получаю коллекцию всех людей, которые являются спонсорами, и сколько у них спонсоров:

def load_sponsors
 query =  " START n = node(*)" 
 query << " MATCH n-[r:sponsor_of]->n2" 
 query << " RETURN ID(n), count(r) AS sponsees ORDER BY sponsees DESC"
 
 sponsors = {}
 @neo.execute_query(query)["data"].each do |id, sponsees|
 	sponsors[id] = sponsees
 end
 sponsors
end

Это создает хэш спонсоров с подсчетом количества спонсоров, которых я использовал в следующей функции для создания коллекции узлов:

def nodes
  query =  " START n = node(*)"
  query << " MATCH n-[r:member_of]->o" 
  query << " WHERE o.name IN ['London', 'Manchester', 'Hamburg'] AND not(has(r.end_date))"
  query << " RETURN DISTINCT(n.name), ID(n)"
 
  sponsors_sponsee_count = load_sponsors
 
  nodes = Set.new
  @neo.execute_query(query)["data"].each do |n| 
  	nodes << { "id" => n[1], "name" => n[0], "size" => 5 + ((sponsors_sponsee_count[n[1]] || 0) * 5) }
  end
 
  nodes
end

У меня есть узлы, представляющие людей во всей организации, поэтому мне нужно отфильтровать, чтобы найти только людей, которые работают в ThoughtWorks Europe, поскольку именно здесь у меня есть данные спонсора. Я добавляю здесь свойство размера, чтобы люди, у которых было больше спонсоров, были более заметны на графике.

Затем у нас есть следующая функция для описания отношений «спонсор»:

def edges
  query =  " START n = node(*)"
  query << " MATCH n-[r:sponsor_of]->n2"
  query << " RETURN ID(r), ID(n), ID(n2)"
 
  @neo.execute_query(query)["data"].collect{|n| {"id" => n[0], "source" => n[1], "target" => n[2]} }
end

Я использую следующий код для генерации нужного мне формата XML:

xml = Builder::XmlMarkup.new(:target=>STDOUT, :indent=>2)
xml.instruct! :xml
xml.gexf 'xmlns' => "http://www.gephi.org/gexf", 'xmlns:viz' => "http://www.gephi.org/gexf/viz"  do
  xml.graph 'defaultedgetype' => "directed", 'idtype' => "string", 'type' => "static" do
    xml.nodes :count => nodes.size do
      nodes.each do |n|
        xml.node :id => n["id"],   :label => n["name"] do
          xml.tag!("viz:size",     :value => n["size"])
          xml.tag!("viz:color",    :b => 255, :g => 255, :r => 255)
          xml.tag!("viz:position", 😡 => rand(100), :y => rand(100))
       end
      end
    end
    xml.edges :count => edges.size do
      edges.each do |e|
        xml.edge:id => e["id"], :source => e["source"], :target => e["target"]
      end
    end
  end
end

В итоге получается что-то вроде следующего:

<?xml version="1.0" encoding="UTF-8"?>
<gexf xmlns="http://www.gephi.org/gexf" xmlns:viz="http://www.gephi.org/gexf/viz">
  <graph defaultedgetype="directed" idtype="string" type="static">
    <nodes count="274">
      <node id="1331" label="Person 1">
        <viz:size value="5"/>
        <viz:color b="255" g="255" r="255"/>
        <viz:position x="69" y="31"/>
      </node>
    ....
    </nodes>
    <edges count="187">
      <edge id="7975" source="56" target="1374"/>
    </edges>
  </graph>
</gexf>

Я установил расположение узлов для рандомизации, потому что алгоритмы Gephi, похоже, работают намного лучше.

Затем я могу создать файл gexf следующим образом:

ruby gephi_me.rb >> sponsors.gexf

Я загрузил его в Gephi и запустил алгоритмы Force Atlas и ‘Noverlap’ по графику, чтобы немного проще визуализировать данные:

Спонсоры

Лучшие 4 спонсора на графике являются спонсорами для 28 человек между ними, а следующие 7 охватывают еще 35 человек.

Интересно, что в середине есть большая группа сирот, у которых нет спонсора — изначально я думал, что это немного странно, что есть так много людей, которые переехали в Великобританию из другой страны и имеют спонсора из этой страны. также входят в эту категорию.

Я написал следующий запрос, чтобы помочь мне узнать, кто такие сироты, заметив это на визуализации:

query =  " START n = node(*)"
  query << " MATCH n-[r:member_of]->o, n<-[r2?:sponsor_of]-n2" 
  query << " WHERE r2 is null and o.name IN ['London', 'Manchester', 'Hamburg'] AND not(has(r.end_date))"
  query << " RETURN DISTINCT(n.name), ID(n)"

Я хотел аннотировать изображение, чтобы указать, какие конкретные люди были для внутреннего использования, и несколько человек в твиттере указали мне на skitch, что сделало мою жизнь удивительно легкой, поэтому я очень рекомендую это.