Leçon 6 sur 8
Unité · Un modèle, plusieurs sorties
Une chaîne qui échoue vaut mieux qu'une chaîne qui devine
L'export amont gagne une colonne, en perd une, change un code de « yes » à « Y », ou arrive avec la moitié des lignes. Une chaîne qui continue produit un nombre faux et plausible. Sept vérifications l'arrêtent, et chacune a une défaillance réelle derrière elle.
La défaillance qui n’a pas de message d’erreur
L’export mensuel arrive. Une colonne qui disait true dit maintenant Y. Votre
conversion booléenne transforme chacune d’elles en valeur manquante, le dénominateur
perd 30 %, et la chaîne s’exécute jusqu’au bout et produit un rapport.
Rien n’a échoué. Le graphique est tracé, le pourcentage est plausible, le fichier est daté d’aujourd’hui, et le nombre est faux.
C’est le mode de défaillance pour lequel cette leçon existe, et il est bien plus courant qu’un plantage. Un plantage se corrige le matin même.
Sept vérifications, chacune avec une défaillance réelle derrière
Chacune correspond à un défaut documenté dans les jeux de données de cette plateforme.
Une : les colonnes attendues sont présentes.
REQUIRED = {"household_id", "district", "water_source", "round_trip_minutes"}
missing = REQUIRED - set(survey.columns)
if missing:
raise ValueError(f"Export is missing columns: {sorted(missing)}")
stopifnot(all(required %in% names(survey)))
Deux : le nombre de lignes est dans la plage attendue.
if not 2_000 <= len(survey) <= 3_000:
raise ValueError(f"Expected 2,000-3,000 households, got {len(survey):,}")
La moitié d’un export est la défaillance silencieuse la plus courante. Un téléchargement tronqué, un filtre resté actif, une plage de dates décalée d’un mois — tous produisent un fichier valide avec trop peu de lignes.
Trois : les codes sont les codes que vous connaissez.
KNOWN = {"piped-into-dwelling", "piped-into-yard", "public-tap", "borehole",
"protected-well", "protected-spring", "unprotected-well",
"unprotected-spring", "surface-water", "tanker-truck", "rainwater"}
unknown = set(survey["water_source"].dropna()) - KNOWN
if unknown:
raise ValueError(f"Unknown water_source values: {sorted(unknown)}")
setdiff(unique(survey$water_source), known)
Un nouveau code est une décision, non une donnée. Quelqu’un a ajouté une option au
formulaire et l’analyse doit décider où elle va — la faire tomber discrètement dans
« autre » revient à laisser un .fillna() prendre la décision.
Quatre : l’identifiant est unique là où il doit l’être.
duplicates = survey["household_id"].duplicated().sum()
if duplicates:
raise ValueError(f"{duplicates} duplicate household_id values")
Le registre scolaire de cette plateforme a exactement ce défaut — deux élèves y figurent deux fois après un transfert jamais radié — et une jointure nue duplique leurs lignes. Une vérification l’aurait attrapé à la jointure plutôt que dans un coefficient.
Cinq : les valeurs manquantes sont là où vous les attendez.
completeness = survey.notna().mean()
if completeness["district"] < 0.99:
raise ValueError(f"district is {completeness['district']:.1%} complete")
Six : les nombres sont dans une plage possible.
implausible = survey[(survey["litres_per_person_day"] < 0)
| (survey["litres_per_person_day"] > 200)]
if len(implausible) > 20:
raise ValueError(f"{len(implausible)} implausible litres values")
Notez le seuil plutôt qu’une tolérance nulle. Onze ménages avec une erreur d’unité est un défaut documenté que cette analyse traite ; deux cents est un problème nouveau.
Sept : la sortie est celle que vous avez déclarée.
assert summary["n"].sum() == len(survey), "rows lost between input and summary"
Un nombre de lignes qui change au travers d’une jointure est l’assertion la plus utile de l’analyse de programme, parce qu’une jointure interne qui écarte un tiers des données ressemble exactement à une jointure interne qui n’en écarte aucune.
Échouez au point de défaillance, non à la fin
def load_survey(path: pathlib.Path) -> pd.DataFrame:
survey = pd.read_csv(path)
check_columns(survey)
check_rows(survey)
check_codes(survey)
return survey # nothing downstream runs on a bad file
load_survey <- function(path) {
survey <- readr::read_csv(path)
check_columns(survey); check_rows(survey); check_codes(survey)
survey
}
Vérifiez à la frontière — là où les données entrent, et après chaque jointure. Une vérification en fin de chaîne vous dit que quelque chose ne va pas ; une vérification à la frontière vous dit quoi.
Levez une erreur, n’avertissez pas. Un avertissement dans un journal que personne ne lit équivaut à aucune vérification, et le journal n’est pas lu précisément les jours chargés où l’export casse.
Ce que fait cette plateforme
Sept vérifications font échouer astro build, délibérément, et elles ont la même
forme.
| Vérification | Attrape |
|---|---|
| Schéma Zod | Un champ manquant ou du mauvais type |
| Références inter-collections | Un chemin pointant vers un cours qui n’existe pas |
| Fichiers hors d’un répertoire de langue | Une entrée sans langue |
| Parité de traduction | Une entrée publiée dans une seule langue |
| Cohérence sujet-secteur | Un sujet étiqueté hors de son secteur |
| Synthétique uniquement | Un jeu de données non déclaré synthétique |
| Ossature du programme | Un cours publié absent de la carte du cursus |
Quatre autres s’exécutent dans pnpm test plutôt que dans la compilation, parce
qu’elles ont besoin de node:fs et que la compilation pré-rend dans un worker
Cloudflare qui n’en a pas. C’est une contrainte réelle qui a décidé où vivent les
vérifications, et elle vaut d’être nommée : placez la vérification là où elle peut
s’exécuter, non là où c’est le plus élégant.
L’une d’elles vérifie une relation que le graphe de références ne peut
structurellement pas atteindre. Les règles de références vérifient qu’un slug déclaré
se résout ; elles ne peuvent pas vérifier qu’une entrée est pointée. Un cours
pourrait donc être livré sans pratique attachée et chaque barrière resterait verte —
d’où l’existence de course-practice.test.ts, qui compte à rebours depuis chaque cours
publié vers le laboratoire et l’exercice qui doivent le nommer.
Toute règle du type « chaque X a au moins un Y » exige un test de cette forme. Un graphe de références est le mauvais outil pour cela.
La règle à retenir
Tout champ nommant un fichier livré exige un drapeau ready et un test de système de
fichiers à côté. Cette plateforme l’a appris trois fois : vingt exemples travaillés
déclarés et jamais écrits, trente-cinq livrables de projet déclarés et jamais écrits, et
un ensemble de chemins de figures qui ne pointaient vers rien.
Un chemin dans un frontmatter est une chaîne de caractères, et rien dans une
compilation ne peut dire s’il pointe vers quelque chose. Le schéma met donc ready à
false par défaut et un test vérifie que le fichier existe.
Rapportez-le en entier
Pipeline checks
The loader validates every raw export before anything downstream runs:
required columns present, 2,000-3,000 rows, water_source values within the
known set, household_id unique, district at least 99% complete.
Implausible litres-per-person values are tolerated up to 20 rows, which is
the documented unit-entry defect; above that the run stops.
Row counts are asserted across every join. A join that changes the row
count stops the pipeline rather than producing a summary.
All checks raise rather than warn. The March run stopped on an unknown
water_source value ("piped-shared"), which turned out to be a new form
option added upstream; it is now mapped explicitly in src/clean.py.
La dernière phrase est ce qui rend la section crédible. Une section de vérifications qui n’a jamais rien attrapé est une section de vérifications que personne n’a testée.
La suite
Tout ce qui précède suppose que vous êtes encore là. La leçon suivante porte sur le document qui doit fonctionner quand vous n’y êtes plus — écrit pour quelqu’un qui a votre poste et rien de votre contexte.