Retour à la leçon·Leçon 4 sur 8·Faire 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.
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.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 veriteNotes du présentateur
Les formulaires papier et les systèmes bâtis à leur image n'ont pas de blanc. Ils ont un code.99signifie « non répondu »,88« sans objet »,-99« non mesuré », et999existe 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.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. Un99dans une colonne d'âge est encore plus discret — c'est un âge possible.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())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 :- Restreignez la sentinelle à sa colonne —
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)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 :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, 99en haut d'une colonne d'âge,-1en bas — sont des sentinelles, non des observations. Un histogramme avec un pic exactement sur 99 est le même signal.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
outcomedans le registre PB prend quatre valeurs etsexen prend deux. Déclarer le vocabulaire vous vaut une alerte dès qu'autre chose apparaît.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 devientNaNau 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 :Vocabulaires catégoriels — En Python
raw_values = set(muac_raw["outcome"].dropna().unique()) declared = set(OUTCOMES.categories) print("inattendues :", raw_values - declared)Vocabulaires catégoriels
- Catégories ordonnées
Notes du présentateur
Certains vocabulaires ont un ordre, et le déclarer fait fonctionner la comparaison :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
Sansordered=Truecette 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 ».Des booléens saisis de cinq façons — En Python
print(muac["oedema"].value_counts(dropna=False))Notes du présentateur
La colonneoedemaen est l'exemple type. Deux communes ont saisiYetNau premier trimestre ; les autrestrueetfalse.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 :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 couvertNotes du présentateur
Faites une correspondance explicite, avec une liste d'autorisation :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 :boolne peut pas porter de manquant ;booleanle peut. Dans ce registre, 44 enregistrements n'ont aucune évaluation d'œdèmes, et les ramener àFalseaffirmerait silencieusement que 44 enfants ont été examinés et déclarés indemnes.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 20Notes 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.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()) # 3Notes du présentateur
Sans regroupement, cela éclate le district en quatre fragments dont aucun ne paraît alarmant.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 premiergroupby, 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 —GonaivescontreGonaïves,St-MarccontreSaint-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.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 brutsNotes du présentateur
Un.dtaissu d'un institut de sondage porte à la fois le code et son sens :1 = Oui,2 = Non,9 = Ne sait pas.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 paquethavende R préserve les étiquettes plus complètement que pandas. Si on vous remet un.dtad'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 ».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 :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 muacNotes du présentateur
Une fonction, un seul endroit à corriger quand le prochain export introduira une cinquième façon d'écrire « non ».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.