{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Calculer le SCA, l'ÉFM et l'ICSR à partir de leurs composantes\n",
    "\n",
    "*Enquête de sécurité alimentaire, 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/food-security-survey-2024/composite-indicators.python.fr.ipynb)"
   ],
   "id": "cell-000"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ce que produit ce document\n",
    "\n",
    "Les trois indicateurs composites de sécurité alimentaire, construits à partir de\n",
    "leurs composantes plutôt que lus dans une colonne précalculée — parce que c'est\n",
    "dans la construction que se logent toutes les erreurs.\n",
    "\n",
    "Valeurs de référence issues des notes de qualité : avec les seuils standards\n",
    "21/35 du SCA, environ 1 % des ménages sont en consommation pauvre et 23 % en\n",
    "consommation limite ; avec le jeu 28/42, 7 % et 39 %. Aucun n'est faux. Ne pas\n",
    "dire lequel a été utilisé l'est.\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",
    "    \"food-security-survey-2024.v1.csv\"\n",
    ")\n",
    "\n",
    "fs = pd.read_csv(URL, dtype={\"household_id\": \"string\"})\n",
    "print(fs.shape)\n",
    "fs.head()"
   ],
   "id": "cell-002"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Contrôler les plages avant de scorer\n",
    "\n",
    "Vingt-trois enregistrements portent une valeur de consommation supérieure à sept\n",
    "jours, ce qui est impossible pour un rappel de sept jours. Un score calculé sans\n",
    "contrôle de plage hérite de la valeur impossible et gonfle ce ménage — en\n",
    "silence, puisque le résultat reste un nombre plausible."
   ],
   "id": "cell-003"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "PONDERATIONS_SCA = {\n",
    "    \"fcs_cereals_tubers\": 2,\n",
    "    \"fcs_pulses\": 3,\n",
    "    \"fcs_vegetables\": 1,\n",
    "    \"fcs_fruit\": 1,\n",
    "    \"fcs_meat_fish_eggs\": 4,\n",
    "    \"fcs_dairy\": 4,\n",
    "    \"fcs_oils_fats\": 0.5,\n",
    "    \"fcs_sugar\": 0.5,\n",
    "}\n",
    "\n",
    "composantes = list(PONDERATIONS_SCA)\n",
    "impossibles = (fs[composantes] > 7).sum().sum()\n",
    "vides = fs[composantes].isna().sum().sum()\n",
    "\n",
    "print(f\"valeurs superieures a 7 jours : {int(impossibles)}\")\n",
    "print(f\"cellules vides                : {int(vides)}\")\n",
    "fs[composantes].agg([\"min\", \"max\"]).T"
   ],
   "id": "cell-004"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# Hors plage n'est pas une mesure. Passez en manquant plutot que de tronquer a 7 :\n",
    "# tronquer invente une valeur que l'enqueteur n'a jamais relevee.\n",
    "for colonne in composantes:\n",
    "    fs[colonne] = fs[colonne].where(fs[colonne].between(0, 7))"
   ],
   "id": "cell-005"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La case vide qui n'est pas un zéro\n",
    "\n",
    "Environ 127 cellules réparties entre produits laitiers, fruits et viande sont\n",
    "vides. **Traiter une case vide comme zéro jour score le ménage comme mangeant\n",
    "moins qu'il n'a mangé**, et les composantes le plus souvent vides portent les\n",
    "pondérations les plus lourdes — produits laitiers et viande valent 4 chacune.\n",
    "C'est la défaillance silencieuse la plus lourde de conséquences de ce jeu de\n",
    "données, et cela tient à une ligne de code dans un sens comme dans l'autre."
   ],
   "id": "cell-006"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "fs[\"sca_complet\"] = fs[composantes].notna().all(axis=1)\n",
    "\n",
    "# NaN se propage : un menage incomplet ne recoit donc aucun score, ce qui est le\n",
    "# comportement correct. La version remplie de zeros ne sert qu'a mesurer le degat.\n",
    "fs[\"sca\"] = sum(fs[c] * w for c, w in PONDERATIONS_SCA.items())\n",
    "fs[\"sca_zeros\"] = sum(fs[c].fillna(0) * w for c, w in PONDERATIONS_SCA.items())\n",
    "\n",
    "incomplets = ~fs[\"sca_complet\"]\n",
    "print(f\"menages dont le SCA est incomplet : {int(incomplets.sum())}\")\n",
    "\n",
    "pd.DataFrame({\n",
    "    \"menages complets (score reel)\": fs.loc[~incomplets, \"sca\"].describe(),\n",
    "    \"menages incomplets, remplis de zeros\": fs.loc[incomplets, \"sca_zeros\"].describe(),\n",
    "}).round(1)"
   ],
   "id": "cell-007"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Les ménages remplis de zéros se situent en moyenne six points sous ceux qui ont\n",
    "répondu intégralement. Cet écart n'est pas un constat sur leur alimentation :\n",
    "c'est le décompte des cases vides comme journées sans manger."
   ],
   "id": "cell-008"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def part(scores, pauvre, limite):\n",
    "    s = scores.dropna()\n",
    "    return {\n",
    "        \"pauvre %\": round((s <= pauvre).mean() * 100, 2),\n",
    "        \"limite %\": round(((s > pauvre) & (s <= limite)).mean() * 100, 2),\n",
    "        \"menages\": len(s),\n",
    "    }\n",
    "\n",
    "pd.DataFrame({\n",
    "    \"exclure les incomplets (correct)\": part(fs[\"sca\"], 21, 35),\n",
    "    \"remplir de zeros et conserver\": part(fs[\"sca_zeros\"], 21, 35),\n",
    "}).T"
   ],
   "id": "cell-009"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Aux seuils 21/35 la distorsion est faible. Aux seuils 28/42, elle ne l'est plus :"
   ],
   "id": "cell-010"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "zeros_incomplets = fs.loc[incomplets, \"sca_zeros\"]\n",
    "print(f\"sur les {len(zeros_incomplets)} menages incomplets, le remplissage par zero classe\")\n",
    "print(f\"  {int((zeros_incomplets <= 21).sum())} en consommation pauvre a 21/35\")\n",
    "print(f\"  {int((zeros_incomplets <= 28).sum())} en consommation pauvre a 28/42\")\n",
    "\n",
    "pd.DataFrame({\n",
    "    \"exclure les incomplets (correct)\": part(fs[\"sca\"], 28, 42),\n",
    "    \"remplir de zeros et conserver\": part(fs[\"sca_zeros\"], 28, 42),\n",
    "}).T"
   ],
   "id": "cell-011"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Trente-deux ménages classés en consommation alimentaire pauvre sur des scores\n",
    "artificiellement bas, parce qu'une case vide a été lue comme un zéro. Chacun\n",
    "d'eux serait comptabilisé dans une charge de cas.\n",
    "\n",
    "## Les deux jeux de seuils, côte à côte"
   ],
   "id": "cell-012"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def groupe_consommation(score, pauvre, limite):\n",
    "    if pd.isna(score):\n",
    "        return None\n",
    "    if score <= pauvre:\n",
    "        return \"pauvre\"\n",
    "    if score <= limite:\n",
    "        return \"limite\"\n",
    "    return \"acceptable\"\n",
    "\n",
    "valides = fs[fs[\"sca_complet\"]].copy()\n",
    "\n",
    "for etiquette, (pauvre, limite) in {\n",
    "    \"21/35\": (21, 35),\n",
    "    \"28/42\": (28, 42),\n",
    "}.items():\n",
    "    valides[f\"groupe {etiquette}\"] = valides[\"sca\"].apply(\n",
    "        lambda s: groupe_consommation(s, pauvre, limite)\n",
    "    )\n",
    "\n",
    "pd.DataFrame({\n",
    "    etiquette: valides[f\"groupe {etiquette}\"].value_counts(normalize=True)\n",
    "    for etiquette in [\"21/35\", \"28/42\"]\n",
    "}).round(3)"
   ],
   "id": "cell-013"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Le jeu 28/42 s'emploie là où l'huile et le sucre sont consommés de manière quasi\n",
    "universelle, ce qui gonfle tous les scores et rend les seuils standards trop\n",
    "généreux. Le retenir relève d'un jugement sur le système alimentaire, non sur les\n",
    "données — et **ce choix fait passer le chiffre de tête de 1 % à 7 % de\n",
    "consommation pauvre**. Énoncez le jeu de seuils employé, dans la même phrase que\n",
    "le chiffre.\n",
    "\n",
    "## L'échelle de la faim dans le ménage\n",
    "\n",
    "L'ÉFM n'est valide que si les trois questions ont reçu une réponse. Une réponse\n",
    "partielle doit être **exclue, et non remplie de zéros** — la remplir de zéros\n",
    "score un ménage affamé comme étant en sécurité alimentaire."
   ],
   "id": "cell-014"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "ITEMS_EFM = [\n",
    "    \"hhs_no_food_in_house\",\n",
    "    \"hhs_sleep_hungry\",\n",
    "    \"hhs_day_and_night_without_eating\",\n",
    "]\n",
    "\n",
    "efm_complet = fs[ITEMS_EFM].notna().all(axis=1)\n",
    "efm_partiel = fs[ITEMS_EFM].isna().any(axis=1) & fs[ITEMS_EFM].notna().any(axis=1)\n",
    "\n",
    "print(f\"complets : {int(efm_complet.sum())}   partiels : {int(efm_partiel.sum())}\")\n",
    "\n",
    "fs[\"efm\"] = fs[ITEMS_EFM].sum(axis=1).where(efm_complet)\n",
    "fs[\"categorie_efm\"] = pd.cut(\n",
    "    fs[\"efm\"], [-1, 1, 3, 6],\n",
    "    labels=[\"faible ou nulle\", \"moderee\", \"severe\"],\n",
    ")\n",
    "\n",
    "(fs[\"categorie_efm\"].value_counts(normalize=True).sort_index() * 100).round(1)"
   ],
   "id": "cell-015"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Notez que `.sum(axis=1)` renverrait volontiers un score pour une réponse\n",
    "partielle : pandas traite les valeurs manquantes comme des zéros dans une somme\n",
    "par ligne. C'est le `.where(efm_complet)` qui rend l'exclusion effective, et son\n",
    "omission est exactement la défaillance que la note met en garde.\n",
    "\n",
    "## L'indice réduit des stratégies de survie"
   ],
   "id": "cell-016"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "PONDERATIONS_ICSR = {\n",
    "    \"rcsi_less_preferred_food\": 1,\n",
    "    \"rcsi_borrowed_food\": 2,\n",
    "    \"rcsi_limit_portion_size\": 1,\n",
    "    \"rcsi_restrict_adult_consumption\": 3,\n",
    "    \"rcsi_reduce_meal_numbers\": 1,\n",
    "}\n",
    "\n",
    "fs[\"icsr\"] = sum(fs[c] * w for c, w in PONDERATIONS_ICSR.items())\n",
    "fs[\"icsr\"].describe().round(1)"
   ],
   "id": "cell-017"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Les pondérations traduisent la gravité, non la fréquence : restreindre la\n",
    "consommation des adultes pour que les enfants mangent vaut 3, acheter des\n",
    "aliments moins appréciés vaut 1. Elles sont fixées par le standard — ne les\n",
    "ajustez pas au contexte, car un ICSR repondéré localement n'est comparable à rien.\n",
    "\n",
    "## Ne pas utiliser l'un comme approximation de l'autre"
   ],
   "id": "cell-018"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "deux = fs[fs[\"sca_complet\"]]\n",
    "correlation = deux[[\"sca\", \"icsr\"]].corr().iloc[0, 1]\n",
    "print(f\"correlation entre SCA et ICSR : {correlation:.3f}\")"
   ],
   "id": "cell-019"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Environ -0,45. Les deux mesurent des choses liées mais distinctes : un ménage peut\n",
    "manger de façon monotone sans avoir encore recours à des stratégies d'adaptation,\n",
    "et un autre peut s'adapter fortement tout en conservant un régime varié grâce à\n",
    "des aliments empruntés. Rapporter l'un à la place de l'autre perd une information\n",
    "réelle, et c'est le genre de raccourci qui tient jusqu'à ce que quelqu'un demande\n",
    "pourquoi les deux tableaux se contredisent."
   ],
   "id": "cell-020"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "pd.crosstab(\n",
    "    deux[\"sca\"].apply(lambda s: groupe_consommation(s, 21, 35)),\n",
    "    pd.cut(deux[\"icsr\"], [-1, 3, 18, 100], labels=[\"faible\", \"moyen\", \"eleve\"]),\n",
    "    normalize=\"index\",\n",
    ").round(3)"
   ],
   "id": "cell-021"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ce que cela produit, et ce que cela ne produit pas\n",
    "\n",
    "Ce sont les données probantes de consommation alimentaire utilisées **dans** une\n",
    "analyse IPC. Ce n'est pas une phase IPC. Une phase est attribuée par un groupe de\n",
    "travail technique qui fait converger plusieurs indicateurs de résultat avec les\n",
    "facteurs contributifs, et un tableau qui affiche « phase 3 » à partir d'une\n",
    "distribution de SCA a escamoté tout le processus analytique que la classification\n",
    "existe pour représenter."
   ],
   "id": "cell-022"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "file_extension": ".py"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
