BayesianBahn · evaluation · 2026-08-17, 2026-08-18, 2026-08-19, 2026-08-20, 2026-08-21, 2026-08-22, 2026-08-23, 2026-08-24, 2026-08-25, 2026-08-29, 2026-08-30, 2026-08-31, 2026-09-01, 2026-09-02, 2026-09-03, 2026-09-04
BayesianBahn’s arrival forecasts and DB’s, scored against what the trains actually did. Sample sizes, method and the limits of each figure are given alongside it.
Every ten minutes, DB’s own forecast was recorded for twenty stations chosen in advance across the whole network — six major hubs down to three village halts. The next day the archive says when each train really arrived, and both forecasts are scored against that.
The model is the app’s own prediction code at commit
eee57d9ba683, which declares version 0.3.0
(versionCode 7). That commit is not a released version, and the version number above is the last released one rather than this code's — the bump happens in the release commit, so a candidate carries its predecessor's. The app code has moved on in 7 commit(s) since 0.3.0 was tagged. The app published in the stores does not contain this model unless a later release says so. It is only ever shown history
from before the day it predicts. It appears twice:
as shipped, which when DB actually reports a delay
anchors on that report and widens it by the error such reports are measured to
carry at that much notice, and leans on the train’s own past runs when DB says
nothing; and history only, which never looks at the live
number at all. The same treatment is given to the connecting train’s departure
report, so a change is the difference of two admitted errors rather than one
distribution and one promise.
Two kinds of answer are scored, and they correspond to the two kinds of journey the app plans. For a journey without a change the answer is an arrival time, scored as a distribution against the arrival that happened. For a journey with one change the answer is the probability of making that change, scored against whether it was made. The two use different scores and are not comparable with each other; each is compared only with DB’s answer to the same question. A third answer joins them below: the complete two-leg journey — the predicted arrival at the far end of a change, against the arrival that happened. It is in the same units as a direct journey, so those two are comparable with each other.
Each row is BayesianBahn’s score minus DB’s over the same predictions, so a negative number is the lower score for BayesianBahn, and lower is better for both scores used here. The interval is what decides it: delays arrive in clusters — one late train produces a dozen correlated predictions — so the range comes from resampling whole trains, not individual predictions. Where the interval crosses zero, the collected days are not enough to claim anything, however suggestive the middle number looks.
| Comparison | Predictions | Score | Ours − DB | 95% interval | Reading |
|---|---|---|---|---|---|
| Arrival time, as shipped | 296,619 | CRPS, minutes | -0.695 | -0.732 to -0.659 | BayesianBahn lower |
| Arrival time, history only | 296,619 | CRPS, minutes | -0.426 | -0.456 to -0.396 | BayesianBahn lower |
| Every connection, as shipped | 395,401 | Brier | -0.005 | -0.006 to -0.005 | BayesianBahn lower |
| Every connection, history only | 395,401 | Brier | -0.001 | -0.002 to -0.000 | BayesianBahn lower |
| Missed connections, as shipped | 14,835 | Brier | -0.242 | -0.256 to -0.227 | BayesianBahn lower |
| Missed connections, history only | 14,835 | Brier | -0.056 | -0.084 to -0.025 | BayesianBahn lower |
| Journey with a change, as shipped | 60,308 | CRPS, minutes | -1.756 | -1.919 to -1.608 | BayesianBahn lower |
| Journey with a change, history only | 60,308 | CRPS, minutes | -1.771 | -1.933 to -1.625 | BayesianBahn lower |
Lead time is counted back from the train’s scheduled departure, because that is when a passenger can still act on the answer. Lower is better; the scores are in minutes and are directly comparable — see the definitions at the foot of the page for why a point forecast and a distribution can be put on one axis.
Read the history-only series as a flat reference, not as a trend. It never looks at the live number and the arrival it predicts does not move, so its answer for a given stop is the same however early it is asked — checked on every render, and for the stops polled more than once the quantiles are identical at every lead time. What little the column does move across the buckets is composition and not behaviour: a train first seen forty minutes out is in the near buckets and in none of the far ones, so each bucket averages a different set of trains. Held to the stops present in all six, the history-only figure is the same in every bucket to three decimal places, while DB still climbs from 1.49 to 3.08 and the shipped model from 1.60 to 2.28. The degradation with lead time is real for the two series that read the live number, and only for those two.
| Before departure | Predictions | DB | History only | As shipped | DB bias | DB surprises |
|---|---|---|---|---|---|---|
| <10m | 21,950 | 1.60 | 2.19 | 1.44 | -1.10 | 5% |
| 10-20m | 49,271 | 1.90 | 2.18 | 1.59 | -1.38 | 7% |
| 20-45m | 77,885 | 2.44 | 2.24 | 1.88 | -1.98 | 10% |
| 45-90m | 49,875 | 2.98 | 2.23 | 2.13 | -2.67 | 13% |
| 1.5-3h | 49,131 | 3.21 | 2.22 | 2.21 | -2.92 | 14% |
| >3h | 48,507 | 3.30 | 2.22 | 2.23 | -3.07 | 15% |
The two variants cross over in this bucket. In the >3h bucket the shipped model scores 2.23 against history alone at 2.22 — leaning on DB’s number makes the answer 0.01 minutes worse there. That is the shape this model was changed to remove, so a crossover appearing here means a reported delay is being believed in a range where it should not be.
The means above summarise a badly skewed distribution, and the two forecasts differ in its upper tail rather than in its middle. That is worth reading in full, because a forecast a minute out and one twenty minutes out have very different consequences for a passenger.
One asymmetry has to be held in mind while reading the boxes. DB answers with a single minute, so its error is a whole number of minutes and is exactly zero — the train arrived in the minute DB named — for 41% of all predictions here. A distribution cannot score zero: it always pays something for its own spread, however well placed it is. So the bottom of the two boxes is not a like-for-like comparison and should not be read as one; the medians and everything above them are. The same asymmetry is why DB's median is exactly 1.00 minute in every bucket.
The last two columns of the table are the point. DB's score is the lower one on slightly more than half of the individual predictions, and its mean is higher all the same. Being closer more often and worse on average is what a heavy tail looks like from the inside.
The box spans the middle half of the predictions, the ruled line across it — overhanging on both sides, so it stays visible where it meets a box edge — is the median, and the whiskers reach the 10th and 90th percentiles. The worst tenth of each forecast therefore reaches past the whisker, and is given exactly in the table below.
| Before departure | Predictions | DB median | Our median | DB p90 | Our p90 | DB p99 | Our p99 | DB over 15 min | Ours over 15 min | DB lower on the prediction | DB mean | Our mean |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| <10m | 21,950 | 1.00 | 0.81 | 3.00 | 2.35 | 21.00 | 16.61 | 1.5% | 1.1% | 58.6% | 1.60 | 1.44 |
| 10-20m | 49,271 | 1.00 | 0.89 | 4.00 | 2.74 | 21.00 | 16.76 | 1.7% | 1.2% | 55.9% | 1.90 | 1.59 |
| 20-45m | 77,885 | 1.00 | 0.89 | 6.00 | 3.55 | 27.00 | 20.71 | 2.6% | 1.7% | 51.9% | 2.44 | 1.88 |
| 45-90m | 49,875 | 1.00 | 0.84 | 7.00 | 4.34 | 34.00 | 24.70 | 3.8% | 2.1% | 48.1% | 2.98 | 2.13 |
| 1.5-3h | 49,131 | 1.00 | 0.82 | 8.00 | 4.65 | 38.00 | 26.55 | 4.5% | 2.4% | 47.5% | 3.21 | 2.21 |
| >3h | 48,507 | 1.00 | 0.82 | 8.00 | 4.76 | 40.00 | 26.88 | 4.7% | 2.4% | 47.2% | 3.30 | 2.23 |
The last two columns give the share of forecasts out by more than 15 minutes. Over 296,619 predictions, most of which are uneventful, this is the part of the distribution a mean is least able to convey.
The app gives not only a time but a range stated to contain the true arrival four times in five. That is checkable: count how often the real arrival fell inside it. A bar at the dashed line matches the stated probability; below it the range is narrower than the forecast’s accuracy supports, and above it wider.
| Before departure | Predictions | History only | As shipped |
|---|---|---|---|
| <10m | 21,950 | 88% | 92% |
| 10-20m | 49,271 | 88% | 91% |
| 20-45m | 77,885 | 88% | 90% |
| 45-90m | 49,875 | 88% | 89% |
| 1.5-3h | 49,131 | 88% | 88% |
| >3h | 48,507 | 88% | 88% |
A connection here is a train arriving at one of the sampled stations and another leaving it a few minutes later, judged from before the first train set off — the last moment at which the answer could still change a decision. DB’s timetable answers yes or no; the app answers with a probability.
| Time to change | Connections | Actually caught | DB | History only | As shipped |
|---|---|---|---|---|---|
| 2-5 min | 56,076 | 90% | 0.086 | 0.078 | 0.071 |
| 6-10 min | 65,502 | 94% | 0.047 | 0.046 | 0.039 |
| 11-20 min | 135,473 | 97% | 0.025 | 0.025 | 0.021 |
| 21-30 min | 138,350 | 99% | 0.011 | 0.012 | 0.010 |
Almost every connection is caught, so answering “yes” every time scores well on the pooled average, and a yes/no answer taken from the timetable is close to doing that. The two outcomes are therefore worth reading apart: the connections that failed are the smaller group and the one the pooled figure says least about.
| Outcome | Connections | DB called it right | DB Brier | History only | …mean P(catch) | As shipped |
|---|---|---|---|---|---|---|
| Connection was caught | 380,566 | 99% | 0.005 | 0.006 | 0.97 | 0.009 |
| Connection was missed | 14,835 | 27% | 0.734 | 0.679 | 0.81 | 0.493 |
Everything above pools the collected days. Pooling gives the more precise estimate; it cannot show whether a result is a property of the model or of one day’s conditions. Here each day stands alone. A column that points the same way in every row is the more durable result; one that changes sign between days is within the range of day-to-day variation.
Read the row lengths too. The first day was collected from the evening onwards, so it is both smaller and drawn only from the busiest hours — a day with fewer missed connections here is not necessarily a calmer day.
| Day | Predictions | DB | History only | As shipped | 80% range, history | 80% range, shipped |
|---|---|---|---|---|---|---|
| 2026-08-17 | 3,894 | 2.93 | 2.59 | 2.20 | 86% | 87% |
| 2026-08-18 | 20,941 | 2.86 | 2.52 | 2.15 | 88% | 90% |
| 2026-08-19 | 20,573 | 3.40 | 2.88 | 2.46 | 85% | 87% |
| 2026-08-20 | 20,914 | 3.19 | 2.67 | 2.33 | 89% | 90% |
| 2026-08-21 | 20,735 | 3.18 | 2.51 | 2.25 | 88% | 89% |
| 2026-08-22 | 17,725 | 1.91 | 1.78 | 1.54 | 91% | 92% |
| 2026-08-23 | 16,613 | 1.76 | 1.73 | 1.58 | 91% | 92% |
| 2026-08-24 | 21,606 | 2.88 | 2.33 | 2.08 | 89% | 90% |
| 2026-08-25 | 21,142 | 3.45 | 2.87 | 2.53 | 85% | 87% |
| 2026-08-29 | 17,613 | 2.57 | 2.16 | 1.89 | 88% | 89% |
| 2026-08-30 | 16,489 | 2.08 | 1.86 | 1.67 | 90% | 90% |
| 2026-08-31 | 19,886 | 1.83 | 1.61 | 1.45 | 90% | 91% |
| 2026-09-01 | 19,936 | 2.42 | 1.87 | 1.67 | 88% | 90% |
| 2026-09-02 | 20,372 | 2.41 | 2.01 | 1.72 | 90% | 92% |
| 2026-09-03 | 19,308 | 2.23 | 1.72 | 1.55 | 89% | 90% |
| 2026-09-04 | 18,872 | 3.00 | 2.40 | 2.10 | 83% | 85% |
This is the smallest sample on the page and the one most easily over-read.
| Day | Missed | DB Brier | History only | As shipped |
|---|---|---|---|---|
| 2026-08-17 | 189 | 0.963 | 0.721 | 0.690 |
| 2026-08-18 | 1,218 | 0.696 | 0.760 | 0.484 |
| 2026-08-19 | 1,693 | 0.639 | 0.701 | 0.453 |
| 2026-08-20 | 1,611 | 0.654 | 0.675 | 0.440 |
| 2026-08-21 | 1,578 | 0.636 | 0.667 | 0.443 |
| 2026-08-22 | 501 | 0.842 | 0.674 | 0.556 |
| 2026-08-23 | 456 | 0.735 | 0.738 | 0.510 |
| 2026-08-24 | 1,415 | 0.721 | 0.706 | 0.517 |
| 2026-08-25 | 1,674 | 0.647 | 0.714 | 0.471 |
| 2026-08-29 | 722 | 0.666 | 0.640 | 0.433 |
| 2026-08-30 | 391 | 0.826 | 0.581 | 0.509 |
| 2026-08-31 | 496 | 0.877 | 0.567 | 0.488 |
| 2026-09-01 | 746 | 0.952 | 0.633 | 0.593 |
| 2026-09-02 | 662 | 0.918 | 0.631 | 0.569 |
| 2026-09-03 | 647 | 0.927 | 0.623 | 0.557 |
| 2026-09-04 | 836 | 0.844 | 0.653 | 0.534 |
Everything above scores a journey with a change by whether the change works. That is not the question a passenger has, which is when they get there — and it cannot be compared with the direct-journey figures, because a probability and a distribution over arrival times are not in the same units. This section asks the passenger's question. The answer is scored the same way a direct journey is, in minutes of CRPS, so the two rows can finally be read against each other.
Both forecasters are asked before the feeder sets off and see the same 5.7 candidate trains on average. DB answers with the arrival of whichever train its own forecasts say the passenger catches; ours answers with a distribution over the arrival, which includes missing the planned connection and taking a later train — it puts 0.8% of its weight on catching none of the candidates at all. The truth is the arrival of whichever train was actually caught.
Truth is not held to that list. A passenger who misses every train the app offered waits for the next one that runs, and the forecast is then wrong by however long that took — so the walk to the train actually boarded goes on past the end of the list, and both forecasters are charged for the difference. That happens on 28.4% of the journeys here. Scoring only the ones that fit inside the list would drop exactly the answers that were most wrong: it raised this model's margin over DB by about 0.8 minutes of CRPS while discarding a third of the evidence.
This is the newest part of the page and rests on the fewest days: the far end of a change only began being polled once the second tier existed, so this table starts later than every other one here.
The table is measured over the journeys both variants answered. They do not answer the same set: a connecting train with too little shared history can be carried by a live report and cannot be carried without one, so the shipped model answers 63,720 journeys against the blinded model's 62,031, and 60,308 are answered by both. Measuring each over its own set would put answers to different questions in one row — it made the live number look worth four times what it is worth on the journeys both answered. Answering more journeys is a real advantage of the shipped model, but it is a difference in coverage and is counted here rather than folded into a score.
| Before departure | Journeys | Boarded past the list | DB | History only | As shipped | 80% range, history | 80% range, shipped |
|---|---|---|---|---|---|---|---|
| <10m | 3,020 | 23% | 3.67 | 3.45 | 3.78 | 72% | 75% |
| 10-20m | 8,583 | 23% | 3.82 | 3.40 | 3.48 | 72% | 76% |
| 20-45m | 26,789 | 46% | 6.10 | 4.79 | 4.75 | 76% | 77% |
| 45-90m | 15,310 | 13% | 7.91 | 6.14 | 6.16 | 70% | 72% |
| 1.5-3h | 6,261 | 0% | 18.08 | 12.07 | 12.04 | 70% | 71% |
| >3h | 345 | 0% | 20.81 | 13.36 | 13.38 | 68% | 68% |
Everything above pools every train at every sampled station. The model does not treat them alike: it carries a separate prior per class of train — a mean of 4.0 minutes for long distance against 1.0 for an S-Bahn — so the classes are the axis those constants were chosen on, and splitting by them is the only way to see whether each one earns its value. Lower is better; the gap is BayesianBahn's score minus DB's, with the interval that decides it.
| Kind of train | Predictions | DB | History only | As shipped | Ours - DB | 95% interval | 80% range |
|---|---|---|---|---|---|---|---|
| Long distance | 2,611 | 6.60 | 6.70 | 4.88 | -1.717 | -2.341 to -1.047 | 86% |
| Regional | 110,057 | 3.58 | 2.97 | 2.50 | -1.083 | -1.161 to -1.007 | 89% |
| Bus (regional prior) | 34,152 | 4.62 | 4.20 | 3.86 | -0.763 | -0.871 to -0.654 | 83% |
| S-Bahn | 149,799 | 1.44 | 1.14 | 1.07 | -0.376 | -0.399 to -0.355 | 91% |
A category that is neither long distance nor one of the named regional
ones still falls through to the regional prior, because that is what
TrainClass.fromCategory does with anything beginning with a
letter. Replacement buses reach it that way, and they are a third of every
regional prediction here. They are not trains — they do not share the
tracks and are not held by the same signals — and they score about half a
minute worse than the regional trains they were pooled with, so they are
given their own row. The model is unchanged: the row name says which prior
they were scored under.
The two are different networks to forecast. DB’s own mean error is 2.82 minutes from Monday to Friday against 2.08 minutes at the weekend, so there is substantially less to improve on in the quieter half. Each figure below is BayesianBahn’s score minus DB’s, computed within that part of the week alone.
| Days | Predictions | DB’s own error | Arrival, shipped | Arrival, history only | Missed connections | Missed, shipped |
|---|---|---|---|---|---|---|
| Monday to Friday | 228,179 | 2.82 | -0.780 (-0.818 to -0.742) | -0.495 (-0.530 to -0.462) | 12,765 | -0.239 (-0.255 to -0.223) |
| Saturday and Sunday | 68,440 | 2.08 | -0.410 (-0.456 to -0.365) | -0.196 (-0.242 to -0.151) | 2,070 | -0.260 (-0.286 to -0.235) |
An interval that spans zero says the collected days cannot separate the two forecasters in that part of the week, not that they are equal.
Delay is not a property of a train alone: a late train occupies a platform and the next one can inherit some of it, so delay accumulated in the morning may still be on the network in the afternoon. If so, an hour-of-day term describes the data rather than merely fitting it.
The collected days show that pattern. The mean arrival delay climbs from 1.16 min at 01:00 to 4.18 min at 16:00, a factor of 3.6, and then drains overnight. Each train counts once, at its scheduled arrival hour, however many times it was polled. Only days that cover the whole clock are included, and only hours carrying at least 100 trains.
The maximum falls in the mid-afternoon rather than at the evening rush. One reading is that accumulated delay is worked off after the last peak departures, so the worst hour to arrive is set by how much delay the network is still carrying rather than by passenger numbers.
The model keeps separate delay statistics per time band, which only makes sense if the hours inside a band resemble each other. Spread is the distance between the latest and earliest hourly mean inside the band: a large spread means the band is averaging together hours that behave differently.
| Band | Hours | Quietest hour | Latest hour | Spread |
|---|---|---|---|---|
| MORNING_PEAK | 06-08 | 2.20 | 2.96 | 0.76 |
| MIDDAY | 09-15 | 2.91 | 3.92 | 1.00 |
| EVENING_PEAK | 16-18 | 3.74 | 4.18 | 0.44 |
| NIGHT | 19-05 | 1.16 | 3.31 | 2.15 |
The bands that ship were chosen from the commuter timetable rather than from this curve, and do not line up with it: the peak band ends before the peak, and the night band spans the range from the quietest hour of the night to the tail of the evening. This is an observation about the model’s bucketing rather than about the trains, and it is only as good as the days collected so far.
Every figure above comes from the commands below. The collector must have been running on the day in question; the archive publishes the ground truth the following morning.
# collect (runs continuously, survives restarts and power cuts) pixi run -e evaluate collect pixi run -e evaluate collect-status # is it still healthy? # once the archive has published the day, score it and rebuild # this page — stages already done are skipped, so adding a day # costs only that day pixi run -e evaluate evaluate 2026-08-17 2026-08-18 2026-08-19 2026-08-20 2026-08-21 2026-08-22 2026-08-23 2026-08-24 2026-08-25 2026-08-29 2026-08-30 2026-08-31 2026-09-01 2026-09-02 2026-09-03 2026-09-04