Lab 2¶
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
from passengersim.contrast import Contrast
pax.versions()
passengersim 0.80
passengersim.core 0.80
For this lab, we are going to work with an unrestricted network. To create this network, we’ll start from our typical 3MKT demo network, and then strip all the fare restrictions away.
from passengersim.config.manipulate import strip_fare_restrictions
cfg = strip_fare_restrictions(pax.Config.from_yaml(pax.demo_network("3MKT/DEMO")))
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-2-output")
First, we will take a look at what happens when we remove all the restrictions, including advance purchase (AP) restrictions, and compare that to a baseline model run where we have no fare restrictions, but we do leave the AP restrictions in place. We’ll otherwise leave the config file unchanged, so both carriers are using the “E” system, which includes untruncation, standard forecasting, and EMSR-B optimization.
@experiments
def NoAP(cfg: pax.Config) -> pax.Config:
"""Experiment with no fare or advance purchase restrictions."""
cfg.simulation_controls.disable_ap = True
return cfg
@experiments
def Baseline(cfg: pax.Config) -> pax.Config:
"""Experiment without fare restrictions but with advance purchase restrictions."""
return cfg
For this lab, we will add on a leg forecast tracer, which will allow us to look in detail at the average forecasts for selected legs.
from passengersim.tracers.forecasts import (
LegForecastTracer,
fig_leg_forecast_dashboard,
)
forecast_tracer = LegForecastTracer(leg_ids=[101, 111])
forecast_tracer.attach(experiments)
Now we can run our experiments and review the results.
results = experiments.run()
Loaded NoAP from lab-2-output/NoAP/NoAP.20260527-162930.pxsim, but the PassengerSim version has changed: running 0.80, found 0.78.dev17+g0a2908ec9
╭─────────────────────────────────────╮ │ licensed for jpn │ │ until 2036-05-28 15:49:32+00:00 UTC │ │ maximum 42000 legs in network │ ╰─────────────────────────────────────╯
Loaded Baseline from lab-2-output/Baseline/Baseline.20260527-162930.pxsim, but the PassengerSim version has changed: running 0.80, found 0.78.dev17+g0a2908ec9
results.fig_carrier_revenues()
Whoa! Those AP restrictions are important. Let’s find out why.
results.fig_fare_class_mix()
results.fig_bookings_by_timeframe(by_carrier="AL1", by_class=True, source_labels=True)
Without the AP restrictions, all the demand purchases the lowest available fare class. The standard forecaster and untruncation algorithms treat demand by class as independent, so they never forecast any demand in higher classes unless they see historical sales in higher classes – which in this case they don’t.
We can see this effect clearly in the leg forecasts. The leg forecast dashboards for legs 101 and 111 are available because we turned on tracing for those legs. They are only available for a single experiment at a time, in part because they already have a lot of complex data to visualize.
fig_leg_forecast_dashboard(results["NoAP"], leg_id=101)
fig_leg_forecast_dashboard(results["Baseline"], leg_id=101)
Our standard forecasts presume independent demand by fare class, which is
not a reasonable assumption for unrestricted networks. Fortunately,
we have conditional forecast equivalents for each of the E, P, and U
RM systems, called L, M, and V respectively.
These RM systems use conditional forecasting, which offers a LOT more options for users to manipulate on how exactly these forecasts are configured.
@experiments
def LE(cfg: pax.Config) -> pax.Config:
cfg.carriers.AL1.rm_system = "L"
cfg.carriers.AL1.frat5 = "curve_C" # curve_A through curve_G are available
cfg.carriers.AL1.rm_system_options = dict(
# fare_adjustment="ki", # "ki" or "mr" or None
# fare_adjustment_scale=0.5, # between 0.0 and 1.0
# regression_weight="sellup", # "sellup", "sellup^2", "fare", "none"
# variance_rollup_algorithm="tf", # "tf" or "dep"
# variance_is_ratio_of_mean=0.0, # 0.0 or some positive float, typically 2.0
# max_cap=0.0, # 0.0 or some positive float, typically 10.0
# q_allocation_algorithm="tf", # "tf" or "dep"
)
return cfg
@experiments
def ME(cfg: pax.Config) -> pax.Config:
cfg.carriers.AL1.rm_system = "M"
cfg.carriers.AL1.frat5 = "curve_C" # curve_A through curve_G are available
cfg.carriers.AL1.rm_system_options = dict(
# fare_adjustment="ki", # "ki" or "mr" or None
# bid_price_vector=True, # True or False
# fare_adjustment_scale=0.5, # between 0.0 and 1.0
# regression_weight="sellup", # "sellup", "sellup^2", "fare", "none"
# variance_rollup_algorithm="tf", # "tf" or "dep"
# variance_is_ratio_of_mean=0.0, # 0.0 or some positive float, typically 2.0
# max_cap=0.0, # 0.0 or some positive float, typically 10.0
# q_allocation_algorithm="tf", # "tf" or "dep"
)
return cfg
@experiments
def VE(cfg: pax.Config) -> pax.Config:
cfg.carriers.AL1.rm_system = "V"
cfg.carriers.AL1.frat5 = "curve_C" # curve_A through curve_G are available
cfg.carriers.AL1.rm_system_options = dict(
# fare_adjustment="ki", # "ki" or "mr" or None
# arrivals_per_time_slice=1.0, # float <= 1.0
# fare_adjustment_scale=0.5, # between 0.0 and 1.0
# regression_weight="sellup", # "sellup", "sellup^2", "fare", "none"
# variance_rollup_algorithm="tf", # "tf" or "dep"
# variance_is_ratio_of_mean=0.0, # 0.0 or some positive float, typically 2.0
# max_cap=0.0, # 0.0 or some positive float, typically 10.0
# q_allocation_algorithm="tf", # "tf" or "dep"
)
return cfg
results = experiments.run()
Using cached results for experiment NoAP
Using cached results for experiment Baseline
Loaded LE from lab-2-output/LE/LE.20260527-162935.pxsim, but the PassengerSim version has changed: running 0.80, found 0.78.dev17+g0a2908ec9
Loaded ME from lab-2-output/ME/ME.20260527-162935.pxsim, but the PassengerSim version has changed: running 0.80, found 0.78.dev17+g0a2908ec9
Loaded VE from lab-2-output/VE/VE.20260527-162935.pxsim, but the PassengerSim version has changed: running 0.80, found 0.78.dev17+g0a2908ec9
results.fig_carrier_revenues()
We can look at all the same outputs, to examine and understand what is happening. For example, lets look at the forecasts for leg 111.
fig_leg_forecast_dashboard(results["LE"], leg_id=111)
If we want to try a bunch of different variations on these settings, we may not want to use the whole experiments framework, but instead just try a bunch of one-off comparisons.
from IPython.display import display
def try_me(cfg: pax.Config, against: str = "Baseline") -> None:
cfg = cfg.model_copy(deep=True)
cfg.carriers.AL1.rm_system = "M"
cfg.carriers.AL1.frat5 = "curve_B" # curve_A through curve_G are available
cfg.carriers.AL1.rm_system_options = dict(
# fare_adjustment="mr", # "ki" or "mr" or None
# bid_price_vector=True, # True or False
# fare_adjustment_scale=.75, # between 0.0 and 1.0
# regression_weight="sellup", # "sellup", "sellup^2", "fare", "none"
# variance_rollup_algorithm="tf", # "tf" or "dep"
# variance_is_ratio_of_mean=0.0, # 0.0 or some positive float, typically 2.0
# max_cap=0.0, # 0.0 or some positive float, typically 10.0
# q_allocation_algorithm="tf", # "tf" or "dep"
)
sim = pax.MultiSimulation(cfg)
out = sim.run()
comp = Contrast({against: results[against], "try_me": out})
display(comp.fig_carrier_revenues())
display(comp.fig_carrier_load_factors())
try_me(cfg)