Sunday, 15 November 2026

Building Intersection Intelligence: the slides and the story

By Mo Khan · Presentation edition · 28 slides

I had never built a mapping application before Intersection Intelligence. I started with a question about a place: what is here, and how do we know? That question became my personal mapping experiment and an internal research application, built with help from Codex and Claude. It is not being used commercially.

This talk follows the journey from South Africa to a single corridor: learning the geospatial stack, importing imperfect datasets, preserving human knowledge, tuning rendering performance and choosing an achievable research MVP. It includes the software architecture, technology stack and Linux deployment.

Open the full-screen presentation ↗

Use Next to reveal each build, or choose a slide from the menu. Show all reveals a whole slide; Notes opens the accompanying explanation. For detailed maps and diagrams, use Full screen. On a phone, opening the presentation in a new tab and rotating to landscape gives it more room.

Click inside the deck to use the keyboard: arrows or Space to advance, A to reveal, N for notes, F for fullscreen. The text version below remains available without the player.

The South African screenshots have company and provider names, commercial values and installation counts obscured. Infrastructure identifiers are omitted. Deterministic masks preserve the original map pixels, labels and geometry. The cover is a fictional intersection illustration generated with AI.

Map data © OpenStreetMap contributors; Protomaps cartography. Research use does not remove attribution or licence obligations.

The story behind the slides

Expand a title to read its explanation.

01 · Building Intersection Intelligence

I had no previous experience building mapping applications. Intersection Intelligence is my personal map-learning experiment and an internal research tool. It is not being used commercially. This talk explains the decisions, mistakes, performance cycles and accepted research MVP. Codex helped implement and verify the work. Claude challenged architecture and reviewed recorded increments. I remained responsible for intent and acceptance. The fictional cover illustration was generated with AI. Revised 14 November 2026.

02 · A question about a place

The starting point was a gap between aggregate knowledge and specific places. A spreadsheet could tell me how many assets were reported in a region. I wanted to select an intersection and understand the evidence behind it. That includes who has responsibility, what equipment is recorded and what remains unknown. I am deliberately using generic terms here because the engineering story does not depend on publishing our commercial information. The aim was to connect geographic context with attributable knowledge. This is the product question that drove the rest of the architecture.

03 · Start with South Africa

Start at country scale in South Africa. This is a screenshot of the signed-in research app captured on 14 November 2026. Company and provider names, commercial values and installation counts are redacted. Map data © OpenStreetMap contributors, https://www.openstreetmap.org/copyright; Protomaps cartography. Published administrative boundaries are reference geography, not cadastral boundaries. The map is a learning and research environment with uneven source coverage. It is not a commercial service. Sensitive UI is covered with deterministic pixel masks. The original map pixels, labels and geometry are unchanged.

04 · The journey in the repository

The design existed before the late-August implementation. The first standalone slice landed on 30 August with deterministic fixtures. Real Gauteng data followed immediately, and MapLibre with PMTiles arrived by 1 September. September then expanded into team contributions, source integration, identity preservation and analytics. Performance work became central later that month. Signal AI reached its first native production increment at the end of September. October and early November added country coverage and hardened deployment boundaries. These are milestones reconstructed from git and release records. They are not an assertion that every broader product ambition was complete at each milestone.

05 · Zoom into Gauteng

The same map moves into Gauteng, where the first real-data slice taught me what the prototype had hidden. Early rendering repairs bounded visible objects, reused coordinates and improved initial paint. By 1 September we moved to MapLibre GL JS with PMTiles. The screenshot shows the later research app, not the original prototype. Sensitive UI is redacted. Map data © OpenStreetMap contributors; Protomaps. https://www.openstreetmap.org/copyright Sensitive UI is covered with deterministic pixel masks. The original map pixels, labels and geometry are unchanged.

06 · Find the city-scale question

The app focuses on Johannesburg and a useful cluster of mapped junctions. The selected city, shortlist and rendered evidence need consistent scope. A fast pan is insufficient if the displayed answer belongs to an earlier viewport. Sensitive UI is redacted. Map data © OpenStreetMap contributors; Protomaps. https://www.openstreetmap.org/copyright Sensitive UI is covered with deterministic pixel masks. The original map pixels, labels and geometry are unchanged.

07 · Follow a corridor

A corridor turns isolated points into a connected geographical question. This view shows a published Johannesburg route, not traffic simulation or a prediction of flow improvement. The screenshot is from the research app on 14 November 2026. Sensitive UI is redacted. Map data © OpenStreetMap contributors; Protomaps. https://www.openstreetmap.org/copyright Sensitive UI is covered with deterministic pixel masks. The original map pixels, labels and geometry are unchanged.

08 · One place, several kinds of evidence

OSM can describe multiple signal nodes and roads. An authority register can report an equipment asset. A colleague can contribute an observation. A regional summary can count assets without giving coordinates. These are different grains. Summing them can double count or create a population that means nothing. We preserved their identities and provenance, then resolved the view appropriate to the user's question. Aggregate counts never became fabricated point locations. Unknown, absent, unavailable and zero remained distinct.

09 · Every source needed a way in

Every provider or authority had different workbook layouts, street conventions, identifiers and completeness. We built custom importers, preserved original values and source identity, then shared only stable matching mechanics. Fuzzy road-name and road-pair matching plus geographic evidence produced candidates. The documented browser workflow used the app and Google Maps to review confusing locations. That did not confer copying rights, establish ownership or turn an agent-assisted result into a human confirmation. Ambiguous and unpositioned rows remained in the inventory. Several assets could share a physical marker. Import validation checked dossiers and exports as well as parsed rows.

10 · Refreshes had to preserve human knowledge

Once people contribute, stable identity becomes a product requirement. Source geography can change, and grouping can split or merge candidates. Rebuilding the map must preserve the human evidence already attached to it. We retained source identities, tracked aliases and surfaced unresolved reconciliation rather than silently reminting records. A specific September correction preserved deleted heads as well as active API rows. That detail shows why checking only the visible current records was insufficient. The diagram is an illustrative identity model, not an actual production record.

11 · Codex and Claude worked through evidence

Codex was the implementation owner in the documented collaboration loop. Claude acted as a peer architect and read-only independent reviewer, inspecting the same repository. I supplied product intent and resolved decisions the evidence could not make for us. The curated ledger recorded questions, findings, corrections and decisions. It was an engineering notebook rather than a message transport. This arrangement reduced the amount of manual copying between assistants. It did not mean that every commit had independent review. Separate workstreams sometimes had different ownership.

12 · Independent review changed the work

These are examples of what the review loop was for. Early MVP reconciliation retired stale assumptions about Leaflet, hosted extraction and the deployment model. Later Signal AI review caught places where shared infrastructure assumed a financial business unit. We also kept responsibility for execution clear: Claude's source review was not a claim that it had run the tests. Codex had to produce that evidence. I had to manage the process too. During the long September AI refactor I pushed for delivery, separated unrelated work and ultimately requested solo continuation. The records preserve that boundary. They do not claim Claude approval for later solo work.

13 · Software architecture

The native JavaScript atlas uses MapLibre and PMTiles. The shared chat client brings its React conversation surface into the map host. The standalone Fastify API owns product semantics and authorised access. Prepared files support map and dossier reads; verified read-only SQLite workers serve analytics. Human evidence uses a separate mutable repository. Signal AI shares execution and operations while Intersection owns its tools and geographic context. The existing platform remains the entitlement authority. Everything is in the monorepo, but the deployment package is isolated. A root-checkout import is not proof that the deployed service starts.

14 · The technology stack

The browser uses a native JavaScript atlas with MapLibre, PMTiles and Protomaps, plus the shared React chat client. Node 24 and Fastify serve product-specific geography and evidence contracts. Python handles heavy geospatial preparation and custom source adapters off-host. Versioned files and read-only SQLite serve prepared evidence. Blob events and conditional heads preserve human contributions. Entra ID, the shared AI platform and GitHub CI supply integrations. This stack diagram describes concrete technologies; the preceding architecture shows responsibility and request flow.

15 · Building and serving are different workloads

The early real-data preparation exceeded the small serving host's memory envelope. We responded by separating the workloads. Operator-triggered Python processing acquires pinned sources, builds geography and validates the resulting release. The application serves prepared artifacts and accepts bounded edits. Application CI packages code; it does not run national extraction. This also makes rollback more understandable because a release has an explicit application identity and an explicit data identity. The benefits are architectural, not a claim of a specific infrastructure saving.

16 · Deployment architecture

This is a generalised view of the documented early-November deployment. App Service sits behind Entra authentication. The browser obtains short-lived read-only access to the geographic basemap account. Restricted source evidence and ownership overlays follow authenticated application paths. Restricted evidence storage restricts network access to the application's integration subnet via a Storage service endpoint. This is a restricted public endpoint design, not Private Link. Prepared application data remain on persistent storage and verified local analytical copies. The map application calls existing entitlement and AI services. Resource names, endpoints, network ranges and credentials are omitted.

17 · A small Linux host shaped the design

The recorded host is one Azure App Service Linux B2 instance, with the built-in Node.js 24 LTS runtime and Always On. Microsoft lists B2 as two cores, 3.5 GB RAM and 10 GB plan storage. Plan storage differs from the temporary local filesystem whose actual free capacity was measured for SQLite copies. Heavy extraction runs away from this worker. Local query copies are recoverable caches backed by verified persistent releases. These specifications do not establish sustained concurrent-user capacity. No resource names, endpoints, prices or private identifiers are shown.

18 · A map release must preserve contribution history

The immutable release holds prepared geographic facts and read models. Mutable human intelligence holds events, current heads and reviews. The write path first creates an event and conditionally advances a head. A stale version receives a conflict. The accepted history follows committed ancestry, so a failed competing event is not automatically an accepted edit. Releases and rollbacks must preserve that history. The same principle applies to map refreshes: current visible rows alone do not prove preservation of every committed head. Explain why this matters to the user: knowledge gained from a site visit should survive a technical improvement to the map.

19 · Rendering improved through repeated cycles

The early prototype needed bounded visible objects, clustering and coordinate reuse. The September traces distinguished acquisition, preparation, transfer and browser source updates from GPU paint. Changing UI heights had resized the map and produced old-bounds queries followed by repeat requests. Reserving layout space and synchronising final dimensions fixed that path. Skipping unchanged resize calls avoided renderer work. Short-lived viewport caches then reused complete contexts, with invalidation for access changes and edits. These were separate increments, not one renderer rewrite.

20 · Measure the visible result, keep the caveats

Thirty primed desktop filter changes produced 30 cache hits with zero new requests and zero aborts. Visible counts and source inputs matched. Coarse application completion was 90.5 ms median and 116.5 ms p95; this was not GPU paint latency. In the September 19 hosted comparison, n=30 per arm, Cape Town street viewport median improved from 771 to 529 ms, while overview median worsened from 1596 to 1687 ms. Sequential host and I/O variation prevents attributing every change to the code. The scope is a dated navigation experiment, not a universal responsiveness or multi-user capacity claim.

