{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Choisir le bon dénominateur — corrigé\n",
    "\n",
    "*Corrigé d'exercice · Fondamentaux de l'analyse de données pour le suivi-évaluation*\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/practice/indicator-denominator.python.fr.ipynb)"
   ],
   "id": "cell-000"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Mode d'emploi\n",
    "\n",
    "Ceci est le corrigé : lisez-le après avoir tenté l'exercice, non à sa place. Les\n",
    "cinq scénarios ont chacun un numérateur admis et un dénominateur contesté, et\n",
    "dans chaque cas la réponse honnête n'est pas « le bon chiffre » mais « le\n",
    "chiffre, et ce qu'il exclut ».\n",
    "\n",
    "Le jeu de données est le registre synthétique de dépistage du PB de\n",
    "l'Artibonite. Rien ici ne décrit un enfant réel."
   ],
   "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",
    "muac[\"screening_date\"] = pd.to_datetime(muac[\"screening_date\"], format=\"%Y-%m-%d\")\n",
    "\n",
    "# Les corrections de la lecon 6, appliquees pour que les scenarios ci-dessous\n",
    "# portent sur les denominateurs et non sur la qualite des donnees.\n",
    "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",
    "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",
    "muac = muac.drop_duplicates()\n",
    "\n",
    "muac[\"mag\"] = (muac[\"muac_mm\"] < 125) | (muac[\"oedema\"] == True)\n",
    "len(muac)"
   ],
   "id": "cell-002"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Scénario 1 — « Quelle proportion des enfants dépistés était malnutrie ? »\n",
    "\n",
    "Le numérateur est admis : les enfants répondant à la définition de cas de la\n",
    "MAG. Quatre dénominateurs sont défendables et donnent quatre chiffres\n",
    "différents."
   ],
   "id": "cell-003"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "candidats = {\n",
    "    \"toutes les lignes du registre\": len(muac),\n",
    "    \"lignes avec une mesure de PB\": int(muac[\"muac_mm\"].notna().sum()),\n",
    "    \"lignes avec PB ou evaluation d'oedeme\": int(\n",
    "        (muac[\"muac_mm\"].notna() | muac[\"oedema\"].notna()).sum()\n",
    "    ),\n",
    "    \"lignes avec PB, oedeme et age\": int(\n",
    "        (\n",
    "            (muac[\"muac_mm\"].notna() | muac[\"oedema\"].notna())\n",
    "            & muac[\"age_months\"].notna()\n",
    "        ).sum()\n",
    "    ),\n",
    "}\n",
    "\n",
    "cas = int(muac[\"mag\"].sum())\n",
    "pd.DataFrame(\n",
    "    {\n",
    "        \"denominateur\": candidats,\n",
    "        \"taux\": {k: cas / v for k, v in candidats.items()},\n",
    "    }\n",
    ").round(4)"
   ],
   "id": "cell-004"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**La réponse.** Retenez « lignes avec PB ou évaluation d'œdème ». Un enfant sans\n",
    "aucune évaluation n'a pas été dépisté pour cet indicateur : l'inclure au\n",
    "dénominateur sous-estime le taux en comptant des non-observations comme des\n",
    "négatifs. Exiger en plus l'âge est trop restrictif — les seuils de PB pour les 6\n",
    "à 59 mois forment une bande unique et n'utilisent pas l'âge, et cette exigence\n",
    "retire une commune bien plus que les autres.\n",
    "\n",
    "**Ce qu'il exclut, énoncé explicitement :** les enfants qui ne sont pas venus se\n",
    "faire dépister. Il s'agit d'un taux parmi les personnes atteintes, non d'une\n",
    "prévalence de population, et le rapport doit le dire.\n",
    "\n",
    "## Scénario 2 — « Quel est notre taux d'aboutissement des référencements ? »\n",
    "\n",
    "Le piège tient à ce que la colonne de décision et la colonne de mesure ont été\n",
    "remplies par deux personnes différentes : quelques enregistrements portent donc\n",
    "un référencement sans mesure derrière. Comptez-les plutôt que de supposer le\n",
    "nombre."
   ],
   "id": "cell-005"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "references = muac[\"outcome\"].isin([\"referred-tsfp\", \"referred-otp\", \"referred-sc\"])\n",
    "sans_mesure = references & muac[\"muac_mm\"].isna()\n",
    "\n",
    "print(f\"referencements consignes  : {int(references.sum())}\")\n",
    "print(f\"dont sans aucune mesure   : {int(sans_mesure.sum())}\")"
   ],
   "id": "cell-006"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**La réponse.** Deux questions distinctes se cachent ici, et elles appellent des\n",
    "dénominateurs différents.\n",
    "\n",
    "- *Le dépistage a-t-il donné lieu à un référencement là où il le fallait ?*\n",
    "  Dénominateur : les enfants répondant à la définition de cas de référencement.\n",
    "  Cela mesure le dépisteur.\n",
    "- *L'enfant référé a-t-il atteint le service ?* Dénominateur : les enfants\n",
    "  référés. Cela mesure le circuit, et ce registre ne peut absolument pas y\n",
    "  répondre — il n'existe aucune trace d'arrivée.\n",
    "\n",
    "Rapporter la seconde à partir de ce fichier reviendrait à inventer un chiffre. La\n",
    "sortie correcte est la première, assortie d'une phrase indiquant que la seconde\n",
    "exige le registre d'admission PCIMA.\n",
    "\n",
    "## Scénario 3 — « La couverture a progressé de 12 % ce trimestre »"
   ],
   "id": "cell-007"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "muac[\"trimestre\"] = muac[\"screening_date\"].dt.to_period(\"Q\")\n",
    "par_trimestre = muac.groupby(\"trimestre\").size()\n",
    "par_trimestre"
   ],
   "id": "cell-008"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**La réponse.** Ce n'est pas de la couverture, et le mot doit être refusé. La\n",
    "couverture est le rapport des cas atteints aux cas existants, et ce registre ne\n",
    "dispose d'aucun dénominateur d'enfants dans la population — seulement de ceux\n",
    "qui se sont présentés. Ce qui a progressé, c'est le volume de dépistage.\n",
    "\n",
    "Notez également que le T1 et le T4 sont tronqués : le premier dépistage a lieu à\n",
    "la mi-janvier et le dernier à la mi-décembre, si bien qu'une comparaison\n",
    "trimestrielle incluant l'un ou l'autre compare des fenêtres inégales."
   ],
   "id": "cell-009"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(\"premier :\", muac[\"screening_date\"].min().date())\n",
    "print(\"dernier :\", muac[\"screening_date\"].max().date())"
   ],
   "id": "cell-010"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Scénario 4 — « Quelle commune est la plus touchée ? »"
   ],
   "id": "cell-011"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "par_commune = (\n",
    "    muac.assign(evalue=muac[\"muac_mm\"].notna() | muac[\"oedema\"].notna())\n",
    "    .groupby(\"commune\")\n",
    "    .agg(evalue=(\"evalue\", \"sum\"), cas=(\"mag\", \"sum\"))\n",
    ")\n",
    "par_commune[\"taux\"] = par_commune[\"cas\"] / par_commune[\"evalue\"]\n",
    "par_commune.sort_values(\"taux\", ascending=False).round(4)"
   ],
   "id": "cell-012"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**La réponse.** Le dénominateur est correct, et la question reste mal posée. La\n",
    "plus petite commune compte moins de 200 évaluations : son intervalle est assez\n",
    "large pour que son rang soit à peu près dépourvu de sens."
   ],
   "id": "cell-013"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from scipy.stats import beta\n",
    "\n",
    "def intervalle(cas, n):\n",
    "    if n == 0:\n",
    "        return (np.nan, np.nan)\n",
    "    bas = beta.ppf(0.025, cas, n - cas + 1) if cas > 0 else 0.0\n",
    "    haut = beta.ppf(0.975, cas + 1, n - cas) if cas < n else 1.0\n",
    "    return (bas, haut)\n",
    "\n",
    "bornes = par_commune.apply(lambda r: intervalle(r[\"cas\"], r[\"evalue\"]), axis=1)\n",
    "par_commune[\"bas\"] = [b[0] for b in bornes]\n",
    "par_commune[\"haut\"] = [b[1] for b in bornes]\n",
    "par_commune.sort_values(\"taux\", ascending=False)[\n",
    "    [\"evalue\", \"taux\", \"bas\", \"haut\"]\n",
    "].round(4)"
   ],
   "id": "cell-014"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Plusieurs intervalles se recouvrent. « La plus touchée » n'est une question\n",
    "répondable que pour la commune dont l'intervalle se détache des autres, et un\n",
    "tableau sans intervalles vous aurait laissé classer les douze avec une assurance\n",
    "infondée.\n",
    "\n",
    "## Scénario 5 — « Notre programme a touché 4 218 enfants cette année »"
   ],
   "id": "cell-015"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(f\"lignes                      : {len(muac)}\")\n",
    "print(f\"child_id distincts          : {muac['child_id'].nunique()}\")\n",
    "\n",
    "cle = [\"commune\", \"screening_date\", \"age_months\", \"sex\", \"muac_mm\"]\n",
    "suspects = muac.duplicated(subset=cle, keep=False) & muac[\"muac_mm\"].notna()\n",
    "print(f\"reenregistrements suspectes : {int(suspects.sum())}\")"
   ],
   "id": "cell-016"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**La réponse.** Des lignes ne sont pas des enfants. Les doublons exacts ont déjà\n",
    "été retirés ci-dessus, mais quelques enfants ont été réenregistrés sous un\n",
    "nouvel identifiant et ne partagent aucune clé : on ne les repère qu'en appariant\n",
    "sur commune, date, âge, sexe et mesure, et ils restent indiscernables d'une\n",
    "coïncidence authentique sans le registre papier.\n",
    "\n",
    "L'affirmation défendable est donc un chiffre assorti de sa réserve : tant\n",
    "d'enregistrements distincts, dont un certain nombre peuvent être des\n",
    "réenregistrements du même enfant. Notez que la vérification ci-dessus signale\n",
    "des *suspects* et non des doublons — deux enfants de même âge et de même sexe\n",
    "dépistés dans une même commune le même jour avec la même mesure, c'est tout à\n",
    "fait possible, et dans une campagne de cette taille c'est même probable. Les\n",
    "trancher exige le registre papier, non une ligne de pandas supplémentaire.\n",
    "\n",
    "## La règle qui sous-tend les cinq scénarios\n",
    "\n",
    "Rédigez le dénominateur sous forme de phrase avant de calculer quoi que ce soit.\n",
    "Si vous n'y parvenez pas, vous n'avez pas encore d'indicateur : vous avez une\n",
    "colonne dont vous vous apprêtez à prendre la moyenne. Et ce que le dénominateur\n",
    "exclut figure dans le rapport, non dans votre tête."
   ],
   "id": "cell-017"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "file_extension": ".py"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
