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
What problem does GP Access Planner solve?
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.
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.
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.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.
- Stage
Checksum and archive validation
A private build plane validates public source archives and loads PostgreSQL without exposing source credentials to the public runtime.
- Stage
Typed ingestion into immutable
dbt separates publisher-grain observations from derived marts; forecasting uses only publication-time-safe features.
- Stage
dbt source-native and derived
Python evaluates seasonal naive, Elastic Net, LightGBM, and CatBoost across twelve rolling origins and three horizons.
- Stage
Rolling-origin baseline and challenger
Approved releases are exported as immutable, checksummed, paged JSON artifacts and uploaded to Cloudflare R2.
- 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.
- Stage
Isolated candidate smoke test
Candidate and production Workers use separate pointers; promotion changes only current.json and preserves rollback artifacts.
- 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 stageRead the architecture as text
- 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.
- dbt separates publisher-grain observationsStage 2 · PostgreSQL
dbt separates publisher-grain observations from derived marts; forecasting uses only publication-time-safe features.
- Python evaluates seasonal naive,Stage 3 · dbt
Python evaluates seasonal naive, Elastic Net, LightGBM, and CatBoost across twelve rolling origins and three horizons.
- Approved releases are exportedStage 4 · scikit-learn
Approved releases are exported as immutable, checksummed, paged JSON artifacts and uploaded to Cloudflare R2.
- 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.
- Candidate and production WorkersStage 6 · CatBoost
Candidate and production Workers use separate pointers; promotion changes only current.json and preserves rollback artifacts.
Pivotal decisions
Technology choices recorded with their trade-offs.
These choices are derived from the project’s published decision record.
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.
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.
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.
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.
- 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
- Calibrate forecast intervals by geography or pooled cohort before presenting them as probabilistic coverage rather than indicative ranges.
- Add automated source-freshness monitoring and a documented release service-level objective before increasing operational reliance.
- Measure API latency, cache hit rate, and R2 request cost under representative concurrent browsing before changing the paged artifact design.
- 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