---
title: "Aboutissement des référencements et points de rupture du circuit"
subtitle: "Référencements de protection, 2024"
author: "Cassion · data-analysis.cassion.dev"
format:
  html:
    toc: true
    code-fold: false
jupyter: python3
---

## Avant tout code

Ce jeu de données modélise des données de protection et de VBG. Il est
synthétique — aucune personne réelle n'y est décrite — et il ne doit jamais
servir de modèle pour stocker de vraies données de cas. La version sûre de cela
est un système de gestion de cas encadré par le consentement, non un CSV.

Remarquez ce qui **n'y figure pas** : ni noms, ni coordonnées, ni texte libre, ni
date d'incident, ni localisation en deçà de l'admin2, ni âge exact, ni type
d'incident, ni détail sur l'auteur. Rien de tout cela n'est nécessaire pour
mesurer si un circuit de référencement fonctionne, et selon les principes de
gestion de l'information VBG les champs au niveau de l'incident ne sortent jamais
de l'agence de gestion de cas. Collecter moins que ce que l'on pourrait est la
discipline ici modélisée.

## Mise en place

```{python}
import pandas as pd
import numpy as np

URL = (
    "https://data-analysis.cassion.dev/datasets/files/"
    "protection-referrals-2024.v1.csv"
)

cas = pd.read_csv(URL, dtype={"case_id": "string"})
print(cas.shape)
cas.head()
```

## Le consentement conditionne le dénominateur

**Le dénominateur de l'aboutissement, ce sont les cas ayant consenti au
référencement, non l'ensemble des cas.** Compter un cas non consentant comme un
échec du circuit déforme à la fois la performance et la décision d'une personne :
le circuit a fait exactement ce qu'il devait lorsque quelqu'un a refusé.

```{python}
print(f"cas                        : {len(cas)}")
print(f"ont consenti au referencement : {cas['consent_to_refer'].sum()} "
      f"({cas['consent_to_refer'].mean():.1%})")

consentants = cas[cas["consent_to_refer"]].copy()
print(f"ont atteint un service     : {consentants['referral_accepted'].sum()} "
      f"({consentants['referral_accepted'].mean():.1%})")
```

Deux chiffres, deux significations distinctes. Les 88,5 % mesurent la manière
dont le service a été proposé — si les personnes se sont senties assez en
sécurité pour accepter un référencement. Les 46 % mesurent le circuit. Ne
rapporter que le second, au regard de l'ensemble des cas, donnerait un taux
d'aboutissement d'environ 41 % et imputerait au circuit les 11,5 % de refus.

## Résoudre les contradictions avant d'accorder foi à quoi que ce soit

Onze cas consignent un délai jusqu'au premier service alors que le référencement
n'a jamais été accepté. Six d'entre eux ne portent aucun référencement effectué.
Ce sont des impossibilités logiques, et un taux d'aboutissement calculé sur elles
est calculé sur des enregistrements qui ne peuvent pas tous être exacts.

```{python}
delai_consigne = cas["days_to_first_service"].notna()

contradictions = pd.DataFrame({
    "delai consigne, non accepte": [
        int((~cas["referral_accepted"] & delai_consigne).sum())
    ],
    "delai consigne, aucun referencement": [
        int((~cas["referral_made"] & delai_consigne).sum())
    ],
    "delai consigne, sans consentement": [
        int((~cas["consent_to_refer"] & delai_consigne).sum())
    ],
    "accepte, aucun delai consigne": [
        int((cas["referral_accepted"] & ~delai_consigne).sum())
    ],
}).T
contradictions.columns = ["cas"]
contradictions
```

La dernière ligne compte autant que les autres : quarante référencements acceptés
n'ont aucun délai consigné, donc **le dénominateur de la promptitude est plus
petit que celui de l'aboutissement.** Employer l'un pour l'autre fausse les deux.

```{python}
cas["contradictoire"] = (~cas["referral_accepted"]) & delai_consigne
print(f"signales comme contradictoires : {int(cas['contradictoire'].sum())}")
```

Signalez-les plutôt que de les supprimer. Dans un contexte de gestion de cas, un
enregistrement contradictoire est un problème de saisie à renvoyer au
travailleur social, et le supprimer détruit la seule trace de l'existence du cas.

## Normaliser le champ handicap avant de désagréger dessus

