Europa Baseline Network

This page reviews the configuration of the Europa network and runs a baseline simulation, with all three carriers using the same revenue management system (E). See the Europa network overview for a description of the carriers and their hubs.

import altair as alt
import passengersim as pax
from passengersim.config.checks import check_config

pax.versions()
passengersim 0.94
passengersim.core 0.94
cfg = pax.Config.from_yaml(pax.demo_network("europa"))

Route Maps

cfg.plotly_route_map("Brat", center_lon=31, center_lat=52, zoom=2.2)
cfg.plotly_route_map("Stroop", center_lon=31, center_lat=52, zoom=2.2)
cfg.plotly_route_map("Tapas", center_lon=0, center_lat=50, zoom=4.5)

For this example page, we will pre-build the flight connections, so we can do some pre-simulation checking to review our network and verify it is constructed correctly.

from passengersim.connection_builder import prebuild_connections

cfg = prebuild_connections(cfg, inplace=False)

Hub Flight Schedules

cfg.fig_hub_schedule("AMS")
cfg.fig_hub_schedule("FRA")
cfg.fig_hub_schedule("MAD")

Check Configuration

check_config(cfg)
 ✅ fare classes match carrier booking classes
 ✅ no fares without demands
 ✅ no markets without fares
 ✅ market lowest fares match configured fare prices
 ✅ no demands without fares
 ✅ no demands without paths
 ✅ fare restrictions: 4 unique restrictions found
   - R2: 60 occurrences
   - R1: 176 occurrences
   - R4: 20 occurrences
   - R3: 40 occurrences
 ✅ reference price scaling is consistent
 ✅ choice model restrictions: 4 unique restrictions found
   - r1: {'business': 0.28, 'leisure': 0.3}
   - r2: {'business': 0.4, 'leisure': 0.3}
   - r3: {'business': 0.3, 'leisure': 0.38}
   - r4: {'business': 0.22, 'leisure': 0.22}
 ✅ choice model and fare restrictions match

List of Markets

cfg.dataframes.markets
orig dest demand_multiplier delta_t distance lowest_fare
0 KEF MAD 1.0 None 1794.496 220.0
1 KEF AMS 1.0 None 1265.292 220.0
2 MAD AMS 1.0 None 905.703 100.0
3 CDG AMS 1.0 None 247.307 85.0
4 JFK AMS 1.0 None 3631.022 800.0
5 KEF FRA 1.0 None 1490.664 280.0
6 MAD FRA 1.0 None 880.763 100.0
7 CDG FRA 1.0 None 277.519 90.0
8 AMS FRA 1.0 None 227.108 75.0
9 JFK FRA 1.0 None 3842.424 800.0
10 KEF HEL 1.0 None 1517.821 380.0
11 MAD HEL 1.0 None 1829.061 250.0
12 CDG HEL 1.0 None 1177.052 300.0
13 AMS HEL 1.0 None 944.658 200.0
14 FRA HEL 1.0 None 956.086 200.0
15 JFK HEL 1.0 None 4102.818 1100.0
16 KEF ATH 1.0 None 2608.413 380.0
17 MAD ATH 1.0 None 1476.248 200.0
18 CDG ATH 1.0 None 1309.374 250.0
19 AMS ATH 1.0 None 1354.762 270.0
20 FRA ATH 1.0 None 1128.011 220.0
21 JFK ATH 1.0 None 4925.660 900.0
22 KEF WAW 1.0 None 1741.344 380.0
23 MAD WAW 1.0 None 1409.401 260.0
24 CDG WAW 1.0 None 833.625 210.0
25 AMS WAW 1.0 None 683.775 150.0
26 FRA WAW 1.0 None 557.831 170.0
27 JFK WAW 1.0 None 4252.119 950.0
28 KEF SIN 1.0 None 7174.082 1400.0
29 MAD SIN 1.0 None 7067.757 1300.0
30 CDG SIN 1.0 None 6659.469 1400.0
31 AMS SIN 1.0 None 6528.644 1100.0
32 FRA SIN 1.0 None 6384.186 1100.0

Market Pricing Distribution

alt.Chart(cfg.dataframes.markets.eval("price_per_mile = lowest_fare / distance")).mark_point().encode(
    x=alt.X("distance", title="Market Distance (miles)"),
    y=alt.Y("price_per_mile", title="Lowest Fare Price per Mile", scale=alt.Scale(zero=False)),
    tooltip=["orig", "dest", "distance", "lowest_fare", "price_per_mile"],
    color="orig",
    shape="dest",
)

Here we have plotted the lowest price fare per mile against the distance. We can see the cluster of lower priced markets with MAD origins (reflecting the LCC that is dominant there) as well as cluster of higher priced markets that are effectively controlled by legacy carriers flying into fortress hubs with zero or nearly zero competition. Note none of this is an outcome of the simulation, this is merely the input/setup of pricing for the network.

