使用Gephi-Toolkit繪製動態網路圖
使用Gephi繪製動態網路圖
研究課題是關於網路演化的,需要繪製網路動態的演化圖,這裡主要是邊的演化,不同的邊有不同的時間。雖然原本的Gephi有動態圖的展示,但是Gephi功能太有限了,對圖的顏色,節點大小等支援都不夠,所以我這裡採用Python+Gephi-Toolkit+Premire的方式完成繪製。這裡重點在於Python和Gephi-ToolKit這兩個,Premire只是將生成的不同時刻的Pdf檔案合併成一個視訊。
Python處理原始資料
我們的原始資料是一個三列的鄰接表資料,三列分別為node1, node2, time,記錄了一條邊的時間。
0 608 2 1 248 1 1 466 1 1 586 1 2 262 1 3 263 1
Gephi識別的gexf格式的檔案的格式,關於這個檔案格式可在這個網頁找到說明 https://gephi.org/gexf/format/
本質上就是一個xml檔案,所以我們使用python中的lxml庫來構建,操作xml,根據原始資料,生成相應的gexf檔案。lxml的API見https://lxml.de/tutorial.html
首先我們根據原始資料生成一個edge_t字典,key為元組形式的邊,value為邊的生成時間:
# 構造edge_t詞典 def plot_graph_static(data_path, out_path): edge_t = dict() with open(data_path, 'r') as f: for line in f: s = line.strip().split(' ') edge_t[(int(s[0]), int(s[1]))] = int(s[2]) print("Edge_t complete!")
接下來就是構造gexf了,按照xml樹形結構的方式構建即可。注意
-
gexf標籤屬性
'mode': 'static', 'defaultedgetype': 'undirected'
即可 -
節點不要重複
-
需要給邊額外增加一個屬性,且這個屬性和邊的權重的值都為邊的生成時間(從1開始),要想增加屬性,應該在graph標籤下新增一個class為edge的attributes標籤:
<?xml version='1.0' encoding='UTF-8'?> <gexf xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.gexf.net/1.3" version="1.3"> <graph mode="static" defaultedgetype="undirected"> <attributes class="edge"> <attribute id="0" title="StartTime" type="string"/> </attributes>
然後在每個edge標籤下新增一個attvalues標籤:
<edge source="0" target="608" weight="2"> <attvalues> <attvalue for="0" value="2"/> </attvalues> </edge>
for屬性的值對應之前attribute標籤的id屬性。之所以這麼設定,是因為後面Gephi-Toolkit需要使用。
最終生成gexf檔案的程式碼如下:
# xml 編寫
nsmap = {'xsi': 'http://www.w3.org/2001/XMLSchema-instance'}
gexf = etree.Element('gexf', nsmap=nsmap)
gexf.set('xmlns', 'http://www.gexf.net/1.3')
gexf.set('version', '1.3')
graph = etree.SubElement(gexf, 'graph', attrib={'mode': 'static', 'defaultedgetype': 'undirected'})
attributes = etree.SubElement(graph, 'attributes', attrib={'class': 'edge'})
edge_attr = etree.Element('attribute', attrib={'id': '0', 'title': 'StartTime', 'type': 'string'})
attributes.append(edge_attr)
nodes = etree.SubElement(graph, 'nodes')
edges = etree.SubElement(graph, 'edges')
node_list = [] # 保證節點不重複
for edge in edge_t.keys():
if edge[0] not in node_list:
node_list.append(edge[0])
xml_node = etree.Element('node', attrib={'id': str(edge[0]), 'label': str(edge[0])})
nodes.append(xml_node)
if edge[1] not in node_list:
node_list.append(edge[1])
xml_node = etree.Element('node', attrib={'id': str(edge[1]), 'label': str(edge[1])})
nodes.append(xml_node)
xml_edge = etree.Element('edge', attrib={'source': str(edge[0]), 'target': str(edge[1]),
'weight': str(edge_t[edge])}) # gephi中邊權重不能<=0
attvalues = etree.SubElement(xml_edge, 'attvalues')
attvalue = etree.Element('attvalue', attrib={'for':'0', 'value':str(edge_t[edge])})
attvalues.append(attvalue)
edges.append(xml_edge)
gexf_tree = etree.ElementTree(gexf)
gexf_tree.write(out_path, pretty_print=True, xml_declaration=True, encoding='utf-8')
以上就是主要的程式碼了,我們記生成的檔案為static.gexf
,但是關於我們課題,我們需要比較兩種不同的演化,所以這兩種演化需要相同的佈局,而且佈局還要好看些,所以我們用Gephi開啟static.gexf
然後調整佈局,節點大小,顏色(關鍵是佈局,後面兩個隨便),然後輸出圖檔案為gexf格式,命名為static_move.gexf
然後根據這個佈局好了的static_move.gexf
,我們根據同一個網路的另一個原始資料(時間不同,之前的是原始時間,這個是我們預測的時間),修改static_move.gexf
裡面的內容,注意
-
在獲取標籤時要注意xml是有名稱空間的,
<gexf xmlns="http://www.gexf.net/1.3" version="1.3" xmlns:viz="http://www.gexf.net/1.3/viz" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.gexf.net/1.3 http://www.gexf.net/1.3/gexf.xsd">
在使用iter迭代標籤時要主要標籤前需要有名稱空間的部分,例如viz標籤:<node id="608" label="608"> <viz:size value="100.0"></viz:size> <viz:position x="-2221.5906" y="-979.51196"></viz:position> <viz:color r="46" g="0" b="18"></viz:color> </node>
我們迭代時就需要這麼寫
node.iter('{' + cur_nsmap['viz'] + '}' + 'color')
即使是沒有名稱空間的標籤,也是採用預設的名稱空間的,迭代時這麼寫
root.iter('{' + cur_nsmap[None] + '}' + 'node')
-
主要利用lxml對屬性的set和get函式修改屬性值。
所以這個在gexf檔案中修改邊時間的的程式碼:
def transfer_gexf(data_path, pattern_path, out_path):
"""
根據pattern_path裡的節點位置,大小等資訊,結合datapath的時間資訊,重新生成一個gexf檔案
"""
print("Transfering graph...")
# 構造edge_t詞典
edge_t = dict()
node_t = dict()
with open(data_path, 'r') as f:
for line in f:
s = line.strip().split(' ')
edge_t[(int(s[0]), int(s[1]))] = int(s[2])
if int(s[0]) not in node_t.keys():
node_t[int(s[0])] = int(s[2])
if int(s[1]) not in node_t.keys():
node_t[int(s[1])] = int(s[2])
print("Edge_t complete!")
with open(pattern_path, 'r') as f:
gexf_tree = etree.parse(f)
root = gexf_tree.getroot()
cur_nsmap = root.nsmap
for edge in root.iter('{' + cur_nsmap[None] + '}' + 'edge'):
# print(edge)
cur_edge_t = str(edge_t[(int(edge.get('source')), int(edge.get('target')))])
edge.set('weight', cur_edge_t)
for att in edge.iter('{' + cur_nsmap[None] + '}' + 'attvalue'):
att.set('value' ,cur_edge_t)
gexf_tree.write(out_path, pretty_print=True, xml_declaration=True, encoding='utf-8')
生成的gexf記為predict_static.gexf
。我們最後是需要把predict_static.gexf
, static_move.gexf
都經過Gephi-Toolkit處理,然後生成一堆pdf檔案的。所以流程都一樣,只不過檔案命名要區分一下。
Gephi-Toolkit處理gexf檔案
我利用gephi-toolkit-demos來了解這個工具,API為https://gephi.org/gephi-toolkit/0.9.2/apidocs/
首先這是一個java程式,我們安裝java1.8, 安裝idea整合開發工具,下載maven(一個包管理工具),我參考https://www.jb51.net/article/122326.htm的前面部分內容進行了配置。配置完了後用idea開啟gephi-toolkit-demos,右側maven裡面點clean,install安裝需要的包。ps:我2020年4月時用還好好的,但是8月份我換電腦後移植過來就install失敗,說有兩個包在中央倉庫(阿里雲)找不到。(((φ(◎ロ◎;)φ))),所以我就把我原來電腦上的倉庫拷貝複製到新電腦上了。
demo程式碼使用很簡單,執行Main.java即可:
// Main.java
package org.gephi.toolkit.demos;
public class Main {
public static void main(String[] args) {
MineDynamic mineDynamic = new MineDynamic();
mineDynamic.script();
}
}
後面註釋掉了原本的內容沒有展示,MineDynamic是我自己根據demo新建立的類。
MineDynamic.java有6部分,
-
初始化,匯入檔案,準備工作(其實不是很懂,但主要改動就是匯入的檔案路徑而已,其他不用管)
public void script() { //Init a project - and therefore a workspace ProjectController pc = Lookup.getDefault().lookup(ProjectController.class); pc.newProject(); Workspace workspace = pc.getCurrentWorkspace(); //Import file ImportController importController = Lookup.getDefault().lookup(ImportController.class); Container container; try { File file = new File(getClass().getResource("/org/gephi/toolkit/demos/bacteria/static_move.gexf").toURI()); container = importController.importFile(file); container.getLoader().setEdgeDefault(EdgeDirectionDefault.UNDIRECTED); } catch (Exception ex) { ex.printStackTrace(); return; } //Append imported data to GraphAPI importController.process(container, new DefaultProcessor(), workspace); //Prepare GraphModel graphModel = Lookup.getDefault().lookup(GraphController.class).getGraphModel(); AppearanceController appearanceController = Lookup.getDefault().lookup(AppearanceController.class); AppearanceModel appearanceModel = appearanceController.getModel(); FilterController filterController = Lookup.getDefault().lookup(FilterController.class); UndirectedGraph originGraph = graphModel.getUndirectedGraph(); System.out.println("OriginNodes: " + originGraph.getNodeCount()); System.out.println("OriginEdges: " + originGraph.getEdgeCount());
-
根據id為‘0’的屬性劃分邊
//Partition with '0' column, which is in the data
Column column = graphModel.getEdgeTable().getColumn("0");
Function func = appearanceModel.getEdgeFunction(originGraph, column, PartitionElementColorTransformer.class);
Partition partition = ((PartitionFunction) func).getPartition();
System.out.println(partition.size() + " partitions found");
這裡就用到之前生成gexf檔案時,構建的edge的StartTime屬性,id為‘0’.
-
根據劃分的個數設定顏色(不同時刻邊的顏色)
Object[] colors; colors = GenColorBarItemByHSL(Color.CYAN, partition.size()); for (int p = 0; p < partition.size(); p++) { System.out.println(p); partition.setColor("" + (p + 1), (Color) colors[p]); } appearanceController.transform(func);
private Color[] GenColorBarItemByHSL(Color startColor, int num) { float[] hsb = Color.RGBtoHSB(startColor.getRed(), startColor.getGreen(), startColor.getBlue(), null); float hue = hsb[0]; float saturation = hsb[1]; float brightness = hsb[2]; Color[] colorList = new Color[num]; for (int i = 0; i < num; i++) { Color vColor = Color.getHSBColor((hue + (float)(i) / (float)(num)) % 1, saturation, brightness); colorList[i] = vColor; } return colorList; }
-
對於每一個時刻, 根據邊權重(和那個屬性一樣的值),過濾掉其他時刻的邊,只顯示當前時刻的邊
for (double i = 1; i < partition.size() + 1; i++) { //Filter by weight EdgeWeightBuilder.EdgeWeightFilter edgeWeightFilter = new EdgeWeightBuilder.EdgeWeightFilter(); edgeWeightFilter.init(graphModel.getGraph()); edgeWeightFilter.setRange(new Range(0.0, i)); //Remove nodes with degree < 10 Query query = filterController.createQuery(edgeWeightFilter); GraphView view = filterController.filter(query); graphModel.setVisibleView(view); //Set the filter result as the visible view //Count nodes and edges on filtered graph UndirectedGraph graph = graphModel.getUndirectedGraphVisible(); System.out.println("Time:" + i + "Nodes: " + graph.getNodeCount() + " Edges: " + graph.getEdgeCount());
-
對於每一個時刻,因為不同權重的邊生成圖後邊的粗細不同,所以我們需要儲存當前的權重,然後把圖的所有邊權重都設為1,繪製完圖後在將邊的權重還原。
-
對於每一個時刻,設定輸出圖的一些屬性(邊寬),並輸出成pdf
//Rank color by Degree(Set all node to Red) Function degreeRanking = appearanceModel.getNodeFunction(graph, AppearanceModel.GraphFunction.NODE_DEGREE, RankingElementColorTransformer.class); RankingElementColorTransformer degreeTransformer = (RankingElementColorTransformer) degreeRanking.getTransformer(); degreeTransformer.setColors(new Color[]{new Color(0xFF0000), new Color(0xFF0000)}); degreeTransformer.setColorPositions(new float[]{0f, 1f}); appearanceController.transform(degreeRanking); //reset edge weight 1 Vector edgeWeights = new Vector(); Column weightCol = graphModel.getEdgeTable().getColumn("weight"); for (Edge n : graphModel.getGraph().getEdges()) { edgeWeights.add(n.getAttribute(weightCol)); n.setAttribute(weightCol, new Double(1.0f)); } //Preview PreviewModel model = Lookup.getDefault().lookup(PreviewController.class).getModel(); model.getProperties().putValue(PreviewProperty.BACKGROUND_COLOR, Color.BLACK); model.getProperties().putValue(PreviewProperty.SHOW_NODE_LABELS, Boolean.FALSE); model.getProperties().putValue(PreviewProperty.EDGE_COLOR, new EdgeColor(EdgeColor.Mode.ORIGINAL)); model.getProperties().putValue(PreviewProperty.EDGE_THICKNESS, new Float(20f)); model.getProperties().putValue(PreviewProperty.EDGE_RESCALE_WEIGHT, Boolean.TRUE); model.getProperties().putValue(PreviewProperty.NODE_LABEL_FONT, model.getProperties().getFontValue(PreviewProperty.NODE_LABEL_FONT).deriveFont(1)); // model.getProperties().putValue(PreviewProperty.NODE_OPACITY, new Float(50f)); model.getProperties().putValue(PreviewProperty.NODE_BORDER_COLOR, new DependantColor(Color.RED)); model.getProperties().putValue(PreviewProperty.NODE_BORDER_WIDTH, new Float(1f)); //Export ExportController ec = Lookup.getDefault().lookup(ExportController.class); try { ec.exportFile(new File("./Result/bacteria/bacteria" + i + ".pdf")); } catch (IOException ex) { ex.printStackTrace(); return; } //restore edge weight for (Edge n : graphModel.getGraph().getEdges()) { n.setAttribute(weightCol, edgeWeights.firstElement()); edgeWeights.remove(0); }
要想理解這些程式碼,最好還是跑下demo,看下程式碼,慢慢才能理解(但是Gephi-Toolkit有些看似能工作的程式碼其實沒有效果,比如看上去我可以直接根據weight劃分邊嘛,為什麼多此一舉還要自己新建一個屬性呢,我試過了,沒用,這是我踩過的最大的坑了。)