Official statistics in R
Twenty-Four Years, Half a Point
How Europeans travel, from official data.
- Role
- Project author
- Classification
- Reproducible experiment
Problem
Typed findings go stale when data updates.
How it was solved
Every number is recomputed from a pinned dataset.
- Eurostat
- Pinned hash
- Recompute
- Report
Result
+0.5
points of car share in 24 years
- 200082.0%
- 202482.5%
Built with
- R
- Quarto
- ggplot2
- Docker
Reproducible experiment / Official statistics
The share of inland passenger distance taken by car in the EU was 82.0 per cent in 2000. In 2024 it is 82.5. The finding is the absence of a finding, which is harder to publish than a trend.
What 24 years moved+0.5 points
The chain of commitments behind a null result
This diagram in words
- Committed response source — Retrieved once and committed, so the report has a fixed input.R
- Pinned by hash gate — Recomputed in CI. An upstream edit fails rather than slips through.GitHub Actions
- JSON-stat decoded process — Dimension indices unpacked by hand rather than through a helper.jsonlite
- Year chosen by coverage gate — By which countries report it, not by which is most recent.
- One render pass process — Prose and figures from the same objects, so they cannot disagree.Quarto · ggplot2
- Half a point artifact — 82.0% to 82.5% across twenty-four years. The absence is the result.
Read the full background
Problem
Why this work exists
A report that types its findings into its prose can be correct on the day it is written and wrong the next time the data is refreshed, with nothing to catch the gap. And the finding here is an absence: the share of inland passenger distance taken by car in the EU has barely moved since 2000, which is harder to publish honestly than a trend, because a flat line read on its own says nothing happened rather than this is the result.
Contribution
What I can claim
Decoded the JSON-stat response by hand rather than using the Eurostat package, committed the raw response with a pinned SHA-256 that CI recomputes, and wrote every figure in the prose as a computed value rather than a literal, so the text and the charts are produced from one pass over the same data.
Method
A report that can be re-run against its own source
Descriptive statistics are the easiest thing to publish and the hardest to trust later, because the numbers in the prose and the numbers in the data drift apart quietly.
66.9 per cent in Hungary to 97.8 in Montenegro, with Germany marked at 84.1. The axis starts at 60, not zero.
Retrieve
The source table is fetched and the response itself committed, so what the report was written against is in the repository rather than on somebody else's server.
Pin
It is fixed by checksum, recomputed in continuous integration.
Decode
The response format is unpacked by hand rather than through a convenience wrapper, which keeps the transformation from source to table readable and reviewable.
Select
The comparison year is chosen by coverage rather than by recency, because the most recent year is the one where the fewest countries have reported.
Render
Prose and figures are produced in the same pass from the same table, so a sentence and the chart beside it cannot disagree.
What this does not show
This is descriptive. It measures shares and says nothing about why any of them moved or stayed put, and the cross-country differences it shows are differences in geography, settlement and policy that it makes no attempt to separate.
How it was built
System
Workflow and decisions
One Quarto document is the whole pipeline: it reads the committed response, decodes it, derives every figure, and emits the prose and the charts together. There is no step in which a number is copied from one artefact into another.
- 1Retrieve the table and commit the response
- 2Pin it by SHA-256, recomputed in CI
- 3Decode JSON-stat by hand
- 4Select the comparison year by coverage
- 5Render prose and figures from one pass
- R
- Quarto
- ggplot2
- jsonlite
- Docker
- GitHub Actions
Evidence
What is actually versioned
The EU27_2020 car share was 82 per cent in 2000 and 82.5 in 2024. Whatever has changed in European transport policy over that period, it has not changed what share of distance is driven.
In 2024 the spread across countries runs from 66.9 per cent in Hungary to 97.8 in Montenegro. Germany is 84.1, ranking 12 of 34.
Gained share between each country's own first and last reported year. The largest gain is Switzerland, up 7.3 points.
Eurostat tran_hv_psmod, retrieved 2026-10-18 and committed with its SHA-256. CI recomputes the digest and fails if it stops matching the README and the provenance file, so the snapshot, the prose and the figures cannot drift apart.
How the work is checked
Quality controls
How the work is checked
- Every figure in the prose is computed at render time; none is typed in
- The raw API response is committed, so the report renders offline and reproducibly
- The comparison year is selected by reporting coverage rather than by taking the latest year present
- JSON-stat is decoded by hand, which is the part of the work worth showing, so the Eurostat package is deliberately not a dependency
- The output is one self-contained HTML file with the figures and CSS inlined
Limitations
Where the evidence stops
- A descriptive report. It measures shares and says nothing about why any of them moved or did not
- Modal split is a share of distance, so a country can hold its car share steady while total distance travelled rises or falls underneath it
- Coverage is uneven across countries and years, which is why the single-year comparisons use one year chosen by coverage rather than the latest available
- One snapshot of one table, pinned on a date. It is reproducible rather than live
What this changed in my practice
What this changed in my practice
An absence is a finding, but it only reads as one next to something that did change. The flat EU line means nothing on its own and means a great deal beside a 30.9 point spread between two countries in a single year.