About the project

Where these numbers come from

This site is a fast, explorable front end for the DUV ultramarathon archive. It adds no results of its own — everything here is read from a public source, checked on the way in, and re-checked as races get corrected. This page explains that pipeline, and where it has limits.

01 · Source

Where the data comes from

Every event, result and runner on this site comes from the Deutsche Ultramarathon-Vereinigung and its public statistics archive at statistik.d-u-v.org (opens in a new tab) — the largest openly available collection of ultramarathon results anywhere, assembled and maintained over decades by its volunteers.

This project is a reader of that archive, not a second source. Every record it holds comes from DUV, apart from a small number created here and marked as such, and where the two disagree, DUV is right by definition.

02 · Scale

What is in it

Race events
119,446
Results
10,509,326
Runners
2,521,586

These are read from the database on every page load — whatever the archive holds right now, not a snapshot written into this page.

03 · Pipeline

How the data is collected

A scraper reads the public DUV pages and writes what it finds into this project's database. It runs nightly, and it re-reads recent events for a period after they take place, because results get corrected in the days and weeks after a race — a provisional finish list is not a final one.

Writes are idempotent: running the scraper twice cannot duplicate a record, and a record that has been corrected by hand is never overwritten by a later run.

04 · Limits

What the archive does and does not say

Fifty years of race reporting is not uniform, and some fields mean different things in different races. These are corrections applied as data comes in — not errors at the source. A field holding a lap count is correct on a backyard ultra; it only becomes a problem when a reader assumes it is a speed.

  • Backyard ultras record laps, not speed.In a backyard race the average-speed field holds the number of laps a runner completed. This is detected per event rather than per result, and those finishes are kept out of pace charts while still counting everywhere else.
  • Some results belong to no identified runner.The source pools these under a single catch-all record. It is kept out of the runner list but stays reachable, and it explains itself — if you have landed on a runner whose results span more than a century, that is what you are looking at.
  • Some names are placeholders.Where a finisher’s name was not recorded, the source writes a stand-in. These are identified by their structure rather than by matching a list of known spellings, and they are never rendered as a person’s name.
  • Age-graded performance needs a year of birth.Where one is missing, the age-graded field simply repeats the raw result. Rather than draw a second line identical to the first, the comparison is left off — which is why some runner pages show one performance line and others show two.

05 · Build

How it is built

Frontend
React and TanStack Start, server-rendered
API
NestJS
Database
MongoDB
Scraper
TypeScript
Components
phanes-ui — built for this project and published to npm as a standalone package
Charts
Hand-built SVG. No charting library.

06 · Project

About this project

A bachelor thesis project, built to find out what falls out of a large, messy, real-world sports dataset when you try to make it fast and honest to browse.

The interesting problems turned out not to be the charts. They were the ones in the section above.