返回文章列表

航空網路分析:地理視覺化與引力模型校準

本文探討如何從拓撲結構提升至地理空間層次來分析航空網路。首先,透過機場的地理座標繪製節點,並利用旅客流量作為邊的權重,動態調整航線透明度,直觀呈現網路的流量分佈。接著,文章深入介紹引力模型的應用,說明如何使用 Haversine

數據科學 網路科學

傳統網路拓撲圖在處理真實空間網路時,常因佈局混亂形成難以解讀的「毛球」困境。本章節旨在超越此限制,將航空網路分析提升至地理資訊整合的層次。我們首先利用機場經緯度數據,建構符合真實地理分佈的視覺化圖景,並透過旅客流量權重突顯關鍵航線,使網路結構與流量強度一目了然。然而,視覺化尚不足以解釋流量成因。因此,我們將導入在交通與經濟地理學中廣泛應用的引力模型。透過 Haversine 公式精確計算距離,該模型能幫助我們量化地分離距離因素的影響,從而更深入探究由非空間因素驅動的特殊連結。

航空網路的地理視覺化與引力模型校準

本章節將進一步深化對航空網路的視覺化與分析。我們將首先展示如何基於節點的地理位置繪製出更具資訊量的網路圖,並利用邊的權重(旅客流量)來調整透明度,以突顯繁忙的航線。隨後,我們將回顧並詳述引力模型在航空交通分析中的應用,特別是如何利用 Haversine 公式計算機場間的距離,並為後續的引力模型校準奠定基礎。

基於地理位置的航空網路視覺化

  • 精細化繪製邊
    • nx.draw_networkx_edges() 的逐條繪製
      • 為了能夠為每條邊設定不同的透明度(alpha),程式碼採用了逐條繪製邊的方式。
      • edgelist=[e]:每次只繪製一條邊 e
      • alpha = G_air.edges[e]['count'] / max_weight:邊的透明度與其旅客流量(count)佔最大旅客流量的比例成正比。流量越大的邊越不透明,越容易被觀察到。
      • edge_color='#7f7fff':使用一種藍色調來表示邊。
    • 節點繪製
      • nx.draw_networkx_nodes(G_air, pos=pos, node_color='#7f7fff', node_size=20):繪製節點,使用與邊相同的顏色,但尺寸較小。
    • ax.set_aspect(1):確保圖表的縱橫比為 1:1,以正確反映地理空間的比例。
  • 視覺化結果的提升
    • 從「毛球」到地理圖景
      • 與先前基於拓撲的「毛球」視覺化相比,這種基於地理位置的佈局(pos 字典)極大地提升了圖的可讀性。
      • 圖中清晰地呈現了美國大陸的地理輪廓,機場節點大致分佈在其地理位置上。
      • 航線(邊)的透明度直觀地顯示了不同航線的旅客流量強度。繁忙的航線(如東西海岸之間、主要樞紐機場之間)會顯得更為明顯。
    • 資訊的豐富性
      • 這種視覺化不僅展示了網路的連接結構,還融入了實際的空間資訊和流量數據,使得分析更加深入。
      • 我們可以觀察到地理距離與航線流量之間的初步關係:較長的航線(如跨大陸)通常流量較大,但也可能存在流量較小的短航線。

