1. 程式人生 > 實用技巧 >使用Gephi-Toolkit繪製動態網路圖

使用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.gexfstatic_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部分,

  1. 初始化,匯入檔案,準備工作(其實不是很懂,但主要改動就是匯入的檔案路徑而已,其他不用管)

    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());
    
  2. 根據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’.

  1. 根據劃分的個數設定顏色(不同時刻邊的顏色)

    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;
        }
    
  2. 對於每一個時刻, 根據邊權重(和那個屬性一樣的值),過濾掉其他時刻的邊,只顯示當前時刻的邊

    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());
    
    
  3. 對於每一個時刻,因為不同權重的邊生成圖後邊的粗細不同,所以我們需要儲存當前的權重,然後把圖的所有邊權重都設為1,繪製完圖後在將邊的權重還原。

  4. 對於每一個時刻,設定輸出圖的一些屬性(邊寬),並輸出成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劃分邊嘛,為什麼多此一舉還要自己新建一個屬性呢,我試過了,沒用,這是我踩過的最大的坑了。)