21 · A measured improvement in repeated queries

This is one dated production experiment, not a current whole-product benchmark. On 22 September we compared isolated execution with a resident SQLite worker. The median of the three repeated provider-count requests fell from 855 milliseconds to 276 milliseconds. The first request was essentially unchanged. Other shapes improved by much less, and some remained multi-second operations. All timed comparisons used verified local SQLite. The run checked result/evidence hashes and CSV parity. This small sample did not establish sustained capacity, twenty-user support or an application-wide speedup.

22 · Performance meant doing less repeated work

The recurring issue was avoidable work. Reopening connections, preparing national evidence for a local query and repeatedly acquiring unchanged data all mattered. We introduced bounded reuse and resident workers, and moved scope selection earlier where it preserved semantics. But early filtering is not correct if it changes what the user asked to count. A page is not a total, and a cache needs an invalidation boundary. We measured the path from user action through acquisition and preparation to SQL and rendering. A faster inner function cannot establish a faster or more capable whole application.

23 · AI in the product uses authorised evidence

Separate development AI from product AI. The product's shared chat interface receives the selected map context. Product-owned read tools retrieve bounded facts through the application boundary. Deterministic code supplies counts and comparisons. The model explains those facts, including gaps and partial results. Saved turns retain their original map context, so panning later does not redefine an earlier 'here'. A nonfinancial application has its own scope rather than a fabricated business unit. Reusing the engine also meant reusing the conversation UX and central operations.

24 · The first usable release required an owner decision

The late-September AI task illustrates the limits of an autonomous engineering process. There was substantial implementation and review effort, but the live feature was still pending. I had to bring the work back to the next useful increment, preserve the existing application's behaviour and separate optional improvements. The first native AI release reached production at the end of September under a bounded owner-approved launch policy. It kept authentication and data-integrity gates while retaining quality and capacity work as explicit follow-up. This is not advice to weaken safeguards. It is a reminder that product priorities require an accountable human decision.

25 · The accepted research slice

As of this source snapshot, the recorded product has a working atlas, dossiers, source-aware analytics, contributions and contextual AI over a nine-country catalogue. Coverage depth varies. The product ledger still tracks performance and concurrency acceptance, richer topology and source coverage, and the validation required for future opportunity models. After storage separation, data and ownership-publication maintenance are gated pending a reviewed procedure. Application and UI releases continue to be supported. The app is a discovery-and-capture alpha, not a live traffic-control system or a proof of traffic benefit. Evidence date: source master retrieved on 9 November, latest commit 7 November. Current state comes from capability and product ledgers, not old roadmap summaries.

26 · Original vision, accepted MVP

The original vision was a rich digital representation of intersections and the surrounding road network, with responsibility, equipment and planning context. The accepted research MVP was discovery and capture: navigate from a country to a junction or corridor, inspect source-aware evidence, preserve reviewed corrections and ask bounded contextual questions. Complete national coverage and a universal current twin were never established. Live control, calibrated traffic benefits and sustained multi-user capacity remain outside acceptance. The distinction matters because a persuasive map can make an unfinished model look authoritative.

27 · What I would repeat

These are the practices I would carry into another unfamiliar stack. Start with a concrete problem and move to representative real data early. Define the meaning of records, totals and missing values before the interface makes them look authoritative. Give independent reviewers bounded questions and ask what evidence would falsify the proposed design. Keep the implementation owner responsible for the tests. Finally, distinguish code completion, deployment and acceptance by a user. The AI agents made implementation accessible and productive, but they still needed the product context and constraints I brought. This is a personal reflection grounded in the development records, not a measured productivity comparison.

28 · Learning a new stack with AI

Return to the original ambition. I wanted a rich picture of a place and its surrounding network. I accepted a smaller research MVP: navigate, inspect evidence, preserve a reviewed correction and ask a bounded question in context. Complete coverage, rich topology, calibrated benefits and sustained capacity remain open. This is my personal learning experiment and internal research tool, not used commercially. Codex and Claude helped me learn the stack while I owned scope and acceptance. OSM licensing still applies to research use.

Further reading

My earlier post, Two AIs, One Branch, No Human Clipboard, explains the collaboration pattern. For the mapping building blocks, see the MapLibre GL JS documentation and the PMTiles guide.

I had never built a mapping app. Then I built Intersection Intelligence, with the help of AI agents (not vibe coding).

November 2026 · South Africa · By Mo Khan

Conceptual aerial artwork of an intersection becoming a digital map.

Cover illustration generated with AI. 

I had never built a mapping application before Intersection Intelligence.

I knew the problem I wanted to solve. I could look at a summary of infrastructure across a region, but I could not follow that summary down to a particular intersection and reliably answer a few ordinary questions. What is here? Who is responsible for it? What evidence supports that answer? When someone learns something new about the site, where does that knowledge go?

A spreadsheet could tell me a total. I wanted to be able to point at a place.

That gap became Intersection Intelligence: my personal map-learning experiment and an internal research application for exploring road intersections, understanding the evidence around them, and capturing knowledge that would otherwise remain scattered across documents and people's heads. It now brings together an interactive atlas, site dossiers, corridor views, analytical comparisons, reviewed contributions and contextual AI chat.

It is not being used commercially. I am sharing it as an account of learning a new technical domain, rather than announcing a commercial offering.

I built it with Codex and Claude. I brought the product intent, the domain questions and the decisions about what mattered. The agents helped me work through an unfamiliar geospatial stack, implement it, challenge the design and test what we had actually delivered. This was a substantial extension of my existing software and systems experience into a domain I had not built in before.

Looking back through the git history, the interesting story is how often the meaning of a seemingly simple feature changed once we tried to make it trustworthy.

The experiment started with a question about a place

The early vision was much bigger than a map with coloured pins. I wanted a useful digital representation of infrastructure: the road context around a junction, the equipment recorded there, the organisations with different responsibilities, and the evidence we still needed to gather.

The longer-term ambition includes richer network analysis and better support for planning. The delivered research application is a discovery-and-capture alpha. It does not operate traffic signals, ingest a universal live traffic feed, or demonstrate that a proposed intervention would improve traffic flow. Those capabilities would require additional data and validation. Keeping that boundary visible has become part of the application's design.

The workflow I kept coming back to was straightforward. Start with a country or area. Find a junction or corridor. Open its dossier. Understand what the sources say. Add or review evidence. Compare the resulting picture with another area, or ask a question in the context already on screen.

That last part matters. A useful map should help someone carry a question through the application without continually restating where they are looking.

South Africa in the atlas, with sensitive UI regions redacted.

The journey starts at country scale. These South African screenshots were captured from the signed-in research app on 14 November 2026 in the OSM junction view. Company and provider names, commercial summaries and installation counts are obscured. Sensitive UI is covered with deterministic pixel masks; the original map pixels, labels and geometry are unchanged. Map data © OpenStreetMap contributors; Protomaps cartography. Published administrative boundaries are reference geography, not cadastral boundaries.

Late August: a prototype became a real geography problem

The design documents had been evolving for months before the first standalone implementation landed on 30 August. That first slice used a deterministic demonstration dataset. It let us exercise the service, authentication, edits and interface without pretending we had already solved national geography processing.

The next step exposed the difference immediately. On 31 August we began proving the application against real OpenStreetMap data in Gauteng. The early browser implementation went through a succession of rendering repairs: limiting visible objects, clustering points, avoiding repeated coordinate work and making the dossier respect the selected scope.

The git log is wonderfully unromantic about this. There are commits for getting the map to initialise, making clusters usable and ensuring the first preview actually paints. These are the details that disappear in a polished demo, but they determine whether someone can use the thing.

By 1 September, the atlas had moved to MapLibre GL JS and a self-hosted PMTiles basemap. MapLibre supplies the browser renderer. PMTiles packages tiled geography into an archive whose readers can fetch the relevant byte ranges on demand. That helped us serve the map without transferring an entire country's geography to the browser. MapLibre documentation and the PMTiles format guide explain those building blocks.

I was learning the difference between the basemap and the application's own evidence. The streets and labels provide geographic context. The records layered over them have their own identities, source dates and access rules. A fast basemap cannot compensate for a slow dossier endpoint or a misleading count.

The atlas focused on Gauteng, with sensitive UI regions redacted.

Country to province: the same interface now shows Gauteng. Zoom changes the amount and form of visible geographic evidence. Provider names and commercial values are redacted.

The Johannesburg city view in the research atlas.

Province to city: Johannesburg becomes the working area. The shortlist and map must agree about the selected scope. Map data © OpenStreetMap contributors; Protomaps cartography.

A Johannesburg corridor in the atlas, with sensitive UI regions redacted.

City to corridor: selecting a named route makes a connected stretch of road the focus. This is a view of published route geography, not a simulation of traffic flow. Map data © OpenStreetMap contributors; Protomaps cartography.

We also learned early that building geography and serving geography were different workloads. The first real extraction experiments exceeded the small application's memory budget. The architecture changed accordingly: prepare and validate the heavy datasets away from the serving worker, then publish versioned artifacts for the application to read.

That was one of the first important decisions AI helped me reach. It came from running the work and measuring it, rather than assuming the initial hosting plan would cope.

The hardest question was what one dot meant

Consider an illustrative junction. OpenStreetMap may describe several signal nodes and the roads around them. An authority register may describe an equipment installation. A colleague may have a recent observation about the same location. A regional spreadsheet may contain an aggregate count that includes it.

Those sources describe different things. Adding their rows together would produce a confident-looking number with no coherent meaning.

The application therefore keeps several kinds of evidence distinct:

Evidence What it can establish What it cannot establish by itself
OpenStreetMap geography Mapped locations, road structure and recorded tags Complete equipment ownership or current operational status
Authority records What a particular source reports about an asset A certain match to a mapped junction when the link is ambiguous
Human contributions An attributable observation or reviewed correction An automatic rewrite of the original source
Regional summaries Counts at their stated area and reporting date Coordinates for individual sites

Four evidence types retain their identities before the application resolves a view.

The public explanation of the evidence model. Examples are generic. No commercial populations or installation records are shown.

We made a particularly important rule explicit: an aggregate count must never become a collection of invented map points. If a source reports more assets than we can position, that difference remains a coverage gap.

We also separated unknown from zero. An empty field does not prove that a piece of equipment is absent. A source that has not been loaded does not establish that an area has no assets. An automated proximity match is a proposal until there is sufficient evidence to accept it.

This is where the software became much more interesting to me. The map made disagreements visible. The real product work was deciding how to preserve those disagreements without making the application incomprehensible.

Onboarding a dataset was its own engineering workflow

The phrase “import the data” concealed a surprising amount of work. Every provider or authority brought its own conventions: workbook layouts, local asset identifiers, street abbreviations, road-pair descriptions, direction suffixes, coordinates of varying quality and missing fields. A parser that worked for one register could confidently misread the next.

