Heuristic Intervention¶
In this example, we will demonstrate the use of a heuristic intervention. These interventions are not based on rigorous forecasts or formal optimization, but instead are just heuristic tweaks that try to improve revenue by changing parts of the RM system. For example, the heuristic we demonstrate here is a booked load factor curve adjustment, which takes an exogenously provided booked load factor curve for individual legs, and nudges the mean forecast up or down if the leg's actual bookings are running much higher or lower than expected at any point in the booking curve. This is meant to replicate what a human analyst might do in a similar situation.
import pandas as pd
import passengersim as pax
pax.versions()
passengersim 0.62 passengersim.core 0.62.dev10+gb28bd899
We'll start by loading the standard 3MKT demo model.
cfg = pax.Config.from_yaml(pax.demo_network("3MKT/DEMO"))
The heuristic adjustment is made using a RmAction, which works just like any other RmAction
when it is inserted in a RmSys's action list.
from passengersim.rm.emsr import ExpectedMarginalSeatRevenue
from passengersim.rm.heuristic_adjustments.booked_lf import BookedLoadFactorAdjustment
from passengersim.rm.standard_forecasting import StandardLegForecast
from passengersim.rm.systems import RmSys, RmSysOption, register_rm_system
from passengersim.rm.untruncation import LegUntruncation
@register_rm_system
class EX(RmSys):
availability_control = "leg"
"""This RM system uses leg-level class allocation availability controls."""
actions = [
LegUntruncation,
StandardLegForecast,
BookedLoadFactorAdjustment,
ExpectedMarginalSeatRevenue,
]
The BookedLoadFactorAdjustment uses a set of booked load factor curves, which are stored
in a configuration's other_controls attribute. This attribute allows users to set arbitrary
other controls, which can be loaded, validated, and used by RmAction steps that call them.
For the booked load factor adjustment, the needed inputs are stored under the
"booked_load_factor_curves" key. These curves are already included in the demo configuration,
so we don't need to do anything special to make them available.
cfg.other_controls["booked_load_factor_curves"]
{'BOSTON': {'lower_bound': {0: 0.64,
1: 0.64,
2: 0.61,
3: 0.59,
4: 0.56,
5: 0.54,
6: 0.53,
7: 0.52,
8: 0.5,
9: 0.48,
10: 0.47,
11: 0.46,
12: 0.45,
13: 0.44,
14: 0.43,
15: 0.42,
16: 0.41,
17: 0.4,
18: 0.4,
19: 0.39,
20: 0.38,
21: 0.37,
22: 0.36,
23: 0.34,
24: 0.33,
25: 0.31,
26: 0.31,
27: 0.3,
28: 0.28,
29: 0.28,
30: 0.27,
31: 0.27,
32: 0.26,
33: 0.25,
34: 0.24,
35: 0.23,
36: 0.22,
37: 0.21,
38: 0.2,
39: 0.19,
40: 0.18,
41: 0.18,
42: 0.17,
43: 0.16,
44: 0.16,
45: 0.15,
46: 0.14,
47: 0.14,
48: 0.13,
49: 0.12,
50: 0.12,
51: 0.11,
52: 0.1,
53: 0.1,
54: 0.09,
55: 0.09,
56: 0.08,
57: 0.06,
58: 0.05,
59: 0.04,
60: 0.02,
61: 0.01,
62: 0.0,
63: 0.0},
'upper_bound': {0: 1.0,
1: 1.0,
2: 0.99,
3: 0.96,
4: 0.94,
5: 0.92,
6: 0.9,
7: 0.88,
8: 0.86,
9: 0.83,
10: 0.81,
11: 0.8,
12: 0.78,
13: 0.76,
14: 0.74,
15: 0.73,
16: 0.71,
17: 0.7,
18: 0.69,
19: 0.68,
20: 0.67,
21: 0.66,
22: 0.64,
23: 0.62,
24: 0.6,
25: 0.59,
26: 0.57,
27: 0.55,
28: 0.54,
29: 0.53,
30: 0.52,
31: 0.51,
32: 0.49,
33: 0.48,
34: 0.46,
35: 0.45,
36: 0.44,
37: 0.43,
38: 0.42,
39: 0.4,
40: 0.39,
41: 0.37,
42: 0.36,
43: 0.35,
44: 0.34,
45: 0.33,
46: 0.31,
47: 0.3,
48: 0.29,
49: 0.27,
50: 0.26,
51: 0.25,
52: 0.24,
53: 0.23,
54: 0.21,
55: 0.2,
56: 0.18,
57: 0.16,
58: 0.13,
59: 0.11,
60: 0.09,
61: 0.06,
62: 0.04,
63: 0.0}},
'CHICAGO': {'lower_bound': {0: 0.71,
1: 0.69,
2: 0.67,
3: 0.64,
4: 0.62,
5: 0.6,
6: 0.58,
7: 0.56,
8: 0.55,
9: 0.53,
10: 0.52,
11: 0.5,
12: 0.49,
13: 0.48,
14: 0.47,
15: 0.46,
16: 0.44,
17: 0.43,
18: 0.42,
19: 0.42,
20: 0.41,
21: 0.4,
22: 0.38,
23: 0.37,
24: 0.35,
25: 0.34,
26: 0.33,
27: 0.32,
28: 0.31,
29: 0.3,
30: 0.29,
31: 0.28,
32: 0.28,
33: 0.27,
34: 0.26,
35: 0.25,
36: 0.24,
37: 0.23,
38: 0.23,
39: 0.22,
40: 0.21,
41: 0.2,
42: 0.19,
43: 0.18,
44: 0.17,
45: 0.17,
46: 0.16,
47: 0.15,
48: 0.14,
49: 0.13,
50: 0.12,
51: 0.12,
52: 0.12,
53: 0.1,
54: 0.1,
55: 0.09,
56: 0.08,
57: 0.07,
58: 0.05,
59: 0.04,
60: 0.03,
61: 0.02,
62: 0.0,
63: 0.0},
'upper_bound': {0: 1.0,
1: 1.0,
2: 1.0,
3: 0.97,
4: 0.94,
5: 0.93,
6: 0.89,
7: 0.88,
8: 0.85,
9: 0.82,
10: 0.81,
11: 0.79,
12: 0.78,
13: 0.75,
14: 0.73,
15: 0.72,
16: 0.71,
17: 0.68,
18: 0.68,
19: 0.67,
20: 0.66,
21: 0.64,
22: 0.62,
23: 0.62,
24: 0.59,
25: 0.57,
26: 0.57,
27: 0.56,
28: 0.53,
29: 0.53,
30: 0.52,
31: 0.5,
32: 0.48,
33: 0.47,
34: 0.46,
35: 0.44,
36: 0.43,
37: 0.42,
38: 0.41,
39: 0.4,
40: 0.38,
41: 0.38,
42: 0.36,
43: 0.35,
44: 0.33,
45: 0.33,
46: 0.32,
47: 0.3,
48: 0.28,
49: 0.28,
50: 0.26,
51: 0.25,
52: 0.23,
53: 0.23,
54: 0.21,
55: 0.2,
56: 0.18,
57: 0.16,
58: 0.13,
59: 0.11,
60: 0.08,
61: 0.06,
62: 0.03,
63: 0.0}}}
You may not that this configuration is itself a dictionary, which has a couple of different curves stored under different keys:
cfg.other_controls["booked_load_factor_curves"].keys()
dict_keys(['BOSTON', 'CHICAGO'])
To tell the action which curve to use for which leg, we'll attach a 'BLF_Curve' tag to each leg, which tells the simulation which curve to use. For this example, we'll attach the "BOSTON" curve to the legs originating at BOS, and the "CHICAGO" tag to legs originating at ORD. This is our choice in assigning these tags; we could alternatively assign tags based on stage length, departure time of day, or any other leg attribute(s) we like. In the extreme, we could create and assign a unique curve to every individual leg in the entire simulation.
for leg in cfg.legs:
if leg.carrier == "AL1":
if leg.orig == "BOS":
leg.tags["BLF_Curve"] = "BOSTON"
if leg.orig == "ORD":
leg.tags["BLF_Curve"] = "CHICAGO"
We're now pretty much all set up and use this new RM system in a simulation in place of a standard "E" type system.
cfg.carriers.AL1.rm_system = "EX"
Let's run it and see what happens.
sim = pax.MultiSimulation(cfg)
summary = sim.run()
summary.fig_carrier_revenues()
summary.fig_carrier_load_factors()
summary.fig_fare_class_mix()
summary.fig_carrier_head_to_head_revenue("AL1", "AL2")