cassionAnalyse de données

Leçon 5 sur 8

Unité · Des valeurs qui ne peuvent pas être vraies

Des règles que le secteur vous a déjà données

Transformer « cela semble faux » en une table de règles avec une source, une gravité et un compte. Bornes dures face aux limites physiques, bornes souples face aux seuils du secteur, et la discipline du signalement plutôt que de la suppression.

PythonR90 minNormes OMS de croissance de l'enfantStandards SphèreEnquête SMART

« Cela semble faux » n’est pas une règle

Quiconque fait ce métier finit par acquérir l’œil. Vous faites défiler une colonne, quelque chose accroche, vous allez voir. Cela marche, et cela ne passe pas à l’échelle, ne se transmet pas à la personne qui vous succédera, et ne se défend pas en revue — car la réponse à « pourquoi avez-vous supprimé cette ligne » est « elle semblait fausse ».

Le remède consiste à écrire la règle : une condition, une source, une gravité et un compte. Une fois qu’elle existe sous cette forme, elle s’exécute sur le fichier du trimestre suivant, elle s’explique d’elle-même, et le nombre de lignes qu’elle a attrapées devient une ligne du journal de nettoyage.

Les seuils existent déjà

Vous n’inventez presque jamais une limite. Ce secteur est exceptionnellement bien pourvu en seuils publiés et défendables, et s’en servir est ce qui fait d’une règle une règle plutôt qu’une opinion.

Domaine Règle Source
PB, 6-59 mois En dessous de 80 mm ou au-dessus de 220 mm, ce n’est pas une mesure OMS / pratique de terrain PCIMA
Score Z poids-taille Hors de l’intervalle -5 à +5 de la référence Normes OMS 2006 de croissance
Anthropométrie SMART Signaler les scores Z à plus de 3 ET de la moyenne de l’enquête Rapport de plausibilité SMART
Quantité d’eau Moins de 15 litres par personne et par jour est sous le minimum Sphere
Collecte d’eau Un aller-retour de plus de 30 minutes est un service limité, pas élémentaire Échelles de service JMP
Chlore résiduel libre 0,2 à 2,0 mg/L au point de consommation Sphere / OMS
Couverture Des doses administrées supérieures à la population cible demandent une explication Guide d’indicateurs UNICEF

Citez la source dans la règle. Un seuil accompagné d’une référence survit à la contestation ; le même chiffre sans référence se rediscute chaque trimestre.

Des règles comme données, pas comme conditions

Écrivez les règles sous forme d’une table que le code parcourt. Deux raisons, et c’est la seconde qui compte.

La première est évidente — ajouter une règle revient à ajouter une ligne. La seconde est qu’une table de règles peut être comptée, rapportée et comparée : vous pouvez afficher combien de lignes chaque règle a attrapées, comparer au trimestre précédent, et coller l’ensemble dans le journal de nettoyage sans le réécrire en prose.

RULES = [
    {
        "id": "muac-range",
        "column": "muac_mm",
        "severity": "error",
        "source": "WHO / CMAM field practice",
        "test": lambda d: d["muac_mm"].between(80, 220) | d["muac_mm"].isna(),
        "message": "MUAC outside 80-220 mm",
    },
    {
        "id": "muac-unit-error",
        "column": "muac_mm",
        "severity": "warning",
        "source": "unit check, cm entered as mm",
        "test": lambda d: ~d["muac_mm"].between(1, 40),
        "message": "MUAC looks like centimetres",
    },
    {
        "id": "age-in-scope",
        "column": "age_months",
        "severity": "warning",
        "source": "CMAM screening protocol, 6-59 months",
        "test": lambda d: d["age_months"].between(6, 59) | d["age_months"].isna(),
        "message": "age outside the screening window",
    },
]
RULES <- tibble::tribble(
  ~id,               ~column,      ~severity,  ~source,                             ~message,
  "muac-range",      "muac_mm",    "error",    "WHO / CMAM field practice",         "MUAC outside 80-220 mm",
  "muac-unit-error", "muac_mm",    "warning",  "unit check, cm entered as mm",      "MUAC looks like centimetres",
  "age-in-scope",    "age_months", "warning",  "CMAM screening protocol, 6-59 mo",  "age outside the screening window"
)

TESTS <- list(
  `muac-range`      = function(d) dplyr::between(d$muac_mm, 80, 220) | is.na(d$muac_mm),
  `muac-unit-error` = function(d) !dplyr::between(d$muac_mm, 1, 40),
  `age-in-scope`    = function(d) dplyr::between(d$age_months, 6, 59) | is.na(d$age_months)
)