We built source-specific importers first, then shared the mechanics that actually proved common. Each adapter preserved the source identity, original values, reporting context and reconciliation status. Re-importing a source needed to replace that source's snapshot predictably, without erasing unrelated contributions or making duplicate assets.

Matching was a separate stage. We normalised road names and aliases, compared road pairs, used fuzzy name matching and geographic evidence, and ranked possible links to OSM junctions. Several source assets could legitimately occupy one physical location. An unmatched row still belonged in the inventory; lack of a reliable position was a fact to retain, not a reason to discard it.

The difficult cases brought us back to the browser. We inspected the application and used Google Maps during the documented location-review workflow to check confusing road names and possible junctions. That helped identify candidates, but a plausible visual match did not establish equipment ownership or turn an agent-assisted review into human confirmation. Google Maps was a review aid, not a source we could freely copy into OSM or redistribute. Third-party sources retain their own terms, and OSM explicitly warns against adding copyrighted source data without permission. OSM source and licensing guidance.

Dataset onboarding from source-specific import through matching, review and publication.

The workflow preserves uncertain and unpositioned records. A candidate link and an accepted relationship are different states.

In one September review, Claude challenged the distinction between an OSM-derived location hint and a canonical junction identity. During browser verification, Codex also found that the UI could expose an old fuzzy candidate after the assisted workflow had selected a different target. Codex corrected the data and consumer boundaries, and verified the resulting dossiers in the browser. This was exactly where the collaboration earned its place: the importer could look correct in isolation while the person clicking the map still received the wrong story.

Making the map feel usable took several tuning cycles

My early mental model was that a slow map meant too many things were being drawn. Sometimes that was true. The first prototype needed bounded visible objects, clustering, coordinate reuse and repairs to its initial paint. Moving to MapLibre and PMTiles gave us a better basis for exploring real geography, but did not finish the performance work.

By September I was judging the experience with a mouse: drag, zoom, change a filter, revisit an area, and inspect a junction without losing my place. We profiled each stage rather than calling every delay “rendering”. Request acquisition, evidence preparation, network transfer, browser source updates and the renderer's worker/GPU work are different costs.

One tuning cycle found that changing labels and panels changed the map's dimensions. The client could query old bounds, then resize and request again. We reserved space for changing content, synchronised the final camera dimensions before querying and skipped unchanged resize calls. The initial city focus then used the intended discovery request followed by the final viewport request.

Another cycle found that filter actions were clearing useful in-memory viewport results. We preserved short-lived entries under complete context keys, while retaining expiry and invalidation for edits and access changes. In the recorded desktop browser run, 30 primed filter changes produced 30 cache hits, zero new requests and zero aborts, with exact visible-count and source-input parity. Median application completion was 90.5 ms, with 116.5 ms at p95. That timing used coarse polling; it was not a GPU paint measurement or a universal sub-100 ms guarantee.

The server still mattered on a cache miss. Spatial pruning and moving selection ahead of expensive enrichment reduced work without changing the answer. In the 19 September hosted comparison, the street-scale Cape Town viewport median fell from 771 ms to 529 ms across 30 requests per arm. The overview median went the other way, from 1,596 ms to 1,687 ms. Those sequential observations include host and I/O variation, so I cannot attribute every millisecond to one code change or claim that every navigation became faster.

The lesson was cumulative: reduce unnecessary drawing, stop accidental repeat requests, reuse only the right fresh evidence, then improve the miss path. Each cycle had to preserve the junction identities, filters, visible counts, dossiers and exports. A quick map showing stale or incomplete evidence would have failed the experiment.

September: preserving what people had learned

Once colleagues could contribute information, a data refresh became a more serious event.

OpenStreetMap changes. Grouping logic improves. A set of source nodes that once represented one candidate may later split or join another group. If the next build simply minted new identities, human observations could become detached from the places they described.

Stable identity and reconciliation became central to the system. We retained source identities, tracked aliases and treated ambiguous changes conservatively. The September junction-accuracy work repeatedly tested whether refreshed geography preserved the facts and review decisions already accepted by people.

One correction in that history is especially revealing: preservation had to include deleted record heads as well as the active records returned by the API. Looking only at what was currently visible would miss part of the committed history.

The write model follows the same principle. A contribution creates an event, and a conditional update advances the record's current head. If two users edit the same version, the stale write conflicts instead of silently overwriting the newer one. The accepted history follows the committed ancestry. A failed competing write does not become an accepted edit just because an event file exists.

Immutable geographic releases and mutable human evidence therefore live behind separate storage boundaries. Publishing a new map, or rolling back an application release, must preserve contribution history.

I had started with the desire to click on an intersection. I was now thinking about identity, concurrency and the durability of institutional knowledge.

The software architecture we arrived at

Intersection Intelligence lives in the same repository as the wider platform, but runs as a standalone application. It reuses the platform's identity and AI capabilities without absorbing the financial application's runtime or its data model.

Software architecture showing the atlas, product API, evidence stores and shared AI platform.

Software architecture, simplified for public discussion. Arrows show the main request and evidence paths. Product-specific datasets and deployment identifiers are omitted.

The map interface is a lean JavaScript application using MapLibre and PMTiles. It requests bounded viewports and opens detailed evidence when needed. The shared Signal AI chat client brings its React-based conversation interface into that native map host.

A Node.js/Fastify service owns the product API. Shared domain code supplies the geographic and evidence semantics used across views. Prepared files support map and dossier reads. Read-only SQLite query workers support analytical requests. A separate mutable repository stores contributions, review state and their history.

The offline preparation pipeline uses Python geospatial tooling to work through OpenStreetMap extracts and other approved sources. It builds the territorial hierarchy, junction and corridor artifacts, indexes and query stores. Source-specific adapters retain provenance rather than assuming every authority's spreadsheet is the same format.

Technology stack from browser renderer to offline processing and hosting.

The concrete technology stack, separate from the request-flow architecture above. Python prepares geography; JavaScript serves and presents it. Shared AI and identity remain explicit integrations.

The fact that the software sits in one repository does not remove these boundaries. The deployed package must contain what its runtime imports. A test passing because a sibling package happens to exist in the checkout is insufficient evidence that the isolated service will start.

This architecture also gave us a useful way to expand. The country catalogue describes administrative structures instead of hardcoding every place into a South African province-and-municipality hierarchy. By late October, the published catalogue covered nine countries across southern Africa and the United Kingdom. That is geographic breadth, with different levels of source coverage in different places. It is not a claim of complete infrastructure knowledge in any of them.

Deployment became part of the design

There are two distinct release activities. An application change packages and deploys code. A geography change prepares, validates and promotes a data release. The second is an explicit operator workflow, separate from ordinary application CI.

Deployment architecture showing offline preparation, code deployment, authenticated application hosting and separated storage.

Deployment architecture as documented in early November 2026. Cloud resource names, hostnames, network ranges, account identifiers, commercial data and operational credentials are intentionally omitted.

The research application runs on one Azure App Service Linux B2 instance, with two cores and 3.5 GB RAM, the built-in Node.js 24 LTS runtime and Always On. Microsoft's published B2 plan lists 10 GB of plan storage; that quota is distinct from the worker's temporary local disk, whose actual headroom we measured for query copies. These are configuration details, not a pricing or capacity promise. Azure App Service Linux plan specifications.

That modest memory envelope shaped the design. Heavy extract processing runs elsewhere. The serving process reads prepared artifacts, bounds query work and keeps recoverable SQLite copies on local disk. Local disk can disappear when a worker is replaced, so verified persistent releases remain the source of recovery. The host is protected by Microsoft Entra authentication. It calls the existing platform API for entitlements and the shared AI service for AI execution. Prepared immutable application datasets reside on persistent application storage, with verified local SQLite copies for ordinary analytical execution.

Basemap archives now occupy a separate storage account. Authorised browsers receive short-lived, read-only signed access to geographic tile ranges. Restricted source evidence remains behind the application, and its storage network access is restricted to the application's integration subnet. Ownership overlays follow the authenticated application path rather than being treated as ordinary basemap content.

The network design uses a storage service endpoint and a restricted public endpoint. It does not use Private Link. That distinction matters when explaining what we built, even in a diagram with all the resource names removed.

Releases also became more selective. A UI-only change should not copy the map dataset or restart the analytical worker. An application release should retain the active data root. A data release needs a validated candidate and a verified recovery path. The release procedure checks what is actually running, rather than treating a successful upload as proof of completion.

There are still operational limits. After the November storage separation, data-release and ownership-publication maintenance remain gated pending the reviewed access procedure. Ordinary application and UI releases remain supported. That is an open engineering task, not something I want a simplified architecture picture to conceal.

The next performance cycle moved into analytical queries

The map also had to coexist with analytical requests. Improving browser navigation would achieve little if a large query repeatedly reopened stores or prepared the same evidence on that small worker.

We profiled the path from the user's action through acquisition, preparation, query execution and rendering. The work led to bounded viewport caches, reuse of freshly validated evidence, local query stores and supervised resident SQLite workers. We moved filtering ahead of expensive enrichment where the semantics allowed it.

The important qualification is “where the semantics allowed it.” Filtering early is only an improvement if it preserves the population the user asked about. Pagination must not turn a partial page into an apparent total. Caching must retain freshness and invalidate when relevant evidence changes.

One small production comparison captures both the value and the limits. In the 22 September resident-worker experiment, the median of three repeated provider-count requests fell from 855 ms to 276 ms. The first request was essentially unchanged, and other query shapes improved by much less. The same comparison checked result equality and export parity.

That is a useful improvement in a measured path. It does not establish a threefold improvement in the whole application, or prove sustained multi-user capacity. The project records keep those acceptance questions open.

I have learned to be suspicious of performance stories that only show the fastest inner function. What matters is whether the person using the map gets the same answer, with the right evidence, sooner.

AI helped build the experiment, then became part of it

These are two different roles for AI in this story.

During development, Codex did sustained implementation and verification work. Claude served as a peer architect and independent reviewer in recorded collaboration sessions. Inside the application, Signal AI answers questions through authorised, read-only tools over the application's evidence.

I had already written about the development arrangement in Two AIs, One Branch, No Human Clipboard. For Intersection Intelligence, the same working pattern became a way to explore a new technical domain while retaining a clear owner for the implementation.

The owner sets intent, Codex implements and verifies, and Claude reviews the evidence.

The collaboration pattern used in the recorded development sessions. Roles varied in separate workstreams; this does not imply that every commit received independent review.

Codex and Claude could examine the same repository and discuss bounded decisions without me copying every response between applications. The durable record was a curated engineering ledger: the question, the evidence, the finding, the correction and the remaining decision.

The independent review was useful because it sometimes changed the design. The early reconciliation explicitly retired obsolete assumptions about the map renderer and running heavy extraction on the serving host. Later AI-platform reviews challenged financial-context assumptions, conversation handling and the boundary between a client timeout and work that was still consuming server resources.

Agreement between two models was never sufficient proof. Codex still had to run the checks and produce execution evidence. A source review did not magically become a browser test, and an approved design did not mean the feature was deployed.

