{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Où renforcer la capacité du circuit — notebook d'analyse\n",
    "\n",
    "*Performance du circuit de référencement en protection · livrable de projet*\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/projects/protection-referral-pathway/notebooks/referral-pathway.python.fr.ipynb)"
   ],
   "id": "cell-000"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Avant toute chose\n",
    "\n",
    "Il s'agit de données de protection et de VBG synthétiques. Aucune personne réelle\n",
    "n'y est décrite, et ce fichier ne doit jamais servir de modèle pour stocker des\n",
    "données de cas réelles — la version sûre de cela est un système de gestion de cas\n",
    "régi par le consentement.\n",
    "\n",
    "Ce que le jeu de données omet délibérément fait partie du livrable : pas de noms,\n",
    "pas de coordonnées, pas de texte libre, pas de date d'incident, pas de\n",
    "localisation en dessous de l'admin2, pas d'âge exact, pas de type d'incident.\n",
    "Aucun de ces éléments n'est nécessaire pour mesurer si un circuit fonctionne, et\n",
    "selon les principes de gestion de l'information sur les VBG, les champs au niveau\n",
    "de l'incident ne quittent jamais l'agence de gestion de cas.\n",
    "\n",
    "## La décision que ce document produit\n",
    "\n",
    "Quelle ligne de service et quelle zone reçoivent une capacité supplémentaire, et\n",
    "si l'écart lié au handicap appelle une réponse distincte.\n",
    "\n",
    "Le problème que ce projet existe pour corriger : le circuit était rapporté sous la\n",
    "forme d'un taux d'aboutissement unique, qui ne désignait rien d'actionnable.\n",
    "\n",
    "Destinataires : le coordinateur du cluster protection et le sous-cluster VBG.\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",
    "    \"protection-referrals-2024.v1.csv\"\n",
    ")\n",
    "\n",
    "MIN_CELL = 20   # seuil de suppression, fixe avec l'agence de gestion de cas\n",
    "\n",
    "cases = pd.read_csv(URL, dtype={\"case_id\": \"string\"})\n",
    "print(f\"{len(cases)} cas\")"
   ],
   "id": "cell-002"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Le consentement conditionne le dénominateur\n",
    "\n",
    "L'aboutissement est mesuré sur les cas ayant **consenti** au référencement.\n",
    "Compter un cas sans consentement comme un échec du circuit fausse à la fois la\n",
    "performance et la décision de la personne — le circuit a fait ce qu'il devait\n",
    "lorsque quelqu'un a refusé."
   ],
   "id": "cell-003"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "DISABILITY = {\"true\": True, \"yes\": True, \"false\": False, \"no\": False}\n",
    "cases[\"disability\"] = (\n",
    "    cases[\"disability_reported\"].astype(\"string\").str.strip().str.lower().map(DISABILITY)\n",
    ")\n",
    "\n",
    "consenting = cases[cases[\"consent_to_refer\"]].copy()\n",
    "\n",
    "pd.Series({\n",
    "    \"cas\": len(cases),\n",
    "    \"ayant consenti\": int(cases[\"consent_to_refer\"].sum()),\n",
    "    \"taux de consentement\": round(cases[\"consent_to_refer\"].mean(), 3),\n",
    "    \"ayant atteint un service\": int(consenting[\"referral_accepted\"].sum()),\n",
    "    \"aboutissement (consentement)\": round(consenting[\"referral_accepted\"].mean(), 3),\n",
    "    \"aboutissement si tous les cas comptes\": round(cases[\"referral_accepted\"].mean(), 3),\n",
    "})"
   ],
   "id": "cell-004"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Les deux chiffres d'aboutissement diffèrent de cinq points, et le mauvais impute\n",
    "au circuit des personnes qui ont choisi de ne pas être référées.\n",
    "\n",
    "## Contradictions traitées avant tout classement"
   ],
   "id": "cell-005"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "timed = cases[\"days_to_first_service\"].notna()\n",
    "\n",
    "contradictions = pd.Series({\n",
    "    \"delai de service saisi, referencement non abouti\": int((~cases[\"referral_accepted\"] & timed).sum()),\n",
    "    \"delai de service saisi, aucun referencement effectue\": int((~cases[\"referral_made\"] & timed).sum()),\n",
    "    \"abouti, aucun delai de service saisi\": int((cases[\"referral_accepted\"] & ~timed).sum()),\n",
    "})\n",
    "contradictions"
   ],
   "id": "cell-006"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Signalés, non supprimés : en gestion de cas, un enregistrement contradictoire est\n",
    "un problème de saisie à renvoyer au travailleur social, et le supprimer détruit la\n",
    "seule trace de l'existence du cas. Les quarante référencements aboutis sans délai\n",
    "de service signifient que le **dénominateur de la ponctualité est plus petit que\n",
    "celui de l'aboutissement** : les deux sont donc rapportés séparément ci-dessous.\n",
    "\n",
    "## Où le circuit se rompt"
   ],
   "id": "cell-007"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def completion(df, by):\n",
    "    out = df.groupby(by).agg(\n",
    "        cas=(\"referral_accepted\", \"size\"),\n",
    "        aboutis=(\"referral_accepted\", \"sum\"),\n",
    "    )\n",
    "    out[\"completion\"] = (out[\"aboutis\"] / out[\"cas\"]).round(3)\n",
    "    return out.sort_values(\"completion\")\n",
    "\n",
    "completion(consenting, \"service_requested\")"
   ],
   "id": "cell-008"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "completion(consenting, \"admin2\")"
   ],
   "id": "cell-009"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Le soutien aux moyens de subsistance aboutit à environ un tiers du taux de la\n",
    "santé. Ce n'est pas la performance des travailleurs sociaux — c'est la question de\n",
    "savoir quels services existent et disposent de capacité, ce sur quoi une décision\n",
    "de capacité doit précisément agir."
   ],
   "id": "cell-010"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "grid = pd.crosstab(\n",
    "    consenting[\"admin2\"], consenting[\"service_requested\"],\n",
    "    values=consenting[\"referral_accepted\"], aggfunc=\"mean\",\n",
    ").round(2)\n",
    "counts = pd.crosstab(consenting[\"admin2\"], consenting[\"service_requested\"])\n",
    "\n",
    "grid.where(counts >= MIN_CELL)"
   ],
   "id": "cell-011"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Les cellules sous le seuil de suppression sont laissées vides. En protection, une\n",
    "petite cellule est un risque de divulgation autant qu'un problème statistique, et\n",
    "le seuil est une décision prise avec l'agence de gestion de cas, non un choix de\n",
    "mise en forme.\n",
    "\n",
    "## La recommandation\n",
    "\n",
    "La capacité va là où le taux d'aboutissement est faible **et** où le volume est\n",
    "suffisant pour que le gain compte. Une ligne de service qui aboutit à 23 % sur\n",
    "30 cas est un problème différent d'une ligne qui aboutit à 23 % sur 200."
   ],
   "id": "cell-012"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "lines = completion(consenting, \"service_requested\")\n",
    "lines[\"shortfall\"] = (\n",
    "    (lines[\"completion\"].max() - lines[\"completion\"]) * lines[\"cas\"]\n",
    ").round(0)\n",
    "lines.sort_values(\"shortfall\", ascending=False)"
   ],
   "id": "cell-013"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La colonne `shortfall` est le nombre de personnes supplémentaires qui\n",
    "atteindraient un service si cette ligne performait comme la meilleure. C'est une\n",
    "estimation grossière — elle suppose que l'écart tient à la capacité et non au\n",
    "besoin — mais elle a la bonne forme pour une décision de capacité, et elle classe\n",
    "différemment du seul taux d'aboutissement."
   ],
   "id": "cell-014"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "areas = completion(consenting, \"admin2\")\n",
    "areas[\"shortfall\"] = (\n",
    "    (areas[\"completion\"].max() - areas[\"completion\"]) * areas[\"cas\"]\n",
    ").round(0)\n",
    "areas.sort_values(\"shortfall\", ascending=False)"
   ],
   "id": "cell-015"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## L'écart lié au handicap"
   ],
   "id": "cell-016"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "gap = completion(consenting.dropna(subset=[\"disability\"]), \"disability\")\n",
    "gap"
   ],
   "id": "cell-017"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from scipy.stats import chi2_contingency\n",
    "\n",
    "known = consenting.dropna(subset=[\"disability\"])\n",
    "table = pd.crosstab(known[\"disability\"], known[\"referral_accepted\"])\n",
    "chi2, p, dof, expected = chi2_contingency(table)\n",
    "\n",
    "print(f\"ecart d'aboutissement : {gap['completion'].iloc[0] - gap['completion'].iloc[-1]:+.3f}\")\n",
    "print(f\"p du khi-deux        : {p:.2e}\")"
   ],
   "id": "cell-018"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "by_service = (\n",
    "    known.groupby([\"service_requested\", \"disability\"])[\"referral_accepted\"]\n",
    "    .agg([\"size\", \"mean\"])\n",
    "    .round(3)\n",
    "    .unstack()\n",
    ")\n",
    "by_service"
   ],
   "id": "cell-019"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**L'écart appelle-t-il une réponse distincte ?** La réponse que soutient ce\n",
    "notebook est oui, pour une raison que l'agrégat masque : l'écart n'est pas\n",
    "concentré sur une seule ligne de service. Il apparaît sur quatre des cinq — la\n",
    "sécurité et la sûreté font exception, à 35 % de part et d'autre — ce qui rend\n",
    "improbable qu'il s'explique par la composition des services demandés par les\n",
    "personnes en situation de handicap. Ce que cela ne dit pas, c'est le mécanisme —\n",
    "inaccessibilité physique, circuits supposant la mobilité, ou autre chose — et cela\n",
    "se répond en interrogeant les travailleurs sociaux, non par ce tableau.\n",
    "\n",
    "Une zone a saisi le handicap en `Yes`/`No` plutôt qu'en `true`/`false`. Sans\n",
    "normalisation, la désagrégation éclate en quatre catégories, dont deux issues de\n",
    "cette seule zone et trop petites pour être interprétées — l'écart aurait donc été\n",
    "invisible.\n",
    "\n",
    "## Ce que cela n'établit pas\n",
    "\n",
    "Les enregistrements de référencement montrent ce que le circuit a fait, non ce\n",
    "dont les personnes avaient besoin. Une ligne de service avec peu de référencements\n",
    "peut être une ligne dont personne n'a besoin ou une ligne que personne n'offre, et\n",
    "ce jeu de données ne permet pas de distinguer les deux.\n",
    "\n",
    "La ponctualité est mesurée du référencement au premier service, non depuis\n",
    "l'incident, parce que le jeu de données ne contient aucune date d'incident et que,\n",
    "selon les principes de gestion de l'information sur les VBG, il ne doit pas en\n",
    "contenir. Une survivante qui a atteint un travailleur social tardivement puis un\n",
    "service rapidement apparaît ici comme rapide."
   ],
   "id": "cell-020"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "file_extension": ".py"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
