cassionAnalyse de données

Retour à la leçonLeçon 4 sur 8Faire entrer l'export

Codes, sentinelles et le 99 qui entre dans une moyenne

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 / 26

    Ce que couvre cette leçon

    • Un nombre qui signifie « pas de réponse »
    • Vocabulaires catégoriels
    • Des booléens saisis de cinq façons
    • Du texte qui devrait être une seule valeur
    • Stata et SPSS transportent leurs étiquettes
    • La fonction de lecture, assemblée
    • Ce qui vient ensuite
    Notes du présentateur
    Valeurs sentinelles, vocabulaires catégoriels, booléens saisis de cinq façons et étiquettes de valeurs Stata — transformer ce que le formulaire a enregistré en ce que pandas peut calculer.
  2. Diapositive 2 / 26

    Un nombre qui signifie « pas de réponse » — En Python

    import pandas as pd
    
    muac = pd.read_csv(RAW / "muac-screening-artibonite-2024.v1.csv")
    print(muac["muac_mm"].mean())        # plusieurs millimetres sous la verite
    Notes du présentateur
    Les formulaires papier et les systèmes bâtis à leur image n'ont pas de blanc. Ils ont un code. 99 signifie « non répondu », 88 « sans objet », -99 « non mesuré », et 999 existe parce que quelqu'un en a eu besoin d'un troisième. Aucun n'est une valeur. Tous sont des nombres du point de vue de pandas.
  3. Diapositive 3 / 26

    Un nombre qui signifie « pas de réponse »

    • Déclarer les sentinelles à la lecture
    Notes du présentateur
    Le résultat est faux et, pire, plausible. Rien n'y ressemble à une erreur, et une moyenne de PB n'est pas un chiffre que la plupart des lecteurs peuvent vérifier à l'œil. Un 99 dans une colonne d'âge est encore plus discret — c'est un âge possible.
  4. Diapositive 4 / 26

    Un nombre qui signifie « pas de réponse » — En Python

    muac = pd.read_csv(
        RAW / "muac-screening-artibonite-2024.v1.csv",
        dtype={"child_id": "string", "commune": "string"},
        na_values={"muac_mm": ["-99"]},
    )
    
    print(muac["muac_mm"].mean())
    print(muac["muac_mm"].isna().sum())
  5. Diapositive 5 / 26

    Un nombre qui signifie « pas de réponse »

    • Restreignez la sentinelle à sa colonne — na_values=["-99"] sans dictionnaire l'applique à toutes les colonnes du…
    • La liste des sentinelles est une documentation, non une tradition orale
    Notes du présentateur
    Restreignez la sentinelle à sa colonne. na_values=["-99"] sans dictionnaire l'applique à toutes les colonnes du fichier, ce qui reste juste jusqu'à ce qu'une colonne porte légitimement -99. Les sentinelles viennent du dictionnaire de codes du formulaire. Écrivez-les là où le code peut les voir :
  6. Diapositive 6 / 26

    Un nombre qui signifie « pas de réponse » — En Python

    SENTINELS = {
        "muac_mm": ["-99"],           # non mesure
        "age_months": ["99", "-1"],   # non repondu / sans objet
    }
    
    muac = pd.read_csv(path, na_values=SENTINELS)
  7. Diapositive 7 / 26

    Un nombre qui signifie « pas de réponse » — En Python

    for column in ["muac_mm", "age_months"]:
        print(column, sorted(muac[column].dropna().unique())[:5],
              sorted(muac[column].dropna().unique())[-5:])
    Notes du présentateur
    Une sentinelle que vous ignorez est invisible. Cela vaut un contrôle direct par colonne numérique avant de lui faire confiance :
  8. Diapositive 8 / 26

    Un nombre qui signifie « pas de réponse »

    Déclarer une sentinelle n'est pas décider quoi faire de la valeur manquante. Cette décision — supprimer, imputer, rapporter séparément — relève de l'analyse, et doit être consignée. Ici vous empêchez seulement un code de se faire passer pour une mesure.
    Notes du présentateur
    Des valeurs agglutinées aux extrêmes — 98, 99 en haut d'une colonne d'âge, -1 en bas — sont des sentinelles, non des observations. Un histogramme avec un pic exactement sur 99 est le même signal.
  9. Diapositive 9 / 26

    Vocabulaires catégoriels — En Python

    OUTCOMES = pd.CategoricalDtype(
        ["no-action", "referred-tsfp", "referred-otp", "referred-sc"], ordered=False
    )
    
    muac["outcome"] = muac["outcome"].astype(OUTCOMES)
    print(muac["outcome"].isna().sum())
    Notes du présentateur
    outcome dans le registre PB prend quatre valeurs et sex en prend deux. Déclarer le vocabulaire vous vaut une alerte dès qu'autre chose apparaît.
  10. Diapositive 10 / 26

    Vocabulaires catégoriels

    • Comptez les manquants immédiatement après la conversion — C'est là le piège : une valeur hors des catégories déclarées…
    Notes du présentateur
    Comptez les manquants immédiatement après la conversion. C'est là le piège : une valeur hors des catégories déclarées devient NaN au lieu de lever une erreur. Un passage de zéro à neuf signifie que neuf lignes portaient une valeur que vous ignoriez, et vous voulez le voir maintenant plutôt que le découvrir comme un trou dans un tableau trois étapes plus loin. Pour savoir lesquelles, comparez avant et après :
  11. Diapositive 11 / 26

    Vocabulaires catégoriels — En Python

    raw_values = set(muac_raw["outcome"].dropna().unique())
    declared = set(OUTCOMES.categories)
    print("inattendues :", raw_values - declared)
  12. Diapositive 12 / 26

    Vocabulaires catégoriels

    • Catégories ordonnées
    Notes du présentateur
    Certains vocabulaires ont un ordre, et le déclarer fait fonctionner la comparaison :
  13. Diapositive 13 / 26

    Vocabulaires catégoriels — En Python

    LADDER = pd.CategoricalDtype(
        ["surface-water", "unimproved", "limited", "basic", "safely-managed"],
        ordered=True,
    )
    
    wash["service"] = wash["service"].astype(LADDER)
    at_least_basic = wash["service"] >= "basic"
    Notes du présentateur
    Sans ordered=True cette comparaison lève une erreur. Avec, l'échelle du JMP se trie et se trace dans le bon ordre plutôt qu'alphabétiquement — c'est la différence entre un graphique lisible et un graphique qui place « basic » entre « unimproved » et « limited ».
  14. Diapositive 14 / 26

    Des booléens saisis de cinq façons — En Python

    print(muac["oedema"].value_counts(dropna=False))
    Notes du présentateur
    La colonne oedema en est l'exemple type. Deux communes ont saisi Y et N au premier trimestre ; les autres true et false.
  15. Diapositive 15 / 26

    Des booléens saisis de cinq façons — En Python

    # Faux d'une maniere difficile a voir : toute chaine non vide est vraie,
    # donc "false" devient True.
    muac["oedema"] = muac["oedema"].astype(bool)
    Notes du présentateur
    Ne convertissez jamais une telle colonne directement :
  16. Diapositive 16 / 26

    Des booléens saisis de cinq façons — En Python

    BOOLEANS = {
        "true": True, "TRUE": True, "Y": True, "y": True, "yes": True, "1": True,
        "false": False, "FALSE": False, "N": False, "n": False, "no": False, "0": False,
    }
    
    muac["oedema"] = (
        muac["oedema"].astype("string").str.strip().str.lower()
        .map({k.lower(): v for k, v in BOOLEANS.items()})
    )
    
    print(muac["oedema"].isna().sum())    # ce que la correspondance n'a pas couvert
    Notes du présentateur
    Faites une correspondance explicite, avec une liste d'autorisation :
  17. Diapositive 17 / 26

    Des booléens saisis de cinq façons — En Python

    muac["oedema"] = muac["oedema"].astype("boolean")     # True / False / <NA>
    Notes du présentateur
    Une liste d'autorisation plutôt qu'une heuristique, pour la raison qui justifie une séance entière sur cette leçon : tout ce qui en sort reste manquant et est compté, au lieu d'être deviné. Une valeur que la table ne couvre pas est une question à poser à qui l'a saisie. Utilisez le booléen nullable de pandas quand un véritable « non renseigné » existe : bool ne peut pas porter de manquant ; boolean le peut. Dans ce registre, 44 enregistrements n'ont aucune évaluation d'œdèmes, et les ramener à False affirmerait silencieusement que 44 enfants ont été examinés et déclarés indemnes.
  18. Diapositive 18 / 26

    Du texte qui devrait être une seule valeur — En Python

    print(wash["district"].value_counts())
    # Nord-Ouest    727
    # NORD-OUEST     33
    # Nord Ouest     22
    # nord-ouest     20
    Notes du présentateur
    Les colonnes proches du texte libre arrivent avec des variations de casse, d'espacement et d'orthographe. L'enquête EAH porte un district écrit de quatre façons.
  19. Diapositive 19 / 26

    Du texte qui devrait être une seule valeur — En Python

    wash["district"] = (
        wash["district"].astype("string").str.strip().str.lower()
        .str.replace(r"\s+", "-", regex=True)
    )
    print(wash["district"].nunique())     # 3
    Notes du présentateur
    Sans regroupement, cela éclate le district en quatre fragments dont aucun ne paraît alarmant.
  20. Diapositive 20 / 26

    Du texte qui devrait être une seule valeur — En Python

    CANONICAL = {
        "gonaives": "Gonaïves",
        "st-marc": "Saint-Marc",
        "saint-marc": "Saint-Marc",
    }
    wash["district"] = wash["district"].map(CANONICAL).fillna(wash["district"])
    Notes du présentateur
    Normalisez avant le premier groupby, non après avoir remarqué que les totaux ne tombent pas juste. Et normalisez vers une forme canonique que vous choisissez, plutôt que de retenir l'orthographe la plus fréquente — la plus fréquente peut changer au prochain export. Là où la variation n'est pas mécanique — Gonaives contre Gonaïves, St-Marc contre Saint-Marc — une table de correspondance est la réponse honnête : Gardez cette table dans le code, non dans votre tête. C'est une décision documentée que quelqu'un devra vérifier.
  21. Diapositive 21 / 26

    Stata et SPSS transportent leurs étiquettes — En Python

    survey = pd.read_stata(path)                        # valeurs converties en etiquettes
    survey = pd.read_stata(path, convert_categoricals=False)   # codes bruts
    Notes du présentateur
    Un .dta issu d'un institut de sondage porte à la fois le code et son sens : 1 = Oui, 2 = Non, 9 = Ne sait pas.
  22. Diapositive 22 / 26

    Stata et SPSS transportent leurs étiquettes — En Python

    with pd.io.stata.StataReader(path) as reader:
        labels = reader.value_labels()
        variables = reader.variable_labels()
    
    print(variables["hh_size"])
    print(labels.get("consent", {}))
    Notes du présentateur
    pandas vous donne l'un ou l'autre. Pour garder les deux — ce que vous voulez, car le code est ce que documente le dictionnaire et l'étiquette est ce dont un lecteur a besoin — lisez les métadonnées séparément : Le paquet haven de R préserve les étiquettes plus complètement que pandas. Si on vous remet un .dta d'un institut de sondage et que les étiquettes comptent, le lire sous R et écrire un CSV accompagné d'un dictionnaire est une étape légitime — et c'est le seul endroit de ce cours où la réponse est « utilisez l'autre langage ».
  23. Diapositive 23 / 26

    La fonction de lecture, assemblée — En Python (suite)

    from pathlib import Path
    import pandas as pd
    
    SENTINELS = {"muac_mm": ["-99"]}
    OUTCOMES = pd.CategoricalDtype(
        ["no-action", "referred-tsfp", "referred-otp", "referred-sc"]
    )
    BOOLEANS = {"true": True, "y": True, "yes": True, "1": True,
                "false": False, "n": False, "no": False, "0": False}
    
    def read_register(path: Path) -> pd.DataFrame:
        muac = pd.read_csv(
            path,
            dtype={"child_id": "string", "commune": "string", "sex": "string"},
            na_values=SENTINELS,
        )
    Notes du présentateur
    Tout ce qui précède appartient à une fonction unique que le notebook et les scripts partagent :
  24. Diapositive 24 / 26

    La fonction de lecture, assemblée — En Python (suite)

        muac["screening_date"] = pd.to_datetime(
            muac["screening_date"], format="%Y-%m-%d", errors="raise"
        )
        muac["outcome"] = muac["outcome"].astype(OUTCOMES)
        muac["oedema"] = (
            muac["oedema"].astype("string").str.strip().str.lower()
            .map(BOOLEANS).astype("boolean")
        )
    
        assert muac["child_id"].notna().all(), "chaque ligne exige un identifiant"
        return muac
    Notes du présentateur
    Une fonction, un seul endroit à corriger quand le prochain export introduira une cinquième façon d'écrire « non ».
  25. Diapositive 25 / 26

    Ce qui vient ensuite

    • Le tableau est en mémoire et chaque colonne dit ce qu'elle signifie.
    Notes du présentateur
    Le tableau est en mémoire et chaque colonne dit ce qu'elle signifie. L'unité suivante le travaille : sélectionner et filtrer sans l'avertissement que personne ne lit, puis regrouper jusqu'au numérateur et au dénominateur qu'exige réellement un indicateur.
  26. Diapositive 26 / 26

    La suite

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