網路並非靜態實體,其結構會隨時間演化,同時網路中的活動亦會反向塑造其結構。本文旨在從靜態分析框架中跳脫,深入探討網路的時間維度。我們將介紹「快照」與「分層網路」等核心方法,前者將連續時間離散化以追蹤結構指標的變化,後者則將時間視為額外維度,用以分析跨時間的流動路徑。在此基礎上,本文將延續殘差網路的概念,展示如何處理帶有時間戳的數據集,例如荷蘭維基百科的連結演變,從而建構並分析隨時間演進的網路模型,揭示其背後的動態驅動因素。
網路結構的動態演化與時間序列分析
本章節將從靜態網路分析轉向動態網路的探討,重點關注網路結構如何隨著時間演變,以及這些演變如何反饋於系統的運作。我們將介紹「快照」和「分層網路」等概念,用以捕捉網路的時態屬性,並以荷蘭維基百科的連結數據為例,展示如何在 NetworkX 中處理時間序列數據,構建隨時間變化的網路模型。
殘差網路的結構特性分析
- 網路測量指標的應用:
- 重複應用:殘差網路雖然是從原始網路中篩選出來的,但其結構特性仍然可以使用先前討論過的網路測量指標(如節點中心性、聚類係數等)進行分析。
- 目的:透過比較原始網路與殘差網路在這些指標上的差異,我們可以更深入地理解引力模型無法解釋的「額外吸引力」是如何影響網路結構的。
- 平均聚類係數的比較:
- 原始網路:
nx.average_clustering(G_air)的結果顯示了一個相對較高的平均聚類係數(約 0.658)。- 解釋:這符合地理基礎設施網路的典型特徵。在地理網路中,如果節點 A 與 B 連接,節點 B 與 C 連接,那麼節點 A 和 C 之間存在連接的可能性也較高,形成緊密的局部社群。這反映了地理上的鄰近性或區域性的交通樞紐效應。
- 殘差網路:
nx.average_clustering(G_residual)的結果顯示了一個非常低的平均聚類係數(約 0.036)。- 解釋:這表明,當我們排除了距離和規模的影響(即只看那些「超預期」的連接)後,網路的局部社群結構大大減弱。殘差網路中的連接更多地是跨越較大地理距離的、具有特殊聯繫的點對點關係,而非形成緊密的局部群組。這印證了殘差網路主要捕捉的是「遠距離但強連結」的關係。
- 原始網路:
- 結構性洞察:
- 這種聚類係數的巨大差異,有力地證明了地理距離在傳統航空網路結構形成中的關鍵作用。
- 而殘差網路的低聚類性,則凸顯了那些不受地理限制、主要由經濟、商業或旅遊等「非地理」因素驅動的強烈聯繫。
動態網路的引入:時間維度的考量
- 網路的時變性:
- 「熱寂」的類比:宇宙的演化和網路的變遷都遵循著時間的流逝。網路結構並非一成不變,而是會隨著時間動態演化。
- 動態網路 (Dynamic Networks):指那些結構隨時間變化的網路。
- 雙向影響:網路結構不僅影響系統的運作(例如,交通網路影響物流效率),系統中的過程(例如,經濟活動、人口遷移)也會反過來影響網路的結構。
- 分析動態網路的方法:
- 快照 (Snapshots):
- 概念:一種捕捉網路在特定時間點狀態的方法,如同拍攝一張照片。
- 構建:一個快照網路包含在該特定時間點存在的所有節點和邊。
- 分析:透過計算一系列時間點的快照網路的屬性,可以追蹤網路結構的演變過程。
- 帶時間標籤的邊:
- 另一種方法是將所有節點和邊都納入一個總體網路,但為每條邊或節點標註其在網路中存在的時間段(出現和消失的時間)。
- 然後,可以根據這些時間標籤來生成特定時間段的快照。
- 快照 (Snapshots):
- 分層網路 (Layered Networks):
- 應用場景:當對網路中隨時間發生的「流」(flow) 感興趣時(例如,貨物運輸、資訊傳播),分層網路是一種有效的建模方式。
- 構建:
- 將時間軸劃分為多個「層」(layer),每個層代表一個時間快照。
- 原始網路中的每個節點,在每個時間層中都對應一個「副本」。
- 這些節點副本之間通過「時間邊」連接起來,表示資訊或物體在時間上的傳遞。
- 優勢:這種結構使得分析在單一時間點內不存在路徑,但在跨越時間後卻能形成的流動成為可能。
處理時間序列數據的實踐
- 案例:荷蘭維基百科連結:
- 數據源:荷蘭維基百科的文章連結數據集,其中連結的出現和消失反映了知識網路的動態變化。
- 數據格式:邊列表 (edge list) 文件,其中每條邊除了節點對外,還包含
begin和end時間戳。 - NetworkX 讀取:
nx.read_edgelist(..., data=[('begin', int), ('end', int)], create_using=nx.MultiGraph)data參數:指定如何讀取邊的其他屬性,並轉換為整數類型。create_using=nx.MultiGraph:使用MultiGraph是因為數據集允許同一對節點之間存在多條邊(例如,在不同時間點出現的連結),每條邊可以有不同的屬性(時間戳)。
- 時間戳與數據規模:
- Unix 時間戳:數據中的時間以整數形式儲存,代表自 1970 年 1 月 1 日以來的秒數。這種統一的數值表示便於時間比較和排序。
- 處理大規模數據:原始數據集包含超過 43,509 個節點,對於直接分析來說過於龐大。
- 聚焦早期階段:由於大多數節點是隨時間逐漸添加的,可以選擇分析數據集的前幾個星期,此時網路規模較小,便於計算和理解。
- 創建時間快照的函數:
- 文中預告,接下來將介紹一個函數,用於根據指定的時間點創建網路的快照。這將是分析網路隨時間演變的基礎。
import networkx as nx
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from pathlib import Path
import math
# --- 假設 G_air_spatial 已載入並處理好所有屬性
---
# 為了程式碼的獨立性,這裡重新載入並處理部分數據
# 實際應用中,應接續前文的 G_air_spatial
# --- 重新創建模擬數據和網路 (如果前文未執行)
---
data_dir_sim = Path('./data_simulated')
carrier_data_path = data_dir_sim / 'BTS2018' / 'carrier.csv'
airport_db_path = data_dir_sim / 'partow' / 'GlobalAirportDatabase.txt'
# 模擬創建 carrier.csv
if not carrier_data_path.exists():
data_dir_sim.mkdir(parents=True, exist_ok=True)
mock_data_carrier = {
'ORIGIN_AIRPORT_ID': [10001, 10001, 10001, 10002, 10002, 10003, 10003, 10001, 10002, 10003, 10004, 10005, 10006, 10007],
'DEST_AIRPORT_ID': [10002, 10003, 10004, 10001, 10003, 10001, 10002, 10005, 10005, 10005, 10001, 10002, 10001, 10002],
'PASSENGERS': [1500, 800, 200, 1200, 900, 750, 600, 100, 150, 300, 50, 70, 1200, 1000],
'YEAR': [2018]*14, 'MONTH': [1]*14
}
df_mock_carrier = pd.DataFrame(mock_data_carrier)
df_mock_carrier.to_csv(carrier_data_path, index=False)
# 模擬創建 GlobalAirportDatabase.txt
if not airport_db_path.exists():
data_dir_partow = data_dir_sim / 'partow'
data_dir_partow.mkdir(parents=True, exist_ok=True)
mock_airport_db_content = """
1:JFK:John F. Kennedy International Airport:USA:40.639751:-73.778925
2:LAX:Los Angeles International Airport:USA:33.941556:-118.408530
3:ORD:Chicago O'Hare International Airport:USA:41.974209:-87.907321
4:ATL:Hartsfield-Jackson Atlanta International Airport:USA:33.640445:-84.427700
5:DEN:Denver International Airport:USA:39.856057:-104.673737
6:SFO:San Francisco International Airport:USA:37.619002:-122.371297
7:SEA:Seattle-Tacoma International Airport:USA:47.448981:-122.309310
8:MIA:Miami International Airport:USA:25.793199:-80.290594
9:DFW:Dallas/Fort Worth International Airport:USA:32.899811:-97.040416
10:LAS:McCarran International Airport:USA:36.084030:-115.153734
11:PHX:Phoenix Sky Harbor International Airport:USA:33.434250:-112.011590
12:IAH:George Bush Intercontinental Airport:USA:29.990163:-95.336770
13:BOS:Logan International Airport:USA:42.365599:-71.009667
14:MSP:Minneapolis–Saint Paul International Airport:USA:44.884794:-93.217711
15:PHL:Philadelphia International Airport:USA:39.874399:-75.242449
16:CLT:Charlotte Douglas International Airport:USA:35.214433:-80.947171
17:EWR:Newark Liberty International Airport:USA:40.689532:-74.174492
18:DTW:Detroit Metropolitan Airport:USA:42.211501:-83.353409
19:FLL:Fort Lauderdale–Hollywood International Airport:USA:26.074199:-80.150667
20:BWI:Baltimore/Washington International Airport:USA:39.177452:-76.668409
21:SLC:Salt Lake City International Airport:USA:40.789999:-111.979111
22:SAN:San Diego International Airport:USA:32.733819:-117.193751
23:TPA:Tampa International Airport:USA:27.977474:-82.531189
24:PDX:Portland International Airport:USA:45.589802:-122.597510
25:IAD:Washington Dulles International Airport:USA:38.953054:-77.456459
26:DCA:Ronald Reagan Washington National Airport:USA:38.851111:-77.037500
27:ANC:Ted Stevens Anchorage International Airport:USA:61.174355:-149.996333 # Alaska, will be removed
28:HNL:Daniel K. Inouye International Airport:USA:21.318694:-157.924694 # Hawaii, will be removed
"""
with open(airport_db_path, 'w') as f:
f.write(mock_airport_db_content.strip())
# --- 重新載入並處理數據
---
G_air_spatial = nx.Graph()
airport_locations = {}
try:
# 載入旅客流量
df_carrier = pd.read_csv(carrier_data_path)
df_carrier_2018 = df_carrier[df_carrier['YEAR'] == 2018]
for index, row in df_carrier_2018.iterrows():
origin = str(row['ORIGIN_AIRPORT_ID'])
dest = str(row['DEST_AIRPORT_ID'])
passengers = int(row['PASSENGERS'])
if origin == dest or passengers == 0: continue
if G_air_spatial.has_edge(origin, dest):
G_air_spatial.edges[origin, dest]['count'] += passengers
else:
G_air_spatial.add_edge(origin, dest, count=passengers)
if origin not in G_air_spatial: G_air_spatial.add_node(origin)
if dest not in G_air_spatial: G_air_spatial.add_node(dest)
# 載入地理位置
with open(airport_db_path) as f:
for row in f:
columns = row.strip().split(':')
if len(columns) >= 16:
code = columns[1]
country = columns[3]
if country == 'USA':
try:
lat = float(columns[14])
long = float(columns[15])
airport_locations[code] = (lat, long)
except ValueError: pass
# 添加地理屬性並篩選大陸機場
nodes_to_remove = []
for node_id in list(G_air_spatial.nodes()):
if node_id in airport_locations:
lat, long = airport_locations[node_id]
if 24 <= lat <= 49 and -125 <= long <= -66:
G_air_spatial.nodes[node_id]['lat'] = lat
G_air_spatial.nodes[node_id]['long'] = long
else:
nodes_to_remove.append(node_id)
else:
nodes_to_remove.append(node_id)
G_air_spatial.remove_nodes_from(nodes_to_remove)
print(f"網路準備就緒: {G_air_spatial.number_of_nodes()} 個節點, {G_air_spatial.number_of_edges()} 條邊。")
except Exception as e:
print(f"初始化網路時發生錯誤: {e}")
# --- Haversine 距離計算函數
---
def haversine(lat1, lon1, lat2, lon2):
R_km = 6371
lat1_rad, lon1_rad, lat2_rad, lon2_rad = map(math.radians, [lat1, lon1, lat2, lon2])
dlon = lon2_rad - lon1_rad
dlat = lat2_rad - lat1_rad
a = math.sin(dlat / 2)**2 + math.cos(lat1_rad) * math.cos(lat2_rad) * math.sin(dlon / 2)**2
c = 2 * math.atan2(math.sqrt(a), math.sqrt(1 - a))
distance = R_km * c
return distance
# --- 為邊添加距離屬性
---
for v, w in G_air_spatial.edges():
try:
p_v = (G_air_spatial.nodes[v]['lat'], G_air_spatial.nodes[v]['long'])
p_w = (G_air_spatial.nodes[w]['lat'], G_air_spatial.nodes[w]['long'])
G_air_spatial.edges[v, w]['distance'] = haversine(p_v[0], p_v[1], p_w[0], p_w[1])
except KeyError:
G_air_spatial.edges[v, w]['distance'] = None
print("已為邊添加距離屬性。")
# --- 計算節點質量 (加權度數)
---
degree_data = list(G_air_spatial.degree(weight='count'))
nx.set_node_attributes(G_air_spatial, dict(degree_data), 'mass')
print("已計算節點質量 (總旅客流量)。")
# --- 簡化方法估計引力模型常數乘數 k
---
k_values = []
for v, w in G_air_spatial.edges():
if v >= w: continue
try:
count = G_air_spatial.edges[v, w]['count']
distance = G_air_spatial.edges[v, w]['distance']
mass_v = G_air_spatial.nodes[v]['mass']
mass_w = G_air_spatial.nodes[w]['mass']
if count is not None and distance is not None and distance > 0 and \
mass_v is not None and mass_w is not None and mass_v > 0 and mass_w > 0:
k_val = count * (distance**2) / (mass_v * mass_w)
k_values.append(k_val)
except KeyError: continue
# --- 計算幾何平均數 g
---
g = 0
valid_k_values = [val for val in k_values if val > 0]
if valid_k_values:
log_k_values = [math.log10(val) for val in valid_k_values]
mean_log_k = sum(log_k_values) / len(log_k_values)
g = 10**mean_log_k
print(f"\n估計的引力模型常數 g (幾何平均數): {g:.4f}")
# --- 計算預期流量和殘差
---
for v, w in G_air_spatial.edges():
if 'distance' in G_air_spatial.edges[v, w] and \
'count' in G_air_spatial.edges[v, w] and \
'mass' in G_air_spatial.nodes[v] and \
'mass' in G_air_spatial.nodes[w]:
count = G_air_spatial.edges[v, w]['count']
distance = G_air_spatial.edges[v, w]['distance']
mass_v = G_air_spatial.nodes[v]['mass']
mass_w = G_air_spatial.nodes[w]['mass']
expected_flow = 0
if distance is not None and distance > 0 and \
mass_v is not None and mass_w is not None and mass_v > 0 and mass_w > 0 and g > 0:
expected_flow = (g * mass_v * mass_w) / (distance**2)
G_air_spatial.edges[v, w]['expected'] = expected_flow
residual = count - expected_flow
G_air_spatial.edges[v, w]['residual'] = residual
if expected_flow > 0 and count > 0:
log_residual = math.log10(count) - math.log10(expected_flow)
G_air_spatial.edges[v, w]['log_residual'] = log_residual
else:
G_air_spatial.edges[v, w]['log_residual'] = None
else:
G_air_spatial.edges[v, w]['expected'] = None
G_air_spatial.edges[v, w]['residual'] = None
G_air_spatial.edges[v, w]['log_residual'] = None
print("已計算預期流量、殘差和對數殘差,並添加到邊屬性。")
# --- 構建殘差網路
---
residual_edges = [
e for e in G_air_spatial.edges()
if G_air_spatial.edges[e].get('log_residual') is not None and G_air_spatial.edges[e]['log_residual'] > 0
]
G_residual = G_air_spatial.edge_subgraph(residual_edges)
# 提取最大的連通分量 (可選,這裡直接使用子圖)
# if G_residual.number_of_nodes() > 0:
# largest_cc = max(nx.connected_components(G_residual), key=len)
# G_residual = G_residual.subgraph(largest_cc).copy()
print(f"已構建殘差網路,包含 {G_residual.number_of_nodes()} 個節點和 {G_residual.number_of_edges()} 條邊。")
# --- 分析殘差網路的網路屬性
---
print("\n--- 殘差網路屬性分析
---
")
# 平均聚類係數
avg_clustering_original = nx.average_clustering(G_air_spatial)
avg_clustering_residual = nx.average_clustering(G_residual) if G_residual.number_of_edges() > 0 else 0
print(f"原始網路平均聚類係數: {avg_clustering_original:.4f}")
print(f"殘差網路平均聚類係數: {avg_clustering_residual:.4f}")
# --- 視覺化殘差網路
---
if G_residual.number_of_edges() > 0:
fig, ax = plt.subplots(figsize=(15, 15))
# 確保 pos 字典存在
if 'pos' not in locals():
print("警告:位置字典 'pos' 未定義,將重新計算地理佈局。")
pos = {}
min_lat = min(data['lat'] for _, data in G_air_spatial.nodes(data=True) if 'lat' in data)
max_lat = max(data['lat'] for _, data in G_air_spatial.nodes(data=True) if 'lat' in data)
min_lon = min(data['long'] for _, data in G_air_spatial.nodes(data=True) if 'long' in data)
max_lon = max(data['long'] for _, data in G_air_spatial.nodes(data=True) if 'long' in data)
fig_width = 15
fig_height = 15
for node_id, data in G_air_spatial.nodes(data=True):
if 'lat' in data and 'long' in data:
lat = data['lat']
long = data['long']
x_coord = fig_width * (long - min_lon) / (max_lon - min_lon)
y_coord = fig_height * (lat - min_lat) / (max_lat - min_lat)
pos[node_id] = (x_coord, y_coord)
nx.draw_networkx_nodes(G_residual, pos=pos, node_color='#7f7fff', node_size=20, ax=ax)
log_residuals_in_residual_graph = [
data['log_residual'] for u, v, data in G_residual.edges(data=True)
if data.get('log_residual') is not None
]
max_log_residual = 0
if log_residuals_in_residual_graph:
max_log_residual = max(log_residuals_in_residual_graph)
for e in G_residual.edges():
log_res = G_residual.edges[e].get('log_residual')
if log_res is not None and max_log_residual > 0:
alpha = log_res / max_log_residual
else:
alpha = 0.5
nx.draw_networkx_edges(
G_residual,
pos=pos,
edgelist=[e],
edge_color='#7f7fff',
alpha=alpha,
width=1.0,
arrows=False,
ax=ax
)
ax.set_aspect(1)
plt.title("US Air Traffic: Residual Network (Connections Exceeding Expected Traffic)", fontsize=18)
plt.xlabel("Longitude (Projected)", fontsize=14)
plt.ylabel("Latitude (Projected)", fontsize=14)
plt.xlim([0, fig_width])
plt.ylim([0, fig_height])
plt.xticks([])
plt.yticks([])
plt.show()
print("\n殘差網路視覺化分析:")
print(" - 殘差網路突顯了具有額外吸引力的連接,這些連接超越了距離和規模的預期。")
print(" - 識別出重要的商業中心(如紐約、舊金山、洛杉磯)和旅遊目的地,它們在殘差網路中扮演核心角色。")
else:
print("\n殘差網路中沒有邊,無法進行視覺化。")
else:
print("\n未能計算出有效的引力模型常數 g。無法進行殘差網路的構建和視覺化。")
@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
網路測量指標應用:
- 分析殘差網路的聚類係數等指標
- 比較原始網路與殘差網路的差異
平均聚類係數比較:
- 原始網路: 高聚類係數 (0.658)
- 特徵: 地理基礎設施網路典型
- 原因: 地理鄰近性、區域樞紐效應
- 殘差網路: 低聚類係數 (0.036)
- 特徵: 排除距離影響後
- 原因: 捕捉點對點的特殊聯繫,而非局部社群
結構性洞察:
- 地理距離是傳統網路結構的關鍵
- 殘差網路揭示非地理因素驅動的聯繫
end note
:動態網路的引入:時間維度的考量;
note right
網路時變性:
- 網路結構隨時間動態演變
- 系統過程影響網路結構,反之亦然
分析方法:
- 快照 (Snapshots):
- 特定時間點的網路狀態
- 追蹤網路演變
- 帶時間標籤的邊:
- 標註出現/消失時間
- 生成時間段快照
分層網路 (Layered Networks):
- 應用: 分析隨時間的流動 (flow)
- 構建:
- 時間層 (snapshots)
- 節點副本,通過時間邊連接
- 優勢: 分析跨越時間的路徑和流動
end note
:處理時間序列數據的實踐;
note right
案例: 荷蘭維基百科連結
- 數據源: 文章連結的出現與消失
- 數據格式: 邊列表 (edge list)
- 包含 begin/end 時間戳
- NetworkX 讀取:
- nx.read_edgelist(...)
- 使用 MultiGraph 處理多重邊
時間戳:
- Unix 時間戳 (秒數)
- 便於時間比較
處理大規模數據:
- 聚焦早期階段 (小規模網路)
創建快照函數:
- 預告後續將介紹
end note
stop
@enduml看圖說話:
此圖示總結了「網路結構的動態演化與時間序列分析」的內容,重點在於從靜態網路分析過渡到動態網路的探討,並介紹了處理時間序列數據的基本方法。流程開頭首先聚焦於「殘差網路的結構特性分析」,透過比較原始網路與殘差網路的平均聚類係數,揭示了地理距離在網路結構形成中的作用以及殘差網路所捕捉的非地理聯繫,接著詳細闡述了「動態網路的引入:時間維度的考量」,介紹了快照、帶時間標籤的邊以及分層網路等概念,用以理解網路的時變性,最後概述了「處理時間序列數據的實踐」,以荷蘭維基百科連結數據為例,展示了如何讀取帶有時間屬性的邊數據,並為後續的時間快照分析奠定基礎。
縱觀現代管理者的多元挑戰,從靜態結構轉向動態演化的分析思維,已是洞察複雜系統的必然路徑。殘差網路的分析精準揭示了傳統模型的認知瓶頸:它雖能解釋常態,卻常忽略驅動系統突破的「超預期」連結。這證明了僅僅描繪當下狀態,已不足以支撐前瞻性的決策。
引入時間維度,不僅是數據處理技術的升級,更是從「結構製圖」邁向「過程敘事」的思維質變。透過快照、分層網路等工具,我們得以觀察連結的生滅、影響力的流轉,從而理解系統的內在生命力與演化邏輯。未來,對動態網路的掌握,將成為區分卓越與平庸分析能力的關鍵指標,它將與預測模型深度整合,用於模擬市場變化、評估組織韌性,甚至預警潛在的系統性風險。
玄貓認為,高階管理者應當推動團隊超越對靜態指標的迷戀,轉而投資於捕捉與詮釋時間序列數據的能力。唯有如此,才能在瞬息萬變的商業生態中,真正掌握系統的脈動,而非僅僅描繪其僵化的骨架。