在商業策略與系統分析中,網路結構並非恆定不變,而是隨著時間動態演進。無論是市場競爭關係、組織內部協作,或是數位平台的用戶互動,其連結的生成與消逝皆蘊含著重要的策略資訊。為了捕捉並分析這些變化,我們需要將連續的時間流離散化為一系列可供分析的靜態橫截面,即「網路快照」。本篇文章將聚焦於實現此過程的關鍵技術,詳細闡述如何從帶有時間戳的邊緣資料中,精準提取特定時刻的網路拓撲結構。此方法不僅是時序視覺化的基礎,更是進一步應用動態中心性計算、社群偵測演化等進階分析的必要前置步驟,為理解複雜系統的演變規律提供了堅實的量化基礎。

網路快照生成與時序演化視覺化

本章節將延續對動態網路的探討,重點在於如何利用提供的時間數據,實際生成特定時間點的網路「快照」。我們將詳細解析 get_snapshot 函數的運作機制,理解它如何將日期轉換為時間戳,並篩選出在該時間點存在的邊。隨後,我們將展示如何運用此函數,生成一系列時間點的網路快照,並為後續的視覺化分析奠定基礎,以觀察網路結構在六個月內的演變。

快照生成函數解析

  • 函數功能
    • get_snapshot(G, date) 函數的核心目的是從一個包含時間屬性的圖 G(在此案例中為 G_wiki,一個 MultiGraph)中,提取出在指定日期 date 時存在的邊,並構建一個新的、僅包含這些邊的 Graph 對象。
  • 日期轉換與時間戳
    • dt = datetime.datetime.strptime(date, '%Y-%m-%d')
      • 使用 Python 的 datetime 模組,將輸入的日期字符串(格式為 ‘YYYY-MM-DD’)解析成一個 datetime 對象。
    • timestamp = time.mktime(dt.timetuple())
      • datetime 對象轉換為時間元組 (timetuple())。
      • time.mktime() 函數將這個時間元組轉換為一個 Unix 時間戳。Unix 時間戳是自 1970 年 1 月 1 日午夜(UTC)以來經過的秒數,這是一種標準化的數值表示,便於進行比較。
  • 篩選邊
    • snapshot_edges = []:初始化一個空列表,用於儲存符合條件的邊。
    • for e in G.edges::遍歷圖 G 中的每一條邊。
    • if G.edges[e]['begin'] <= timestamp and G_wiki.edges[e]['end'] >= timestamp:
      • 這是篩選邊的關鍵條件。對於圖 G 中的每一條邊 e,它必須滿足以下兩個條件:
        1. G.edges[e]['begin'] <= timestamp:邊的「開始時間戳」必須早於或等於目標時間戳。
        2. G_wiki.edges[e]['end'] >= timestamp:邊的「結束時間戳」必須晚於或等於目標時間戳。
      • 注意:這裡的程式碼片段有一個小錯誤,G_wiki.edges[e]['end'] 應該是 G.edges[e]['end'],因為函數的輸入是 G。假設這裡是指向 G 的屬性。
      • 邏輯:這兩個條件共同確保了在給定的 timestamp 時刻,該邊是「活躍」或「存在」的。
    • snapshot_edges.append(e):如果邊滿足條件,則將其添加到 snapshot_edges 列表中。
  • 構建快照圖
    • return nx.Graph(G.edge_subgraph(snapshot_edges))
      • G.edge_subgraph(snapshot_edges):NetworkX 的 edge_subgraph() 方法會創建一個新的圖,其中包含 snapshot_edges 中的所有邊,以及這些邊所連接的所有節點。
      • nx.Graph(...):將返回的子圖(通常是 GraphMultiGraph,取決於 G 的類型)轉換為一個標準的 nx.Graph 對象(無向圖,每對節點最多一條邊)。這一步驟可能會丟失原始 MultiGraph 中關於多重邊的資訊,如果時間數據非常密集且需要區分同一時間點不同屬性的邊,可能需要保留 MultiGraph 類型。
      • 返回值:函數返回一個新的 nx.Graph 對象,代表了在指定日期 date 時的網路快照。