引力模型校準:距離計算與模型原理回顧

  • 分析的關鍵問題
    • 儘管視覺化效果顯著提升,但我們仍面臨一個核心分析問題:如果兩機場之間存在高流量邊(高權重),這僅僅是因為它們地理上「方便地」靠近,還是因為兩地區之間存在特殊的「關係」(例如,經濟聯繫、人口密度差異等)?
    • 這正是引力模型試圖解決的問題:區分距離效應與其他潛在因素的影響
  • 引力模型的距離計算
    • Haversine 公式
      • 為了應用引力模型,首先需要準確計算機場之間的地理距離。
      • Haversine 公式是一種常用的方法,用於計算地球表面兩點之間的大圓距離。它假設地球是一個完美的球體。
      • 公式細節
        • 輸入:兩個點的緯度 qp(通常是 (lat, long) 元組)。
        • 將角度從度轉換為弧度:theta = lat * math.pi / 180phi = long * math.pi / 180
        • 計算經度和緯度差的弧度值:dlon = phi2 - phi1dlat = theta2 - theta1
        • 核心計算:a = sin(dlat / 2)**2 + cos(theta1) * cos(theta2) * sin(dlon / 2)**2
        • 計算球面角度:c = 2 * atan2(sqrt(a), sqrt(1 - a))
        • 最終距離:distance = R_km * c,其中 R_km 是地球的平均半徑(約 6371 公里)。
  • 引力模型的預期流量計算
    • 一旦計算出機場間的距離 d_ij,並且已經確定了機場的「質量」 $M_i, M_j$(總旅客流量),就可以計算引力模型預期的流量 $I_{ij}$: $$ I_{ij} = k \frac{M_i M_j}{d_{ij}^2} $$ 其中 $k$ 是一個常數,用於調整模型。在實際應用中,這個常數可以通過擬合數據來估計。
  • 後續分析
    • 透過比較實際流量 count 與引力模型預期流量 expected_flow,可以分析:
      • 模型擬合度:實際流量與預期流量的相關性。
      • 殘差分析actual_flow - expected_flow 的殘差。這些殘差可能代表了由距離和質量無法解釋的「特殊關係」或「吸引力」。
      • 例如,如果兩個機場距離較遠,但實際流量遠高於引力模型預期,則可能表明這兩個地區之間存在強勁的經濟或文化聯繫。反之,如果距離較近但流量較低,則可能表示該航線的吸引力不足或存在其他競爭因素。

程式碼實現細節

  • 繪製邊的透明度
    • 程式碼通過遍歷所有邊,並根據其旅客流量佔最大流量的比例來設定 alpha 值,實現了動態透明度的視覺化。
    • max_weight = max([G_air.edges[e]['count'] for e in G_air.edges]):首先找到網路中最大的邊權重。
    • alpha = G_air.edges[e]['count'] / max_weight:計算歸一化的流量值,作為透明度。
  • Haversine 函數實現
    • 程式碼片段提供了 haversine(q, p) 函數的定義,該函數接收兩個 (lat, long) 點,並返回它們之間的距離(以公里為單位)。
    • 包含了角度轉換、球面三角學計算以及最終的距離計算步驟。
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 已載入並處理好節點屬性 (lat, long, count) 
---
# 為了程式碼的獨立性,這裡重新載入並處理部分數據
# 實際應用中,應接續前文的 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}")

# --- 創建地理投影佈局 
---
def haversine(lat1, lon1, lat2, lon2):
    """Calculate the distance between two (lat, long) points using Haversine formula."""
    R_km = 6371 # Earth radius in kilometers
    
    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

# 創建位置字典 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)

print(f"已為 {len(pos)} 個節點生成地理位置佈局。")

# --- 視覺化航空網路,使用地理位置佈局和動態透明度 
---
fig, ax = plt.subplots(figsize=(15, 15))

# 繪製節點
nx.draw_networkx_nodes(G_air_spatial, pos=pos, node_color='#7f7fff', node_size=20, ax=ax)

# 確定最大邊權重以用於透明度計算
max_weight = 0
if G_air_spatial.number_of_edges() > 0:
    max_weight = max([G_air_spatial.edges[e]['count'] for e in G_air_spatial.edges])

# 逐條繪製邊,根據旅客流量設定透明度
for e in G_air_spatial.edges:
    if max_weight > 0: # 避免除以零
        alpha = G_air_spatial.edges[e]['count'] / max_weight
    else:
        alpha = 0.5 # 如果沒有邊,給一個預設值

    nx.draw_networkx_edges(
        G_air_spatial,
        pos=pos,
        edgelist=[e],
        edge_color='#7f7fff',
        alpha=alpha,
        width=1.0, # 邊的寬度
        arrows=False, # 不顯示箭頭
        ax=ax
    )

