---
title: "Calculer la prévalence avec les normes de croissance de l'OMS"
subtitle: "Enquête nutritionnelle SMART, 2024 · R"
author: "Cassion · data-analysis.cassion.dev"
format:
  html:
    toc: true
    code-fold: false
---

## Ce que produit ce document

Les scores z poids-taille au regard des normes de croissance OMS 2006, et les
prévalences de MAG et de MAS assorties d'un effet de plan pour l'échantillon en
grappes.

Les scores z ne sont délibérément **pas** livrés dans ce jeu de données. Les
calculer est l'exercice, et lire une colonne précalculée n'apprend rien des trois
décisions qui déplacent la réponse : le contrôle de plage, la position de mesure,
et la règle d'exclusion des valeurs aberrantes retenue.

Tous les jeux de données de cette plateforme sont synthétiques. Aucun enfant réel
n'est décrit, et ces résultats ne doivent pas être cités comme une situation
nutritionnelle réelle.

## Utiliser le paquet officiel, non son propre code LMS

Les normes de l'OMS sont une table LMS, et réimplémenter l'interpolation est une
source d'erreur discrète bien connue. `anthro` est maintenu par l'OMS et applique
lui-même l'ajustement longueur/taille.

```{r}
#| message: false
# install.packages("anthro")
library(readr)
library(dplyr)
library(anthro)

URL <- paste0(
  "https://data-analysis.cassion.dev/datasets/files/",
  "smart-nutrition-survey-2024.v1.csv"
)

smart <- read_csv(URL, col_types = cols(
  child_id = col_character(),
  .default = col_guess()
))

glimpse(smart)
```

## Contrôler les plages avant d'exclure les valeurs aberrantes

Quatorze enregistrements portent des mesures impossibles — des poids erronés d'un
facteur dix dans les deux sens, et des tailles saisies en mètres. Ce ne sont pas
des valeurs aberrantes à écarter statistiquement : ce sont des erreurs de saisie,
et elles doivent être exclues avant toute règle d'exclusion, car un seul enfant à
114 kg déplace la moyenne de l'enquête au regard de laquelle se calcule le flag
SMART.

```{r}
impossibles <- smart |>
  filter(weight_kg < 2 | weight_kg > 30 | height_cm < 45 | height_cm > 130)

impossibles |> select(child_id, team, weight_kg, height_cm)
```

```{r}
plausibles <- smart |>
  filter(
    is.na(weight_kg) | is.na(height_cm) |
      !(weight_kg < 2 | weight_kg > 30 | height_cm < 45 | height_cm > 130)
  )

cat("conserves :", nrow(plausibles), "sur", nrow(smart), "\n")
cat("age manquant :", sum(is.na(plausibles$age_months)),
    " poids manquant :", sum(is.na(plausibles$weight_kg)), "\n")
```

## La position de mesure n'est pas facultative

Les enfants de moins de deux ans sont mesurés couchés — longueur en décubitus —
et les plus âgés debout. La longueur excède la taille d'environ 0,7 cm pour un
même enfant : mélanger les deux sans ajustement biaise donc tous les scores z de
la moitié la plus jeune de l'échantillon.

N'ajustez **pas** la colonne à la main. Transmettez la position à
`anthro_zscores` via `measure` et laissez-le appliquer la règle de l'OMS, qui
dépend de l'âge autant que de la position.

```{r}
z <- anthro_zscores(
  sex             = ifelse(plausibles$sex == "m", 1, 2),
  age             = plausibles$age_months,
  is_age_in_month = TRUE,
  weight          = plausibles$weight_kg,
  lenhei          = plausibles$height_cm,
  measure         = ifelse(plausibles$measured_lying, "l", "h")
)

scores <- plausibles |>
  mutate(whz = z$zwfl, flag_oms = z$fwfl)

table(scores$flag_oms, useNA = "ifany")
```

## Deux règles d'exclusion, deux enquêtes légèrement différentes

Les flags OMS sont des bornes fixes : un score z poids-taille hors de -5 à +5 est
biologiquement implausible. Les flags SMART sont relatifs : au-delà de 3 écarts
types de la moyenne de *l'enquête*. Ils excluent des enfants différents, et un
rapport de plausibilité énonce lequel a été employé.

```{r}
moyenne_z <- mean(scores$whz, na.rm = TRUE)
et_z      <- sd(scores$whz, na.rm = TRUE)

scores <- scores |>
  mutate(flag_smart = abs(whz - moyenne_z) > 3 * et_z)

scores |>
  summarise(
    exclus_oms   = sum(flag_oms == 1, na.rm = TRUE),
    exclus_smart = sum(flag_smart, na.rm = TRUE),
    les_deux     = sum(flag_oms == 1 & flag_smart, na.rm = TRUE)
  )
```

La règle SMART se rapporte à une moyenne que les observations exclues influencent
elles-mêmes, ce qui explique que le contrôle de plage doive venir en premier.

## La prévalence

**Les œdèmes priment sur l'anthropométrie.** Un enfant porteur d'œdèmes
bilatéraux prenant le godet est en malnutrition aiguë sévère quel que soit son
rapport poids-taille : le numérateur de la MAS n'est donc pas le simple
décompte des scores inférieurs à -3.

