Airline Revenue Management: an introduction¶
The example is from Andirs Moller, Werner Romisch, Klaus Weber, Airline Network Revenue Management by Multistage Stochastic Programming.
An airline company manages flights between three cities \(A,B,C\). In the middle of the three cities, there is a hub serves as a transition point. The company wants to determine the seat protection level for all itineraries, fare classes to maximize the revenue over a horizon of 14 stages. Every stage corresponds to a departure date. Cancellation rate \(\Gamma\) is deterministic. Demand is random.
Notation¶
Formulation¶
Where \(A_{kl}\) is a 0-1 matrix that indicates whether a booking request for a particular itinerary and fare class fills up one unit of cabin capacity in a flight leg. For example, if a booking request is made for class B2 of itinerary AHB. Since this request fills up one unit of business cabin in both flight leg AH and flight leg HB, Entry (AHB,B2) in matrix \(A_{B,AH}\) and \(A_{B,HB}\) is one.
Data¶
[1]:
import numpy, pandas
from scipy.stats import beta
import seaborn
seaborn.set_style('darkgrid')
[2]:
I = ['AH','HA','BH','HB','CH','HC','AHB','BHA','AHC','CHA','BHC','CHB']
J = ['B1','B2','E1','E2','E3','E4']
L = ['AH','HA','BH','HB','CH','HC']
K = ['B','E']
Rate = {'AH':0.1,'HA':0.1,'BH':0.1,'HB':0.1,'CH':0.05,'HC':0.05,'AHB':0.05,'BHA':0.05,'AHC':0,'CHA':0,'BHC':0,'CHB':0}
R = {'B':24, 'E':215}
There are 14 departure dates thus the problem has 14 stages. We will generate 100 samples for each stage. A random seed is set.
[3]:
day = [182,126,84,56,35,21,14,10,7,5,3,2,1,0]
T = 14
Model the uncertain demand¶
[4]:
airFare = pandas.read_csv("data/airFare.csv", index_col = [0,1])
The first two columns give parameters of Gamma distribution; the next two columns give parameters of Beta distribution; the last column gives the prices.
[5]:
airFare.head(n = 12)
[5]:
| k | theta | a | b | fare | ||
|---|---|---|---|---|---|---|
| i | j | |||||
| AH | B1 | 3.00 | 1.5 | 12 | 1.5 | 500 |
| B2 | 3.00 | 1.5 | 8 | 2.0 | 340 | |
| E1 | 10.00 | 1.2 | 6 | 2.0 | 200 | |
| E2 | 13.33 | 1.2 | 4 | 3.0 | 160 | |
| E3 | 22.00 | 1.0 | 3 | 4.0 | 130 | |
| E4 | 30.00 | 1.0 | 2 | 4.0 | 100 | |
| HA | B1 | 3.00 | 1.5 | 12 | 1.5 | 500 |
| B2 | 3.00 | 1.5 | 8 | 2.0 | 340 | |
| E1 | 10.00 | 1.2 | 6 | 2.0 | 200 | |
| E2 | 13.33 | 1.2 | 4 | 3.0 | 160 | |
| E3 | 22.00 | 1.0 | 3 | 4.0 | 130 | |
| E4 | 30.00 | 1.0 | 2 | 4.0 | 100 |
[6]:
def demand_sampler(random_state, size):
demand = numpy.zeros([size,T,len(I)*len(J)])
for i_idx,i in enumerate(I):
for j_idx,j in enumerate(J):
gamma_k,gamma_theta,beta_a,beta_b = airFare.loc[i,j][:4]
G = random_state.gamma(gamma_k, gamma_theta, size=size) * (1-Rate[i])
for t in range(1,T):
B = (
beta.cdf(1-day[t]/day[0], beta_a, beta_b)
- beta.cdf(1-day[t-1]/day[0], beta_a, beta_b)
)
demand[:,t,6*i_idx+j_idx] = random_state.poisson(G*B)
return demand
[7]:
airFare.head(10)
[7]:
| k | theta | a | b | fare | ||
|---|---|---|---|---|---|---|
| i | j | |||||
| AH | B1 | 3.00 | 1.5 | 12 | 1.5 | 500 |
| B2 | 3.00 | 1.5 | 8 | 2.0 | 340 | |
| E1 | 10.00 | 1.2 | 6 | 2.0 | 200 | |
| E2 | 13.33 | 1.2 | 4 | 3.0 | 160 | |
| E3 | 22.00 | 1.0 | 3 | 4.0 | 130 | |
| E4 | 30.00 | 1.0 | 2 | 4.0 | 100 | |
| HA | B1 | 3.00 | 1.5 | 12 | 1.5 | 500 |
| B2 | 3.00 | 1.5 | 8 | 2.0 | 340 | |
| E1 | 10.00 | 1.2 | 6 | 2.0 | 200 | |
| E2 | 13.33 | 1.2 | 4 | 3.0 | 160 |
Solution¶
[8]:
from msppy.msp import MSIP
from msppy.solver import SDDiP
import gurobipy
from msppy.evaluation import Evaluation, EvaluationTrue
Consider using Strengthened Benders’ cuts to solve the MSIP. For illustration purpose, the Markov chain approximation below is very coarse and I only run SDDiP for 10 iterations.
[9]:
airline = MSIP(T, sense=-1)
airline.add_Markovian_uncertainty(demand_sampler)
for t in range(T):
m = airline[t]
# accumulated fulfilled booking requests
B_now, B_past = m.addStateVars(I,J, vtype='I', name="B")
# accumulated cancellation
C_now, C_past = m.addStateVars(I,J, vtype='I', name="C")
if t == 0:
m.addConstrs(B_now[(i,j)] == 0 for i in I for j in J)
m.addConstrs(C_now[(i,j)] == 0 for i in I for j in J)
else:
# new fulfilled booking requests
b = m.addVars(I,J, obj=numpy.array(airFare['fare']), name="b", vtype='I')
c = m.addVars(I,J, obj=-numpy.array(airFare['fare']), name="c", vtype='I')
# accumulated fulfilled booking requests updated
m.addConstrs(B_now[(i,j)] == B_past[(i,j)] + b[(i,j)] for i in I for j in J)
m.addConstrs(C_now[(i,j)] == C_past[(i,j)] + c[(i,j)] for i in I for j in J)
# number of cancellation is depending on the cancellation rate
m.addConstrs(C_now[(i,j)] <= Rate[i] * B_now[(i,j)] + 0.5 for i in I for j in J)
m.addConstrs(C_now[(i,j)] >= Rate[i] * B_now[(i,j)] - 0.5 for i in I for j in J)
# capacity constraint
m.addConstrs(
gurobipy.quicksum(
B_now[(i,j)] - C_now[(i,j)] for i in I for j in J
if l in i and j.startswith(k)
)
<= R[k] for k in K for l in L
)
m.addConstrs(
(b[(i,j)] <= 0 for i in I for j in J),
uncertainty_dependent=[
6*i+j for i in range(len(I)) for j in range(len(J))
]
)
airline.discretize(
method='SA',
n_Markov_states=10,
n_sample_paths=10*1000,
int_flag=1,
)
airline_sddp = SDDiP(airline)
airline_sddp.solve(cuts=['SB'], max_iterations=30, logToConsole=0)
result = Evaluation(airline)
result.run(n_simulations=100)
resultTrue = EvaluationTrue(airline)
resultTrue.run(n_simulations=100)
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
Academic license - for non-commercial use only
[10]:
airline_sddp.plot_bounds();