{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Promptitude au regard du standard clinique\n",
    "\n",
    "*Référencements de protection, 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/protection-referrals-2024/timeliness.python.fr.ipynb)"
   ],
   "id": "cell-000"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La question, et pourquoi le chronomètre compte\n",
    "\n",
    "Pour une survivante de VBG cherchant des soins de santé, 72 heures n'est pas une\n",
    "cible administrative. La prophylaxie post-exposition au VIH doit débuter dans les\n",
    "72 heures pour être efficace, et la contraception d'urgence obéit à une fenêtre\n",
    "comparable. Un référencement qui aboutit au quatrième jour est un événement\n",
    "différent d'un référencement abouti au deuxième, et un délai moyen le dissimule\n",
    "entièrement.\n",
    "\n",
    "Ce jeu de données est synthétique. Aucune personne réelle n'y est décrite, et il\n",
    "ne doit jamais servir de modèle pour stocker de vraies données de cas.\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",
    "cas = pd.read_csv(URL, dtype={\"case_id\": \"string\"})\n",
    "print(cas.shape)"
   ],
   "id": "cell-002"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Construire le dénominateur délibérément\n",
    "\n",
    "La promptitude se mesure sur les référencements **acceptés** : un référencement\n",
    "qui n'a jamais atteint un service n'a pas de date de service et relève de\n",
    "l'indicateur d'aboutissement, non de celui-ci. On restreint ensuite à la\n",
    "population à laquelle le standard s'applique."
   ],
   "id": "cell-003"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "acceptes = cas[cas[\"referral_accepted\"]].copy()\n",
    "\n",
    "vbg_sante = acceptes[\n",
    "    (acceptes[\"case_category\"] == \"gbv\")\n",
    "    & (acceptes[\"service_requested\"] == \"health\")\n",
    "].copy()\n",
    "\n",
    "print(f\"tous les cas                        : {len(cas)}\")\n",
    "print(f\"referencements acceptes             : {len(acceptes)}\")\n",
    "print(f\"referencements VBG-sante acceptes   : {len(vbg_sante)}\")\n",
    "print(f\"  avec un delai de service consigne : {int(vbg_sante['days_to_first_service'].notna().sum())}\")"
   ],
   "id": "cell-004"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Chaque restriction est une décision, et chacune doit être défendable. Le standard\n",
    "des 72 heures est clinique et s'applique aux soins de santé après violence\n",
    "sexuelle — l'appliquer à un référencement d'aide juridique reviendrait à inventer\n",
    "une cible.\n",
    "\n",
    "## Les dates manquantes constituent l'analyse\n",
    "\n",
    "Quarante référencements acceptés, sur l'ensemble du jeu de données, n'ont aucun\n",
    "délai de service consigné. **Le dénominateur de la promptitude est donc plus\n",
    "petit que celui de l'aboutissement**, et le traitement retenu pour ces quarante\n",
    "change la réponse."
   ],
   "id": "cell-005"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "manquants = vbg_sante[\"days_to_first_service\"].isna()\n",
    "print(f\"referencements VBG-sante acceptes sans delai : {int(manquants.sum())}\")"
   ],
   "id": "cell-006"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Trois traitements défendables, trois chiffres différents :"
   ],
   "id": "cell-007"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "FENETRE_JOURS = 3  # 72 heures\n",
    "\n",
    "dans_fenetre = vbg_sante[\"days_to_first_service\"] <= FENETRE_JOURS\n",
    "consigne = vbg_sante[\"days_to_first_service\"].notna()\n",
    "\n",
    "traitements = pd.DataFrame([\n",
    "    {\n",
    "        \"traitement\": \"exclure les manquants (consignes seuls)\",\n",
    "        \"denominateur\": int(consigne.sum()),\n",
    "        \"sous 72h\": int((dans_fenetre & consigne).sum()),\n",
    "    },\n",
    "    {\n",
    "        \"traitement\": \"compter les manquants hors fenetre\",\n",
    "        \"denominateur\": len(vbg_sante),\n",
    "        \"sous 72h\": int((dans_fenetre & consigne).sum()),\n",
    "    },\n",
    "    {\n",
    "        \"traitement\": \"compter les manquants dans la fenetre\",\n",
    "        \"denominateur\": len(vbg_sante),\n",
    "        \"sous 72h\": int((dans_fenetre & consigne).sum() + manquants.sum()),\n",
    "    },\n",
    "])\n",
    "traitements[\"taux\"] = (traitements[\"sous 72h\"] / traitements[\"denominateur\"]).round(3)\n",
    "traitements"
   ],
   "id": "cell-008"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Lequel retenir ?** Le premier est l'option honnête par défaut : il rapporte ce\n",
    "qui est connu et énonce son dénominateur. Le deuxième est l'option conservatrice,\n",
    "défendable si vous avez des raisons de penser qu'une date manquante signifie un\n",
    "service jamais délivré. Le troisième ne l'est pas : il suppose le meilleur cas\n",
    "pour les dossiers que vous connaissez le moins bien.\n",
    "\n",
    "Rapportez le premier, et indiquez dans la même phrase le nombre\n",
    "d'enregistrements qu'il a exclus. Un chiffre de promptitude sans son dénominateur\n",
    "n'est pas une mesure.\n",
    "\n",
    "## La distribution, non la moyenne"
   ],
   "id": "cell-009"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "delais_consignes = vbg_sante.loc[consigne, \"days_to_first_service\"]\n",
    "\n",
    "print(f\"moyenne : {delais_consignes.mean():.1f} jours\")\n",
    "print(f\"mediane : {delais_consignes.median():.0f} jours\")\n",
    "\n",
    "delais_consignes.value_counts().sort_index().head(12)"
   ],
   "id": "cell-010"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La moyenne ne sert quasiment à rien ici. Le standard est un seuil : ce qui compte\n",
    "est la part du bon côté de ce seuil et la distance à laquelle le reste s'en\n",
    "écarte. Un programme dont la moyenne est de quatre jours peut avoir tous ses cas\n",
    "à quatre jours, ou la moitié à un jour et la moitié à sept — et un seul de ces\n",
    "deux profils constitue une défaillance de service."
   ],
   "id": "cell-011"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "bandes = pd.cut(\n",
    "    delais_consignes,\n",
    "    bins=[-1, 3, 7, 14, np.inf],\n",
    "    labels=[\"sous 72h\", \"4-7 jours\", \"8-14 jours\", \"plus de 14 jours\"],\n",
    ")\n",
    "(bandes.value_counts(normalize=True).sort_index() * 100).round(1)"
   ],
   "id": "cell-012"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Où se situe le délai"
   ],
   "id": "cell-013"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def promptitude(df, par, fenetre=FENETRE_JOURS):\n",
    "    connus = df[df[\"days_to_first_service\"].notna()]\n",
    "    out = connus.groupby(par).agg(\n",
    "        referencements=(\"days_to_first_service\", \"size\"),\n",
    "        dans_fenetre=(\"days_to_first_service\", lambda s: (s <= fenetre).sum()),\n",
    "        mediane_jours=(\"days_to_first_service\", \"median\"),\n",
    "    )\n",
    "    out[\"sous 72h\"] = (out[\"dans_fenetre\"] / out[\"referencements\"]).round(3)\n",
    "    return out.sort_values(\"sous 72h\")\n",
    "\n",
    "promptitude(vbg_sante, \"admin2\")"
   ],
   "id": "cell-014"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Petits dénominateurs à nouveau — les zones apportent entre six et vingt-sept\n",
    "référencements chacune, si bien que l'ordre entre elles est fragile et que la\n",
    "plus petite n'est pas interprétable du tout. Rapportez les zones, rapportez les\n",
    "effectifs, et renoncez à les classer."
   ],
   "id": "cell-015"
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "promptitude(acceptes[acceptes[\"service_requested\"] == \"health\"], \"case_category\")"
   ],
   "id": "cell-016"
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ce que cela ne peut pas vous dire\n",
    "\n",
    "Le chronomètre démarre ici au référencement et s'arrête au premier service. Il ne\n",
    "démarre pas à l'incident, car ce jeu de données ne porte délibérément aucune date\n",
    "d'incident — et selon les principes de gestion de l'information VBG, il ne le\n",
    "devrait pas.\n",
    "\n",
    "Cela signifie qu'**une survivante ayant atteint un travailleur social au\n",
    "cinquième jour et un service au sixième apparaît ici comme un référencement d'un\n",
    "jour**, largement dans la fenêtre, alors que la fenêtre clinique était déjà\n",
    "close. L'indicateur mesure le circuit de référencement, non le délai total\n",
    "d'accès aux soins, et un rapport qui confond les deux surestime ce que le\n",
    "programme a accompli.\n",
    "\n",
    "Énoncez-le dans les limites. C'est la différence entre un indicateur de circuit\n",
    "honnête et une affirmation sur des résultats cliniques que les données ne\n",
    "peuvent pas soutenir.\n",
    "\n",
    "## Ce qu'il faut rapporter\n",
    "\n",
    "La part sous 72 heures, son dénominateur, le nombre de référencements acceptés\n",
    "exclus faute de date de service, la distribution plutôt que la moyenne, et\n",
    "l'énoncé explicite que le chronomètre démarre au référencement et non à\n",
    "l'incident."
   ],
   "id": "cell-017"
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "file_extension": ".py"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
