Retour à la leçon·Leçon 1 sur 8·Avant de modifier quoi que ce soit
Le profil que l'on exécute avant d'y toucher
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
- Le fichier est une pièce à conviction tant que vous ne l'éditez pas
- Lisez-le d'abord comme du texte
- Ce à quoi un profil doit répondre
- La forme, face à une attente que vous écrivez
- L'inventaire des colonnes
- Les sentinelles ne sont pas encore des valeurs manquantes
- La clé en est-elle une ?
- Les bornes, avant de croire un résumé
- Enregistrez le profil à côté des données
- Comparer cet export au précédent
- La suite
Notes du présentateur
Un export neuf est une pièce à conviction tant que vous n'y touchez pas. Le lire d'abord comme du texte brut, inventorier chaque colonne, prouver ou réfuter la clé, et enregistrer le résultat comme trace de ce qui est arrivé.Le fichier est une pièce à conviction tant que vous ne l'éditez pas
- Dans six mois, quelqu'un vous demandera si le chiffre d'août a toujours été ainsi.
Notes du présentateur
Dans six mois, quelqu'un vous demandera si le chiffre d'août a toujours été ainsi. Cette conversation a deux issues possibles. Soit vous ouvrez un fichier enregistré le jour de l'arrivée de l'export et vous y lisez la réponse, soit vous la reconstituez de mémoire, à partir d'un script modifié onze fois depuis. La première chose à faire d'un export neuf n'est donc pas de le nettoyer. C'est de le profiler et d'écrire ce profil, avant qu'une seule valeur ne change. Le profil coûte vingt lignes, et c'est le seul artefact capable de prouver un jour l'état du fichier à son arrivée. Cette leçon construit ce profil. Le cours Fondamentaux enseignait les cinq contrôles qui repèrent les défauts ; celui-ci en fait quelque chose que l'on conserve.Lisez-le d'abord comme du texte
- Lisez une première fois avec tout en chaîne de caractères — Cette lecture sert à regarder, pas à calculer
Notes du présentateur
Tout lecteur de fichier devine.read_csvregarde les premiers milliers de lignes et décide que chaque colonne est un nombre, une date ou une chaîne, et chacune de ces suppositions peut détruire une information irrécupérable — un zéro initial sur un code de formation sanitaire, une sentinelle-99moyennée, une date lue à l'américaine. Lisez une première fois avec tout en chaîne de caractères. Cette lecture sert à regarder, pas à calculer.Lisez-le d'abord comme du texte — En Python
import pandas as pd PATH = "muac-screening-artibonite-2024.v1.csv" raw = pd.read_csv(PATH, dtype="string", keep_default_na=False) print(raw.shape) print(raw.dtypes)Lisez-le d'abord comme du texte — En R
library(readr) library(dplyr) PATH <- "muac-screening-artibonite-2024.v1.csv" raw <- read_csv(PATH, col_types = cols(.default = col_character())) dim(raw)Notes du présentateur
Deux arguments travaillent réellement dans l'appel Python.dtype="string"désactive l'inférence de type ;keep_default_na=Falseempêche pandas de transformer les chaînes littéralesNA,N/A,nulletnanen valeurs manquantes avant que vous ne les ayez vues. Dans ce secteur, cela compte plus qu'il n'y paraît — une colonne où un enquêteur a tapéNApour « sans objet » n'est pas la même colonne que celle où le champ a été sauté, et pandas confond les deux par défaut. La lecture R impose délibérémentcol_character()partout, ce dontproblems()a besoin pour être utile — en lisant strictement plus tard, toute valeur non convertible est signalée au lieu d'être silencieusement transformée enNA.Ce à quoi un profil doit répondre
- Combien est arrivé ? Lignes et colonnes, face à ce que vous attendiez.
- Qu'est réellement chaque colonne ? Ses valeurs brutes, pas le type deviné.
- Où sont les trous ? Par colonne et par site — jamais un chiffre global.
- La clé en est-elle une ? Non pas « la colonne existe-t-elle » mais « est-elle unique ».
- Qu'est-ce qui sort des bornes ? Face aux limites du secteur, pas à celles des données.
- Quels codes circulent ? Chaque valeur distincte de chaque colonne catégorielle.
Notes du présentateur
Six questions, et un profil qui ne répond pas aux six n'en est pas un.La forme, face à une attente que vous écrivez — En Python
EXPECTED_COLUMNS = [ "child_id", "commune", "screening_date", "age_months", "sex", "muac_mm", "oedema", "outcome", ] print(f"{len(raw)} rows, {raw.shape[1]} columns") print("missing columns:", set(EXPECTED_COLUMNS) - set(raw.columns)) print("unexpected columns:", set(raw.columns) - set(EXPECTED_COLUMNS))La forme, face à une attente que vous écrivez — En R
EXPECTED_COLUMNS <- c( "child_id", "commune", "screening_date", "age_months", "sex", "muac_mm", "oedema", "outcome" ) cat(nrow(raw), "rows,", ncol(raw), "columns\n") setdiff(EXPECTED_COLUMNS, names(raw)) setdiff(names(raw), EXPECTED_COLUMNS)Notes du présentateur
Ce registre compte 4 218 lignes et huit colonnes. L'intérêt n'est pas le décompte, c'est que vous ayez écrit ce que vous attendiez. Une colonne qui disparaît discrètement d'un export à l'autre est la rupture la plus fréquente que livrent les plateformes de collecte, et elle reste invisible tant que rien ne compare à une liste.L'inventaire des colonnes — En Python
def inventory(df): rows = [] for column in df.columns: values = df[column] counts = values.value_counts(dropna=False) rows.append({ "column": column, "blank": int((values == "").sum()), "distinct": int(values.nunique(dropna=False)), "top": counts.index[0] if len(counts) else None, "top_n": int(counts.iloc[0]) if len(counts) else 0, }) return pd.DataFrame(rows) print(inventory(raw).to_string(index=False))Notes du présentateur
Pour chaque colonne — combien de lignes vides, combien de valeurs distinctes, et les quelques plus fréquentes. Ce seul tableau remplace l'essentiel de ce que l'on fait en faisant défiler le fichier.L'inventaire des colonnes — En R
inventory <- function(df) { purrr::map_dfr(names(df), function(column) { values <- df[[column]] counts <- sort(table(values, useNA = "ifany"), decreasing = TRUE) tibble::tibble( column = column, blank = sum(values == "" | is.na(values)), distinct = dplyr::n_distinct(values), top = names(counts)[1], top_n = as.integer(counts[1]) ) }) } print(inventory(raw), n = Inf)L'inventaire des colonnes
Colonne Vides Distinctes Remarque age_months 226 55 5,4 % de vides — la leçon 2 demande où oedema 44 4 quatre valeurs distinctes dans une colonne booléenne Notes du présentateur
Exécutez-le sur ce registre et deux lignes méritent qu'on s'y arrête.L'inventaire des colonnes
- Quatre valeurs distinctes dans une colonne booléenne — voilà le constat
Notes du présentateur
Quatre valeurs distinctes dans une colonne booléenne, voilà le constat. Le registre contienttrue,false, une chaîne vide etN— vingt-cinq lignes où un enquêteur a utilisé les conventionsY/Ndans une colonne que le formulaire attendait entrue/false. Convertissez cette colonne en booléen et les lignesNdeviennent manquantes, silencieusement, et vous avez perdu vingt-cinq négatifs enregistrés en les traitant comme non renseignés. C'est tout l'argument en faveur d'une première lecture en texte. Après la conversion, il n'y a plus rien à trouver.Les sentinelles ne sont pas encore des valeurs manquantes — En Python
print(raw["muac_mm"].value_counts().head()) print("rows coded -99:", int((raw["muac_mm"] == "-99").sum()))Notes du présentateur
muac_mma l'air complète — aucune case vide. Elle ne l'est pas.Les sentinelles ne sont pas encore des valeurs manquantes — En R
raw |> count(muac_mm, sort = TRUE) |> head() sum(raw$muac_mm == "-99")Notes du présentateur
Soixante-douze lignes portent-99, le code du registre pour « non mesuré ». Lisez la colonne comme un nombre sans déclarer ce code et la moyenne perd environ deux millimètres, la charge de cas est sous-estimée, parce que vous avez moyenné soixante-douze enfants à moins quatre-vingt-dix-neuf. Les codes sentinelles sont documentés dans le dictionnaire des données et nulle part ailleurs. Consultez le dictionnaire avant la première lecture numérique, à chaque fois —-99,-1,999,9999et88sont tous en usage dans ce secteur, et aucun n'a l'air faux dans un résumé.La clé en est-elle une ? — En Python
duplicated_ids = raw["child_id"].duplicated(keep=False) print("rows sharing a child_id:", int(duplicated_ids.sum())) print("distinct ids involved:", raw.loc[duplicated_ids, "child_id"].nunique())Notes du présentateur
La colonne nomméechild_idest un identifiant. Savoir si elle est unique est une autre question, et la réponse est ici non.La clé en est-elle une ? — En R
raw |> group_by(child_id) |> filter(n() > 1) |> ungroup() |> summarise(rows = n(), ids = n_distinct(child_id))Notes du présentateur
Vingt-quatre lignes réparties sur douze identifiants. Toute jointure que vous écrirez désormais fait une hypothèse sur cette colonne, et cette hypothèse est actuellement fausse. L'unité 2 traite de cela ; le rôle du profil se borne à le faire apparaître dès le premier jour plutôt qu'au milieu d'une jointure qui double discrètement une charge de cas.Les bornes, avant de croire un résumé — En Python
muac = pd.to_numeric(raw["muac_mm"], errors="coerce") muac = muac.where(muac != -99) print(muac.describe()) print(muac.nsmallest(10).tolist()) print(muac.nlargest(10).tolist())Notes du présentateur
Un résumé en cinq nombres masque précisément les valeurs que vous cherchez, car une ligne aberrante déplace à peine un quartile. Regardez les extrêmes directement.Les bornes, avant de croire un résumé — En R
muac <- suppressWarnings(as.numeric(raw$muac_mm)) muac[muac == -99] <- NA summary(muac) head(sort(muac), 10) head(sort(muac, decreasing = TRUE), 10)Les bornes, avant de croire un résumé
- Les sept premières sont des centimètres jamais convertis — un PB de 13,4 cm
- Regardez toujours les dix plus petites et les dix plus grandes valeurs de toute colonne de mesure — Cela coûte une…
Notes du présentateur
Les dix plus petites valeurs donnent 13, 14, 14, 15, 16, 17, 18 puis sautent à saisi 13. La moyenne ne les remarque pas. La queue de distribution les nomme immédiatement. Regardez toujours les dix plus petites et les dix plus grandes valeurs de toute colonne de mesure. Cela coûte une ligne, et c'est le contrôle le plus rentable de cette leçon.Enregistrez le profil à côté des données — En Python (suite)
from pathlib import Path import json profile = { "file": PATH, "rows": len(raw), "columns": list(raw.columns), "blank_by_column": {c: int((raw[c] == "").sum()) for c in raw.columns}, "distinct_by_column": {c: int(raw[c].nunique()) for c in raw.columns}, "duplicate_key_rows": int(raw["child_id"].duplicated(keep=False).sum()), "categorical_values": { c: sorted(raw[c].unique().tolist()) for c in ["commune", "sex", "oedema", "outcome"] }, }Notes du présentateur
Le profil ne vaut d'être écrit que s'il survit à la session.Enregistrez le profil à côté des données — En Python (suite)
Path("outputs/profiles").mkdir(parents=True, exist_ok=True) Path("outputs/profiles/muac-2024-q4.json").write_text(json.dumps(profile, indent=2))Enregistrez le profil à côté des données — En R (suite)
profile <- list( file = PATH, rows = nrow(raw), columns = names(raw), blank_by_column = sapply(raw, function(x) sum(x == "" | is.na(x))), distinct_by_column = sapply(raw, dplyr::n_distinct), duplicate_key_rows = sum(duplicated(raw$child_id) | duplicated(raw$child_id, fromLast = TRUE)), categorical_values = lapply( raw[c("commune", "sex", "oedema", "outcome")], function(x) sort(unique(x)) ) ) dir.create(here::here("outputs", "profiles"), recursive = TRUE, showWarnings = FALSE) jsonlite::write_json( profile,Enregistrez le profil à côté des données — En R (suite)
here::here("outputs", "profiles", "muac-2024-q4.json"), pretty = TRUE, auto_unbox = TRUE )Notes du présentateur
Du JSON plutôt qu'un tableau imprimé, parce que toute la section suivante consiste à en comparer deux.Comparer cet export au précédent — En Python
previous = json.loads(Path("outputs/profiles/muac-2024-q3.json").read_text()) for column, values in profile["categorical_values"].items(): was, now = set(previous["categorical_values"][column]), set(values) if was != now: print(f"{column}: new {sorted(now - was)}, gone {sorted(was - now)}") growth = (profile["rows"] - previous["rows"]) / previous["rows"] print(f"row count moved {growth:.1%}")Notes du présentateur
La plupart des exports sont le même export, un trimestre plus tard. La question intéressante n'est donc jamais « qu'y a-t-il dans ce fichier » mais « qu'est-ce qui a changé ».Comparer cet export au précédent — En R
previous <- jsonlite::read_json( here::here("outputs", "profiles", "muac-2024-q3.json"), simplifyVector = TRUE ) for (column in names(profile$categorical_values)) { was <- previous$categorical_values[[column]] now <- profile$categorical_values[[column]] if (!setequal(was, now)) { cat(column, ": new", setdiff(now, was), "| gone", setdiff(was, now), "\n") } } sprintf("row count moved %.1f%%", 100 * (profile$rows - previous$rows) / previous$rows)Comparer cet export au précédent
Un profil que vous n'avez pas enregistré est un profil que vous n'avez pas exécuté. Toute sa valeur tient à la possibilité de le rouvrir plus tard.
Notes du présentateur
Une valeur nouvelle dans une colonne catégorielle est le moyen le plus courant pour qu'une analyse se mette à produire des réponses fausses sans jamais échouer. Un formulaire gagne une modalité de réponse, une treizième commune est ajoutée, un antigène est renommé — et toutcase_whenécrit avant ce jour envoie désormais la valeur nouvelle dans sa branche par défaut. Comparer les profils l'attrape à l'arrivée, le seul moment où c'est bon marché.La suite
- Vous savez désormais que ce registre a perdu 5,4 % de ses âges.
Notes du présentateur
Vous savez désormais que ce registre a perdu 5,4 % de ses âges. Ce chiffre seul ne veut presque rien dire — il importe énormément de savoir si ces 226 lignes sont dispersées sur douze communes ou concentrées sur une seule. La leçon suivante décompose la non-réponse jusqu'à ce qu'elle cesse d'avoir l'air d'un accident ou qu'elle prouve qu'elle en est un, et chiffre ce que la suppression de ces lignes ferait au classement que vous publiez.