cassionAnalyse de données

Retour à la leçonLeçon 6 sur 8Rendre les données fiables

Décisions de nettoyage et journal de nettoyage

Le même diaporama que les téléchargements, rendu sous forme de page. Lancez le diaporama pour le présenter en plein écran — les flèches ou un clic avancent d'une diapositive, Échap quitte.

Diapositives · PDFDiapositives · PowerPoint

  1. Diapositive 1 / 32

    Ce que couvre cette leçon

    • Le nettoyage est un ensemble de décisions, pas un script
    • Les quatre options
    • Traitement de ce registre
    • Chiffrez ce que la décision coûte
    • Le journal de nettoyage
    • La seule chose à ne jamais faire
    • La suite
    Notes du présentateur
    Trancher le traitement de chaque défaut, chiffrer ce que la décision coûte au résultat final, et la consigner pour qu'un auditeur — ou vous-même dans six mois — puisse suivre le raisonnement.
  2. Diapositive 2 / 32

    Le nettoyage est un ensemble de décisions, pas un script

    • Le profilage de la leçon précédente a produit une liste de défauts.
    Notes du présentateur
    Le profilage de la leçon précédente a produit une liste de défauts. Aucun n'a de traitement objectivement correct. Chacun a un traitement défendable et une justification, et c'est la justification qui compte : un relecteur ne vous reprochera pas d'avoir supprimé sept lignes, il vous reprochera de ne pas pouvoir savoir pourquoi. Donc, pour chaque défaut, trois éléments. Ce que vous avez fait, pourquoi, et combien de lignes cela a touché.
  3. Diapositive 3 / 32

    Les quatre options

    OptionQuand elle s'appliqueCe qu'elle coûte
    CorrigerVous savez quelle valeur était viséeRien, si vous avez raison
    Passer en manquantLa valeur est fausse et irrécupérableDe la précision, et peut-être du biais
    Supprimer la ligneLa ligne n'est pas une observation réelleDe la charge de cas, si vous vous trompez
    Conserver et signalerLa valeur peut être juste, sans moyen de trancherRien, mais le lecteur doit la traiter
    Notes du présentateur
    Chaque défaut reçoit l'une d'entre elles. « Conserver et signaler » est sous-employée. C'est l'option honnête lorsqu'une valeur est suspecte sans être impossible, et elle renvoie le jugement à la personne qui lit l'analyse plutôt que de le dissimuler à l'intérieur.
  4. Diapositive 4 / 32

    Traitement de ce registre

    • Erreurs d'unité — sept enregistrements en centimètres — Récupérables
    Notes du présentateur
    Erreurs d'unité — sept enregistrements en centimètres. Récupérables. Un PB de 13,3 a été mesuré à 133 mm ; toute l'affaire tient à la virgule. Corrigez-les, et consignez-le.
  5. Diapositive 5 / 32

    Traitement de ce registre — En Python

    import numpy as np
    
    erreur_unite = muac["muac_mm"].notna() & (muac["muac_mm"] < 40)
    n_erreur_unite = int(erreur_unite.sum())
    
    muac.loc[erreur_unite, "muac_mm"] = muac.loc[erreur_unite, "muac_mm"] * 10
  6. Diapositive 6 / 32

    Traitement de ce registre — En R

    erreur_unite <- !is.na(muac$muac_mm) & muac$muac_mm < 40
    n_erreur_unite <- sum(erreur_unite)
    
    muac <- muac |>
      mutate(muac_mm = if_else(!is.na(muac_mm) & muac_mm < 40, muac_mm * 10L, muac_mm))
  7. Diapositive 7 / 32

    Traitement de ce registre

    • Valeurs implausibles qui ne sont pas des erreurs d'unité — Irrécupérables
    Notes du présentateur
    Multiplier par dix n'est sûr ici que parce que les plages plausibles ne se chevauchent pas : rien de réellement mesuré en millimètres n'est en dessous de 40, et rien de réellement mesuré en centimètres n'est au-dessus de 22. Vérifiez que les deux plages sont disjointes avant d'appliquer une règle de ce genre. Si elles se chevauchent, vous ne pouvez pas distinguer les cas et les valeurs ne sont pas récupérables. Valeurs implausibles qui ne sont pas des erreurs d'unité. Irrécupérables. Passez-les en manquant, mais ne supprimez pas la ligne : l'enfant a bien été dépisté, et le reste de l'enregistrement reste une donnée probante.
  8. Diapositive 8 / 32

    Traitement de ce registre — En Python

    implausible = muac["muac_mm"].notna() & (
        (muac["muac_mm"] < 80) | (muac["muac_mm"] > 220)
    )
    n_implausible = int(implausible.sum())
    
    muac.loc[implausible, "muac_mm"] = np.nan
  9. Diapositive 9 / 32

    Traitement de ce registre — En R

    implausible <- !is.na(muac$muac_mm) & (muac$muac_mm < 80 | muac$muac_mm > 220)
    n_implausible <- sum(implausible)
    
    muac <- muac |>
      mutate(muac_mm = if_else(implausible, NA_integer_, muac_mm))
  10. Diapositive 10 / 32

    Traitement de ce registre

    • Doublons exacts — douze lignes — À supprimer
    Notes du présentateur
    Doublons exacts — douze lignes. À supprimer. Un formulaire soumis deux fois sur une mauvaise connexion correspond à un dépistage, pas à deux.
  11. Diapositive 11 / 32

    Traitement de ce registre — En Python

    avant = len(muac)
    muac = muac.drop_duplicates()
    n_doublons_exacts = avant - len(muac)
  12. Diapositive 12 / 32

    Traitement de ce registre — En R

    avant <- nrow(muac)
    muac <- distinct(muac)
    n_doublons_exacts <- avant - nrow(muac)
  13. Diapositive 13 / 32

    Traitement de ce registre

    • Réenregistrements sous un nouvel identifiant — six suspectés — À conserver et signaler
    Notes du présentateur
    Réenregistrements sous un nouvel identifiant — six suspectés. À conserver et signaler. Vous ne pouvez pas les distinguer d'authentiques coïncidences sans retourner au registre : marquez-les et dites-le dans le rapport.
  14. Diapositive 14 / 32

    Traitement de ce registre — En Python

    cle = ["commune", "screening_date", "age_months", "sex", "muac_mm"]
    muac["doublon_suspecte"] = muac.duplicated(subset=cle, keep=False) & muac[
        "muac_mm"
    ].notna()
    
    n_suspectes = int(muac["doublon_suspecte"].sum())
  15. Diapositive 15 / 32

    Traitement de ce registre — En R

    muac <- muac |>
      group_by(commune, screening_date, age_months, sex, muac_mm) |>
      mutate(doublon_suspecte = n() > 1 & !is.na(muac_mm)) |>
      ungroup()
    
    n_suspectes <- sum(muac$doublon_suspecte)
  16. Diapositive 16 / 32

    Traitement de ce registre

    • Codage incohérent des œdèmes — Récupérable
    Notes du présentateur
    Codage incohérent des œdèmes. Récupérable. Y et N signifient bien ce qu'ils paraissent signifier ; les cases vides, non, et elles deviennent manquantes.
  17. Diapositive 17 / 32

    Traitement de ce registre — En Python

    table_oedeme = {
        "true": True, "TRUE": True, "Y": True, "y": True, "yes": True,
        "false": False, "FALSE": False, "N": False, "n": False, "no": False,
    }
    muac["oedema"] = muac["oedema"].astype("string").str.strip().map(table_oedeme)
    n_oedeme_manquant = int(muac["oedema"].isna().sum())
  18. Diapositive 18 / 32

    Traitement de ce registre — En R

    muac <- muac |>
      mutate(
        oedema = case_when(
          tolower(trimws(oedema)) %in% c("true", "y", "yes") ~ TRUE,
          tolower(trimws(oedema)) %in% c("false", "n", "no") ~ FALSE,
          TRUE ~ NA
        )
      )
    
    n_oedeme_manquant <- sum(is.na(muac$oedema))
  19. Diapositive 19 / 32

    Traitement de ce registre

    • Âge manquant concentré sur Gros-Morne — Voilà le cas qui exige une décision plutôt qu'une règle, et il fait l'objet de…
    Notes du présentateur
    Âge manquant concentré sur Gros-Morne. Voilà le cas qui exige une décision plutôt qu'une règle, et il fait l'objet de la section suivante.
  20. Diapositive 20 / 32

    Chiffrez ce que la décision coûte — En Python (suite)

    seuil_mag = 125
    
    complet = muac.dropna(subset=["age_months", "muac_mm"])
    tous_mesures = muac.dropna(subset=["muac_mm"])
    
    def taux_mag(df):
        return (df["muac_mm"] < seuil_mag).mean()
    
    comparaison = pd.DataFrame(
        {
            "en_supprimant": complet.groupby("commune").apply(taux_mag),
            "en_conservant": tous_mesures.groupby("commune").apply(taux_mag),
        }
    )
    comparaison["ecart"] = (
        comparaison["en_supprimant"] - comparaison["en_conservant"]
    Notes du présentateur
    La raison de prendre l'âge manquant au sérieux, c'est que le traitement évident — supprimer les lignes sans âge — change la réponse. Mesurez-le au lieu de le supposer.
  21. Diapositive 21 / 32

    Chiffrez ce que la décision coûte — En Python (suite)

    )
    print(comparaison.sort_values("ecart", ascending=False).round(4))
  22. Diapositive 22 / 32

    Chiffrez ce que la décision coûte — En R

    taux_mag <- function(df) mean(df$muac_mm < 125, na.rm = TRUE)
    
    complet <- muac |> filter(!is.na(age_months), !is.na(muac_mm))
    tous_mesures <- muac |> filter(!is.na(muac_mm))
    
    comparaison <- full_join(
      complet |> group_by(commune) |> summarise(en_supprimant = taux_mag(pick(everything()))),
      tous_mesures |> group_by(commune) |> summarise(en_conservant = taux_mag(pick(everything()))),
      by = "commune"
    ) |>
      mutate(ecart = en_supprimant - en_conservant) |>
      arrange(desc(abs(ecart)))
    
    comparaison
  23. Diapositive 23 / 32

    Chiffrez ce que la décision coûte

    Principe général : supprimez des lignes pour une analyse donnée, jamais du jeu de données. Une ligne sans âge reste une donnée probante sur le PB. Filtrer au point d'utilisation maintient chaque analyse sur le plus grand échantillon qu'elle admette.
    Notes du présentateur
    Gros-Morne bouge, et les autres communes à peine. Voilà le biais, rendu visible. Comme l'indicateur ne dépend pas ici de l'âge — les seuils de PB pour les 6 à 59 mois forment une bande unique, à la différence des scores z poids-taille — la bonne décision consiste à conserver les lignes, à les utiliser pour l'indicateur PB, et à ne les exclure que des analyses qui exigent réellement l'âge. C'est une meilleure décision que la suppression, et elle n'est disponible que parce que vous avez regardé.
  24. Diapositive 24 / 32

    Le journal de nettoyage — En Python (suite)

    journal_nettoyage = pd.DataFrame(
        [
            {
                "regle": "Erreur d'unité PB corrigée (cm vers mm)",
                "colonne": "muac_mm",
                "action": "corrige",
                "lignes": n_erreur_unite,
                "justification": "Les valeurs sous 40 sont des centimètres non convertis. "
                "Les plages plausibles en mm et en cm sont disjointes, la correction est donc sans ambiguïté.",
            },
            {
                "regle": "PB implausible passé en manquant",
                "colonne": "muac_mm",
                "action": "passe-manquant",
                "lignes": n_implausible,
                "justification": "Hors de 80-220 mm, ce n'est pas une mesure pour 6-59 mois. "
    Notes du présentateur
    Tout ce qui précède devient des lignes d'un tableau livré avec l'analyse.
  25. Diapositive 25 / 32

    Le journal de nettoyage — En Python (suite)

                "Ligne conservée : le dépistage a bien eu lieu.",
            },
            {
                "regle": "Doubles soumissions exactes supprimées",
                "colonne": "toutes",
                "action": "supprime",
                "lignes": n_doublons_exacts,
                "justification": "Des lignes identiques correspondent à un formulaire soumis deux fois sur une mauvaise connexion.",
            },
            {
                "regle": "Réenregistrement suspecté signalé",
                "colonne": "doublon_suspecte",
                "action": "signale",
                "lignes": n_suspectes,
                "justification": "Mêmes commune, date, âge, sexe et PB sous des identifiants différents. "
                "Indiscernable d'une coïncidence sans le registre papier.",
  26. Diapositive 26 / 32

    Le journal de nettoyage — En Python (suite)

            },
            {
                "regle": "Codage des œdèmes harmonisé",
                "colonne": "oedema",
                "action": "corrige",
                "lignes": n_oedeme_manquant,
                "justification": "Ennery et Desdunes ont utilisé Y/N au T1. Les cases vides restent manquantes.",
            },
            {
                "regle": "Âge manquant conservé",
                "colonne": "age_months",
                "action": "conserve",
                "lignes": int(muac["age_months"].isna().sum()),
                "justification": "60 % de manquants sur la semaine du 10 au 14 juin à Gros-Morne. "
                "Supprimer modifie le classement des communes ; les seuils de PB n'exigent pas l'âge.",
            },
  27. Diapositive 27 / 32

    Le journal de nettoyage — En Python (suite)

        ]
    )
    
    journal_nettoyage.to_csv("output/tables/journal-nettoyage.csv", index=False)
    print(journal_nettoyage[["regle", "action", "lignes"]])
  28. Diapositive 28 / 32

    Le journal de nettoyage — En R

    journal_nettoyage <- tibble::tribble(
      ~regle,                                      ~colonne,             ~action,          ~lignes,
      "Erreur d'unité PB corrigée (cm vers mm)",   "muac_mm",            "corrige",        n_erreur_unite,
      "PB implausible passé en manquant",          "muac_mm",            "passe-manquant", n_implausible,
      "Doubles soumissions exactes supprimées",    "toutes",             "supprime",       n_doublons_exacts,
      "Réenregistrement suspecté signalé",         "doublon_suspecte",   "signale",        n_suspectes,
      "Codage des œdèmes harmonisé",               "oedema",             "corrige",        n_oedeme_manquant,
      "Âge manquant conservé",                     "age_months",         "conserve",       sum(is.na(muac$age_months))
    )
    
    readr::write_csv(journal_nettoyage, "output/tables/journal-nettoyage.csv")
    journal_nettoyage
  29. Diapositive 29 / 32

    Le journal de nettoyage

    • Les effectifs viennent du code, pas de la mémoire. Si la règle change, l'effectif change avec elle.
    • Il est livré avec l'analyse — en annexe du rapport, et non comme un fichier sur le portable de quelqu'un.
    • La justification est une phrase, pas une catégorie. « Qualité des données » n'est pas une justification.
    • Il s'écrit pendant le nettoyage, et non reconstitué après coup. Les journaux reconstitués sont faux précisément là…
    Notes du présentateur
    Quatre propriétés rendent ce journal utile plutôt que cérémoniel :
  30. Diapositive 30 / 32

    La seule chose à ne jamais faire

    • Ne modifiez pas data/raw/.
    Notes du présentateur
    Ne modifiez pas data/raw/. Pas une cellule, pas pour corriger une coquille manifeste, pas « juste cette fois » la veille d'une échéance. Chacune des corrections ci-dessus est du code. Elle est donc visible, réversible, appliquée à l'identique à l'export du trimestre suivant, et réexécutable par quelqu'un qui doute de vous. Une cellule modifiée à la main dans un tableur n'est rien de tout cela, et elle est invisible six mois plus tard lorsque le chiffre est contesté.
  31. Diapositive 31 / 32

    La suite

    • Les données sont propres et les décisions sont consignées.
    Notes du présentateur
    Les données sont propres et les décisions sont consignées. La leçon suivante les transforme en indicateur — ce qui suppose d'affronter le fait qu'un taux est un numérateur rapporté à un dénominateur, et que presque personne ne s'accorde sur le dénominateur.
  32. Diapositive 32 / 32

    La suite

    Lire la leçon complète, avec le code exécutable Retour à la leçon