I also had to manage the process. The late-September AI integration grew into a long refactor and review exercise. I pushed for a first usable release, separated unrelated boundary work from that delivery, and eventually asked Codex to continue without the reviewer. The history preserves that change of responsibility. It would be misleading to describe the subsequent work as independently approved by Claude.

The agents made unfamiliar implementation work much more accessible to me. They did not remove the need to set scope, challenge complexity or decide when the next useful increment should reach a user.

Giving the map a conversational interface

The existing platform already had Signal AI. Bringing it into Intersection Intelligence prompted a broader question: which parts should every future application be able to reuse?

The answer included both the engine and the interface. We separated the shared conversation client, authenticated execution and central operations from product-owned tools and context. Intersection Intelligence owns the meaning of its geography and evidence. The financial application retains the meaning of its financial tools.

A selected map area cannot be represented by a made-up business unit just to satisfy an existing financial gate. Likewise, “here” in a saved conversation must retain the area that was selected for that turn. Panning the map later must not silently change what an earlier question meant.

The model receives bounded evidence through the product's read tools. Counts and calculated comparisons come from deterministic code. The answer must distinguish complete results, partial pages, unknown fields and unavailable evidence. The model helps explain those facts in conversation.

This is also why I care about reusable chat UX. People should not have to learn a different conversation panel for every application. The map keeps its own navigation and space, while the shared client supplies familiar conversation controls and rich answer rendering.

The first native AI release reached production at the end of September under an explicitly bounded launch decision. Answer quality, latency and capacity continued to need work. Reuse gave us a foundation for improving that experience across applications.

What I would carry into the next unfamiliar stack

I would start again with a concrete user question and a narrow, real-data slice. The demonstration dataset helped us prove interfaces. Real geography exposed the work those interfaces actually needed to do.

I would establish evidence and identity rules earlier than feels necessary. A map is persuasive. That makes it especially important to explain what its colours, counts and points mean before people depend on them.

I would keep the build pipeline separate from the serving path, and measure the actual user journey early. I would also keep the distinction between implementation, deployment and acceptance visible throughout the project. Those are different achievements.

And I would keep using AI as an engineering collaborator. The productive loop was specific: explain the intent, inspect the existing system, propose a bounded change, implement it, challenge it with evidence, and check the result a user would actually receive. Long conversations alone were never the measure of progress.

Today I can navigate to a place, inspect its evidence, preserve an attributable correction and ask questions in context. There is still more to do on coverage, richer topology, performance and validation of future analytical capabilities.

I began without experience building mapping applications. I now have a working geospatial research application and a much better understanding of the questions underneath one. The most valuable change is that I can look at an unfamiliar stack and begin with a useful problem, knowing that I can work through the engineering with AI while remaining responsible for the result.

Returning to the vision: what counted as an acceptable MVP?

The original vision imagined a rich digital representation of intersections and their wider road network. It included questions about responsibility, equipment, corridors and future planning. The requirements were useful as a direction, but building every ambition before trying the map would have postponed the learning I wanted.

The acceptable MVP became a discovery-and-capture research alpha: follow a place from the map to its evidence, preserve a reviewed correction, and ask a bounded question in context. Here is how I now compare the vision with the delivered slice.

Original requirement or ambition Accepted research MVP Still outside that acceptance
Navigate from a country to a useful local place Country, area, junction and published corridor navigation Complete, equally detailed coverage everywhere
Bring disparate source records together Source-specific imports, provenance and explicit candidate links Automatic certainty for ambiguous matches
Build a durable shared picture Dossiers, attributable contributions, review and preserved identities A complete, continuously current national twin
Ask questions about what is on screen Contextual AI using authorised read tools and deterministic facts Unbounded answers or predictions unsupported by data
Make exploration usable on modest hosting Measured navigation and query improvements on one B2 host A universal latency target or sustained multi-user capacity claim
Explore network and planning possibilities Published corridors and evidence for further research Live control, calibrated traffic simulation or proven intervention benefits

That was a deliberate stopping point for a useful first experiment, not a claim that the entire vision had shipped. Coverage, richer topology, AI quality, sustained capacity and the gated data-maintenance procedure remain work to do.

For me, the result is already meaningful. I can move from a country map to a road, inspect why a record is there, and see where the evidence runs out. I learned how the stack fits together by building it with Codex and Claude, testing the awkward cases and making the acceptance decisions myself. The project remains my personal learning experiment and an internal research tool, with no commercial use.

Technology and further reading

The stack combines MapLibre GL JS, PMTiles and Protomaps, OpenStreetMap extracts, Python geospatial preparation, Node.js/Fastify, SQLite, Azure App Service and Blob Storage, Microsoft Entra authentication, and the shared Signal AI platform.

OpenStreetMap is a community-built data source available under the ODbL, including for commercial use subject to its terms. Calling this project research does not remove attribution, licence or applicable share-alike obligations. Data licensing and access to OSM-operated tile services are separate matters; this application uses prepared PMTiles basemaps. See OpenStreetMap attribution and licensing and the OSM tile service policy.

Sunday, 18 October 2026

Forecasting my business with Codex changed how I think about hiring data scientists

A business owner's account of using AI agents to investigate real sales history, test machine-learning models and decide what was useful enough to build into BizOps (Authored with Codex).

I wanted my business application to help me look forward.

BizOps could already show me what had happened. We had financial plans, sales history, product information and operational reporting. We had an AI chat interface. My next question seemed reasonable: could we use that history to learn how customers behave, forecast the next financial year and let me test different futures before discussing them with the CEO and CFO?

I wanted to know which customers were likely to buy again, which were declining, what they might buy and where our next growth opportunity could come from. I also wanted to move sliders and explore a major customer cutting expenditure, a contract ending or a region recovering. Eventually, I wanted to understand the conditions under which the business would become vulnerable.

That became a substantial research exercise with Codex. It also changed how I think about commissioning data science. The agent could inspect a codebase, research methods, write the data adapters, train models, build benchmarks, investigate failures and produce working local reports. My contribution was to keep bringing the work back to the business question, challenge conclusions and supply better source data.

The journey included premature recommendations, disappointing models and moments when I had to tell the agent to stop and rethink. Those parts explain the result as much as the models we eventually kept.

Our business operates in traffic infrastructure and equipment, with substantial public-sector and municipal exposure. Purchasing is shaped by projects, tenders, maintenance programmes, budgets and the timing of expenditure. Customers can buy in bursts and then go quiet. Some return after long gaps. Product demand reflects installed equipment and replacement cycles as well as new projects. The financial-year push matters.

A quiet period has several possible meanings. A customer may have enough stock, be waiting for funding, have delayed a project or have moved to another supplier. Sales history can reveal patterns associated with those possibilities. It cannot directly tell us which explanation is true. A billing record also tells us something different from an order, a tender award or a signed renewal.

We considered a wider set of inputs: tender history, contracts, production metrics, engineering projects and public evidence such as Auditor-General findings on municipal financial health. Those could become useful context. I wanted to prove what our sales history could tell us first. Current contract knowledge could not be slipped into an old forecast as though the model had inferred it. The completed experiments used sales-derived history; the wider market-research layer remains future work.

That domain context mattered throughout. A method that performs well on frequent supermarket purchases does not automatically become the best method for irregular municipal equipment sales. Much of my role was explaining why a technically plausible answer still needed another look.

Codex initially suggested CatBoost. I challenged it: how had it arrived at that recommendation, and had it actually researched suitable methods for this problem? CatBoost's handling of categorical information made it a plausible candidate. That was a starting hypothesis, with much more work needed before calling it the right choice. The CatBoost paper explains the algorithm's techniques; it does not settle a forecasting decision for my business.

I asked Codex to consult Claude through our collaboration skill. Codex led the investigation and implementation; Claude provided independent, read-only challenges to the design and source code. Their exchanges addressed the target, credits, customer and product granularity, historical availability of inputs and validation. Agreement between agents was a design checkpoint. Actual predictions still had to face actual outcomes.

The recommendation changed to a strongly regularised, pooled LightGBM starting model. “Pooled” meant learning across customer histories instead of building a separate complex model for each sparse customer. The plan used past sales, recency and calendar information to predict future monthly signed revenue, including credits. The LightGBM research, forecasting literature and published business benchmarks provided reasons to try that family. None established a universal winner for municipal sales.

CatBoost remained an early recommendation. LightGBM was the boosted implementation we actually tested. That distinction is easy to lose when a long conversation gets compressed into “we tried AI.”

We began with the data already available through BizOps and its local Cosmos environment. It covered the recent financial years and connected naturally to the application. I expected Codex to use its existing knowledge of the platform and inspect the documentation and data rather than ask me to describe everything again. That expectation was reasonable: access to the repository should make an agent a better collaborator.

The first models did not deliver the customer spending forecasts I wanted. We revised features, regularisation and the way annual and monthly targets were represented. Some experiments searched a small, declared set of settings on earlier development years; later ones tested a fixed recipe. Throughout, simple historical references remained in the comparison. Repeating last year's pattern or using a recent run rate is a meaningful competitor when a learned model is supposed to improve a business decision.

We then investigated Chronos-2, a pretrained time-series model. A useful way to think about it is that the model arrives with patterns learned from other time series and conditions its predictions on the history supplied to it. Our initial zero-shot experiments used those pretrained weights locally; they did not train a new foundation model from scratch. The checkpoint itself postdated the historical forecast origins, so these were retrospective method comparisons, not a reconstruction of a model available then.

There was also a separate, genuine local fine-tuning experiment using LoRA adapters. These train a relatively small set of additional parameters while retaining the pretrained base. Adaptation improved the matched model's starting performance in several comparisons. It still failed our business forecasting requirements. Lower training loss and improvement over an earlier neural result were insufficient reasons to replace a stronger simple reference.

Other experiments explored seasonal smoothing, methods for intermittent demand, annual tree models, quantile regression and separate activity-and-amount models. Those represented different hypotheses: perhaps we were asking a monthly model to solve an annual spending problem; perhaps the occurrence of a purchase and its size needed separate treatment; perhaps credits required an explicit component. The experiments gave us evidence about those questions, including evidence that several plausible approaches were inadequate.

At one point I became frustrated. I could see work continuing, but I could not see clearly enough what each attempt was testing or whether we were getting closer. I asked whether we were still searching for perfection. An agent can execute experiments quickly and still leave its owner feeling stuck in a loop. Progress updates need to explain the hypothesis, the result and the decision that follows. A stream of completed scripts is a poor substitute.

I also challenged the data conversation. When the limitations of the application extract became apparent, why had we not moved sooner to raw exports? When recommendations started expanding towards more detailed invoice information, I pushed back. We needed to work with the data I could realistically provide and establish what it could support.

I supplied an SAP Excel export covering 2015–2026. That changed the practical approach. We were experimenting on my workstation; Cosmos did not need to be an intermediary. Codex could preserve the workbook and build a direct, versioned adapter for research.

