cassionAnalyse de données

Leçon 4 sur 8

Unité · La même réponse deux fois

Quatre choses qui changent la réponse alors que rien n'a changé

L'horloge, la graine, l'ordre des fichiers et la locale. Chacune produit un résultat différent depuis un code identique et des données identiques, chacune est invisible dans un diff, et cette plateforme en a rencontré trois sur quatre en production.

PythonR135 minCritères d'évaluation du CAD de l'OCDEEnquête SMART

Un : l’horloge

from datetime import date
report_date = date.today()          # different every day, by design

Tout ce qui lit l’heure courante fait différer chaque exécution. Un pied de page « produit le », un nom de fichier avec la date du jour, un filtre sur « les 30 derniers jours », une graine aléatoire dérivée du temps.

Le remède est de prendre la date des données ou d’un paramètre.

import pandas as pd

survey = pd.read_csv("data/raw/household-survey-2025.v1.csv")
as_of = pd.to_datetime(survey["interview_date"]).max()   # from the data
as_of <- max(as.Date(survey$interview_date))

Cette plateforme s’est trompée là-dessus et l’échec est instructif. Les documents de cours et les diaporamas de leçon prenaient à l’origine leur date de production du champ updated le plus récent de toute la bibliothèque de contenu. Déterministe — et faux quand même, parce que publier un nouveau cours réhorodatait chaque document existant et réécrivait les quatre-vingt-seize binaires de diaporama.

La date est désormais rattachée à l’artefact. Celle d’un document vient de son propre cours et de ses leçons, celle d’un diaporama de sa propre leçon, celle d’un rapport de son propre projet. Une réponse fausse déterministe reste une réponse fausse, et « le diff est énorme mais correct » est la façon dont personne ne le relit.

Deux : la graine

import numpy as np

rng = np.random.default_rng(20260729)      # stated, committed, reproducible
sample = rng.choice(households, size=200, replace=False)
set.seed(20260729)
sample(households, 200)

Tout ce qui est aléatoire exige une graine et la graine a sa place dans le code. Intervalles bootstrap, échantillonnage aléatoire pour vérification, simulation, partitions d’apprentissage et de test, points de nuage déplacés aléatoirement.

Fixez-la une fois, en tête, visiblement. Une graine enfouie trois fonctions plus bas est une graine que quelqu’un déplacera.

Et dites-le dans la sortie. L’intervalle bootstrap du cours de régression est rapporté avec « 400 rééchantillonnages, graine 20260729 » exactement pour cette raison — un intervalle que personne ne peut reproduire n’est pas un intervalle.

Chaque jeu de données de cette plateforme est produit par un script à graine fixe, ce qui fait de pnpm datasets:generate une étape de vérification : il réécrit les vingt CSV et git status ne signale aucun changement.

Trois : l’ordre dans lequel les fichiers reviennent

import glob
for path in glob.glob("data/raw/*.csv"):     # order is filesystem-dependent
    ...

glob et os.listdir renvoient les fichiers dans un ordre qui diffère selon les systèmes et parfois d’une exécution à l’autre. Si quoi que ce soit en aval dépend de l’ordre — une concaténation, une déduplication au premier arrivé, un index de ligne — le résultat diffère.

for path in sorted(glob.glob("data/raw/*.csv")):
    ...
for (path in sort(list.files("data/raw", full.names = TRUE))) { }

Triez. Toujours. Cela coûte six caractères.

La même chose vaut pour l’ordre des dictionnaires et des groupes. groupby dans pandas trie par défaut et dplyr::group_by non ; un graphique dont l’ordre des barres vient d’un groupement non trié est un graphique dont l’ordre peut changer sans que les données changent.

Quatre : la locale

float("1,234")           # ValueError, or 1.234, depending on where you are

Le séparateur décimal, le séparateur de milliers, le format de date et l’ordre de tri des caractères accentués dépendent tous de la locale, et le public de cette plateforme travaille dans les deux conventions par définition.

survey = pd.read_csv(path, decimal=".", thousands=None)     # stated, not inferred
dates = pd.to_datetime(survey["date"], format="%Y-%m-%d")   # explicit
readr::read_csv(path, locale = locale(decimal_mark = ".", date_format = "%Y-%m-%d"))

Énoncez le format plutôt que de laisser le lecteur l’inférer. pd.to_datetime sans format est une fonction qui devine, et elle devine différemment sur 03/04/2025 selon ce qu’il y a d’autre dans la colonne.

Le tri est le plus subtil. sorted() sur des noms de communes français place Étroit après Zone sous une locale et avant Fond sous une autre, si bien que l’ordre des catégories d’un graphique devient dépendant de la machine.

Le test qui attrape les quatre

python run.py && cp -r outputs outputs-first
python run.py && diff -r outputs outputs-first

Exécutez deux fois et comparez. Tout ce qui diffère est l’une des quatre, et le diff nomme le fichier.

Exécutez deux fois sur des machines différentes et vous attrapez en plus la locale et l’ordre des fichiers, ce qu’une seule machine ne peut pas faire.

Mettez-le dans l’intégration continue, qui est une machine propre à chaque fois, et la vérification s’exécute que quiconque y pense ou non.

L’habitude de la sortie déterministe

Pour tout ce qu’une compilation produit, une sortie identique octet pour octet sur entrée inchangée est l’objectif, et il est atteignable plus souvent qu’on ne le suppose.

Source de bruit Remède
Horodatages dans les métadonnées de fichier SOURCE_DATE_EPOCH, ou les retirer
Une date de production dans un pied de page La prendre du contenu
Une compression avec horodatage Une archive dont les dates d’entrée sont fixées
L’ordre de sommation en virgule flottante Trier avant de réduire là où cela compte
Un identifiant aléatoire incorporé Le dériver d’une empreinte du contenu

Cette plateforme fixe SOURCE_DATE_EPOCH pour pdfTeX et pour pandoc, parce qu’un .pptx est une archive dont les dates d’entrée et les propriétés de document bougeraient sinon à chaque reconstruction de chaque diaporama.

Le bénéfice est qu’un diff signifie quelque chose. Quand reconstruire ne produit aucun changement, un changement dans le diff est un vrai changement — et la relecture devient possible.

Rapportez-le en entier

Determinism

  All randomness is seeded: seed 20260729, set in src/config.py and reported
  with every bootstrap interval in this report.

  Dates are taken from the data (the latest interview date) rather than from
  the clock. No output contains a generation timestamp.

  File iteration is sorted. Group order is stated explicitly rather than
  inherited from the grouping library's default.

  CSV reading states the decimal mark and the date format rather than
  inferring them.

  Verified: running the pipeline twice produces byte-identical outputs, and
  CI runs it on a clean machine on every push.

La dernière ligne est l’affirmation et les quatre au-dessus sont la façon dont elle a été obtenue. Un rapport qui affirme le déterminisme sans dire laquelle des quatre il a traitée a probablement traité la graine et aucune autre.

La suite

Une chaîne reproductible qui produit un rapport vaut la peine d’exister. La leçon suivante lui en fait produire douze, depuis un seul modèle, sans une seule copie.

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.