Building Urban Rail Atlas: Making Railway Ridership Data Understandable
How Urban Rail Atlas turns official Greater Tokyo railway statistics into explainable views, with a Python pipeline, versioned contracts, and a static-first Next.js application.
Urban Rail Atlas brings together my research background in data visualization and my interests in data engineering, railways, and urban sociology. Subways and above-ground rail offer a concrete subject for connecting these interests: there is a network to explore, public data to interpret, and a wider city behind the statistics.
I built the project to explore official railway ridership statistics across Greater Tokyo through station pages, line views, historical trends, and comparisons. The technical challenge is making those views useful while preserving what the source data actually means.
A railway station's passenger count looks like a simple number until you try to compare it with another station. One source may count people boarding trains. Another may count entries and exits together. A published value may describe one station on one line, or several connected stations reported as a single unit. Putting these values into the same ranking can produce a convincing chart with an unclear meaning.
Why build another railway website?
The urban sociology interest gives the project a broader context. Railway statistics can be a starting point for questions about how people move through a city and how different places relate to its transport network. Ridership alone cannot explain a neighborhood's social or economic conditions, but it offers one perspective to investigate alongside other evidence.
The product starts with a gap between available information and usable explanation. Railway operators and government portals publish useful statistics, but the information is spread across sources, formats, years, and definitions. A reader interested in the relative scale or historical development of stations has to reconstruct much of that context before making a comparison.
The atlas organizes that work into an explorable publication. A station view can show what the source reports, which period it covers, and how the series changes over time. A line view adds network context. A comparison should help readers investigate a question while making incompatible measures visibly separate.
The geographic boundary is Greater Tokyo: Tokyo Metro, Toei Subway, and the approved JR East conventional-rail coverage in Tokyo, Kanagawa, Saitama, and Chiba. Keeping that boundary makes room for source review and clearer explanations. Each additional operator would introduce another set of identity, methodology, and licensing decisions.
The available statistics also define the product's time scale. Annual observations and daily averages support historical exploration. They do not establish how crowded a train is right now. The interface is designed around the questions those statistics can answer.
Make the data model carry the meaning
The project separates a station's identity from its observations. A station has a stable product identifier, source references, and line memberships. An observation attaches a value to a reporting entity, metric, period, and source version.
That separation matters at interchanges. Tokyo Metro's Otemachi survey unit covers several line-stations. Treating the published total as a separate count for each member would duplicate passengers in the interface. Dividing it equally would introduce an estimate the source never supplied. The model instead preserves the published unit as a complex and points readers to the level where the value is actually reported.
Names are insufficient identifiers, too. Stations operated by different companies can share a name without representing the same statistical entity. Conversely, one source can report multiple named stations together. Operator-level identities remain intact, with explicit groups where the source supports them.
Comparison follows the same principle. Metrics include reporting-unit and comparability information. The frontend uses the contract's comparability group to decide whether observations can be directly compared. That guard is one part of a valid comparison: the displayed period and methodology still matter. JR East's MLIT-derived statistics remain in a separate metric group rather than being silently combined with the subway surveys.
Missingness also needs meaning. A missing observation, a value reported at a complex, and a withheld series are different states. None should become zero merely because a chart component expects a number. A gap in a historical series stays a gap, with an explanation when one is available.
Separate acquisition from publication
The system has two runtimes with a versioned boundary between them:
Official sources
↓
Python acquisition and immutable snapshots
↓
Validation, normalization, and entity resolution
↓
Versioned frontend view models
↓
Next.js pages and interactive charts
Python owns source-specific work. Adapters handle the upstream formats used by the Public Transportation Open Data Center and MLIT's station ridership dataset. Retrieved snapshots retain checksums and retrieval metadata, allowing later processing to work from known inputs instead of silently requesting a different version of a dataset.
Pydantic models and validation steps check the canonical records. Normalization makes sources usable within the product without erasing their original definitions. Entity resolution connects those records to stable product identities. The export then assembles the views needed by the application.
The current exporter assembles the canonical dataset in memory from stored snapshots. Persisted canonical Parquet tables are an architectural direction, but they are not part of the implemented path. The distinction matters when describing reproducibility: source snapshots and repeatable exports exist today; a fully persisted analytical data platform does not.
Acquisition and export are separate operations. A deployment does not need to call the upstream APIs or expose their credentials to the browser. Contributors can also generate clearly marked synthetic fixtures to develop the frontend without acquiring the production dataset.
Let contracts protect the frontend
The TypeScript application consumes normalized view models through a shared contracts package. Zod validates those models when the application loads them. Contract versions make a changed schema explicit, and incompatible data fails validation instead of rendering a partially understood record.
This gives each side a manageable responsibility. Python understands source formats, source identity, and transformations. Next.js understands pages, interaction, and presentation. Chart components receive prepared observations and provenance rather than parsing an operator's response format.
The same boundary helps with bilingual identity. Source names and reviewed display names can be represented separately. A more readable passenger-facing label must not quietly replace the canonical source identity or change which statistic is attached to it. Missing language coverage should remain visible rather than being filled with an unreviewed translation.
For a small project, this is enough structure to support independent changes without introducing a database or an application API for every view. The contract is the agreement that keeps those changes coherent.
Keep deployment simple while respecting the data boundary
Most pages are generated ahead of time with Next.js. The comparison route is a deliberate exception: it renders on the server so an arbitrary selection of stations can use private input without publishing a reusable history matrix as a browser data file.
The generated JSON lives under the application's private .data directory. Production builds restore an explicitly selected export from private storage and verify the pinned artifact's size and digest before using it. The project includes guards against placing the export in public, where it would be served directly.
This is an architectural consequence of source-specific publication terms. Permission to display statistics does not automatically authorize distributing the source dataset as a convenient JSON download. The boundary concerns reusable exports; it does not imply that values displayed on a public page are secret.
A checksum establishes that an artifact contains the selected bytes. It cannot establish that those bytes still satisfy a later data-review decision. A review of JR East series, for example, led to complete-series withholding rules and a corrected candidate export. Promoting a reviewed artifact and checking the resulting deployment are separate steps; accepting an old checksum alone would miss that change in meaning.
Make the explanation part of the visualization
The frontend uses Apache ECharts for standard charts, with D3 reserved for custom route-spine and network work. The more consequential choice is what accompanies the marks: metric definitions, reference periods, source versions, attribution, and missing-data notes.
A route spine can help a reader understand station order without implying a geographically accurate map. A historical chart can show available observations without drawing through missing years. A comparison can place incompatible groups separately instead of manufacturing one overall winner.
Charts also need ways to communicate beyond color and pointer interaction. Table or text alternatives, keyboard navigation, readable focus states, and responsive layouts are part of making the statistics usable. The repository's browser checks cover multiple engines and viewport sizes, but automated checks still leave room for manual accessibility review.
What this project demonstrates
Urban Rail Atlas brings together data engineering, frontend development, and editorial decisions. Its useful constraints become visible in ordinary code: an explicit metric instead of a generic passenger-count field, a validated contract instead of arbitrary JSON, and a missing-data state instead of an invented zero.
The project also shows why a public-data website benefits from restraint. A new chart is valuable when its underlying question is supported. An additional source is valuable when its meaning and publication terms are understood. A smaller set of explainable views gives readers a stronger basis for interpretation than a larger collection of numbers whose differences have been hidden.
The next improvements can deepen that explanation, strengthen review and release operations, and make existing views easier to explore. The foundation is already clear: every displayed number should retain a path back to the definition, period, and source that make it meaningful.