We tested the same boosted and Chronos recipes on the shorter recent history and the longer Excel history, comparing FY24, FY25 and FY26 against common actual outcomes and common references. More history produced mixed changes. It revealed additional purchasing behaviour, but did not consistently repair the customer, product and business forecasts. We had to keep distinguishing “more observations” from “a better prediction problem.”

The more consequential change was how we divided the problem. I asked whether different models could serve different expectations. Customer behaviour, product propensity and annual revenue had been bundled into an ambition that sounded simple in a product mockup and was much harder to validate.

Around that point, I asked Codex to find and use a data-science skill. We installed the community data-scientist skill from magnus919's agent-skills repository, pinned to a reviewed version. It provides methodological instructions: formulate the question, examine assumptions, choose a defensible method, evaluate it and make the result reproducible. Loading it does not change the agent's model weights or confer a professional qualification.

The skill reinforced a useful change in emphasis. We already had chronological tests and frozen specifications. We now made the distinction between describing history, predicting another billing event and forecasting money much more explicit. The improvement I would credit to this stage was a clearer research direction. It came from the skill, my steering and the accumulated evidence together; we did not isolate a causal accuracy gain from installing a file.

Even the skill needed scrutiny. Generic examples using shuffled validation folds were unsuitable for our time-dependent histories. Illustrative code and hardware heuristics needed checking against the actual runtime. A specialist skill is a resource an agent must apply thoughtfully, with the business's data and engineering rules taking precedence.

The first model we banked answered a narrower question: for a known customer, how likely is another positive-billing month within the next three, six or twelve months?

It uses a regularised logistic model in a discrete-time survival framework. At each historical monthly starting point, the model learns the conditional likelihood of the next billing event from activity features such as recency, frequency, gaps and seasonality. It pools information across customers, and combines the monthly conditional predictions into coherent probabilities over the supported windows. It learns coefficients from observed outcomes; the resulting predictions go beyond a hand-written “recent customers are good” rule.

Defining the event carefully was essential. Multiple invoice lines did not become multiple independent orders. A month with a positive billing line could still count as activity when a credit offset its net revenue. A credit-only month did not count as a positive purchase event. Follow-up stopped at the available training cutoff, so an unknown future was never labelled as inactivity. That gave us a behaviour model whose target we could explain and test.

The second banked model applied the same general approach to customer and broad product-category combinations. It incorporated customer activity, category history, changing mix, seasonality and related-category information. It scored combinations a customer had bought before as well as eligible combinations they had never bought. We used governed material-to-category mappings from the platform, with unresolved history explicitly unclassified, because the business discussion belonged at portfolio level rather than individual material codes.

This model supports questions about likely category activity and potential sales priorities. Several categories can be relevant to one customer. Their probabilities overlap; they are not shares that add up to a spending budget. A promising buying propensity also does not tell us the quantity, invoice value or profitability of a future purchase.

Both propensity models produced encouraging retrospective evidence against their declared simple references. They also retained weaknesses around sparse histories, quiet customers and raw probability reliability. Separate conditional-spending experiments reinforced the boundary: attaching an amount model to a useful buying probability still failed the monetary requirements. Better estimates of who might buy did not solve how much they would spend.

Later I found a much larger SAP billing export extending back to 2003. My reaction was understandable: surely more than two decades of history would give the models enough to learn from. It made a much richer experiment possible. It also brought older account codes, changing product descriptions, different report columns and historical regimes into view.

The adapters had real work to do. One workbook contained duplicate-looking quantity headings with different meanings. The application's financial-year convention needed to be derived consistently from dates. The larger export was a different SAP report, so copying column assumptions from the previous adapter would have misinterpreted fields. Credits, cancellations and unresolved materials had to survive the transformation. Four implausibly future-dated lines were preserved and quarantined when the available evidence could not justify a replacement date.

We checked overlap between exports instead of assuming a longer file was simply the earlier one with extra years attached. Revenue and source cost required separate reconciliation. We also kept current catalogue classifications distinct from historical knowledge: mapping old materials into today's reporting categories helps a business owner understand the portfolio, but does not recreate the exact catalogue that existed at every old forecast date.

Customer identity raised another limit. An SAP account code is an observed account, with uncertain continuity to other historical codes. Our limited current customer master could help corroborate a mapping; it could not become the definitive truth about decades of accounts. I eventually paused that reconciliation. Useful research could continue with the remaining identity limits visible.

The third component was an annual business-revenue model: a small, regularised vector autoregression, or VAR. It learns relationships among the past seasonal changes of related category revenue and source-cost series. In practical terms, it estimates a matrix of lag relationships, applies a constraint to limit unstable propagation and forecasts monthly values whose sum can be evaluated as annual business revenue.

That is learned statistical modelling. A neural network is one possible form of machine learning, and a fitted logistic or autoregressive model is another way to learn predictive relationships. The useful distinction for us was whether the chosen method improved the task it was intended to perform. The VAR earned a place as an annual business-total research baseline. Its success did not carry over automatically to customer spending, category amounts, management gross profit or cash. The current FY27 monetary outputs remain research diagnostics, with planning readiness still unestablished.

We also reran the boosted and zero-shot Chronos recipes on the new billing export, comparing history from 2003 with history from 2015 on the same source and target years. The already evaluated VAR supplied a fixed comparison without unnecessary refitting. The arms shared observed target outcomes, accounting rules and references, while retaining their own available histories and known accounts. Longer history could reveal additional accounts; that difference stayed visible. The exercise preserved the useful annual-total result and the much weaker evidence for detailed customer and product amounts.

Full-history propensity refits and chronological calibration had their own experiment records. Calibration adjusts the probability scale rather than teaching a new ordering of customers. It helped some task and horizon comparisons and weakened others. An improvement in one window did not justify silently replacing every banked probability.

“Banked” became an important word. We preserved a specific recipe, data identity, fitted states, model card and evidence for reuse. We had three accepted research components, each with a defined task. Production qualification and future reliability still required work. The original propensity banks retained their 2015–2026 source identities; later full-history refits were separate experiments. The revenue bank used the extended history. Discovering a larger dataset did not silently rewrite the earlier records.

Training meant writing and running a local data pipeline. Codex transformed preserved source rows into consistent, dated monthly histories in Python. Features stopped at each event-date cutoff within the accepted, as-restated source. That preserved local training chronology without pretending to reconstruct every historical revision. Scalers were fitted within the permitted history. Known missing periods, observed inactivity and unobserved prehistory had different meanings. Signed financial values were retained and reconciled using exact cents at the reporting boundary.

For the historical tests, we moved the clock back. To forecast an old financial year, local fitting saw only its allowed earlier sales-derived data. We repeated that across forecast origins and compared the results at the customer, product and whole-business levels relevant to each experiment. Rolling-origin evaluation explains this approach. We also acknowledged that historical years already examined during the research were reused retrospective evidence, even when a particular run kept their outcomes outside fitting.

The metrics followed the question. Money forecasts needed annual errors, signed bias and comparisons that exposed cancellation between individual misses. Behaviour models needed probability error, calibration and useful ranking against simple activity references. In a sparse dataset, an impressive overall classification accuracy can reward predicting no activity for nearly everyone. I wanted a useful customer list and understandable probabilities, which required more specific tests.

Engineering verification had a separate purpose. Tests checked source transformations, dates, credits, rounding and incomplete months. Saved-state replays checked whether retained models reproduced their outputs. Independent audits reconstructed calculations, and local browser reports let me inspect the actual results. Passing those checks established that the machinery worked as recorded. Forecast usefulness still depended on the comparisons with real outcomes. We had several experiments that executed correctly and failed predictively.

All this changed the product strategy. My early mockups imagined a richer monetary forecast than the models had earned. I wanted independent customer and product sliders. We eventually agreed to build the first increment around the demonstrated capabilities: a business outlook, customer buying likelihood and category intelligence, with historical context and budget comparisons where an eligible forecast and a comparable plan exist.

Management assumptions would have their own clear place. A decision to explore lower customer spending is an assumption I supply, with its arithmetic consequences visible. It is not proof that a propensity model has learned the causal effect of losing a tender. Profit, runway and workforce scenarios also need expenses, working capital, cash and cost-response assumptions. Sales history alone cannot answer when a business will run out of cash.

I then asked for a strategic view through FY32. We ran a bounded longer-horizon VAR experiment on the full source, comparing one- through six-year annual and cumulative errors with simple baselines. For uncertainty, we generated joint residual paths, advanced each path through the model and summed its monthly values before calculating annual ranges. The time-series bootstrap literature informed that design; historical coverage determined how much trust the ranges deserved.

The short-horizon advantage was modest, and it did not persist at the longer leads. The raw simulation ranges also contained historical outcomes less often than their nominal labels suggested. We retained the original annual bank and treated the continuation as research evidence. That result gave us a clear product decision: build strategic growth, downturn and recovery controls as explicit management scenarios, with model projections available only at their demonstrated level of support. Simulated futures add possible paths, never additional observed sales history.

The local pipeline now exists. It preserves source identities, builds research datasets, fits and evaluates models, retains protected results and produces inspectable reports. Derived research datasets, fitted states and predictions remain in encrypted local artifacts outside Git. Original SAP workbooks are preserved separately and are not committed to source control; those original files are not covered by the research-artifact encryption. Source code, specifications, tests and technical identities are versioned so a later result can be traced and reproduced.

The production design keeps that computation separate from everyday use. Training and batch inference can run locally. An approved publication process would create portable, immutable result releases in private storage. The existing authorised BizOps API would serve them to the page and to Signal AI. The application would not need my workstation online for every question, and moving a scenario slider would not start a training job or ask a language model to perform the arithmetic. Publication, rollback, refresh and production acceptance are still work to build.

The chat experience will use those same versioned results through bounded tools. It should be able to explain the model's question, the history behind a profile, the forecast origin, the scenario assumptions and the relevant limitations. Its fluency must not turn a purchase probability into an invented revenue forecast. As new data arrive, we can produce new versions and roll the planning window forward while retaining the predictions issued earlier.

That architecture also fits the economics of this experiment. We did not begin by provisioning a permanent ML server or a GPU cluster. A substantial longer-horizon evaluation ran in minutes on my workstation with a few gigabytes of memory. Some earlier adaptation and replay runs took much longer. Execution time for one script is only one part of the journey; investigation, corrections, interpretation and owner review took place across days.

Which brings me back to the hiring question.

I built a business prediction engine without hiring a data scientist

The research and engineering Codex performed here is work I would ordinarily budget several days for a human specialist, and potentially a couple of weeks once data investigation, competing approaches, validation, reporting and architecture are included. That is my estimate of the assignment, rather than a measured comparison between Codex and a particular data scientist. We did not run a head-to-head productivity study.

What I can say directly is that I commissioned a substantial data-science workflow through AI agents and obtained executable work, preserved evidence and decisions I could review. Codex did far more than suggest Python snippets. It did the implementation and much of the investigation, with Claude and additional agents providing bounded review. My existing platform, workstation, subscription and time supplied the environment. The business context and acceptance decisions remained my responsibility.

