Félix Boittinprisme-donnees.frFR
Prisme case study, part 2 of 2

How it’s built

A warehouse that rebuilds from the sources on a single machine, a static site generated from it, and rules that stop a wrong join or an unsupported sentence before it ships.

Data
2,717,481 rows in the indicator cube: 52 indicators from 14 catalogued sources, 34,858 communes, 1960 to 2025
Code
37,000 lines of Python, 3,400 of SQL in 60 dbt models, and 9,500 of JavaScript for the site
Tests
462 Python tests, 155 tests of the site’s modules and 34 data tests in dbt, run on every pull request
Site
454 addresses in the sitemap, each built ahead of time and served as a static file
Decisions
16 architecture decision records

Counted on the main branch on 21 October 2026.

The big picture

No server to look after. GitHub Actions rebuilds everything from the sources, and Vercel serves the result.

Public sources
Insee: census, Filosofi, civil registration, commune codes
DGFiP property sales (DVF), Drees access to GPs
Eurostat, World Bank
IGN and GISCO boundaries
Warehouse, DuckDB and dbt
raw: everything as text, as published
staging: types and meaning
intermediate: one geography, through the commune crosswalk
marts: the indicator cube and the tables the site reads
Site, Observable Framework
Analyses and place pages computed at build time
Explorer, workshop and comparison, in the browser
Parquet and CSV downloads, a SQL console
Built and checked withGitHub ActionsVerceluvpytestnode:testPlaywrightDuckDB-WASM
The architecture at a glance, from the code on 21 October 2026.

A figure’s journey

Six steps between a file published by Insee or Eurostat and a sentence on a page.

  1. FetchEvery address lives in a catalogue file, with its licence and its vintages. Downloads are cached and logged, so a rerun fetches nothing twice.
  2. LoadFiles go into DuckDB as text, exactly as published. Typing and interpretation come later, where they can be tested.
  3. HarmoniseEach source enters through the commune crosswalk, rebuilt from every official code change since 2003 and checked against Insee’s own table, and leaves in one reference geography.
  4. PublishThe cube and the site tables carry a status for every row: published, hidden, absent, or too few sales.
  5. WriteAn analysis computes, then asserts. Every number in the text comes from the calculation, and each claim is checked against the data: one that fails stops the build.
  6. ShipAfter a green CI on main, the site is built on the real data, tested end to end in a browser, then sent to Vercel already built.

Rules the code can’t break

In data, a wrong join raises no exception. So these rules are written down, and most of them are tested.

One geography

Communes merge every year. Every French figure is moved into one reference year of the official geography before it leaves the warehouse. EU regions use NUTS 2024: a code from another version is dropped, never converted.

int_geo__passage_communes · ADR 0005

Add only what adds up

A population can be split between communes by weight. A median or a rate only crosses a one-to-one link, a commune that was renamed or recoded, never one that merged.

poids · nature

A gap is never a zero

A value can be hidden for confidentiality, not published at all, or based on too few observations. Each case has its own status, and none is ever filled in.

statut

No single property sale

DVF addresses and plots never leave the raw layer. No price is shown below 20 sales, and no individual sale appears in a table, an export, a test file or a page.

seuil_ventes_min = 20

A year of data isn’t a year of geography

Filosofi 2023 comes in the 2026 commune codes, Eurostat’s 2026 data in NUTS 2024. Each source declares its geography year in the catalogue.

millesime_geo

Every source has its licence

Each source has a sheet with its licence, attribution and terms, and every figure cites it. Not all are open: GISCO boundaries are for non-commercial use only.

catalogue/<source>.yml

Every figure is computed

Pages contain no hard-coded numbers. A sentence only says what the calculation shows: the direction, the strength, the interval.

ADR 0004 · affirmer

Questions without a language model

The explorer reads questions with a dictionary drawn from the indicator register and answers with templates. No API bill, no invented figure, and the same answer every time.

questions.js

Statistics, with their limits

A public method page sets out how each claim is tested.

  • Correlations with a 95% confidence interval, and Spearman’s when ranks matter more than values.
  • Country fixed effects, to tell what happens between countries from what happens within them.
  • A correction for multiple tests when many indicators are compared at once.
  • Spatial autocorrelation, with Moran’s I: neighbouring regions aren’t independent observations.
  • Robustness checks: does the result hold without outliers, in another year, on a log scale?
What Prisme doesn’t claim

A link between territories says nothing about the people who live in them: that is the ecological fallacy, and a guide on the blog explains it. Recent years are often provisional, and every figure carries its vintage.

For search engines and AI assistants

A reference site has to be found, and quoted correctly.

  • llms.txt and llms-full.txt: a map of the site, and the full text of every analysis, guide and method page in one file.
  • Structured data on every page: Article with the key points as its abstract, Dataset and DataDownload for the cube, FAQPage, DefinedTermSet for the glossary.
  • A social image per article, drawn at build time from its title and first chart.
  • An English page for the data, with hreflang, so the cube can be found outside France.
  • An Atom feed for the analyses, and a sitemap of 454 addresses.