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?