在複雜網路分析中,有效的視覺化是洞察資料結構與關係的關鍵第一步。然而,標準的自動化佈局演算法,如隨機或力導向佈局,在面對具有清晰社群結構的網路時,往往難以生成直觀且易於解讀的圖譜。邊的過度交叉與節點的混亂分佈,時常掩蓋了重要的群體模式。為解決此問題,分析師必須採用更具策略性的佈局方法。本文將從優化現有佈局著手,探討如何將社群偵測的分析結果回饋至視覺化流程,透過對節點的策略性重排,顯著提升圓形佈局的清晰度。此外,我們將深入另一種結構化佈局——殼層佈局,解析其在呈現節點階層與中心性方面的獨特價值,並展示如何透過結合社群資訊,使其應用更具彈性與洞察力。

優化網路視覺化:社群導向佈局與殼層佈局的應用

本章節將延續對網路視覺化優化方法的探討,重點在於如何透過社群導向的節點重排來提升圓形佈局的效果,並介紹另一種常用的佈局方式——殼層佈局 (Shell Layout)。我們將展示如何利用社群偵測結果來指導節點在圓形佈局中的排列,進而獲得更清晰的社群結構視覺呈現。隨後,我們將深入解析殼層佈局的原理、優勢與局限性,並展示如何結合社群資訊來應用 NetworkX 的 shell_layout() 函數,以更有效地視覺化網路結構,特別是節點的中心性。

社群導向的圓形佈局優化

  • 核心目標:進一步提升圓形佈局在視覺化社群結構方面的能力,減少邊的交叉。
  • 程式碼解析 (community_net 函數)
    • 社群偵測communities = nxcom.greedy_modularity_communities(G_in)。此處沿用了前文提到的基於模組度優化的貪婪演算法(如 Louvain 演算法),用於識別網路中的社群劃分。
    • 節點處理與顏色映射
      • G_out = nx.Graph():創建一個新的圖對象,用於儲存重排後的節點和邊。
      • node_color = []:一個列表,用於儲存每個節點的顏色資訊。
      • node_community = {}:一個字典,用於記錄每個節點所屬的社群索引。
      • for i, com in enumerate(communities)::遍歷所有偵測到的社群。i 是社群的索引(從 0 開始)。
      • for v in com::遍歷當前社群 com 中的所有節點 v
      • G_out.add_node(v):將節點 v 添加到新的圖 G_out 中。
      • node_color.append(get_color(i)):為節點 v 分配一個顏色。這裡調用了 get_color(i) 函數(假設該函數在第 7 章中定義,用於根據社群索引 i 返回一個顏色值),這樣同一社群的節點將具有相同的顏色。
      • node_community[v] = i:記錄節點 v 所屬的社群索引 i
    • 邊的複製G_out.add_edges_from(G_in.edges()):將原始圖 G_in 中的所有邊複製到新的圖 G_out 中。
  • 應用與視覺化
    • node_color, node_community, G = community_net(G_karate):調用 community_net 函數處理 Zachary 空手道俱樂部網路,獲取節點顏色列表、節點社群映射以及重排後的圖 G
    • nx.draw_networkx(G, pos=nx.circular_layout(G), node_color=node_color)
      • 使用 nx.circular_layout(G) 生成佈局。注意:這裡的程式碼片段仍然是使用預設的 circular_layout,它可能不會直接利用 community_net 函數中節點的社群資訊來優化佈局順序。更進一步的優化需要手動根據社群資訊來排序節點,然後再計算圓形佈局的位置。
      • node_color=node_color:將根據社群分配的顏色傳遞給繪圖函數,使得不同社群的節點顯示為不同顏色。
  • 視覺化效果
    • 相較於未經排序的圓形佈局,這種方法「產生了一個稍微不那麼擁擠的視覺化,具有更清晰的社群結構」。透過為不同社群的節點賦予不同顏色,以及(理想情況下)將同一社群的節點在圓周上分組排列,可以更容易地識別出網路中的社群劃分。

殼層佈局 (Shell Layout)

  • 基本概念
    • 殼層佈局將節點放置在「同心圓」(concentric circles) 上。
  • 優點
    • 空間利用率:相較於單純的圓形佈局,它能在相同的空間內「容納更多節點」,因為節點被分佈在多個圓層上。
    • 中心性傳達:可以將「更中心的節點放置在更靠近中心的位置」,從而傳達節點的中心性資訊。
  • 局限性
    • 「仍然不能很好地捕捉社群結構」,並且「可能obscure some edges」(掩蓋一些邊)。
    • 儘管比單純的圓形佈局更靈活,但它仍然不是專門為清晰展示社群結構而設計的。
  • NetworkX 實現
    • nx.shell_layout(G, nlist=None, center=None, scale=1.0) 函數。
    • nlist 參數是一個列表的列表,其中每個內部列表代表一個「殼層」(shell),並包含該殼層上的節點。這允許使用者精確控制哪些節點位於哪個圓層上。
    • 如果 nlist 未提供,shell_layout 會嘗試根據節點的度數或其他屬性自動分層。

