Source code for passengersim.config.frat5_curves

# TITLE: Frat5 Curves
from __future__ import annotations

from pydantic import ValidationInfo, field_validator

from .named import Named


[docs] class Frat5Curve(Named, extra="forbid"): """ Fare Ratio at which 50% of customers will buy up to a higher fare. This is expressed as a curve, because the ratio generally changes over the booking horizon, because the mixture of customer types changes as the departure date approaches. """ enforce_monotonic: bool = True """Enforce monotonicity of Frat 5 curves. Typically it is expected that the Frat5 curve is monotonic, i.e. that the average willingness to pay only increases as the departure date approaches. It is easy to accidentally define the Frat5 curve "backwards", and thus PassengerSim will check that the Frat5 curve is monotonically increasing by default. To violate this assumption, set `enforce_monotonic` to False, which will disable the check that the Frat5 curve is monotonic. """ curve: dict[int, float] """Define a Frat5 curve. To be consistent with the econometric interpretation of the Frat5 curve, the values should increase as the keys (DCPs, e.g. days to departure) decrease. This implies that average willingness to pay increases as the departure date approaches. Example ------- .. code-block:: yaml - name: curve_C curve: 63: 1.4 56: 1.4 49: 1.5 42: 1.5 35: 1.6 31: 1.7 28: 1.8 24: 1.9 21: 2.3 17: 2.7 14: 3.2 10: 3.3 7: 3.4 5: 3.4 3: 3.5 1: 3.5 """ max_cap: float = 10.0 """ Maximum Q-equivalent demand implied by any unit of demand in any fare class. This cap is applied only on the recording of Q-equivalent demand that occurs within the simulation engine itself, and not as part of any RM step. Simulation-recorded Q-equivalent demand can be used by RM steps, such as within PODS-like hybrid forecasting models, but the max-cap filter transform is implicitly already baked in to the Q-equivalent demand before the RM step can use it. This can be contrasted against a `max_cap` parameter used in the RM step, which can applied against observed demand within the RM step, but the RM step receives the "raw" sales data, without adulteration by the simulation engine. """ @field_validator("curve") def _frat5_curves_accumulate(cls, v: dict[int, float], info: ValidationInfo): """Check that all curve values do not decrease as DCP keys decrease.""" if "enforce_monotonic" in info.data and not info.data["enforce_monotonic"]: # if the user has explicitly set enforce_monotonic to False, then # we do not check that the Frat5 curve is monotonic return v sorted_dcps = reversed(sorted(v.keys())) i = 0 for dcp in sorted_dcps: assert v[dcp] >= i, f"frat5 curve {info.data['name']} moves backwards at dcp {dcp}" i = v[dcp] return v @field_validator("curve") def _frat5_curves_gt_1(cls, v: dict[int, float], info: ValidationInfo): """Check that all curve values are greater than 1.0. Values that are less than 1.0 imply that lowering the fare will reduce demand, which is not consistent with the econometric interpretation of the Frat5 curve. Similarly, values that are exactly 1.0 imply that any fare increase no matter how small will instantly reduce demand to zero, which is theoretically plausible as a corner case but in practice is not realistic, and will cause numerical issues in simulation. """ for dcp, val in v.items(): assert val > 1.0, f"frat5 curve {info.data['name']} is not greater than 1 at {dcp}" return v