That changes the starting point for a manager considering a full-time data scientist. I would now want to test a bounded, consequential business problem with an agent, a knowledgeable owner and clear evaluation criteria before assuming a permanent specialist hire was necessary. A capable Codex subscription can give a business leader access to substantial research and engineering capacity without a dedicated human implementing every experiment. Selective specialist review may be a better initial purchase than committing immediately to full-time headcount.

Yes, I think parts of the data-scientist role are at risk. Work centred on routine preparation, model wrappers, standard comparisons and report production is increasingly contestable. Expertise has to earn its place through better questions, stronger judgment, novel methods, reliable operations or a level of assurance the agent-assisted process cannot provide. High-stakes decisions and unfamiliar statistical problems can justify substantial human expertise. Managers should make that case explicitly, based on the work their business actually needs.

This experience also made me more demanding of agents. Access to a skill file does not excuse weak assumptions. More experiments do not excuse poor communication. A confident model recommendation needs reasons, and an attractive result needs a fair benchmark. When the evidence is disappointing, I expect the agent to tell me what failed and what we learned, without quietly changing the definition of success.

We ended this phase with three bounded research models, a reproducible local pipeline, an honest account of failed approaches and a more practical product plan. My ambition for BizOps is still broad. The route now follows questions we can test, capabilities we can explain and assumptions I can own in a management discussion.

I would have previously expected to hire specialist capacity to get this far. Now I know how much of that work I can direct through agents. That will change my next hiring conversation.

Machine learning does not have to mean a neural network

Two of our banked models use supervised machine learning: customer buying likelihood and customer–product-category propensity. Both use logistic hazard models whose coefficients are learned from historical examples. Logistic regression is an established supervised learning method, despite its roots in statistics. The third banked component, our revenue VAR, is best described as a fitted statistical forecasting model. All three estimate relationships from data; none is simply a manually assigned score. Scikit-learn’s supervised-learning documentation places logistic regression within that same family of learning methods.

For the propensity models, we designed the historical features and specified the training objective. Training then determined how those features contributed to predictions. We did not manually decide how much importance each customer’s recency, purchasing frequency or seasonality should receive. A neural network would also learn weights, but could combine its inputs through additional layers and nonlinear relationships. That extra flexibility would need to produce better predictions to justify its complexity.

We also genuinely tested neural forecasting. Chronos is a pretrained neural model, and our experiments included both using its existing weights and training smaller adapters through LoRA. The neural configurations we evaluated did not provide sufficiently dependable monetary forecasts to earn promotion. That finding applies to those configurations and tasks. It does not establish that neural networks cannot work for this business. A purpose-built neural customer-propensity model would be a different experiment from the monetary forecasting approaches already tested.

If we pursue that experiment, we can begin with the sales history already available. We do not need to make detailed invoice enrichment or another decade of data a prerequisite. We do need consistent monthly histories, traceable product-category mappings, explicit account-identity limitations, and a distinction between observed inactivity and unavailable history. Twenty-three years of records provide valuable evidence, but repeated examples from overlapping periods are not twenty-three years of independent evidence for every customer.

One plausible candidate would be a compact network shared across customers and categories. It could learn from sequences of purchasing activity, recency, seasonality and changing product mix, allowing sparse histories to benefit from patterns elsewhere in the portfolio. Learning across related series is an established direction in global neural forecasting research. That supports investigating the approach; it does not identify a winning architecture for our particular business.

The output must match the question. For buying propensity, a neural model could estimate monthly conditional billing probabilities and combine them into three-, six- and twelve-month likelihoods, while retaining our treatment of incomplete follow-up. For revenue, it would additionally need to learn positive spending over the same forecast window conditional on activity, with credits handled consistently and separately. Accurate buying probabilities alone do not establish accurate spending forecasts. The combined monetary outputs would need their own evaluation and coherent aggregation into business totals.

Training would involve choosing the network’s size, historical lookback, learning rate and regularization, then updating its weights over batches of historical examples. Those choices—and when to stop training—must be assessed on chronological development data. Otherwise, a flexible network could become increasingly good at reproducing the past without improving its forecasts.

The acceptance test would remain practical: compare the neural candidate with our banked models and simple controls using the same forecast origins, target populations and accounting rules. We would examine probability error, ranking, calibration, monetary errors and uncertainty coverage according to the task. Because FY24–FY26 have already informed this research, another comparison against those years would remain retrospective evidence. Newly arriving outcomes would be important for establishing prospective reliability.

A bounded experiment could still follow our frugal operating model: train locally, measure runtime and memory, and retain reproducible fitted outputs. We would assess whether GPU acceleration justified its cost rather than assume it was mandatory. A successful neural model would not automatically require an additional always-on production service.

What remains unsolved is substantial. We have not established dependable customer and category spending forecasts, calibrated six-year revenue predictions, or a complete model of business survival. Bankruptcy and workforce scenarios also require expenses, cash, working capital and assumptions about how costs respond. Our research produced trained components with useful, bounded evidence. The broader forecasting platform remains an objective whose capabilities must continue to earn their place through testing..

Technical appendix: what we actually trained and tested

A practitioner should be able to distinguish an implemented model from a plausible story about one. Our three banks contain fitted states, versioned feature definitions, training cutoffs, fixed experiment specifications, dependency identities, saved predictions and evaluation evidence. “Banked” means preserved and accepted for a stated research task. It does not mean that every output has passed production or financial-planning qualification.

A plain-language guide to the terminology

Training estimates a model's numerical relationships from historical examples. Inference applies the fitted model to make a prediction. A feature is an input such as months since the last purchase. A parameter is learned, such as the weight attached to that feature; a hyperparameter is a setting we choose, such as the penalty on large weights. An intercept is a constant term. More parameters do not automatically mean more useful intelligence.

The forecast origin is the date at which available information stops; the horizon is how far ahead we predict. A backtest moves that origin into the past and checks the subsequent actual outcome. A baseline or control is a simple reference, such as repeating the previous year, which a more complex model should justify improving upon. Granularity means the level being scored: customer, category, month or whole-business year.

TermWhat it means in this projectHow to read it
VAR — vector autoregressionA model of several related time series, using their earlier values to predict subsequent values jointlyOur VAR(1) uses one preceding monthly vector of seasonal changes; the 1 is not one year of training history
APE — absolute percentage errorOne absolute forecast error divided by the absolute actual value, multiplied by 100Used for an individual annual total; lower is better
WAPE — weighted absolute percentage errorThe sum of absolute forecast errors divided by the sum of absolute actuals, multiplied by 100Larger actual totals carry more weight; it is not an equal-weight average of annual APEs
AP — average precisionA summary of precision across recall levels when ranking likely buyers; precision is the fraction of selected cases with positive billing, and recall is the fraction of all positive-billing cases selectedHigher is better; AP is neither percentage accuracy nor an individual buying probability
Brier scoreThe average squared difference between each predicted probability and its observed 0 or 1 outcomeLower is better; zero means perfect probabilities on those observations, not a guarantee about the future
Log lossA probability-error score that penalizes confidently wrong predictions especially stronglyLower is better; theoretical log loss is unbounded. Our scorer clips probabilities to [1e-12, 1 - 1e-12] for finite computation
CalibrationWhether groups assigned similar probabilities experience events at approximately those ratesA well-ranked customer list can still overstate its buying probabilities
Prediction interval and coverageAn interval describes a range for a future outcome; coverage is how often historical outcomes fell inside the stated rangesA nominal 95% label is a target, not evidence of 95% empirical coverage

For example, the reported 4.03% annual WAPE describes combined absolute annual errors relative to combined absolute annual actuals. It does not mean 95.97% of customers were predicted correctly, or that next year's outcome has a 95.97% probability of being right. Likewise, 88.8% AP is a ranking score, not an 88.8% chance of purchasing.

The datasets and the time boundary

The data grew in three stages. Months below describe source coverage before restricting each training fold; they are not independent observations for every customer.

Source used during the journeyClosed monthly coverageWhat it added
Initial Cosmos-derived platform historyAugust 2020–September 2026; 74 monthsA practical starting point using data already in the application
First direct SAP workbookAugust 2015–July 2026; 132 monthsEleven complete August–July fiscal years, with detailed billing lines
Extended SAP billing exportOctober 2003–September 2026; 276 monthsTwenty-two complete fiscal years plus partial edge years; a much longer view of repeat and dormant behaviour

We preserved source workbooks, checked date and sign semantics, and built monthly account and account–category panels. A positive-billing event means at least one positive billed line: a positive sale offset by a credit still counts as activity. Credit-only months do not. Signed revenue retains credits rather than silently dropping them. Unresolved dates stayed quarantined. Detailed materials were mapped to reporting categories through traceable catalogue links, with an explicit Unclassified bucket. We did not treat a limited present-day customer master as definitive evidence for twenty years of account identity.

Historical tests moved the cutoff forward. Training labels, fitted scaling and features stopped at the origin; the following fiscal year supplied the outcomes.

Test targetLast training monthHeld-out target months2015-start training months2003-start training months
FY24July 2023August 2023–July 202496238
FY25July 2024August 2024–July 2025108250
FY26July 2025August 2025–July 2026120262

The two original propensity banks use the 2015 workbook. Their results below must not be relabelled as 2003-history models. The later revenue comparison uses two start dates from the same extended export, keeping target actuals and reporting taxonomy fixed. That isolates the history-window comparison more fairly than comparing scores from different exports.

These are chronological, retrospective backtests, following the logic of rolling-origin evaluation. They are not newly untouched holdouts after the entire research journey. We reused FY24–FY26 while choosing approaches. The export, category mapping and source costs are also a current, as-restated vintage; event-date cutoffs do not reconstruct exactly what management knew years earlier. No random row split, invented extra years, tender outcome or contract-status label was used to establish these results.

Three banked models, three prediction targets

The first two models are supervised statistical learning. The third is an estimated multivariate time-series model. All learn numerical parameters from data. The simple rules shown later are comparison controls, not substitutes quietly passed off as trained models.

Model 1: customer purchase likelihood

The question is: will an already observed account generate positive billing within the next three, six or twelve months? The target is billing activity, not contract renewal, physical product demand or the size of an order.

We implemented a pooled discrete-time logistic hazard model. Discrete-time means we work in monthly steps. The hazard is the chance of the next billing event in a month, conditional on no event yet since the forecast origin. Survival means the probability of still waiting for that event. Logistic describes the function that converts a weighted feature score into a probability between zero and one. Pooled means one shared set of coefficients learns across accounts; different histories still produce different predictions.

This formulation fits the observable question in our business: irregular purchases separated by quiet months, with an uncertain wait until the next bill. We can model timing and seasonality without deciding that every quiet account has permanently left. Sharing coefficients offers a practical way to learn from sparse account histories. Discrete-time event-history methods and their treatment of censoring are well established in Allison's foundational work. Our choice of features and overlapping landmarks is an adaptation to test, not a prescription from that paper.

