Network Pulse

← All notes

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):

  1. 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.28 and U+FEFF is zero-width.
  2. Marks removed, Version part set to 3.0. This got past the packager entirely, which was the point of the exercise.
  3. Failed on version compatibility instead — so something in Metadata, Settings or the report config block 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.