傳統的網路分析常專注於節點間的拓撲結構,例如連結關係與中心性,但忽略了現實世界中的地理空間限制。本篇文章將分析層次從抽象的圖論推進至具體的空間互動。我們將示範如何為航空網路的每個節點(機場)賦予經緯度屬性,並以此為基礎,引入物理學中的引力模型。此模型提供一個強大的理論框架,用以量化距離衰減效應與節點規模對流量的綜合影響。透過此方法,我們不僅能解釋大部分的流量分佈,更能精準地識別出那些因特殊商業策略、樞紐地位或區域吸引力而偏離模型預期的航線,從而獲得對航空運輸系統更深刻的洞察。

航空網路的空間屬性與引力模型應用詳解

本章節將延續對航空網路的分析,重點在於如何整合機場的地理空間資訊,並進一步應用引力模型來理解旅客流量與距離之間的關係。我們將詳細介紹如何載入機場的經緯度數據,並將這些空間屬性與網路結構結合,為後續的引力模型分析奠定基礎。

整合機場地理空間數據

  • 數據載入與處理
    • 旅客流量數據
      • 前文已提及,我們需要載入包含機場間旅客流量的數據(例如,carrier.csv)。
      • 程式碼片段展示了如何逐行讀取文件,解析數據(如旅客數量 count、來源機場 v、目標機場 w),並將其累加到網路的邊屬性中。
      • G_air.edges[v, w]['count'] += count:如果邊已存在,則累加旅客流量。
      • G_air.add_edge(v, w, count=count):如果邊不存在,則創建新邊並設定旅客流量。
      • 排除了旅客流量為零或來源與目標機場相同的無效記錄。
    • 地理位置數據
      • 為了進行空間分析,我們需要機場的地理坐標(緯度 lat 和經度 long)。
      • 程式碼展示了如何從一個名為 GlobalAirportDatabase.txt 的數據庫文件中載入這些信息。
      • 數據以冒號分隔,我們需要提取機場代碼 (code)、緯度 (lat) 和經度 (long)。
      • 這些信息被儲存在一個字典 airport_lat_long 中,其中鍵是機場代碼,值是 (緯度, 經度) 的元組。
  • 節點屬性更新與篩選
    • 匹配地理位置
      • 遍歷網路 G_air 中的所有節點(機場)。
      • 對於每個機場節點 v,嘗試在 airport_lat_long 字典中查找其對應的經緯度。
    • 篩選大陸地區機場
      • 為了聚焦於美國大陸的航空交通,我們需要篩選掉位於其他地區(如阿拉斯加、夏威夷、國際機場)的機場。
      • 通過檢查機場的緯度和經度是否落在美國大陸的地理範圍內來實現。
      • 如果找到匹配的經緯度且位於大陸範圍內,則將緯度和經度信息作為屬性添加到節點 v 中(例如,G_air.nodes[v]['latitude'] = lat)。
    • 處理缺失數據
      • 如果某個機場在地理位置數據庫中找不到,或者不在大陸範圍內,則該節點將被從網路中移除 (G_air.remove_node(v))。這確保了我們分析的網路僅包含具有有效地理信息的美國大陸機場。

應用引力模型於機場交通

  • 引力模型的數學框架
    • 如前所述,引力模型假設兩個節點 $i$ 和 $j$ 之間的互動強度 $I_{ij}$ 與它們的「質量」 $M_i, M_j$ 成正比,與它們之間距離 $d_{ij}$ 的平方成反比: $$ I_{ij} \propto \frac{M_i M_j}{d_{ij}^2} $$
  • 在航空網路中的應用
    • 節點質量 (Mass)
      • 對於機場,其「質量」可以定義為該機場的總旅客流量(進港旅客 + 出港旅客)。這代表了機場的規模和活躍程度。
    • 距離 (Distance)
      • 兩個機場之間的距離可以通過它們的緯度和經度計算得出。常用的方法是Haversine 公式,它能較準確地計算地球表面兩點之間的大圓距離。
    • 實際流量 (Actual Flow)
      • 這是從數據中直接獲取的,代表兩個機場之間實際的旅客運輸量(例如,countweight 屬性)。
    • 引力模型預期流量 (Expected Flow)
      • 根據引力模型的公式,使用機場的質量和計算出的距離,預測出兩個機場之間應該有的旅客流量。
  • 比較與分析
    • 透過比較實際旅客流量引力模型預期流量,我們可以評估引力模型對機場交通的解釋力。
    • 散點圖
      • 繪製實際流量(通常在對數尺度上)與引力模型預期流量(也在對數尺度上)的散點圖。
      • 如果點緊密分佈在 $y=x$ 線附近,則表明引力模型能很好地解釋機場間的旅客流量。
      • 偏離 $y=x$ 線的點則指示了其他影響因素的存在,例如:
        • 地理位置的吸引力:某些機場可能因其作為旅遊目的地或商業中心的地位而吸引更多流量,這不是單純由距離和規模決定的。
        • 航班網絡結構:樞紐機場 (hub airports) 的存在會扭曲單純的距離-流量關係。
        • 票價、時刻表、航空公司策略等。