應用殼層佈局:結合社群偵測

  • 目標:利用社群偵測結果來指導節點在殼層佈局中的分佈,以期獲得更好的視覺效果。
  • 程式碼解析
    • 節點度數排序
      • degrees = dict(G.degree()):獲取網路中所有節點的度數。
      • labels = sorted(degrees.keys(), key=lambda x: degrees[x], reverse=True):根據節點的度數(從高到低)對節點進行排序。這通常用於將度數較高的節點(潛在的中心節點)放置在更內層的殼層。
    • 構建殼層列表 (nlist)
      • nlist = []:初始化用於儲存殼層節點列表的列表。
      • i, k = 0, 6i 是當前處理的節點索引,k 代表每個殼層的節點數量(或目標數量)。
      • while i < len(labels)::循環直到處理完所有節點。
      • 內部邏輯(程式碼截斷):這部分程式碼的目的是根據節點的度數排序(labels)和預設的殼層大小(k),將節點分組放入不同的殼層。例如,度數最高的 k 個節點可能構成第一層(最內層),接下來的 k 個節點構成第二層,以此類推。
    • 結合社群資訊(預期):雖然程式碼片段主要展示了基於度數的排序,但更進階的做法是結合社群偵測結果。可以讓每個社群的節點盡可能地集中在某一層或連續的幾層中,或者將度數高的節點(可能跨社群)放在內層,度數低的節點放在外層。
    • 調用 shell_layout
      • pos = nx.shell_layout(G, nlist=constructed_nlist):使用構建好的 nlist(節點分層結構)來調用 shell_layout,生成節點位置。
    • 繪製:最後,使用生成的 pos 進行繪圖。
import networkx as nx
import matplotlib.pyplot as plt
import numpy as np
import community as nxcom # 假設 community 模組已安裝 (pip install python-louvain)
from pathlib import Path
import pandas as pd

# --- 載入 Zachary 空手道俱樂部網路 
---
try:
    G_karate = nx.karate_club_graph()
    print(f"成功載入 Zachary 空手道俱樂部網路: {G_karate.number_of_nodes()} 個節點, {G_karate.number_of_edges()} 條邊。")
except Exception as e:
    print(f"載入 Zachary 空手道俱樂部網路時發生錯誤: {e}")
    G_karate = nx.Graph()

# --- 社群導向的圓形佈局函數 (接續前文) 
---
# 假設 get_color 函數已定義,這裡我們模擬一個
def get_color(community_index, num_communities=4): # 假設最多4個社群
    cmap = plt.get_cmap('viridis', num_communities)
    return cmap(community_index % num_communities)

def community_net_optimized(G_in):
    """
    識別社群,並準備節點顏色和社群映射,用於視覺化。
    返回: node_color 列表, node_community 字典, G_out (節點列表)
    """
    if G_in.number_of_nodes() == 0:
        return [], {}, []

    node_color_list = []
    node_community_map = {}
    
    try:
        communities = list(nxcom.greedy_modularity_communities(G_in))
        num_communities = len(communities)
        print(f"偵測到 {num_communities} 個社群。")
    except Exception as e:
        print(f"計算社群時發生錯誤: {e}")
        communities = [[node] for node in G_in.nodes()] # 退化為每個節點一個社群
        num_communities = len(communities)

    # 創建節點到社群索引的映射,並準備顏色列表
    for i, community in enumerate(communities):
        for node in community:
            node_community_map[node] = i
            node_color_list.append(get_color(i, num_communities)) # 獲取顏色

    # 為了讓 circular_layout 根據社群順序排列,我們需要一個排序後的節點列表
    # 將節點按社群索引排序,然後在社群內部按節點 ID 排序
    sorted_nodes_by_community = []
    for i in range(num_communities):
        community_nodes = sorted([node for node, comm_idx in node_community_map.items() if comm_idx == i])
        sorted_nodes_by_community.extend(community_nodes)

    # 確保所有節點都被包含,即使是孤立節點
    all_nodes = list(G_in.nodes())
    if len(sorted_nodes_by_community) < len(all_nodes):
        remaining_nodes = sorted([node for node in all_nodes if node not in node_community_map])
        sorted_nodes_by_community.extend(remaining_nodes)
        # 為這些節點分配一個預設社群索引(例如 -1 或 num_communities)
        for node in remaining_nodes:
            node_community_map[node] = num_communities # 或 -1

    # 為了讓 nx.draw_networkx 使用這個排序,我們需要一個自訂佈局
    # 這裡我們返回節點列表和顏色,讓外部繪圖函數處理佈局
    return node_color_list, node_community_map, sorted_nodes_by_community # 返回排序後的節點列表

