引言

在航空业这个高度复杂且竞争激烈的行业中,优化建模扮演着至关重要的角色。航空公司每天面临着无数的决策问题:如何高效地调度飞机和机组人员?如何为数以千计的座位定价以最大化收益?如何设计航线网络以覆盖市场需求?这些问题不仅规模庞大,而且充满了不确定性,如天气变化、市场需求波动等。优化建模通过数学和计算方法,帮助航空公司将这些复杂的现实问题转化为可求解的数学模型,从而做出更科学、更高效的决策。

本文将深入探讨航空公司运营中的核心优化问题,从航班调度、机组排班到航线网络设计,再到收益管理,逐一分析其面临的挑战、常用的建模方法以及实际的解题思路。我们将通过详细的理论讲解和具体的代码示例,展示如何将这些优化技术应用于解决航空业的实际难题。

1. 航班调度优化 (Flight Scheduling Optimization)

航班调度是航空公司运营的基石。它涉及到在满足各种约束(如飞机可用性、机场时刻表、维护要求等)的前提下,为一系列航班分配飞机,目标通常是最大化飞机利用率、最小化运营成本或最大化网络连通性。

1.1 问题描述与挑战

核心问题:给定一个航班网络(包含一系列航班段,每个航班段有出发地、目的地、计划起飞和到达时间),以及一个飞机机队(每架飞机有其类型、当前位置、维护计划等),如何为每个航班分配一架飞机,使得整个调度方案可行且最优。

主要挑战

  • 规模巨大:大型航空公司每天有数千个航班,可能的分配方案数量是天文数字。
  • 复杂约束
    • 时间约束:飞机必须在上一个航班到达后,经过一定的地面周转时间(Turnaround Time)才能执行下一个航班。
    • 位置约束:飞机必须物理上位于下一个航班的出发地才能执行该航班(除非进行空载调机,但成本高昂)。
    • 机型约束:某些航线或机场可能对飞机型号有特殊要求(如跑道长度、噪音限制)。
    • 维护约束:飞机需要定期进行维护,维护计划必须被严格遵守。
    • 机组约束:虽然机组排班是另一个问题,但航班调度必须为机组排班提供可行的基础。
  • 不确定性:航班延误、飞机故障等突发事件会破坏原有调度,需要快速重新规划。

1.2 建模思路:网络流模型

航班调度问题通常可以被建模为一个最小费用最大流问题 (Minimum Cost Maximum Flow Problem)整数线性规划 (Integer Linear Programming, ILP) 问题。我们可以构建一个时空网络图来表示所有可能的飞机移动。

模型构建

  1. 节点 (Nodes)
    • 航班节点:每个航班段的出发和到达都可以看作节点。更常见的是,将每个航班段本身看作一个需要被“服务”的需求。
    • 地面节点:在每个机场的每个时间点,可以设置地面节点,表示飞机在该时刻处于该机场的地面状态。
    • 源点 (Source) 和 汇点 (Sink):表示飞机的初始和最终状态。
  2. 弧 (Arcs)
    • 飞行弧 (Flight Arcs):连接航班出发地和目的地的节点,表示执行该航班。每条飞行弧对应一个航班需求。
    • 地面等待弧 (Ground Arcs):连接同一机场不同时间点的地面节点,表示飞机在地面停留。
    • 调机弧 (Ferry Arc):如果允许空载调机,可以连接不同机场的节点。
  3. 费用 (Costs)
    • 飞行弧的费用可以是运营成本(燃油、机组等)。
    • 地面等待弧的费用可以是固定的地面费用或为了惩罚过长的等待时间。
    • 目标通常是总费用最小化。
  4. 约束 (Constraints)
    • 流量守恒:对于每架飞机,其路径必须是连续的(从源点出发,经过一系列飞行和地面弧,最终到达汇点)。
    • 需求满足:每个航班必须被恰好一架飞机服务(即每个航班弧的流量为1)。
    • 飞机数量:使用的飞机总数不能超过机队规模。

