返回文章列表

單模網路轉換為關聯網路的原理與實踐

本文探討將單模網路(Single-mode Network)轉換為關聯網路(Affiliation

網路科學 資料科學

在網路科學的分析框架中,區分不同類型的網路結構至關重要。單模網路,如社交網路中的友誼圖,直接描繪同質實體間的關係,但其分析視角有所侷限。當研究焦點轉向關係本身,例如探討哪些「友誼」是連結不同社群的關鍵橋樑時,就需要一種新的模型。關聯網路(或稱二分圖)為此提供了理想的框架,它透過兩種不同類型的節點來描繪關係。本文的核心即在於闡述如何進行這種結構性的轉換:將單模網路中的「邊界」實體化為節點,從而建構出一個全新的二分圖。這個過程不僅是技術操作,更是一種分析視角的重構,讓我們能從關係的角度重新審視網路的結構與動態。

轉換單模網路為關聯網路:以友誼網路為例

在前一節中,我們探討了如何識別和處理已有的關聯網路。本節將進一步探討一個更具挑戰性的場景:如何將一個現有的單模網路(Single-mode Network)轉換為關聯網路。我們將以 Zachary 空手道俱樂部網路為例,說明這種轉換的原理和實踐。

單模網路的局限與轉換需求

Zachary 空手道俱樂部網路是一個典型的單模網路,其中節點代表俱樂部成員,邊界代表他們之間的友誼關係。在這個網路中,所有節點都屬於同一類型(成員),邊界直接連接兩個成員。

然而,有時我們可能希望以不同的視角來分析這種關係。例如,我們可能想將「友誼關係」本身也視為一個「實體」,以便分析不同成員如何透過這些「友誼」節點產生間接聯繫,或者分析哪些「友誼」節點連接了特定的成員群體。這就催生了將單模網路轉換為關聯網路的需求。

轉換原理:將邊界視為節點

將單模網路轉換為關聯網路的一種常見方法是「將邊界本身也變成節點」。具體來說:

  1. 創建新的節點類型:引入一個新的節點類型,用來代表原網路中的每一條邊界。
  2. 建立連接
    • 對於原網路中的每一個節點 v,創建一個與之關聯的新節點(代表 v)。
    • 對於原網路中的每一條邊界 (v, w),創建一個新的節點,代表這條邊界。
    • 將原節點 v 與代表邊界 (v, w) 的新節點連接起來。
    • 將原節點 w 也與代表邊界 (v, w) 的新節點連接起來。

這樣,我們就得到了一個具有兩種節點類型的網路:

  • 類型一:原網路中的節點(例如,空手道俱樂部成員)。
  • 類型二:代表原網路中邊界的新節點(例如,友誼關係)。

在這個新的關聯網路中,邊界連接的是一個「成員」節點和一個「友誼」節點。

程式碼實踐:轉換 Zachary 網路

以下程式碼演示瞭如何將 Zachary 空手道俱樂部網路 G 轉換為一個關聯網路 B。在這個新的網路 B 中:

  • 一種類型的節點代表原網路中的成員。
  • 另一種類型的節點代表原網路中的友誼邊界。
import networkx as nx
from networkx.algorithms import bipartite
from networkx import NetworkXError

# 載入 Zachary 空手道俱樂部網路
G = nx.karate_club_graph()

print("--- 檢查原始 Zachary 網路是否為關聯網路 
---
")
try:
    # 嘗試找到節點的兩個集合
    left_original, right_original = bipartite.sets(G)
    print("原始 Zachary 網路被識別為關聯網路。")
    print("左節點:", left_original)
    print("右節點:", right_original)
except NetworkXError as e:
    # 預期結果:原始網路不是關聯網路
    print(f"原始 Zachary 網路不是關聯網路。錯誤訊息: {e}")

print("\n--- 轉換 Zachary 網路為關聯網路 
---
")
# 創建一個新的圖 B,用於表示轉換後的關聯網路
B = nx.Graph()

# 創建代表邊界的節點,並與原節點連接
# 為了讓 NetworkX 自動處理節點的唯一性,我們使用元組 (v, w) 來代表邊界節點
# 這裡我們需要確保邊界的表示是唯一的,例如總是 (較小節點ID, 較大節點ID)
processed_edges = set()
for v, w in G.edges():
    # 確保邊界的表示是規範化的,例如 (min(v,w), max(v,w))
    edge_node_repr = tuple(sorted((v, w)))
    if edge_node_repr not in processed_edges:
        # 添加代表邊界的節點
        # 我們將邊界節點表示為一個元組,例如 (v, w)
        # 為了讓 NetworkX 區分節點和邊界節點,我們可以給邊界節點一個特殊的標記,
        # 或者確保它們的表示方式與原始節點ID不同。
        # 一種簡單的方式是將邊界節點表示為一個包含原節點ID的元組,例如 (v, w)
        # 這裡我們直接使用 (v,w) 作為邊界節點的ID,NetworkX 會自動處理
        # 為了避免重複添加邊界節點,我們使用一個集合來記錄
        processed_edges.add(edge_node_repr)

        # 添加邊界節點
        # 這裡我們將邊界節點的ID設為元組 (v, w)
        # 為了讓 NetworkX 能夠區分原始節點和邊界節點,
        # 我們可以將邊界節點的ID設計成與原始節點ID不同。
        # 一種方法是將邊界節點ID設為一個字串,例如 f"edge_{v}_{w}"
        # 或者,更常見的做法是,將邊界節點本身作為一個新的節點,
        # 然後將原節點與之連接。
        # 在這裡,我們採用一種常見的轉換方式:
        # 創建一個節點代表邊界 (v, w),然後將 v 和 w 都連接到這個邊界節點。
        # 為了讓邊界節點ID唯一且與原始節點ID不同,我們可以使用一個特殊的前綴,
        # 或者直接使用元組 (v, w) 作為節點ID,只要保證原始節點ID不是元組即可。
        # 這裡我們直接使用元組 (v, w) 作為邊界節點的ID。
        # NetworkX 會自動處理節點的添加。
        # 為了確保邊界節點的表示唯一,我們使用 sorted((v, w))

        # 添加邊界節點 B.add_node(edge_node_repr) # 其實 add_edges_from 會自動添加節點

        # 將原始節點 v 和 w 連接到代表邊界 (v, w) 的節點
        B.add_edge(v, edge_node_repr)
        B.add_edge(w, edge_node_repr)

print(f"轉換後的新網路 B 的節點數: {B.number_of_nodes()}")
print(f"轉換後的新網路 B 的邊界數: {B.number_of_edges()}")

print("\n--- 檢查轉換後的網路 B 是否為關聯網路 
---
")
try:
    # 嘗試找到節點的兩個集合
    # 這裡的 left 和 right 將分別代表原始節點和邊界節點
    left, right = bipartite.sets(B)

    print("轉換後的網路 B 是一個關聯網路。")
    print("左節點 (原始節點):")
    # 為了更清晰地顯示,我們只打印原始節點的ID (非元組)
    original_nodes_in_left = {node for node in left if not isinstance(node, tuple)}
    print(original_nodes_in_left)

    print("\n右節點 (邊界節點):")
    # 打印邊界節點的表示 (元組)
    edge_nodes_in_right = {node for node in right if isinstance(node, tuple)}
    print(edge_nodes_in_right)

    # 驗證節點類型
    print("\n驗證節點類型:")
    for node in left:
        if isinstance(node, tuple):
            print(f"節點 {node} 應為邊界節點,但出現在左集合。")
    for node in right:
        if not isinstance(node, tuple):
            print(f"節點 {node} 應為原始節點,但出現在右集合。")

except NetworkXError as e:
    # 這應該不會發生,因為我們構造的就是一個二分圖
    print(f"轉換後的網路 B 不是關聯網路。錯誤訊息: {e}")
except Exception as e:
    print(f"檢查轉換後的網路時發生錯誤: {e}")

在上面的程式碼中,我們首先嘗試直接檢查 Zachary 網路,確認它不是一個關聯網路。然後,我們創建了一個新的圖 B,並透過遍歷原圖 G 的邊界,為每一條邊界創建一個新的節點(表示為一個元組 (v, w)),並將原節點 vw 都連接到這個新的邊界節點。最後,我們再次使用 bipartite.sets(B) 來檢查轉換後的網路 B,這次它應該被成功識別為一個關聯網路,其中一組節點是原始成員,另一組節點是代表友誼的邊界。

看圖說話:

此圖示總結了「轉換單模網路為關聯網路:以友誼網路為例」,旨在說明如何將單模網路轉換為關聯網路。流程開頭首先聚焦於「轉換單模網路為關聯網路:以友誼網路為例」,透過「分割」結構,詳細闡述了「單模網路的局限與轉換需求」(以「Zachary 網路的例子」為引,說明了「單一節點類型」、「邊界直接連接成員」,並提出了「轉換需求: 分析邊界本身作為實體」),接著探討了「轉換原理:將邊界視為節點」(闡述了「創建新節點類型」、「建立連接: 原節點 <-> 邊界節點」,並指出了「結果: 兩種節點類型的網路 (二分圖)」),並展示了「程式碼實踐:轉換 Zachary 網路」(提及了「載入 Zachary 網路 G」、「創建新圖 B」、「遍歷 G 的邊界,創建邊界節點」、「連接原節點與邊界節點」,並說明了「使用 bipartite.sets(B) 驗證」)。最後,圖示以「總結與未來方向」作結,強調了「單模轉關聯網路的實踐」、「理解轉換原理與 NetworkX 實現」,並預告了「下一章:關聯網路的投影操作」。

關聯網路的辨識與投影:從友誼到傳粉者

在前一節中,我們成功地將一個單模的友誼網路轉換為關聯網路,並驗證了其結構。本節將進一步探討關聯網路的辨識方法,並引入一個新的應用場景——傳粉者網路,作為進一步學習關聯網路操作的基礎。

關聯網路的辨識與驗證

  • bipartite.sets() 的輸出
    • 當我們成功地將一個單模網路轉換為關聯網路(如範例中的 B),bipartite.sets(B) 函數會返回兩個節點集合。
    • 觀察輸出的節點集合,我們可以清楚地看到其中一個集合包含原始節點的 ID(例如,數字 033),而另一個集合包含代表邊界的節點(以元組形式表示,例如 (5, 6))。這證實了轉換的成功,NetworkX 準確地識別出了關聯網路的兩種節點類型。
  • bipartite.is_bipartite() 函數
    • 如果我們僅僅想知道一個網路是否具有關聯網路的結構,而不需要獲取具體的節點集合,可以使用更簡潔的 bipartite.is_bipartite(G) 函數。
    • 這個函數會返回一個布林值 (TrueFalse),指示該網路是否為二分圖(即關聯網路)。這對於快速檢查網路結構非常有用。

新的應用場景:傳粉者網路

為了更深入地理解關聯網路的應用,我們將引入一個新的範例:傳粉者網路(Pollinator Networks)。

  • 網路結構
    • 傳粉者網路描述了動物物種(如蜜蜂、蝴蝶)與它們所幫助授粉的植物物種之間的關係。
    • 在這個網路中,通常有兩種節點:
      • 動物物種(例如,蜜蜂、蝴蝶)。
      • 植物物種(例如,向日葵、薰衣草)。
    • 邊界連接一個動物物種節點和一個植物物種節點,表示該動物物種會為該植物物種授粉。
  • 數據來源
    • 範例數據來自一項對西班牙加泰隆尼亞地區(Cap de Creus)某些物種的研究(Bartomeus et al., 2008)。
    • 數據以 TSV (Tab-Separated Values) 格式提供,可以輕鬆轉換為邊列表(edgelist)格式,但範例程式碼將直接從原始 TSV 文件解析數據。
  • 處理不連通節點
    • 在實際數據處理中,網路可能包含一些與主體網路不連通的孤立節點。
    • 範例程式碼將使用 connected_components() 函數(將在後續章節「In-Between: Communities」中詳細介紹)來忽略這些不連通的節點,專注於分析網路的主要結構。

直接解析 TSV 數據

範例程式碼展示了如何直接從 TSV 文件讀取傳粉者網路的數據,並將其載入到 NetworkX 的圖物件中。

import networkx as nx
from networkx.algorithms import bipartite
from pathlib import Path
import csv # 引入 csv 模組以處理 TSV

# 創建數據目錄路徑 (假設 'data' 目錄存在於當前路徑下)
data_dir = Path('.') / 'data'
bartomeus_file = data_dir / 'bartomeus2008' / 'Bartomeus_Ntw_nceas.txt'

# 創建一個空的圖物件,用於儲存傳粉者網路
B_pollinator = nx.Graph()

print(f"\n--- 載入傳粉者網路數據自: {bartomeus_file} 
---
")
try:
    with open(bartomeus_file, 'r', encoding='utf-8') as f:
        # 使用 csv.reader 處理 TSV 文件,指定分隔符為 tab
        reader = csv.reader(f, delimiter='\t')

        # 假設第一行是標頭,我們需要跳過它
        # 實際數據格式可能不同,需要根據文件內容調整
        header = next(reader)
        print(f"跳過標頭: {header}")

        # 遍歷文件的每一行
        for row in reader:
            # 假設 TSV 文件包含兩列:物種1, 物種2
            # 這裡我們假設第一列是動物物種,第二列是植物物種
            # 實際情況需要根據文件內容確定節點類型
            if len(row) == 2:
                node1 = row[0].strip()
                node2 = row[1].strip()

                # 為了確保 NetworkX 能夠區分節點類型,
                # 我們可以為節點添加屬性,或者在創建圖時就明確節點類型。
                # 在這裡,我們直接添加邊界,NetworkX 會自動創建節點。
                # 後續可以通過檢查節點屬性或使用 bipartite.sets() 來區分類型。
                B_pollinator.add_edge(node1, node2)
            else:
                print(f"警告: 跳過格式不正確的行: {row}")

    print("傳粉者網路數據載入完成。")
    print(f"節點數量: {B_pollinator.number_of_nodes()}")
    print(f"邊界數量: {B_pollinator.number_of_edges()}")

    # 這裡可以選擇性地使用 connected_components() 來處理不連通的節點
    # 例如,獲取最大的連通分量
    # from networkx.algorithms.components import connected_components
    # largest_cc = max(connected_components(B_pollinator), key=len)
    # B_pollinator_lcc = B_pollinator.subgraph(largest_cc)
    # print(f"最大連通分量節點數: {B_pollinator_lcc.number_of_nodes()}")

    # 接下來可以對 B_pollinator (或其子圖) 進行關聯網路的檢查和分析

except FileNotFoundError:
    print(f"錯誤:找不到檔案 '{bartomeus_file}'。請確保檔案存在於正確的路徑。")
except Exception as e:
    print(f"載入傳粉者網路數據時發生錯誤: {e}")

這個範例展示了如何讀取外部數據文件,並將其轉換為 NetworkX 的圖結構。接下來,我們就可以對這個傳粉者網路進行關聯網路的分析。

看圖說話:

此圖示總結了「關聯網路的辨識與投影:從友誼到傳粉者」,旨在探討關聯網路的辨識方法,並引入傳粉者網路作為新的應用範例。流程開頭首先聚焦於「關聯網路的辨識與投影:從友誼到傳粉者」,透過「分割」結構,詳細闡述了「關聯網路的辨識與驗證」(說明了「bipartite.sets() 輸出解釋」、「bipartite.is_bipartite() 函數」,並提及了「快速檢查網路結構」),接著探討了「新的應用場景:傳粉者網路」(介紹了「網路結構: 動物物種 <-> 植物物種」、「數據來源: Bartomeus et al. (2008) 研究」,並提及了「處理不連通節點」),並展示了「直接解析 TSV 數據」(說明了「讀取 TSV 文件」、「添加邊界,NetworkX 自動創建節點」,以及「後續分析關聯網路結構」)。最後,圖示以「總結與未來方向」作結,強調了「關聯網路辨識與數據載入」、「傳粉者網路範例」,並預告了「下一章:關聯網路的投影操作」。

深入剖析將單模網路轉換為關聯網路的技術路徑後,我們得以洞見一種更具深度的結構分析思維。此方法的核心價值,在於將抽象的「關係」實體化為可分析的「節點」,無論是從既有友誼網路中人工建構(如 Zachary 俱樂部),或是直接解析原生二分結構的數據(如傳粉者生態系),皆展現了其高度的應用彈性。這種視角轉換不僅是數據結構的重塑,更是分析思維的躍升,它使我們能超越「誰與誰連結」的表層問題,進而探問「哪些關係是關鍵」以及「不同群體如何透過特定關係類型產生連結」。

展望未來,這種將邊界節點化的能力,預示著網路分析將更專注於多層次、多類型的複雜互動系統。關聯網路本身往往只是中間步驟,其真正的分析潛力,將在後續的「投影」(Projection)操作中被完全釋放。玄貓認為,對於追求深度洞察的分析者而言,熟練掌握從單模到關聯的轉換,並進一步利用投影從中提煉新知,已是從描述性分析邁向推斷性與結構性洞察的關鍵能力。