Capstone dossier · what every dish-type costs in Amsterdam, with honest ranges
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.
The verdict
Amsterdam has a going rate for everything, and now it is written down. The Dish Atlas prices 200 dish-type cells (147 at full confidence, 53 stabilised by pooling), covering 96% of 32,651 clean dishes: each cell gets a typical price (the empirical median) and a 90% range (where 90% of that dish-type’s prices actually fall). Carpaccio as a starter: €15, range €11.50 to €23. Friet: €5.80. Bitterballen: €7.20. A dorade main: €24.50. No venue is named anywhere in the atlas; it prices dish-types, never restaurants.
Two design decisions carry the credibility. Typical = the median, not the mean, because on skewed price distributions the model’s own pooled mean misleads by 27 to 38% (the cheese-board cell reads €10.50 by geometric mean and €14.50 by median; the median is what a diner actually encounters). And the range is the partially-pooled quantity: big cells show their own percentiles, thin cells borrow spread from their parent cells, so a 20-dish cell cannot fake a precise range it has not earned.
Figure 1: The going rates: typical price (dot) and 90% range (line) for twelve recognizable cells. Each label is the cell’s exemplar, the most common cleaned dish name inside it; it stands for the whole cell, not for one dish.
Why the obvious approach lies, twice
The first lie is averaging across products. “The average dish price in Amsterdam” mixes a €2.50 side with a €49 Wagyu; it measures menu composition, nothing else. The coordinate system (dossier D1) fixes that: prices are only ever summarised within a cell, where dishes are the same kind of thing.
The second lie is subtler: the mean lies even within a cell. Dish prices are right-skewed (a long tail of expensive versions), and the model is fitted on log-prices, whose natural “average” back-transforms to the geometric mean. On skewed cells that number drifts far from what a diner actually meets. The skew check that caught this compared the model’s pooled mean against the empirical median across all confident cells: gaps up to 27 to 38%. Verdict: the model keeps the mean internally for validation, and the atlas displays the median. The variance components confirm the median is reliable even at 15 dishes per cell; what is unreliable in small cells is the tails, and that is exactly where pooling is aimed.
The model: pooling for the tails
The fit is a nested random-effects model on log price:
The nesting is the point: a cell sits inside a role:protein:method family, which sits inside a role:protein family. A thin cell (say, 18 lamb stews) does not have to estimate its price spread alone; it borrows from its parents in proportion to how little data it has. The displayed 90% range blends the cell’s own 5th-to-95th percentile span with the model’s pooled predictive spread, weighted by cell size (weight = n/(n+40)). Big cells are effectively empirical; thin cells are honestly wider than their own data would naively suggest. The venue random effect keeps one expensive restaurant’s menu from imprinting on a cell.
Escalating to a fully Bayesian fit (brms) was considered and declined, on the light-first rule: with 31,000+ dishes the residual spread is pinned, the median would not move, and the extra machinery would change nothing a reader can see. The lme4 empirical-Bayes fit runs in four seconds and reproduces to the cent.
What it found, beyond the lookup
Sure bets and gambles. Range width is information. The most price-predictable dishes in the city are the cuisine-format ones: pizza (a diavola’s 90% range spans only ±35% of its €17.50 typical) and curries. The widest are the catch-all and sharing formats: a borrelplank runs €9.80 to €49.70 around an €18.50 typical, and a sharing-size kipsaté anywhere from €12.70 to €60. The practical reading for a diner: you can trust a pizza price sight-unseen; never assume a sharing platter.
Why widths differ this much: the quality ceiling. The model cannot see undisclosed portion size or ingredient quality, and dish names differ enormously in how much they pin those down. A borrelplank has no quality ceiling: the same menu line covers kaasblokjes with supermarket worst and an ibérico charcuterie board, and the price honestly spans that distance. A pizza, even loaded with the most premium ingredients, still has a pizza ceiling: the format bounds the plate, and the city carries a reference price for it. Range width is that invisibility made visible. Wide cells are cells where the name does not pin the product; tight cells are cells where it does. (Seen through dossier D4’s decomposition: the ranges are its non-dish 41%, venue character plus item noise, displayed cell by cell; D4’s “ladder and the rungs” section shows the two dossiers are one set of numbers.)
Figure 2: Sure bets vs gambles: the narrowest and widest 90% ranges relative to the typical price, among confident cells with at least 100 dishes. Cuisine-format dishes price tightly; sharing and catch-all formats do not.
The atlas is a lookup table. The ten biggest cells, as they ship:
Table 1: The ten biggest atlas cells. Typical = empirical median; the range is the 90% partially-pooled interval.
cell
exemplar
n
venues
typical
90% range
main · meat · none · plate
kalfslever
2097
449
€21.70
€11.0 – €37.1
main · meat · none · sandwich
carpaccio
1998
394
€12.50
€5.5 – €21.6
sweet · any · none · other
affogato
1916
518
€7.70
€3.0 – €13.7
main · fish · none · plate
dorade
969
339
€24.50
€12.0 – €38.7
side · vegan · none · other
friet
937
421
€5.80
€2.5 – €9.6
main · meat · grilled · plate
bife de chorizo gr
912
309
€25.50
€14.0 – €48.3
main · meat · baked · pizza
diavola
905
112
€17.50
€12.3 – €22.4
sweet · any · baked · other
cheesecake
751
324
€6.00
€3.0 – €12.9
main · vegetarian · baked · pizza
margherita
719
116
€16.00
€9.9 – €22.3
main · vegetarian · none · sandwich
mozzarella di bufala
650
276
€9.50
€4.9 – €18.0
The stress-test battery
The threat
The test
The verdict
The displayed center misleads on skewed cells
geomean vs median across all confident cells
gaps up to 27–38% (cheese board €10.50 vs €14.50): median displayed, geomean demoted to a validation column
Package lines masquerade as dish-types
cell-contents audit, triggered by a band-width sort on the public atlas page
four package coordinates (444 items: promo bundles, prix-fixe lines, high teas, desserts-for-two) quarantined whole to dish_atlas_excluded.csv; survivors’ typicals unmoved, range endpoints shift ≤ €0.30
The pooling constant is arbitrary
audit re-ran with alternative K_W values
ranges move 3–4%: not sensitive
The pipeline drifted from the canonical corpus
adversarial data-layer audit, reproduced from scratch
lineage faithful: the atlas dish table is a bit-identical subset of canonical items.csv (set-difference zero both ways)
Ranges are fantasy
hand-validation of 10 cells against real menu dishes
exemplars faithful; ranges correctly exclude real extremes (a €49 Wagyu, a €78.50 T-bone, a €67.50 Dover sole all sit outside their cells’ 90% bands, as 5%-tails should)
Dessert prices biased by a silent filter
audit reproduction
985 sweets (34%) were being dropped, skewing sweet cells ~15–20% low; fixed, desserts recovered (the cheesecake cell grew 436 → 751 dishes)
Per-piece prices contaminate cells
mechanism-matched hygiene guard (D1)
main · fish · raw · sushi median €4.40 → €16.50
A heavier model would change the answer
lme4 vs brms escalation decision
declined on light-first grounds: 31k dishes pin the spread, the median cannot move; lme4 reproduces to the cent in 4 seconds
What this is, and what it isn’t
No venue is named, structurally. The atlas aggregates over venues by construction; the per-venue claims that sank the earlier Value Atlas cannot be expressed in it.
The exemplar is a label, not a promise. “Butter chicken” is the most common name inside the meat-curry cell, but actual butter chickens cluster in the upper part of that cell’s range. Read “meat curry, e.g. butter chicken”, never “butter chicken costs €20.80”. (This framing is a known presentation task carried forward from the audit.)
Within-cell quality and portion are invisible. A €12 and a €19 carpaccio may differ in provenance and grams; the atlas sees neither. It prices the type, honestly wide.
Menu price, not paid price. No service, no specials, no portion inflation observed.
A snapshot. One city, one month, ~4 in 10 restaurants. The going rates are solid within that scope and dated by construction.
Code:R/50_dish_atlas_hygiene.R (cells + hygiene), R/51_dish_atlas_fit.R (the nested lme4 fit, the median decision, the K_W=40 blend).
Data:data/processed/dish_atlas.csv (200 cells), dish_atlas_dishes.csv (32,651 dishes), dish_atlas_excluded.csv (the four quarantined package coordinates, with reasons).
Journal trail:2026-06-30_001 (the atlas crystallises), 2026-06-30_002 (the fit + the skew catch + 10-cell hand-validation), 2026-06-30_005/_006 (audit + fixes), 2026-07-07_013 (the package-cell curation).
Sources: restaurant websites (menus), OpenStreetMap (venue universe). All open data. Snapshot · June 2026.