1.3 实战解题思路与代码示例

我们使用Python的PuLP库来解决一个简化的航班调度问题。假设我们有2架飞机,3个航班需要调度。

问题设定

  • 航班
    • F1: A -> B, 08:00 - 10:00
    • F2: B -> C, 11:00 - 13:00
    • F3: C -> A, 14:00 - 16:00
  • 飞机:P1, P2,初始都在A机场。
  • 约束:地面周转时间至少1小时。
  • 目标:最小化总飞行时间(或成本),并尽可能让飞机完成闭环(回到A)。
import pulp

# 1. 定义问题
prob = pulp.LpProblem("Flight_Scheduling", pulp.LpMinimize)

# 2. 定义数据
# 航班数据: (航班ID, 出发地, 目的地, 出发时间, 到达时间)
flights = [
    ('F1', 'A', 'B', 8, 10),
    ('F2', 'B', 'C', 11, 13),
    ('F3', 'C', 'A', 14, 16)
]

# 飞机数据
planes = ['P1', 'P2']
initial_location = {'P1': 'A', 'P2': 'A'}

# 3. 定义决策变量
# x[plane][flight] = 1 如果飞机plane执行航班flight, 否则为0
x = pulp.LpVariable.dicts("assign", 
                          ((p, f[0]) for p in planes for f in flights), 
                          cat='Binary')

# 4. 定义目标函数
# 最小化总飞行时间 (这里简化为每个航班的固定成本,实际中可以是更复杂的成本)
prob += pulp.lpSum([x[p, f[0]] for p in planes for f in flights])

# 5. 定义约束

# 约束1: 每个航班必须被恰好一架飞机执行
for f in flights:
    prob += pulp.lpSum([x[p, f[0]] for p in planes]) == 1

# 约束2: 飞机路径的连续性(简化版:检查时间冲突和位置)
# 这是一个简化的约束,实际中需要更复杂的图模型来保证路径连续性。
# 这里我们只检查单架飞机不能同时执行两个航班。
# 更严格的约束需要考虑飞机的时空路径。
for p in planes:
    for i in range(len(flights)):
        for j in range(i + 1, len(flights)):
            f1 = flights[i]
            f2 = flights[j]
            # 如果f1的到达时间 + 周转时间 > f2的出发时间,则不能由同一架飞机执行
            # 假设周转时间为1小时
            if f1[4] + 1 > f2[3]:
                prob += x[p, f1[0]] + x[p, f2[0]] <= 1

# 约束3: 飞机初始位置约束 (简化:如果飞机初始不在出发地,则不能执行第一个航班)
# 这个约束在简单问题中可能不明显,但在复杂网络中很重要。
# 例如,如果P1在A,F1从A出发,P1可以执行。如果F2从B出发,P1执行完F1后可以在B。
# 这个约束通常通过构建时空图来自然满足,这里我们省略。

# 6. 求解
prob.solve()

# 7. 输出结果
print("Status:", pulp.LpStatus[prob.status])
for p in planes:
    assigned_flights = [f[0] for f in flights if pulp.value(x[p, f[0]]) == 1]
    if assigned_flights:
        print(f"飞机 {p} 执行航班: {', '.join(assigned_flights)}")

# 预期结果示例 (可能因求解器而异):
# 飞机 P1 执行航班: F1, F2, F3  (如果时间允许闭环)
# 或者
# 飞机 P1 执行航班: F1, F3
# 飞机 P2 执行航班: F2

代码解释

  • 我们定义了决策变量x[p, f]来表示飞机p是否执行航班f
  • 目标函数简单地最小化了执行的航班数量(因为每个航班必须被执行,这会变成一个常数,实际中应最小化成本,如燃油、过夜成本等)。
  • 约束1确保每个航班都被覆盖。
  • 约束2是一个简化的“无冲突”约束,防止一架飞机在同一时间执行多个航班。它检查了航班之间的时间重叠。更完善的模型会使用网络流或更复杂的ILP公式来确保路径的连续性(即飞机执行完F1后必须在B,才能执行从B出发的F2)。

