{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "5cfdcf48",
   "metadata": {},
   "source": [
    "# MKT-004 — Conjunction prediction engine for the 2026 World Cup\n",
    "\n",
    "Reproduces every numerical claim and figure in `paper.md` from the committed\n",
    "datasets in `data/`. The datasets are outputs of the world-cup engine\n",
    "(github.com/away-de-petropolis/World-cup) at commit `63e6d53`, the state after\n",
    "the recency-weighted scorer model was integrated and re-gated. The 2026 tables\n",
    "come from a single frozen run: snapshot `c7f3f5afa805`, seed `20260611`,\n",
    "50,000 simulations, calibration gate GREEN.\n",
    "\n",
    "Run top to bottom with a clean kernel: `pip install pandas` then\n",
    "`jupyter nbconvert --to notebook --execute notebook.ipynb`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "1e1d52ad",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-03T14:54:21.758429Z",
     "iopub.status.busy": "2026-06-03T14:54:21.758333Z",
     "iopub.status.idle": "2026-06-03T14:54:22.543629Z",
     "shell.execute_reply": "2026-06-03T14:54:22.543178Z"
    },
    "tags": [
     "setup"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "pandas 3.0.1\n"
     ]
    }
   ],
   "source": [
    "import json\n",
    "from pathlib import Path\n",
    "import pandas as pd\n",
    "\n",
    "DATA = Path('data')\n",
    "FIGS = Path('figures')\n",
    "load = lambda name: pd.read_csv(DATA / name)\n",
    "print('pandas', pd.__version__)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6887230b",
   "metadata": {},
   "source": [
    "## Match-outcome calibration (A1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "c5a35e01",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-03T14:54:22.544829Z",
     "iopub.status.busy": "2026-06-03T14:54:22.544722Z",
     "iopub.status.idle": "2026-06-03T14:54:22.565504Z",
     "shell.execute_reply": "2026-06-03T14:54:22.565085Z"
    },
    "tags": [
     "match-calibration"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "     engine  brier  log_loss    ece    n\n",
      "        Elo 0.5241    0.8936 0.0555 1767\n",
      "Dixon-Coles 0.5077    0.8673 0.0371 1767\n",
      "per-match winner: Dixon-Coles\n"
     ]
    }
   ],
   "source": [
    "mc = load('match_calibration.csv')\n",
    "print(mc.to_string(index=False))\n",
    "best = mc.loc[mc['log_loss'].idxmin(), 'engine']\n",
    "assert best == 'Dixon-Coles', best\n",
    "print('per-match winner:', best)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "518126eb",
   "metadata": {},
   "source": [
    "## Beat-the-closing-line on history (A4)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "2b9955ae",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-03T14:54:22.566654Z",
     "iopub.status.busy": "2026-06-03T14:54:22.566557Z",
     "iopub.status.idle": "2026-06-03T14:54:22.570012Z",
     "shell.execute_reply": "2026-06-03T14:54:22.569596Z"
    },
    "tags": [
     "beat-the-line"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "mean model log-loss : 1.897\n",
      "mean market log-loss: 2.199\n",
      "model beats market  : 3 of 4\n"
     ]
    }
   ],
   "source": [
    "ch = load('champion_vs_market_history.csv')\n",
    "mean_model = round(ch['model_log_loss'].mean(), 3)\n",
    "mean_market = round(ch['market_log_loss'].mean(), 3)\n",
    "beats = int(ch['model_beats_market'].sum())\n",
    "print('mean model log-loss :', mean_model)\n",
    "print('mean market log-loss:', mean_market)\n",
    "print('model beats market  :', beats, 'of', len(ch))\n",
    "assert mean_model == 1.897 and mean_market == 2.199 and beats == 3"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "91d6a86c",
   "metadata": {},
   "source": [
    "## Engine-selection inversion (A1 vs the contest target)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "d523d620",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-03T14:54:22.571149Z",
     "iopub.status.busy": "2026-06-03T14:54:22.571079Z",
     "iopub.status.idle": "2026-06-03T14:54:22.587650Z",
     "shell.execute_reply": "2026-06-03T14:54:22.587242Z"
    },
    "tags": [
     "engine-inversion"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "scope        champion  per-match\n",
      "model                           \n",
      "Dixon-Coles     2.504      0.867\n",
      "Elo             1.889      0.894\n",
      "Market          2.199        NaN\n"
     ]
    }
   ],
   "source": [
    "el = load('engine_logloss.csv')\n",
    "pivot = el.pivot_table(index='model', columns='scope', values='log_loss')\n",
    "print(pivot.to_string())\n",
    "assert pivot.loc['Dixon-Coles', 'per-match'] < pivot.loc['Elo', 'per-match']\n",
    "assert pivot.loc['Elo', 'champion'] < pivot.loc['Dixon-Coles', 'champion']\n",
    "assert pivot.loc['Elo', 'champion'] < pivot.loc['Market', 'champion']"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "314d597b",
   "metadata": {},
   "source": [
    "## Goal-count band (A2)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "24fd2e9a",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-03T14:54:22.588678Z",
     "iopub.status.busy": "2026-06-03T14:54:22.588610Z",
     "iopub.status.idle": "2026-06-03T14:54:22.591830Z",
     "shell.execute_reply": "2026-06-03T14:54:22.591425Z"
    },
    "tags": [
     "q4-band"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      " year      top_scorer  scorer_rank  band_5_8_mass  realized_count_mass\n",
      " 2010   Thomas Muller          NaN           0.51                0.306\n",
      " 2014 James Rodriguez        115.0           0.52                0.134\n",
      " 2018      Harry Kane         24.0           0.63                0.182\n",
      " 2022   Kylian Mbappe          9.0           0.84                0.105\n",
      "mean 5-8 band mass: 0.625\n"
     ]
    }
   ],
   "source": [
    "q4 = load('q4_goal_count_band.csv')\n",
    "mean_band = round(q4['band_5_8_mass'].mean(), 3)\n",
    "print(q4.to_string(index=False))\n",
    "print('mean 5-8 band mass:', mean_band)\n",
    "assert mean_band == 0.625"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3041d7ae",
   "metadata": {},
   "source": [
    "## Player-of-the-Tournament calibration (A3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "89d26eaf",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-03T14:54:22.592816Z",
     "iopub.status.busy": "2026-06-03T14:54:22.592752Z",
     "iopub.status.idle": "2026-06-03T14:54:22.595876Z",
     "shell.execute_reply": "2026-06-03T14:54:22.595498Z"
    },
    "tags": [
     "pott"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      " year  golden_ball  model_rank  model_p\n",
      " 2010 Diego Forlan           5    0.076\n",
      " 2014 Lionel Messi           1    0.380\n",
      " 2018  Luka Modric           5    0.047\n",
      " 2022 Lionel Messi           1    0.483\n",
      "Golden Ball in model top-5: 4 of 4 | at rank 1: 2\n"
     ]
    }
   ],
   "source": [
    "pt = load('pott_calibration.csv')\n",
    "top5 = int((pt['model_rank'] <= 5).sum())\n",
    "top1 = int((pt['model_rank'] == 1).sum())\n",
    "print(pt.to_string(index=False))\n",
    "print('Golden Ball in model top-5:', top5, 'of', len(pt), '| at rank 1:', top1)\n",
    "assert top5 == 4 and top1 == 2"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "159c4542",
   "metadata": {},
   "source": [
    "## Scorer identity (Q3): calibration and the honest concentration finding"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "6f10965d",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-03T14:54:22.596817Z",
     "iopub.status.busy": "2026-06-03T14:54:22.596755Z",
     "iopub.status.idle": "2026-06-03T14:54:22.601849Z",
     "shell.execute_reply": "2026-06-03T14:54:22.601457Z"
    },
    "tags": [
     "scorer-recency"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "       arm  half_life_years  mean_scorer_log_loss  adopted\n",
      "  baseline              NaN                12.656    False\n",
      "recency-1y              1.0                12.051     True\n",
      "recency-2y              2.0                12.189    False\n",
      "recency-3y              3.0                12.284    False\n",
      " year     golden_boot  base_p  recency_p\n",
      " 2010   Thomas Muller  0.0000     0.0000\n",
      " 2014 James Rodriguez  0.0009     0.0021\n",
      " 2018      Harry Kane  0.0066     0.0178\n",
      " 2022   Kylian Mbappe  0.0168     0.0316\n"
     ]
    }
   ],
   "source": [
    "rec = load('scorer_recency.csv')\n",
    "best = rec.loc[rec['mean_scorer_log_loss'].idxmin()]\n",
    "print(rec.to_string(index=False))\n",
    "assert best['arm'] == 'recency-1y' and bool(best['adopted']) is True\n",
    "py = load('scorer_recency_peryear.csv')\n",
    "print(py.to_string(index=False))\n",
    "# Recency never hurts; it nearly triples the mass on the realized winner in 2018 and nearly doubles it in 2022.\n",
    "assert (py['recency_p'] >= py['base_p']).all()\n",
    "assert py.set_index('year').loc[2018, 'recency_p'] == 0.0178"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "be5167ac",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-03T14:54:22.602812Z",
     "iopub.status.busy": "2026-06-03T14:54:22.602732Z",
     "iopub.status.idle": "2026-06-03T14:54:22.605950Z",
     "shell.execute_reply": "2026-06-03T14:54:22.605586Z"
    },
    "tags": [
     "scorer-concentration"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "                baseline  post_recency\n",
      "metric                                \n",
      "modal_share       0.2850        0.2380\n",
      "top5_share        0.6670        0.5560\n",
      "effective_n       7.8000       11.4000\n",
      "joint_argmax_p    0.0102        0.0095\n"
     ]
    }
   ],
   "source": [
    "sc = load('scorer_concentration.csv').set_index('metric')\n",
    "print(sc.to_string())\n",
    "# The honest finding: integrating recency FLATTENED the marginal and LOWERED P(win).\n",
    "# Modal share down, effective field up, top-5 down, joint-argmax probability down.\n",
    "assert sc.loc['modal_share', 'baseline'] == 0.285 and sc.loc['modal_share', 'post_recency'] == 0.238\n",
    "assert sc.loc['effective_n', 'post_recency'] > sc.loc['effective_n', 'baseline']\n",
    "assert sc.loc['joint_argmax_p', 'post_recency'] < sc.loc['joint_argmax_p', 'baseline']"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "60759bda",
   "metadata": {},
   "source": [
    "## The joint pick and its honest probability"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "b42b73a3",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-03T14:54:22.606932Z",
     "iopub.status.busy": "2026-06-03T14:54:22.606872Z",
     "iopub.status.idle": "2026-06-03T14:54:22.610270Z",
     "shell.execute_reply": "2026-06-03T14:54:22.609891Z"
    },
    "tags": [
     "joint-ticket"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "selected: Spain / Erling Haaland / 7 p = 0.0094\n",
      "strict argmax goals: 8 p = 0.0095\n",
      "joint P(selected): 0.94 percent | edge 12.5 x\n"
     ]
    }
   ],
   "source": [
    "jt = load('joint_ticket.csv')\n",
    "pick = jt.loc[jt['rank'] == 1].iloc[0]\n",
    "p = float(pick['joint_p'])\n",
    "argmax = jt.loc[jt['joint_p'].idxmax()]\n",
    "casual = 0.00075   # midpoint of the 0.05 to 0.10 percent casual-entry band\n",
    "print('selected:', pick['winner'], '/', pick['top_scorer'], '/', int(pick['exact_goals']), 'p =', p)\n",
    "print('strict argmax goals:', int(argmax['exact_goals']), 'p =', float(argmax['joint_p']))\n",
    "print('joint P(selected):', round(p * 100, 2), 'percent | edge', round(p / casual, 1), 'x')\n",
    "# The pick tilts off the strict argmax (goals=8) to the less public count (goals=7).\n",
    "assert int(pick['exact_goals']) == 7 and round(p * 100, 2) == 0.94\n",
    "assert int(argmax['exact_goals']) == 8"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ea8b10b1",
   "metadata": {},
   "source": [
    "## Regenerate the figure specs from the datasets"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "18bc8fb7",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-03T14:54:22.611224Z",
     "iopub.status.busy": "2026-06-03T14:54:22.611165Z",
     "iopub.status.idle": "2026-06-03T14:54:22.616186Z",
     "shell.execute_reply": "2026-06-03T14:54:22.615810Z"
    },
    "tags": [
     "figure:01-champion-vs-market"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "wrote figures/01-champion-vs-market.vega.json\n"
     ]
    }
   ],
   "source": [
    "long = ch.melt(id_vars='year', value_vars=['model_log_loss', 'market_log_loss'],\n",
    "               var_name='series', value_name='log_loss')\n",
    "long['series'] = long['series'].map({'model_log_loss': 'model', 'market_log_loss': 'market'})\n",
    "long['year'] = long['year'].astype(str)\n",
    "spec = {\n",
    "  '$schema': 'https://vega.github.io/schema/vega-lite/v5.json',\n",
    "  'description': 'Champion log-loss, model versus devigged market, for the 2010 to 2022 World Cups. Lower is better; the model is lower in three of four years.',\n",
    "  'data': {'values': long.to_dict('records')},\n",
    "  'mark': 'bar',\n",
    "  'encoding': {\n",
    "    'x': {'field': 'year', 'type': 'nominal', 'title': 'World Cup'},\n",
    "    'xOffset': {'field': 'series'},\n",
    "    'y': {'field': 'log_loss', 'type': 'quantitative', 'title': 'Champion log-loss (lower is better)'},\n",
    "    'color': {'field': 'series', 'type': 'nominal', 'title': 'Forecaster'}\n",
    "  }\n",
    "}\n",
    "(FIGS / '01-champion-vs-market.vega.json').write_text(json.dumps(spec, indent=2) + '\\n')\n",
    "print('wrote figures/01-champion-vs-market.vega.json')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "45ebdee6",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-03T14:54:22.617112Z",
     "iopub.status.busy": "2026-06-03T14:54:22.617043Z",
     "iopub.status.idle": "2026-06-03T14:54:22.619605Z",
     "shell.execute_reply": "2026-06-03T14:54:22.619229Z"
    },
    "tags": [
     "figure:02-engine-logloss-inversion"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "wrote figures/02-engine-logloss-inversion.vega.json\n"
     ]
    }
   ],
   "source": [
    "spec = {\n",
    "  '$schema': 'https://vega.github.io/schema/vega-lite/v5.json',\n",
    "  'description': 'The engine-selection inversion. Dixon-Coles wins on per-match log-loss; Elo wins on mean champion log-loss over 2010 to 2022 and also beats the market.',\n",
    "  'data': {'values': el.to_dict('records')},\n",
    "  'mark': 'bar',\n",
    "  'encoding': {\n",
    "    'x': {'field': 'model', 'type': 'nominal', 'title': 'Forecaster', 'sort': ['Elo', 'Dixon-Coles', 'Market']},\n",
    "    'xOffset': {'field': 'scope'},\n",
    "    'y': {'field': 'log_loss', 'type': 'quantitative', 'title': 'Log-loss (lower is better)'},\n",
    "    'color': {'field': 'scope', 'type': 'nominal', 'title': 'Scope'}\n",
    "  }\n",
    "}\n",
    "(FIGS / '02-engine-logloss-inversion.vega.json').write_text(json.dumps(spec, indent=2) + '\\n')\n",
    "print('wrote figures/02-engine-logloss-inversion.vega.json')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "7808a734",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-03T14:54:22.620561Z",
     "iopub.status.busy": "2026-06-03T14:54:22.620496Z",
     "iopub.status.idle": "2026-06-03T14:54:22.623889Z",
     "shell.execute_reply": "2026-06-03T14:54:22.623522Z"
    },
    "tags": [
     "figure:03-q4-goal-count-band"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "wrote figures/03-q4-goal-count-band.vega.json\n"
     ]
    }
   ],
   "source": [
    "bars = q4[['year', 'band_5_8_mass']].copy()\n",
    "bars['year'] = bars['year'].astype(str)\n",
    "spec = {\n",
    "  '$schema': 'https://vega.github.io/schema/vega-lite/v5.json',\n",
    "  'description': 'Model probability that the realized top scorer finishes on five to eight goals, by tournament, against the 0.50 gate threshold.',\n",
    "  'layer': [\n",
    "    {'data': {'values': bars.to_dict('records')}, 'mark': 'bar',\n",
    "     'encoding': {'x': {'field': 'year', 'type': 'nominal', 'title': 'World Cup'},\n",
    "                  'y': {'field': 'band_5_8_mass', 'type': 'quantitative', 'title': 'Model mass on 5 to 8 goals', 'scale': {'domain': [0, 1]}}}},\n",
    "    {'data': {'values': [{'threshold': 0.5}]}, 'mark': {'type': 'rule', 'strokeDash': [4, 4]},\n",
    "     'encoding': {'y': {'field': 'threshold', 'type': 'quantitative'}}}\n",
    "  ]\n",
    "}\n",
    "(FIGS / '03-q4-goal-count-band.vega.json').write_text(json.dumps(spec, indent=2) + '\\n')\n",
    "print('wrote figures/03-q4-goal-count-band.vega.json')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "78e81707",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-03T14:54:22.624823Z",
     "iopub.status.busy": "2026-06-03T14:54:22.624762Z",
     "iopub.status.idle": "2026-06-03T14:54:22.628665Z",
     "shell.execute_reply": "2026-06-03T14:54:22.628245Z"
    },
    "tags": [
     "figure:04-model-vs-market-2026"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "wrote figures/04-model-vs-market-2026.vega.json\n"
     ]
    }
   ],
   "source": [
    "wm = load('wc2026_winner_marginal.csv')\n",
    "# Label only the teams discussed in the prose; the low-probability cluster\n",
    "# (Brazil, Germany, Netherlands, Norway, Belgium, Colombia, Japan) stays as\n",
    "# unlabelled points so the text labels do not collide.\n",
    "labelled = ['Spain', 'Argentina', 'France', 'England', 'Portugal']\n",
    "spec = {\n",
    "  '$schema': 'https://vega.github.io/schema/vega-lite/v5.json',\n",
    "  'description': 'Engine 2026 champion marginal versus the devigged Polymarket winner market for the twelve leading teams. Points above the diagonal are teams the model rates higher than the crowd.',\n",
    "  'layer': [\n",
    "    {'data': {'values': [{'d': 0}, {'d': 0.26}]}, 'mark': {'type': 'line', 'strokeDash': [4, 4], 'opacity': 0.5},\n",
    "     'encoding': {'x': {'field': 'd', 'type': 'quantitative'}, 'y': {'field': 'd', 'type': 'quantitative'}}},\n",
    "    {'data': {'values': wm.to_dict('records')},\n",
    "     'layer': [\n",
    "       {'mark': 'point', 'encoding': {'x': {'field': 'market_p', 'type': 'quantitative', 'title': 'Devigged market probability'}, 'y': {'field': 'model_p', 'type': 'quantitative', 'title': 'Model champion probability'}}},\n",
    "       {'transform': [{'filter': {'field': 'team', 'oneOf': labelled}}], 'mark': {'type': 'text', 'dx': 6, 'dy': -6, 'align': 'left'}, 'encoding': {'x': {'field': 'market_p', 'type': 'quantitative'}, 'y': {'field': 'model_p', 'type': 'quantitative'}, 'text': {'field': 'team'}}}\n",
    "     ]}\n",
    "  ]\n",
    "}\n",
    "(FIGS / '04-model-vs-market-2026.vega.json').write_text(json.dumps(spec, indent=2) + '\\n')\n",
    "print('wrote figures/04-model-vs-market-2026.vega.json')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "687f7bf1",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-03T14:54:22.629620Z",
     "iopub.status.busy": "2026-06-03T14:54:22.629558Z",
     "iopub.status.idle": "2026-06-03T14:54:22.634127Z",
     "shell.execute_reply": "2026-06-03T14:54:22.633829Z"
    },
    "tags": [
     "figure:05-scorer-concentration"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "wrote figures/05-scorer-concentration.vega.json\n"
     ]
    }
   ],
   "source": [
    "shares = sc.loc[['modal_share', 'top5_share']].reset_index().melt(\n",
    "    id_vars='metric', value_vars=['baseline', 'post_recency'], var_name='arm', value_name='share')\n",
    "shares['metric'] = shares['metric'].map({'modal_share': 'modal scorer', 'top5_share': 'top-5 cumulative'})\n",
    "shares['arm'] = shares['arm'].map({'baseline': 'baseline', 'post_recency': 'post-recency'})\n",
    "spec = {\n",
    "  '$schema': 'https://vega.github.io/schema/vega-lite/v5.json',\n",
    "  'description': 'Concentration of the 2026 top-scorer marginal before and after integrating recency weighting. Recency lowered the modal share and the top-five cumulative mass, making the race more open.',\n",
    "  'data': {'values': shares.to_dict('records')},\n",
    "  'mark': 'bar',\n",
    "  'encoding': {\n",
    "    'x': {'field': 'metric', 'type': 'nominal', 'title': 'Concentration measure'},\n",
    "    'xOffset': {'field': 'arm'},\n",
    "    'y': {'field': 'share', 'type': 'quantitative', 'title': 'Probability mass', 'scale': {'domain': [0, 1]}},\n",
    "    'color': {'field': 'arm', 'type': 'nominal', 'title': 'Scorer model'}\n",
    "  }\n",
    "}\n",
    "(FIGS / '05-scorer-concentration.vega.json').write_text(json.dumps(spec, indent=2) + '\\n')\n",
    "print('wrote figures/05-scorer-concentration.vega.json')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "d01312ca",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-03T14:54:22.635186Z",
     "iopub.status.busy": "2026-06-03T14:54:22.635121Z",
     "iopub.status.idle": "2026-06-03T14:54:22.639274Z",
     "shell.execute_reply": "2026-06-03T14:54:22.638913Z"
    },
    "tags": [
     "figure:06-recency-recovery"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "wrote figures/06-recency-recovery.vega.json\n"
     ]
    }
   ],
   "source": [
    "rlong = py.melt(id_vars='year', value_vars=['base_p', 'recency_p'],\n",
    "                var_name='arm', value_name='p')\n",
    "rlong['arm'] = rlong['arm'].map({'base_p': 'baseline', 'recency_p': 'recency 1y'})\n",
    "rlong['year'] = rlong['year'].astype(str)\n",
    "spec = {\n",
    "  '$schema': 'https://vega.github.io/schema/vega-lite/v5.json',\n",
    "  'description': 'Probability the scorer model assigned to the realized Golden Boot winner, all-equal baseline against the adopted one-year recency half-life, by tournament. Higher is better.',\n",
    "  'data': {'values': rlong.to_dict('records')},\n",
    "  'mark': 'bar',\n",
    "  'encoding': {\n",
    "    'x': {'field': 'year', 'type': 'nominal', 'title': 'World Cup'},\n",
    "    'xOffset': {'field': 'arm'},\n",
    "    'y': {'field': 'p', 'type': 'quantitative', 'title': 'P(model names realized Golden Boot)'},\n",
    "    'color': {'field': 'arm', 'type': 'nominal', 'title': 'Scorer model'}\n",
    "  }\n",
    "}\n",
    "(FIGS / '06-recency-recovery.vega.json').write_text(json.dumps(spec, indent=2) + '\\n')\n",
    "print('wrote figures/06-recency-recovery.vega.json')"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.13"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
