Lab 5: Adding Passenger Segments

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-5.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

pax.versions()
passengersim 0.80
passengersim.core 0.80
cfg = pax.Config.from_yaml(pax.demo_network("3MKT/DEMO"))
cfg.outputs._write_no_files()
cfg.simulation_controls.num_trials = 1

For this lab, we will be making new customer segments. To simplify the process, we will set our configuration to use common reference prices. This setup puts the control over relative willingness-to-pay between segments into the ChoiceModel configs, instead of having it in the Demand configs. This is less flexible (all demands sharing a choice model must have the same reference price multiplier) but also easier to maintain (as there is only one value to look at, in the choice model, for scaling the reference prices, as opposed to unique values for each demand).

from passengersim.transforms import common_reference_prices

cfg = common_reference_prices(cfg, "leisure")

We can run the demo to establish a baseline result.

sim = pax.Simulation(cfg)
out = sim.run()

Task Completed after 1.16 seconds
out.fig_carrier_revenues()

To establish a new customer segment, we need to think about a few things:

  • What choice model do these customers use to make their purchase decisions?

  • When do these customers arrive during the booking curve?

cm = sim.choice_models["leisure"]
dmd = sim.demands.select(segment="leisure", orig="BOS", dest="LAX")
cm.max_wtp(dmd.reference_price, n_draws=1_000_000, raw=True)
{'mean': 373.4033063800838,
 'stdev': 173.47400170407738,
 'raw': array([207.3999763 , 433.09048574, 323.72624606, ..., 244.30899255,
        208.24766472, 312.79117435], shape=(1000000,))}

One big part of the choice model is the customer’s maximum willingness to pay. This is generally analogous to a budget for travel: each customer can pay only so much, and not more. Any product which costs more than the budget is infeasible for that customer and is immediately removed from consideration, even if it is otherwise very appealing.

We can visualize the distribution of maximum willingness to pay in any O-D market by passenger segment:

cfg.fig_max_wtp_distributions(orig="BOS", dest="LAX")

Suppose we want to add a “family” segment, with a somewhat higher willingness to pay than the leisure segment.
We can add the new segment like this:

cfg.choice_models["family"] = cfg.choice_models["leisure"].model_copy(deep=True)

cfg.choice_models["family"].reference_price_multiplier = 1.75
cfg.choice_models["family"].name = "family"

We also need to make some demands that can generate customers from the new segment.

cfg.demands
[Demand(orig='BOS', dest='ORD', segment='business', base_demand=70.0, reference_price=100.0, emult=None, distance=863.753, choice_model='business', dwm_tolerance=0.0, todd_curve=None, curve='c1', group_sizes=None, prob_saturday_night=None, prob_num_days=[], deterministic=False, overrides=[]),
 Demand(orig='BOS', dest='ORD', segment='leisure', base_demand=90.0, reference_price=100.0, emult=None, distance=863.753, choice_model='leisure', dwm_tolerance=0.0, todd_curve=None, curve='c2', group_sizes=None, prob_saturday_night=None, prob_num_days=[], deterministic=False, overrides=[]),
 Demand(orig='ORD', dest='LAX', segment='business', base_demand=120.0, reference_price=150.0, emult=None, distance=1739.799, choice_model='business', dwm_tolerance=0.0, todd_curve=None, curve='c1', group_sizes=None, prob_saturday_night=None, prob_num_days=[], deterministic=False, overrides=[]),
 Demand(orig='ORD', dest='LAX', segment='leisure', base_demand=150.0, reference_price=150.0, emult=None, distance=1739.799, choice_model='leisure', dwm_tolerance=0.0, todd_curve=None, curve='c2', group_sizes=None, prob_saturday_night=None, prob_num_days=[], deterministic=False, overrides=[]),
 Demand(orig='BOS', dest='LAX', segment='business', base_demand=100.0, reference_price=200.0, emult=None, distance=2603.449, choice_model='business', dwm_tolerance=0.0, todd_curve=None, curve='c1', group_sizes=None, prob_saturday_night=None, prob_num_days=[], deterministic=False, overrides=[]),
 Demand(orig='BOS', dest='LAX', segment='leisure', base_demand=140.0, reference_price=200.0, emult=None, distance=2603.449, choice_model='leisure', dwm_tolerance=0.0, todd_curve=None, curve='c2', group_sizes=None, prob_saturday_night=None, prob_num_days=[], deterministic=False, overrides=[])]
