{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "9ce43867",
   "metadata": {},
   "source": [
    "# MKT-006 reproducibility notebook\n",
    "\n",
    "Two roads to the same champion: a homegrown World Cup model side by side with Goldman Sachs.\n",
    "\n",
    "This notebook consumes only the committed CSVs under `data/` and regenerates the derived deviation table and every Vega-Lite figure. It runs top to bottom with no network access. The model runs themselves happen in the source repositories (`world-cup`, `poly`); this notebook consumes the exported artifacts, matching the MKT-004 pattern."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "d1bd7b22",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-12T16:15:49.614647Z",
     "iopub.status.busy": "2026-06-12T16:15:49.614425Z",
     "iopub.status.idle": "2026-06-12T16:15:49.977234Z",
     "shell.execute_reply": "2026-06-12T16:15:49.976730Z"
    },
    "tags": [
     "setup"
    ]
   },
   "outputs": [],
   "source": [
    "import json\n",
    "import pathlib\n",
    "import pandas as pd\n",
    "\n",
    "PAPER_DIR = pathlib.Path('.')\n",
    "DATA = PAPER_DIR / 'data'\n",
    "FIG = PAPER_DIR / 'figures'\n",
    "FIG.mkdir(exist_ok=True)\n",
    "\n",
    "V5 = 'https://vega.github.io/schema/vega-lite/v5.json'\n",
    "\n",
    "def write_spec(name, spec):\n",
    "    path = FIG / name\n",
    "    path.write_text(json.dumps(spec, indent=2, ensure_ascii=False) + '\\n')\n",
    "    print('wrote', path)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "35ce73e2",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-12T16:15:49.978770Z",
     "iopub.status.busy": "2026-06-12T16:15:49.978667Z",
     "iopub.status.idle": "2026-06-12T16:15:49.994007Z",
     "shell.execute_reply": "2026-06-12T16:15:49.993582Z"
    },
    "tags": [
     "load"
    ]
   },
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>team</th>\n",
       "      <th>kickoff_p</th>\n",
       "      <th>gs_p</th>\n",
       "      <th>market_p</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>Spain</td>\n",
       "      <td>0.240</td>\n",
       "      <td>0.257</td>\n",
       "      <td>0.159</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Argentina</td>\n",
       "      <td>0.168</td>\n",
       "      <td>0.143</td>\n",
       "      <td>0.085</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>France</td>\n",
       "      <td>0.120</td>\n",
       "      <td>0.189</td>\n",
       "      <td>0.166</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>England</td>\n",
       "      <td>0.054</td>\n",
       "      <td>0.050</td>\n",
       "      <td>0.108</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Brazil</td>\n",
       "      <td>0.045</td>\n",
       "      <td>0.076</td>\n",
       "      <td>0.081</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>Colombia</td>\n",
       "      <td>0.042</td>\n",
       "      <td>0.020</td>\n",
       "      <td>0.017</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>Portugal</td>\n",
       "      <td>0.034</td>\n",
       "      <td>0.048</td>\n",
       "      <td>0.093</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>Netherlands</td>\n",
       "      <td>0.029</td>\n",
       "      <td>0.050</td>\n",
       "      <td>0.038</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>Germany</td>\n",
       "      <td>0.027</td>\n",
       "      <td>0.045</td>\n",
       "      <td>0.055</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "          team  kickoff_p   gs_p  market_p\n",
       "0        Spain      0.240  0.257     0.159\n",
       "1    Argentina      0.168  0.143     0.085\n",
       "2       France      0.120  0.189     0.166\n",
       "3      England      0.054  0.050     0.108\n",
       "4       Brazil      0.045  0.076     0.081\n",
       "5     Colombia      0.042  0.020     0.017\n",
       "6     Portugal      0.034  0.048     0.093\n",
       "7  Netherlands      0.029  0.050     0.038\n",
       "8      Germany      0.027  0.045     0.055"
      ]
     },
     "execution_count": 2,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Source tables (committed inputs; see data/manifest.yaml for provenance).\n",
    "cm = pd.read_csv(DATA / 'champion_marginal.csv')\n",
    "bt = pd.read_csv(DATA / 'champion_backtest.csv')\n",
    "mc = pd.read_csv(DATA / 'match_calibration.csv')\n",
    "esel = pd.read_csv(DATA / 'engine_selection.csv')\n",
    "rub = pd.read_csv(DATA / 'assessment_rubric.csv')\n",
    "ledger = pd.read_csv(DATA / 'build_ledger.csv')\n",
    "cm\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "c70e26e5",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-12T16:15:49.995166Z",
     "iopub.status.busy": "2026-06-12T16:15:49.995093Z",
     "iopub.status.idle": "2026-06-12T16:15:50.002614Z",
     "shell.execute_reply": "2026-06-12T16:15:50.002238Z"
    },
    "tags": [
     "tilt"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "wrote data/market_deviation.csv\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>team</th>\n",
       "      <th>market_p</th>\n",
       "      <th>kickoff_dev</th>\n",
       "      <th>gs_dev</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>Spain</td>\n",
       "      <td>0.159</td>\n",
       "      <td>8.1</td>\n",
       "      <td>9.8</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>Argentina</td>\n",
       "      <td>0.085</td>\n",
       "      <td>8.3</td>\n",
       "      <td>5.8</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>France</td>\n",
       "      <td>0.166</td>\n",
       "      <td>-4.6</td>\n",
       "      <td>2.3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>England</td>\n",
       "      <td>0.108</td>\n",
       "      <td>-5.4</td>\n",
       "      <td>-5.8</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>Brazil</td>\n",
       "      <td>0.081</td>\n",
       "      <td>-3.6</td>\n",
       "      <td>-0.5</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>Colombia</td>\n",
       "      <td>0.017</td>\n",
       "      <td>2.5</td>\n",
       "      <td>0.3</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>Portugal</td>\n",
       "      <td>0.093</td>\n",
       "      <td>-5.9</td>\n",
       "      <td>-4.5</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>Netherlands</td>\n",
       "      <td>0.038</td>\n",
       "      <td>-0.9</td>\n",
       "      <td>1.2</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>Germany</td>\n",
       "      <td>0.055</td>\n",
       "      <td>-2.8</td>\n",
       "      <td>-1.0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "          team  market_p  kickoff_dev  gs_dev\n",
       "0        Spain     0.159          8.1     9.8\n",
       "1    Argentina     0.085          8.3     5.8\n",
       "2       France     0.166         -4.6     2.3\n",
       "3      England     0.108         -5.4    -5.8\n",
       "4       Brazil     0.081         -3.6    -0.5\n",
       "5     Colombia     0.017          2.5     0.3\n",
       "6     Portugal     0.093         -5.9    -4.5\n",
       "7  Netherlands     0.038         -0.9     1.2\n",
       "8      Germany     0.055         -2.8    -1.0"
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "# Derived: deviation from the common devigged-market baseline (percentage points).\n",
    "dev = cm[['team', 'market_p']].copy()\n",
    "dev['kickoff_dev'] = ((cm['kickoff_p'] - cm['market_p']) * 100).round(1)\n",
    "dev['gs_dev'] = ((cm['gs_p'] - cm['market_p']) * 100).round(1)\n",
    "dev.to_csv(DATA / 'market_deviation.csv', index=False)\n",
    "print('wrote', DATA / 'market_deviation.csv')\n",
    "dev\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "3ce69aee",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-12T16:15:50.003601Z",
     "iopub.status.busy": "2026-06-12T16:15:50.003532Z",
     "iopub.status.idle": "2026-06-12T16:15:50.006750Z",
     "shell.execute_reply": "2026-06-12T16:15:50.006356Z"
    },
    "tags": [
     "fig-champion-marginal"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "wrote figures/01-champion-marginal.vega.json\n"
     ]
    }
   ],
   "source": [
    "# Figure 1 — head-to-head champion probabilities (Kickoff / Goldman Sachs / Market).\n",
    "order = list(cm['team'])\n",
    "labels = {'kickoff_p': 'Kickoff', 'gs_p': 'Goldman Sachs', 'market_p': 'Market'}\n",
    "vals = []\n",
    "for _, r in cm.iterrows():\n",
    "    for col, name in labels.items():\n",
    "        vals.append({'team': r['team'], 'forecaster': name, 'p': round(float(r[col]), 3)})\n",
    "spec1 = {\n",
    "    '$schema': V5,\n",
    "    'description': 'Head-to-head champion probabilities for the 2026 World Cup: the Kickoff engine, the Goldman Sachs model, and the devigged betting market. Both models rate Spain the clear favourite and both sit above the market on Spain and Argentina.',\n",
    "    'data': {'values': vals},\n",
    "    'mark': 'bar',\n",
    "    'encoding': {\n",
    "        'x': {'field': 'team', 'type': 'nominal', 'title': 'Team', 'sort': order},\n",
    "        'xOffset': {'field': 'forecaster', 'sort': ['Kickoff', 'Goldman Sachs', 'Market']},\n",
    "        'y': {'field': 'p', 'type': 'quantitative', 'title': 'Champion probability'},\n",
    "        'color': {'field': 'forecaster', 'type': 'nominal', 'title': 'Forecaster', 'sort': ['Kickoff', 'Goldman Sachs', 'Market']},\n",
    "    },\n",
    "}\n",
    "write_spec('01-champion-marginal.vega.json', spec1)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "36e1ab15",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-12T16:15:50.007715Z",
     "iopub.status.busy": "2026-06-12T16:15:50.007644Z",
     "iopub.status.idle": "2026-06-12T16:15:50.010785Z",
     "shell.execute_reply": "2026-06-12T16:15:50.010393Z"
    },
    "tags": [
     "fig-market-deviation"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "wrote figures/02-market-deviation.vega.json\n"
     ]
    }
   ],
   "source": [
    "# Figure 2 — deviation from the common market baseline (the mutual anti-market tilt).\n",
    "vals = []\n",
    "for _, r in dev.iterrows():\n",
    "    vals.append({'team': r['team'], 'forecaster': 'Kickoff', 'deviation': round(float(r['kickoff_dev']), 1)})\n",
    "    vals.append({'team': r['team'], 'forecaster': 'Goldman Sachs', 'deviation': round(float(r['gs_dev']), 1)})\n",
    "spec2 = {\n",
    "    '$schema': V5,\n",
    "    'description': 'Each model probability minus the devigged-market probability, in percentage points. Both models lean the same way on the four teams that define the tilt: overweight Spain and Argentina, underweight England and Portugal. France is the most pronounced sign disagreement.',\n",
    "    'data': {'values': vals},\n",
    "    'mark': 'bar',\n",
    "    'encoding': {\n",
    "        'x': {'field': 'team', 'type': 'nominal', 'title': 'Team', 'sort': order},\n",
    "        'xOffset': {'field': 'forecaster', 'sort': ['Kickoff', 'Goldman Sachs']},\n",
    "        'y': {'field': 'deviation', 'type': 'quantitative', 'title': 'Deviation from market (percentage points)'},\n",
    "        'color': {'field': 'forecaster', 'type': 'nominal', 'title': 'Forecaster', 'sort': ['Kickoff', 'Goldman Sachs']},\n",
    "    },\n",
    "}\n",
    "write_spec('02-market-deviation.vega.json', spec2)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "5941a4ff",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-12T16:15:50.011811Z",
     "iopub.status.busy": "2026-06-12T16:15:50.011738Z",
     "iopub.status.idle": "2026-06-12T16:15:50.014953Z",
     "shell.execute_reply": "2026-06-12T16:15:50.014629Z"
    },
    "tags": [
     "fig-champion-backtest"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "wrote figures/03-champion-backtest.vega.json\n"
     ]
    }
   ],
   "source": [
    "# Figure 3 — champion backtest, Kickoff engine versus the devigged closing line, 2010 to 2022.\n",
    "vals = []\n",
    "for _, r in bt.iterrows():\n",
    "    vals.append({'year': str(int(r['year'])), 'series': 'model', 'log_loss': float(r['model_log_loss'])})\n",
    "    vals.append({'year': str(int(r['year'])), 'series': 'market', 'log_loss': float(r['market_log_loss'])})\n",
    "spec3 = {\n",
    "    '$schema': V5,\n",
    "    'description': 'Champion log-loss for the Kickoff engine against the devigged closing line, by tournament. Lower is better. The model scores lower in 2010, 2014, and 2022 and higher in 2018.',\n",
    "    'data': {'values': vals},\n",
    "    'mark': 'bar',\n",
    "    'encoding': {\n",
    "        'x': {'field': 'year', 'type': 'nominal', 'title': 'World Cup'},\n",
    "        'xOffset': {'field': 'series', 'sort': ['model', 'market']},\n",
    "        'y': {'field': 'log_loss', 'type': 'quantitative', 'title': 'Champion log-loss (lower is better)'},\n",
    "        'color': {'field': 'series', 'type': 'nominal', 'title': 'Forecaster', 'sort': ['model', 'market']},\n",
    "    },\n",
    "}\n",
    "write_spec('03-champion-backtest.vega.json', spec3)\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "d76400e5",
   "metadata": {
    "execution": {
     "iopub.execute_input": "2026-06-12T16:15:50.015949Z",
     "iopub.status.busy": "2026-06-12T16:15:50.015879Z",
     "iopub.status.idle": "2026-06-12T16:15:50.024804Z",
     "shell.execute_reply": "2026-06-12T16:15:50.024405Z"
    },
    "tags": [
     "summary"
    ]
   },
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "champion backtest: model mean log-loss 1.897 vs market 2.199; model beats market 3/4\n",
      "Spain+Argentina mass: Kickoff 40.8% vs Goldman Sachs 40.0%\n",
      "France gap: Goldman Sachs 18.9% vs Kickoff 12.0% (6.9 pp); GS market dev +2.3, Kickoff market dev -4.6\n",
      "Kickoff vs Goldman Sachs champion gaps (pp), largest first:\n",
      "  France       6.9\n",
      "  Brazil       3.1\n",
      "  Argentina    2.5\n",
      "  Colombia     2.2\n",
      "  Netherlands  2.1\n",
      "  Germany      1.8\n",
      "  Spain        1.7\n",
      "  Portugal     1.4\n",
      "  England      0.4\n",
      "both overweight: ['Spain', 'Argentina', 'Colombia']\n",
      "both underweight: ['England', 'Brazil', 'Portugal', 'Germany']\n",
      "rubric edges (orthogonal strengths, not a summable score): Goldman Sachs leads 3 dimensions, Kickoff leads 4\n",
      "per-match: Dixon-Coles log-loss 0.8673 vs Elo 0.8936\n",
      "champion-marginal engine selection log-loss: Elo 1.889 vs Dixon-Coles 2.504\n"
     ]
    }
   ],
   "source": [
    "# Headline numbers earned by the tables above (traceable summary).\n",
    "mean_model = bt['model_log_loss'].mean()\n",
    "mean_market = bt['market_log_loss'].mean()\n",
    "beats = int(bt['model_beats_market'].sum())\n",
    "print(f'champion backtest: model mean log-loss {mean_model:.3f} vs market {mean_market:.3f}; model beats market {beats}/4')\n",
    "\n",
    "row = cm.set_index('team')\n",
    "k_conc = row.loc['Spain', 'kickoff_p'] + row.loc['Argentina', 'kickoff_p']\n",
    "g_conc = row.loc['Spain', 'gs_p'] + row.loc['Argentina', 'gs_p']\n",
    "print(f'Spain+Argentina mass: Kickoff {k_conc*100:.1f}% vs Goldman Sachs {g_conc*100:.1f}%')\n",
    "\n",
    "france = dev.set_index('team').loc['France']\n",
    "print(f\"France gap: Goldman Sachs {row.loc['France','gs_p']*100:.1f}% vs Kickoff {row.loc['France','kickoff_p']*100:.1f}% \"\n",
    "      f\"({(row.loc['France','gs_p']-row.loc['France','kickoff_p'])*100:.1f} pp); \"\n",
    "      f\"GS market dev {france['gs_dev']:+.1f}, Kickoff market dev {france['kickoff_dev']:+.1f}\")\n",
    "\n",
    "gaps = cm.assign(gap=((cm['kickoff_p'] - cm['gs_p']).abs() * 100).round(1)).sort_values('gap', ascending=False)\n",
    "print('Kickoff vs Goldman Sachs champion gaps (pp), largest first:')\n",
    "for _, g in gaps.iterrows():\n",
    "    print(f\"  {g['team']:12s} {g['gap']:.1f}\")\n",
    "shared_over = [t for t in order if dev.set_index('team').loc[t,'kickoff_dev']>0 and dev.set_index('team').loc[t,'gs_dev']>0]\n",
    "shared_under = [t for t in order if dev.set_index('team').loc[t,'kickoff_dev']<0 and dev.set_index('team').loc[t,'gs_dev']<0]\n",
    "print('both overweight:', shared_over)\n",
    "print('both underweight:', shared_under)\n",
    "\n",
    "gs_leads = int((rub['edge'] == 'Goldman Sachs').sum())\n",
    "k_leads = int((rub['edge'] == 'Kickoff').sum())\n",
    "print(f'rubric edges (orthogonal strengths, not a summable score): Goldman Sachs leads {gs_leads} dimensions, Kickoff leads {k_leads}')\n",
    "print('per-match: Dixon-Coles log-loss', float(mc.set_index('engine').loc['Dixon-Coles','log_loss']),\n",
    "      'vs Elo', float(mc.set_index('engine').loc['Elo','log_loss']))\n",
    "print('champion-marginal engine selection log-loss: Elo',\n",
    "      float(esel.set_index('engine').loc['Elo','champion_log_loss']),\n",
    "      'vs Dixon-Coles', float(esel.set_index('engine').loc['Dixon-Coles','champion_log_loss']))\n"
   ]
  }
 ],
 "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
}