2. 机组排班优化 (Crew Scheduling Optimization)

机组排班(Crew Scheduling)是航空业中公认的最复杂、最具挑战性的优化问题之一。它通常分为两个阶段:机组生成 (Crew Pairing)机组分配 (Crew Rostering)

2.1 问题描述与挑战

核心问题:在航班调度确定后,如何为航班分配飞行员、乘务员等机组人员,确保每个航班都有足够且合格的机组,同时遵守复杂的劳动法规、工会协议和安全规定。

主要挑战

  • 法规复杂性:各国和各航空公司都有严格的飞行时间、休息时间、连续工作天数等规定(如FAA、EASA规定)。
  • 成本高昂:机组成本是航空公司第二大运营成本(仅次于燃油),包括工资、住宿、差旅费等。
  • 大规模组合优化:可能的排班方案数量极其庞大。
  • 多目标性:需要平衡成本、公平性(工作负荷均衡)、员工偏好等。

2.2 建模思路:集合覆盖/分割模型

机组排班通常先通过集合覆盖 (Set Covering)集合分割 (Set Partitioning) 模型来生成高效的“航班串”(Pairings),然后再进行具体的人员排班。

模型构建 (Pairing Generation)

  1. 航班串 (Pairings):一个航班串是一系列航班的组合,代表一个机组从一个基地出发,执行一系列航班,最终返回同一基地(或另一个基地并过夜)的完整工作周期。
  2. 决策变量x_p = 1 如果选择航班串p,否则为0。
  3. 目标函数:最小化总成本(每个航班串有一个成本,包括工资、过夜费等)。
  4. 约束
    • 覆盖约束:每个航班必须被至少一个选中的航班串覆盖(集合覆盖)或恰好一个(集合分割)。
    • 复杂约束:航班串本身必须满足所有法规和休息要求(这些约束在生成航班串时就要满足)。

这是一个列生成 (Column Generation) 问题,因为航班串的数量是指数级的,无法预先全部列出。通常使用启发式算法(如分支定价 Branch-and-Price)来求解。

2.3 实战解题思路与代码示例

由于完整的机组排班代码极其复杂,我们展示一个简化的集合分割模型,假设我们已经生成了一些可行的航班串。

问题设定

  • 航班:F1, F2, F3 (同上)
  • 生成的航班串 (Pairings)
  • 目标:选择航班串覆盖所有航班,且总成本最低。
import pulp

# 1. 定义问题
prob = pulp.LpProblem("Crew_Pairing", pulp.LpMinimize)

# 2. 定义数据
flights = ['F1', 'F2', 'F3']
# pairings: {pairing_id: (flights_list, cost)}
pairings = {
    'P1': (['F1', 'F2'], 100),
    'P2': (['F3'], 50),
    'P4': (['F2', 'F3'], 110)
}

# 3. 定义决策变量
# x[p] = 1 如果选择航班串p
x = pulp.LpVariable.dicts("pairing", pairings.keys(), cat='Binary')

# 4. 定义目标函数
# 最小化总成本
prob += pulp.lpSum([pairings[p][1] * x[p] for p in pairings])

# 5. 定义约束
# 每个航班必须被至少一个选中的航班串覆盖 (集合覆盖)
for f in flights:
    # 找到包含航班f的所有航班串
    containing_pairings = [p for p in pairings if f in pairings[p][0]]
    prob += pulp.lpSum([x[p] for p in containing_pairings]) >= 1

# 6. 求解
prob.solve()

# 7. 输出结果
print("Status:", pulp.LpStatus[prob.status])
selected_pairings = [p for p in pairings if pulp.value(x[p]) == 1]
total_cost = sum(pairings[p][1] for p in selected_pairings)
print(f"选中的航班串: {selected_pairings}")
print(f"总成本: {total_cost}")

