{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 18 · Gardiens buteurs, notebook de preuve\n",
    "\n",
    "**Statut : exécuté.** `preuves/data/18-buts-gardiens.csv` est l'export du\n",
    "pipeline de l'étude (`goalies_goals`) : un but par ligne, daté, rattaché à un\n",
    "gardien, à une ligue et à un type de but.\n",
    "\n",
    "**Niveau de preuve.** Recalcul complet des comptes, des parts et de la\n",
    "chronologie depuis les 70 lignes. Le corpus est un inventaire : le notebook\n",
    "vérifie ce qu'il contient, pas ce qu'il ne contient pas.\n",
    "\n",
    "| | |\n",
    "|---|---|\n",
    "| **Page** | `etudes/gardiens-buteurs.html` |\n",
    "| **Source** | `goalies_goals/output/goalie_goals_2000_2026.csv` |\n",
    "| **Sortie** | `preuves/out/18.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 = {\"18-buts-gardiens.csv\": [\"continent\", \"country\", \"league\", \"season\", \"season_start\",\n",
    "                                    \"date\", \"match_id\", \"home\", \"away\", \"player\", \"player_id\",\n",
    "                                    \"minute\", \"is_penalty\", \"goal_type\", \"source\"]}\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 goalies_goals/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",
    "g = frames[\"18-buts-gardiens.csv\"]"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Contrôles d'intégrité\n",
    "\n",
    "Un but est une ligne, sauf deux cas documentés plus bas : deux buts du même\n",
    "gardien dans le même match. Le contrôle porte donc sur `match_id`, `player` et\n",
    "`minute`, et les paires sont nommées plutôt que tolérées en silence."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "assert len(g) == 70\n",
    "assert g.goal_type.value_counts().to_dict() == {\"penalty\": 46, \"free_kick\": 10,\n",
    "                                                \"open_play\": 8, \"unknown\": 6}\n",
    "assert g.continent.value_counts().to_dict() == {\"South America\": 59, \"Europe\": 11}\n",
    "assert g.country.nunique() == 10 and g.league.nunique() == 11\n",
    "assert int(g.season_start.min()) == 2016 and int(g.season_start.max()) == 2026\n",
    "assert g.date.min() == \"2016-01-30\" and g.date.max() == \"2026-10-13\"\n",
    "assert g.match_id.nunique() == 68\n",
    "assert not g.duplicated([\"match_id\", \"player\", \"minute\"]).any()\n",
    "doublons = g[g.duplicated([\"match_id\", \"player\"], keep=False)]\n",
    "assert len(doublons) == 4 and doublons.player.nunique() == 2\n",
    "assert set(doublons.player) == {\"Ronald Koeman\", \"Gaspar Servio\"}\n",
    "assert doublons.groupby(\"player\").minute.apply(list).to_dict() == {\n",
    "    \"Gaspar Servio\": [23, 15], \"Ronald Koeman\": [62, 100]}\n",
    "print(\"70 buts, 68 matchs, doublons documentés :\", sorted(doublons.player.unique()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Calcul\n",
    "\n",
    "Les comptes publiés : continent, type de but, top 3 des buteurs et découpage\n",
    "chronologique en cinq puis six saisons. 2026-27 est une saison entamée."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "top = g.player.value_counts()\n",
    "top3 = [(j, int(n)) for j, n in top.head(3).items()]\n",
    "par_type = {t: int(n) for t, n in g.goal_type.value_counts().items()}\n",
    "par_continent = {c: int(n) for c, n in g.continent.value_counts().items()}\n",
    "par_saison = {int(s): int(n) for s, n in g.season_start.value_counts().sort_index().items()}\n",
    "premieres = int(g.loc[g.season_start <= 2020].shape[0])\n",
    "suivantes = int(g.loc[g.season_start >= 2021].shape[0])\n",
    "part_penalties = 100.0 * par_type[\"penalty\"] / len(g)\n",
    "pays_europe = sorted(g.loc[g.continent == \"Europe\", \"country\"].unique())\n",
    "print(\"top 3\", top3, \"| 5 premières\", premieres, \"| 6 suivantes\", suivantes,\n",
    "      \"| penalties\", f\"{part_penalties:.1f} %\")\n",
    "print(\"Europe\", par_continent.get(\"Europe\"), \"pays\", pays_europe)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ce qui est asserté\n",
    "\n",
    "Chaque chiffre de la page est retrouvé depuis les lignes. Les aveux portent sur\n",
    "la couverture annoncée et sur les deux buts du même match, que la page ne\n",
    "distingue pas."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "assert len(g) == 70 and g.match_id.nunique() == 68\n",
    "assert (par_continent[\"South America\"], par_continent[\"Europe\"]) == (59, 11)\n",
    "assert par_type == {\"penalty\": 46, \"free_kick\": 10, \"open_play\": 8, \"unknown\": 6}\n",
    "assert round(part_penalties, 1) == 65.7 and abs(part_penalties / 100 - 2 / 3) < 0.01\n",
    "assert top3 == [(\"Sebastián Viera\", 11), (\"Luis Ardente\", 9), (\"Sebastian Bertoli\", 6)]\n",
    "assert set(g.loc[g.player.isin([j for j, _ in top3]), \"continent\"]) == {\"South America\"}\n",
    "assert (premieres, suivantes) == (41, 29) and premieres + suivantes == 70\n",
    "assert par_saison[2026] == 3  # 2026-27 partiel\n",
    "assert (g.country.nunique(), g.league.nunique()) == (10, 11)\n",
    "print(\"OK : continent, types, top 3, chronologie et couverture conformes à la page\")"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "SORTIE.mkdir(parents=True, exist_ok=True)\n",
    "payload = {\n",
    "    \"etude\": \"18\",\n",
    "    \"statut\": \"exécuté\",\n",
    "    \"niveau\": \"recalcul\",\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\": {\n",
    "        \"buts\": len(g), \"matchs\": int(g.match_id.nunique()),\n",
    "        \"par_continent\": par_continent, \"par_type\": par_type, \"par_saison\": par_saison,\n",
    "        \"top3\": top3, \"part_penalties\": part_penalties,\n",
    "        \"cinq_premieres\": premieres, \"six_suivantes\": suivantes,\n",
    "        \"pays\": int(g.country.nunique()), \"ligues\": int(g.league.nunique()),\n",
    "        \"periode\": {\"debut\": g.date.min(), \"fin\": g.date.max()},\n",
    "        \"doublons_meme_match\": doublons[[\"player\", \"date\", \"minute\"]].to_dict(\"records\"),\n",
    "    },\n",
    "    \"assertions_ok\": [\n",
    "        \"70 buts, 68 matchs distincts, du 2016-01-30 au 2026-10-13\",\n",
    "        \"59 buts en Amérique du Sud, 11 en Europe\",\n",
    "        \"46 penalties, 10 coups francs, 8 en jeu ouvert, 6 non renseignés\",\n",
    "        \"46 penalties sur 70, soit 65,7 %\",\n",
    "        \"top 3 sud-américain : Viera 11, Ardente 9, Bertoli 6\",\n",
    "        \"41 buts sur les cinq premières saisons, 29 sur les six suivantes\",\n",
    "        \"10 pays et 11 ligues dans l'export\",\n",
    "    ],\n",
    "    \"non_verifie\": [\n",
    "        \"couverture annoncée de neuf ligues européennes : l'export ne contient de buts que dans six pays d'Europe (Italie, Belgique, Espagne, Angleterre, Turquie, Pays-Bas)\",\n",
    "        \"deux buts du même gardien dans le même match (Koeman 62e et 100e, Servio 15e et 23e) : comptés séparément, la page ne les distingue pas\",\n",
    "    ],\n",
    "}\n",
    "(SORTIE / \"18.json\").write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding=\"utf-8\")\n",
    "print(\"écrit : preuves/out/18.json\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
