You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A forecast for trains whose run number is too new to have one. IRIS gives
every run its own number and renumbers at each timetable change, so a train
that has run for years can arrive with almost nothing behind it: over eleven
collected days a quarter of arrivals fell through to a class-wide prior that
knows neither the station nor the hour. Those trains still have a line, and
the line has run all along. pipeline/build_shards.py now publishes a second
set of shards keyed by line and station, and the app reads one when — and only
when — the train's own history comes up short. Walk-forward over 781,000
archive events at 62 stations, on exactly the events the prior answers today,
the line scores 0.43 min of CRPS better (95% 0.40..0.46, resampling whole
trains) with a shard available for 88% of them; across the December 2025
timetable change, where the population doubles to 11% of events, the same
0.41. Regional and S-Bahn both gain; long distance almost never carries a
line number, so there is nothing to say about it either way. The screens say
which line the numbers came from rather than claiming they are this train's.
Changed
A reported delay is now an anchor with an admitted error, not a promise.
0.3.0 took DB's live delay and adjusted it by what the train's own past runs
did; where DB reported nothing it used history alone. The adjustment was the
weaker half. A report is now the centre of its own distribution, widened by
the error such reports are measured to carry at that much notice — fitted on
82,482 arrivals, and asymmetric, because a train that has lost time rarely
makes it back. The connecting train's departure report is treated the same
way, so a change is the difference of two admitted errors rather than one
distribution against one promise. Scored against DB over the same thirteen
days as 0.3.0: arrival CRPS −0.564 → −0.678 minutes, missed connections
−0.147 → −0.225 Brier, and "every connection" moves from not separated from
DB (+0.001) to −0.005. Over sixteen days the shipped model is −0.695 minutes
on 296,619 arrivals and −0.242 on 14,835 missed connections.
A train's history and its line's are pooled, not switched between. The
first version of the above picked one or the other. Backtesting the
combination showed the switch to be a step function fitted to a curve: the
train's own runs now take n / (n + 8) of the weight and its line's the rest,
so a fresh run number answers almost entirely from its line and a settled one
barely notices the line is there. Worth 0.105 min of CRPS against answering
from the line alone (95% 0.094..0.116) and 0.002 against the number alone
(95% 0.002..0.003), where no fixed weighting can have both ends —
half-and-half wins by 0.119 where the number has nothing and loses 0.032
across the 87% of predictions where it has plenty. End to end, 0.025 min
against having no line at all, with 0.5% of arrivals left to the class prior
instead of 4.8%. Above 32 effective runs of its own a train does not read the
line shard at all, which costs 0.002 and saves a fetch on seven predictions
in eight. Where the line contributed, the screens name it.
Forecasts are faster than before the line existed. The time-of-day filter
is the only code that touches every run in a shard, and it split and parsed
two "HH:mm" strings per run on every prediction. HistoricalRun parses its
planned time once, when the shard is read: a forecast over the largest line
shard in the country (8,505 runs) takes 0.17 ms instead of 0.64, and an
ordinary number-only forecast 0.042 ms instead of 0.094.
The line shard is a separate lookup, not a second candidate key. HistoryRepository.load had always asked for a line-keyed shard when the
number's key missed. Now that those shards exist, that would have handed a
line's pooled runs to every caller — including the two-leg model, which pairs
a candidate's departure and arrival by date and would have joined one train's
departure to another train's arrival. Line-keying also loses to number-keying
wherever the number has a history (0.13 min of CRPS, 95% 0.12..0.13), so it
is now reached only through the fallback that measured better.
Fixed
A connection search froze the screen it had just put a spinner on.
Reported from a Pixel 10 as a hang and repeated crash (#2); the attached trace
is an ANR, with the main thread five seconds deep in app code. Every viewModelScope.launch named no dispatcher, which is the UI thread, and IrisClient left the IO dispatcher as soon as the bytes arrived — so the XML
parse, the history model and the connection mixture all ran where the frames
are drawn. Nothing could draw while the search ran, so the spinner stopped and
the result appeared all at once, and on a slow enough route Android killed the
app instead. Parsing and planning now run off the main thread; only the state
assignments Compose reads stay on it.
The cancellation figure read "n/a" for every train DB had reported on.
Anchoring on a live report returned before the train's own record was
consulted, and the cancellation rate comes from that record — so the one
number that says how often this train simply does not run went blank exactly
when the app had most to say about it.
A change could be answered with no number at all. The probability of
boarding underflows to exactly zero for a connection far enough out of reach,
and the rule that always keeps the first candidate then kept it at zero
weight — a distribution whose weights sum to nothing, which answers NaN rather
than declining to answer.
The on-demand cache grew for the life of the install, and a file written
during a kill stayed "fresh" for eighteen hours. Both are solved problems in
RFC 9111, so the hand-written disk cache is gone and OkHttp does it: entries
are evicted, a partial write is never published, and revalidating an unchanged
shard now costs a round trip instead of the file.