# 预期结果:
# 选中的航班串: ['P1', 'P2'] (覆盖F1,F2,F3, 成本150)
# 或者
# 选中的航班串: ['P4'] (覆盖F2,F3, 但F1未覆盖,所以不可行,需要覆盖约束)
# 或者
# 选中的航班串: ['P1', 'P2'] 是最优解。
# 注意:P4覆盖F2,F3,P2覆盖F3,但F1未被覆盖,所以需要P1或另一个覆盖F1的串。
# 如果只有P1,P2,P4,最优解是P1+P2=150。
# 如果有P5: [F1] (成本60),则P5+P4=160 > P1+P2=150。

代码解释

  • 这个模型展示了集合分割的核心思想。
  • 决策变量x[p]表示是否选择一个预先生成的航班串。
  • 目标是最小化成本。
  • 约束确保每个航班都被覆盖。
  • 在实际应用中,航班串是通过复杂的图搜索算法动态生成的,并使用列生成技术迭代求解。

3. 航线网络设计 (Airline Network Design)

航线网络设计是战略层面的决策,决定了航空公司服务哪些城市对(OD),以及使用何种航线结构(点对点、枢纽辐射)。

3.1 问题描述与挑战

核心问题:在给定的潜在市场(城市对)和需求预测下,选择一组航线和航班频率,以最大化利润或市场份额,同时受限于机队资源和机场容量。

主要挑战

  • 需求依赖性:航线的需求往往依赖于网络的连通性(通过枢纽的中转客流)。
  • 竞争:需要考虑竞争对手的航线和定价。
  • 长期投资:飞机采购、航线申请等决策具有长期影响。
  • 多目标:平衡网络覆盖率、运营成本、收益和风险。

3.2 建模思路:混合整数规划 (MIP)

航线网络设计通常使用混合整数规划模型,结合需求预测模型(如Logit模型)。

模型构建

  1. 决策变量
    • y_ij:是否开通城市对ij的直飞航线(二进制)。
    • f_ij:航线ij的航班频率(整数或连续)。
    • q_ik:从起点i到终点k的乘客需求(可能通过枢纽j中转)。
  2. 目标函数:最大化总利润 = 总收益 - 总成本。
    • 收益 = sum(需求 * 票价)
    • 成本 = sum(航线固定成本 * y_ij + 单位航班成本 * f_ij)
  3. 约束
    • 需求满足:乘客需求必须被网络覆盖(直飞或中转)。
    • 流量守恒:在枢纽节点,流入等于流出。
    • 容量限制:机场起降架次、飞机座位数限制。
    • 机队约束:总航班量不能超过机队总飞行能力。

3.3 实战解题思路与代码示例

我们模拟一个简化的枢纽辐射网络设计问题。假设有4个城市,我们决定是否将其作为枢纽,并连接哪些航线。

问题设定

  • 城市:A, B, C, D
  • 需求矩阵 (简化,仅直飞需求,单位:乘客/天):
    • A->B: 100, A->C: 150, A->D: 80
    • B->C: 120, B->D: 90
    • C->D: 110
  • 成本:开通一条航线固定成本500,每个航班(每日)运营成本100。
  • 收益:每个乘客贡献收益2。
  • 约束:如果开通航线,至少每日1班。如果需求>200,可以增加班次。
import pulp

# 1. 定义问题
prob = pulp.LpProblem("Network_Design", pulp.LpMaximize)

# 2. 定义数据
cities = ['A', 'B', 'C', 'D']
# 需求 (i, j): demand
demands = {
    ('A', 'B'): 100, ('A', 'C'): 150, ('A', 'D'): 80,
    ('B', 'C'): 120, ('B', 'D'): 90,
    ('C', 'D'): 110
}
# 成本参数
fixed_cost = 500
variable_cost_per_flight = 100
revenue_per_passenger = 2