Carrier Service Capacity

We can calculate the number of seats offered by each carrier on each served route.

cfg.dataframes.legs.groupby(["carrier", "orig", "dest"])[["capacity"]].sum().reset_index()
carrier orig dest capacity
0 Brat AMS FRA 300
1 Brat CDG FRA 450
2 Brat FRA ATH 450
3 Brat FRA HEL 300
4 Brat FRA SIN 300
5 Brat FRA WAW 300
6 Brat JFK FRA 300
7 Brat MAD FRA 300
8 Stroop AMS ATH 300
9 Stroop AMS FRA 200
10 Stroop AMS HEL 270
11 Stroop AMS SIN 300
12 Stroop AMS WAW 170
13 Stroop CDG AMS 250
14 Stroop JFK AMS 300
15 Stroop KEF AMS 320
16 Stroop MAD AMS 220
17 Tapas KEF MAD 360
18 Tapas MAD AMS 240
19 Tapas MAD ATH 240
20 Tapas MAD FRA 360
21 Tapas MAD HEL 120
22 Tapas MAD WAW 360

Simulation Results

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

╭─────────────────── Summary revenue statistics · Europa ───────────────────╮
│                            Brat            Stroop             Tapas       │
│ Run      Samples     Mean  ~95% CI     Mean  ~95% CI     Mean  ~95% CI    │
│ Europa       900   932.1K   ±7,237   928.2K   ±7,336   291.7K   ±2,360    │
│ Post-burn-in only · Approx. 95% CI for mean                               │
╰───────────────────────────────────────────────────────────────────────────╯
out.fig_carrier_revenues()
out.fig_carrier_load_factors()
out.fig_carrier_rasm()
out.fig_carrier_yields()
out.fig_fare_class_mix()
out.fig_leg_load_v_distance(carrier="Brat", beeswarm=1000)
out.fig_leg_load_v_distance(carrier="Stroop", beeswarm=1000)
out.fig_leg_load_v_distance(carrier="Tapas", beeswarm=1000)

The Tapas network is much smaller than the other carriers, both in terms of seats flown and also distance of legs, as there are no long-haul legs for this carrier.

Market Shares

out.market_shares()
carrier Brat Stroop Tapas
orig dest
AMS ATH 30.679067 69.320933 0.000000
FRA 55.099152 44.900848 0.000000
HEL 16.008772 83.991228 0.000000
SIN 25.466540 74.533460 0.000000
WAW 16.972072 83.027928 0.000000
CDG AMS 0.000000 100.000000 0.000000
ATH 68.816622 31.183378 0.000000
FRA 92.156420 7.843580 0.000000
HEL 56.462642 43.537358 0.000000
SIN 64.801863 35.198137 0.000000
WAW 79.223803 20.776197 0.000000
FRA ATH 100.000000 0.000000 0.000000
HEL 100.000000 0.000000 0.000000
SIN 100.000000 0.000000 0.000000
WAW 100.000000 0.000000 0.000000
JFK AMS 0.000000 100.000000 0.000000
ATH 58.323946 41.676054 0.000000
FRA 76.168032 23.831968 0.000000
HEL 51.752344 48.247656 0.000000
WAW 52.644019 47.355981 0.000000
KEF AMS 0.000000 81.250695 18.749305
ATH 0.000000 42.266403 57.733597
FRA 0.000000 45.738023 54.261977
HEL 0.000000 68.014879 31.985121
MAD 0.000000 0.000000 100.000000
SIN 0.000000 100.000000 0.000000
WAW 0.000000 31.917893 68.082107
MAD AMS 0.000000 33.662542 66.337458
ATH 12.440618 7.672209 79.887173
FRA 37.618604 2.587385 59.794011
HEL 16.600961 22.606935 60.792103
SIN 55.062873 44.937127 0.000000
WAW 9.480951 7.467462 83.051586
out.market_shares(agg="orig")
carrier Brat Stroop Tapas
orig
AMS 34.858498 65.141502 0.000000
CDG 65.454048 34.545952 0.000000
FRA 100.000000 0.000000 0.000000
JFK 49.339247 50.660753 0.000000
KEF 0.000000 46.496964 53.503036
MAD 20.158162 14.775521 65.066317
out.market_shares(agg="dest")
carrier Brat Stroop Tapas
dest
AMS 0.000000 63.073160 36.926840
ATH 44.210479 32.516361 23.273159
FRA 48.933506 19.446180 31.620314
HEL 43.679342 37.261557 19.059101
MAD 0.000000 0.000000 100.000000
SIN 50.857332 49.142668 0.000000
WAW 35.785805 20.952654 43.261541