# --- 繪製社群導向的圓形佈局 (使用排序後的節點) 
---
def draw_optimized_circular_layout(G_in, title="Optimized Circular Layout by Community"):
    if G_in.number_of_nodes() == 0:
        print("圖為空,無法繪製優化圓形佈局。")
        return

    node_colors, node_community_map, sorted_nodes = community_net_optimized(G_in)
    
    # 創建一個基於 sorted_nodes 的自訂佈局
    # 這裡我們使用 nx.circular_layout,但確保節點順序是我們想要的
    # NetworkX 的 circular_layout 函數實際上會根據 G.nodes() 的順序來排列
    # 所以我們需要一個方法來傳遞排序後的節點順序
    
    # 一種方法是創建一個新的圖,節點順序是我們想要的,然後應用 circular_layout
    G_ordered = nx.Graph()
    G_ordered.add_nodes_from(sorted_nodes)
    G_ordered.add_edges_from(G_in.edges()) # 添加邊

    pos = nx.circular_layout(G_ordered) # 在排序後的圖上生成佈局

    plt.figure(figsize=(8, 8))
    nx.draw_networkx(
        G_in, # 使用原始圖 G_in 繪製,但 pos 是基於 G_ordered 的
        pos=pos, 
        with_labels=True, 
        node_size=300, 
        node_color=node_colors, # 使用社群顏色
        edge_color='gray', 
        alpha=0.7, 
        width=0.5
    )
    plt.title(title, fontsize=16)
    plt.axis('off')
    plt.show()

