{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Échelles de service du JMP pour l'eau, l'assainissement et l'hygiène\n",
    "\n",
    "*Enquête ménage EAH, 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/wash-household-survey-2024/jmp-ladders.python.fr.ipynb)"
   ],
   "id": "cell-000"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ce que produit ce document\n",
    "\n",
    "Chaque ménage placé sur les trois échelles de service du JMP — eau de boisson,\n",
    "assainissement, hygiène — et la couverture par district. Ce sont les définitions\n",
    "au regard desquelles sont rapportés les indicateurs de l'ODD 6 : les appliquer\n",
    "correctement fait la différence entre un chiffre qu'un cluster acceptera et un\n",
    "chiffre qu'il renverra.\n",
    "\n",
    "Valeurs de référence issues des notes de qualité : environ 13 % des ménages sous\n",
    "le minimum Sphere de 15 litres par personne et par jour, environ 39 % au-delà de\n",
    "30 minutes de trajet aller-retour, environ 13 % de défécation à l'air libre, et\n",
    "environ 34 % de service d'hygiène de base.\n",
    "\n",
    "Tous les jeux de données de cette plateforme sont synthétiques. Aucun ménage réel\n",
    "n'est décrit.\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",
    "    \"wash-household-survey-2024.v1.csv\"\n",
    ")\n",
    "\n",
    "wash = pd.read_csv(URL, dtype={\"household_id\": \"string\", \"community\": \"string\"})\n",
    "print(wash.shape)\n",
    "wash.head()"
   ],
   "id": "cell-002"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Normaliser le district avant de regrouper dessus\n",
    "\n",
    "Une équipe d'enquêteurs a orthographié le nom du district Nord-Ouest de quatre\n",
    "manières différentes. Regroupez sans normaliser et vous obtenez six districts au\n",
    "lieu de trois, en éclatant le moins performant en quatre morceaux assez petits\n",
    "pour ne rien signaler."
   ],
   "id": "cell-003"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(wash[\"district\"].value_counts())"
   ],
   "id": "cell-004"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "wash[\"district\"] = (\n",
    "    wash[\"district\"].str.strip().str.lower().str.replace(\" \", \"-\", regex=False)\n",
    ")\n",
    "print(wash[\"district\"].value_counts())"
   ],
   "id": "cell-005"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Normalisez avant de regrouper, systématiquement. C'est la manière la plus\n",
    "fréquente pour un tableau par district de dissimuler discrètement son plus\n",
    "mauvais résultat.\n",
    "\n",
    "## L'échelle de l'eau de boisson\n",
    "\n",
    "L'échelle **n'est pas une propriété de la source seule.** Une source améliorée à\n",
    "plus de 30 minutes de trajet aller-retour relève du service *limité*, non du\n",
    "service de base — et environ un quart de ces ménages se retrouvent en service\n",
    "limité pour cette seule raison. Une analyse qui classe sur le seul type de source\n",
    "les manque tous."
   ],
   "id": "cell-006"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "AMELIOREES = {\n",
    "    \"piped-into-dwelling\", \"piped-into-yard\", \"public-tap\", \"borehole\",\n",
    "    \"protected-well\", \"protected-spring\", \"tanker-truck\",\n",
    "}\n",
    "NON_AMELIOREES = {\"unprotected-well\", \"unprotected-spring\"}\n",
    "\n",
    "def echelle_eau(ligne):\n",
    "    source = ligne[\"water_source\"]\n",
    "    if source == \"surface-water\":\n",
    "        return \"eau de surface\"\n",
    "    if source in NON_AMELIOREES:\n",
    "        return \"non amelioree\"\n",
    "    if pd.isna(ligne[\"round_trip_minutes\"]):\n",
    "        return \"amelioree, duree inconnue\"\n",
    "    return \"base\" if ligne[\"round_trip_minutes\"] <= 30 else \"limite\"\n",
    "\n",
    "wash[\"service_eau\"] = wash.apply(echelle_eau, axis=1)\n",
    "(wash[\"service_eau\"].value_counts(normalize=True) * 100).round(1)"
   ],
   "id": "cell-007"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Le niveau « géré en toute sécurité » est délibérément absent. Il exige que la\n",
    "source soit sur la parcelle, disponible au besoin **et** exempte de\n",
    "contamination — or cette enquête ne teste la qualité que sur un tiers des\n",
    "ménages, si bien que l'échelon supérieur ne peut être attribué à la majeure\n",
    "partie de l'échantillon. Rapporter le service de base et s'arrêter là est\n",
    "honnête ; inventer un chiffre de service géré en toute sécurité ne l'est pas.\n",
    "\n",
    "## L'échelle de l'assainissement"
   ],
   "id": "cell-008"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ASSAINISSEMENT_AMELIORE = {\n",
    "    \"flush-to-sewer\", \"flush-to-septic\", \"vip-latrine\", \"pit-latrine-with-slab\",\n",
    "}\n",
    "\n",
    "def echelle_assainissement(ligne):\n",
    "    installation = ligne[\"sanitation_facility\"]\n",
    "    if installation == \"open-defecation\":\n",
    "        return \"defecation a l'air libre\"\n",
    "    if installation not in ASSAINISSEMENT_AMELIORE:\n",
    "        return \"non amelioree\"\n",
    "    return \"limite\" if ligne[\"shared_sanitation\"] else \"base\"\n",
    "\n",
    "wash[\"service_assainissement\"] = wash.apply(echelle_assainissement, axis=1)\n",
    "(wash[\"service_assainissement\"].value_counts(normalize=True) * 100).round(1)"
   ],
   "id": "cell-009"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "C'est le partage qui sépare le service de base du service limité. Un ménage\n",
    "disposant d'une latrine VIP en parfait état mais partagée avec trois autres\n",
    "relève du service *limité*, et un tableau construit sur le seul type\n",
    "d'installation le rapportera comme service de base.\n",
    "\n",
    "## L'échelle de l'hygiène"
   ],
   "id": "cell-010"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def echelle_hygiene(ligne):\n",
    "    if ligne[\"handwashing_facility\"] == \"no-facility\":\n",
    "        return \"aucune installation\"\n",
    "    return \"base\" if ligne[\"soap_observed\"] else \"limite\"\n",
    "\n",
    "wash[\"service_hygiene\"] = wash.apply(echelle_hygiene, axis=1)\n",
    "(wash[\"service_hygiene\"].value_counts(normalize=True) * 100).round(1)"
   ],
   "id": "cell-011"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "L'hygiène de base exige une installation **avec eau et savon présents**,\n",
    "observés et non déclarés. La distinction compte : un tiers de ces ménages\n",
    "possèdent une installation sans savon, et la question « vous lavez-vous les\n",
    "mains ? » les aurait tous comptés comme conformes.\n",
    "\n",
    "## La couverture par district"
   ],
   "id": "cell-012"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def couverture(df, colonne, niveau):\n",
    "    return (\n",
    "        df.groupby(\"district\")[colonne]\n",
    "        .apply(lambda s: (s == niveau).mean() * 100)\n",
    "        .round(1)\n",
    "    )\n",
    "\n",
    "tableau = pd.DataFrame({\n",
    "    \"eau de base\": couverture(wash, \"service_eau\", \"base\"),\n",
    "    \"assainissement de base\": couverture(wash, \"service_assainissement\", \"base\"),\n",
    "    \"hygiene de base\": couverture(wash, \"service_hygiene\", \"base\"),\n",
    "    \"defecation a l'air libre\": couverture(\n",
    "        wash, \"service_assainissement\", \"defecation a l'air libre\"\n",
    "    ),\n",
    "    \"menages\": wash.groupby(\"district\").size(),\n",
    "})\n",
    "tableau.sort_values(\"eau de base\")"
   ],
   "id": "cell-013"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Le standard Sphere de quantité\n",
    "\n",
    "L'échelle ne dit rien de la quantité. Sphere fixe un minimum de 15 litres par\n",
    "personne et par jour, et c'est une question distincte de celle de savoir si la\n",
    "source est améliorée."
   ],
   "id": "cell-014"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "MINIMUM_SPHERE = 15\n",
    "\n",
    "sous_minimum = wash[\"litres_per_person_day\"] < MINIMUM_SPHERE\n",
    "print(f\"sous {MINIMUM_SPHERE} l/p/j : {sous_minimum.mean():.1%}\")\n",
    "print(f\"au-dela de 30 minutes aller-retour : {(wash['round_trip_minutes'] > 30).mean():.1%}\")\n",
    "\n",
    "pd.crosstab(\n",
    "    wash[\"service_eau\"],\n",
    "    sous_minimum.map({True: \"sous Sphere\", False: \"au niveau ou au-dessus\"}),\n",
    "    normalize=\"index\",\n",
    ").round(3)"
   ],
   "id": "cell-015"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Des ménages en service *de base* passent tout de même sous le minimum Sphere.\n",
    "L'accès et la quantité sont deux indicateurs différents, et aucun ne se\n",
    "substitue à l'autre.\n",
    "\n",
    "## Les erreurs d'unité que personne ne remarque\n",
    "\n",
    "Onze enregistrements portent le temps de collecte en heures plutôt qu'en\n",
    "minutes, et quatorze portent les litres pour le ménage entier plutôt que par\n",
    "personne. Les deux sont parfaitement plausibles isolément — un trajet de 2, ou\n",
    "40 litres par jour — et ne se détachent que rapportés à la taille du ménage."
   ],
   "id": "cell-016"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "litres_suspects = (\n",
    "    wash[\"litres_per_person_day\"] > 60\n",
    ") & wash[\"household_size\"].notna()\n",
    "\n",
    "wash.loc[litres_suspects, [\n",
    "    \"household_id\", \"household_size\", \"litres_per_person_day\",\n",
    "]].assign(\n",
    "    total_menage_implique=lambda d: d[\"litres_per_person_day\"] * d[\"household_size\"],\n",
    "    si_total_menage=lambda d: d[\"litres_per_person_day\"] / d[\"household_size\"],\n",
    ").head(10)"
   ],
   "id": "cell-017"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lisez la dernière colonne : divisées par la taille du ménage, ces valeurs\n",
    "redeviennent ordinaires. C'est la signature d'un chiffre par ménage saisi dans\n",
    "une colonne par personne. Signalez-les ; ne les redimensionnez pas en silence,\n",
    "car vous ne pouvez pas prouver quelle lecture l'enquêteur avait en tête.\n",
    "\n",
    "## Ce qu'il faut rapporter\n",
    "\n",
    "Énoncez l'échelon de l'échelle, le dénominateur sur lequel il repose, et\n",
    "l'indicateur de quantité séparément. Et dites quels ménages n'ont pas pu être\n",
    "classés : ceux dont la durée de collecte n'a pas été relevée ne sont pas en\n",
    "service « de base », ils sont inconnus, et les faire basculer dans le décompte du\n",
    "service de base est la manière dont un chiffre de couverture dérive vers le haut\n",
    "sans que personne ne l'ait décidé."
   ],
   "id": "cell-018"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "file_extension": ".py"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
