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 passengersim as pax

pax.versions()
passengersim 0.80
passengersim.core 0.80
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()
   ╭─────────────────────────────────────╮                                                                         
   │ licensed for jpn                    │                                                                         
   │ until 2036-05-28 15:49:32+00:00 UTC │                                                                         
   │ maximum 42000 legs in network       │                                                                         
   ╰─────────────────────────────────────╯                                                                         

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()