```{r}
analysables <- scores |> filter(flag_oms == 0, !is.na(whz))

prevalence <- analysables |>
  summarise(
    enfants = n(),
    mag = mean(whz < -2 | oedema),
    mas = mean(whz < -3 | oedema),
    moyenne_z = mean(whz),
    et_z      = sd(whz)
  )

prevalence |> mutate(across(c(mag, mas), ~ round(100 * .x, 1)),
                     across(c(moyenne_z, et_z), ~ round(.x, 2)))
```

Une malnutrition aiguë globale proche de 14,9 % et une malnutrition sévère
proche de 3,9 %. On se situe juste sous le seuil d'urgence OMS de 15 % — soit
exactement la position où les choix analytiques ci-dessus cessent d'être
théoriques, puisqu'une autre règle d'exclusion ou un ajustement de position omis
font franchir la ligne au chiffre.

Notez l'écart type du score z. SMART l'attend entre 0,8 et 1,2 environ ; une
valeur supérieure suggère une erreur de mesure qui gonfle la dispersion, et cette
enquête se situe au sommet de la plage acceptable pour une raison que la section
suivante identifie.

## L'effet équipe, qui n'est pas un constat nutritionnel

```{r}
analysables |>
  group_by(team) |>
  summarise(
    enfants   = n(),
    moyenne_z = round(mean(whz), 2),
    mag       = round(100 * mean(whz < -2 | oedema), 1),
    .groups = "drop"
  )
```

L'équipe 3 rapporte une MAG proche de 22 % contre 10 à 16 % pour les autres, avec
un score z moyen de -1,09 contre -0,43 à -0,69. Une différence réelle d'état
nutritionnel entre des grappes attribuées aléatoirement, d'une telle ampleur,
serait extraordinaire. Il s'agit d'un artefact de mesure — une équipe qui mesure
les tailles trop longues ou les poids trop légers — et le rapporter comme un
constat géographique orienterait des ressources vers les mauvaises grappes.

## L'effet de plan

C'est un échantillon en grappes. Les enfants d'une même grappe se ressemblent,
donc la taille d'échantillon effective est inférieure au nombre d'enfants, et un
intervalle de confiance calculé comme pour un sondage aléatoire simple se
sous-estime lui-même.

```{r}
grappes <- analysables |>
  mutate(cas = whz < -2 | oedema) |>
  group_by(cluster) |>
  summarise(m = n(), y = sum(cas), .groups = "drop")

k     <- nrow(grappes)
M     <- sum(grappes$m)
p_bar <- sum(grappes$y) / M

# Variance de grappe ultime pour un estimateur de ratio. Elle utilise l'ecart de
# chaque grappe entre son nombre de cas et celui que le taux global predit pour
# sa taille — et non la variance des taux par grappe, qui ignore que les grappes
# n'ont pas la meme taille.
var_grappe <- (k / ((k - 1) * M^2)) * sum((grappes$y - p_bar * grappes$m)^2)
var_eas    <- p_bar * (1 - p_bar) / M

deff <- var_grappe / var_eas
icc  <- (deff - 1) / (mean(grappes$m) - 1)

cat(sprintf("grappes : %d   enfants : %d   taille moyenne : %.1f\n",
            k, M, mean(grappes$m)))
cat(sprintf("effet de plan : %.2f   ICC : %.3f\n", deff, icc))
cat(sprintf("taille effective : %.0f sur %d\n", M / deff, M))
```

Un effet de plan proche de 2,3 et un ICC autour de 0,045 sont ordinaires pour une
enquête nutritionnelle en grappes. Un effet de plan inférieur à 1 ou supérieur à
4 environ signale généralement un calcul erroné plutôt qu'une enquête inhabituelle
— c'est une vérification à mener sur votre propre arithmétique avant de la
rapporter.

```{r}
et_grappe <- sqrt(var_grappe)
et_eas    <- sqrt(var_eas)

cat(sprintf("MAG %.1f%%  (IC 95%% %.1f - %.1f)  en tenant compte des grappes\n",
            100 * p_bar, 100 * (p_bar - 1.96 * et_grappe), 100 * (p_bar + 1.96 * et_grappe)))
cat(sprintf("          (IC 95%% %.1f - %.1f)  en ignorant les grappes\n",
            100 * (p_bar - 1.96 * et_eas), 100 * (p_bar + 1.96 * et_eas)))
```

Deux choses à y lire. L'intervalle correct est plus large, et un rapport qui
ignore les grappes revendique une précision que le protocole ne peut pas offrir —
la manière la plus courante pour une enquête de surestimer ce qu'elle sait.

Et la borne supérieure franchit 15 %. L'estimation ponctuelle se situe sous le
seuil d'urgence de l'OMS ; l'intervalle n'exclut pas d'être au-dessus. C'est cette
phrase-là que le rapport doit porter, et non un « 14,9 %, sous le seuil
d'urgence » sec.

## Ce qu'il faut rapporter

La prévalence avec son intervalle et l'effet de plan employé, la règle
d'exclusion nommée, les exclusions comptées, et la comparaison entre équipes —
car une enquête où une équipe s'écarte des autres d'un demi-score z présente un
problème de mesure qui prime sur tout chiffre de prévalence du rapport.