print("\n--- 社群導向的圓形佈局 
---
")
draw_optimized_circular_layout(G_karate, title="Zachary Karate Club: Optimized Circular Layout")

# --- 殼層佈局 (Shell Layout) 
---
print("\n--- 殼層佈局 (Shell Layout) 
---
")

def draw_shell_layout(G, title="Shell Layout"):
    """
    繪製網路的殼層佈局。
    """
    if G.number_of_nodes() == 0:
        print("圖為空,無法繪製殼層佈局。")
        return

    # --- 結合社群偵測來構建殼層 
---
    try:
        communities = list(nxcom.greedy_modularity_communities(G))
        num_communities = len(communities)
        
        # 創建 nlist:將每個社群的節點放入一個殼層
        nlist = []
        for community in communities:
            # 對社群內的節點進行排序,以確保佈局的可重複性
            nlist.append(sorted(list(community))) 
        
        # 如果節點數量不足以填滿多個殼層,可以調整 nlist 的結構
        # 例如,如果社群數量少於預期的殼層數,可以將較大的社群拆分或合併
        # 這裡為了簡化,我們假設每個社群就是一個殼層
        
        # 確保所有節點都被包含,即使是孤立節點
        all_nodes_in_shells = [node for shell in nlist for node in shell]
        all_graph_nodes = list(G.nodes())
        if len(all_nodes_in_shells) < len(all_graph_nodes):
            isolated_nodes = sorted([node for node in all_graph_nodes if node not in all_nodes_in_shells])
            if isolated_nodes:
                nlist.append(isolated_nodes) # 將孤立節點放在最後一個殼層
                print(f"將 {len(isolated_nodes)} 個孤立節點添加到最後一個殼層。")
        
        print(f"使用 {len(nlist)} 個殼層進行佈局。")
        
        pos = nx.shell_layout(G, nlist=nlist)
        
    except Exception as e:
        print(f"構建殼層或計算 shell_layout 時發生錯誤: {e}")
        print("退回到預設的 shell_layout。")
        pos = nx.shell_layout(G) # 退回到預設佈局

    # --- 繪製 
---
    plt.figure(figsize=(8, 8))
    
    # 為了讓殼層佈局更清晰,可以考慮根據社群給節點上色
    node_colors = []
    community_map = {}
    for i, shell in enumerate(nlist):
        for node in shell:
            community_map[node] = i
    
    num_shells = len(nlist)
    cmap = plt.get_cmap('tab10', num_shells) # 使用不同的顏色映射
    for node in G.nodes():
        community_idx = community_map.get(node, -1) # 獲取節點所屬殼層/社群索引
        if community_idx != -1:
            node_colors.append(cmap(community_idx % num_shells))
        else:
            node_colors.append('gray') # 孤立節點或未分類節點

    nx.draw_networkx(
        G, 
        pos=pos, 
        with_labels=True, 
        node_size=300, 
        node_color=node_colors, # 使用根據殼層/社群的顏色
        edge_color='gray', 
        alpha=0.7, 
        width=0.5
    )
    plt.title(title, fontsize=16)
    plt.axis('off')
    plt.show()

# 繪製結合社群的殼層佈局
draw_shell_layout(G_karate, title="Zachary Karate Club: Shell Layout by Community")

print("\n--- 殼層佈局的優勢與局限 
---
")
print("優勢: 空間利用率高,可容納更多節點;中心節點可放置於內層,傳達中心性資訊。")
print("局限: 對社群結構的捕捉不如專門的社群佈局;可能掩蓋部分邊。")
print("結合社群偵測有助於將相關節點分組到相似的殼層,提升視覺效果。")
@startuml
!define DISABLE_LINK
!define PLANTUML_FORMAT svg
!theme _none_

skinparam dpi auto
skinparam shadowing false
skinparam linetype ortho
skinparam roundcorner 5
skinparam defaultFontName "Microsoft JhengHei UI"
skinparam defaultFontSize 16
skinparam minClassWidth 100

start

:優化網路視覺化:社群導向佈局與殼層佈局的應用;:社群導向的圓形佈局優化;
note right
核心目標: 提升圓形佈局對社群結構的視覺化能力
程式碼解析 (community_net):
  - 社群偵測: nxcom.greedy_modularity_communities
  - 節點處理與顏色映射:
    - 創建新圖 G_out
    - node_color 列表 (基於社群顏色)
    - node_community 字典 (節點->社群索引)
  - 邊的複製: G_out.add_edges_from(G_in.edges())
應用與視覺化:
  - 調用 community_net 獲取顏色、映射、排序節點
  - 使用 nx.circular_layout(G_ordered) 繪製
  - node_color 參數賦予社群顏色
視覺化效果:
  - 減少擁擠感
  - 更清晰的社群結構
end note

:殼層佈局 (Shell Layout);
note right
基本概念: 節點放置於同心圓上
優點:
  - 空間利用率高 (容納更多節點)
  - 中心性傳達 (中心節點靠近中心)
局限:
  - 難捕捉社群結構
  - 可能掩蓋邊
NetworkX 實現: nx.shell_layout()
  - nlist 參數: 控制節點分層
end note

:應用殼層佈局:結合社群偵測;
note right
目標: 利用社群指導節點分佈
程式碼解析:
  - 節點度數排序 (獲取中心節點)
  - 構建殼層列表 (nlist):
    - 基於社群劃分
    - 結合度數排序 (預期)
  - 調用 nx.shell_layout(G, nlist=constructed_nlist)
繪製:
  - 使用自訂 pos 進行繪圖
  - 節點上色 (根據社群或殼層)
end note

stop

@enduml

看圖說話:

此圖示總結了「優化網路視覺化:社群導向佈局與殼層佈局的應用」的內容,重點在於介紹如何透過社群偵測來優化圓形佈局,以及解析殼層佈局的原理與結合社群資訊的應用方法。流程開頭首先聚焦於「社群導向的圓形佈局優化」,說明了如何利用社群偵測結果來為節點分配顏色,並透過排序節點來提升圓形佈局的視覺效果,接著詳細闡述了「殼層佈局 (Shell Layout)」的基本概念、優點與局限性,並重點介紹了「應用殼層佈局:結合社群偵測」的策略,展示了如何根據社群劃分來構建節點分層結構,進而應用 shell_layout 以獲得更具洞察力的視覺化結果。

縱觀現代管理者的多元挑戰,從社群導向的圓形佈局到結合屬性資訊的殼層佈局,其核心價值已清晰可見,關鍵在於將抽象的數據分析轉化為直觀的策略洞察。傳統佈局方法僅提供網路結構的基礎樣貌,而社群導向的優化與殼層佈局的應用,則代表了從「被動呈現」到「主動敘事」的思維躍遷。前者犧牲了絕對的幾何對稱,換取了社群結構的清晰度;後者則在空間利用與中心性表達上取得平衡,但需謹慎處理其對社群邊界的模糊化。這兩者之間的取捨,恰恰反映了數據分析師在面對複雜問題時,必須依據特定商業目標來選擇最適視覺化策略的專業判斷力。

我們預見,未來的網路視覺化將不再滿足於單一維度的優化,而是朝向「語意化佈局」發展,即融合社群、中心性、節點屬性等多重資訊,動態建構出能完整呈現商業故事的視覺框架。

對於追求卓越的數據分析師與管理者而言,玄貓認為,真正的專業壁壘並非掌握多少種佈局演算法,而是深刻理解每種佈局背後的邏輯與取捨,並能將分析洞察精準地「編碼」至視覺空間中,從而引導決策、創造價值。