leisure_to_LAX = [d for d in cfg.demands if d.segment == "leisure" and d.dest == "LAX"]
leisure_to_LAX
[Demand(orig='ORD', dest='LAX', segment='leisure', base_demand=150.0, reference_price=150.0, emult=None, distance=1739.799, choice_model='leisure', dwm_tolerance=0.0, todd_curve=None, curve='c2', group_sizes=None, prob_saturday_night=None, prob_num_days=[], deterministic=False, overrides=[]),
 Demand(orig='BOS', dest='LAX', segment='leisure', base_demand=140.0, reference_price=200.0, emult=None, distance=2603.449, choice_model='leisure', dwm_tolerance=0.0, todd_curve=None, curve='c2', group_sizes=None, prob_saturday_night=None, prob_num_days=[], deterministic=False, overrides=[])]
family_to_LAX = []

for d in leisure_to_LAX:
    f = d.model_copy(deep=True)
    f.segment = "family"
    f.choice_model = "family"
    f.curve = "c_family"
    f.base_demand = 0.25 * d.base_demand  # add new family demand equal to 25% of original leisure demand
    family_to_LAX.append(f)

    d.base_demand = 0.75 * d.base_demand  # reduce existing regular leisure demand by 25%

cfg.demands.extend(family_to_LAX)
cfg.fig_max_wtp_distributions(orig="BOS", dest="LAX")

We also need to set up the booking curve that tells the simulator when these customer arrive. We can see our current booking curves like this:

cfg.fig_booking_curves()

When we created the new “family” demands, we assigned them booking curve “c_family” but we didn’t say what that is yet.

from pytest import raises

with raises(Exception) as e:
    cfg.model_revalidate()

print(e)
<ExceptionInfo 1 validation error for Config
  Value error, Demand ORD-LAX:family has unknown customer arrival curve 'c_family' [type..._certificate': None}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.13/v/value_error tblen=3>

We’ll need to define that booking curve to run with this config. Let’s do that now.

cfg.booking_curves["c_family"] = cfg.booking_curves["c2"].model_copy(deep=True)
cfg.booking_curves["c_family"].name = "c_family"
cfg.booking_curves["c_family"].curve = {
    63: 0.33,
    56: 0.51,
    49: 0.72,
    42: 0.79,
    35: 0.81,
    31: 0.83,
    28: 0.86,
    24: 0.88,
    21: 0.90,
    17: 0.91,
    14: 0.93,
    10: 0.95,
    7: 0.97,
    5: 0.98,
    3: 0.99,
    1: 1.0,
}

Now we can review the config with the new booking curve.

cfg.fig_booking_curves()

Let’s run the model again.

sim2 = pax.MultiSimulation(cfg)
out2 = sim2.run()
   ╭─────────────────────────────────────╮                                                                         
   │ licensed for jpn                    │                                                                         
   │ until 2036-05-28 15:49:32+00:00 UTC │                                                                         
   │ maximum 42000 legs in network       │                                                                         
   ╰─────────────────────────────────────╯                                                                         

from passengersim.contrast import Contrast

comp = Contrast({"original": out, "with_family": out2})
comp.fig_carrier_revenues()
comp.fig_bookings_by_timeframe(by_carrier="AL1", by_class=True)
cfg.to_yaml_parts(directory="lab-5-family")
# let's try an "executive" demand as well
cfg.demands[4]
Demand(orig='BOS', dest='LAX', segment='business', base_demand=100.0, reference_price=200.0, emult=None, distance=2603.449, choice_model='business', dwm_tolerance=0.0, todd_curve=None, curve='c1', group_sizes=None, prob_saturday_night=None, prob_num_days=[], deterministic=False, overrides=[])
d1 = cfg.demands[4]
e1 = d1.model_copy(deep=True)
e1.segment = "executive"
e1.choice_model = "executive"
e1.curve = "c_exec"
e1.base_demand = 0.50 * d1.base_demand

cfg.demands.append(e1)
d1.base_demand = 0.50 * d1.base_demand
cfg.choice_models["executive"] = cfg.choice_models["business"].model_copy(deep=True)

cfg.choice_models["executive"].reference_price_multiplier = 4.0
cfg.choice_models["executive"].emult = 1.9
cfg.choice_models["executive"].name = "executive"
cfg.booking_curves["c_exec"] = cfg.booking_curves["c1"].model_copy(deep=True)
cfg.booking_curves["c_exec"].name = "c_exec"
cfg.booking_curves["c_exec"].curve = {
    63: 0.0,
    56: 0.0,
    49: 0.0,
    42: 0.0,
    35: 0.0,
    31: 0.0,
    28: 0.0,
    24: 0.0,
    21: 0.03,
    17: 0.06,
    14: 0.10,
    10: 0.13,
    7: 0.18,
    5: 0.36,
    3: 0.77,
    1: 1.0,
}
cfg.fig_booking_curves()
sim3 = pax.MultiSimulation(cfg)
out3 = sim3.run()

comp = Contrast({"original": out, "with_family": out2, "with_executive": out3})
comp.fig_carrier_revenues()
comp.fig_bookings_by_timeframe(by_carrier="AL1", by_class=True)