Une zone a consigné le handicap en `Yes` et `No` plutôt qu'en `true` et `false`.
Laissée telle quelle, la désagrégation se fragmente en quatre catégories, dont
deux trop petites pour être interprétées — et ces deux-là proviennent d'une seule
zone : ce n'est donc pas un sous-ensemble aléatoire.

```{python}
print(cas["disability_reported"].value_counts(dropna=False))
```

```{python}
HANDICAP = {"true": True, "yes": True, "false": False, "no": False}
cas["handicap"] = (
    cas["disability_reported"].astype("string").str.strip().str.lower().map(HANDICAP)
)
consentants = cas[cas["consent_to_refer"]].copy()
print(consentants["handicap"].value_counts(dropna=False))
```

## Où le circuit se rompt

```{python}
def aboutissement(df, par):
    out = df.groupby(par).agg(
        cas=("referral_accepted", "size"),
        aboutis=("referral_accepted", "sum"),
    )
    out["taux"] = (out["aboutis"] / out["cas"]).round(3)
    return out.sort_values("taux")

aboutissement(consentants, "service_requested")
```

Le soutien aux moyens d'existence aboutit à environ 23 % contre environ 62 % pour
la santé. Ce n'est pas un écart de performance des travailleurs sociaux : c'est
un énoncé sur les services qui existent et disposent de capacités. Une analyse de
circuit qui s'arrête à 46 % global escamote tout le constat.

```{python}
aboutissement(consentants, "admin2")
```

Entre environ 31 % et 57 % selon les six zones. Croisez les deux ventilations et
le point de rupture devient localisable :

```{python}
grille = pd.crosstab(
    consentants["admin2"],
    consentants["service_requested"],
    values=consentants["referral_accepted"],
    aggfunc="mean",
).round(2)

effectifs = pd.crosstab(consentants["admin2"], consentants["service_requested"])
grille.where(effectifs >= 20)
```

Les cellules de moins de vingt cas sont laissées vides plutôt qu'affichées. Un
taux d'aboutissement sur huit cas n'est pas un constat, et en protection une
petite cellule constitue aussi un risque de divulgation — voir plus bas.

## Le constat d'équité

```{python}
par_handicap = aboutissement(consentants.dropna(subset=["handicap"]), "handicap")
par_handicap
```

Les cas où un handicap est signalé aboutissent à environ 31 % contre 48 % lorsque
aucun ne l'est. C'est le constat que ce jeu de données existe pour faire
apparaître, et il n'apparaît que si l'on désagrège.

```{python}
from scipy.stats import chi2_contingency

table = pd.crosstab(
    consentants.dropna(subset=["handicap"])["handicap"],
    consentants.dropna(subset=["handicap"])["referral_accepted"],
)
chi2, p, dof, attendu = chi2_contingency(table)
print(f"ecart d'aboutissement : "
      f"{par_handicap['taux'].iloc[-1] - par_handicap['taux'].iloc[0]:+.3f}")
print(f"p du khi-deux         : {p:.2e}")
```

Un écart de dix-sept points, peu susceptible d'être dû au hasard. Ce qu'il ne
vous dit pas, c'est *pourquoi* — si les services sont physiquement inaccessibles,
si les circuits supposent une mobilité que certains clients n'ont pas, ou tout
autre chose. À cette question répondent les travailleurs sociaux, non ce tableau.

## Les petites cellules sont un risque de protection, pas seulement statistique

```{python}
sensibles = pd.crosstab(consentants["admin2"], consentants["case_category"])
sensibles.where(sensibles < 20)
```

Ici, toutes les cellules dépassent vingt : rien n'est supprimé — et ce résultat
vide est précisément la raison pour laquelle ce contrôle doit rester dans la
chaîne plutôt que d'être exécuté une seule fois. Dans un tableau par district,
une cellule de trois cas de VBG peut identifier une survivante pour quiconque
connaît la zone. Traitez le seuil de suppression comme une décision de protection
prise avec l'agence de gestion de cas, non comme une préférence de mise en forme.

## Ce qu'il faut rapporter

L'aboutissement au regard du dénominateur conditionné par le consentement,
décomposé assez finement pour localiser le point de rupture, avec les
enregistrements contradictoires signalés et comptés. L'écart lié au handicap,
énoncé clairement. Et rien à une granularité susceptible d'identifier une
personne — ce qui, dans ce secteur, est la contrainte qui prime sur toute
préférence analytique.