Chaque test réussit sur une valeur manquante. C’est délibéré et cela prend constamment les gens en défaut : NA < 80 n’est pas faux, c’est inconnu, et une règle qui traite l’inconnu comme une infraction rapportera votre non-réponse deux fois — une fois comme non-réponse et une fois comme valeur implausible. La non-réponse est le problème de la leçon 2. Gardez les deux séparés.

Appliquer la table

def apply_rules(df, rules):
    results = []
    flags = pd.DataFrame(index=df.index)
    for rule in rules:
        ok = rule["test"](df)
        flags[rule["id"]] = ~ok
        results.append({
            "rule": rule["id"],
            "severity": rule["severity"],
            "column": rule["column"],
            "failed": int((~ok).sum()),
            "share": round((~ok).mean(), 4),
            "source": rule["source"],
        })
    return pd.DataFrame(results), flags


report, flags = apply_rules(muac, RULES)
print(report.to_string(index=False))
apply_rules <- function(df, rules, tests) {
  flags <- purrr::map_dfc(rules$id, function(id) {
    tibble::tibble(!!id := !tests[[id]](df))
  })
  report <- rules |>
    dplyr::mutate(
      failed = purrr::map_int(id, ~ sum(flags[[.x]], na.rm = TRUE)),
      share  = round(failed / nrow(df), 4)
    )
  list(report = report, flags = flags)
}

out <- apply_rules(muac, RULES, TESTS)
out$report
Règle Gravité Échecs Part
muac-range error 7 0,17 %
muac-unit-error warning 7 0,17 %
age-in-scope warning 0 0,00 %

Sept lignes échouent aux deux règles sur le PB, ce qui est la réponse souhaitée : les mêmes sept enregistrements sont hors bornes et ressemblent à des centimètres, donc il s’agit d’une erreur d’unité et non de sept fautes sans rapport. Deux règles qui concordent constituent un indice sur la cause ; une règle isolée dirait seulement que quelque chose ne va pas.

age-in-scope n’attrape rien, et cela mérite d’être conservé aussi. Une règle qui ne se déclenche jamais coûte une ligne et prouve que la contrainte tient — et le jour où elle se déclenche, elle vous dit que le programme a changé.

Bornes dures et bornes souples

Toutes les règles ne veulent pas dire la même chose, et les confondre est la façon dont un script de nettoyage se met à supprimer de vraies données.

  • Les bornes dures sont physiquement ou logiquement impossibles. Une taille de ménage négative, un PB de 450 mm, une date de sortie antérieure à l’admission, des doses administrées à plus d’enfants qu’il n’en existe. Ce sont toujours des erreurs.
  • Les bornes souples sont implausibles mais possibles. Dix litres par personne et par jour est sous le minimum Sphere, et c’est aussi une réalité vécue par de vrais ménages — c’est le constat, pas un défaut.

L’enquête EAH montre à quel point il est facile de confondre les deux. Douze ménages déclarent plus de 60 litres par personne et par jour, contre une médiane autour de quinze. Soixante litres par personne n’est pas impossible ; c’est ce que consomme réellement un ménage doté d’un branchement de cour et d’un jardin. Mais dans un fichier où quelques enquêteurs ont noté la consommation totale du ménage dans une colonne par personne, c’est aussi exactement l’allure de cette erreur.

suspect = wash[wash["litres_per_person_day"] > 60][
    ["household_id", "community", "household_size", "litres_per_person_day", "water_source"]
]
suspect["implied_total"] = suspect["litres_per_person_day"] * suspect["household_size"]
print(suspect)
wash |>
  filter(litres_per_person_day > 60) |>
  mutate(implied_total = litres_per_person_day * household_size) |>
  select(household_id, community, household_size, litres_per_person_day,
         water_source, implied_total)

Regardez implied_total. Si un ménage de sept personnes est enregistré à 140 litres par personne, le total implicite est de 980 litres par jour transportés à la main, ce qui n’arrive pas. La règle qui tranche une valeur ambiguë est généralement une deuxième colonne, pas un seuil plus serré.

Le même piège se loge dans le temps de collecte. Quarante-deux ménages déclarent un aller-retour de moins de six minutes depuis une source qui n’est pas sur leur parcelle. Certains sont un robinet au bout de la rue. D’autres sont un enquêteur qui a écrit des heures dans un champ en minutes. Rien dans la colonne ne les distingue, et la règle honnête signale les quarante-deux pour relecture au lieu de prétendre savoir lesquels sont lesquels.

Des règles qui comparent deux colonnes

Les règles les plus utiles portent rarement sur la plage d’une seule colonne. Elles portent sur deux colonnes qui doivent concorder.

