{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Liste de supervision d'après la couverture et l'abandon — notebook d'analyse\n",
    "\n",
    "*Analyse de la couverture vaccinale et de l'abandon · livrable de projet*\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/projects/vaccination-coverage-analysis/notebooks/coverage-analysis.python.fr.ipynb)"
   ],
   "id": "cell-000"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La décision que ce document produit\n",
    "\n",
    "Lesquelles des trente-huit formations sanitaires — dix — reçoivent une visite de\n",
    "supervision au trimestre prochain. Dix est ce que permet le temps du superviseur :\n",
    "il s'agit donc d'une liste courte à plafond ferme, et la sortie est la liste\n",
    "assortie du motif d'inscription de chacune — une visite sans question posée est\n",
    "une journée perdue.\n",
    "\n",
    "Le problème que ce projet existe pour corriger : le tableau de bord précédent\n",
    "classait les formations d'après la couverture rapportée sans tenir compte du fait\n",
    "qu'elles avaient rapporté ou non, si bien qu'une formation silencieuse\n",
    "ressemblait à une formation défaillante.\n",
    "\n",
    "Destinataire : le superviseur PEV du district.\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",
    "MONTHS = 12\n",
    "VISITS = 10\n",
    "\n",
    "epi = pd.read_csv(URL, dtype={\"facility_id\": \"string\"})\n",
    "epi[\"period\"] = pd.to_datetime(epi[\"period\"])\n",
    "epi[\"month\"] = epi[\"period\"].dt.to_period(\"M\")\n",
    "\n",
    "print(f\"{epi['facility_id'].nunique()} formations, {epi['month'].nunique()} mois\")\n",
    "print(f\"visites de supervision disponibles : {VISITS}\")"
   ],
   "id": "cell-002"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La complétude du rapportage d'abord, car elle conditionne tout le reste\n",
    "\n",
    "Une formation qui n'a pas rapporté apparaît sous forme de ligne à zéro dose.\n",
    "Sommez sans filtrer et une formation silencieuse devient une formation qui n'a\n",
    "vacciné personne."
   ],
   "id": "cell-003"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "facility_months = epi.drop_duplicates([\"facility_id\", \"month\"])\n",
    "\n",
    "completeness = facility_months.groupby(\"facility_id\")[\"report_submitted\"].mean()\n",
    "by_month = facility_months.groupby(\"month\")[\"report_submitted\"].mean()\n",
    "\n",
    "print(f\"completude du district : {facility_months['report_submitted'].mean():.1%}\")\n",
    "by_month.round(3)"
   ],
   "id": "cell-004"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Août et septembre sont les mois problématiques du district, non ceux d'une\n",
    "formation. Une liste de supervision bâtie sur ces deux mois seuls irait visiter\n",
    "quiconque s'est trouvé silencieux pendant une perturbation générale.\n",
    "\n",
    "## Trois signaux, répondant chacun à une question différente"
   ],
   "id": "cell-005"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "reported = epi[epi[\"report_submitted\"]]\n",
    "\n",
    "series = (\n",
    "    reported[reported[\"antigen\"].isin([\"penta1\", \"penta3\"])]\n",
    "    .pivot_table(index=\"facility_id\", columns=\"antigen\",\n",
    "                 values=\"doses_administered\", aggfunc=\"sum\")\n",
    "    .dropna()\n",
    ")\n",
    "series[\"dropout\"] = (series[\"penta1\"] - series[\"penta3\"]) / series[\"penta1\"]\n",
    "series[\"completeness\"] = completeness\n",
    "\n",
    "# Coverage on a denominator restricted to the months the facility reported, so\n",
    "# it measures vaccination rather than reporting.\n",
    "target = reported.groupby(\"facility_id\")[\"target_population\"].first()\n",
    "months_reported = facility_months.groupby(\"facility_id\")[\"report_submitted\"].sum()\n",
    "penta3 = reported[reported[\"antigen\"] == \"penta3\"].groupby(\"facility_id\")[\n",
    "    \"doses_administered\"\n",
    "].sum()\n",
    "series[\"coverage\"] = penta3 / (target * months_reported / MONTHS)\n",
    "\n",
    "series[[\"penta1\", \"penta3\", \"dropout\", \"completeness\", \"coverage\"]].describe().round(3)"
   ],
   "id": "cell-006"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Les signalements, et celui qui ne mord pas"
   ],
   "id": "cell-007"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "DROPOUT_LIMIT = 0.25\n",
    "COMPLETENESS_FLOOR = 0.70\n",
    "\n",
    "series[\"over_reporting\"] = series[\"dropout\"] < 0\n",
    "series[\"high_dropout\"] = series[\"dropout\"] > DROPOUT_LIMIT\n",
    "series[\"low_completeness\"] = series[\"completeness\"] < COMPLETENESS_FLOOR\n",
    "\n",
    "flags = pd.Series({\n",
    "    \"penta3 superieur au penta1 sur le total annuel\": int(series[\"over_reporting\"].sum()),\n",
    "    f\"abandon superieur a {DROPOUT_LIMIT:.0%}\": int(series[\"high_dropout\"].sum()),\n",
    "    f\"completude inferieure a {COMPLETENESS_FLOOR:.0%}\": int(series[\"low_completeness\"].sum()),\n",
    "})\n",
    "flags"
   ],
   "id": "cell-008"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Le signalement d'abandon n'attrape rien ici**, et cela mérite d'être rapporté\n",
    "plutôt que de retirer discrètement le critère. L'abandon médian avoisine 14 % et\n",
    "aucune formation ne dépasse 25 % sur le total annuel : une liste de supervision\n",
    "bâtie sur le seul abandon serait vide. Conserver le contrôle et indiquer qu'il\n",
    "s'est déclenché zéro fois est plus utile au superviseur du trimestre prochain que\n",
    "de le supprimer."
   ],
   "id": "cell-009"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "series[\"flags\"] = (\n",
    "    series[\"over_reporting\"].astype(int)\n",
    "    + series[\"high_dropout\"].astype(int)\n",
    "    + series[\"low_completeness\"].astype(int)\n",
    ")\n",
    "int((series[\"flags\"] > 0).sum())"
   ],
   "id": "cell-010"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Quatorze formations échouent à au moins un contrôle et dix visites sont\n",
    "disponibles : la liste doit donc être priorisée et non simplement filtrée.\n",
    "\n",
    "## Prioriser, avec le motif attaché\n",
    "\n",
    "Le sur-rapportage prime sur la faible complétude : une formation qui rapporte\n",
    "plus de troisièmes doses que de premières produit des chiffres faux, alors qu'une\n",
    "formation qui n'a pas rapporté produit des chiffres absents. L'absence se répare\n",
    "en demandant ; le faux est déjà entré dans le total du district."
   ],
   "id": "cell-011"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def reason(row):\n",
    "    reasons = []\n",
    "    if row[\"over_reporting\"]:\n",
    "        reasons.append(\"penta3 > penta1 sur l'annee\")\n",
    "    if row[\"low_completeness\"]:\n",
    "        reasons.append(f\"a rapporte {row['completeness']:.0%} des mois\")\n",
    "    if row[\"high_dropout\"]:\n",
    "        reasons.append(f\"abandon {row['dropout']:.0%}\")\n",
    "    return \"; \".join(reasons)\n",
    "\n",
    "series[\"priority\"] = (\n",
    "    series[\"over_reporting\"].astype(int) * 2 + series[\"low_completeness\"].astype(int)\n",
    ")\n",
    "series[\"reason\"] = series.apply(reason, axis=1)\n",
    "\n",
    "shortlist = (\n",
    "    series[series[\"flags\"] > 0]\n",
    "    .sort_values([\"priority\", \"completeness\"], ascending=[False, True])\n",
    "    .head(VISITS)\n",
    ")\n",
    "\n",
    "shortlist[[\"completeness\", \"dropout\", \"coverage\", \"reason\"]].round(3)"
   ],
   "id": "cell-012"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "not_visited = (\n",
    "    series[series[\"flags\"] > 0]\n",
    "    .drop(index=shortlist.index)\n",
    "    [[\"completeness\", \"dropout\", \"reason\"]]\n",
    "    .round(3)\n",
    ")\n",
    "print(f\"en echec sur un controle mais non visitees ce trimestre : {len(not_visited)}\")\n",
    "not_visited"
   ],
   "id": "cell-013"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Nommer les formations ayant échoué à un contrôle sans figurer sur la liste est\n",
    "tout l'intérêt d'une liste plafonnée. Ce sont les premières candidates du\n",
    "trimestre suivant, et un superviseur qui l'ignore supposera que la liste était\n",
    "exhaustive.\n",
    "\n",
    "## Le classement qui était faux auparavant"
   ],
   "id": "cell-014"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "naive_coverage = penta3 / target          # every month counted, reported or not\n",
    "comparison = pd.DataFrame({\n",
    "    \"denominateur restreint aux declarants\": series[\"coverage\"],\n",
    "    \"tous les mois comptes\": naive_coverage,\n",
    "    \"completude\": series[\"completeness\"],\n",
    "})\n",
    "comparison[\"rang_correct\"] = comparison[\"denominateur restreint aux declarants\"].rank(ascending=True)\n",
    "comparison[\"rang_naif\"] = comparison[\"tous les mois comptes\"].rank(ascending=True)\n",
    "comparison[\"variation_rang\"] = (\n",
    "    comparison[\"rang_correct\"] - comparison[\"rang_naif\"]\n",
    ").astype(int)\n",
    "\n",
    "comparison.sort_values(\"variation_rang\", key=abs, ascending=False).head(8).round(3)"
   ],
   "id": "cell-015"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Des formations se déplacent de plusieurs rangs entre les deux classements, et\n",
    "elles se déplacent selon leur fréquence de rapportage plutôt que selon leur\n",
    "qualité de vaccination. C'est le défaut qu'a livré le tableau de bord précédent,\n",
    "et la raison d'être de ce notebook.\n",
    "\n",
    "## Ce que cela n'établit pas\n",
    "\n",
    "La couverture repose sur `target_population`, une estimation administrative que\n",
    "ce jeu de données prend pour acquise. Là où l'estimation est fausse, le chiffre de\n",
    "couverture l'est dans le même sens, et une formation dont l'aire de responsabilité\n",
    "est surestimée paraîtra sous-performante quel que soit le nombre d'enfants\n",
    "qu'elle atteint.\n",
    "\n",
    "La liste de supervision est donc une liste de formations dont les **données**\n",
    "méritent une conversation. Savoir si le service lui-même défaille est ce à quoi\n",
    "sert la visite."
   ],
   "id": "cell-016"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "file_extension": ".py"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
