Q vs Conditional Forecasting¶
There are two different approaches to forecasting the priceable (i.e. price sensitive) demand for unrestricted networks: Q forecasting, and conditional forecasting.
Under Q forecasting, each priceable booking is converted into an equivalent number of Q-class bookings, where the "Q" class is the lowest possible fare class for the relevant path. Forecasts are then made for how much demand there would be for that Q class, and then those forecasts are broadcast up to the various higher fare classes, based on the sellup probability in each time frame along the booking curve.
Under conditional forecasting, bookings are all recorded in the class where they occur, and the level of demand in that class is computed conditional on it being the lowest available fare class at that time.
These two methodologies are similar, and indeed with certain configurations and under the right conditions (e.g. flights never sell out, the lowest available fare class is always the same for full time frames) the methodologies become identical. In practice, and in most simulations, these particular conditions do not actually hold, so these algorithms are not actually identical, so PassengerSim allows you to choose one or the other as appropriate.
import numpy as np
import pandas as pd
import passengersim as pax
pax.versions()
passengersim 0.65.dev82+g7131f9b20.d20260209 passengersim.core 0.65.dev82+g7131f9b20
cfg = pax.Config.from_yaml(pax.demo_network("3MKT/DEMO"))
In this example, we will work with a completely fenceless marketplace. To convert the typical 3MKT network into a restriction-free network, we can use a tool to strip the restrictions.
from passengersim.config.manipulate import strip_ap_restrictions, strip_fare_restrictions
cfg = strip_fare_restrictions(strip_ap_restrictions(cfg))
For this example, we will assign Q forecasting to AL1. The "Q" RM system uses Q forecasting, as well as a ProBP optimizer. We also need to give this carrier a Frat5 curve, and tell it that we need to store the Q history while running the simulation.
cfg.carriers.AL1.rm_system = "Q" # Q forecasting
cfg.carriers.AL1.frat5 = "curve_C"
cfg.carriers.AL1.store_q_history = True
For AL2, we will use conditional forecasting. We'll select the "M" system, which pairs the conditional forecast with a ProBP optimizer.
cfg.carriers.AL2.rm_system = "M" # Conditional forecasting
cfg.carriers.AL2.frat5 = "curve_C"
Having set up our carriers with the desired RM systems, we can now run the model and see how they do.
sim = pax.MultiSimulation(cfg)
summary = sim.run()
summary.fig_carrier_revenues()
summary.fig_carrier_load_factors()
summary.fig_carrier_rasm()
summary.fig_fare_class_mix()
summary.fig_carrier_head_to_head_revenue("AL1", "AL2", mean_adjusted=False)
Changing RM System Options¶
We can also change some of the options associated with the RM systems. For example,
the Q system allows us to disable the detruncation of priceable demand. This has only
a very modest impact of results, as priceable demand is presumed to be censored and in
need of detruncation only when a leg is completely sold out. To remove the detruncation
of priceable demand from the Q system, we can use the rm_system_options setting.
from passengersim.experiments import Experiments
experiments = Experiments(cfg, output_dir=False)
@experiments.existing(summary)
def baseline(cfg: pax.Config) -> pax.Config:
return cfg
@experiments
def no_price_detrunc(cfg: pax.Config) -> pax.Config:
cfg.carriers.AL1.rm_system_options = {"priceable_detruncation": "none"}
return cfg
Now we can run the additional experiment, and we can see that the results are only very slightly different.
summaries = experiments.run(write_report=False)
Using cached results for experiment baseline
summaries.fig_carrier_revenues()
summaries.fig_carrier_load_factors()
summaries.fig_carrier_rasm()
summaries.fig_fare_class_mix()
summaries["no_price_detrunc"].fig_carrier_head_to_head_revenue("AL1", "AL2")
Swapping for Leg Forecasting¶
The standard "Q" RM system that comes with PassengerSim uses path-based forecasting and a ProBP optimizer. We can alternatively use the "Qe" system, which uses leg-based forecasts and an EMSR capacity allocation optimization.
@experiments
def al1_Qe(cfg: pax.Config) -> pax.Config:
cfg.carriers.AL1.rm_system = "Qe"
return cfg
summaries = experiments.run(write_report=False)
Using cached results for experiment baseline
Using cached results for experiment no_price_detrunc
summaries.fig_carrier_revenues()
summaries["al1_Qe"].fig_carrier_head_to_head_revenue("AL1", "AL2", mean_adjusted=False)
Carrier 2 Responds¶
@experiments
def al2_Q(cfg: pax.Config) -> pax.Config:
cfg.carriers.AL1.rm_system = "Qe"
cfg.carriers.AL2.rm_system = "Q"
cfg.carriers.AL2.store_q_history = True
return cfg
summaries = experiments.run(write_report=False)
Using cached results for experiment baseline
Using cached results for experiment no_price_detrunc
Using cached results for experiment al1_Qe
summaries.fig_carrier_revenues()
summaries["al2_Q"].fig_carrier_head_to_head_revenue("AL1", "AL2", mean_adjusted=False)
summaries.fig_fare_class_mix()