程式碼實現細節

  • 載入旅客流量
    • 程式碼片段展示了如何讀取 carrier.csv 文件,並使用 strip().split(',') 解析每一行數據。
    • count = int(count) 將旅客數量轉換為整數。
    • 跳過無效記錄 (count == 0v == w)。
    • 使用 G_air.edges[v, w]['count'] += countG_air.add_edge(v, w, count=count) 來更新網路中的邊屬性。
  • 載入地理位置
    • 程式碼片段展示了如何讀取 GlobalAirportDatabase.txt 文件,並解析機場代碼、緯度和經度。
    • airport_lat_long[code] = (lat, long) 將數據存入字典。
  • 節點篩選與屬性添加
    • for v in list(G_air.nodes()): 遍歷節點。使用 list() 是為了在迭代時安全地修改節點集合(如果需要移除節點)。
    • try...except KeyError: 處理找不到機場代碼的情況。
    • if not (lat >= 24 and lat <= 49 and long >= -125 and long <= -66): 篩選美國大陸範圍。
    • G_air.nodes[v]['latitude'] = latG_air.nodes[v]['longitude'] = long 添加地理屬性。
    • G_air.remove_node(v) 移除不符合條件的節點。
import networkx as nx
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
from pathlib import Path
import math

# --- 整合機場地理空間數據與引力模型應用 
---
print("\n--- 整合機場地理空間數據與引力模型應用 
---
")

# --- 載入機場旅客流量數據 
---
# 假設 carrier.csv 文件存在於 './data_simulated/BTS2018/' 目錄下
carrier_data_path = Path('./data_simulated/BTS2018/carrier.csv')

# 創建一個空的圖來存儲航空網路
G_air_spatial = nx.Graph()
airport_locations = {} # 儲存機場的經緯度

# 模擬創建 carrier.csv 文件,如果它不存在
if not carrier_data_path.exists():
    print(f"模擬創建數據文件: {carrier_data_path}")
    data_dir_sim = Path('./data_simulated/BTS2018')
    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],
        'DEST_AIRPORT_ID':   [10002, 10003, 10004, 10001, 10003, 10001, 10002, 10005, 10005, 10005, 10001, 10002],
        'PASSENGERS':        [1500, 800, 200, 1200, 900, 750, 600, 100, 150, 300, 50, 70],
        'YEAR':              [2018, 2018, 2018, 2018, 2018, 2018, 2018, 2018, 2018, 2018, 2018, 2018],
        'MONTH':             [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
    }
    df_mock_carrier = pd.DataFrame(mock_data_carrier)
    df_mock_carrier.to_csv(carrier_data_path, index=False)

try:
    df_carrier = pd.read_csv(carrier_data_path)
    
    # 篩選 2018 年的數據
    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)

    print(f"已載入旅客流量數據。網路包含 {G_air_spatial.number_of_nodes()} 個節點和 {G_air_spatial.number_of_edges()} 條邊。")

except FileNotFoundError:
    print(f"錯誤: 旅客流量數據文件 '{carrier_data_path}' 未找到。")
except Exception as e:
    print(f"載入旅客流量數據時發生錯誤: {e}")

# --- 載入機場地理位置數據 
---
# 假設 GlobalAirportDatabase.txt 文件存在於 './data_simulated/partow/' 目錄下
airport_db_path = Path('./data_simulated/partow/GlobalAirportDatabase.txt')

