---
title: "Échelles de service du JMP pour l'eau, l'assainissement et l'hygiène"
subtitle: "Enquête ménage EAH, 2024"
author: "Cassion · data-analysis.cassion.dev"
format:
  html:
    toc: true
    code-fold: false
jupyter: python3
---

## Ce que produit ce document

Chaque ménage placé sur les trois échelles de service du JMP — eau de boisson,
assainissement, hygiène — et la couverture par district. Ce sont les définitions
au regard desquelles sont rapportés les indicateurs de l'ODD 6 : les appliquer
correctement fait la différence entre un chiffre qu'un cluster acceptera et un
chiffre qu'il renverra.

Valeurs de référence issues des notes de qualité : environ 13 % des ménages sous
le minimum Sphere de 15 litres par personne et par jour, environ 39 % au-delà de
30 minutes de trajet aller-retour, environ 13 % de défécation à l'air libre, et
environ 34 % de service d'hygiène de base.

Tous les jeux de données de cette plateforme sont synthétiques. Aucun ménage réel
n'est décrit.

## Mise en place

```{python}
import pandas as pd
import numpy as np

URL = (
    "https://data-analysis.cassion.dev/datasets/files/"
    "wash-household-survey-2024.v1.csv"
)

wash = pd.read_csv(URL, dtype={"household_id": "string", "community": "string"})
print(wash.shape)
wash.head()
```

## Normaliser le district avant de regrouper dessus

Une équipe d'enquêteurs a orthographié le nom du district Nord-Ouest de quatre
manières différentes. Regroupez sans normaliser et vous obtenez six districts au
lieu de trois, en éclatant le moins performant en quatre morceaux assez petits
pour ne rien signaler.

```{python}
print(wash["district"].value_counts())
```

```{python}
wash["district"] = (
    wash["district"].str.strip().str.lower().str.replace(" ", "-", regex=False)
)
print(wash["district"].value_counts())
```

Normalisez avant de regrouper, systématiquement. C'est la manière la plus
fréquente pour un tableau par district de dissimuler discrètement son plus
mauvais résultat.

## L'échelle de l'eau de boisson

L'échelle **n'est pas une propriété de la source seule.** Une source améliorée à
plus de 30 minutes de trajet aller-retour relève du service *limité*, non du
service de base — et environ un quart de ces ménages se retrouvent en service
limité pour cette seule raison. Une analyse qui classe sur le seul type de source
les manque tous.

```{python}
AMELIOREES = {
    "piped-into-dwelling", "piped-into-yard", "public-tap", "borehole",
    "protected-well", "protected-spring", "tanker-truck",
}
NON_AMELIOREES = {"unprotected-well", "unprotected-spring"}

def echelle_eau(ligne):
    source = ligne["water_source"]
    if source == "surface-water":
        return "eau de surface"
    if source in NON_AMELIOREES:
        return "non amelioree"
    if pd.isna(ligne["round_trip_minutes"]):
        return "amelioree, duree inconnue"
    return "base" if ligne["round_trip_minutes"] <= 30 else "limite"

wash["service_eau"] = wash.apply(echelle_eau, axis=1)
(wash["service_eau"].value_counts(normalize=True) * 100).round(1)
```

Le niveau « géré en toute sécurité » est délibérément absent. Il exige que la
source soit sur la parcelle, disponible au besoin **et** exempte de
contamination — or cette enquête ne teste la qualité que sur un tiers des
ménages, si bien que l'échelon supérieur ne peut être attribué à la majeure
partie de l'échantillon. Rapporter le service de base et s'arrêter là est
honnête ; inventer un chiffre de service géré en toute sécurité ne l'est pas.

## L'échelle de l'assainissement

```{python}
ASSAINISSEMENT_AMELIORE = {
    "flush-to-sewer", "flush-to-septic", "vip-latrine", "pit-latrine-with-slab",
}

def echelle_assainissement(ligne):
    installation = ligne["sanitation_facility"]
    if installation == "open-defecation":
        return "defecation a l'air libre"
    if installation not in ASSAINISSEMENT_AMELIORE:
        return "non amelioree"
    return "limite" if ligne["shared_sanitation"] else "base"

wash["service_assainissement"] = wash.apply(echelle_assainissement, axis=1)
(wash["service_assainissement"].value_counts(normalize=True) * 100).round(1)
```

