{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# 17 · Coups francs directs, notebook de preuve\n",
    "\n",
    "**Statut : exécuté.** Les cinq exports `preuves/data/17-*.csv` sortent du pipeline\n",
    "de l'étude (`freekicks_analysis`) : périmètre et fiabilité du codage, saisons de\n",
    "clubs, Premier League, distances et lucarne. Aucun chiffre de la page n'est\n",
    "saisi ici.\n",
    "\n",
    "**Niveaux de preuve.** Recalcul pour les totaux, les taux, les évolutions et la\n",
    "bande de conversion ; agrégats pour le périmètre (compteurs du contrôle de\n",
    "codage, exportés sans repasser sur les 257 008 lignes brutes) et pour les\n",
    "distances et la lucarne (tables déjà agrégées par le pipeline).\n",
    "\n",
    "| | |\n",
    "|---|---|\n",
    "| **Page** | `etudes/coups-francs.html` |\n",
    "| **Sources** | `freekicks_analysis/output/v2_clubs_saisons.csv`, `v2_premier_league.csv`, `v2_distance.csv`, `v2_lucarne_tendance.csv`, `summary_coverage.csv` |\n",
    "| **Sortie** | `preuves/out/17.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 = {\n",
    "    \"17-clubs-saisons.csv\": [\"season\", \"matches\", \"shots\", \"goals\", \"shots_per_match\",\n",
    "                             \"goals_per_match\", \"conversion\"],\n",
    "    \"17-premier-league.csv\": [\"season\", \"matches\", \"shots\", \"goals\", \"shots_per_match\",\n",
    "                              \"goals_per_match\", \"conversion\", \"in_progress\"],\n",
    "    \"17-distance.csv\": [\"band\", \"shots\", \"goals\", \"on_target_pct\", \"conversion\"],\n",
    "    \"17-lucarne.csv\": [\"season\", \"goals\", \"top_corner\", \"top_corner_pct\"],\n",
    "    \"17-perimetre.csv\": [\"metric\", \"value\"],\n",
    "}\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 freekicks_analysis/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",
    "clubs = frames[\"17-clubs-saisons.csv\"]\n",
    "pl = frames[\"17-premier-league.csv\"]\n",
    "dist = frames[\"17-distance.csv\"]\n",
    "lu = frames[\"17-lucarne.csv\"]\n",
    "peri = frames[\"17-perimetre.csv\"].set_index(\"metric\").value.astype(int).to_dict()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Contrôles d'intégrité\n",
    "\n",
    "Les trois tables de tirs doivent totaliser le même nombre de tentatives et de\n",
    "buts : 7 152 et 385. Si une ligne manquait d'un côté, le notebook s'arrête ici."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "assert list(clubs.season) == [\"2021/2022\", \"2022/2023\", \"2023/2024\", \"2024/2025\",\n",
    "                             \"2025/2026\", \"2026/2027\"]\n",
    "assert int(clubs.matches.sum()) == 9134\n",
    "assert int(clubs.shots.sum()) == 7152 and int(clubs.goals.sum()) == 385\n",
    "assert int(clubs.iloc[-1].matches) == 250  # 2026-27 en cours\n",
    "assert len(pl) == 18 and int(pl.in_progress.sum()) == 1 and bool(pl.iloc[-1].in_progress)\n",
    "assert int(pl.matches.sum()) == 6506\n",
    "assert int(pl.shots.sum()) == 7138 and int(pl.goals.sum()) == 398\n",
    "assert int(pl.loc[~pl.in_progress, \"matches\"].sum()) == 6456\n",
    "assert int(dist.shots.sum()) == 7152 and int(dist.goals.sum()) == 385\n",
    "assert int(lu.goals.sum()) == 424 and int(lu.top_corner.sum()) == 218\n",
    "assert peri == {\"raw_rows\": 257008, \"reliable_rows\": 240314,\n",
    "                \"reliable_matches\": 120157, \"excluded_groups\": 27,\n",
    "                \"excluded_matches\": 8347}\n",
    "print(\"9 134 matchs de clubs · 7 152 tirs · 385 buts · périmètre\", peri)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Calcul\n",
    "\n",
    "Le volume et l'efficacité sont séparés, comme dans l'étude : une évolution de\n",
    "tirs par match, une autre de buts par match, et la bande de conversion des\n",
    "saisons complètes."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "def taux(a, b):\n",
    "    return 100.0 * a / b\n",
    "\n",
    "\n",
    "conversion = taux(clubs.goals.sum(), clubs.shots.sum())\n",
    "premier = pl.iloc[0]\n",
    "dernier = pl.loc[pl.season == \"2025/2026\"].iloc[0]\n",
    "delta_tirs = taux(dernier.shots_per_match - premier.shots_per_match, premier.shots_per_match)\n",
    "delta_buts = taux(dernier.goals_per_match - premier.goals_per_match, premier.goals_per_match)\n",
    "pl_complet = pl.loc[~pl.in_progress]\n",
    "bande = (float(pl_complet.conversion.min()), float(pl_complet.conversion.max()))\n",
    "lucarne = taux(lu.top_corner.sum(), lu.goals.sum())\n",
    "distances = {r.band: {\"tirs\": int(r.shots), \"buts\": int(r.goals),\n",
    "                      \"conversion\": float(r.conversion)} for r in dist.itertuples()}\n",
    "saisons = {r.season: {\"matchs\": int(r.matches), \"tirs\": int(r.shots),\n",
    "                      \"buts\": int(r.goals), \"tirs_par_match\": float(r.shots_per_match),\n",
    "                      \"buts_par_match\": float(r.goals_per_match),\n",
    "                      \"conversion\": float(r.conversion)} for r in clubs.itertuples()}\n",
    "print(f\"conversion {conversion:.2f} % ; Premier League {premier.shots_per_match:.3f} → \"\n",
    "      f\"{dernier.shots_per_match:.3f} ({delta_tirs:+.2f} %), {premier.goals_per_match:.3f} → \"\n",
    "      f\"{dernier.goals_per_match:.3f} ({delta_buts:+.2f} %) ; \"\n",
    "      f\"bande {bande} ; lucarne {lucarne:.1f} %\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ce qui est asserté\n",
    "\n",
    "Une assertion par chiffre de la page, à l'arrondi affiché. La page arrondit\n",
    "−61,66 % à −62 % et −44,05 % à −44 % ; le notebook garde les valeurs exactes\n",
    "dans son export. La lucarne, elle, ne tombe pas juste : voir les aveux."
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "assert round(conversion, 2) == 5.38\n",
    "assert round(taux(pl.goals.sum(), pl.shots.sum()), 2) == 5.58\n",
    "assert (round(premier.shots_per_match, 2), round(dernier.shots_per_match, 2)) == (1.72, 0.66)\n",
    "assert round(delta_tirs) == -62\n",
    "assert (round(premier.goals_per_match, 3), round(dernier.goals_per_match, 3)) == (0.084, 0.047)\n",
    "assert round(delta_buts) == -44\n",
    "assert [round(x, 1) for x in bande] == [3.6, 7.8]\n",
    "assert round(lucarne, 1) == 51.4\n",
    "assert int(distances[\"<20 m\"][\"tirs\"]) + int(distances[\">36 m\"][\"tirs\"]) == 1137\n",
    "assert round(lu.top_corner.sum() / 424 * 100, 1) == 51.4\n",
    "print(\"OK : totaux, conversion, Premier League, bande et lucarne conformes aux exports\")"
   ]
  },
  {
   "cell_type": "code",
   "metadata": {},
   "execution_count": null,
   "outputs": [],
   "source": [
    "SORTIE.mkdir(parents=True, exist_ok=True)\n",
    "payload = {\n",
    "    \"etude\": \"17\",\n",
    "    \"statut\": \"exécuté\",\n",
    "    \"niveau\": \"recalcul · agrégats (périmètre)\",\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",
    "        \"clubs\": {\"matchs\": int(clubs.matches.sum()), \"tirs\": int(clubs.shots.sum()),\n",
    "                  \"buts\": int(clubs.goals.sum()), \"conversion\": conversion},\n",
    "        \"saisons\": saisons,\n",
    "        \"perimetre\": peri,\n",
    "        \"premier_league\": {\"matchs\": int(pl.matches.sum()), \"tirs\": int(pl.shots.sum()),\n",
    "                           \"buts\": int(pl.goals.sum()),\n",
    "                           \"conversion\": taux(pl.goals.sum(), pl.shots.sum()),\n",
    "                           \"premier\": {\"saison\": premier.season,\n",
    "                                       \"tirs_par_match\": float(premier.shots_per_match),\n",
    "                                       \"buts_par_match\": float(premier.goals_per_match)},\n",
    "                           \"dernier\": {\"saison\": dernier.season,\n",
    "                                       \"tirs_par_match\": float(dernier.shots_per_match),\n",
    "                                       \"buts_par_match\": float(dernier.goals_per_match)},\n",
    "                           \"delta_tirs_pct\": delta_tirs, \"delta_buts_pct\": delta_buts,\n",
    "                           \"bande_conversion\": list(bande)},\n",
    "        \"distances\": distances,\n",
    "        \"lucarne\": {\"buts\": int(lu.goals.sum()), \"lucarne\": int(lu.top_corner.sum()),\n",
    "                    \"part\": lucarne},\n",
    "    },\n",
    "    \"assertions_ok\": [\n",
    "        \"7 152 tirs directs, 385 buts, 5,38 % de conversion dans l'export clubs\",\n",
    "        \"6 saisons de clubs, dont 2026-27 en cours (250 matchs sur 9 134)\",\n",
    "        \"périmètre : 257 008 lignes brutes, 240 314 retenues, 120 157 matchs, 27 groupes et 8 347 matchs exclus\",\n",
    "        \"Premier League : 6 506 matchs, 7 138 tirs, 398 buts, 5,58 %\",\n",
    "        \"1,72 → 0,66 tir par match (−62 %) et 0,084 → 0,047 but par match (−44 %)\",\n",
    "        \"bande de conversion saisonnière 3,6–7,8 % sur les 17 saisons complètes\",\n",
    "        \"distances : 7 152 tirs et 385 buts sur six tranches, totaux identiques à l'export clubs\",\n",
    "        \"lucarne : 218 buts sur 424 (51,4 %) dans l'export 2021-2026\",\n",
    "    ],\n",
    "    \"non_verifie\": [\n",
    "        \"53,8 % des buts directs en lucarne : l'export 2021-2026 donne 218 sur 424, soit 51,4 % ; l'écart n'est pas résolu\",\n",
    "        \"10,5 % de conversion à 18–22 m et 4,9 % à 26–30 m : l'export ne fournit que six tranches de 4 m (<20, 20-24, …, >36)\",\n",
    "        \"rattachement des 7 152 tirs aux 120 157 matchs du périmètre : l'export des tirs ne couvre que les clubs\",\n",
    "    ],\n",
    "}\n",
    "(SORTIE / \"17.json\").write_text(json.dumps(payload, indent=2, ensure_ascii=False), encoding=\"utf-8\")\n",
    "print(\"écrit : preuves/out/17.json\")"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
