The punctuality feeds, and a Power BI project that reads them
Status: published, generated 14 September 2026 against the feeds as they stood that day.
Download the Power BI project · the report theme on its own
Unzip it anywhere and open SYMCA Punctuality.pbip in Power BI Desktop. It is
a PBIP project — the text format Desktop opens natively — rather than a .pbix,
which is a proprietary binary that cannot be generated outside the product.
The .pbit templates do not open — don't use them
There are two .pbit files in /static/downloads from an attempt on
22 September 2026 to hand the same model over as a single double-clickable
template. Neither opens, so they are not linked here. Three builds were
tried against Desktop 2.157 (August 2026):
- Byte-order marks on every part inside the package. Refused as "encrypted or
corrupted", with the inner error
'1.28' is not a valid .pbix file version number— a value that is a valid version number, because the string actually parsed was\ufeff1.28and U+FEFF is zero-width. - Marks removed,
Versionpart set to3.0. This got past the packager entirely, which was the point of the exercise. - Failed on version compatibility instead — so something in
Metadata,Settingsor the reportconfigblock does not match what that Desktop expects.
The generator and its checks are kept in tools/powerbi/build_template.py,
because the encoding finding is worth keeping and the checks catch thirteen
real faults. What defeated it is that there is no Power BI on the machine that
writes these, so the container format was reasoned about rather than copied.
If this is ever picked up again, start by exporting a template from Desktop
itself (File → Export → Power BI template) and matching its parts one for
one. Do not reason about it again.
The project folder above is unaffected by any of this: it is a different format, and it is the artefact that was actually handed over.
The feeds
Five CSV endpoints under https://performance.networkpulse.info, anonymous, no
key. All of them are the same numbers the site itself reports, so a figure in a
spreadsheet and a figure on a page cannot disagree.
| Endpoint | Grain | Size |
|---|---|---|
/api/punctuality/daily.csv?area=370&from=…&to=… |
day × operator × line × route | ~430 KB for a fortnight |
/api/reports/periods.csv |
financial period × operator | 3 KB |
/api/reports/daily.csv |
day, network total | 2 KB |
/api/capture-health/rebadges.csv |
one row per journey re-badge | 240 KB |
/api/vix/compare.csv |
journey, both systems side by side | 13 KB |
area=370 is not optional on the first of those. Unscoped it covers everywhere
the platform tracks — two bounding boxes, most of the north of England and one
around West Sussex — and the request is refused above 400,000 rows. Every other
figure on this platform is South Yorkshire, and a report mixing the two reads as
a South Yorkshire number while describing something else.
Refresh after 05:00 UK. The nightly rollup runs at 02:20 and the exports finish by 04:10.
Which column is the measure
route_observations and route_on_time. Not timepoint_*.
The difference is whether arrival at the last stop counts. It should not: the last leg of a timetable carries slack, an interchange is where a bus is meant to wait, and nobody misses a bus that got there early. Counted in, the terminus runs early on roughly three arrivals in ten against fewer than three in a hundred along the route — a different measurement, not different driving — and it holds the headline about two points low. First South Yorkshire reads 83.6% with it and 86.0% without.
The terminus is still reported, in its own columns, because when a bus reaches its destination is worth knowing. It is simply a different question from whether it kept to the timetable on the way.
Colours
The project opens on Power BI's own CY24SU10, which is the teal-blue default.
SYMCA.json in the zip is the theme that matches the rest of this platform —
the same orange, dark grey for everything ordinary, red kept for a fault. Apply
it with View → Themes → Browse for themes.
It ships as a file to import rather than baked into the project. Embedding a
custom theme means a resource package inside report.json, and a report.json
this project could not open accounted for three of the four failures getting
here. A one-click import cannot break a file that is now working.
What the project contains
Six tables, all ordinary queries. Four read the feeds; Date and Operators
derive from the first of those — Date from the range the data actually holds,
so it cannot offer a day that was never recorded, and Operators from the
operators that actually have data, so one slicer covers every table.
Twenty-two measures, each with a description and in a display folder. Three
parameters: AreaCode, FromDate and ToDate.
Two earlier builds would not open, and both are worth recording because the lesson is the same one twice.
The first had a measure called Observations against a column called
observations on the same table. Names within a table are case-insensitive to
the model engine, so it refused the whole file. Validating that every field a
visual references exists is not the same as validating that every name is
free.
The second built Date and Operators as DAX calculated tables, whose columns
carry names the engine infers from the expression — and a relationship pointing
at one of those was rejected outright: relationship 'Operators-Punctuality'
uses an invalid column ID. Rather than guess which attribute the engine
disliked, both are now ordinary queries with ordinary columns, deriving from the
Punctuality query rather than fetching anything again. There are no calculated
tables and no inferred columns left in the model. The construct that broke is
gone rather than adjusted, which is the only fix worth making to something that
cannot be tested locally.
The third loaded the model — six queries, import mode, no complaint — and then
failed to render the report: cannot read properties of undefined (reading
'customTheme'). A report's config must carry a themeCollection, which the
ribbon reads while the report loads; I had dropped it when adding the visuals.
The base theme is named rather than shipped, so nothing has to resolve.
The generators now refuse to write a model whose measures collide with columns, whose relationships or measure expressions name something absent, whose sort columns are missing, or whose declared columns are not produced by their own query; and a report without a theme collection or with an incomplete visual.
Three failures, three different layers — names, model structure, report chrome — and each one found by opening the file rather than by any check I could run here. That is the honest cost of generating a format blind, and it is worth knowing before anyone asks for a second one.
Two of the measures exist to be compared rather than reported.
OTP % including terminus is there to reconcile against figures produced
elsewhere. Period OTP % (complete only) excludes part periods in the measure
rather than in a report filter, so a visual cannot show one by having a filter
forgotten.
Five pages and nineteen visuals. Every visual was checked against the model —
nothing names a measure or column that does not exist, every referenced field is
projected, every query alias is declared and used. What could not be checked is
whether Power BI Desktop likes the layout, because there is no Power BI on the
machine that generated it. If a visual comes up blank, delete it and drag the
fields back; the measure behind it is right. If the project refuses to open at
all, that is the report rather than the model: delete
SYMCA Punctuality.Report/report.json, reopen, and a fresh empty report is
built over the same model.
What the numbers do not say
Every figure counts stops where a bus was tracked. A journey nobody tracked and a journey that did not run leave the same absence, so this is a ceiling on performance rather than a count of cancellations. Nothing in the data can tell the two apart, and a report that implies otherwise is overstating what was measured.
Recording began 4 August 2026. There is nothing earlier and there never will be:
this is built from live observations that were not being made. Start time
adherence begins 7 August, so sta_pct is empty before then — left null on
purpose, because coercing it to zero draws a collapse that never happened.
Today is excluded everywhere. It is a part day, and a part day in a punctuality figure reads as a bad one.
The on-time window is one minute early to five minutes 59 late, the national standard. It is also the window Vix uses — checked against their own export rather than assumed — so where our figures and theirs differ, it is not because the two systems disagree about what "on time" means. See the Vix comparison.
If the feeds cannot be reached
The data/ folder in the zip holds all four files as they were on
14 September 2026. Where a corporate network blocks outbound HTTPS, each query
takes a one-line change from Web.Contents to File.Contents and nothing else
moves. The README in the zip has the exact edit.