{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Couverture et taux d'abandon par formation sanitaire\n",
    "\n",
    "*Couverture vaccinale de routine, 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/routine-vaccination-coverage-2024/coverage-dropout.python.fr.ipynb)"
   ],
   "id": "cell-000"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ce que produit ce document\n",
    "\n",
    "La couverture mensuelle et annuelle par antigène, le taux d'abandon entre penta1\n",
    "et penta3 par formation sanitaire, et le signalement de sur-rapportage — appliqué\n",
    "à un niveau d'agrégation où il signifie réellement quelque chose.\n",
    "\n",
    "Tous les jeux de données de cette plateforme sont synthétiques. La couverture\n",
    "présentée ne décrit aucun district réel.\n",
    "\n",
    "## Mise en place"
   ],
   "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",
    "    \"vaccination-coverage-2024.v1.csv\"\n",
    ")\n",
    "\n",
    "epi = pd.read_csv(URL, dtype={\"facility_id\": \"string\"})\n",
    "epi[\"period\"] = pd.to_datetime(epi[\"period\"])\n",
    "epi[\"mois\"] = epi[\"period\"].dt.to_period(\"M\")\n",
    "\n",
    "print(f\"{epi['facility_id'].nunique()} formations, \"\n",
    "      f\"{epi['mois'].nunique()} mois, \"\n",
    "      f\"{epi['antigen'].nunique()} antigenes\")\n",
    "epi.head()"
   ],
   "id": "cell-002"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Un rapport non transmis n'est pas zéro enfant vacciné\n",
    "\n",
    "Une formation qui n'a pas rapporté apparaît sous la forme d'une ligne avec\n",
    "`report_submitted` à faux et zéro dose — et **non** comme une ligne absente.\n",
    "Sommez les doses sans filtrer sur ce drapeau et vous affirmez qu'aucun enfant de\n",
    "cette aire de responsabilité n'a été vacciné ce mois-là."
   ],
   "id": "cell-003"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "non_transmis = ~epi[\"report_submitted\"]\n",
    "print(f\"formations-mois non rapportees : {int(non_transmis.sum() / epi['antigen'].nunique())}\")\n",
    "print(f\"lignes non rapportees a 0 dose : \"\n",
    "      f\"{int((non_transmis & (epi['doses_administered'] == 0)).sum())} \"\n",
    "      f\"sur {int(non_transmis.sum())}\")"
   ],
   "id": "cell-004"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Toutes, sans exception. C'est là le piège : le zéro a l'apparence d'une donnée.\n",
    "\n",
    "## Le dénominateur est annuel, le rapportage est mensuel\n",
    "\n",
    "`target_population` est la cible **annuelle**, répétée sur chaque ligne. La\n",
    "couverture mensuelle est le nombre de doses rapporté à la cible divisée par\n",
    "douze. Diviser par le chiffre annuel complet sous-estime la couverture d'un\n",
    "facteur douze, et c'est l'erreur la plus courante face à des données de cette\n",
    "forme."
   ],
   "id": "cell-005"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "MOIS_PAR_AN = 12\n",
    "\n",
    "penta3 = epi[epi[\"antigen\"] == \"penta3\"].copy()\n",
    "\n",
    "mensuel = penta3.groupby(\"mois\").apply(\n",
    "    lambda g: pd.Series({\n",
    "        \"doses\": g[\"doses_administered\"].sum(),\n",
    "        \"cible_annuelle\": g[\"target_population\"].sum(),\n",
    "        \"completude\": g[\"report_submitted\"].mean(),\n",
    "    }),\n",
    "    include_groups=False,\n",
    ")\n",
    "\n",
    "mensuel[\"denominateur_annuel_faux\"] = mensuel[\"doses\"] / mensuel[\"cible_annuelle\"]\n",
    "mensuel[\"denominateur_mensuel_correct\"] = mensuel[\"doses\"] / (\n",
    "    mensuel[\"cible_annuelle\"] / MOIS_PAR_AN\n",
    ")\n",
    "mensuel.round(3)"
   ],
   "id": "cell-006"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La première colonne donne une couverture d'environ 5 %, qui se lirait comme un\n",
    "effondrement catastrophique du programme. C'est une erreur d'arithmétique.\n",
    "\n",
    "## L'abandon entre penta1 et penta3\n",
    "\n",
    "L'abandon est la part des enfants qui commencent la série sans la terminer :\n",
    "`(penta1 − penta3) / penta1`. C'est un meilleur signal de programme que la\n",
    "couverture, car il ne dépend d'aucune estimation de population — les deux termes\n",
    "proviennent du même registre."
   ],
   "id": "cell-007"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "rapportees = epi[epi[\"report_submitted\"]]\n",
    "\n",
    "serie = (\n",
    "    rapportees[rapportees[\"antigen\"].isin([\"penta1\", \"penta3\"])]\n",
    "    .pivot_table(\n",
    "        index=\"facility_id\", columns=\"antigen\",\n",
    "        values=\"doses_administered\", aggfunc=\"sum\",\n",
    "    )\n",
    "    .dropna()\n",
    ")\n",
    "\n",
    "serie[\"abandon\"] = (serie[\"penta1\"] - serie[\"penta3\"]) / serie[\"penta1\"]\n",
    "serie = serie.sort_values(\"abandon\", ascending=False)\n",
    "\n",
    "print(f\"abandon median : {serie['abandon'].median():.1%}\")\n",
    "serie.head(10).round(3)"
   ],
   "id": "cell-008"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "L'abandon médian avoisine 14 %. Une formation rapportant un abandon proche de\n",
    "zéro mérite autant de suspicion qu'une formation en rapportant un très élevé :\n",
    "un abandon quasi nul signifie généralement que le décompte de penta3 a été\n",
    "reconstitué à partir de celui de penta1 plutôt que compté.\n",
    "\n",
    "## Le signalement de sur-rapportage, appliqué au bon niveau\n",
    "\n",
    "Un penta3 supérieur au penta1 est impossible dans un calendrier réel : un enfant\n",
    "ne peut pas recevoir la troisième dose sans la première. C'est le signal\n",
    "classique de sur-rapportage.\n",
    "\n",
    "Appliqué mois par mois, il ne sert à rien ici :"
   ],
   "id": "cell-009"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "par_mois = (\n",
    "    rapportees[rapportees[\"antigen\"].isin([\"penta1\", \"penta3\"])]\n",
    "    .pivot_table(\n",
    "        index=[\"facility_id\", \"mois\"], columns=\"antigen\",\n",
    "        values=\"doses_administered\",\n",
    "    )\n",
    "    .dropna()\n",
    ")\n",
    "par_mois[\"impossible\"] = par_mois[\"penta3\"] > par_mois[\"penta1\"]\n",
    "\n",
    "signalees_une_fois = par_mois.groupby(\"facility_id\")[\"impossible\"].any().sum()\n",
    "print(f\"formations signalees au moins une fois : {signalees_une_fois} \"\n",
    "      f\"sur {epi['facility_id'].nunique()}\")"
   ],
   "id": "cell-010"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Trente-sept sur trente-huit. Un contrôle qui signale la quasi-totalité du\n",
    "district n'identifie rien : le bruit ordinaire d'un mois à l'autre franchit cette\n",
    "ligne en permanence, parce que les deux doses sont administrées à des enfants\n",
    "différents à des mois différents et que les décomptes n'ont aucune raison\n",
    "d'évoluer de concert.\n",
    "\n",
    "Le contrôle doit s'appliquer là où le bruit se compense :"
   ],
   "id": "cell-011"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "persistantes = serie[serie[\"abandon\"] < 0]\n",
    "print(f\"formations avec penta3 > penta1 sur le total annuel : {len(persistantes)}\")\n",
    "persistantes.round(3)"
   ],
   "id": "cell-012"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Six formations. Voilà une liste sur laquelle un superviseur peut agir."
   ],
   "id": "cell-013"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mois_signales = par_mois.groupby(\"facility_id\")[\"impossible\"].agg([\"sum\", \"size\"])\n",
    "mois_signales.columns = [\"mois signales\", \"mois rapportes\"]\n",
    "mois_signales.loc[persistantes.index].sort_values(\"mois signales\", ascending=False)"
   ],
   "id": "cell-014"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La couverture par antigène, sur un dénominateur qui tient"
   ],
   "id": "cell-015"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def couverture_annuelle(df):\n",
    "    rapportees_seules = df[df[\"report_submitted\"]]\n",
    "    return (\n",
    "        rapportees_seules.groupby(\"antigen\")\n",
    "        .apply(\n",
    "            lambda g: g[\"doses_administered\"].sum()\n",
    "            / (g[\"target_population\"].sum() / MOIS_PAR_AN),\n",
    "            include_groups=False,\n",
    "        )\n",
    "        .rename(\"couverture\")\n",
    "        .to_frame()\n",
    "        .round(3)\n",
    "    )\n",
    "\n",
    "couverture_annuelle(epi)"
   ],
   "id": "cell-016"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Le dénominateur ne compte ici que les formations ayant rapporté, ce qui maintient\n",
    "numérateur et dénominateur sur le même ensemble de formations. C'est l'objet de\n",
    "l'exemple sur l'ajustement pour la complétude, et c'est ce qui distingue une\n",
    "couverture qui varie avec le rapportage d'une couverture qui varie avec la\n",
    "vaccination.\n",
    "\n",
    "## Ce qu'il faut rapporter\n",
    "\n",
    "La couverture avec son dénominateur énoncé, l'abandon comme signal de programme\n",
    "indépendant de toute estimation de population, et la liste de sur-rapportage\n",
    "accompagnée du niveau d'agrégation auquel elle a été calculée. Un signalement qui\n",
    "se déclenche sur trente-sept formations sur trente-huit n'est pas un constat :\n",
    "c'est un contrôle mal spécifié."
   ],
   "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
}
