Project 2 · Portfolio

AI-Assisted Bus Stop Data Cleaner

A tool that finds the same bus stop hiding twice under two different names — using fast, ordinary checks for the obvious cases, and an AI model for the genuinely tricky ones, with a person always making the final call.

View the live demo

What it does

You upload a real UK bus timetable's stop list — a GTFS extract (the standard spreadsheet-style format timetables are published in; this demo comes bundled with a real route, First Leeds' Route 56). The tool checks every pair of stops for two warning signs: near-identical names and being close together on the map — for example, "Leeds, Corn Exchange" and "Leeds Corn Exchange (Stop CE1)".

Plain, cheap checks (no AI involved) handle the clear-cut cases. Only the pairs that are genuinely ambiguous — where the name and distance evidence don't obviously agree — are sent to Claude, Anthropic's AI model, for a judgement call. You then see every suggestion side by side with the original data and tick which ones to actually fix. Nothing is ever changed automatically — you download a cleaned file only once you've approved each fix yourself.

Why it matters

Duplicate bus stop records are a quiet, common problem in public transport open data — the same physical stop ends up listed twice because it was renamed, re-coded, or just typed in slightly differently somewhere along the way. Anything downstream that assumes "one stop = one row" — journey planners, accessibility information, even a simple stop count — can quietly get it wrong.

Checking for this by eye across thousands of stops doesn't scale, but blindly auto-merging anything with a similar name is dangerous too — two real, different stops very often share a name (think both sides of the same road). This project's answer is to let cheap, explainable rules make the obvious calls, and reserve the AI model — and a human's final sign-off — for the cases that genuinely need judgement.

Who it helps

For anyone who maintains bus-stop open data — a local authority, an operator, a transport data team — it's a much faster way to find candidate duplicate records than scrolling through a spreadsheet by hand. For anyone reviewing this as a piece of work, it demonstrates a responsible way to use an AI model in a real tool: cheap non-AI checks run first, the AI is only asked about the hard cases, every AI call is counted against a hard daily spending cap, and nothing it suggests is applied until a person has approved it.

Under the hood

Technology used, and why

Every tool below was picked for a specific reason, not out of habit — the table explains each one in plain terms.

Python

The programming language the whole app is written in — the standard choice for this kind of "tidy up messy real-world data" work.

pandas

Does the cheap, obvious tidying first — spotting near-identical names and nearby stops — so the AI model is only ever asked about the genuinely tricky pairs.

Anthropic Claude API — Claude Haiku 4.5

A fast, cost-efficient Claude model is asked to judge only the stop pairs the rule-based checks couldn't confidently resolve on their own — not every stop, just the hard calls.

Streamlit

Turns the cleaning logic straight into the web page you can open in a browser, without having to separately build a front end from scratch.

SQLite

A simple, file-based database that remembers how many AI tokens have been spent today, so the daily spending cap still holds even if the app restarts.

pytest

An automated test suite (21 checks) covers the duplicate-finding logic and the daily spending cap, plus the AI step itself using a stand-in for Claude — so the AI-calling code is checked without spending real tokens, and a human still makes the final call on its real answers.

Docker & Docker Compose

Packages the whole app so it runs the same way everywhere, with an automatic restart policy and no separate installation steps.

How it's built

Architecture

Shown here as two diagrams at two levels of detail — first what happens from the moment you upload a file to the moment you download a cleaned one, then exactly what happens on the AI step, where the daily spending cap is enforced.

1. The big picture — from upload to a file you control

One pass through the app, start to finish. Nothing is written back anywhere on the server — the cleaned file is generated fresh in your browser from whatever you've approved.

flowchart LR
    accTitle: End-to-end flow diagram
    accDescr: You upload a bus stop file. The app tidies it with pandas. The AI model reviews only the hard cases pandas could not resolve. You approve or reject each suggested fix. You download the cleaned file.
    A["You upload a
bus stop file"] --> B["App tidies it
with pandas"] B --> C["AI reviews only
the hard cases"] C --> D["You approve or
reject each fix"] D --> E["Download the
cleaned file"] classDef io fill:#fde8d7,stroke:#c2410c,stroke-width:2px,color:#1f2937 classDef proc fill:#dcfce7,stroke:#15803d,stroke-width:2px,color:#1f2937 class A,E io class B,C,D proc

In plain terms: you upload a stop list on the left, pandas and the AI work on it in the middle, and a cleaned file you control comes out on the right — nothing changes until you say so.

2. What happens when you press "Run cleaning"

Zooming in on the "AI reviews only the hard cases" box above: the app checks the daily budget before spending any money, not after.

flowchart LR
    accTitle: AI step and daily spending cap diagram
    accDescr: When Run cleaning is pressed, the app checks whether any AI token budget is left for today. If yes, Claude judges the hard pairs and the actual tokens used are added to today's total. If no, the app shows a limit reached message and does not call the AI at all.
    F["'Run cleaning'
pressed"] --> G{"Budget left
today?"} G -->|Yes| H["Claude judges
the hard pairs"] H --> I["Actual tokens used
added to today's total"] G -->|No| J["'Limit reached' shown
no AI call made"] classDef step fill:#dcfce7,stroke:#15803d,stroke-width:2px,color:#1f2937 classDef check fill:#e0e7ff,stroke:#4338ca,stroke-width:2px,color:#1f2937 class F,H,I,J step class G check

The budget is only ever updated using the actual token count the Claude API reports back for that call, never an estimate — so the daily total can't quietly drift from reality.

See it running for real

This isn't a mock-up — it's a live tool, built with a real bus route's real stop data.

View the live demo