← All work
Deployed technical projectHealthcare · Public sector

A public-data planning product that forecasts recorded general-practice appointments across England while keeping observed access signals and hypothetical capacity explicitly separate.

GP Access Planner

The business question

What problem does GP Access Planner solve?

The answer

Delivered a source-traceable 7, 14, and 28-day planning surface for 104 sub-ICBs, backed by 32.9 million validated source rows, rolling-origin evaluation, immutable releases, and a live edge API.

Demonstrated value

Delivered a source-traceable 7, 14, and 28-day planning surface for 104 sub-ICBs, backed by 32.9 million validated source rows, rolling-origin evaluation, immutable releases, and a live edge API.

My role

Senior Healthcare Analyst

Built a public-data GP access planning product across 104 sub-ICBs, reconciling 32.9 million source rows and governing 7, 14, and 28-day forecasts through twelve rolling origins and explicit WAPE, MASE, coverage, and geography gates.
Primary stakeholder

A primary-care planning analyst, healthcare analytics reviewer, or hiring manager assessing data contracts, forecast governance, and public-cloud delivery.

Context and stakes

The decision context behind the system.

The live product gives planners a bounded view of where recorded appointment pressure may merit review over the next four weeks. It covers 104 sub-ICBs and keeps context, forecasts, and hypothetical capacity in distinct lanes. The approved baseline remains below the 15% 28-day WAPE ceiling, but interval under-coverage and the absence of operational capacity data are explicit decision limits.

Public primary-care data can describe recorded access pressure, but it cannot reveal actual available capacity. GP Access Planner turns that limitation into the product contract: observed appointments and access signals stay separate, forecasts are promoted only through named gates, and visitor-entered capacity remains clearly synthetic. Every headline claim traces to evaluation, release, or live-deployment evidence.

Approach

A bounded approach shaped by the published constraints.

Delivered a source-traceable 7, 14, and 28-day planning surface for 104 sub-ICBs, backed by 32.9 million validated source rows, rolling-origin evaluation, immutable releases, and a live edge API.

Design constraints

  • Recorded appointments are not available slots, workload, utilisation, cancellations, or actual capacity.
  • The nominal 80% forecast interval achieved 55.09% coverage and is presented only as an indicative range.
  • No challenger passed every promotion gate, so the approved forecast intentionally remains seasonal naive.
  • The public snapshot has batch freshness and does not stream operational NHS data.

Decision journey

The system moves through an inspectable sequence.

Each stage below comes from the published project manifest.

  1. Stage

    Checksum and archive validation

    A private build plane validates public source archives and loads PostgreSQL without exposing source credentials to the public runtime.

  2. Stage

    Typed ingestion into immutable

    dbt separates publisher-grain observations from derived marts; forecasting uses only publication-time-safe features.

  3. Stage

    dbt source-native and derived

    Python evaluates seasonal naive, Elastic Net, LightGBM, and CatBoost across twelve rolling origins and three horizons.

  4. Stage

    Rolling-origin baseline and challenger

    Approved releases are exported as immutable, checksummed, paged JSON artifacts and uploaded to Cloudflare R2.

  5. Stage

    Immutable serving-artifact export and

    A Cloudflare Worker reads a bounded release pointer, applies request limits, and serves versioned envelopes to a static Next.js interface.

  6. Stage

    Isolated candidate smoke test

    Candidate and production Workers use separate pointers; promotion changes only current.json and preserves rollback artifacts.

  7. Stage

    Static planning interface and

    Static planning interface and bounded edge API

Interactive architecture

The published architecture, made interactive.

A private build plane checksum-validates selected public releases, preserves publisher grain in PostgreSQL, and uses dbt to separate source-native observations from derived marts. Python evaluates seasonal naive, Elastic Net, LightGBM, and CatBoost over twelve rolling origins. The baseline remains champion because no challenger passed every horizon and geography gate. The approved snapshot is exported as immutable paged artifacts, checksum-matched into R2, exercised through an isolated candidate Worker, and promoted by changing one bounded pointer. A static Next.js interface consumes versioned API envelopes; scenarios run as deterministic, non-persistent calculations at the edge.

