Aller au contenu
English

Livrer un jeu de données réutilisable

  • Leçon 18
  • 55 min
  • Module 6 : Contrats de données et passation

Ce que vous saurez faire

Livrez le jeu de données comme un projet que tout le monde peut relancer à partir de zéro avec une commande. Les mêmes données d’entrée doivent produire les mêmes fichiers. Si les données changent, l’exécution doit s’arrêter avec un message clair.

Carnet JupyterFaites cette leçon dans un carnet JupyterColab · Kaggle · GitHub · en anglais

Le carnet (notebook) Jupyter écrit check_input.py et run_pipeline.py. Il exécute le pipeline deux fois à partir d’un instantané propre et compare les sommes de contrôle SHA-256. Ensuite, il utilise l’exportation d’août et fait l’exercice du module dans une nouvelle copie. Ses premières cellules installent pandas, téléchargent les données d’exercice et reconstruisent les fichiers des leçons précédentes. Il fonctionne donc sans votre projet local. Lisez les explications ici et exécutez le code dans le carnet.

Le carnet est en anglais. Colab et Kaggle ouvrent votre propre copie. Il faut un compte Google ou Kaggle gratuit. Vous êtes bloqué ? Consultez le carnet de solution.

Avant de commencerCe qu’il faut savoir et avoir sous la mainEnviron 55 minutes

Ce que vous devez savoir

Ce qu’il vous faut sur votre ordinateur (seulement pour travailler localement; le carnet n’en a pas besoin)

Testé avec Python 3.14.6 et pandas 3.0.6 sur macOS. Les commandes Windows n’ont pas été testées. Le carnet a été testé dans Jupyter.

Priya ne veut pas seulement les fichiers. Elle veut pouvoir les recréer : le mois prochain, sur son ordinateur, après une correction. Mei fait trois promesses :

  1. Une commande. Toute la préparation s’exécute avec une commande, à partir d’un instantané propre. Personne n’a besoin de mémoriser l’ordre des étapes.
  2. Mêmes entrées, mêmes sorties. Deux exécutions sur le même instantané produisent les mêmes fichiers, octet pour octet. Un processus qui possède cette propriété est reproductible.
  3. Entrées modifiées, échec clair. Quand l’exportation change d’une façon que les scripts ne prévoient pas, l’exécution s’arrête et indique ce qui a changé.

Comment prouver la deuxième promesse? Vous ne pouvez pas comparer 1 369 lignes à l’œil. Vous comparez des sommes de contrôle. Voici un petit exemple dans le mode interactif de Python :

Interactive Python
>>> import hashlib
>>> hashlib.sha256(b"T-1,email,2\n").hexdigest()
'aba761b1f15fa31acb1313ea20118288337bfaba145be01e9ead1e03c7e47ee2'
>>> hashlib.sha256(b"T-1,email,2\n").hexdigest()
'aba761b1f15fa31acb1313ea20118288337bfaba145be01e9ead1e03c7e47ee2'
>>> hashlib.sha256(b"T-1,Email,2\n").hexdigest()
'ebd66142a4ce9df959615da1256552e7679bbe07d069ed9017a31eabfac4b550'

Les mêmes octets donnent la même somme de contrôle. Une seule majuscule en donne une complètement différente. Donc, si deux exécutions donnent la même somme de contrôle pour train.csv, les deux fichiers sont identiques.

Le projet de Mei contient maintenant tout ce dont Priya a besoin, à un endroit fixe :

