A personal data project, built to learn by doing
I like taking messy, real-world data and turning it into something useful and understandable. I built Transit Analysis to explore APIs, geospatial data, graph-like relationships, realtime pipelines, databases and interactive visualisation — and to bring all of it together in one dashboard that keeps itself up to date.
Scroll↓
1 · Why transport data?
Public transport is a useful playground because almost everything has a time, a place and a relationship to something else. Vehicles belong to trips, trips belong to lines, stops form routes, and realtime predictions constantly change the picture.
2 · The data path
Python collectors turn public feeds into a consistent internal model. Some values are read directly, some are calculated, and in Zürich even the current vehicle position has to be estimated from timetable predictions and route geometry.
3 · The result
The result is a live geographic view of what is happening now and analytics built from history that keeps accumulating in the background. The site is not a static showcase — it keeps collecting, processing and updating.
I’m Marcel, and I have a particular interest in data, digital processes and building practical solutions around them. Transit Analysis started as a personal learning project: I wanted to work with data that is messy, live and interconnected rather than with a finished static dataset.
The project gave me a reason to work hands-on with APIs, geospatial data, graph-like network relationships, realtime collection, data modelling, Python, SQL and interactive visualisation. The goal was not just to experiment with each piece separately, but to combine them into a small application that is always online and continuously becomes more interesting as its history grows.
Static GTFS and realtime feeds are collected continuously instead of downloading one finished dataset for analysis.
Stops, coordinates, route shapes and moving vehicles are combined on interactive maps and used to understand trip progress.
A transport network naturally behaves like a graph: stops connect to routes, routes contain ordered stops, trips belong to services and vehicles move through that structure.
Collectors clean, match and transform the feeds, maintain live state and fold short-lived observations into compact historical tables.
The same data is presented as live maps, route profiles, heatmaps and charts so patterns can be explored rather than only queried.
The application runs continuously on a small server. The collectors keep gathering data while the public dashboard reads from the latest database state.
The full source code is available on GitHub. If you are working on something similar, spot a problem, have an idea for the project or simply want to exchange thoughts about data and transport, you can reach me at transitanalysis@protonmail.ch.
Helsinki and Zürich end up in the same dashboard, but the raw data is not equivalent. I deliberately kept the city-specific collection pipelines separate and only converged them at the database and web-API layer.
HSL provides static GTFS together with realtime feeds that include vehicle positions. The collector can therefore use reported locations and combine them with timetable data to calculate delay and trip progress.
The Swiss realtime source used here provides TripUpdates but no VehiclePositions. The collector maps the national Swiss trip to the corresponding ZVV trip, reconstructs predicted stop times and estimates the vehicle's location along the ZVV route shape.
Helsinki keeps its own timetable, live snapshot and historical aggregates in
helsinki/transit.sqlite.
Zürich uses the same analytical model where possible but adds mapping tables that connect Swiss identifiers and stop sequences to the ZVV network.
Zürich realtime data comes from a nationwide Swiss feed and has to be linked to the static ZVV network before it can be used for route and stop analytics.
The 94.1% figure is a validation snapshot, not a permanent quality guarantee. The exact share changes with the services present in the realtime feed.
Both city databases follow the same basic idea: live state is overwritten, raw observations are temporary, and compact aggregates are retained for analytics. Zürich adds its own mapping and prediction tables around that shared core.
The important split happens before the databases: Helsinki can use reported realtime positions, while Zürich needs an additional mapping and estimation layer. Once the observations are normalized, the same Flask application and frontend can serve both cities.
I deliberately kept the stack small. Python does the collection and API work, SQLite stores the data, and plain JavaScript draws the interactive parts.
Fetch feeds, parse GTFS and protobuf data, match trips and stops, calculate delays, estimate Zürich positions and maintain aggregates.
Serves the pages and the read-only JSON API. The web process reads the databases but does not maintain the collection history itself.
One database per city. Write-ahead logging allows collectors to write while the public application continues to read.
Maps, charts, filters and even the scroll animation on this page are plain JavaScript.
Used for trends, comparisons and route-level delay profiles drawn from the JSON API.
Used for live vehicles, route geometry and stop-level spatial views rendered interactively with WebGL.
Separate schedule, realtime and web services run from the same project image and share persistent data.
Handles HTTPS and reverse proxying in front of the Flask application.
A route-level average can tell you that a service is late, but not where the problem develops. The stop profile keeps one value per trip and stop so consecutive stops can be compared.
Helsinki and Zürich do not have identical semantics here. Zürich values are based on the latest available realtime timetable prediction around a stop passage, not a GPS-confirmed measured arrival time.
The interactive maps use MapLibre GL and therefore rely on WebGL / hardware-accelerated browser rendering. Some managed work computers disable WebGL or hardware acceleration through company policy. Security products can also block third-party map or JavaScript resources.
In those environments an interactive map may fail to render even though ordinary HTML pages and the JSON API still work. Reducing external runtime dependencies and providing a graceful fallback for browsers without WebGL are therefore part of the final hardening of the project.
The dashboard becomes more useful over time: one afternoon can show what is happening now, but recurring weekday, hourly and route patterns need weeks of continuously collected history.
Transit Analysis is a personal side project and is not affiliated with HSL, ZVV or OpenTransportData.swiss. It exists as a practical way for me to explore realtime APIs, data engineering, geospatial analysis and visualisation.