cassionAnalyse de données

Retour à la leçonLeçon 1 sur 8Avant 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.

Diapositives · PDFDiapositives · PowerPoint

  1. Diapositive 1 / 28

    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é.
  2. Diapositive 2 / 28

    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.
  3. Diapositive 3 / 28

    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_csv regarde 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 -99 moyenné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.
  4. Diapositive 4 / 28

    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)
  5. Diapositive 5 / 28

    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=False empêche pandas de transformer les chaînes littérales NA, N/A, null et nan en 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é NA pour « 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ément col_character() partout, ce dont problems() a besoin pour être utile — en lisant strictement plus tard, toute valeur non convertible est signalée au lieu d'être silencieusement transformée en NA.
  6. Diapositive 6 / 28

    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.
  7. Diapositive 7 / 28

    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))
  8. Diapositive 8 / 28

    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.
  9. Diapositive 9 / 28

    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.
  10. Diapositive 10 / 28

    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)
  11. Diapositive 11 / 28

    L'inventaire des colonnes

    ColonneVidesDistinctesRemarque
    age_months226555,4 % de vides — la leçon 2 demande où
    oedema444quatre 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.
  12. Diapositive 12 / 28

    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 contient true, false, une chaîne vide et N — vingt-cinq lignes où un enquêteur a utilisé les conventions Y/N dans une colonne que le formulaire attendait en true/false. Convertissez cette colonne en booléen et les lignes N deviennent 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.
  13. Diapositive 13 / 28

    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_mm a l'air complète — aucune case vide. Elle ne l'est pas.
  14. Diapositive 14 / 28

    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, 9999 et 88 sont tous en usage dans ce secteur, et aucun n'a l'air faux dans un résumé.
  15. Diapositive 15 / 28

    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ée child_id est un identifiant. Savoir si elle est unique est une autre question, et la réponse est ici non.
  16. Diapositive 16 / 28

    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.
  17. Diapositive 17 / 28

    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.
  18. Diapositive 18 / 28

    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)
  19. Diapositive 19 / 28

    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.
  20. Diapositive 20 / 28

    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.
  21. Diapositive 21 / 28

    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))
  22. Diapositive 22 / 28

    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,
  23. Diapositive 23 / 28

    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.
  24. Diapositive 24 / 28

    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é ».
  25. Diapositive 25 / 28

    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)
  26. Diapositive 26 / 28

    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 tout case_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é.
  27. Diapositive 27 / 28

    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.
  28. Diapositive 28 / 28

    La suite

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