Booking Curve Reconstruction
Shipped05.2026 - 06.2026|2m
A simulation engine that reconstructs a bus's full booking curve from a sparse ~30% aggregator sample, so a dynamic-pricing engine has a demand signal to price against instead of flying blind.
The measurement
- Measured on
- roughly 25% error on a representative 40-trip sample, averaged over five independent ~30% draws
- Against a baseline of
- 92% of trips fall inside their predicted band; mean signed error is approximately zero
What it does
- Five layers, each earning its place: a non-homogeneous Poisson process for arrivals, switching to negative binomial where a bucket is overdispersed; a Gaussian copula so buckets correlate instead of being treated as independent; channel stratification that reproduces the aggregator's biased sampling rather than assuming a flat 30%; a conditional tabular GAN to augment thin cohorts; and velocity scaling so a fast-selling bus is not dragged back to the mean.
- 64 intra-day time buckets from 84 hours before departure to 180 minutes after, which is what makes last-day velocity and post-departure walk-ins visible at all.
- Fully vectorised in NumPy: 5,000 simulations across 64 buckets with no Python loops, under a hard capacity cap and a hard floor set by the bookings already observed.
- Kept a transparent baseline alongside the pipelined engine on purpose, so the added complexity could be judged against something simple rather than asserted.
- Made the backtest honest before reporting it: leak-free cohorts predicted from past trips only, peers restricted to departures before the target, and accuracy averaged over five independent samples so the headline does not ride on one lucky draw.
How it is put together
Five layers, each one added because a simpler model failed in a specific way. The right-hand column is the failure, which is the part that makes the layer a decision rather than a flourish.
what each stage of the simulation does, and the problem it was added to solvea single-stage independent-Normal Monte Carlo baseline, kept alongside it on purpose
Each layer, and the failure it fixes
- 01NHPP arrivalsModels arrivals per bucket as a non-homogeneous Poisson process, switching to negative binomial where a bucket is overdispersedA plain normal model allowed negative ticket counts
- 02Gaussian copulaA 64x64 correlation matrix so a surge at 72h raises the expectation of a surge at 24hIndependent buckets underestimate how a busy trip stays busy
- 03Channel stratificationReproduces the aggregator's biased sampling with bucket-specific Bernoulli drawsA flat 30% assumption over-counts last-minute demand
- 04CTGAN augmentationGenerates synthetic booking curves for cohorts with fewer than 50 real tripsThin cohorts had too little history to model at all
- 05Velocity scalingCompares live pace against channel-adjusted peers and scales future arrival rates rather than reverting themMean reversion dragged fast-selling trips back toward average
The whole Monte Carlo run is vectorised in NumPy - 5,000 simulations across 64 intra-day buckets with no Python loops - under a hard capacity cap and a hard floor set by the bookings already observed. The simple baseline was kept and reported beside the pipelined engine, so the added complexity had to justify itself against something transparent instead of being asserted.
reconstruction accuracy at departure, averaged over five independent drawsa 44-seat bus, so 5.5 seats is roughly a quarter of the quantity being predicted
There is no curve plotted here, deliberately.The real per-trip curves live in the notebooks' own interactive backtest HTML, which is linked above. Drawing an illustrative three-line chart from numbers chosen to look right would have been the easiest figure on this site to produce and the only dishonest one on it.
Show the numbers
| Measure | Value | Condition |
|---|---|---|
| mean absolute error | 5.5 seats | at departure, roughly 25% - averaged over five independent ~30% draws |
| band calibration | 92% | share of trips whose true occupancy falls inside their predicted band |
| mean signed error | ~0 | the error is variance, not bias - the better failure to have |
Where it falls down
The error is variance, not bias, which is the better failure to have but still 5.5 seats on a 44-seat bus. Velocity scaling is currently held at a setting that is not the one with the best average error, on the argument that it targets the rare fast-selling trips an average over random trips understates - a defensible call, but a call. Cold-start trips on thin routes can fall back to a sparse peer pool, and the method has only been demonstrated on one dense route.
Stack
- Python
- NumPy
- SciPy
- NHPP
- Gaussian copula
- CTGAN
- Monte Carlo