CROSS_RULES = [
    {
        "id": "referral-without-measurement",
        "severity": "warning",
        "test": lambda d: ~(d["outcome"].str.startswith("referred") & d["muac_mm"].isna()),
        "message": "referred but no MUAC recorded",
    },
    {
        "id": "outcome-contradicts-muac",
        "severity": "warning",
        "test": lambda d: ~((d["muac_mm"] >= 125) & (d["oedema"] != True)
                            & (d["outcome"] != "no-action")),
        "message": "no-action expected from the measurement",
    },
]
CROSS_TESTS <- list(
  `referral-without-measurement` = function(d)
    !(startsWith(d$outcome, "referred") & is.na(d$muac_mm)),
  `outcome-contradicts-muac` = function(d)
    !(d$muac_mm >= 125 & !d$oedema & d$outcome != "no-action")
)

Dix enregistrements portent une décision de référencement sans mesure derrière, et neuf portent une sortie que leur propre mesure contredit. Aucun des deux n’est impossible — un enfant peut être référé sur jugement clinique, et un référencement peut être justifié pour une raison que le registre ne contient pas. Mais les deux méritent un coup de téléphone, et aucun n’est repérable en regardant l’une ou l’autre colonne isolément.

Signalez, ne supprimez pas

La règle ajoute une colonne. Elle ne retire pas de ligne.

muac = muac.join(flags.add_prefix("flag_"))
muac["flag_any_error"] = flags[[r["id"] for r in RULES if r["severity"] == "error"]].any(axis=1)
muac <- dplyr::bind_cols(muac, dplyr::rename_with(out$flags, ~ paste0("flag_", .x)))
muac$flag_any_error <- dplyr::if_any(dplyr::starts_with("flag_"), identity)

Trois choses que cela vous apporte et qu’un filtre ne donne pas.

  • Le compte reste disponible. « 4 218 dépistages, 7 exclus pour mesure implausible » exige les deux nombres, et un tableau filtré en a jeté un.
  • L’exclusion est réversible. Un relecteur en désaccord avec une règle peut relancer l’analyse sans elle, en une ligne.
  • Les signalements sont analysables. Ce qui nous amène à la dernière section.

Appliquez l’exclusion une seule fois, au moment du calcul, et dites-le.

analysis = muac[~muac["flag_any_error"]]
print(f"{len(muac)} screenings, {len(analysis)} analysed, "
      f"{muac['flag_any_error'].sum()} excluded")
analysis <- muac |> filter(!flag_any_error)
sprintf("%d screenings, %d analysed, %d excluded",
        nrow(muac), nrow(analysis), sum(muac$flag_any_error))

Le taux de signalement est lui-même un indicateur

Une fois que les signalements sont des colonnes, regroupez-les comme n’importe quoi d’autre — et ce qui revient est une affirmation sur la collecte des données plutôt que sur les enfants.

Score Z poids-pour-taille moyen par équipe de mesure dans l’enquête SMART, tracé en profondeur sous zéro. Les grappes ayant été attribuées aux équipes indépendamment de l’état nutritionnel, un écart de cette ampleur entre équipes est un défaut de mesure et non une différence réelle entre populations.

print(muac.groupby("commune")[["flag_muac_unit_error", "flag_any_error"]].mean())
muac |>
  summarise(across(starts_with("flag_"), mean), .by = commune)

Si une commune, une équipe ou un enquêteur concentre l’essentiel d’un signalement, le constat n’est pas dans les données — il est dans la supervision. C’est une chose bien plus utile à mettre dans un rapport mensuel qu’un chiffre corrigé, parce qu’elle est actionnable : l’équipe peut être reformée, et le fichier du trimestre suivant sera meilleur au lieu d’être simplement nettoyé.

Une règle qui se déclenche partout est une règle sur le monde. Une règle qui se déclenche à un seul endroit est une règle sur une équipe.

La suite

Toutes les règles vues jusqu’ici comparent une valeur à un nombre. La leçon suivante traite des valeurs qu’il faut comparer à une liste — les noms de sites et de villages en texte libre qui arrivent en quatre graphies, où l’autorité est une liste administrative et où le plus dur est de refuser d’inventer un appariement.

Animer cette leçon

La leçon en diaporama, la prose étant reléguée dans les notes du présentateur plutôt que projetée. Produit à partir de cette page, dont il ne peut donc pas s'écarter.

Lancer le diaporamaLire les diapositives

Le PDF ne requiert aucun logiciel et se projette depuis n'importe quel poste. Le fichier PowerPoint est fait pour être modifié : appliquez la charte de votre organisation, retirez une section pour une séance plus courte, ou fusionnez deux leçons en atelier.