返回文章列表

圖形資料處理與Python實作技術

本文介紹如何使用 Python 和 igraph 函式庫處理圖形資料,包含從 CSV 檔案匯入節點和邊、計算度中心性、分析連線元件、以及使用 select() 方法查詢和篩選節點與邊等實用技巧。文章以 Facebook 相互讚資料集為例,示範如何建構圖形、分析圖形結構,並探索熱門節點的屬性。

圖形資料函式庫 Python

本文探討如何運用 Python 和 igraph 函式庫有效地處理圖形資料。從 CSV 檔案匯入節點和邊資料開始,逐步建構圖形結構,接著示範如何計算節點的度中心性,並藉此分析圖形的連線元件特性。同時,文章詳細說明如何使用 select() 方法,根據節點和邊的屬性進行精確查詢和篩選,進一步探索圖形資料的深層資訊。最後,以 Facebook 相互讚資料集為例,示範如何找出熱門節點,並分析其鄰居節點的屬性,展現圖形分析在實際應用中的價值。

使用 GraphData 模型進行圖形資料處理

在前面的章節中,我們逐步建立了一個圖形(graph),並從 Facebook 的相互讚(mutual likes)資料中匯入節點(nodes)和邊(edges)。現在,我們將這個過程通用化,建立一些 Python 方法來簡化未來的圖形匯入工作。

編寫通用的圖形匯入方法

讓我們回顧一下從 Facebook 相互讚 .csv 檔案建立圖形的步驟:

  1. 建立一個空的 igraph Graph() 物件
  2. 匯入包含節點、節點屬性和邊的 .csv 檔案
  3. 將節點新增到圖形中,並進行一些測試
  4. 將邊新增到圖形中,並進行更多測試

步驟 1:定義一個用於匯入 .csv 檔案的函式

首先,我們定義一個函式 read_csv() 用於匯入 .csv 檔案:

def read_csv(csv_path):
    '''
    匯入一個 csv 檔案。
    
    :param csv_path: 要匯入的 csv 檔案路徑。
    :return: 從 csv 檔案中讀取的列表的列表。
    '''
    import csv
    import os
    assert os.path.exists(csv_path), f'在 {csv_path} 處找不到檔案。'
    with open(csv_path, 'r', encoding='utf-8') as csv_file:
        reader = csv.reader(csv_file)
        data = [row for row in reader]
    return data

內容解密:

  • read_csv() 函式接受一個檔案路徑 csv_path,並檢查該檔案是否存在。
  • 使用 csv.reader 讀取檔案內容,並將其轉換為列表的列表。
  • assert 陳述式用於檢查檔案是否存在,如果不存在,則列印錯誤資訊。

步驟 2-4:定義一個用於新增節點到圖形的函式

接下來,我們定義一個函式 add_nodes() 用於將節點新增到圖形中:

def add_nodes(g, nodes, attributes):
    '''
    將節點新增到圖形中。
    
    :param g: 一個 igraph Graph() 物件。
    :param nodes: 一個包含節點和節點屬性的列表的列表,第一個元素是標題。
    :param attributes: 一個對應於 nodes 列表標題的屬性列表。
    '''
    assert nodes[0][0] == 'id', f'匯入的 csv 檔案的第一列應該是 ID 標題 "id",而不是 {nodes[0][0]}。'
    node_ids = [int(row[0]) for row in nodes[1:]]
    assert node_ids == list(range(len(node_ids))), f'節點 ID 應該在匯入的 csv 檔案中從 0 到 {len(node_ids)}-1 依次遞增。'
    assert isinstance(attributes, list), f'要新增到圖形的屬性應該是一個列表,而不是 {type(attributes)}。'
    
    g.add_vertices(len(node_ids))
    headers = nodes[0]
    for attribute in attributes:
        attr_index = headers.index(attribute)
        g.vs[attribute] = [row[attr_index] for row in nodes[1:]]
    return g

內容解密:

  • add_nodes() 函式接受一個圖形物件 g、一個包含節點資訊的列表 nodes 和一個屬性列表 attributes
  • 使用 assert 陳述式檢查 nodes 列表的第一列是否為 ‘id’,以及節點 ID 是否依次遞增。
  • 將節點新增到圖形中,並根據 attributes 列表將相應的屬性新增到節點上。

步驟 5:定義一個用於新增邊到圖形的函式

最後,我們定義一個函式 add_edges() 用於將邊新增到圖形中:

