Lab 1¶
Does it say “notebook is read only” in a button on your toolbar? Is the “save” button greyed
out?
If you want to be able to run this notebook and save your work, you need to copy it to your user
directory. In the file browser on the left (click the folder icon), right click this notebook file
(lab-1.ipynb), copy it, then navigate to your user home directory, and right click and choose paste.
Then open that copy instead of the read-only copy, and you’ll be good to go.
import passengersim as pax
cfg = pax.Config.from_yaml(pax.demo_network("3MKT/DEMO"))
Understanding the Impact of RM¶
We are going to run a series of experiments, so we’ll set up an Experiments class to keep track of
and compare the results.
from passengersim.experiments import Experiments
experiments = Experiments(cfg, output_dir="lab-1-output")
First Come, First Served¶
First, let’s suppose that neither carrier uses any RM optimization, and just offers all fare classes as first come, first served, subject only to the existing restrictions (fare rules and advance purchase). To simulate this, we’ll create an RM system that does nothing and set the config so that both carriers use this system.
from typing import ClassVar
from passengersim.rm import RmAction, RmSys, register_rm_system
class Nothing(RmAction):
"""An RM step that does nothing."""
frequency = "dcp"
produces: ClassVar[set[str]] = {"bucket_allocations"}
# We don't actually produce bucket allocations, but as a safeguard against mistakes
# PassengerSim requires that an RM system that uses leg bucket availability controls
# must include an action that proclaims that it produces bucket allocations.
def run(self, sim: pax.Simulation, days_prior: int):
return
@register_rm_system
class Z(RmSys):
"""A custom RM system that applies zero optimization."""
availability_control = "leg"
actions: ClassVar[list] = [Nothing]
@experiments
def ZZ(cfg: pax.Config) -> pax.Config:
cfg.carriers.AL1.rm_system = "Z"
cfg.carriers.AL2.rm_system = "Z"
return cfg
summary = experiments.run()
Loaded ZZ from lab-1-output/ZZ/ZZ.20261027-144412.pxsim, but the PassengerSim version has changed: running 0.94, found 0.93
╭────────── Summary revenue statistics · ZZ ───────────╮ │ AL1 AL2 │ │ Run Samples Mean ~95% CI Mean ~95% CI │ │ ZZ 750 87,575 ±750 87,479 ±791 │ │ Post-burn-in only · Approx. 95% CI for mean │ ╰──────────────────────────────────────────────────────╯
summary.fig_carrier_revenues()
summary.fig_carrier_load_factors()
summary.fig_bookings_by_timeframe(by_carrier="AL1", by_class=True)
AL1 implements EMSR-B¶
Now let’s have one of the carriers implement EMSR-B, with EM demand untruncation and standard forecasting.
@experiments
def EZ(cfg: pax.Config) -> pax.Config:
cfg.carriers.AL1.rm_system = "E"
cfg.carriers.AL2.rm_system = "Z"
return cfg
summary = experiments.run()
Using cached results for experiment ZZ
Loaded EZ from lab-1-output/EZ/EZ.20261027-144416.pxsim, but the PassengerSim version has changed: running 0.94, found 0.93
╭────────── Summary revenue statistics · EZ ───────────╮ │ AL1 AL2 │ │ Run Samples Mean ~95% CI Mean ~95% CI │ │ EZ 750 94,820 ±1,109 82,137 ±695 │ │ Post-burn-in only · Approx. 95% CI for mean │ ╰──────────────────────────────────────────────────────╯
summary.fig_carrier_revenues()
summary.fig_carrier_load_factors()
summary.fig_bookings_by_timeframe(by_carrier="AL1", by_class=True, source_labels=True)
summary.fig_bookings_by_timeframe(by_carrier="AL2", by_class=True, source_labels=True)
Both Carriers implement EMSR-B¶
@experiments
def EE(cfg: pax.Config) -> pax.Config:
cfg.carriers.AL1.rm_system = "E"
cfg.carriers.AL2.rm_system = "E"
return cfg
summary = experiments.run()
Using cached results for experiment ZZ
Using cached results for experiment EZ
Loaded EE from lab-1-output/EE/EE.20261027-144419.pxsim, but the PassengerSim version has changed: running 0.94, found 0.93
╭────────── Summary revenue statistics · EE ───────────╮ │ AL1 AL2 │ │ Run Samples Mean ~95% CI Mean ~95% CI │ │ EE 750 90,429 ±870 90,655 ±912 │ │ Post-burn-in only · Approx. 95% CI for mean │ ╰──────────────────────────────────────────────────────╯
summary.fig_carrier_revenues()
summary.fig_bookings_by_timeframe(by_carrier="AL1", by_class=True, source_labels=True)
Understanding Relative Fares¶
For this section, we’ll start a new collection of experiments.
ex_fares = Experiments(cfg, output_dir="lab-1b-output")
We start with a baseline experiment where both carriers are using the “normal” fare structures and the “E” RM system.
@ex_fares
def Baseline(cfg: pax.Config) -> pax.Config:
return cfg
summary = ex_fares.run()
summary.fig_carrier_revenues()
Loaded Baseline from lab-1b-output/Baseline/Baseline.20261027-144422.pxsim, but the PassengerSim version has changed: running 0.94, found 0.93
╭────────── Summary revenue statistics · Baseline ──────────╮ │ AL1 AL2 │ │ Run Samples Mean ~95% CI Mean ~95% CI │ │ Baseline 750 90,429 ±870 90,655 ±912 │ │ Post-burn-in only · Approx. 95% CI for mean │ ╰───────────────────────────────────────────────────────────╯
@ex_fares
def TopFareUp_OrdLax(cfg: pax.Config) -> pax.Config:
for fare in cfg.fares:
if fare.booking_class == "Y0" and fare.orig == "ORD" and fare.dest == "LAX":
fare.price *= 1.1
return cfg
summary = ex_fares.run()
summary.fig_carrier_revenues()
Using cached results for experiment Baseline
Loaded TopFareUp_OrdLax from lab-1b-output/TopFareUp_OrdLax/TopFareUp_OrdLax.20261027-144426.pxsim, but the PassengerSim version has changed: running 0.94, found 0.93
╭────────── Summary revenue statistics · TopFareUp_OrdLax ──────────╮ │ AL1 AL2 │ │ Run Samples Mean ~95% CI Mean ~95% CI │ │ TopFareUp_OrdLax 750 89,120 ±860 88,937 ±886 │ │ Post-burn-in only · Approx. 95% CI for mean │ ╰───────────────────────────────────────────────────────────────────╯
summary.fig_fare_class_mix()
summary.fig_bookings_by_timeframe(by_carrier="AL1", by_class=True)
Questions¶
What happens if you mark up the price of the top fare in the BOS-ORD market instead? Why is this different?
What about BOS-LAX?
What if only AL1 makes this pricing change?