Capstone dossier · one commodity, no model, the same answer
Author
Dei Martinez Elurbe
Published
July 2, 2026
Dossier, not the essay. Backstage layer for technical readers; the capstone essay is the general-audience artifact. Corpus: Snapshot · June 2026, ~900 priced venues, roughly 4 in 10 of Amsterdam’s ~2,028 restaurants. No venue is named.
The verdict
The bitterbal is Amsterdam’s accidental gift to price measurement: a genuine commodity. Most cafés buy near-identical product from the same handful of suppliers, portion it in plates of six or eight, and price it. Hold the product constant like that and price differences become pure environment; that is the same trick The Economist plays with the Big Mac, and it is why this is an index, not a listicle.
Three results. The city’s going rate is €1.33 a piece (138 venues, vanilla bitterballen only). The tourist gradient is real and agrees with the hedonic model (dossier D5): price per piece correlates with tourism exposure at ρ = +0.39 (a Spearman rank correlation), and the tercile ladder runs €1.22 → €1.33 → €1.37. In practical currency: a plate of eight costs about €9.75 in the least-touristed third of the city and €10.93 in the most-touristed. The €1.20 gap is one bitterbal: order eight in the centre, pay for nine. And one finding we did not expect to be this loud: the spread. An identical supplier-made product ranges from €0.68 to €2.67 per piece, a 4× ratio on a good the textbook says competition should grind to one price.
The triangulation is the point of the exercise: a 35,000-dish Bayesian regression and a one-product price check with no model in it point the same way. A finding that survives both is very hard to dismiss as a modelling artifact.
Why a bitterbal, specifically
Every confounder the hedonic model works hard to control, the bitterbal removes by its nature. No chef’s interpretation (it arrives frozen from Van Dobben, Kwekkeboom, or the wholesaler). No portion mystery (menus state the count: 6 stuks, 8 stuks). No menu-composition channel (it is one item, not a menu). What remains when you compare it across the city is location and venue, nothing else. The discipline that keeps it honest is vanilla-only: the headline excludes every truffle, oxtail, vegan, and “ambachtelijk” variant (those are tagged and tracked separately: 62 vegetarian-variant items at 53 venues, plus a handful of cheese and seafood specials). A commodity comparison only works on the commodity.
The method, in five lines
String-match bitterballen in the parsed menus; tag variants (vegan, homemade, specialty) and set them aside; keep vanilla items with a stated portion count; divide price by count to get price per piece; compare. The only statistics involved are a median and a rank correlation. That poverty of machinery is deliberate: this instrument’s job is to check the big model from outside the big model.
What it found
Show the code
suppressPackageStartupMessages({library(dplyr); library(ggplot2)})b <- readr::read_csv("../../data/processed/m3_bitterballen_venues.csv",show_col_types =FALSE)ggplot(b, aes(ppp)) +geom_histogram(binwidth = .1, fill ="#d9a08a", colour ="white", boundary =0) +geom_vline(xintercept =median(b$ppp), colour ="#b5623f") +annotate("text", x =median(b$ppp), y =Inf, label ="median €1.33",hjust =-0.1, vjust =1.5, size =3.2, colour ="#b5623f") +labs(x ="Price per vanilla bitterbal (€)", y ="venues") +theme_minimal()
Figure 1: The law-of-one-price violation: price per vanilla bitterbal across 138 venues. An identical supplier-made product spans €0.68 to €2.67, a 4× ratio; the city median is €1.33.
The index, base 100 at the city median (boroughs with few index venues are indicative only; 32 venues lack a borough label in this artifact and appear only in the venue-level analysis):
Table 1: The Bitterballen Index by borough: median price per piece, indexed to the city median (€1.33 = 100).
stadsdeel
venues
median €/piece
index
Centrum
40
€1.33
100
Oost
9
€1.33
100
Zuid
26
€1.33
100
West
12
€1.22
91
Zuidoost
4
€1.18
88
Noord
10
€1.12
84
Weesp
1
€1.08
81
Nieuw-West
4
€1.02
77
And the agreement with the hedonic model, the reason this dossier exists:
Figure 2: The triangulation: price per bitterbal against tourism exposure at venue level (123 venues with a tourism score), tercile medians as diamonds. Spearman ρ = +0.39, the same direction and comparable size as the hedonic tourist tax.
Because the extraction is one script, the index re-runs on one command against any future snapshot: the Bitterballen Monitor is free to operate, should the corpus ever be re-captured.
The stress-test battery
The threat
The test
The verdict
Stale numbers shipped as fresh
user-caught staleness: the first index ran on v1-era data
re-run on the canonical corpus: 107 → 138 venues, median €1.33 unchanged, ρ strengthened +0.376 → +0.389 (bitterballen live exactly in the café segment the re-capture most improved)
Variants contaminate the commodity claim
variant tagging + vanilla-only headline
vegan/specialty/cheese tracked separately; headline is beef-ragout vanilla only
Focal-price quantisation misleads
borough medians snap to €1.33
disclosed; the venue-level scatter is the primary display, the table is secondary
Per-piece arithmetic errors
portion counts read from menu text (“8 stuks”), unnormalizable items excluded
only stated-count items enter; price per piece is a division, not an estimate
Small-n boroughs over-read
venue counts printed in the table
Zuidoost/Nieuw-West (4 each) and Weesp (1) flagged indicative
Agreement is coincidence
direction and approximate magnitude vs D5, plus the tercile ladder monotone €1.22/€1.33/€1.37
two independent instruments, one story
What this is, and what it isn’t
Café and bar territory, not fine dining. Bitterballen live where borrel lives; the index covers that segment and says nothing about tasting menus.
One product, one month, n = 138. The index is a cross-check and a story, not a census.
Menu price, not paid price, as everywhere in this project.
The 4× spread is dispersion, not necessarily gouging. Part is venue class (a beach-club bitterbal is an experience surcharge), part location, part genuine pricing noise; the index measures the spread, it does not adjudicate it.
Lineage
Method + prose source:docs/m3_blog_draft.md (§The Bitterballen Index; much of its copy is capstone-ready), PROJECT.md decision #7/#13 (scope + vanilla-only discipline).
Journal trail:2026-06-04_002 (built, ρ=+0.376), 2026-06-10_001/_002 (the staleness catch + canonical re-run), 2026-06-11_001 (the index framing: “how is this not just an average price per area?”).
Sources: restaurant websites (menus), OpenStreetMap (venue universe), Open Amsterdam / CBS (tourism). All open data. Snapshot · June 2026.