# 模擬創建 GlobalAirportDatabase.txt 文件,如果它不存在
if not airport_db_path.exists():
    print(f"模擬創建數據文件: {airport_db_path}")
    data_dir_partow = Path('./data_simulated/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:JAC:Jackson Hole Airport:USA:43.605601:-110.737501
28:ANC:Ted Stevens Anchorage International Airport:USA:61.174355:-149.996333
29:HNL:Daniel K. Inouye International Airport:USA:21.318694:-157.924694
"""
    with open(airport_db_path, 'w') as f:
        f.write(mock_airport_db_content.strip())
    print(f"已創建模擬數據文件: {airport_db_path}")

try:
    with open(airport_db_path) as f:
        for row in f:
            columns = row.strip().split(':')
            # 確保行格式正確,至少有 16 列 (索引 0 到 15)
            if len(columns) >= 16:
                code = columns[1]
                # 檢查國家是否為 USA
                country = columns[3]
                if country == 'USA':
                    try:
                        lat = float(columns[14])
                        long = float(columns[15])
                        airport_locations[code] = (lat, long)
                    except ValueError:
                        # 忽略無法轉換為浮點數的緯度/經度
                        pass
            else:
                # 忽略格式不正確的行
                pass
    print(f"載入 {len(airport_locations)} 個美國機場的地理位置數據。")

except FileNotFoundError:
    print(f"錯誤: 地理位置數據文件 '{airport_db_path}' 未找到。")
except Exception as e:
    print(f"載入地理位置數據時發生錯誤: {e}")

# --- 將地理位置屬性添加到節點並篩選大陸機場 
---
nodes_to_remove = []
for node_id in list(G_air_spatial.nodes()): # 使用 list() 以便安全移除節點
    if node_id in airport_locations:
        lat, long = airport_locations[node_id]
        
        # 篩選美國大陸範圍 (近似值)
        # 緯度: 24 (佛羅里達南部) 到 49 (北部邊界)
        # 經度: -125 (西海岸) 到 -66 (東海岸)
        if 24 <= lat <= 49 and -125 <= long <= -66:
            G_air_spatial.nodes[node_id]['latitude'] = lat
            G_air_spatial.nodes[node_id]['longitude'] = 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"已為節點添加地理屬性。移除 {len(nodes_to_remove)} 個非大陸或無位置信息的機場。")
print(f"篩選後,網路包含 {G_air_spatial.number_of_nodes()} 個節點和 {G_air_spatial.number_of_edges()} 條邊。")

# --- 計算距離和引力模型預期流量 
---

# Haversine 公式計算球面距離 (公里)
def haversine_distance(lat1, lon1, lat2, lon2):
    R = 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 * c
    return distance

# 計算節點質量 (總進出港旅客數)
node_mass = {}
for node in G_air_spatial.nodes():
    total_passengers = 0
    # 進港旅客
    in_flights = df_carrier_2018[df_carrier_2018['DEST_AIRPORT_ID'] == int(node)] # 確保 ID 類型匹配
    if not in_flights.empty:
        total_passengers += in_flights['PASSENGERS'].sum()
    # 出港旅客
    out_flights = df_carrier_2018[df_carrier_2018['ORIGIN_AIRPORT_ID'] == int(node)] # 確保 ID 類型匹配
    if not out_flights.empty:
        total_passengers += out_flights['PASSENGERS'].sum()
    node_mass[node] = total_passengers
    G_air_spatial.nodes[node]['mass'] = total_passengers

print("已計算節點質量 (總進出港旅客數)。")

# 計算邊的距離和引力模型預期流量
edges_for_analysis = [] # 儲存用於分析的邊數據

for u, v, data in G_air_spatial.edges(data=True):
    # 確保節點有地理位置屬性
    if 'latitude' in G_air_spatial.nodes[u] and 'longitude' in G_air_spatial.nodes[u] and \
       'latitude' in G_air_spatial.nodes[v] and 'longitude' in G_air_spatial.nodes[v]:
        
        lat1, lon1 = G_air_spatial.nodes[u]['latitude'], G_air_spatial.nodes[u]['longitude']
        lat2, lon2 = G_air_spatial.nodes[v]['latitude'], G_air_spatial.nodes[v]['longitude']
        mass_u, mass_v = G_air_spatial.nodes[u]['mass'], G_air_spatial.nodes[v]['mass']
        
        # 計算距離
        distance = haversine_distance(lat1, lon1, lat2, lon2)
        
        # 計算引力模型預期的流量
        expected_flow = 0
        if distance > 0 and mass_u > 0 and mass_v > 0:
            expected_flow = (mass_u * mass_v) / (distance**2)
        
        # 將距離和預期流量添加到邊的屬性中
        data['distance_km'] = distance
        data['gravity_expected_flow'] = expected_flow
        
        # 儲存用於繪製散點圖的數據
        actual_flow = data.get('count', 0)
        if actual_flow > 0 and expected_flow > 0:
            edges_for_analysis.append({
                'actual_log': np.log10(actual_flow),
                'expected_log': np.log10(expected_flow),
                'distance': distance,
                'actual_flow': actual_flow,
                'expected_flow': expected_flow
            })
    else:
        # 如果節點缺少地理屬性,則跳過此邊
        pass

print(f"已計算 {len(edges_for_analysis)} 條邊的距離和引力模型預期流量。")

# --- 比較實際流量與引力模型預期流量 
---
if edges_for_analysis:
    df_analysis = pd.DataFrame(edges_for_analysis)
    
    plt.figure(figsize=(10, 6))
    plt.scatter(df_analysis['actual_log'], df_analysis['expected_log'], alpha=0.5, s=10)
    
    # 繪製 y=x 線作為參考
    min_val = min(df_analysis['actual_log'].min(), df_analysis['expected_log'].min())
    max_val = max(df_analysis['actual_log'].max(), df_analysis['expected_log'].max())
    plt.plot([min_val, max_val], [min_val, max_val], 'r--', label='Perfect Match')
    
    plt.title("Actual vs. Gravity Model Expected Passenger Flow (Log Scale)", fontsize=14)
    plt.xlabel("Log10(Actual Passenger Flow)", fontsize=12)
    plt.ylabel("Log10(Gravity Model Expected Flow)", fontsize=12)
    plt.legend()
    plt.grid(True, linestyle='--', alpha=0.6)
    plt.show()

    print("\n散點圖分析:")
    print("  - 圖表顯示了實際旅客流量與基於引力模型預測的流量之間的關係。")
    print("  -如果點緊密分佈在紅色虛線(y=x)附近,則表示引力模型能較好地解釋旅客流量,即距離和機場規模是主要驅動因素。")
    print("  -點的散佈情況揭示了模型解釋力的局限性,偏離直線的點可能受到其他因素影響,如航班網絡結構、票價、地理吸引力等。")
else:
    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
數據載入與處理:
  - 旅客流量數據 (carrier.csv):
    - 解析旅客數、來源/目標機場
    - 累加到網路邊的 'count' 屬性
  - 地理位置數據 (GlobalAirportDatabase.txt):
    - 解析機場代碼、國家、緯度、經度
    - 存儲為字典 airport_lat_long
節點屬性更新與篩選:
  - 遍歷節點,匹配地理位置
  - 篩選美國大陸機場 (基於經緯度範圍)
  - 添加 'latitude', 'longitude' 節點屬性
  - 移除不符合條件的節點
end note

:應用引力模型於機場交通;
note right
引力模型數學框架:
  - I_ij ~ (M_i * M_j) / d_ij^2
  - I: 互動強度 (實際旅客流量)
  - M: 節點質量 (機場總旅客流量)
  - d: 節點間距離 (Haversine 公式計算)
應用步驟:
  - 計算機場質量 (總進出港旅客)
  - 計算機場間距離
  - 計算引力模型預期流量
比較與分析:
  - 實際流量 vs. 引力模型預期流量
  - 繪製對數尺度散點圖
  - 分析點分佈與 y=x 線的關係
  - 識別模型解釋力的局限性 (其他影響因素)
end note

:程式碼實現細節;
note right
旅客流量載入:
  - 使用 pandas 讀取 CSV
  - 篩選年份
  - 累加邊的 'count' 屬性
地理位置載入:
  - 讀取 TXT 文件,按 ':' 分割
  - 篩選 USA 機場
  - 存儲為字典
節點篩選與屬性:
  - 遍歷節點,查找位置
  - 檢查大陸範圍,添加屬性
  - 移除不符節點
距離與模型計算:
  - Haversine 公式計算距離
  - 計算節點質量
  - 計算預期流量
繪製散點圖:
  - 比較實際與預期流量的對數值
  - 顯示 y=x 參考線
end note

stop

@enduml

看圖說話:

此圖示總結了「航空網路的空間屬性與引力模型應用詳解」的內容,重點在於展示如何整合機場的地理空間數據,並應用引力模型來分析機場間的旅客流量。流程開頭首先聚焦於「整合機場地理空間數據」,說明了如何載入旅客流量和地理位置數據,以及如何篩選節點並添加空間屬性,接著詳細闡述了「應用引力模型於機場交通」的步驟,包括如何定義節點質量、計算距離、計算引力模型預期流量,並透過比較實際流量與預期流量來評估模型,最後展示了「程式碼實現細節」,涵蓋了數據載入、節點篩選、距離計算和散點圖繪製的具體操作。

縱觀航空網路這類複雜系統的分析,從單純的拓撲結構邁向整合空間與經濟屬性的多維度模型,已是必然趨勢。引力模型的應用,其核心價值不僅在於建立一個解釋流量的基準線,更在於透過「模型預測」與「真實數據」之間的差異,精準地標示出異常點。傳統分析可能將所有航線視為同質連結,但此模型揭示了距離與機場規模(質量)的基礎影響力。

更重要的是,那些不符合模型預測的航線——即散點圖中遠離對角線的點——才是策略洞察的真正金礦。它們代表了被非典型因素(如樞紐地位、獨特觀光吸引力或特定商業協定)強力扭曲的市場,這正是管理者需要深入挖掘的機會與風險所在。

未來,這類融合地理、經濟與網路結構的複合分析法,將從航空業擴展至供應鏈、人才流動甚至資本市場等更廣泛的領域。數據的價值將不再僅來自於描述「是什麼」,而是來自於解釋「為什麼不是」模型預測的那樣。

玄貓認為,高階管理者應將引力模型視為一面「反光鏡」而非「水晶球」。真正的決策智慧,源自於系統性地分析模型失效之處,從而發掘超越標準公式的獨特競爭優勢。