{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 19 · Ballon d'Or 2026, notebook de preuve\n",
    "\n",
    "**Statut : exécuté.** `preuves/data/19-touches.csv` est l'export des six cartes\n",
    "de touches : trois joueurs, club et sélection, une ligne par carte. Les volumes,\n",
    "les parts et les moyennes par 90 se recalculent depuis ces six lignes.\n",
    "\n",
    "**Niveau de preuve.** Recalcul des agrégats cartographiques. Les cartes elles-mêmes\n",
    "ne sont pas redessinées ici, et les mesures absentes de l'export (passes,\n",
    "amplitude 5–95, couloirs) sont déclarées non vérifiées plutôt que devinées.\n",
    "\n",
    "| | |\n",
    "|---|---|\n",
    "| **Page** | `etudes/ballon-dor.html` |\n",
    "| **Sources** | `ballon_dor/output/_touch_counts.json`, `_dispersion.json` |\n",
    "| **Sortie** | `preuves/out/19.json` |"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "import json, hashlib, platform, datetime\n",
    "from pathlib import Path\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "\n",
    "RACINE = Path(\"preuves/data\")\n",
    "SORTIE = Path(\"preuves/out\")\n",
    "ENTREES = {\"19-touches.csv\": [\"player\", \"kind\", \"matches\", \"touches\", \"minutes\", \"touches90\",\n",
    "                              \"own_half_pct\", \"final_third_pct\", \"box_pct\", \"xspan\", \"yspan\"]}\n",
    "manquants = [nom for nom in ENTREES if not (RACINE / nom).exists()]\n",
    "if manquants:\n",
    "    raise FileNotFoundError(\n",
    "        \"Corpus incomplet, rien n'est calculé. Fichiers attendus dans preuves/data/ : \"\n",
    "        + \", \".join(manquants) + \". Ils sont produits depuis ballon_dor/output/.\")\n",
    "frames = {nom: pd.read_csv(RACINE / nom, sep=\";\") for nom in ENTREES}\n",
    "for nom, colonnes in ENTREES.items():\n",
    "    assert list(frames[nom].columns) == colonnes, (nom, list(frames[nom].columns))\n",
    "t = frames[\"19-touches.csv\"]\n",
    "JOUEURS = [\"Kane\", \"Mbappé\", \"Yamal\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Contrôles d'intégrité\n",
    "\n",
    "Six cartes, trois joueurs, deux maillots. Les minutes servent de dénominateur aux\n",
    "touches par 90 : elles doivent être strictement positives."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "assert len(t) == 6\n",
    "assert set(t.player) == set(JOUEURS) and set(t.kind) == {\"club\", \"nation\"}\n",
    "assert not t.duplicated([\"player\", \"kind\"]).any()\n",
    "assert int(t.touches.sum()) == 8179 and float(t.minutes.sum()) == 14204.0\n",
    "assert (t.minutes > 0).all() and (t.matches > 0).all()\n",
    "assert np.allclose(t.touches90, t.touches / t.minutes * 90, rtol=1e-3)\n",
    "assert (t[[\"own_half_pct\", \"final_third_pct\", \"box_pct\"]] >= 0).all().all()\n",
    "print(t[[\"player\", \"kind\", \"matches\", \"touches\", \"touches90\"]].to_string(index=False))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Calcul\n",
    "\n",
    "Les totaux par joueur, puis les trois cartes de club et les trois cartes de\n",
    "sélection. Les arrondis de la page sont reproduits tels quels."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "def val(joueur, genre, colonne):\n",
    "    return float(t.loc[(t.player == joueur) & (t.kind == genre), colonne].iloc[0])\n",
    "\n",
    "\n",
    "total = int(t.touches.sum())\n",
    "totaux = {j: int(t.loc[t.player == j, \"touches\"].sum()) for j in JOUEURS}\n",
    "club = {j: {\"matchs\": int(val(j, \"club\", \"matches\")), \"touches\": int(val(j, \"club\", \"touches\")),\n",
    "            \"touches90\": val(j, \"club\", \"touches90\"), \"own_half\": val(j, \"club\", \"own_half_pct\"),\n",
    "            \"final_third\": val(j, \"club\", \"final_third_pct\"), \"box\": val(j, \"club\", \"box_pct\"),\n",
    "            \"xspan\": val(j, \"club\", \"xspan\"), \"yspan\": val(j, \"club\", \"yspan\")}\n",
    "        for j in JOUEURS}\n",
    "nation = {j: {\"matchs\": int(val(j, \"nation\", \"matches\")),\n",
    "              \"touches\": int(val(j, \"nation\", \"touches\")),\n",
    "              \"touches90\": val(j, \"nation\", \"touches90\"),\n",
    "              \"own_half\": val(j, \"nation\", \"own_half_pct\"),\n",
    "              \"final_third\": val(j, \"nation\", \"final_third_pct\"),\n",
    "              \"box\": val(j, \"nation\", \"box_pct\")}\n",
    "          for j in JOUEURS}\n",
    "print(\"total\", total, \"| totaux\", totaux)\n",
    "print(\"club\", {j: (club[j][\"matchs\"], round(club[j][\"own_half\"], 1),\n",
    "                   round(club[j][\"final_third\"], 1)) for j in JOUEURS})"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ce qui est asserté\n",
    "\n",
    "Chaque chiffre de carte est asserté à l'arrondi de la page. Les mesures que\n",
    "l'export ne porte pas restent en aveu : le notebook ne fabrique pas une passe\n",
    "qu'il n'a pas."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "assert total == 8179\n",
    "assert totaux == {\"Kane\": 2000, \"Mbappé\": 2633, \"Yamal\": 3546}\n",
    "assert [club[j][\"matchs\"] for j in JOUEURS] == [44, 42, 38]\n",
    "assert [nation[j][\"matchs\"] for j in JOUEURS] == [12, 12, 10]\n",
    "assert [round(club[j][\"own_half\"], 1) for j in JOUEURS] == [35.5, 9.2, 18.5]\n",
    "assert [round(nation[j][\"own_half\"], 1) for j in JOUEURS] == [30.0, 4.4, 16.2]\n",
    "assert [round(club[j][\"final_third\"]) for j in JOUEURS] == [46, 70, 58]\n",
    "assert [round(nation[j][\"final_third\"]) for j in JOUEURS] == [53, 74, 68]\n",
    "assert [round(club[j][\"touches90\"], 1) for j in JOUEURS] == [40.0, 46.9, 76.5]\n",
    "assert max(club, key=lambda j: club[j][\"own_half\"]) == \"Kane\"\n",
    "assert max(club, key=lambda j: club[j][\"final_third\"]) == \"Mbappé\"\n",
    "assert max(totaux, key=totaux.get) == \"Yamal\"\n",
    "print(\"OK : 8 179 touches, six cartes, parts et moyennes conformes à la page\")"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "SORTIE.mkdir(parents=True, exist_ok=True)\n",
    "payload = {\n",
    "    \"etude\": \"19\",\n",
    "    \"statut\": \"exécuté\",\n",
    "    \"niveau\": \"recalcul · agrégats (cartes)\",\n",
    "    \"genere_le\": datetime.datetime.now(datetime.timezone.utc).isoformat(timespec=\"seconds\"),\n",
    "    \"environnement\": {\"python\": platform.python_version(), \"pandas\": pd.__version__,\n",
    "                      \"numpy\": np.__version__},\n",
    "    \"entrees\": {n: {\"sha256\": hashlib.sha256((RACINE / n).read_bytes()).hexdigest(),\n",
    "                    \"octets\": (RACINE / n).stat().st_size} for n in ENTREES},\n",
    "    \"chiffres\": {\"total_touches\": total, \"totaux\": totaux, \"club\": club, \"nation\": nation},\n",
    "    \"assertions_ok\": [\n",
    "        \"8 179 touches sur six cartes\",\n",
    "        \"2 000 / 2 633 / 3 546 touches pour Kane, Mbappé, Yamal\",\n",
    "        \"44 / 42 / 38 matchs de club ; 12 / 12 / 10 en sélection\",\n",
    "        \"part dans sa moitié : 35,5 % / 9,2 % / 18,5 % (club)\",\n",
    "        \"dernier tiers en club : 46,3 % / 69,8 % / 58,3 % ; en sélection : 53,3 % / 74,1 % / 68,3 %\",\n",
    "        \"touches par 90 en club : 40,0 / 46,9 / 76,5\",\n",
    "        \"Yamal le plus volumineux ; Mbappé le plus haut ; Kane le seul au-dessus de 30 % dans sa moitié\",\n",
    "        \"aucune ligne manquante : trois joueurs, deux maillots, six cartes\",\n",
    "    ],\n",
    "    \"non_verifie\": [\n",
    "        \"amplitude verticale de 80,1 / 54,2 / 65,8 m : l'export donne des étendues de 76,3 / 51,7 / 62,7 m, sans le calcul 5–95 centiles\",\n",
    "        \"passes par 90 (26,1 / 32,2 / 50,8), part vers l'avant (50,5 %), passes progressives (22,3 %), longueur moyenne (18,1 m), réussite (84,6 %) et distance (8,8 km) : non exportées par 19-touches.csv\",\n",
    "        \"85 % des touches de Yamal dans le couloir droit (club) : le partage par couloir n'est pas dans l'export\",\n",
    "        \"contrôle SofaScore (r = 0,90–0,99 sur 6 matchs communs) et distance sur 44 / 42 / 35 matchs : non exportés\",\n",
    "    ],\n",
    "}\n",
    "(SORTIE / \"19.json\").write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding=\"utf-8\")\n",
    "print(\"écrit : preuves/out/19.json\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