視覺化時序演化的準備

  • 定義視覺化時間點
    • dates = [...]:一個列表,其中包含了一系列需要生成快照並進行視覺化的日期字符串。
    • 這些日期涵蓋了從 2001 年 10 月初到 11 月初的六週時間。透過觀察這段時間內的網路變化,可以初步了解網路的演變趨勢。
  • 後續步驟的預期
    • 程式碼片段結束於定義 dates 列表。
    • 接下來的步驟將會:
      1. 遍歷 dates 列表中的每一個日期。
      2. 對每個日期調用 get_snapshot(G_wiki, date) 函數,生成對應的網路快照。
      3. 對每個快照進行視覺化(可能使用與之前類似的地理佈局和邊屬性視覺化方法)。
      4. 比較不同時間點的視覺化結果,以觀察網路結構(節點、邊、連接模式)的動態變化。

程式碼實現細節

  • 模組導入
    • import datetime:用於日期解析。
    • import time:用於時間戳轉換。
  • get_snapshot 函數
    • 接收圖 G 和日期字符串 date 作為輸入。
    • 內部進行日期解析、時間戳轉換。
    • 迭代邊,根據 beginend 時間戳篩選邊。
    • 使用 edge_subgraph 創建快照圖並返回。
  • 視覺化時間點定義
    • dates 列表提供了視覺化分析的離散時間點。
import networkx as nx
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from pathlib import Path
import datetime
import time
import math # 確保 math 模組已導入,儘管在此函數中未使用

# --- 假設 G_wiki 已載入並處理好邊的 'begin' 和 'end' 屬性 ---
# 為了程式碼的獨立性,這裡模擬創建一個 MultiGraph
# 實際應用中,應接續前文的 G_wiki 載入

# --- 模擬創建 G_wiki MultiGraph ---
data_dir = Path('./data_simulated') # 假設數據目錄存在
ligtenberg_dir = data_dir / 'ligtenberg2017'
ligtenberg_dir.mkdir(parents=True, exist_ok=True)
wikilinks_csv_path = ligtenberg_dir / 'wikilinks.csv'

# 模擬創建 wikilinks.csv
if not wikilinks_csv_path.exists():
    print("創建模擬 wikilinks.csv...")
    mock_edges_data = []
    # 模擬一些節點和邊,涵蓋不同的時間段
    # 邊格式: node1,node2,begin,end
    # Unix timestamp 示例: 2001-10-01 00:00:00 -> 1001865600
    # 2001-11-06 00:00:00 -> 1005004800
    
    # 邊 1: 存在於整個時間段
    mock_edges_data.append({'node1': 'A', 'node2': 'B', 'begin': 1001865600, 'end': 1005004800})
    # 邊 2: 在中期出現
    mock_edges_data.append({'node1': 'B', 'node2': 'C', 'begin': 1002556800, 'end': 1005004800}) # 2001-10-08
    # 邊 3: 在後期出現
    mock_edges_data.append({'node1': 'A', 'node2': 'C', 'begin': 1002730000, 'end': 1005004800}) # 2001-10-10
    # 邊 4: 在早期消失
    mock_edges_data.append({'node1': 'A', 'node2': 'D', 'begin': 1001865600, 'end': 1002556800}) # 消失於 2001-10-08
    # 邊 5: 存在於整個時間段,但可能與其他邊形成三角形
    mock_edges_data.append({'node1': 'C', 'node2': 'D', 'begin': 1001865600, 'end': 1005004800})
    # 邊 6: 僅存在於後期
    mock_edges_data.append({'node1': 'B', 'node2': 'D', 'begin': 1003500000, 'end': 1005004800}) # 2001-10-21

    df_mock_edges = pd.DataFrame(mock_edges_data)
    # NetworkX read_edgelist 預期格式是 node1,node2,attr1,attr2,...
    # 所以我們需要將 begin 和 end 寫入 CSV,並確保順序正確
    df_mock_edges.to_csv(wikilinks_csv_path, index=False, header=['node1', 'node2', 'begin', 'end'])
    print("模擬 wikilinks.csv 創建完成。")