def add_edges(g, edges):
    '''
    將邊新增到圖形中,假設節點已經存在。
    
    :param g: 一個 igraph Graph() 物件。
    :param edges: 一個包含邊資訊的列表的列表,第一個元素是標題。
    '''
    assert len(edges[0]) == 2, f'匯入的 edges csv 檔案中的每個元素應該長度為 2,代表兩個節點之間的邊,而不是 {len(edges[0])}。'
    edges_to_add = [[int(row[0]), int(row[1])] for row in edges[1:]]
    g.add_edges(edges_to_add)
    return g

內容解密:

  • add_edges() 函式接受一個圖形物件 g 和一個包含邊資訊的列表 edges
  • 使用 assert 陳述式檢查 edges 列表中的每個元素是否長度為 2。
  • 將邊新增到圖形中。

在Python中實作模型

add_edges()方法中,我們將標頭後的所有列轉換為整數對,然後使用這個列表來以列表方式將邊新增到我們的圖中。然後,傳回增加了邊的圖。在此方法中,我們增加了一個額外的斷言陳述式,以確保匯入的.csv檔案包含兩列,分別對應邊連線的兩個節點。值得注意的是,儘管此資料集不包含邊屬性,但這些屬性可以包含在邊列表中,這將導致斷言陳述式失敗。然而,對於沒有邊屬性的圖,每行不應該有多餘的元素。

編寫通用包裝方法

最後,我們可以編寫一個通用的包裝方法來自動化此過程中的各個步驟:

def graph_from_attributes_and_edgelist(node_attr_csv, edgelist_csv, attributes):
    import igraph
    g = igraph.Graph(directed=False)
    nodes = read_csv(node_attr_csv)
    edges = read_csv(edgelist_csv)
    g = add_nodes(g, nodes, attributes)
    g = add_edges(g, edges)
    return g

內容解密:

  1. 匯入必要的igraph函式庫,用於建立和操作圖。
  2. 建立一個空的無向圖物件g
  3. 從指定的CSV檔案中讀取節點屬性和邊列表。
  4. 使用add_nodesadd_edges方法分別將節點和邊新增到圖中。
  5. 傳回構建完成的圖。

呼叫包裝方法

現在,我們只需要呼叫graph_from_attributes_and_edgelist函式並傳入所需的引數,即可建立一個圖:

node_attr_path = './data/musae_facebook_target.csv'
edgelist_path = './data/musae_facebook_edges.csv'
attributes = ['page_name', 'page_type']
g = graph_from_attributes_and_edgelist(node_attr_path, edgelist_path, attributes)

內容解密:

  1. 指定節點屬性和邊列表的CSV檔案路徑。
  2. 定義要從節點屬性CSV檔案中匯入的屬性列表。
  3. 呼叫graph_from_attributes_and_edgelist函式建立圖。

驗證圖的正確性

為了確認新建立的圖與之前步驟中的圖相同,我們可以列印一些圖屬性並進行比較。以下陳述式應該列印與之前範例相同的值:

print(g.vs[0]['page_name'])
print(g.vs[0]['page_type'])
first_edge = g.es[0]
print(first_edge.source)
print(first_edge.target)
print(len(g.es))
print(g.vs[0]['page_name'])
print(g.vs[18427]['page_name'])

內容解密:

  1. 列印第一個節點的page_namepage_type屬性。
  2. 取得第一條邊的源節點和目標節點ID。
  3. 列印圖中的邊總數。
  4. 再次列印第一個和第18428個節點的page_name屬性,以驗證資料的正確性。

分析圖結構

現在我們已經驗證了圖的正確性,可以開始使用它來回答根據網路的問題。首先,我們來分析圖的結構。一個常見的做法是檢視圖的度分佈,即節點的度中心性分佈。度中心性簡單來說就是一個節點與多少其他節點分享邊。

檢視度分佈

我們可以使用igraphdegree_distribution方法來快速構建和列印度分佈:

histogram = g.degree_distribution(bin_width=5)
print(histogram)

內容解密:

  1. 使用degree_distribution方法計算圖的度分佈。
  2. bin_width引數控制直方圖的分箱寬度。

繪制度分佈直方圖

為了更直觀地展示度分佈,我們可以使用matplotlib函式庫繪製直方圖:

import matplotlib.pyplot as plt
bins = 30
plt.hist(g.degree(), bins)
plt.xlabel('Node degree centrality')
plt.ylabel('Frequency')
plt.show()