C'est le partage qui sépare le service de base du service limité. Un ménage
disposant d'une latrine VIP en parfait état mais partagée avec trois autres
relève du service *limité*, et un tableau construit sur le seul type
d'installation le rapportera comme service de base.

## L'échelle de l'hygiène

```{python}
def echelle_hygiene(ligne):
    if ligne["handwashing_facility"] == "no-facility":
        return "aucune installation"
    return "base" if ligne["soap_observed"] else "limite"

wash["service_hygiene"] = wash.apply(echelle_hygiene, axis=1)
(wash["service_hygiene"].value_counts(normalize=True) * 100).round(1)
```

L'hygiène de base exige une installation **avec eau et savon présents**,
observés et non déclarés. La distinction compte : un tiers de ces ménages
possèdent une installation sans savon, et la question « vous lavez-vous les
mains ? » les aurait tous comptés comme conformes.

## La couverture par district

```{python}
def couverture(df, colonne, niveau):
    return (
        df.groupby("district")[colonne]
        .apply(lambda s: (s == niveau).mean() * 100)
        .round(1)
    )

tableau = pd.DataFrame({
    "eau de base": couverture(wash, "service_eau", "base"),
    "assainissement de base": couverture(wash, "service_assainissement", "base"),
    "hygiene de base": couverture(wash, "service_hygiene", "base"),
    "defecation a l'air libre": couverture(
        wash, "service_assainissement", "defecation a l'air libre"
    ),
    "menages": wash.groupby("district").size(),
})
tableau.sort_values("eau de base")
```

## Le standard Sphere de quantité

L'échelle ne dit rien de la quantité. Sphere fixe un minimum de 15 litres par
personne et par jour, et c'est une question distincte de celle de savoir si la
source est améliorée.

```{python}
MINIMUM_SPHERE = 15

sous_minimum = wash["litres_per_person_day"] < MINIMUM_SPHERE
print(f"sous {MINIMUM_SPHERE} l/p/j : {sous_minimum.mean():.1%}")
print(f"au-dela de 30 minutes aller-retour : {(wash['round_trip_minutes'] > 30).mean():.1%}")

pd.crosstab(
    wash["service_eau"],
    sous_minimum.map({True: "sous Sphere", False: "au niveau ou au-dessus"}),
    normalize="index",
).round(3)
```

Des ménages en service *de base* passent tout de même sous le minimum Sphere.
L'accès et la quantité sont deux indicateurs différents, et aucun ne se
substitue à l'autre.

## Les erreurs d'unité que personne ne remarque

Onze enregistrements portent le temps de collecte en heures plutôt qu'en
minutes, et quatorze portent les litres pour le ménage entier plutôt que par
personne. Les deux sont parfaitement plausibles isolément — un trajet de 2, ou
40 litres par jour — et ne se détachent que rapportés à la taille du ménage.

```{python}
litres_suspects = (
    wash["litres_per_person_day"] > 60
) & wash["household_size"].notna()

wash.loc[litres_suspects, [
    "household_id", "household_size", "litres_per_person_day",
]].assign(
    total_menage_implique=lambda d: d["litres_per_person_day"] * d["household_size"],
    si_total_menage=lambda d: d["litres_per_person_day"] / d["household_size"],
).head(10)
```

Lisez la dernière colonne : divisées par la taille du ménage, ces valeurs
redeviennent ordinaires. C'est la signature d'un chiffre par ménage saisi dans
une colonne par personne. Signalez-les ; ne les redimensionnez pas en silence,
car vous ne pouvez pas prouver quelle lecture l'enquêteur avait en tête.

## Ce qu'il faut rapporter

Énoncez l'échelon de l'échelle, le dénominateur sur lequel il repose, et
l'indicateur de quantité séparément. Et dites quels ménages n'ont pas pu être
classés : ceux dont la durée de collecte n'a pas été relevée ne sont pas en
service « de base », ils sont inconnus, et les faire basculer dans le décompte du
service de base est la manière dont un chiffre de couverture dérive vers le haut
sans que personne ne l'ait décidé.
