{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Malnutrition aiguë globale par commune\n",
    "\n",
    "*Dépistage du périmètre brachial — Artibonite, 2024*\n",
    "\n",
    "Cassion · data-analysis.cassion.dev\n",
    "\n",
    "[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/thecassion/cassion-learning-platform/blob/main/apps/data-analysis/public/datasets/examples/muac-screening-artibonite-2024/gam-by-commune.python.fr.ipynb)"
   ],
   "id": "cell-000"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ce que produit ce document\n",
    "\n",
    "Les prévalences de MAG et de MAS par commune, avec intervalles de confiance à\n",
    "95 %, à partir d'un registre de dépistage de masse communautaire. Les valeurs de\n",
    "référence figurent dans les notes de qualité du jeu de données : une MAG globale\n",
    "proche de 8,6 % et une MAS proche de 2,2 %, avec une amplitude d'environ 5 % à\n",
    "15 % selon les communes. Si vos chiffres s'en écartent nettement, vous avez un\n",
    "bogue et non un résultat.\n",
    "\n",
    "Tous les jeux de données de cette plateforme sont synthétiques. Rien ici ne\n",
    "décrit un enfant réel, et ces chiffres ne doivent jamais être cités comme des\n",
    "prévalences réelles.\n",
    "\n",
    "## Mise en place\n",
    "\n",
    "Le téléchargement rend ce document exécutable dans Colab, où il n'existe aucun\n",
    "fichier local. En local, sur votre propre copie, c'est le même code avec un\n",
    "chemin différent."
   ],
   "id": "cell-001"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "\n",
    "URL = (\n",
    "    \"https://data-analysis.cassion.dev/datasets/files/\"\n",
    "    \"muac-screening-artibonite-2024.v1.csv\"\n",
    ")\n",
    "\n",
    "muac = pd.read_csv(\n",
    "    URL,\n",
    "    dtype={\"child_id\": \"string\", \"commune\": \"string\", \"sex\": \"string\"},\n",
    "    na_values={\"muac_mm\": [\"-99\"]},\n",
    ")\n",
    "\n",
    "muac[\"screening_date\"] = pd.to_datetime(muac[\"screening_date\"], format=\"%Y-%m-%d\")\n",
    "\n",
    "print(muac.shape)\n",
    "muac.head()"
   ],
   "id": "cell-002"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Deux arguments font ici tout le travail. Le `na_values` restreint à `muac_mm`\n",
    "empêche la valeur sentinelle `-99` d'entrer dans la moyenne : sans lui, le PB\n",
    "moyen ressort inférieur de plusieurs millimètres, et rien dans le résultat n'a\n",
    "l'air anormal. Le `dtype` conserve les identifiants sous forme de texte, ce qui\n",
    "devient déterminant dès la première jointure avec un autre fichier.\n",
    "\n",
    "## Corriger ce qui fausserait l'indicateur\n",
    "\n",
    "Deux défauts de ce registre changent la réponse. Sept enregistrements sont\n",
    "restés en centimètres sans jamais être convertis ; les plages plausibles en\n",
    "millimètres et en centimètres ne se chevauchant pas, la correction est sans\n",
    "ambiguïté. Deux communes ont consigné les œdèmes en `Y`/`N` au premier trimestre\n",
    "plutôt qu'en `true`/`false`."
   ],
   "id": "cell-003"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "erreur_unite = muac[\"muac_mm\"].notna() & (muac[\"muac_mm\"] < 40)\n",
    "muac.loc[erreur_unite, \"muac_mm\"] = muac.loc[erreur_unite, \"muac_mm\"] * 10\n",
    "\n",
    "implausible = muac[\"muac_mm\"].notna() & (\n",
    "    (muac[\"muac_mm\"] < 80) | (muac[\"muac_mm\"] > 220)\n",
    ")\n",
    "muac.loc[implausible, \"muac_mm\"] = np.nan\n",
    "\n",
    "table_oedeme = {\n",
    "    \"true\": True, \"TRUE\": True, \"Y\": True, \"y\": True, \"yes\": True,\n",
    "    \"false\": False, \"FALSE\": False, \"N\": False, \"n\": False, \"no\": False,\n",
    "}\n",
    "muac[\"oedema\"] = muac[\"oedema\"].astype(\"string\").str.strip().map(table_oedeme)\n",
    "\n",
    "print(f\"erreurs d'unite corrigees : {int(erreur_unite.sum())}\")\n",
    "print(f\"implausibles en manquant  : {int(implausible.sum())}\")\n",
    "print(f\"oedemes encore manquants  : {int(muac['oedema'].isna().sum())}\")"
   ],
   "id": "cell-004"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Définir l'indicateur avant de le calculer\n",
    "\n",
    "- **Numérateur** — enfants dont le PB est inférieur à 125 mm, ou porteurs\n",
    "  d'œdèmes bilatéraux prenant le godet.\n",
    "- **Dénominateur** — enfants disposant d'une mesure de PB valide **ou** d'une\n",
    "  évaluation des œdèmes consignée.\n",
    "- **Désagrégation** — commune.\n",
    "\n",
    "Les œdèmes signent une malnutrition aiguë sévère quelle que soit la mesure :\n",
    "filtrer sur le seul `muac_mm` sous-estime donc la charge de cas, et la\n",
    "sous-estime précisément parmi les cas les plus graves."
   ],
   "id": "cell-005"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "MAS_MM, MAG_MM = 115, 125\n",
    "\n",
    "muac[\"evalue\"] = muac[\"muac_mm\"].notna() | muac[\"oedema\"].notna()\n",
    "\n",
    "muac[\"mas\"] = np.where(\n",
    "    ~muac[\"evalue\"],\n",
    "    np.nan,\n",
    "    ((muac[\"muac_mm\"] < MAS_MM) | (muac[\"oedema\"] == True)).astype(float),\n",
    ")\n",
    "muac[\"mag\"] = np.where(\n",
    "    ~muac[\"evalue\"],\n",
    "    np.nan,\n",
    "    ((muac[\"muac_mm\"] < MAG_MM) | (muac[\"oedema\"] == True)).astype(float),\n",
    ")"
   ],
   "id": "cell-006"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Le tableau d'indicateurs\n",
    "\n",
    "Les colonnes `depistes` et `denominateur` sont distinctes à dessein : elles\n",
    "diffèrent des lignes sans aucune évaluation, et un lecteur qui voit les deux peut\n",
    "juger sur quelle part du registre repose le taux."
   ],
   "id": "cell-007"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "tableau = (\n",
    "    muac.groupby(\"commune\", dropna=False)\n",
    "    .agg(\n",
    "        depistes=(\"child_id\", \"size\"),\n",
    "        denominateur=(\"evalue\", \"sum\"),\n",
    "        cas_mas=(\"mas\", \"sum\"),\n",
    "        cas_mag=(\"mag\", \"sum\"),\n",
    "    )\n",
    "    .reset_index()\n",
    ")\n",
    "\n",
    "tableau[\"taux_mag\"] = tableau[\"cas_mag\"] / tableau[\"denominateur\"]\n",
    "tableau[\"taux_mas\"] = tableau[\"cas_mas\"] / tableau[\"denominateur\"]\n",
    "\n",
    "tableau = tableau.sort_values(\"taux_mag\", ascending=False)\n",
    "tableau.round(4)"
   ],
   "id": "cell-008"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Énoncer son degré d'incertitude\n",
    "\n",
    "Un taux calculé sur 340 enfants n'est pas la même affirmation qu'un taux calculé\n",
    "sur 40. L'intervalle de Clopper-Pearson est exact pour une proportion et ne se\n",
    "dérègle pas sur de petits effectifs, ce qui importe pour les communes les moins\n",
    "peuplées."
   ],
   "id": "cell-009"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from scipy.stats import beta\n",
    "\n",
    "def clopper_pearson(succes, n, confiance=0.95):\n",
    "    if n == 0:\n",
    "        return (np.nan, np.nan)\n",
    "    alpha = 1 - confiance\n",
    "    bas = beta.ppf(alpha / 2, succes, n - succes + 1) if succes > 0 else 0.0\n",
    "    haut = beta.ppf(1 - alpha / 2, succes + 1, n - succes) if succes < n else 1.0\n",
    "    return (bas, haut)\n",
    "\n",
    "bornes = tableau.apply(\n",
    "    lambda r: clopper_pearson(r[\"cas_mag\"], r[\"denominateur\"]), axis=1\n",
    ")\n",
    "tableau[\"mag_bas\"] = [b[0] for b in bornes]\n",
    "tableau[\"mag_haut\"] = [b[1] for b in bornes]\n",
    "\n",
    "tableau[[\"commune\", \"denominateur\", \"taux_mag\", \"mag_bas\", \"mag_haut\"]].round(4)"
   ],
   "id": "cell-010"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Représenter le classement avec son incertitude"
   ],
   "id": "cell-011"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "\n",
    "SEUIL_URGENCE = 0.15\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(8, 5))\n",
    "ordre = tableau.sort_values(\"taux_mag\")\n",
    "\n",
    "ax.errorbar(\n",
    "    ordre[\"taux_mag\"] * 100,\n",
    "    range(len(ordre)),\n",
    "    xerr=[\n",
    "        (ordre[\"taux_mag\"] - ordre[\"mag_bas\"]) * 100,\n",
    "        (ordre[\"mag_haut\"] - ordre[\"taux_mag\"]) * 100,\n",
    "    ],\n",
    "    fmt=\"o\",\n",
    "    color=\"#2F5D50\",\n",
    "    ecolor=\"#9AA8A3\",\n",
    "    capsize=3,\n",
    ")\n",
    "\n",
    "ax.axvline(SEUIL_URGENCE * 100, color=\"#B5533C\", linestyle=\"--\", linewidth=1)\n",
    "ax.text(\n",
    "    SEUIL_URGENCE * 100 + 0.3, 0.2,\n",
    "    \"seuil d'urgence (15%)\", color=\"#B5533C\", fontsize=9,\n",
    ")\n",
    "\n",
    "ax.set_yticks(range(len(ordre)))\n",
    "ax.set_yticklabels(ordre[\"commune\"])\n",
    "ax.set_xlabel(\"Prevalence de la MAG par PB (%)\")\n",
    "ax.set_title(\"Malnutrition aigue globale par commune, avec IC 95%\")\n",
    "ax.spines[[\"top\", \"right\"]].set_visible(False)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ],
   "id": "cell-012"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lire le résultat\n",
    "\n",
    "Plusieurs intervalles se recouvrent. L'ordre entre ces communes n'est donc pas\n",
    "soutenu par les données, et une décision qui attribuerait un site PCIMA\n",
    "supplémentaire au seul rang lirait une précision que le dépistage ne possède\n",
    "pas.\n",
    "\n",
    "Notez aussi ce que l'intervalle ne couvre pas. Il s'agit d'un recensement des\n",
    "enfants venus au dépistage et non d'un échantillon probabiliste : l'intervalle ne\n",
    "décrit donc que la variation d'échantillonnage. Savoir si ces enfants\n",
    "ressemblent à ceux qui ne sont pas venus constitue généralement la plus grande\n",
    "source d'erreur, et cela relève de la section des limites de tout rapport\n",
    "construit sur ces données."
   ],
   "id": "cell-013"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "file_extension": ".py"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