Drag to pan · use controls to zoom · select any stage
100%
feedsfeedsfeedsfeedsfeedsStage 1A private build planePythonStage 2dbt separates publisher-grain observationsPostgreSQLStage 3Python evaluates seasonal naive,dbtStage 4Approved releases are exportedscikit-learnStage 5A Cloudflare Worker readsLightGBMStage 6Candidate and production WorkersCatBoost
Read the architecture as text
  1. A private build planeStage 1 · Python

    A private build plane validates public source archives and loads PostgreSQL without exposing source credentials to the public runtime.

  2. dbt separates publisher-grain observationsStage 2 · PostgreSQL

    dbt separates publisher-grain observations from derived marts; forecasting uses only publication-time-safe features.

  3. Python evaluates seasonal naive,Stage 3 · dbt

    Python evaluates seasonal naive, Elastic Net, LightGBM, and CatBoost across twelve rolling origins and three horizons.

  4. Approved releases are exportedStage 4 · scikit-learn

    Approved releases are exported as immutable, checksummed, paged JSON artifacts and uploaded to Cloudflare R2.

  5. A Cloudflare Worker readsStage 5 · LightGBM

    A Cloudflare Worker reads a bounded release pointer, applies request limits, and serves versioned envelopes to a static Next.js interface.

  6. Candidate and production WorkersStage 6 · CatBoost

    Candidate and production Workers use separate pointers; promotion changes only current.json and preserves rollback artifacts.

Technology used
PythonPostgreSQLdbtscikit-learnLightGBMCatBoostNext.jsTypeScriptCloudflare WorkersCloudflare R2

Pivotal decisions

Technology choices recorded with their trade-offs.

These choices are derived from the project’s published decision record.

Decision

Use Seasonal-naive champion with gated challengers within the published boundary

A named baseline makes improvement claims testable and remained the only model to pass every declared horizon and geography gate.

Constraints
  • Precomputed forecasts and bounded artifacts remove database credentials and model inference from the public request path, at the cost of batch freshness.
  • Publisher-grain rows remain traceable, but paged serving indexes add many small immutable objects to each release.
  • Seasonal naive is less sophisticated than the challengers, but it was the only model that passed every declared promotion gate.
Alternatives
  • Promote LightGBM or CatBoost on aggregate performance alone
Criteria
Fit for the stated scope · Inspectable evidence · Explicit operating boundary
Selected
Seasonal-naive champion with gated challengers
Why
A named baseline makes improvement claims testable and remained the only model to pass every declared horizon and geography gate.
Trade-off
The simpler model gives up sophistication but avoids promoting a challenger with short-horizon or geography regressions.
What supported it
10.96% 28-day WAPE: Approved seasonal-naive forecast across 104 eligible sub-ICBs and twelve rolling origins.
Revisit when
Calibrate forecast intervals by geography or pooled cohort before presenting them as probabilistic coverage rather than indicative ranges.
Decision

Use Immutable R2 artifacts and pointer promotion within the published boundary

Precomputed, checksum-verified objects provide traceability, bounded reads, candidate isolation, and rollback without public database access.

Constraints
  • Precomputed forecasts and bounded artifacts remove database credentials and model inference from the public request path, at the cost of batch freshness.
  • Publisher-grain rows remain traceable, but paged serving indexes add many small immutable objects to each release.
  • Seasonal naive is less sophisticated than the challengers, but it was the only model that passed every declared promotion gate.
Alternatives
  • Serve PostgreSQL queries and live model inference
Criteria
Fit for the stated scope · Inspectable evidence · Explicit operating boundary
Selected
Immutable R2 artifacts and pointer promotion
Why
Precomputed, checksum-verified objects provide traceability, bounded reads, candidate isolation, and rollback without public database access.
Trade-off
Batch releases are less fresh and contain many small objects, but the public runtime holds no database credentials.
What supported it
32.9M Validated source rows: Exact loaded row count across 29 analytical resources in the private build snapshot.
Revisit when
Add automated source-freshness monitoring and a documented release service-level objective before increasing operational reliance.
Decision