ax.set_aspect(1) # 設定圖表縱橫比為 1:1
plt.title("US Air Traffic Network: Geospatial Layout with Traffic Volume", 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("  - 繁忙的航線(較不透明的邊)集中在主要城市和樞紐機場之間。")
print("  - 這種視覺化有助於識別交通模式,但仍未完全校準距離效應。")
print("  - 引力模型將用於進一步分析流量與距離的關係。")

# --- 引力模型距離計算函數 
---
def haversine_distance_func(lat1, lon1, lat2, lon2):
    """Calculate the distance between two (lat, long) points using Haversine formula."""
    R_km = 6371 # Earth radius in kilometers
    
    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

print("\nHaversine 距離計算函數已定義。")
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
逐條繪製邊:
  - 使用 nx.draw_networkx_edges 針對單條邊
  - 實現動態透明度 (alpha)
邊的透明度 (alpha):
  - alpha = 邊權重 / 最大權重
  - 流量越大,邊越不透明
節點繪製:
  - 使用 nx.draw_networkx_nodes
  - 節點大小和顏色可調
圖表設置:
  - ax.set_aspect(1): 確保縱橫比為 1:1
end note

:視覺化結果的提升;
note right
從「毛球」到地理圖景:
  - 基於地理位置佈局,更直觀
  - 顯示美國大陸輪廓
  - 邊的透明度反映旅客流量
資訊的豐富性:
  - 結合空間資訊和流量數據
  - 識別繁忙航線和主要樞紐
  - 初步觀察距離與流量關係
end note

:引力模型校準:距離計算與模型原理回顧;
note right
分析的關鍵問題:
  - 高流量邊是因距離近還是特殊關係?
  - 引力模型用於區分距離效應與其他因素
距離計算 (Haversine 公式):
  - 計算地球表面兩點間大圓距離
  - 輸入: (lat, long) 元組
  - 輸出: 距離 (公里)
模型預期流量計算:
  - I_ij = k * (M_i * M_j) / d_ij^2
  - I: 實際流量
  - M: 節點質量 (總旅客流量)
  - d: 距離
後續分析:
  - 比較實際流量與預期流量
  - 分析殘差,識別模型無法解釋的因素
end note

stop

@enduml

看圖說話:

此圖示總結了「航空網路的地理視覺化與引力模型校準」的內容,重點在於展示如何透過精細化的視覺化來呈現航空網路的空間結構與流量分佈,並為後續的引力模型分析做準備。流程開頭首先聚焦於「精細化繪製邊」,說明瞭如何透過逐條繪製邊並根據旅客流量設定透明度來實現動態視覺化,接著闡述了「視覺化結果的提升」,對比了地理空間佈局相較於傳統佈局的優勢,並指出了其資訊豐富性,最後詳細介紹了「引力模型校準:距離計算與模型原理回顧」,回顧了引力模型的核心問題、Haversine 公式在距離計算中的作用,以及如何利用這些資訊進行後續的流量與距離關係分析。

解構此數據分析方法的關鍵進程可以發現,從抽象的拓撲結構呈現,進展至疊加地理與流量資訊的空間視覺化,本身已是分析深度的重大躍升。然而,這種直觀呈現的價值,恰好也凸顯了其分析瓶頸:無法有效剝離地理距離對流量的基礎影響。引力模型的引入,正是為了解決此核心挑戰,它不僅整合了節點質量(機場總流量)與邊的距離(航線長度),更建立了一個理論基準線,用以衡量實際流量偏離預期的程度,成功將分析層次從「描述現象」推進至「探究動因」。

展望未來,引力模型校準後的殘差分析,將成為商業洞察的核心金礦。這些「異常值」不再是統計誤差,而是指向特殊經濟廊道、互補性產業鏈或潛在市場競爭的明確信號。我們預見,這類結合領域知識與數據模型的複合分析框架,將從航空業擴散至物流、零售乃至數位服務等所有具備網路效應的領域,成為發掘隱性市場動力的標準配備。

玄貓認為,對於追求數據驅動決策的管理者而言,推動團隊從單純的視覺化報表演進至模型校準分析,不僅是技術升級,更是建立深刻市場洞察力的策略性投資。