# 載入 MultiGraph
try:
    G_wiki = nx.read_edgelist(
        str(wikilinks_csv_path), # 確保路徑是字串
        data=[('begin', int), ('end', int)],
        create_using=nx.MultiGraph() # 確保使用 MultiGraph
    )
    print(f"成功載入 G_wiki MultiGraph: {G_wiki.number_of_nodes()} 個節點, {G_wiki.number_of_edges()} 條邊。")
except FileNotFoundError:
    print(f"錯誤:找不到模擬數據文件 {wikilinks_csv_path}")
    G_wiki = nx.MultiGraph() # 創建一個空圖以避免後續錯誤
except Exception as e:
    print(f"載入 G_wiki 時發生錯誤: {e}")

# --- 快照生成函數 ---
def get_snapshot(G, date_str):
    """
    從 MultiGraph 中提取指定日期的網路快照。
    G: 包含 'begin' 和 'end' 時間戳的 MultiGraph。
    date_str: 'YYYY-MM-DD' 格式的日期字符串。
    返回: 在該日期存在的邊構成的 nx.Graph 快照。
    """
    try:
        dt = datetime.datetime.strptime(date_str, '%Y-%m-%d')
        # 使用 UTC 時間戳以避免時區問題,儘管 mktime 默認使用本地時間
        # 更穩健的方式是使用 pytz 或 datetime.timezone.utc
        # 這裡為簡化,假設本地時間與數據一致或影響不大
        timestamp = time.mktime(dt.timetuple()) 
        
        snapshot_edges_data = [] # 儲存 (u, v, key) 以便構建子圖
        
        # 遍歷 MultiGraph 中的所有邊及其鍵 (key)
        for u, v, key, data in G.edges(keys=True, data=True):
            # 檢查邊是否存在於給定時間戳
            # 確保 'begin' 和 'end' 屬性存在且為數字
            if 'begin' in data and 'end' in data and \
               isinstance(data['begin'], (int, float)) and \
               isinstance(data['end'], (int, float)):
                
                if data['begin'] <= timestamp and data['end'] >= timestamp:
                    snapshot_edges_data.append((u, v, key)) # 儲存邊的 (u, v, key)
            else:
                # 如果邊屬性不完整,可以選擇忽略或記錄警告
                pass 

        # 使用 edge_subgraph 創建包含這些邊的子圖
        # 注意: edge_subgraph 返回的圖類型與原圖一致 (MultiGraph)
        # 如果需要標準 Graph,需要進一步轉換
        snapshot_G = G.edge_subgraph(snapshot_edges_data)
        
        # 為了簡化,我們返回一個標準的 Graph,假設不需要處理同一時間點的多重邊
        # 如果需要保留多重邊信息,則返回 snapshot_G (MultiGraph)
        return nx.Graph(snapshot_G) 

    except ValueError as ve:
        print(f"日期格式錯誤或無效日期 '{date_str}': {ve}")
        return nx.Graph() # 返回空圖
    except KeyError as ke:
        print(f"處理邊時缺少屬性: {ke}。請確保邊具有 'begin' 和 'end' 屬性。")
        return nx.Graph() # 返回空圖
    except Exception as e:
        print(f"生成快照時發生未知錯誤: {e}")
        return nx.Graph() # 返回空圖

# --- 定義需要視覺化的日期 ---
dates_to_visualize = [
    '2001-10-01',
    '2001-10-08',
    '2001-10-15',
    '2001-10-22',
    '2001-10-29',
    '2001-11-06',
]

print("\n已定義需要生成快照的日期列表。")
print("接下來將調用 get_snapshot 函數為每個日期生成網路快照。")