# 3. 定义决策变量
# y[i][j] = 1 如果开通i->j航线
y = pulp.LpVariable.dicts("route", ((i, j) for i in cities for j in cities if i != j), cat='Binary')
# f[i][j] = i->j的航班频率 (每日班次)
f = pulp.LpVariable.dicts("frequency", ((i, j) for i in cities for j in cities if i != j), lowBound=0, cat='Integer')

# 4. 定义目标函数
# 利润 = 收益 - (固定成本 + 可变成本)
# 收益 = sum(需求 * 收益率) * (如果航线开通且频率足够)
# 这里简化:假设需求能被完全满足,只要航线开通且有足够频率。
# 更复杂的模型会考虑需求分配和频率对需求的影响。
# 我们假设每个航班能服务200个乘客。
# 总收益 = sum( min(需求, 200 * f_ij) * revenue_per_passenger ) - sum(fixed_cost * y_ij + variable_cost * f_ij)

# 简化目标:最大化覆盖的需求 - 成本
# 假设只要开通航线(f_ij >=1),就能满足需求。
# 我们用一个简化的目标:最大化 (覆盖的需求 * 收益 - 成本)
# 但需求覆盖需要频率变量。我们假设频率为1时,能服务150乘客,频率为2时,服务300,以此类推。
# 为了简化,我们让目标直接与频率和航线开通挂钩。

prob += pulp.lpSum([revenue_per_passenger * demands.get((i,j),0) * y[i,j] for i,j in demands]) \
        - pulp.lpSum([fixed_cost * y[i,j] + variable_cost_per_flight * f[i,j] for i,j in demands])

# 5. 定义约束

# 约束1: 如果开通航线,频率至少为1
for i,j in demands:
    prob += f[i,j] >= y[i,j]

# 约束2: 频率上限 (简化,假设最大2班/天)
for i,j in demands:
    prob += f[i,j] <= 2

# 约束3: 需求满足 (如果需求 > 0,则必须开通航线,即 y[i,j] = 1)
# 这是一个强约束,实际中可能更灵活(通过中转)。
# 这里我们强制直飞需求必须由直飞航线满足。
for i,j in demands:
    if demands[(i,j)] > 0:
        prob += y[i,j] == 1

# 6. 求解
prob.solve()

# 7. 输出结果
print("Status:", pulp.LpStatus[prob.status])
total_profit = pulp.value(prob.objective)
print(f"总利润: {total_profit}")
for i,j in demands:
    if pulp.value(y[i,j]) == 1:
        print(f"航线 {i}->{j}: 频率 {pulp.value(f[i,j])} 班/天")

# 预期结果:
# 由于需求>0就必须开通,所有航线都会开通,频率根据利润最大化决定。
# 检查利润:收益 = (100+150+80+120+90+110)*2 = 1300
# 成本 = 6条航线 * 500 + (1+1+1+1+1+1)*100 = 3000 + 600 = 3600
# 利润 = 1300 - 3600 = -2300 (亏损)
# 这说明在当前成本和收益下,开通所有航线是亏损的。
# 模型应该选择性地开通航线。
# 让我们修改约束3,让模型自由选择是否开通。

# --- 重新求解,放松约束 ---
prob2 = pulp.LpProblem("Network_Design_Free", pulp.LpMaximize)
y2 = pulp.LpVariable.dicts("route", ((i, j) for i in cities for j in cities if i != j), cat='Binary')
f2 = pulp.LpVariable.dicts("frequency", ((i, j) for i in cities for j in cities if i != j), lowBound=0, cat='Integer')

# 目标函数同上
prob2 += pulp.lpSum([revenue_per_passenger * demands.get((i,j),0) * y2[i,j] for i,j in demands]) \
        - pulp.lpSum([fixed_cost * y2[i,j] + variable_cost_per_flight * f2[i,j] for i,j in demands])