ticket-data/
run_pipeline.py the one command
DATASET.md the dataset card (Track lineage)
requirements.txt pinned versions
get_data.py, q.py from the labs repository
queries/ extract_tickets.sql, extract_outcomes.sql, 60-contract-checks.sql
scripts/ check_input.py the input schema (new in this lesson)
extract.py, clean.py, build_features.py, split.py
validate.py the output contract (Validation rules)
data/raw/ the export files
data/extract/ data/clean/ data/model/ written by the pipeline
reports/ cleaning.json and validation.json: the quality report
Question de Priya Fichier qui y répond
Comment les données ont-elles été sélectionnées? queries/*.sql, scripts/extract.py, data/extract/manifest.json
Quel schéma l’exportation doit-elle respecter? scripts/check_input.py
Sur quoi puis-je compter dans les fichiers livrés? scripts/validate.py, reports/validation.json
Comment la séparation a-t-elle été faite? scripts/split.py, data/model/split_manifest.json
Qu’est-ce qui a été supprimé ou corrigé? reports/cleaning.json, data/clean/quarantine.csv
Mes fichiers sont-ils identiques aux vôtres? data/model/MANIFEST.json (cette leçon)

Il manque deux nouveaux fichiers : la vérification des entrées et la commande unique.

validate.py vérifie les sorties. La nouvelle exportation peut aussi rendre les entrées incompatibles : une colonne renommée ou des nombres écrits en texte. Le pipeline doit détecter cela avant de charger les données. Sinon, un fichier chargé peut provoquer des erreurs silencieuses. La vérification se fait dans un script distinct, scripts/check_input.py. Il utilise seulement la bibliothèque standard de Python.

scripts/check_input.py
"""Check the raw export files before anything loads them: each file must have
the expected columns, and each value must be readable as its column's type.
python scripts/check_input.py
python scripts/check_input.py --tickets data/raw/tickets-2026-08-01.csv
This check looks at the shape of the data only. Messy values (spellings,
duplicates, sentinels) are the cleaning step's job. Exit code 1 on failure.
"""
import argparse
import csv
import sys
from datetime import datetime
from pathlib import Path
# The schema of the export of 2026-07-01: column name -> type.
SCHEMAS = {
"customers": {"customer_id": "text", "segment": "text", "region": "text", "joined_on": "date"},
"tickets": {
"ticket_id": "text", "customer_id": "text", "created_at": "datetime", "channel": "text",
"team": "text", "priority": "integer", "order_value": "number", "word_count": "integer",
"first_reply_minutes": "integer", "priority_now": "integer", "closed_at": "datetime",
},
"outcomes": {
"outcome_id": "text", "ticket_id": "text", "recorded_at": "datetime", "status": "text",
"resolution_code": "text", "csat": "integer",
},
}
# Each type has a function that raises ValueError for a value it cannot read.
READERS = {
"text": str,
"integer": int,
"number": float,
"date": lambda v: datetime.strptime(v, "%Y-%m-%d"),
"datetime": lambda v: datetime.strptime(v, "%Y-%m-%d %H:%M:%S"),
}
def check_file(path, schema):
"""Return a list of problems. An empty list means the file is OK."""
if not path.exists():
return [f"file not found: {path}"]
with open(path, newline="", encoding="utf-8") as f:
reader = csv.reader(f)
header = next(reader, [])
rows = list(reader)
problems = []
missing = [c for c in schema if c not in header]
extra = [c for c in header if c not in schema]
for column in missing:
problems.append(f"missing column: {column}")
for column in extra:
hint = " (was a column renamed?)" if missing else ""
problems.append(f"unexpected column: {column}{hint}")
# An empty value is a missing value. It is allowed here; only values that
# are present must have the right type.
for column, kind in schema.items():
if column not in header or kind == "text":
continue
i = header.index(column)
bad = []
for line, row in enumerate(rows, start=2): # line 1 is the header
value = row[i]
if value == "":
continue
try:
READERS[kind](value)
except ValueError:
bad.append((line, value))
if bad:
line, value = bad[0]
present = sum(1 for row in rows if row[i] != "")
problems.append(f"{column}: {len(bad)} of {present} values are not a valid {kind}"
f" (first: line {line}, {value!r})")
return problems
def main():
parser = argparse.ArgumentParser()
for name in SCHEMAS:
parser.add_argument(f"--{name}", default=f"data/raw/{name}.csv", type=Path)
args = parser.parse_args()
failed = False
for name, schema in SCHEMAS.items():
path = getattr(args, name)
problems = check_file(path, schema)
print(f"{'PASS' if not problems else 'FAIL'} {path}")
for problem in problems:
print(f" {problem}")
failed = failed or bool(problems)
if failed:
print("The input does not match the expected schema. Do not load it.")
sys.exit(1)
print("The input matches the expected schema.")
if __name__ == "__main__":
main()
Partie Pourquoi
SCHEMAS Le schéma attendu, écrit à la main, comme dans validate.py. C’est le contrat avec l’exportation du service de soutien.
READERS Une fonction par type. float("$10.85") déclenche ValueError, donc la vérification détecte les nombres écrits en texte.
--tickets et les autres options Par défaut, le script vérifie data/raw/. Avec une option, il vérifie un nouveau fichier avant son utilisation.
Seulement l’en-tête et les types Les graphies comme Email (courriel) ne sont pas des problèmes de schéma. clean.py les traite et validate.py vérifie le résultat. Chaque vérification a un seul rôle.

run_pipeline.py exécute les six étapes dans l’ordre. Il s’arrête à la première étape qui échoue. À la fin, il écrit data/model/MANIFEST.json, avec une somme de contrôle et un nombre de lignes pour chaque fichier de sortie.

run_pipeline.py
"""Run the whole preparation, in order, from larkfield.db to a validated
dataset. Stop at the first step that fails. At the end, write
data/model/MANIFEST.json with a SHA-256 checksum of every output file.
python get_data.py --reset start from a clean snapshot
python run_pipeline.py
python run_pipeline.py --tickets data/raw/tickets-2026-08-01.csv
check a new export before you load it
"""
import argparse
import csv
import hashlib
import json
import shutil
import sqlite3
import subprocess
import sys
from datetime import datetime, timezone
from importlib.metadata import version
from pathlib import Path
STEPS = [
"scripts/check_input.py",
"scripts/extract.py",
"scripts/clean.py",
"scripts/build_features.py",
"scripts/split.py",
"scripts/validate.py",
]
# Folders that only the pipeline writes. They are deleted before a run, so an
# old file can never look like the output of this run.
OUTPUT_FOLDERS = [Path("data/extract"), Path("data/clean"), Path("data/model")]
OUTPUTS = [
"data/extract/tickets.csv", "data/extract/outcomes.csv",
"data/clean/tickets.csv", "data/clean/quarantine.csv", "data/clean/outcomes.csv",
"data/model/tickets_model.csv", "data/model/train.csv", "data/model/valid.csv", "data/model/test.csv",
"data/model/build_summary.json", "data/model/split_manifest.json",
"reports/cleaning.json", "reports/validation.json",
]
MANIFEST = Path("data/model/MANIFEST.json")
def sha256(path):
return hashlib.sha256(Path(path).read_bytes()).hexdigest()
def describe(path):
"""Checksum, and the number of data rows for a CSV file."""
entry = {"sha256": sha256(path)}
if path.endswith(".csv"):
with open(path, newline="", encoding="utf-8") as f:
entry["rows"] = sum(1 for _ in csv.reader(f)) - 1 # minus the header
return entry
def main():
parser = argparse.ArgumentParser()
parser.add_argument("--tickets", default="data/raw/tickets.csv",
help="the raw tickets file for the input check")
args = parser.parse_args()
if not Path("larkfield.db").exists():
sys.exit("larkfield.db not found. Run: python get_data.py")
for folder in OUTPUT_FOLDERS:
shutil.rmtree(folder, ignore_errors=True)
started = datetime.now(timezone.utc)
for number, step in enumerate(STEPS, start=1):
command = [sys.executable, step] # the same Python that runs this script
if step == "scripts/check_input.py":
command += ["--tickets", args.tickets]
name = Path(step).stem
result = subprocess.run(command, capture_output=True, text=True)
if result.returncode != 0:
print(f"{number}/{len(STEPS)} {name}: FAILED\n")
print((result.stdout + result.stderr).rstrip())
print(f"\nStopped at step {number} ({name}). No MANIFEST.json was written: this run delivered no dataset.")
sys.exit(1)
print(f"{number}/{len(STEPS)} {name}: ok")
# The extraction manifest says which snapshot and which selection the
# dataset comes from. Its time stamp goes in "run", with this run's.
extract = json.loads(Path("data/extract/manifest.json").read_text(encoding="utf-8"))
outputs = {path: describe(path) for path in OUTPUTS}
manifest = {
"dataset": "Larkfield ticket escalation dataset",
"source": extract["source"],
"selection": extract["selection"],
"queries": {f["query"]: f["query_sha256"] for f in extract["files"]},
"steps": STEPS,
"outputs": outputs,
# One checksum for all outputs together: equal outputs give an
# equal fingerprint, whatever the time of the run.
"outputs_fingerprint": hashlib.sha256(json.dumps(outputs, sort_keys=True).encode()).hexdigest(),
"run": {
"started_at": started.isoformat(timespec="seconds"),
"extracted_at": extract["extracted_at"],
"python": sys.version.split()[0],
"sqlite": sqlite3.sqlite_version,
"pandas": version("pandas"),
},
}
MANIFEST.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
model = outputs["data/model/tickets_model.csv"]["rows"]
parts = ", ".join(f"{p} {outputs[f'data/model/{p}.csv']['rows']}" for p in ["train", "valid", "test"])
print(f"Done: {model} rows ({parts}).")
print(f"Wrote {MANIFEST}. Outputs fingerprint: {manifest['outputs_fingerprint']}")
if __name__ == "__main__":
main()

Quatre décisions permettent de relancer le pipeline sans risque :

  • Supprimer d’abord les anciennes sorties. Si une étape échoue, son ancien fichier d’hier ne doit pas sembler être le résultat d’aujourd’hui. L’exécution commence donc avec des dossiers de sortie vides.
  • Chaque étape est un programme distinct. subprocess.run lance chaque script avec le même Python (sys.executable) et lit son code de sortie. C’est pourquoi validate.py et check_input.py se terminent avec sys.exit(1) en cas d’échec. Une étape qui échoue arrête l’exécution, et son propre message s’affiche.
  • Séparer le contenu des heures. Les sommes de contrôle sont dans outputs. L’heure de l’exécution et celle de l’extraction sont dans run. extracted_at change à chaque exécution. Donc, manifest.json lui-même n’est pas dans outputs; son contenu est copié dans le manifeste.
  • Une empreinte unique. outputs_fingerprint est une somme de contrôle de toutes les sommes de contrôle des sorties réunies. Pour comparer deux exécutions, vous comparez une seule ligne.

Exemple détaillé : deux exécutions à partir d’un instantané propre

Section intitulée « Exemple détaillé : deux exécutions à partir d’un instantané propre »

Mei part d’un instantané propre. get_data.py --reset supprime la base de données et la reconstruit à partir des fichiers CSV vérifiés :

python get_data.py --reset
Deleted larkfield.db.
customers 240 rows
tickets 1417 rows
outcomes 1874 rows
Built larkfield.db. Check: customers 240, tickets 1417, outcomes 1874.
python run_pipeline.py
1/6 check_input: ok
2/6 extract: ok
3/6 clean: ok
4/6 build_features: ok
5/6 split: ok
6/6 validate: ok
Done: 1369 rows (train 927, valid 227, test 215).
Wrote data/model/MANIFEST.json. Outputs fingerprint: 2b2460321543ffb73fe9d5b04fd8f5a9d77ee990fff37823b0855b911ba4b855

Une partie du manifeste :

data/model/MANIFEST.json (part)
"outputs": {
"data/extract/tickets.csv": {
"sha256": "96786af9e2ebb2492219867ac302f57fabdefb4c53fb60a1071d9412e70e6f39",
"rows": 1416
},
...
"data/model/train.csv": {
"sha256": "e83c4a056c6e26595777760918e49d9f9b9b39be7bb561a20aefd25dd4c177b8",
"rows": 927
},
...
},
"outputs_fingerprint": "2b2460321543ffb73fe9d5b04fd8f5a9d77ee990fff37823b0855b911ba4b855",
"run": {
"started_at": "2026-10-07T03:13:22+00:00",
"extracted_at": "2026-10-07T03:13:22+00:00",
"python": "3.14.6",
"sqlite": "3.50.4",
"pandas": "3.0.6"
}

Elle conserve une copie du manifeste, relance le pipeline et compare. L’empreinte est identique. Ensuite, elle compare les deux fichiers ligne par ligne avec diff, un outil standard sur macOS et Linux :

diff first-run.json data/model/MANIFEST.json
77,78c77,78
< "started_at": "2026-10-07T03:13:22+00:00",
< "extracted_at": "2026-10-07T03:13:22+00:00",
---
> "started_at": "2026-10-07T03:13:25+00:00",
> "extracted_at": "2026-10-07T03:13:25+00:00",

Seules les deux heures diffèrent. Les 13 fichiers de sortie sont tous identiques. Le pipeline est reproductible.

Mei ajoute les sommes de contrôle des sorties à DATASET.md, pour que Priya puisse vérifier les fichiers reçus. Elle ajoute aussi check_input.py comme étape 1 dans le tableau Transformation order (ordre des transformations). Elle place la commande au-dessus : « Exécutez python get_data.py --reset, puis python run_pipeline.py. »

DATASET.md (new section)
## Output checksums (SHA-256)
| File | Rows | SHA-256 |
|---|---|---|
| `data/model/tickets_model.csv` | 1,369 | cb6e783f28cb17f45a7ec1412a3714a49d4ec10a292f8077dcc7bafb7421235b |
| `data/model/train.csv` | 927 | e83c4a056c6e26595777760918e49d9f9b9b39be7bb561a20aefd25dd4c177b8 |
| `data/model/valid.csv` | 227 | 5a01365d2294d32db58a55a48a8f5b4c5958e0dc381ae06c9fdc33c5fb0e88cd |
| `data/model/test.csv` | 215 | 1c0aa441b17db00d66f5f5df9f604c38d99787fd69c9894f987a7ca548d04d1b |
Outputs fingerprint (all 13 output files): `2b2460321543ffb73fe9d5b04fd8f5a9d77ee990fff37823b0855b911ba4b855`. Two runs gave the same fingerprint. On Windows, pandas writes CSV files with different line endings, so the checksums differ from these; compare two runs on the same computer.
  1. Créez scripts/check_input.py et run_pipeline.py avec le code ci-dessus. Placez run_pipeline.py dans le dossier du projet, pas dans scripts/.

  2. Dans le terminal, allez dans le dossier de votre projet et vérifiez que l’environnement est actif.

  3. Partez d’un instantané propre : python get_data.py --reset. La dernière ligne est Built larkfield.db. Check: customers 240, tickets 1417, outcomes 1874. (base larkfield.db créée; vérification des nombres de clients, de billets et de résultats).

  4. Exécutez python run_pipeline.py. Vous devriez voir six lignes ok (réussite) et Done: 1369 rows (train 927, valid 227, test 215). (terminé : 1 369 lignes, dont 927 d’entraînement, 227 de validation et 215 de test). Notez l’empreinte.

  5. Conservez une copie du manifeste :

    Terminal
    cp data/model/MANIFEST.json first-run.json
  6. Exécutez de nouveau python run_pipeline.py. L’empreinte est la même qu’à l’étape 4.

  7. Comparez les sorties des deux manifestes avec Python :

    Fenêtre de terminal
    python -c "import json; a = json.load(open('first-run.json')); b = json.load(open('data/model/MANIFEST.json')); print('same outputs' if a['outputs'] == b['outputs'] else 'different outputs')"
    Sortie
    same outputs
  8. Copiez les quatre sommes de contrôle de data/model/ depuis MANIFEST.json dans une nouvelle section de DATASET.md.

À vous de jouer : utiliser l’exportation d’août dans le pipeline

Section intitulée « À vous de jouer : utiliser l’exportation d’août dans le pipeline »

Le service de soutien a été mis à jour en juillet. L’exportation suivante, du 1 août 2026, contient 40 nouveaux billets de soutien. Avant de la charger, Mei la vérifie.

  1. Téléchargez le fichier dans data/raw/ :

    Terminal
    curl -o data/raw/tickets-2026-08-01.csv https://raw.githubusercontent.com/Nextia-Intelligence/nextia-learning-labs/main/C04/data/tickets-2026-08-01.csv
  2. Faites une prédiction : quelle étape s’arrête et quels problèmes le message indique-t-il?

  3. Exécutez python run_pipeline.py --tickets data/raw/tickets-2026-08-01.csv.

  4. Regardez dans data/model/. Y a-t-il un fichier MANIFEST.json?

Afficher le résultat attendu
python run_pipeline.py --tickets data/raw/tickets-2026-08-01.csv
1/6 check_input: FAILED
PASS data/raw/customers.csv
FAIL data/raw/tickets-2026-08-01.csv
missing column: channel
unexpected column: contact_channel (was a column renamed?)
order_value: 34 of 34 values are not a valid number (first: line 2, '$10.85')
PASS data/raw/outcomes.csv
The input does not match the expected schema. Do not load it.
Stopped at step 1 (check_input). No MANIFEST.json was written: this run delivered no dataset.

La première étape arrête l’exécution. Le message indique les deux changements : channel est devenu contact_channel, et order_value contient un signe $. Parmi les 40 billets, 34 ont une valeur de commande. Les 6 autres concernent des comptes sans commande, donc leur valeur vide est permise. Il n’y a ni MANIFEST.json ni dossier data/model/ : l’exécution a supprimé les anciennes sorties et n’a rien livré.

La correction ne se fait pas dans la vérification. Mei interroge Grace sur la mise à jour. Ensuite, elle modifie clean.py (renommer la colonne, retirer le $) et le schéma dans check_input.py. Le résultat est une nouvelle version du jeu de données, consignée dans le tableau Changes (modifications) de DATASET.md.

Pour reconstruire le jeu de données du 1 juillet, exécutez python run_pipeline.py sans option.

Erreur courante

Une exportation modifiée sans vérification des entrées

Symptôme : un script qui fonctionnait le mois dernier donne des résultats étranges ou une erreur loin de la cause. Par exemple, le nombre de valeurs de commande « inconnues » (−1) devient soudainement 0. Ou le script s’arrête avec KeyError: 'channel' (erreur de clé : channel).

Cause : le schéma de l’exportation a changé, et rien ne l’a vérifié avant l’utilisation des données. pandas lit le fichier d’août sans avertissement :

Interactive Python
>>> import pandas as pd
>>> df = pd.read_csv("data/raw/tickets-2026-08-01.csv")
>>> df["order_value"].head(3).tolist()
['$10.85', '$25.58', '$91.70']
>>> df["order_value"].dtype
<StringDtype(na_value=nan)>
>>> (df["order_value"] == -1).sum()
np.int64(0)
>>> df["channel"]
Traceback (most recent call last):
...
KeyError: 'channel'
>>> pd.read_csv("data/raw/tickets-2026-08-01.csv", dtype={"order_value": "Float64"})
Traceback (most recent call last):
...
ValueError: Unable to parse string "$10.85" at position 0

order_value est maintenant du texte. La comparaison avec −1 n’échoue pas; elle est fausse pour chaque ligne. La règle des valeurs « inconnues » ne trouve donc rien. KeyError indique une colonne, mais pas la raison. ValueError indique une valeur, mais ni la colonne ni le fichier.

Correction :

  1. Exécutez scripts/check_input.py sur chaque nouvelle exportation avant de la charger. Il indique le fichier, la colonne manquante, la colonne inattendue et la colonne contenant les mauvaises valeurs.
  2. Gardez la vérification comme étape 1 de run_pipeline.py, pour qu’on ne puisse pas l’oublier.
  3. Quand la vérification échoue, modifiez volontairement le schéma et le nettoyage, puis créez une nouvelle version du jeu de données.

Dans le travail en IA : un modèle en production reçoit ses entrées de systèmes qui changent. Une colonne renommée peut rendre toutes les valeurs d’une variable manquantes. Le modèle continue à faire des prédictions, mais elles sont moins bonnes. Une vérification des entrées transforme ce problème en arrêt clair.

L’exercice du module : exécuter le pipeline de préparation à partir d’un instantané propre des entrées. D’abord, un petit exemple détaillé.

Exemple détaillé : les étapes à la main, avec leurs nombres

Section intitulée « Exemple détaillé : les étapes à la main, avec leurs nombres »

Avant de faire confiance à la commande unique, Mei exécute chaque étape à la main, une fois, depuis un instantané propre. Elle note ce que chaque étape affiche. Chaque nombre doit correspondre aux registres de provenance de la dernière leçon.

Commande Ce qu’elle affiche (lignes principales) Code de sortie
python get_data.py --reset Built larkfield.db. Check: customers 240, tickets 1417, outcomes 1874. 0
python scripts/check_input.py 3 × PASS (réussite), The input matches the expected schema. (les entrées respectent le schéma attendu) 0
python scripts/extract.py data/extract/tickets.csv: 1416 rows (1 416 lignes), data/extract/outcomes.csv: 1869 rows (1 869 lignes), Not selected: 1 ticket row(s) (non sélectionnée : 1 ligne de billet) 0
python scripts/clean.py "exact_duplicates_removed": 14 (doublons exacts supprimés), "quarantined": 4 (en quarantaine), "clean_rows": 1398 (lignes nettoyées); résultats "clean_rows": 1834 0
python scripts/build_features.py "too_recent_for_label": 29 (trop récentes pour l’étiquette), "rows": 1369 (lignes), "escalated": 188 (transmis au niveau supérieur) 0
python scripts/split.py entraînement "rows": 927, validation "rows": 227, test "rows": 215 0
python scripts/validate.py All 13 rules passed. (les 13 règles sont respectées) 0

Chaque nombre découle du précédent : 1 417 − 1 = 1 416; 1 416 − 14 − 4 = 1 398; 1 398 − 29 = 1 369; 927 + 227 + 215 = 1 369. run_pipeline.py exécute les mêmes étapes, donc il doit donner les mêmes nombres.

Prouvez que le projet fonctionne partout, pas seulement dans votre dossier avec ses anciens fichiers et son ancien environnement.

  1. Créez un nouveau dossier, par exemple ~/projects/ticket-data-fresh.
  2. Copiez-y seulement le code et les données brutes : get_data.py, q.py, run_pipeline.py, requirements.txt, DATASET.md, les dossiers queries/ et scripts/, et data/raw/. Ne copiez pas larkfield.db, data/extract/, data/clean/, data/model/, reports/ ou .venv/.
  3. Dans le nouveau dossier, créez un nouvel environnement virtuel, activez-le et installez requirements.txt, comme dans le cours de Python.
  4. Exécutez python get_data.py.
  5. Exécutez python run_pipeline.py.
  6. Comparez l’empreinte à celle du dossier de votre projet.
  7. Exécutez le pipeline avec l’exportation d’août et vérifiez qu’il s’arrête à l’étape 1.
  8. Écrivez deux phrases dans DATASET.md, sous Changes ou dans une nouvelle section Reproducibility (reproductibilité) : la date, l’empreinte et « deux environnements ont donné la même empreinte ».
Afficher la réponse modèle

La nouvelle copie de Mei, dans un autre dossier avec un autre environnement virtuel (pandas 3.0.6) :

python run_pipeline.py (fresh copy)
1/6 check_input: ok
2/6 extract: ok
3/6 clean: ok
4/6 build_features: ok
5/6 split: ok
6/6 validate: ok
Done: 1369 rows (train 927, valid 227, test 215).
Wrote data/model/MANIFEST.json. Outputs fingerprint: 2b2460321543ffb73fe9d5b04fd8f5a9d77ee990fff37823b0855b911ba4b855

L’empreinte est la même que dans le dossier du projet. Avec --tickets data/raw/tickets-2026-08-01.csv, l’exécution s’arrête à l’étape 1 et indique channel, contact_channel et order_value, comme dans « À vous de jouer ».

La note dans DATASET.md : « 6 octobre 2026 : une nouvelle copie avec un nouvel environnement virtuel a donné l’empreinte des sorties 2b2460321543ffb73fe9d5b04fd8f5a9d77ee990fff37823b0855b911ba4b855, identique à celle du dossier du projet. L’exportation d’août s’arrête à la vérification des entrées. »

Si vos empreintes diffèrent, comparez les sections outputs des deux manifestes pour trouver le premier fichier différent. Ensuite, regardez l’étape qui l’écrit. Causes fréquentes : un fichier que vous avez oublié de copier, un script modifié dans un seul dossier ou une version différente de pandas.

Vérifiez votre compréhension

Rappel : nommez les quatre types de règles de validation d'un contrat de données, avec un exemple de Larkfield pour chacun.

Afficher la réponse

Valeurs obligatoires : ticket_id, created_at, channel et la cible ne doivent pas manquer. Valeurs et plages permises : channel vaut email (courriel), chat (clavardage), phone (téléphone), web_form (formulaire Web) ou unknown (inconnu); order_value va de 0 à 2 000. Clés uniques : chaque ticket_id apparaît une seule fois, et aucun billet n’est dans deux sous-ensembles. Distributions attendues : le taux d’escalade se situe entre 5 % et 30 %, dans l’ensemble et dans chaque sous-ensemble.

Vérifiez votre compréhension

Prédiction : Mei augmente MAX_WORDS dans clean.py de 10 000 à 50 000 et exécute run_pipeline.py. Quelle étape s'arrête, et pourquoi? Quelles sommes de contrôle changeraient si l'étape ne s'arrêtait pas?

Afficher la réponse

validate.py (étape 6) arrête l’exécution. T-10361, qui compte 48 210 mots, n’est plus mis en quarantaine et atteint donc la table de modélisation. La règle « les nombres et les dates respectent les plages permises » échoue : word_count: outside 0 to 10000 in 1 row (first: T-10361, 48210) (word_count : hors de la plage de 0 à 10000 dans une ligne; première occurrence : T-10361, 48210). Aucun fichier MANIFEST.json n’est écrit. Sans cette règle, les deux fichiers d’extraction garderaient leurs sommes de contrôle, car l’extraction précède le nettoyage. Tous les fichiers produits à partir de clean.py changeraient, donc l’empreinte changerait aussi. La règle de nettoyage et le contrat doivent concorder, sinon le contrat arrête l’exécution.

Vérifiez votre compréhension

Vérification de fin : montrez que votre préparation est reproductible et qu'elle échoue avec un message utile lorsque le schéma change. Quelles preuves donnez-vous à Priya?

Afficher la réponse

Deux preuves, toutes deux tirées de vos propres exécutions :

  1. Des résultats reproductibles. Deux exécutions de python run_pipeline.py à partir d’un instantané propre (python get_data.py --reset), avec la même valeur de outputs_fingerprint. Les sections outputs (résultats) des deux manifestes sont identiques; seuls started_at (début de l’exécution) et extracted_at (moment de l’extraction) diffèrent. Une exécution dans une nouvelle copie, avec un nouvel environnement, renforce la preuve.
  2. Un échec avec un message utile. Le résultat de python run_pipeline.py --tickets data/raw/tickets-2026-08-01.csv : l’exécution s’arrête à l’étape 1. Le message indique que channel manque, que contact_channel est inattendu et que order_value n’est pas un nombre, avec l’exemple $10.85. Aucun fichier MANIFEST.json n’est écrit.

La vérification est démontrée lorsque les deux preuves sont présentées. Elle est partiellement démontrée dans deux cas : l’exportation modifiée n’est pas testée, ou le message d’échec ne nomme pas les colonnes. Elle est non encore démontrée si les étapes doivent être exécutées à la main, ou si les empreintes diffèrent sans explication.

Récapitulatif

Dans ce module, vous avez transformé un ensemble de scripts en un jeu de données fiable qu’une autre personne peut recréer.

  • Règles de validation : un contrat de données énumère les valeurs obligatoires, les valeurs et plages permises, les clés uniques et les distributions attendues. validate.py vérifie 13 règles sur les fichiers livrés, écrit reports/validation.json et se termine avec le code 1 en cas d’échec. Chaque règle doit être testée sur des données qui ne la respectent pas.
  • Provenance des données : la table des instantanés, manifest.json, cleaning.json, quarantine.csv, build_summary.json et split_manifest.json consignent chaque étape. DATASET.md décrit la provenance (données synthétiques, generate.py, graine 2026, CC0), l’ordre des transformations, les versions et les limites.
  • Livraison : python get_data.py --reset et python run_pipeline.py construisent le jeu de données à partir d’un instantané propre avec une seule commande. MANIFEST.json contient une somme de contrôle SHA-256 de chaque résultat, sauf le moment d’exécution, et deux exécutions donnent la même empreinte. check_input.py arrête une exportation modifiée avant son chargement, avec un message qui nomme le problème.

Trois études de cas sur des données réelles suivent. Terminez-en au moins une pour obtenir le certificat. Puis vient le travail final : vous préparez le jeu de données depuis le début et le livrez avec les preuves de chaque étape.

Leçon suivante : Expliquer les ventes d'une boutique en ligne