Custom Forecaster¶
In this example, we use a custom forecast algorithm, the "Olympic Average", implemented in Python. In this forecast, the mean (mu) forecast for any bucket is computed by taking the 26 historical values, discarding the highest and lowest, and taking the mean of the remaining 24 values.
Do try this at home -- but not at work
We're not suggesting this is a great idea, it's merely a straightforward idea that we can use to demostrate how to implement a custom RM action in PassengerSim.
import passengersim as pax
pax.versions()
passengersim 0.59.dev9+ga3f6c1b98 passengersim.core 0.59.dev1+g671876c7d
We'll first run a simulation without the change as a baseline. As this tutorial is meant as a technology demonstration and not for serious statistical analysis, we'll only run a small sample so it goes fast, and we won't worry about statistical validity.
cfg = pax.Config.from_yaml(pax.demo_network("3MKT/DEMO"))
cfg.simulation_controls.num_samples = 150
sim = pax.MultiSimulation(cfg)
baseline_summary = sim.run()
baseline_summary.fig_carrier_revenues()
Now we'll write our custom RM system. This is going to be done in two parts: first,
we will write our custom forecast algorithm as a RmAction. Then we'll define a
RmSys that uses that custom forecast algorithm.
from passengersim.rm import RmAction
class OlympicForecast(RmAction):
"""
In this step, we use a custom forecast algorithm, the "Olympic Average",
implemented in Python. In this forecast, the mean (mu) forecast for any
bucket is computed by taking the 26 historical values, discarding the highest
and lowest, and taking the mean of the remaining 24 values.
This forecaster is meant primarily as a technology demonstration for how
to implement a custom RM step in Python.
"""
requires: set[str] = {"leg_demand"}
produces: set[str] = {"leg_forecast"}
frequency = "dcp"
def run(self, sim: pax.Simulation, days_prior: int):
if not self.should_run(sim, days_prior):
return
dcp_index = self.get_dcp_index(days_prior)
for leg in sim.eng.legs.set_filters(carrier=self.carrier):
for bkt in leg.buckets:
# get historic demand, for all recorded departures and remaining TFs
history = bkt.forecast.get_detruncated_demand_array()[:, dcp_index:]
# sum historic demand to come over all remaining time periods
hdtc = history.sum(1)
# compute average pickup excluding min and max
avg_pickup = (hdtc.sum() - hdtc.max() - hdtc.min()) / (hdtc.size - 2)
# set forecast mean excluding outliers
bkt.fcst_mean = avg_pickup
# compute standard dev as normal, including outliers
bkt.fcst_std_dev = history.std()
Now we have our custom forecaster. But this isn't a whole RM system; we still need to handle demand untruncation before we do forecasting, and then to have some kind of optimizer that will do something with the forecasts. For these other things, we'll draw from PassengerSim's standard library of RM components to build a complete RM system.
from passengersim.rm import RmSys, register_rm_system
from passengersim.rm.emsr import ExpectedMarginalSeatRevenue
from passengersim.rm.untruncation import LegUntruncation
@register_rm_system
class Olympic(RmSys):
"""A custom RM system using Olympic Average forecasting."""
availability_control = "leg"
"""This RM system uses leg-level class allocation availability controls."""
actions = [
LegUntruncation,
OlympicForecast,
ExpectedMarginalSeatRevenue,
]
The last thing to do is to actually assign this system to a carrier who will use it.
The register_rm_system reads the name of the class that it is decorating, and makes
that class available as a named RM system that you can use in configs.
cfg.carriers.AL1.rm_system = "Olympic"
Now we're ready to run, and see what chaos we have wrought.
sim = pax.MultiSimulation(cfg)
summary = sim.run()
Comparing Results¶
from passengersim.contrast import Contrast
comp = Contrast(Baseline=baseline_summary, Olympic=summary)
comp.fig_carrier_revenues()
Taking a look at the revenue results, we see that the Olympic forecaster has turned in less than record settings revenues. This is a small sample, so we shouldn't necessarily read a whole lot into this result, but we can at least say it's not looking promising at this point. We can take a look through some of the other output reports to see if anything stands out to us as good or bad.
comp.fig_carrier_load_factors()
comp.fig_fare_class_mix()
comp.fig_segmentation_by_timeframe("bookings", by_carrier="AL1", by_class=True, source_labels=True)
The other thing we may want to keep in mind is runtime. Even on this tiny network and with a tiny number of samples, the custom RM system takes noticably longer to run.
baseline_summary.metadata("time.runtime"), summary.metadata("time.runtime")
(2.0634050369262695, 2.706063747406006)
Customization is great as it adds lots of flexibility to the system, but it's not completely free. Passing data into Python for the custom tools, and processing that data in a Python environment is typically a bit (or sometimes a lot) slower than running the same mathematical algorithms in well optimized C/C++ code. To maximize the speed that the Simulator runs, users should generally use RM actions from PassengerSim's standard library whenever possible.