# 约束1: 如果开通,频率>=1
for i,j in demands:
    prob2 += f2[i,j] >= y2[i,j]
# 约束2: 频率上限
for i,j in demands:
    prob2 += f2[i,j] <= 2

# 求解
prob2.solve()
print("\n--- 优化后网络设计 ---")
print("Status:", pulp.LpStatus[prob2.status])
total_profit = pulp.value(prob2.objective)
print(f"总利润: {total_profit}")
for i,j in demands:
    if pulp.value(y2[i,j]) == 1:
        print(f"航线 {i}->{j}: 频率 {pulp.value(f2[i,j])} 班/天")

# 预期结果:
# 模型会选择性地开通利润最高的航线。
# 例如,A->C (需求150, 收益300, 成本500+100=600, 亏损300)
# 似乎所有航线单独看都是亏损的 (收益<成本)。
# 这说明我们的成本参数设置得太高,或者收益太低。
# 让我们调整参数:固定成本200,可变成本50,收益不变。
# A->C: 收益300, 成本200+50=250, 利润50。
# 重新计算:
# A->B: 200 - 250 = -50
# A->C: 300 - 250 = 50
# A->D: 160 - 250 = -90
# B->C: 240 - 250 = -10
# B->D: 180 - 250 = -70
# C->D: 220 - 250 = -30
# 只有A->C是盈利的。
# 模型应该只选择A->C。
# 让我们用新参数运行代码:
fixed_cost = 200
variable_cost_per_flight = 50
prob3 = pulp.LpProblem("Network_Design_Optimal", pulp.LpMaximize)
y3 = pulp.LpVariable.dicts("route", ((i, j) for i in cities for j in cities if i != j), cat='Binary')
f3 = pulp.LpVariable.dicts("frequency", ((i, j) for i in cities for j in cities if i != j), lowBound=0, cat='Integer')
prob3 += pulp.lpSum([revenue_per_passenger * demands.get((i,j),0) * y3[i,j] for i,j in demands]) \
        - pulp.lpSum([fixed_cost * y3[i,j] + variable_cost_per_flight * f3[i,j] for i,j in demands])
for i,j in demands:
    prob3 += f3[i,j] >= y3[i,j]
    prob3 += f3[i,j] <= 2
prob3.solve()
print("\n--- 调整参数后优化 ---")
print("Status:", pulp.LpStatus[prob3.status])
print(f"总利润: {pulp.value(prob3.objective)}")
for i,j in demands:
    if pulp.value(y3[i,j]) == 1:
        print(f"航线 {i}->{j}: 频率 {pulp.value(f3[i,j])} 班/天")

# 最终预期结果:
# 只有 A->C 被选中,频率为1,利润50。

代码解释

  • 这个例子展示了如何使用MIP进行网络设计。
  • 关键在于决策变量y(是否开通)和f(频率)的联动。
  • 目标函数平衡了收益和成本。
  • 通过调整成本和收益参数,我们可以看到模型如何选择最优的航线组合。这反映了战略决策的权衡。

4. 收益管理 (Revenue Management / Pricing)

收益管理是航空公司实现利润最大化的关键战术工具。其核心是在合适的时间、以合适的价格,将合适的座位卖给合适的乘客。

4.1 问题描述与挑战

核心问题:在航班座位有限且需求不确定的情况下,如何动态调整票价和座位分配(超售),以最大化航班总收入。

主要挑战

  • 需求不确定性:不同票价等级的需求是随机的,且相互影响。
  • 易逝性:座位的价值随时间推移而迅速下降(航班起飞后,空座位的价值为零)。
  • 多等级票价:不同票价等级之间存在复杂的替代和互补关系。
  • 竞争动态:竞争对手的定价策略会影响自身需求。

4.2 建模思路:动态规划 (Dynamic Programming) / 网络流量控制

