cassionAnalyse de données

Leçon 8 sur 8

Unité · Des graphiques qui ne peuvent pas dériver

Le graphique et la phrase viennent du même fichier

Chaque règle de ce cours est une règle dont il faut se souvenir le jour venu. Produire la figure depuis le jeu de données supprime le fait de s'en souvenir — et c'est le seul moyen pour qu'une correction de données déplace le graphique et la phrase ensemble, plutôt que l'un des deux.

PythonR135 minDéfinitions d'indicateurs de l'UNICEFCritères d'évaluation du CAD de l'OCDENorme humanitaire fondamentale (CHS)

L’échec que cela prévient

Un graphique est exporté en PNG en mars, collé dans un rapport, et le jeu de données est corrigé en mai. La prose est mise à jour parce que quelqu’un la lit. Le graphique ne l’est pas, parce que personne ne relit une image.

Rien dans aucune compilation ne peut le détecter. La figure est une image valide, le rapport est un document valide, et le nombre de la légende et la barre derrière lui sont désormais en désaccord.

Le remède est structurel plutôt que procédural. Si la figure est produite depuis le jeu de données par un script qui tourne à chaque compilation, une correction déplace les deux ou aucun, et « penser à régénérer le graphique » cesse d’être une chose dont quiconque doit se souvenir.

Ce que fait cette plateforme

Chaque figure de ce site est déclarée comme une fonction qui lit un CSV commité et renvoie la description d’un graphique.

def food_security_instruments() -> Figure:
    survey = read("food-security-survey-2024.v1.csv")
    coping = {row["household_id"]: row for row in read("livelihood-coping-2024.v1.csv")}

    # ... compute the four prevalences from the rows ...

    return Figure(
        slug="food-security-instruments",
        kind="bar",
        title={"en": "Four instruments, one set of households", "fr": "..."},
        caption={"en": "Four food security instruments applied to the same 1,955 "
                       "households. The highest prevalence is seven times the lowest...",
                 "fr": "..."},
        x_label={"en": "Households flagged (%)", "fr": "Ménages signalés (%)"},
        bars=bars,
    )
# The same shape in R: a function returning data plus its labels, rendered once.

Aucun nombre n’est saisi. Les 7,4 % de la légende ne sont pas écrits dans la légende — la légende énonce la relation et les barres portent les valeurs, toutes calculées depuis le fichier.

Deux sorties depuis une description. SVG pour le web, TikZ pour le document PDF et le diaporama Beamer. Une figure écrite une fois apparaît à trois endroits et ne peut pas différer entre eux.

Bibliothèque standard uniquement. Les générateurs n’ont besoin que d’un Python d’origine et d’aucune installation, si bien qu’un contributeur peut régénérer chaque figure sans chaîne d’outils.

Les quatre propriétés à copier

Quel que soit l’outil employé, voici ce qui fait fonctionner le dispositif.

Une description, plusieurs rendus. Le graphique est des données plus des étiquettes, et le moteur de rendu est séparé. C’est ce qui permet à la même figure d’être un SVG web, un vectoriel d’impression et une diapositive sans trois fichiers qui dérivent.

Bilingue par construction. Titre, légende et étiquette d’axe sont des dictionnaires indexés par langue, si bien qu’une figure ne peut pas exister dans une seule. La règle de parité de traduction de la plateforme atteint les figures pour la même raison qu’elle atteint les leçons.

Sortie commitée. Les fichiers SVG et TikZ sont dans le dépôt, si bien que le site se construit sans Python sur le chemin de déploiement et qu’un contributeur sans la chaîne d’outils peut quand même livrer un changement de contenu.

Un test qui échoue quand ils divergent. figures.test.ts vérifie que chaque figure référencée par une leçon existe, qu’une leçon française référence la figure française, que chaque figure existe dans les deux langues et les deux formats, et qu’aucune légende ne fait moins de vingt caractères.

# The check that matters most, in one line:
assert (OUT / f"{figure.slug}.{locale}.svg").exists()
# A test is what turns a convention into a constraint.

Le faire sous matplotlib ou ggplot2

La même discipline, sans la machinerie de cette plateforme.

import pathlib
import pandas as pd
import matplotlib.pyplot as plt

FIGURES = pathlib.Path("outputs/figures")

def gam_by_commune() -> None:
    screening = pd.read_csv("data/muac-screening-artibonite-2024.v1.csv")
    summary = (screening.assign(case=screening["muac_mm"] < 125)
               .groupby("commune")["case"].agg(["mean", "size"])
               .sort_values("mean"))

    fig, ax = plt.subplots(figsize=(6.5, 4))
    ax.barh(summary.index, summary["mean"])
    ax.set_xlabel("GAM (%)")
    fig.savefig(FIGURES / "gam-by-commune.svg", bbox_inches="tight")
    plt.close(fig)

    summary.to_csv(FIGURES / "gam-by-commune.csv")     # the numbers, beside the chart