內容解密:

  1. 匯入matplotlib.pyplot函式庫用於繪圖。
  2. 設定直方圖的分箱數量為30。
  3. 使用g.degree()取得所有節點的度,並繪製直方圖。
  4. 設定X軸和Y軸標籤,並顯示圖形。

使用圖形資料模型(Working with GraphData Models)

在分析圖形結構時,瞭解節點之間的連線方式至關重要。度中心性(Degree Centrality)是一種衡量節點重要性的指標,代表與該節點直接相連的邊的數量。

度分佈圖表分析

透過計算節點的度分佈,我們可以得到以下

此圖表示許多節點具有少數連線,而少數節點具有大量連線,這是一種常見的圖形結構。高度連線的節點被稱為樞紐節點(Hub Nodes),它們在網路中扮演著至關重要的角色,使得節點之間的連線更加緊密。

測量連線性

並非所有圖形都是完全連線的。為了了解Facebook相互讚資料集的連線性質,我們可以使用igraph函式庫的clusters()方法檢查連線元件的數量:

connected_components = g.clusters()
print(connected_components)

輸出結果將顯示圖形中的連線元件數量以及每個節點所屬的元件。在我們的例子中,圖形是完全連線的,只存在一個連線元件。

內容解密:

  1. g.clusters():呼叫igraph的clusters()方法,計算圖形中的連線元件。
  2. connected_components:儲存計算結果,代表圖形中的連線元件。
  3. 在無向圖中,連線元件的概念相對簡單;而在有向圖中,則需要區分弱連線元件強連線元件

檢視最高度節點

為了找出網路中度中心性最高的節點,我們可以按照以下步驟進行:

  1. 結合page_name和節點度,使用zip函式將兩個元組組合成列表,並按降序排序:

degree = list(zip(g.vs[‘page_name’], g.degree())) sorted_degree = sorted(degree, key=lambda x: x[1], reverse=True) print(sorted_degree[:10])


   從輸出結果中,我們發現「US Army」節點具有最高的度中心性,擁有709個相互讚。

   #### 內容解密:
   1. `g.vs['page_name']`:取得所有節點的頁面名稱。
   2. `g.degree()`:計算每個節點的度。
   3. `sorted_degree`:按度中心性降序排序後的節點列表。