# --- 示例:生成並顯示第一個快照的節點和邊數量 ---
if G_wiki.number_of_edges() > 0:
    first_date = dates_to_visualize[0]
    snapshot_0 = get_snapshot(G_wiki, first_date)
    print(f"\n快照 '{first_date}':")
    print(f"  - 節點數量: {snapshot_0.number_of_nodes()}")
    print(f"  - 邊數量: {snapshot_0.number_of_edges()}")
else:
    print("\nG_wiki 圖為空,無法生成快照。")

# --- 後續步驟預期 ---
# 1. 遍歷 dates_to_visualize 列表
# 2. 對每個日期調用 get_snapshot 函數生成快照
# 3. (可選) 儲存這些快照,或直接進行視覺化
# 4. 視覺化每個快照,觀察網路結構隨時間的變化
@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
函數: get_snapshot(G, date_str)
  - 輸入: 
    - G: 包含時間屬性的 MultiGraph
    - date_str: 'YYYY-MM-DD' 日期字符串
  - 核心步驟:
    1. 日期解析與時間戳轉換:
       - datetime.datetime.strptime()
       - time.mktime() -> Unix timestamp
    2. 篩選邊:
       - 遍歷 G.edges
       - 條件: G.edges[e]['begin'] <= timestamp AND G.edges[e]['end'] >= timestamp
       - 收集符合條件的邊 (u, v, key)
    3. 構建快照圖:
       - G.edge_subgraph(snapshot_edges_data)
       - 轉換為 nx.Graph (簡化處理)
  - 返回: 
    - 代表該時間點網路狀態的 nx.Graph 快照
錯誤處理:
  - 日期格式錯誤 (ValueError)
  - 屬性缺失 (KeyError)
  - 其他未知錯誤
end note

:視覺化時序演化的準備;
note right
定義視覺化時間點:
  - dates_to_visualize 列表
  - 包含多個日期字符串
  - 涵蓋一段時間範圍 (例如六個月)
後續步驟預期:
  - 遍歷日期列表
  - 調用 get_snapshot 生成快照
  - 視覺化每個快照
  - 比較不同時間點的網路變化
end note

stop

@enduml

看圖說話:

此圖示總結了「網路快照生成與時序演化視覺化」的內容,重點在於解析 get_snapshot 函數如何從一個動態網路數據中提取特定時間點的網路狀態,並為後續的時序分析和視覺化做準備。流程開頭首先聚焦於「快照生成函數解析」,詳細說明了函數如何將日期轉換為時間戳,並根據邊的起始和結束時間戳來篩選出在該時間點活躍的邊,最終構建出一個標準的網路快照圖,接著概述了「視覺化時序演化的準備」,說明了如何定義一系列需要分析的日期,並預期了後續將如何利用這些快照來觀察網路結構隨時間的演變。

結論

縱觀現代管理者的多元挑戰,將抽象的「時間流」轉化為一系列可觸摸、可分析的離散「狀態」,是從複雜數據中提煉決策智慧的關鍵一步。get_snapshot 函數所代表的快照生成技術,其核心價值不僅是程式碼的實現,更是分析思維從靜態拓樸到動態演化的典範轉移。相較於一次性的全域網路分析,此方法雖犧牲了時間的絕對連續性,卻換來了在關鍵時刻進行深度剖析的精準度與效能。技術上,透過時間戳精準篩選活躍關係,是將此分析哲學落地的核心執行步驟,而快照「時間粒度」的選擇,則是一項考驗分析者對成本與洞察之間進行策略性權衡的修煉。

我們預見,未來的分析重點將從單純「生成快照」進階到「量化快照序列」,例如追蹤關鍵節點影響力的興衰,或偵測社群結構的突變點。這不僅是視覺化呈現,更是通往預測性網路分析的基石。

玄貓認為,對於致力於從數據中挖掘深層敘事的管理者與分析師而言,熟練掌握此快照技術,是解鎖時序數據內隱藏動態故事的首要且不可或缺的關鍵能力。