收益管理中最经典的模型是期望边际座位收入 (Expected Marginal Seat Revenue, EMSR) 模型,它基于动态规划的思想。

模型构建

  1. 状态:剩余座位数,剩余时间。
  2. 决策:对于一个新来的预订请求(某个票价等级),决定接受还是拒绝。
  3. 价值函数V(s, t) 表示在时间t剩余s个座位时,未来能获得的最大期望收入。
  4. 贝尔曼方程V(s, t) = max { p_accept * (p + V(s-1, t+1)) + (1 - p_accept) * V(s, t+1) } 其中p_accept是接受请求的概率(或决策),p是票价。 更常见的简化是计算保护水平 (Protection Levels):为高价票保留的座位数。

4.3 实战解题思路与代码示例

我们使用EMSRb模型(EMSR的改进版)来计算为高价票保留的座位数。

问题设定

  • 航班:A->B,总座位数 C = 100。
  • 票价等级
    • Y (全价): 票价 $200,预计需求 30 (标准差 10)。
    • M (中价): 票价 $150,预计需求 50 (标准差 15)。
    • Q (低价): 票价 $100,预计需求 80 (标准差 20)。
  • 目标:为Y和M等级计算保护水平,即至少保留多少座位给这些等级的乘客。
import scipy.stats
import numpy as np

def calculate_protection_level(total_capacity, fare_high, demand_high, std_high, fare_low, demand_low, std_low):
    """
    计算为高价票保护的座位数 (EMSRb 简化逻辑)
    逻辑:比较增加一个低价票的期望收益 vs 保护一个座位给高价票的期望收益
    """
    # 保护水平 P
    # 我们需要找到一个保护水平 P,使得:
    # 如果低价票需求 > (总容量 - P),则拒绝低价票,保留座位给高价票。
    # 简单的启发式:计算高价票的期望收益,然后看低价票需求分布,找到一个点,
    # 在这个点上,卖出一个低价票的边际收益 < 损失一个高价票的期望收益。
    
    # 简化版 EMSRb:
    # 保护水平 P 满足: P = F^{-1}( (fare_high - fare_low) / fare_high )
    # 其中 F 是高价票需求的累积分布函数。
    # 这个公式假设低价票需求总是足够大。
    
    # 更准确的 EMSRb (针对两个等级):
    # 保护水平 P* 满足: Prob(Demand_High >= P*) = (fare_high - fare_low) / fare_high
    # 即:高价票需求超过 P* 的概率 = 保护一个座位的临界概率。
    
    critical_ratio = (fare_high - fare_low) / fare_high
    
    # 使用高价票需求的分布 (正态分布)
    # P = mean + z * std
    # z = norm.ppf(critical_ratio)
    z = scipy.stats.norm.ppf(critical_ratio)
    protection_level = demand_high + z * std_high
    
    # 保护水平不能超过总容量,也不能小于0
    protection_level = max(0, min(total_capacity, protection_level))
    
    return protection_level

# 1. 计算 Y 等级对 M 等级的保护水平
# 假设我们按票价从高到低处理:先处理Y,再处理M,最后处理Q。
# 我们需要计算:
# - 为 Y 保护多少座位 (相对于 M 和 Q)
# - 为 M 保护多少座位 (相对于 Q)

# 场景1: Y vs M+Q (保护Y)
# 我们将 M 和 Q 视为一个整体 "低价票" 市场。
# 低价票平均票价 = (150*50 + 100*80) / (50+80) = 125
# 低价票需求 = 50+80 = 130
# 低价票标准差 = sqrt(15^2 + 20^2) = sqrt(225+400) = sqrt(625) = 25 (假设独立)
# 但通常我们逐级计算。

# 逐级计算:
# Level 1: Y vs (M+Q)
fare_Y = 200
demand_Y = 30
std_Y = 10

# 合并低价票 (M+Q)
fare_MQ_avg = (150*50 + 100*80) / (50+80) # 约 125
demand_MQ = 50 + 80 # 130
std_MQ = np.sqrt(15**2 + 20**2) # 25