Use Static Next.js interface with a Worker API within the published boundary

Static product routes keep deployment small while the Worker supplies versioned data envelopes, request limits, and deterministic scenarios.

Constraints
  • Precomputed forecasts and bounded artifacts remove database credentials and model inference from the public request path, at the cost of batch freshness.
  • Publisher-grain rows remain traceable, but paged serving indexes add many small immutable objects to each release.
  • Seasonal naive is less sophisticated than the challengers, but it was the only model that passed every declared promotion gate.
Alternatives
  • Run a stateful full-stack application server
Criteria
Fit for the stated scope · Inspectable evidence · Explicit operating boundary
Selected
Static Next.js interface with a Worker API
Why
Static product routes keep deployment small while the Worker supplies versioned data envelopes, request limits, and deterministic scenarios.
Trade-off
The surface cannot support persistent user accounts or saved scenarios, which matches the anonymous portfolio boundary.
What supported it
404,772 / 404,772 Checksum-matched release files: Every local object in release 2026-08-13.1 matched its remote R2 counterpart.
Revisit when
Measure API latency, cache hit rate, and R2 request cost under representative concurrent browsing before changing the paged artifact design.
Decision

Use Publisher-grain source preservation within the published boundary

Keeping observed rows at their lowest published grain makes displayed summaries traceable and preserves source nulls and suppression markers.

Constraints
  • Precomputed forecasts and bounded artifacts remove database credentials and model inference from the public request path, at the cost of batch freshness.
  • Publisher-grain rows remain traceable, but paged serving indexes add many small immutable objects to each release.
  • Seasonal naive is less sophisticated than the challengers, but it was the only model that passed every declared promotion gate.
Alternatives
  • Publish only aggregate dashboards
Criteria
Fit for the stated scope · Inspectable evidence · Explicit operating boundary
Selected
Publisher-grain source preservation
Why
Keeping observed rows at their lowest published grain makes displayed summaries traceable and preserves source nulls and suppression markers.
Trade-off
Traceability increases artifact volume and requires bounded paging and serving indexes.
What supported it
99.98% smaller Bounded metadata response: The production metadata response fell from 31,107,910 bytes to 6,134 bytes.
Revisit when
Keep capacity schedules synthetic unless governed operational slot and roster data become available with explicit authorization and new disclosure controls.

Results and evaluation

Measured results within the disclosed evaluation boundary.

10.96%28-day WAPE
Method
Weighted absolute percentage error at the longest declared planning horizon.
Meaning
Aggregate error remained below the 15% active-model ceiling.
Limitation
Recorded appointments are not available slots, workload, utilisation, cancellations, or actual capacity.

Limitations and next steps

Useful evidence, bounded claims.

What this does not prove

  • Recorded appointments are not available slots, workload, utilisation, cancellations, or actual capacity.
  • The nominal 80% forecast interval achieved 55.09% coverage and is presented only as an indicative range.
  • No challenger passed every promotion gate, so the approved forecast intentionally remains seasonal naive.
  • The public snapshot has batch freshness and does not stream operational NHS data.
  • Synthetic capacity scenarios do not model skill mix, appointment duration, absence, estates, or local booking rules.
  • The anonymous deployment has no clinical availability objective and must not be used for patient-level or clinical decisions.

What I would do next

  1. Calibrate forecast intervals by geography or pooled cohort before presenting them as probabilistic coverage rather than indicative ranges.
  2. Add automated source-freshness monitoring and a documented release service-level objective before increasing operational reliance.
  3. Measure API latency, cache hit rate, and R2 request cost under representative concurrent browsing before changing the paged artifact design.
  4. Keep capacity schedules synthetic unless governed operational slot and roster data become available with explicit authorization and new disclosure controls.

This public-data portfolio product supports review of recorded appointment pressure only. It contains no PHI, operational slot data, rosters, free text, or clinical advice, and it makes no claim to measure NHS capacity or utilisation.

Explore the rest of the work