---
title: "Choisir le bon dénominateur — corrigé"
subtitle: "Corrigé d'exercice · Fondamentaux de l'analyse de données pour le suivi-évaluation"
author: "Cassion · data-analysis.cassion.dev"
format:
  html:
    toc: true
    code-fold: false
jupyter: python3
---

## Mode d'emploi

Ceci est le corrigé : lisez-le après avoir tenté l'exercice, non à sa place. Les
cinq scénarios ont chacun un numérateur admis et un dénominateur contesté, et
dans chaque cas la réponse honnête n'est pas « le bon chiffre » mais « le
chiffre, et ce qu'il exclut ».

Le jeu de données est le registre synthétique de dépistage du PB de
l'Artibonite. Rien ici ne décrit un enfant réel.

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

URL = (
    "https://data-analysis.cassion.dev/datasets/files/"
    "muac-screening-artibonite-2024.v1.csv"
)

muac = pd.read_csv(
    URL,
    dtype={"child_id": "string", "commune": "string", "sex": "string"},
    na_values={"muac_mm": ["-99"]},
)
muac["screening_date"] = pd.to_datetime(muac["screening_date"], format="%Y-%m-%d")

# Les corrections de la lecon 6, appliquees pour que les scenarios ci-dessous
# portent sur les denominateurs et non sur la qualite des donnees.
erreur_unite = muac["muac_mm"].notna() & (muac["muac_mm"] < 40)
muac.loc[erreur_unite, "muac_mm"] = muac.loc[erreur_unite, "muac_mm"] * 10

table_oedeme = {
    "true": True, "TRUE": True, "Y": True, "y": True, "yes": True,
    "false": False, "FALSE": False, "N": False, "n": False, "no": False,
}
muac["oedema"] = muac["oedema"].astype("string").str.strip().map(table_oedeme)
muac = muac.drop_duplicates()

muac["mag"] = (muac["muac_mm"] < 125) | (muac["oedema"] == True)
len(muac)
```

## Scénario 1 — « Quelle proportion des enfants dépistés était malnutrie ? »

Le numérateur est admis : les enfants répondant à la définition de cas de la
MAG. Quatre dénominateurs sont défendables et donnent quatre chiffres
différents.

```{python}
candidats = {
    "toutes les lignes du registre": len(muac),
    "lignes avec une mesure de PB": int(muac["muac_mm"].notna().sum()),
    "lignes avec PB ou evaluation d'oedeme": int(
        (muac["muac_mm"].notna() | muac["oedema"].notna()).sum()
    ),
    "lignes avec PB, oedeme et age": int(
        (
            (muac["muac_mm"].notna() | muac["oedema"].notna())
            & muac["age_months"].notna()
        ).sum()
    ),
}

cas = int(muac["mag"].sum())
pd.DataFrame(
    {
        "denominateur": candidats,
        "taux": {k: cas / v for k, v in candidats.items()},
    }
).round(4)
```

**La réponse.** Retenez « lignes avec PB ou évaluation d'œdème ». Un enfant sans
aucune évaluation n'a pas été dépisté pour cet indicateur : l'inclure au
dénominateur sous-estime le taux en comptant des non-observations comme des
négatifs. Exiger en plus l'âge est trop restrictif — les seuils de PB pour les 6
à 59 mois forment une bande unique et n'utilisent pas l'âge, et cette exigence
retire une commune bien plus que les autres.

**Ce qu'il exclut, énoncé explicitement :** les enfants qui ne sont pas venus se
faire dépister. Il s'agit d'un taux parmi les personnes atteintes, non d'une
prévalence de population, et le rapport doit le dire.

## Scénario 2 — « Quel est notre taux d'aboutissement des référencements ? »

Le piège tient à ce que la colonne de décision et la colonne de mesure ont été
remplies par deux personnes différentes : quelques enregistrements portent donc
un référencement sans mesure derrière. Comptez-les plutôt que de supposer le
nombre.

```{python}
references = muac["outcome"].isin(["referred-tsfp", "referred-otp", "referred-sc"])
sans_mesure = references & muac["muac_mm"].isna()