2. 若要檢視特定型別的最高度節點,可以使用`vs.select()`方法。例如,找出度中心性最高的電視節目:
   ```python
tv_nodes = g.vs.select(page_type_eq='tvshow')
tv_indices = [node.index for node in tv_nodes]
tv_degree = list(zip(g.vs[tv_indices]['page_name'], g.degree(tv_indices)))
sorted_tv_degree = sorted(tv_degree, key=lambda x: x[1], reverse=True)
print(sorted_tv_degree[:10])

結果顯示,「Today Show」的度中心性最高,擁有141個相互讚。

內容解密:

  1. g.vs.select(page_type_eq='tvshow'):選取所有型別為電視節目的節點。
  2. g.degree(tv_indices):計算所選電視節目節點的度。
  3. sorted_tv_degree:按度中心性降序排序後的電視節目列表。

使用select()查詢圖形

vs.select()是一種強大的查詢方法,透過指定節點屬性和比較運算子,可以靈活地篩選節點。例如,找出型別為政府或政治家的節點:

gov_pol_nodes = g.vs.select(page_type_in=['government', 'politician'])

內容解密:

  1. page_type_in=['government', 'politician']:指定篩選條件,找出型別為政府或政治家的節點。
  2. _in運算子用於檢查屬性值是否在給定的列表中。

透過這些方法,我們可以深入瞭解圖形資料集中的重要節點和結構特徵,為進一步的分析和應用奠定基礎。

使用 select() 方法探索熱門節點的屬性

在瞭解 select() 方法後,我們現在可以進一步探索最受歡迎的電視節點的屬性,並瞭解它在 Facebook 互粉圖中的重要性。

熱門節點的屬性

我們先前發現,互粉數最高的電視節目是《Today Show》,其度中心性(degree centrality)為 141。我們可以透過檢查其鄰居節點來瞭解這個度中心性最高的節點的更多資訊。在無向網路中,例如我們的 Facebook 互粉圖,與某個節點透過邊相連的節點被稱為鄰居節點(incident nodes)。找出《Today Show》的所有鄰居節點將揭示哪些 Facebook 頁面在我們的資料集中分享共同興趣。

  1. 為了做到這一點,我們將再次使用 select() 方法,但這次是針對邊。使用 es.select() 方法可以根據邊的屬性(如果有的話)找到邊,語法與表 2.1 相同。然而,我們的邊不包含任何屬性,我們更感興趣的是它們連線的節點。

    就像 vs.select() 方法一樣,es.select() 有一些特殊的引數,可以用來找到連線具有特定條件的節點的邊——同樣,每個引數都以下劃線開頭。

    下表顯示了與 vs.select() 命令一起使用的各種 dunder 方法,也稱為 magic 方法。這些方法可以用來執行「功能」列中列出的功能:

Edge select 引數字首功能
_incident找出與特定節點或節點列表相關聯的邊,忽略邊的方向。
_source (或 _from)在有向圖中,找出以特定節點或節點列表為起始的邊。
_target (或 _to)在有向圖中,找出以特定節點或節點列表為終止的邊。
_within找出兩端節點都在給定列表中的邊。
_between找出兩端節點分別在兩個給定列表中的邊(這兩個列表以元組形式提供)。

表 2.2 – es.select() 特殊引數

  1. 現在,因為我們的互粉圖是無向的,讓我們使用 _incident 引數在 es.select() 中選擇與《Today Show》相關聯的邊。es.select() 使用節點 ID,因此我們需要先找到我們感興趣的電視節目的節點 ID:
today_show_id = g.vs.select(page_name_eq='Today Show')[0].index
print(today_show_id)

在這裡,與本章前面的做法類別似,我們使用 vs.select() 找到頁面名稱等於《Today Show》的節點。由於 vs.select() 傳回一個可迭代物件,我們需要取第一個元素 [0] 以存取其 .index 屬性。我們的 print 陳述式將顯示我們的節點 ID 是 909。

  1. 現在,我們可以使用 es.select() 找到包含節點 909 的邊。我們指定 _incident 引數等於我們的 today_show_id 變數,並將其放在一個列表中(注意 es.select() 需要列表形式的節點,用於表 2.2 中的所有特殊引數):
today_show_edges = g.es.select(_incident=[today_show_id])
  1. 就像使用 vs.select() 一樣,這行程式碼傳回一個包含邊的可迭代物件。我們可以使用 today_show_edges 中每個邊的 .source.target 屬性提取每個邊的兩個節點:
sources = [edge.source for edge in today_show_edges]
targets = [edge.target for edge in today_show_edges]
print(sources)
print(targets)

這將向我們展示兩個節點 ID 的列表,所有這些 ID 都存在於與《Today Show》相關聯的邊中。

注意:在我們的互粉圖中,由於圖是無向的,源和目標實際上是等價的。這裡,源和目標僅由 igraph 為每個邊標註,根據先前使用 add_edges() 新增的每個二元節點對的索引。

  1. 請注意,代表《Today Show》的節點 909 在我們的列表中多次出現。這是預期的,因為我們要求 igraph 傳回所有包含它的邊。然而,為了檢查《Today Show》的鄰居節點,我們需要從結果中刪除它。我們可以使用 set() 傳回 sourcestargets 列表串聯後的元素集合,然後刪除節點 909(對應於《Today Show》):
neighbor_nodes = list(set(sources + targets))
neighbor_nodes.remove(909)
print(neighbor_nodes)
print(len(neighbor_nodes))

內容解密:

  • 首先,我們使用列表推導式從 today_show_edges 中提取源和目標節點 ID,分別儲存在 sourcestargets 中。
  • 然後,我們使用 set() 合併並去除重複的節點 ID,再轉換回列表,得到所有鄰居節點的唯一 ID 列表 neighbor_nodes
  • 最後,從 neighbor_nodes 中移除《Today Show》的 ID(909),並列印預出最終的鄰居節點列表及其長度。

我們的第一次 print() 陳述式現在應該顯示一個唯一的節點 ID 列表。我們還可以列印預出 neighbor_nodes 列表元素的數量,應該是 141。注意,這等於先前計算出的《Today Show》的度中心性。

程式碼邏輯解析

  • 使用 igraph 的 select() 方法對節點和邊進行篩選。
  • 利用 _incident 引數找出與特定節點相關聯的邊。
  • 處理傳回的可迭代物件以提取所需資訊,例如鄰居節點。
  • 使用集合運算去除重複元素並最佳化結果。

最佳實踐建議

  • 在處理大規模網路資料時,注意計算效率和記憶體使用。
  • 利用 igraph 提供的高效演算法和資料結構簡化網路分析任務。
  • 結合具體的研究問題和資料特徵選擇適當的分析方法和視覺化策略。