if __name__ == "__main__":
    FIGURES.mkdir(parents=True, exist_ok=True)
    gam_by_commune()
gam_by_commune <- function() {
  summary <- screening |>
    mutate(case = muac_mm < 125) |>
    summarise(gam = mean(case), n = n(), .by = commune)

  ggsave("outputs/figures/gam-by-commune.svg", width = 6.5, height = 4)
  write_csv(summary, "outputs/figures/gam-by-commune.csv")
}

Écrivez les nombres à côté du graphique. Un CSV auprès de chaque figure est ce qui permet à un relecteur de vérifier le graphique sans relancer l’analyse, et ce qui permet de vérifier la légende contre quelque chose.

Une fonction par figure, un script qui les lance toutes. Non un carnet où le graphique est la cellule 34 et dépend de l’exécution de la cellule 12.

Enregistrez en SVG, non en PNG, partout où la destination le permet : il reste net quand quelqu’un le redimensionne, et c’est du texte, donc il se compare.

Ce qu’il faut commiter et ce qu’il faut ignorer

Commiter Ignorer
Le script générateur Les aperçus PNG exportés
La sortie des figures, si la compilation ne peut pas exécuter Python Les points de reprise de carnets
Le CSV des nombres auprès de chaque figure Tout ce qui contient un horodatage

Sortie déterministe ou rien. Une figure qui incorpore la date de production change à chaque exécution, et le diff devient un bruit qui cache un vrai changement. Cette plateforme fixe SOURCE_DATE_EPOCH exactement pour cette raison, et les figures ne portent aucune date.

L’habitude en trois lignes pour un rapport sans chaîne

La plupart des travaux de programme n’ont pas de chaîne de figures et n’en auront pas. La version minimale viable vaut quand même la peine.

Un script qui produit chaque figure du rapport, lancé avant l’envoi du rapport, en une seule commande.

Aucun graphique fait à la main dans un tableur, parce que c’est celui qui ne sera pas régénéré.

La légende produite avec la figure, ou au moins les nombres qu’elle contient lus depuis le même objet que celui d’où le graphique a été tracé.

caption = (f"Global acute malnutrition by commune, {summary['size'].sum():,} children "
           f"screened. {(summary['mean'] > 0.10).sum()} of {len(summary)} communes "
           f"are above the 10% emergency threshold.")
# If the caption is an f-string, it cannot disagree with the chart.

Cette seule ligne représente l’essentiel du bénéfice. Une légende calculée depuis les données est une légende qui ne peut pas survivre à une correction du jeu de données sans être corrigée elle aussi.

Rapportez-le en entier

Figure production for this report

  All figures are generated from the committed dataset files by
  scripts/figures.py, run as part of the report build. No figure is drawn by
  hand and no number in a caption is typed.

  Each figure writes its underlying values to a CSV of the same name, so any
  figure can be checked without rerunning the analysis.

  Figures are produced in English and French from one description, so a
  figure cannot exist in only one language.

  Output is deterministic: rerunning the script on unchanged data produces
  byte-identical files, so a diff shows only real changes.

Le dernier paragraphe est ce qui rend le reste vérifiable. Une chaîne dont la sortie bouge à chaque exécution ne peut servir à rien vérifier, et une chaîne dont la sortie est stable transforme « ce graphique est-il à jour ? » en une question à laquelle le système de gestion de versions répond.

La suite

C’est le cours. Huit leçons et une idée : un graphique est une affirmation, et chaque décision de sa fabrication — la marque, l’intervalle, la palette, l’axe, la légende — est une décision sur ce que cette affirmation dit.

Le laboratoire construit deux figures pour un rapport réel à travers une chaîne qui vous appartient, et l’exercice prend un graphique juste dans chacun de ses nombres et trouve les quatre décisions qui l’ont rendu malhonnête quand même.

Animer cette leçon

La leçon en diaporama, la prose étant reléguée dans les notes du présentateur plutôt que projetée. Produit à partir de cette page, dont il ne peut donc pas s'écarter.

Lancer le diaporamaLire les diapositives

Le PDF ne requiert aucun logiciel et se projette depuis n'importe quel poste. Le fichier PowerPoint est fait pour être modifié : appliquez la charte de votre organisation, retirez une section pour une séance plus courte, ou fusionnez deux leçons en atelier.