print(f"referencements consignes  : {int(references.sum())}")
print(f"dont sans aucune mesure   : {int(sans_mesure.sum())}")
```

**La réponse.** Deux questions distinctes se cachent ici, et elles appellent des
dénominateurs différents.

- *Le dépistage a-t-il donné lieu à un référencement là où il le fallait ?*
  Dénominateur : les enfants répondant à la définition de cas de référencement.
  Cela mesure le dépisteur.
- *L'enfant référé a-t-il atteint le service ?* Dénominateur : les enfants
  référés. Cela mesure le circuit, et ce registre ne peut absolument pas y
  répondre — il n'existe aucune trace d'arrivée.

Rapporter la seconde à partir de ce fichier reviendrait à inventer un chiffre. La
sortie correcte est la première, assortie d'une phrase indiquant que la seconde
exige le registre d'admission PCIMA.

## Scénario 3 — « La couverture a progressé de 12 % ce trimestre »

```{python}
muac["trimestre"] = muac["screening_date"].dt.to_period("Q")
par_trimestre = muac.groupby("trimestre").size()
par_trimestre
```

**La réponse.** Ce n'est pas de la couverture, et le mot doit être refusé. La
couverture est le rapport des cas atteints aux cas existants, et ce registre ne
dispose d'aucun dénominateur d'enfants dans la population — seulement de ceux
qui se sont présentés. Ce qui a progressé, c'est le volume de dépistage.

Notez également que le T1 et le T4 sont tronqués : le premier dépistage a lieu à
la mi-janvier et le dernier à la mi-décembre, si bien qu'une comparaison
trimestrielle incluant l'un ou l'autre compare des fenêtres inégales.

```{python}
print("premier :", muac["screening_date"].min().date())
print("dernier :", muac["screening_date"].max().date())
```

## Scénario 4 — « Quelle commune est la plus touchée ? »

```{python}
par_commune = (
    muac.assign(evalue=muac["muac_mm"].notna() | muac["oedema"].notna())
    .groupby("commune")
    .agg(evalue=("evalue", "sum"), cas=("mag", "sum"))
)
par_commune["taux"] = par_commune["cas"] / par_commune["evalue"]
par_commune.sort_values("taux", ascending=False).round(4)
```

**La réponse.** Le dénominateur est correct, et la question reste mal posée. La
plus petite commune compte moins de 200 évaluations : son intervalle est assez
large pour que son rang soit à peu près dépourvu de sens.

```{python}
from scipy.stats import beta

def intervalle(cas, n):
    if n == 0:
        return (np.nan, np.nan)
    bas = beta.ppf(0.025, cas, n - cas + 1) if cas > 0 else 0.0
    haut = beta.ppf(0.975, cas + 1, n - cas) if cas < n else 1.0
    return (bas, haut)

bornes = par_commune.apply(lambda r: intervalle(r["cas"], r["evalue"]), axis=1)
par_commune["bas"] = [b[0] for b in bornes]
par_commune["haut"] = [b[1] for b in bornes]
par_commune.sort_values("taux", ascending=False)[
    ["evalue", "taux", "bas", "haut"]
].round(4)
```

Plusieurs intervalles se recouvrent. « La plus touchée » n'est une question
répondable que pour la commune dont l'intervalle se détache des autres, et un
tableau sans intervalles vous aurait laissé classer les douze avec une assurance
infondée.

## Scénario 5 — « Notre programme a touché 4 218 enfants cette année »

```{python}
print(f"lignes                      : {len(muac)}")
print(f"child_id distincts          : {muac['child_id'].nunique()}")

cle = ["commune", "screening_date", "age_months", "sex", "muac_mm"]
suspects = muac.duplicated(subset=cle, keep=False) & muac["muac_mm"].notna()
print(f"reenregistrements suspectes : {int(suspects.sum())}")
```

**La réponse.** Des lignes ne sont pas des enfants. Les doublons exacts ont déjà
été retirés ci-dessus, mais quelques enfants ont été réenregistrés sous un
nouvel identifiant et ne partagent aucune clé : on ne les repère qu'en appariant
sur commune, date, âge, sexe et mesure, et ils restent indiscernables d'une
coïncidence authentique sans le registre papier.

L'affirmation défendable est donc un chiffre assorti de sa réserve : tant
d'enregistrements distincts, dont un certain nombre peuvent être des
réenregistrements du même enfant. Notez que la vérification ci-dessus signale
des *suspects* et non des doublons — deux enfants de même âge et de même sexe
dépistés dans une même commune le même jour avec la même mesure, c'est tout à
fait possible, et dans une campagne de cette taille c'est même probable. Les
trancher exige le registre papier, non une ligne de pandas supplémentaire.

## La règle qui sous-tend les cinq scénarios

Rédigez le dénominateur sous forme de phrase avant de calculer quoi que ce soit.
Si vous n'y parvenez pas, vous n'avez pas encore d'indicateur : vous avez une
colonne dont vous vous apprêtez à prendre la moyenne. Et ce que le dénominateur
exclut figure dans le rapport, non dans votre tête.
