{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Calculer la prévalence avec les normes de croissance de l'OMS\n",
    "\n",
    "*Enquête nutritionnelle SMART, 2024 · R*\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/smart-nutrition-survey-2024/whz-prevalence.r.fr.ipynb)"
   ],
   "id": "cell-000"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ce que produit ce document\n",
    "\n",
    "Les scores z poids-taille au regard des normes de croissance OMS 2006, et les\n",
    "prévalences de MAG et de MAS assorties d'un effet de plan pour l'échantillon en\n",
    "grappes.\n",
    "\n",
    "Les scores z ne sont délibérément **pas** livrés dans ce jeu de données. Les\n",
    "calculer est l'exercice, et lire une colonne précalculée n'apprend rien des trois\n",
    "décisions qui déplacent la réponse : le contrôle de plage, la position de mesure,\n",
    "et la règle d'exclusion des valeurs aberrantes retenue.\n",
    "\n",
    "Tous les jeux de données de cette plateforme sont synthétiques. Aucun enfant réel\n",
    "n'est décrit, et ces résultats ne doivent pas être cités comme une situation\n",
    "nutritionnelle réelle.\n",
    "\n",
    "## Utiliser le paquet officiel, non son propre code LMS\n",
    "\n",
    "Les normes de l'OMS sont une table LMS, et réimplémenter l'interpolation est une\n",
    "source d'erreur discrète bien connue. `anthro` est maintenu par l'OMS et applique\n",
    "lui-même l'ajustement longueur/taille."
   ],
   "id": "cell-001"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "#| message: false\n",
    "# install.packages(\"anthro\")\n",
    "library(readr)\n",
    "library(dplyr)\n",
    "library(anthro)\n",
    "\n",
    "URL <- paste0(\n",
    "  \"https://data-analysis.cassion.dev/datasets/files/\",\n",
    "  \"smart-nutrition-survey-2024.v1.csv\"\n",
    ")\n",
    "\n",
    "smart <- read_csv(URL, col_types = cols(\n",
    "  child_id = col_character(),\n",
    "  .default = col_guess()\n",
    "))\n",
    "\n",
    "glimpse(smart)"
   ],
   "id": "cell-002"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Contrôler les plages avant d'exclure les valeurs aberrantes\n",
    "\n",
    "Quatorze enregistrements portent des mesures impossibles — des poids erronés d'un\n",
    "facteur dix dans les deux sens, et des tailles saisies en mètres. Ce ne sont pas\n",
    "des valeurs aberrantes à écarter statistiquement : ce sont des erreurs de saisie,\n",
    "et elles doivent être exclues avant toute règle d'exclusion, car un seul enfant à\n",
    "114 kg déplace la moyenne de l'enquête au regard de laquelle se calcule le flag\n",
    "SMART."
   ],
   "id": "cell-003"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "impossibles <- smart |>\n",
    "  filter(weight_kg < 2 | weight_kg > 30 | height_cm < 45 | height_cm > 130)\n",
    "\n",
    "impossibles |> select(child_id, team, weight_kg, height_cm)"
   ],
   "id": "cell-004"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plausibles <- smart |>\n",
    "  filter(\n",
    "    is.na(weight_kg) | is.na(height_cm) |\n",
    "      !(weight_kg < 2 | weight_kg > 30 | height_cm < 45 | height_cm > 130)\n",
    "  )\n",
    "\n",
    "cat(\"conserves :\", nrow(plausibles), \"sur\", nrow(smart), \"\\n\")\n",
    "cat(\"age manquant :\", sum(is.na(plausibles$age_months)),\n",
    "    \" poids manquant :\", sum(is.na(plausibles$weight_kg)), \"\\n\")"
   ],
   "id": "cell-005"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La position de mesure n'est pas facultative\n",
    "\n",
    "Les enfants de moins de deux ans sont mesurés couchés — longueur en décubitus —\n",
    "et les plus âgés debout. La longueur excède la taille d'environ 0,7 cm pour un\n",
    "même enfant : mélanger les deux sans ajustement biaise donc tous les scores z de\n",
    "la moitié la plus jeune de l'échantillon.\n",
    "\n",
    "N'ajustez **pas** la colonne à la main. Transmettez la position à\n",
    "`anthro_zscores` via `measure` et laissez-le appliquer la règle de l'OMS, qui\n",
    "dépend de l'âge autant que de la position."
   ],
   "id": "cell-006"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "z <- anthro_zscores(\n",
    "  sex             = ifelse(plausibles$sex == \"m\", 1, 2),\n",
    "  age             = plausibles$age_months,\n",
    "  is_age_in_month = TRUE,\n",
    "  weight          = plausibles$weight_kg,\n",
    "  lenhei          = plausibles$height_cm,\n",
    "  measure         = ifelse(plausibles$measured_lying, \"l\", \"h\")\n",
    ")\n",
    "\n",
    "scores <- plausibles |>\n",
    "  mutate(whz = z$zwfl, flag_oms = z$fwfl)\n",
    "\n",
    "table(scores$flag_oms, useNA = \"ifany\")"
   ],
   "id": "cell-007"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Deux règles d'exclusion, deux enquêtes légèrement différentes\n",
    "\n",
    "Les flags OMS sont des bornes fixes : un score z poids-taille hors de -5 à +5 est\n",
    "biologiquement implausible. Les flags SMART sont relatifs : au-delà de 3 écarts\n",
    "types de la moyenne de *l'enquête*. Ils excluent des enfants différents, et un\n",
    "rapport de plausibilité énonce lequel a été employé."
   ],
   "id": "cell-008"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "moyenne_z <- mean(scores$whz, na.rm = TRUE)\n",
    "et_z      <- sd(scores$whz, na.rm = TRUE)\n",
    "\n",
    "scores <- scores |>\n",
    "  mutate(flag_smart = abs(whz - moyenne_z) > 3 * et_z)\n",
    "\n",
    "scores |>\n",
    "  summarise(\n",
    "    exclus_oms   = sum(flag_oms == 1, na.rm = TRUE),\n",
    "    exclus_smart = sum(flag_smart, na.rm = TRUE),\n",
    "    les_deux     = sum(flag_oms == 1 & flag_smart, na.rm = TRUE)\n",
    "  )"
   ],
   "id": "cell-009"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La règle SMART se rapporte à une moyenne que les observations exclues influencent\n",
    "elles-mêmes, ce qui explique que le contrôle de plage doive venir en premier.\n",
    "\n",
    "## La prévalence\n",
    "\n",
    "**Les œdèmes priment sur l'anthropométrie.** Un enfant porteur d'œdèmes\n",
    "bilatéraux prenant le godet est en malnutrition aiguë sévère quel que soit son\n",
    "rapport poids-taille : le numérateur de la MAS n'est donc pas le simple\n",
    "décompte des scores inférieurs à -3."
   ],
   "id": "cell-010"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "analysables <- scores |> filter(flag_oms == 0, !is.na(whz))\n",
    "\n",
    "prevalence <- analysables |>\n",
    "  summarise(\n",
    "    enfants = n(),\n",
    "    mag = mean(whz < -2 | oedema),\n",
    "    mas = mean(whz < -3 | oedema),\n",
    "    moyenne_z = mean(whz),\n",
    "    et_z      = sd(whz)\n",
    "  )\n",
    "\n",
    "prevalence |> mutate(across(c(mag, mas), ~ round(100 * .x, 1)),\n",
    "                     across(c(moyenne_z, et_z), ~ round(.x, 2)))"
   ],
   "id": "cell-011"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Une malnutrition aiguë globale proche de 14,9 % et une malnutrition sévère\n",
    "proche de 3,9 %. On se situe juste sous le seuil d'urgence OMS de 15 % — soit\n",
    "exactement la position où les choix analytiques ci-dessus cessent d'être\n",
    "théoriques, puisqu'une autre règle d'exclusion ou un ajustement de position omis\n",
    "font franchir la ligne au chiffre.\n",
    "\n",
    "Notez l'écart type du score z. SMART l'attend entre 0,8 et 1,2 environ ; une\n",
    "valeur supérieure suggère une erreur de mesure qui gonfle la dispersion, et cette\n",
    "enquête se situe au sommet de la plage acceptable pour une raison que la section\n",
    "suivante identifie.\n",
    "\n",
    "## L'effet équipe, qui n'est pas un constat nutritionnel"
   ],
   "id": "cell-012"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "analysables |>\n",
    "  group_by(team) |>\n",
    "  summarise(\n",
    "    enfants   = n(),\n",
    "    moyenne_z = round(mean(whz), 2),\n",
    "    mag       = round(100 * mean(whz < -2 | oedema), 1),\n",
    "    .groups = \"drop\"\n",
    "  )"
   ],
   "id": "cell-013"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "L'équipe 3 rapporte une MAG proche de 22 % contre 10 à 16 % pour les autres, avec\n",
    "un score z moyen de -1,09 contre -0,43 à -0,69. Une différence réelle d'état\n",
    "nutritionnel entre des grappes attribuées aléatoirement, d'une telle ampleur,\n",
    "serait extraordinaire. Il s'agit d'un artefact de mesure — une équipe qui mesure\n",
    "les tailles trop longues ou les poids trop légers — et le rapporter comme un\n",
    "constat géographique orienterait des ressources vers les mauvaises grappes.\n",
    "\n",
    "## L'effet de plan\n",
    "\n",
    "C'est un échantillon en grappes. Les enfants d'une même grappe se ressemblent,\n",
    "donc la taille d'échantillon effective est inférieure au nombre d'enfants, et un\n",
    "intervalle de confiance calculé comme pour un sondage aléatoire simple se\n",
    "sous-estime lui-même."
   ],
   "id": "cell-014"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "grappes <- analysables |>\n",
    "  mutate(cas = whz < -2 | oedema) |>\n",
    "  group_by(cluster) |>\n",
    "  summarise(m = n(), y = sum(cas), .groups = \"drop\")\n",
    "\n",
    "k     <- nrow(grappes)\n",
    "M     <- sum(grappes$m)\n",
    "p_bar <- sum(grappes$y) / M\n",
    "\n",
    "# Variance de grappe ultime pour un estimateur de ratio. Elle utilise l'ecart de\n",
    "# chaque grappe entre son nombre de cas et celui que le taux global predit pour\n",
    "# sa taille — et non la variance des taux par grappe, qui ignore que les grappes\n",
    "# n'ont pas la meme taille.\n",
    "var_grappe <- (k / ((k - 1) * M^2)) * sum((grappes$y - p_bar * grappes$m)^2)\n",
    "var_eas    <- p_bar * (1 - p_bar) / M\n",
    "\n",
    "deff <- var_grappe / var_eas\n",
    "icc  <- (deff - 1) / (mean(grappes$m) - 1)\n",
    "\n",
    "cat(sprintf(\"grappes : %d   enfants : %d   taille moyenne : %.1f\\n\",\n",
    "            k, M, mean(grappes$m)))\n",
    "cat(sprintf(\"effet de plan : %.2f   ICC : %.3f\\n\", deff, icc))\n",
    "cat(sprintf(\"taille effective : %.0f sur %d\\n\", M / deff, M))"
   ],
   "id": "cell-015"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Un effet de plan proche de 2,3 et un ICC autour de 0,045 sont ordinaires pour une\n",
    "enquête nutritionnelle en grappes. Un effet de plan inférieur à 1 ou supérieur à\n",
    "4 environ signale généralement un calcul erroné plutôt qu'une enquête inhabituelle\n",
    "— c'est une vérification à mener sur votre propre arithmétique avant de la\n",
    "rapporter."
   ],
   "id": "cell-016"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "et_grappe <- sqrt(var_grappe)\n",
    "et_eas    <- sqrt(var_eas)\n",
    "\n",
    "cat(sprintf(\"MAG %.1f%%  (IC 95%% %.1f - %.1f)  en tenant compte des grappes\\n\",\n",
    "            100 * p_bar, 100 * (p_bar - 1.96 * et_grappe), 100 * (p_bar + 1.96 * et_grappe)))\n",
    "cat(sprintf(\"          (IC 95%% %.1f - %.1f)  en ignorant les grappes\\n\",\n",
    "            100 * (p_bar - 1.96 * et_eas), 100 * (p_bar + 1.96 * et_eas)))"
   ],
   "id": "cell-017"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Deux choses à y lire. L'intervalle correct est plus large, et un rapport qui\n",
    "ignore les grappes revendique une précision que le protocole ne peut pas offrir —\n",
    "la manière la plus courante pour une enquête de surestimer ce qu'elle sait.\n",
    "\n",
    "Et la borne supérieure franchit 15 %. L'estimation ponctuelle se situe sous le\n",
    "seuil d'urgence de l'OMS ; l'intervalle n'exclut pas d'être au-dessus. C'est cette\n",
    "phrase-là que le rapport doit porter, et non un « 14,9 %, sous le seuil\n",
    "d'urgence » sec.\n",
    "\n",
    "## Ce qu'il faut rapporter\n",
    "\n",
    "La prévalence avec son intervalle et l'effet de plan employé, la règle\n",
    "d'exclusion nommée, les exclusions comptées, et la comparaison entre équipes —\n",
    "car une enquête où une équipe s'écarte des autres d'un demi-score z présente un\n",
    "problème de mesure qui prime sur tout chiffre de prévalence du rapport."
   ],
   "id": "cell-018"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "R",
   "language": "R",
   "name": "ir"
  },
  "language_info": {
   "name": "R",
   "file_extension": ".r"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
