{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Choisir six communautés pour la réhabilitation — notebook d'analyse\n",
    "\n",
    "*Couverture EAH et qualité de l'eau par district · 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/wash-coverage-analysis/notebooks/wash-ladders.python.fr.ipynb)"
   ],
   "id": "cell-000"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La décision que ce document produit\n",
    "\n",
    "Lesquelles des dix-huit communautés — six — bénéficient d'une réhabilitation de\n",
    "point d'eau au prochain cycle de financement. Six est ce que couvre le budget.\n",
    "\n",
    "Le problème que ce projet existe pour corriger : la couverture était rapportée en\n",
    "comptant les sources améliorées et en ignorant entièrement le temps de collecte,\n",
    "si bien qu'une communauté dotée d'un forage fonctionnel mais éloigné obtenait le\n",
    "même score qu'une communauté avec un robinet dans la cour.\n",
    "\n",
    "Destinataires : le coordinateur EAH et l'autorité de l'eau du district.\n",
    "\n",
    "Tous les jeux de données de cette plateforme sont synthétiques. Aucun ménage ni\n",
    "aucune communauté réels ne sont décrits.\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",
    "REHABILITATIONS = 6\n",
    "SPHERE_MINIMUM = 15\n",
    "WALK_LIMIT = 30\n",
    "\n",
    "wash = pd.read_csv(URL, dtype={\"household_id\": \"string\", \"community\": \"string\"})\n",
    "print(f\"{len(wash)} menages\")"
   ],
   "id": "cell-002"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Normaliser le district avant tout regroupement\n",
    "\n",
    "Une équipe d'enquêteurs a orthographié Nord-Ouest de quatre manières. Sans\n",
    "normalisation, cela éclate le district le moins performant en quatre fragments,\n",
    "dont aucun ne paraît alarmant."
   ],
   "id": "cell-003"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(wash[\"district\"].value_counts().to_dict())"
   ],
   "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(f\"districts apres normalisation : {wash['district'].nunique()}\")\n",
    "print(f\"communautes : {wash['community'].nunique()}\")"
   ],
   "id": "cell-005"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La classification que l'ancien rapport se trompait à établir\n",
    "\n",
    "L'échelle de l'eau de boisson du JMP n'est pas une propriété de la source. Une\n",
    "source améliorée à plus de trente minutes de trajet aller-retour relève du service\n",
    "**limité**, non du service de base — et cette distinction est la raison même pour\n",
    "laquelle ce projet a été commandé."
   ],
   "id": "cell-006"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "IMPROVED = {\n",
    "    \"piped-into-dwelling\", \"piped-into-yard\", \"public-tap\", \"borehole\",\n",
    "    \"protected-well\", \"protected-spring\", \"tanker-truck\",\n",
    "}\n",
    "\n",
    "wash[\"improved_source\"] = wash[\"water_source\"].isin(IMPROVED)\n",
    "wash[\"basic_water\"] = wash[\"improved_source\"] & (\n",
    "    wash[\"round_trip_minutes\"] <= WALK_LIMIT\n",
    ")\n",
    "\n",
    "pd.Series({\n",
    "    \"source amelioree (l'ancienne mesure)\": round(wash[\"improved_source\"].mean(), 3),\n",
    "    \"service de base (source et duree)\": round(wash[\"basic_water\"].mean(), 3),\n",
    "    \"amelioree mais au-dela de 30 minutes\": round(\n",
    "        (wash[\"improved_source\"] & ~wash[\"basic_water\"]).mean(), 3\n",
    "    ),\n",
    "})"
   ],
   "id": "cell-007"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un quart des ménages relèvent du service limité pour le seul temps de collecte.\n",
    "Sous l'ancienne mesure, chacun d'eux était compté comme couvert.\n",
    "\n",
    "## Le besoin par communauté, sur les quatre éléments que change une réhabilitation"
   ],
   "id": "cell-008"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "communities = wash.groupby([\"district\", \"community\"]).agg(\n",
    "    menages=(\"household_id\", \"size\"),\n",
    "    basic_water=(\"basic_water\", \"mean\"),\n",
    "    over_walk_limit=(\"round_trip_minutes\", lambda s: (s > WALK_LIMIT).mean()),\n",
    "    below_sphere=(\"litres_per_person_day\", lambda s: (s < SPHERE_MINIMUM).mean()),\n",
    "    unimproved=(\"improved_source\", lambda s: 1 - s.mean()),\n",
    ").round(3)\n",
    "\n",
    "communities.sort_values(\"basic_water\").head(8)"
   ],
   "id": "cell-009"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "L'enquête a réparti un nombre de ménages à peu près égal par communauté, ce qui\n",
    "est commode : les communautés sont directement comparables sans pondération, et un\n",
    "taux issu de l'une est aussi précis qu'un taux issu d'une autre."
   ],
   "id": "cell-010"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "communities[\"menages\"].describe().round(1)"
   ],
   "id": "cell-011"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Le classement, et le choix qu'il contient\n",
    "\n",
    "Une réhabilitation raccourcit le trajet et rétablit la fonctionnalité. Elle ne\n",
    "change ni l'assainissement ni l'hygiène : ceux-ci sont donc exclus du classement —\n",
    "les inclure permettrait à une communauté aux latrines défaillantes d'évincer une\n",
    "communauté au point d'eau hors service, ce que ce budget n'achète pas."
   ],
   "id": "cell-012"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def standardise(series):\n",
    "    return (series - series.mean()) / series.std()\n",
    "\n",
    "communities[\"need\"] = (\n",
    "    standardise(1 - communities[\"basic_water\"])\n",
    "    + standardise(communities[\"over_walk_limit\"])\n",
    "    + standardise(communities[\"below_sphere\"])\n",
    "    + standardise(communities[\"unimproved\"])\n",
    ") / 4\n",
    "\n",
    "ranked = communities.sort_values(\"need\", ascending=False)\n",
    "ranked.head(10)"
   ],
   "id": "cell-013"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "selected = ranked.head(REHABILITATIONS)\n",
    "selected.index.tolist()"
   ],
   "id": "cell-014"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Le classement survit-il au retrait d'une composante ?\n",
    "\n",
    "Un composite qui se réordonne entièrement lorsqu'on retire une entrée est un\n",
    "composite auquel l'autorité de l'eau ne devrait pas se fier."
   ],
   "id": "cell-015"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "components = [\"basic_water\", \"over_walk_limit\", \"below_sphere\", \"unimproved\"]\n",
    "\n",
    "stability = {}\n",
    "for dropped in components:\n",
    "    kept = [c for c in components if c != dropped]\n",
    "    score = sum(\n",
    "        standardise(1 - communities[c] if c == \"basic_water\" else communities[c])\n",
    "        for c in kept\n",
    "    ) / len(kept)\n",
    "    top = set(score.nlargest(REHABILITATIONS).index)\n",
    "    stability[f\"sans {dropped}\"] = len(top & set(selected.index))\n",
    "\n",
    "pd.Series(stability, name=f\"sur {REHABILITATIONS} encore selectionnees\")"
   ],
   "id": "cell-016"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La liste est stable au retrait de n'importe quelle composante, ce qui la rend\n",
    "défendable en réunion. Là où elle ne l'est pas, dites-le et nommez les\n",
    "communautés qui bougent.\n",
    "\n",
    "## La qualité de l'eau est une question distincte, sur un dénominateur plus petit"
   ],
   "id": "cell-017"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "quality = wash.groupby([\"district\", \"community\"]).agg(\n",
    "    chlorine_tested=(\"free_residual_chlorine_mgl\", lambda s: s.notna().mean()),\n",
    "    ecoli_tested=(\"ecoli_cfu_100ml\", lambda s: s.notna().mean()),\n",
    "    ecoli_detected=(\"ecoli_cfu_100ml\", lambda s: (s > 0).sum() / max(s.notna().sum(), 1)),\n",
    ").round(3)\n",
    "\n",
    "quality.loc[selected.index]"
   ],
   "id": "cell-018"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La qualité n'est testée que sur environ un tiers des ménages : ces taux reposent\n",
    "donc sur un dénominateur trois fois plus petit que celui des chiffres d'accès. Ils\n",
    "sont rapportés à côté de la sélection et non à l'intérieur : une eau contaminée\n",
    "appelle un traitement ou une nouvelle source, ce qui est une intervention\n",
    "différente de la réhabilitation d'un point d'eau éloigné.\n",
    "\n",
    "## La sortie que reçoit l'autorité de l'eau"
   ],
   "id": "cell-019"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "recommendation = selected.reset_index()[\n",
    "    [\"district\", \"community\", \"menages\", \"basic_water\",\n",
    "     \"over_walk_limit\", \"below_sphere\", \"need\"]\n",
    "]\n",
    "recommendation[\"basic_water\"] = (recommendation[\"basic_water\"] * 100).round(1)\n",
    "recommendation[\"over_walk_limit\"] = (recommendation[\"over_walk_limit\"] * 100).round(1)\n",
    "recommendation[\"below_sphere\"] = (recommendation[\"below_sphere\"] * 100).round(1)\n",
    "recommendation.round(3)"
   ],
   "id": "cell-020"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "by_district = pd.DataFrame({\n",
    "    \"communautes\": communities.groupby(\"district\").size(),\n",
    "    \"selectionnees\": selected.reset_index().groupby(\"district\").size(),\n",
    "}).fillna(0).astype(int)\n",
    "by_district"
   ],
   "id": "cell-021"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si la sélection se concentre sur un district, c'est un constat à énoncer\n",
    "explicitement — il sera lu comme un choix politique à moins que le rapport ne\n",
    "montre qu'il découle du classement.\n",
    "\n",
    "## Ce que cela n'établit pas\n",
    "\n",
    "Les réponses des ménages sur le temps de collecte sont déclarées, non mesurées.\n",
    "Là où une communauté sous-estime ou surestime systématiquement le trajet, le\n",
    "classement suit, et rien dans ce jeu de données ne permet de le détecter.\n",
    "\n",
    "L'enquête ne dit pas non plus **pourquoi** un point d'eau est éloigné ou non\n",
    "amélioré. Il peut s'agir d'une pompe cassée, d'un forage à sec, ou d'une\n",
    "communauté qui n'en a jamais eu. Ces cas appellent des financements différents, et\n",
    "le budget de réhabilitation ne couvre que le premier."
   ],
   "id": "cell-022"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "file_extension": ".py"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
