{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Qualité de l'eau et chloration\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/chlorine-vs-ecoli.python.fr.ipynb)"
   ],
   "id": "cell-000"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La question\n",
    "\n",
    "Un chlore résiduel libre supérieur ou égal à 0,2 mg/L prédit-il une classe de\n",
    "risque E. coli plus faible ? Et — c'est ce qui décide si la réponse signifie\n",
    "quelque chose — quelle part de l'échantillon peut réellement y répondre ?\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\"})\n",
    "wash[\"district\"] = (\n",
    "    wash[\"district\"].str.strip().str.lower().str.replace(\" \", \"-\", regex=False)\n",
    ")\n",
    "len(wash)"
   ],
   "id": "cell-002"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Commencer par le dénominateur, non par le résultat\n",
    "\n",
    "La qualité de l'eau n'est testée que sur un sous-ensemble. Rapporter un résultat\n",
    "de qualité au regard de l'échantillon complet surestime l'effort de test réalisé,\n",
    "et c'est la première chose qu'un relecteur vérifiera."
   ],
   "id": "cell-003"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "testes = pd.DataFrame({\n",
    "    \"menages\": [\n",
    "        len(wash),\n",
    "        int(wash[\"free_residual_chlorine_mgl\"].notna().sum()),\n",
    "        int(wash[\"ecoli_cfu_100ml\"].notna().sum()),\n",
    "        int(\n",
    "            (\n",
    "                wash[\"free_residual_chlorine_mgl\"].notna()\n",
    "                & wash[\"ecoli_cfu_100ml\"].notna()\n",
    "            ).sum()\n",
    "        ),\n",
    "    ]\n",
    "}, index=[\"enquetes\", \"chlore teste\", \"E. coli teste\", \"les deux testes\"])\n",
    "testes[\"part de l'echantillon\"] = (testes[\"menages\"] / len(wash)).round(3)\n",
    "testes"
   ],
   "id": "cell-004"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Seule la dernière ligne peut répondre à la question.** Tout ce qui suit est\n",
    "calculé sur ce sous-ensemble, et le rapport doit le dire : un constat sur la\n",
    "chloration présenté au regard de 2 403 ménages alors que 320 ont été testés pour\n",
    "les deux paramètres est une déformation des données probantes, quel que soit le\n",
    "constat.\n",
    "\n",
    "## La non-réponse n'est pas aléatoire\n",
    "\n",
    "Environ 530 ménages déclarent traiter leur eau sans qu'aucune mesure de chlore ne\n",
    "soit consignée. C'est une incohérence entre champs, non une valeur manquante au\n",
    "hasard, et cela compte : supprimer ces lignes retire des ménages qui traitent\n",
    "leur eau plus souvent que la moyenne, ce qui biaise la comparaison dans une\n",
    "direction prévisible."
   ],
   "id": "cell-005"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "traite_non_teste = wash[\"water_treated_at_home\"] & wash[\n",
    "    \"free_residual_chlorine_mgl\"\n",
    "].isna()\n",
    "print(f\"traitent leur eau sans mesure de chlore : {int(traite_non_teste.sum())}\")\n",
    "\n",
    "pd.crosstab(\n",
    "    wash[\"water_treated_at_home\"],\n",
    "    wash[\"free_residual_chlorine_mgl\"].notna().map(\n",
    "        {True: \"chlore teste\", False: \"non teste\"}\n",
    "    ),\n",
    "    normalize=\"index\",\n",
    ").round(3)"
   ],
   "id": "cell-006"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Les ménages qui traitent leur eau sont ici *davantage* susceptibles d'être\n",
    "testés : le sous-ensemble testé surreprésente donc le traitement. Tout effet\n",
    "estimé sur lui est un effet parmi les testés, non dans la population.\n",
    "\n",
    "## La comparaison\n",
    "\n",
    "La valeur guide de l'OMS pour le chlore résiduel libre au point de distribution\n",
    "est de 0,2 mg/L. E. coli est rapporté en classes de risque normalisées plutôt\n",
    "qu'en dénombrement brut, parce que le dénombrement est surdispersé et que ce sont\n",
    "les classes qui déclenchent une action de programme."
   ],
   "id": "cell-007"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "deux = wash.dropna(subset=[\"free_residual_chlorine_mgl\", \"ecoli_cfu_100ml\"]).copy()\n",
    "\n",
    "deux[\"chlore_suffisant\"] = deux[\"free_residual_chlorine_mgl\"] >= 0.2\n",
    "deux[\"classe_risque\"] = pd.cut(\n",
    "    deux[\"ecoli_cfu_100ml\"],\n",
    "    bins=[-1, 0, 10, 100, np.inf],\n",
    "    labels=[\"0 (conforme)\", \"1-10 (faible)\", \"11-100 (intermediaire)\", \">100 (eleve)\"],\n",
    ")\n",
    "\n",
    "tableau = pd.crosstab(\n",
    "    deux[\"chlore_suffisant\"],\n",
    "    deux[\"classe_risque\"],\n",
    "    normalize=\"index\",\n",
    ").round(3)\n",
    "tableau.index = [\"sous 0,2 mg/L\", \"0,2 mg/L ou plus\"]\n",
    "tableau"
   ],
   "id": "cell-008"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "effectifs = pd.crosstab(deux[\"chlore_suffisant\"], deux[\"classe_risque\"])\n",
    "effectifs.index = [\"sous 0,2 mg/L\", \"0,2 mg/L ou plus\"]\n",
    "effectifs"
   ],
   "id": "cell-009"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Le motif est net : les ménages au niveau de la valeur guide ou au-dessus sont\n",
    "bien plus souvent exempts d'E. coli détectable, et aucun n'atteint la classe de\n",
    "risque élevé.\n",
    "\n",
    "## Est-ce davantage que du bruit d'échantillonnage ?"
   ],
   "id": "cell-010"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from scipy.stats import chi2_contingency\n",
    "\n",
    "conformes = pd.crosstab(\n",
    "    deux[\"chlore_suffisant\"], deux[\"ecoli_cfu_100ml\"] == 0\n",
    ")\n",
    "chi2, p, dof, attendu = chi2_contingency(conformes)\n",
    "\n",
    "taux_bas = conformes.loc[False, True] / conformes.loc[False].sum()\n",
    "taux_haut = conformes.loc[True, True] / conformes.loc[True].sum()\n",
    "\n",
    "print(f\"conformes, chlore sous 0,2   : {taux_bas:.1%}\")\n",
    "print(f\"conformes, chlore >= 0,2     : {taux_haut:.1%}\")\n",
    "print(f\"ecart                        : {taux_haut - taux_bas:+.1%}\")\n",
    "print(f\"p du khi-deux                : {p:.2e}\")"
   ],
   "id": "cell-011"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Deux réserves sur cette valeur p. Elle indique que l'association est peu\n",
    "susceptible d'être due au hasard ; elle n'indique pas que la chloration l'a\n",
    "*causée*, car les ménages qui chlorent diffèrent de ceux qui ne le font pas par\n",
    "des caractéristiques que cette enquête ne relève pas. Et sur quelques centaines\n",
    "d'observations, une valeur p aussi faible traduit surtout l'ampleur de l'écart :\n",
    "c'est la taille d'effet qu'il faut rapporter, non la valeur p.\n",
    "\n",
    "## Où se trouvent les ménages non testés"
   ],
   "id": "cell-012"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "par_district = wash.groupby(\"district\").agg(\n",
    "    menages=(\"household_id\", \"size\"),\n",
    "    chlore_teste=(\"free_residual_chlorine_mgl\", lambda s: s.notna().mean()),\n",
    "    ecoli_teste=(\"ecoli_cfu_100ml\", lambda s: s.notna().mean()),\n",
    ").round(3)\n",
    "par_district"
   ],
   "id": "cell-013"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si la couverture des tests diffère selon le district, une comparaison de la\n",
    "qualité de l'eau entre districts est en partie une comparaison de qui a été\n",
    "testé. Vérifiez-le avant de classer quoi que ce soit.\n",
    "\n",
    "## Ce qu'il faut rapporter\n",
    "\n",
    "Le constat, le dénominateur sur lequel il repose, et le fait que le\n",
    "sous-ensemble testé surreprésente les ménages qui traitent leur eau. Puis la\n",
    "recommandation sur laquelle le programme peut effectivement agir — ici, tester\n",
    "davantage de ménages, et non conclure fermement sur la chloration à partir de\n",
    "320 d'entre eux."
   ],
   "id": "cell-014"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "file_extension": ".py"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