A landmark is a dated snapshot from which we ask about the next event. Each eligible monthly landmark produces an at-risk sequence. Follow-up stops at the first subsequent positive month, twelve months, or the training cutoff. An incomplete future is right-censored: we use the follow-up actually observed, without labelling the unobserved remainder as non-purchasing. Landmarks start after twelve globally observed months; an account's own exposure starts when it first appears. Repeated landmarks and horizons are dependent training examples, not additional independent customers.

The model has 23 features: observed exposure, recency, positive-month frequencies over several windows, completed gaps, changes in annual activity, target-calendar activity, sine/cosine seasonality and horizon terms. It does not use customer ID or monetary amounts as predictors. Historical features describe the prefix; future-month features condition on no intervening event.

For a nondegenerate fit, training estimates 23 slopes and an intercept. With training-standardized features z, the conditional monthly hazard and cumulative probability are:

h_m = sigmoid(b0 + beta' z_m) P(first positive billing by month H) = 1 - product_m=1..H(1 - h_m) loss = mean Bernoulli negative log likelihood + (0.01 / 2) * ||beta||²

This survival product combines conditional hazards; it does not assume independent unconditional monthly purchases. The intercept is unpenalized. The L2 penalty, a form of regularization, discourages large weights that might fit historical noise too closely; it trades some flexibility for stability. Feature means and population standard deviations are fitted only on the training rows, with scale one for constant columns. Initialization uses the training event-rate logit and zero slopes. SciPy L-BFGS-B uses maxiter=1000, ftol=1e-12 and gtol=1e-7; non-success or nonfinite fits are rejected without a retry. L-BFGS-B is the numerical optimization algorithm that updates the weights to reduce the loss; maxiter limits iterations, while ftol and gtol are stopping tolerances for objective improvement and gradient size. Those are fixed numerical settings, not parameters selected to win on FY26. Optimizer success is a computational check, not a guarantee of statistical validity.

The useful output is an account-prioritization and dormancy view. It cannot discover the identity of an account that has never appeared, or convert a buying probability into expected revenue without another qualified model. The original bank contains the three historical test fits; it does not contain a later FY27 customer fit by implication.

Model 2: customer–product-category propensity

This asks: which reporting categories is an observed customer likely to buy within each horizon? We modeled customer–category pairs, including pairs with no previous purchase, using the same censored hazard formulation and optimization policy.

The pair-level question is naturally sparser than the account-level question: a customer may buy regularly but purchase a particular equipment category only occasionally. Pooling pair history with customer activity and portfolio information lets the model learn shared associations instead of fitting a separate model to every thin customer–category history. That is why we reused the event-time formulation at this finer level, then tested it separately.

There are 35 features and, in a nondegenerate fit, 35 learned slopes plus an intercept. Pair recency and frequency combine with overall customer activity, category activity, historical mix shifts, other-category affinity and seasonality. Recency means time since the last event; affinity describes an observed purchasing association. Beta(1,1) smoothing stabilizes sparse activity-rate features by adding one event and one non-event pseudo-count, so very short histories do not immediately produce extreme rates. Some mix features use historical positive-billed-value shares; this is still an activity target, not a fitted spend prediction. Pair exposure begins only once both the customer and category are observed.

This learns an association across customers and categories rather than merely listing last year's orders. It supports portfolio reviews, repeat-purchase prioritization and tentative cross-sell exploration. It is not a causal recommendation engine: exposure, availability and sales effort were not randomized. Category probabilities can overlap because one customer can buy several categories; they are neither mutually exclusive product shares nor revenue allocations.

The original bank preserves three test fits and a fourth July 2026 opening fit using the full 132-month workbook. Later expanded-history or calibration experiments are separate evidence, not an invisible upgrade to that bank.

How the propensity models performed

For each task, we evaluated the complete origin-known population. Brier score is mean squared probability error; log loss is Bernoulli negative log likelihood. Lower is better for both. Average precision (AP) measures ranking across the precision–recall curve; higher is better. AP is shown on a percentage scale, not as percentage accuracy. Brier and log loss are shown in their native units. Metric definitions matter particularly when positive outcomes are relatively uncommon.

These are the learned models' twelve-month results. Mean probability and observed event rate are population summaries, not individual customer disclosures.

TaskTargetBrier ↓Log loss ↓AP ↑Mean probabilityObserved event rate
CustomerFY240.0950.31587.1%34.9%28.5%
CustomerFY250.0710.25690.5%31.8%24.4%
CustomerFY260.0650.24189.2%28.9%21.3%
Customer–categoryFY240.0490.17979.4%13.4%11.9%
Customer–categoryFY250.0380.14377.9%12.1%9.0%
Customer–categoryFY260.0350.13182.4%11.4%9.2%

The customer model's Brier score fell across these three test years; that is not a controlled learning curve. Training history, the evaluated accounts and business conditions all changed. The customer model's annual AP also did not improve monotonically. A good report should preserve that distinction.

For the same twelve-month outcomes pooled across the three origins, the comparisons were:

TaskMethodBrier ↓Log loss ↓AP ↑
CustomerLearned hazard0.0760.26988.8%
CustomerRecency/frequency0.2370.95876.4%
CustomerSeasonal activity0.3351.10472.2%
Customer–categoryLearned hazard0.0400.15079.9%
Customer–categoryCustomer activity × affinity0.0870.32568.5%
Customer–categoryPair recency/frequency0.2080.70159.1%
Customer–categorySeasonal pair0.0970.34367.3%
Customer–categoryPrevious-FY repeat*0.0752.06847.6%
Customer–categoryThree-year annual frequency*0.0540.19762.1%

The two starred category controls were added as post-hoc sanity checks: a smoothed frequency of activity across complete historical twelve-month blocks, using up to the latest three, and repetition of the previous fiscal year's activity. They did not trigger a model refit or rewrite the original acceptance screen. Their results are useful checks, not evidence of a preregistered comparison.

The other controls use recent activity frequency, same-calendar seasonal activity, or overall customer activity combined with category affinity. We score them against the same targets as the learned models. Pooling recomputes metrics from all scored entity–origin rows; it is not an unweighted average of the yearly scores.

The horizon breakdown also exposes probability bias:

TaskMonths aheadBrier ↓AP ↑Mean probabilityObserved event rate
Customer30.06383.1%18.4%16.9%
Customer60.06885.9%24.8%19.8%
Customer120.07688.8%31.7%24.5%
Customer–category30.02870.4%5.9%5.6%
Customer–category60.03375.8%8.6%7.5%
Customer–category120.04079.9%12.3%10.0%

The learned models improve on these controls, but their twelve-month mean probabilities still exceed the realized event frequencies. Lower Brier score alone does not establish good calibration: it reflects discrimination as well as calibration. A reliability curve, subgroup checks and fresh prospective outcomes remain necessary before treating a stated probability as a dependable decision threshold. Nor can customer and customer–category Brier scores be compared as if the tasks had the same prevalence. The distinction follows standard probability-calibration guidance.

Model 3: annual whole-business revenue

The banked monetary model is a seasonal-change ridge vector autoregression, VAR(1). It operates on category-level signed revenue and source-cost channels jointly. Here, signed values preserve positive billings and negative credits. The accepted research task is the annual sum of business revenue. Source cost is an input; it does not make management gross profit or cash flow qualified outputs.

This was a suitable candidate because the categories form related monthly histories, and the annual decision does not require a separate complex spend model for every sparse account. A VAR can learn predictive relationships across those histories as well as within each one. Seasonal differencing compares a month with the same month a year earlier, giving the model a way to work with changes around an annual pattern. It does not guarantee that those changes are stable through every business regime. VARs for related series and coefficient shrinkage are established forecasting methods, described in Forecasting: Principles and Practice.

We chose a short lag and ridge regularization to limit flexibility and numerical instability relative to the available history. Ridge is an L2 penalty on the coefficient matrix, rather than a rule that sets selected coefficients exactly to zero. Its suitability here rests on the recorded annual comparisons and low compute cost; the later six-year experiment shows where that case weakens.

For K categories there are M = 2K channels. Each channel is scaled by its training-prefix mean absolute monthly level, with structural-zero handling. We difference normalized levels at lag twelve, then estimate how the vector of seasonal changes depends on its previous monthly value:

z_t = channel-scaled monthly levels d_t = z_t - z_(t-12) d_t = a + d_(t-1) B + error_t ridge objective = ||Y_centered - X_centered B||²_F / N + 0.1 * ||B||²_F

Training learns an M × M lag matrix and an M-component intercept from T - 13 transition rows. It uses a float64 ridge solve, with a condition-number guard of 1e12. Singular-value decomposition (SVD) expresses a matrix in directions with different amplification strengths. We cap those singular values at 0.95, then recompute the intercept from the training means. In the equation, the squared Frobenius norm is the sum of squared matrix entries; the separate operator 2-norm cap limits the largest directional amplification. These are different controls. The cap bounds propagation of perturbations in the seasonal-change dynamics; it does not guarantee bounded revenue levels or calibrated forecast errors. The ridge penalty, lag and cap were fixed for this experiment, without a hyperparameter sweep over the reported test years.

Forecasting recursively generates twelve monthly points, restores seasonal levels and channel scales, rounds signed category/month values once, and derives parent totals by exact sums. A cross-category coefficient is a learned predictive association, not proof that one product causes another to sell. Historical customer-share allocations attached to this model are bookkeeping allocations, not learned customer spending.

Its use case is an annual business revenue reference and a candidate baseline for an explicitly labelled management sensitivity. The original revenue bank retained sixteen fitted prefix states across the two history arms. Historical annual screening passed; current FY27 estimates, category/customer money and operational publication remain separately unqualified.

Revenue benchmarks: more history, same target actuals

The following is a whole-business annual comparison using the extended SAP export. Every method is scored against the same full signed target population, including accounts that were unknown at the forecast origin. VAR includes an unallocated entrant reserve in its business total; Boost and Chronos forecast origin-known named series without that reserve. These are delivered-system comparisons, not an experiment isolating only the learning algorithm.

Each FY column is annual absolute percentage error (APE). The final column is pooled annual WAPE: absolute annual errors summed across origins, divided by the sum of absolute annual actuals. Errors are made absolute before pooling, so over- and under-prediction cannot cancel. A zero denominator would be reported as undefined. WAPE is not “100 minus accuracy.”

WAPE gives a readable, scale-free summary for the business-level comparison, and is documented in commercial forecasting evaluation. It can tolerate some zero-valued observations without dividing by each one separately. It can also hide individual failures, weight large periods heavily and change as the test period's scale changes. Hyndman explains why WAPE can mislead on trending or otherwise nonstationary series. We therefore read these as descriptive errors on the named, identical target windows, alongside baselines and granularity checks. They do not certify a stable future error rate. Training-scaled measures such as mean absolute scaled error (MASE) and root mean squared scaled error (RMSSE) are useful complements for future evaluation; they have not been added to these preserved results.

MethodTraining startFY24 APE ↓FY25 APE ↓FY26 APE ↓Pooled annual WAPE ↓
VAR2003 onward1.96%8.08%1.83%4.03%
VAR2015 onward3.03%9.32%2.28%4.94%
Boost2003 onward12.91%27.26%15.67%18.85%
Boost2015 onward21.25%32.08%24.84%26.27%
Chronos2003 onward10.96%3.95%180.56%68.32%
Chronos2015 onward0.24%11.38%171.01%64.29%
Repeat last FYCommon prefix control3.59%12.82%1.87%6.17%
Three-year seasonal meanCommon prefix control20.30%18.71%11.28%16.57%

“Boost” here is the implemented LightGBM activity–amount system, not CatBoost. Gradient boosting combines decision trees sequentially so later trees improve the chosen loss after earlier ones. Its fixed recipe fits 350 boosting rounds per component: a positive-activity classifier, a conditional positive-amount Gamma regressor and an unconditional credit-magnitude Tweedie regressor with variance power 1.3. It combines activity probability times positive amount, subtracts credit magnitude, and reconciles joint customer–category points to customer anchors. Gamma models strictly positive, skewed amounts; this Tweedie model accommodates a mixture of zero and positive credit magnitudes. Separating occurrence from size was a reasonable hypothesis for intermittent billing, but this implementation did not beat the annual VAR. Those are actual fitted trees and objectives, not a prompt asking a language model to estimate sales.

“Chronos” in this table is pinned pretrained Chronos-2 used without weight updates, with eligible prefix history and deterministic calendar covariates. Its signed point comes from a monotonically rearranged 21-quantile grid integrated by the trapezoidal rule with flat outer tails; it is not the returned median or a demonstrated calibrated conditional mean. The public checkpoint postdates the simulated origins, so we cannot certify historical pretraining availability. Zero-shot means using pretrained weights without fitting them to our history. A quantile is a percentile-like point in a predicted distribution; calendar covariates are extra inputs such as the month. Chronos was worth testing because it could transfer patterns learned from other time series, but transfer is a hypothesis to benchmark. A separate experiment genuinely updated adapter weights through LoRA, low-rank adaptation: small trainable adapter matrices augment the frozen base model. These table rows are not that experiment.

The full-history VAR improved its pooled annual error relative to its 2015 arm and both controls in this three-year comparison. More history also helped this Boost recipe, but did not rescue Chronos: the FY26 error dominates its pooled score. This is evidence for the selected business task and dataset, not a universal ranking of the model families.

Annual aggregation hides substantial timing error. Full-history VAR's pooled whole-business monthly WAPE was 33.79%, versus 33.62% for repeating last FY, despite its 4.03% annual WAPE. Category and historical customer-allocation quality also failed qualification. We therefore banked an annual business research model, rather than advertising accurate monthly or customer revenue forecasts.

What changed when we forecast six years ahead

We extended the same VAR recurrence, including its generated seasonal buffer after month twelve. Historical July origins ran from 2007 through 2025; only fully observed targets through FY26 were scored. That provides 19 one-year outcomes and 14 six-year outcomes, with 99 observed origin–year outcomes across all leads. They overlap heavily. They are not 99 independent business histories.

For raw uncertainty, each origin used 2,000 paths with coefficients fixed. A residual is an observed value minus the fitted model value. Block bootstrap simulation resamples runs of residuals rather than isolated months, retaining some of their time dependence. We centered the final model's residual vectors and sampled circular twelve-month blocks jointly across all channels. Each path advanced its own seasonal state. A fixed origin-specific seed made the draw reproducible. We summed rounded monthly/category values within each complete path before taking nearest-rank annual and cumulative quantiles; summing marginal quantiles would have produced a different, invalid aggregation.

This resamples historical residual variation and preserves within-block temporal and cross-channel structure. It does not incorporate parameter uncertainty or an unseen structural break. Simulated paths were never fed back as additional training history. More paths reduce simulation noise; they do not create more observed fiscal years.

The table scores the individual year at each lead, not cumulative revenue through that year. Baselines hold the origin's last-year pattern or trailing three-year seasonal mean fixed, without refreshing them from future actuals. Coverage is the percentage of historical outcomes inside the raw central interval.

Lead yearsOriginsVAR WAPE ↓Last FY WAPE ↓Seasonal mean WAPE ↓80% interval coverage95% interval coverage
11916.29%16.59%22.95%57.9%84.2%
21823.65%24.75%29.58%66.7%83.3%
31731.04%31.34%34.20%47.1%64.7%
41635.88%35.25%35.58%37.5%81.2%
51540.56%36.37%35.37%40.0%60.0%
61439.40%35.91%37.51%35.7%50.0%

The one-year advantage over repeating last FY is small over this longer test period. At four to six years the controls outperform VAR. Its nominal 95% interval covers only 50% of the six-year outcomes. That is serious undercoverage, not a validated 95% assurance level. Small, overlapping samples also prevent treating these percentages as precise population estimates.

We produced opening and latest-data FY27–FY32 research paths, but this evidence does not justify presenting them as reliable six-year financial forecasts. The strategic product direction is consequently an assumption-led growth, downturn and recovery planner, clearly distinguished from a model prediction. A latest-data FY27 landing also contains closed actual months; those must stay fixed when a future scenario is changed. Monthly update origins and remaining-year forecasting need their own validation.

Is this industry best practice?

The model families and evaluation methods have established foundations. Our method choices combine those foundations with the business question and our own results. A method's presence in a textbook does not certify our implementation or make it the best choice for municipal infrastructure sales. There is no evidence here for an industry-wide winning algorithm.

The stronger claim is about the discipline: define the target, respect time order, keep transformations inside the training window, compare against credible simple controls, examine the level of detail actually needed, measure probability quality and interval coverage, and preserve failed results. These align with established forecast-evaluation practice. Our small, repeatedly examined set of fiscal-year tests still leaves selection uncertainty and prospective performance to establish.

For our three banks, the rationale is specific. The customer hazard fits the wait until observable billing; the category hazard fits the same event question for sparse customer–category pairs; the regularized VAR offers a compact annual business reference using related seasonal histories. Their recorded comparisons supported retaining them for those tasks. The exact 23 or 35 features, penalty settings, one-lag choice and stability cap are our design decisions, not industry-mandated settings. More independent outcomes could change those choices.

That is how I would explain the solution to a CEO or CFO: established statistical methods, adapted to the available evidence, tested against alternatives and assigned specific uses. The claim is stronger when we can explain where each model helps and where a management assumption must take over.

Reproducibility and measured computational cost

The preserved evidence binds input, mapping, feature schema, experiment specification, source code, environment and model state through hashes. Private derived datasets, fitted states and detailed results are retained outside Git in Windows Data Protection API (DPAPI)-protected artifacts; raw source workbooks remain separate and are not encrypted by that mechanism. Git holds code, specifications, dependency locks, tests and technical manifests. Machine-bound encryption is not itself a disaster-recovery strategy.

For the appendix, independent metric extraction checked the saved native/result identities and recalculated all 54 aggregate propensity records in the companion evidence, all 18 revenue comparison scores, the common revenue controls and the long-horizon origin summaries. Propensity metrics matched saved receipts within 1e-12, and cumulative hazards reproduced delivered probabilities. This was saved-evidence verification: it did not reread the raw workbooks, refit weights, rerun Chronos or certify source-system completeness.

The completed long-horizon experiment also has a separate saved-state/seed replay: zero fits, exact equality of complete result and native payloads. An independent audit checked draw indices, annual aggregation and metrics, plus 63 monthly witness paths. That is a stated audit scope, not a claim to have independently recomputed every simulated monthly recurrence. Forty-seven focused tests cover data, evaluation, model, runner, audit and reporting mechanics; passing them does not establish forecast accuracy.

Measured runs used the existing Python 3.12.14 CPU environment with 72 pinned packages. The banked fits used one native numerical thread. A CPU is the general-purpose processor; a GPU is a processor often used to accelerate neural-network computation. RSS, resident set size, measures physical memory occupied by a process; GiB, gibibyte, is a binary memory unit equal to 2³⁰ bytes. Peak RSS below is observed process memory, not total workstation memory or a minimum hardware requirement.

Completed local workloadFits or replayWall timePeak RSS
Original customer likelihood evaluationThree historical fits30.40 seconds0.45 GiB
Original customer–category evaluationThree historical fits plus FY27 opening fit271.81 seconds1.22 GiB
Original annual VAR evaluationSixteen prefix fits138.04 seconds1.31 GiB
Long-horizon VAR evaluation and simulationThirteen earlier fits, eight saved states reused391.17 seconds1.95 GiB
Complete long-horizon saved-state/seed replayZero fits330.31 seconds1.96 GiB

These are measured batch workloads on this machine. They do not measure the whole research journey, developer productivity against a human control group, interactive latency or production throughput. The long-horizon run's 8 GiB/20-minute guard was an experiment bound, not a claim that production requires a dedicated machine of that size.

The cost-frugal pipeline and production boundary

The upper panel is implemented: preserve and qualify source data, build cutoff-safe features, fit and batch-score, compare against controls, replay and audit, then retain the accepted or rejected evidence. The lower panel is the agreed design for the new Traffic feature. Its publisher, release integration, application routes and model-backed AI tools still need implementation. Existing BizOps hosting, authentication and Signal AI infrastructure can be reused.

The workstation will train and generate supported batch outputs. A separate authorized publication boundary will validate lineage, permissions, supported origins/horizons and release readiness. Production will receive small, typed, non-executable result tables in private immutable storage, not raw SAP workbooks, local DPAPI blobs or a Python training runtime.

Publication will write and read back all required objects, write a ready manifest last, then change the active release pointer conditionally using If-Match. This selects a complete validated release; it does not assume a multi-object upload is atomic. Failed publication retains a visibly dated last-approved release. Rollback selects an intact compatible version; absent or unsupported results remain explicitly unavailable.

The existing Node API—the application programming interface through which the app requests data—will enforce business-unit (BU) and account access before exposing results. The dashboard and Signal AI tools will pin the same release, cutoff, model/data version and qualification status. Budget is a separate same-FY comparator, not a training label. Scenario controls will call shared deterministic arithmetic, so chat and charts cannot silently disagree about the effect of an assumption. A large language model (LLM) can retrieve and explain approved outputs; it will not invent coefficients, perform unverified financial arithmetic or retrain on a slider drag.

Refresh, inference and retraining are different operations. A newly imported month can trigger a local assessment; it cannot automatically qualify an origin or model capability we have never validated. Annual rollover will create a new dated output and preserve earlier forecast vintages, enabling comparison with subsequent actuals. The laptop is needed for preparing updates, not for users reading an approved published release.

This design avoids an additional always-on ML server, Python endpoint, GPU or managed training platform in the first increment. Existing storage, application hosting and AI usage still have costs. The savings come from performing bounded batch work locally and serving compact results through infrastructure we already operate, while retaining a clear path to a stronger model when evidence supports one.