protection_Y = calculate_protection_level(100, fare_Y, demand_Y, std_Y, fare_MQ_avg, demand_MQ, std_MQ)
print(f"为 Y 等级保护的座位数: {protection_Y:.0f}")

# Level 2: M vs Q (在非Y座位中保护M)
# 剩余座位 = 100 - protection_Y
remaining_capacity = 100 - protection_Y
fare_M = 150
demand_M = 50
std_M = 15
fare_Q = 100
demand_Q = 80
std_Q = 20

protection_M = calculate_protection_level(remaining_capacity, fare_M, demand_M, std_M, fare_Q, demand_Q, std_Q)
print(f"为 M 等级保护的座位数 (在非Y座位中): {protection_M:.0f}")

# 最终座位分配策略:
# - Y 等级最多可售: protection_Y (约 30-40 个座位)
# - M 等级最多可售: protection_M (在剩余座位中)
# - Q 等级可售: 剩余所有座位

# 解释结果:
# 保护水平的计算基于概率。如果高价票需求超过保护水平的概率很高,
# 那么拒绝低价票、保留座位给高价票的期望收益就更高。
# critical_ratio = (200-125)/200 = 0.375
# z = norm.ppf(0.375) ≈ -0.32
# protection_Y = 30 + (-0.32)*10 ≈ 27
# 这意味着,如果低价票需求很大,我们应该保留大约27个座位给Y等级。
# 如果Y等级实际需求只有30,这个保护水平是合理的。
# 如果Y等级需求很低,我们可能会卖出更多低价票。

# 让我们打印更详细的解释
print("\n--- 收益管理策略解释 ---")
print(f"总座位: 100")
print(f"Y等级 (票价$200, 需求~30): 保护水平 {protection_Y:.0f}。意味着最多向低价市场释放 100-{protection_Y:.0f} = {100-protection_Y:.0f} 个座位。")
print(f"M等级 (票价$150, 需求~50): 在非Y座位中保护 {protection_M:.0f}。意味着在 {100-protection_Y:.0f} 个座位中,最多向Q等级释放 {100-protection_Y:.0f}-{protection_M:.0f} 个座位。")
print(f"Q等级 (票价$100, 需求~80): 可售座位 = 总座位 - Y保护 - M保护 = {100 - protection_Y - protection_M:.0f}")

代码解释

  • 我们使用了EMSRb模型的核心思想,即通过计算保护水平来决定座位分配。
  • calculate_protection_level 函数计算了在给定票价和需求分布下,应该为高价票保留多少座位。
  • 这是一个简化的静态模型。实际的收益管理系统是动态的,会根据实时预订情况不断更新需求预测和保护水平。
  • 这个例子展示了如何利用统计学(正态分布)和经济学(边际收益)来解决定价和座位分配问题。

5. 总结

航空公司的优化建模是一个庞大而精深的领域,涵盖了从战略规划到日常运营的方方面面。本文通过四个核心主题——航班调度、机组排班、航线网络设计和收益管理——深入探讨了其面临的挑战、建模思路和实战解题方法。

  • 航班调度 关注飞机资源的有效利用,通常建模为网络流或整数规划问题。
  • 机组排班 极其复杂,涉及严格的法规约束,常使用集合覆盖模型和列生成算法。
  • 航线网络设计 是战略决策,需要平衡市场需求、成本和竞争,通常使用混合整数规划。
  • 收益管理 是战术层面的利润最大化工具,利用动态规划和统计学方法来动态定价和分配座位。

通过Python代码示例,我们展示了如何将这些复杂的数学模型转化为可执行的代码,从而解决实际的航空业务问题。这些技术不仅提高了航空公司的运营效率和盈利能力,也深刻地改变了现代航空业的运作方式。随着人工智能和大数据技术的发展,未来的航空优化将更加智能、精准和实时。