Les règles de validation
- Leçon 16
- 55 min
- Module 6 : Contrats de données et passation
Leçon 16 sur 18 · SQL et préparation des données pour l'IALes règles de validation
1. Sources et structure des données
2. L'essentiel de SQL
3. Des requêtes d'analyse utiles
4. Nettoyer et transformer les données
5. Préparer la modélisation sans risque
6. Contrats de données et passation
Ce que vous saurez faire
Décrivez ce sur quoi Priya peut compter dans le jeu de données avec des règles qu’un script vérifie chaque fois. Ainsi, un jeu de données incorrect déclenche un arrêt avec un message clair avant d’atteindre un modèle.
Carnet JupyterFaites cette leçon dans un carnet Jupyter
Le carnet (notebook) Jupyter contient toutes les requêtes, tous les scripts, exercices et contrôles de cette leçon, avec les consignes. Il construit scripts/validate.py en quatre cellules, puis modifie volontairement les données pour tester les règles. Ses premières cellules installent pandas, téléchargent les données d’exercice et reconstruisent les fichiers des modules 3 à 5. Il fonctionne donc sans votre projet local. Lisez les explications ici et exécutez le code là-bas.
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 main
Ce que vous devez savoir
GROUP BYetHAVING, dans Agréger correctement.NOT INet NULL, dans Sélectionner et filtrer.- La table de modélisation et sa cible, dans Caractéristiques et moment de la cible, et la séparation temporelle, dans Choisir une séparation.
- Les colonnes qui révèlent la réponse, dans Ajuster les transformations correctement.
Ce qu’il vous faut sur votre ordinateur (seulement pour le travail local; le carnet et les encadrés de cette page n’en ont pas besoin)
- Le projet
~/projects/ticket-dataaveclarkfield.db, créé dans Préparer une base de données d’exercice : créer la base de données. Si la base de données manque ou a changé, réinitialisez la base de données avecpython get_data.py --reset. - L’environnement virtuel
.venvavec pandas 3.0.6, créé dans Profiler avant de modifier : le même profil dans pandas. Activez-le avant d’exécuter un script. - Les scripts et les sorties des modules 3 à 5, exécutés dans cet ordre :
scripts/extract.py,queries/extract_tickets.sqletqueries/extract_outcomes.sql, dans Une extraction reproductible : construire extract.py. Ils écrivent dansdata/extract/.scripts/clean.py, dans Traiter les valeurs désordonnées : terminer clean.py. Il écrit dansdata/clean/etreports/cleaning.json.scripts/build_features.py, dans Caractéristiques et moment de la cible : build_features.py. Il écrit dansdata/model/tickets_model.csv.scripts/split.py, dans Choisir une séparation : créer et vérifier la séparation. Il écrittrain.csv,valid.csvettest.csvdansdata/model/.
- Si un script manque ou ne fonctionne plus, copiez la version complète du cours depuis C04/pipeline au même endroit dans votre projet. Puis exécutez de nouveau les quatre scripts, dans l’ordre indiqué. Cette leçon modifie volontairement
build_features.pyettest.csv; les étapes expliquent comment les réparer. - Les requêtes enregistrées de ce module vont dans
queries/, avec une numérotation à partir de60-.
Testé avec Python 3.14.6 et pandas 3.0.6 sur macOS, et avec SQLite 3.49.1 dans le navigateur. Le carnet a été testé dans Jupyter.
Une promesse sur les données
Section intitulée « Une promesse sur les données »Priya entraînera un modèle avec les fichiers de Mei. Elle demande : « Sur quoi puis-je compter? Est-ce que ticket_id est unique? Est-ce que channel peut manquer? Est-ce que la cible peut valoir 2? » Mei répond avec une liste de règles. Chaque règle indique ce qui doit être vrai dans chaque fichier livré.
D’abord, deux termes :
- Le schéma d’une table est la liste de ses colonnes et le type de chaque colonne. Par exemple :
ticket_idest du texte,priorityest un entier. - Un contrat de données est une entente entre la personne qui crée un jeu de données et celles qui l’utilisent. Il contient le schéma et les règles sur les valeurs. Les règles indiquent les valeurs permises, les colonnes qui doivent avoir une valeur et la colonne qui sert de clé unique. Elles décrivent aussi les données dans leur ensemble.
Une règle de validation est un contrôle du contrat. Voici cinq billets de soutien et quatre règles. Lisez la table et trouvez les problèmes avant de lire la dernière colonne.
| ticket_id | channel | priority | escalated_72h | Enfreint une règle? |
|---|---|---|---|---|
| T-1 | 2 | 0 | non | |
| T-2 | 1 | 1 | valeurs permises : Email ne figure pas dans la liste |
|
| T-3 | chat | 5 | 0 | plage : priority doit être de 1 à 3; clé unique : T-3 apparaît deux fois |
| T-3 | chat | 5 | 0 | les deux mêmes règles |
| T-4 | NULL | 3 | 0 | valeur requise : channel manque |
Les quatre règles :
- Valeurs requises.
channeldoit avoir une valeur dans chaque ligne. - Valeurs et plages permises.
channelest l’une des valeursemail,chat,phone,web_form.priorityva de 1 à 3. - Clé unique. Chaque
ticket_idapparaît une seule fois. - Distribution attendue. Entre 5 % et 30 % des billets sont transférés à un agent supérieur. Ici, 1 billet sur 5 est transféré : 20 %. Cette règle est respectée.
Les trois premières règles examinent les lignes individuellement. La quatrième examine toute la table. Elle détecte un autre type d’erreur : chaque ligne semble correcte, mais le total est faux. Par exemple, un bogue dans le code de la cible pourrait marquer presque aucun billet comme transféré. Chaque valeur reste alors 0 ou 1. Seul le taux révèle le bogue.
Aucun de ces problèmes ne déclenche d’erreur dans SQL ou pandas. Une règle rend visible une erreur silencieuse.
Des règles sous forme de requêtes qui ne doivent retourner aucune ligne
Section intitulée « Des règles sous forme de requêtes qui ne doivent retourner aucune ligne »Une façon utile d’écrire une règle : écrire une requête qui retourne les lignes qui enfreignent la règle. La règle est respectée lorsque la requête retourne 0 ligne. Sinon, le résultat montre les lignes incorrectes, et pas seulement « faux ».
Les encadrés ci-dessous exécutent chaque règle sur l’exportation brute, la table tickets dans larkfield.db. L’exportation brute n’est pas propre, donc la plupart des règles y trouvent des problèmes. Après le pipeline, les mêmes règles ne doivent rien trouver.
Colonnes requises. La première règle vérifie le schéma lui-même : les colonnes sont-elles présentes? SQLite énumère les colonnes d’une table avec pragma_table_info('tickets'). La requête énumère chaque colonne attendue qui manque dans la table.
Exécutez du SQL dans votre navigateur
Essayez : les colonnes requises sont-elles présentes?
Exécutez la requête. L’encadré affiche « The statement ran and returned no rows » (l’instruction s’est exécutée sans retourner de lignes) : chaque colonne attendue existe. Puis remplacez 'channel' par 'contact_channel' et exécutez-la de nouveau. Vous devriez voir une ligne, contact_channel. Dans la dernière leçon de ce module, une nouvelle exportation renomme channel ainsi.
Tab ajoute quatre espaces. Appuyez sur Esc, puis sur Tab, pour quitter la zone. Ctrl+Enter exécute la requête. Chaque exécution part d’une nouvelle copie de la base de données d’exercice.
Clé unique. Une clé primaire est une colonne dont la valeur diffère dans chaque ligne, ce qui permet d’identifier une ligne. L’exportation n’en garantit pas, donc vous devez la vérifier.
Faites d’abord une prédiction : combien d’identifiants de billets apparaissent plus d’une fois? Vous les avez trouvés au module 1.
Exécutez du SQL dans votre navigateur
Essayez : ticket_id est-il unique?
Exécutez la requête. Vous devriez voir 14 lignes, de T-10031 à T-11291, chacune avec 2 copies. Ces lignes sont des doublons exacts causés par le chevauchement des pages d’exportation. clean.py les supprime.
Tab ajoute quatre espaces. Appuyez sur Esc, puis sur Tab, pour quitter la zone. Ctrl+Enter exécute la requête. Chaque exécution part d’une nouvelle copie de la base de données d’exercice.
Valeurs permises. La requête suivante semble correcte, mais elle contient une erreur silencieuse. Faites une prédiction : combien de groupes retourne-t-elle, et le canal manquant en fait-il partie?
Exécutez du SQL dans votre navigateur
À vous : détectez chaque canal non permis
Exécutez la requête. Vous voyez 4 groupes : chat, EMAIL, Email et webform. Les 38 lignes sans canal n’y sont pas. Pour un canal NULL, channel NOT IN (...) n’est pas vrai, mais inconnu, donc WHERE exclut la ligne. Modifiez la requête pour qu’elle retourne aussi le groupe NULL. Le contrôle compare votre résultat au nôtre.
Tab ajoute quatre espaces. Appuyez sur Esc, puis sur Tab, pour quitter la zone. Ctrl+Enter exécute la requête. Chaque exécution part d’une nouvelle copie de la base de données d’exercice.
Afficher la réponse
SELECT channel, COUNT(*) AS nFROM ticketsWHERE channel IS NULL OR channel NOT IN ('email', 'chat', 'phone', 'web_form')GROUP BY channelORDER BY n DESC;Cinq groupes : NULL 38, Email 20, chat 8, EMAIL 7, webform 5. Cela donne 78 lignes. Une règle sur les valeurs permises doit préciser comment elle traite une valeur manquante. Ici, les valeurs manquantes ne sont pas permises.
Plages. Une règle de plage donne la plus petite et la plus grande valeur permise. Grace dit qu’une commande de plus de 2 000 $ est très rare chez Larkfield. Une commande négative est impossible. Faites une prédiction : quels deux problèmes du module 4 cette règle trouve-t-elle?
Exécutez du SQL dans votre navigateur
Essayez : valeurs de commande hors de la plage de 0 à 2 000
Exécutez la requête. Vous devriez voir 36 lignes. Les 11 premières ont -1, la valeur sentinelle qui signifie « inconnu ». Les 25 autres sont des billets de janvier soumis par formulaire Web, avec des montants en cents. Le dernier, T-10147, est aussi le billet dont l’année est 2036.
Tab ajoute quatre espaces. Appuyez sur Esc, puis sur Tab, pour quitter la zone. Ctrl+Enter exécute la requête. Chaque exécution part d’une nouvelle copie de la base de données d’exercice.
Exécutez du SQL dans votre navigateur
À vous : une règle de plage pour word_count
Le contrat dit : word_count va de 0 à 10 000. Zéro est permis, car un message peut contenir seulement une pièce jointe. Écrivez une requête qui retourne ticket_id et word_count pour chaque ligne qui enfreint cette règle. Le contrôle compare votre résultat au nôtre.
Tab ajoute quatre espaces. Appuyez sur Esc, puis sur Tab, pour quitter la zone. Ctrl+Enter exécute la requête. Chaque exécution part d’une nouvelle copie de la base de données d’exercice.
Afficher la réponse
SELECT ticket_id, word_countFROM ticketsWHERE word_count < 0 OR word_count > 10000;Une ligne : T-10361 avec 48 210 mots, le fichier de journaux (logs) collé dans le message. clean.py la met en quarantaine avec la raison word_count_above_10000.
Sur votre ordinateur : toutes les règles dans un rapport
Section intitulée « Sur votre ordinateur : toutes les règles dans un rapport »Sur votre ordinateur, vous pouvez réunir les règles dans une requête qui compte les problèmes pour chacune. UNION ALL place les résultats de plusieurs instructions SELECT les uns sous les autres. Enregistrez ceci dans queries/60-contract-checks.sql :
-- Each rule counts the rows that break it. A rule passes with 0.SELECT 'ticket_id is unique' AS rule, (SELECT COUNT(*) FROM (SELECT ticket_id FROM tickets GROUP BY ticket_id HAVING COUNT(*) > 1)) AS problemsUNION ALLSELECT 'required values are present', COUNT(*)FROM ticketsWHERE ticket_id IS NULL OR created_at IS NULL OR team IS NULL OR priority IS NULLUNION ALLSELECT 'channel has an allowed value', COUNT(*)FROM ticketsWHERE channel IS NULL OR channel NOT IN ('email', 'chat', 'phone', 'web_form')UNION ALLSELECT 'order_value is 0 to 2000', COUNT(*)FROM ticketsWHERE order_value < 0 OR order_value > 2000UNION ALLSELECT 'word_count is 0 to 10000', COUNT(*)FROM ticketsWHERE word_count < 0 OR word_count > 10000UNION ALLSELECT 'created_at is in the first half of 2026', COUNT(*)FROM ticketsWHERE created_at < '2026-01-01' OR created_at >= '2026-07-01';rule problems--------------------------------------- --------ticket_id is unique 14required values are present 0channel has an allowed value 78order_value is 0 to 2000 36word_count is 0 to 10000 1created_at is in the first half of 2026 1
6 rowsLa première ligne compte les identifiants de billets, les autres comptent les lignes. Une règle est respectée dans l’exportation brute : aucun billet n’a d’identifiant, d’heure, d’équipe ou de priorité manquants. Les cinq autres trouvent les problèmes que les modules 3 à 5 traitent. C’est le but : les règles décrivent les données après le pipeline.
Le contrat dans validate.py
Section intitulée « Le contrat dans validate.py »Les règles SQL vérifient la table brute dans la base de données. Priya reçoit des fichiers CSV. Mei réécrit donc le contrat en Python pour les fichiers dans data/model/. Le script est scripts/validate.py. Il utilise seulement pandas, sans bibliothèque supplémentaire. Il comporte quatre parties.
Partie 1 : le contrat sous forme de données
Section intitulée « Partie 1 : le contrat sous forme de données »Les règles sont au début du fichier, sous forme de simples valeurs Python. N’importe qui peut les lire sans lire le code. Modifier le contrat revient à modifier ces lignes.
"""Check the delivered dataset against its contract: the rules that Priya canrely on in data/model/. Prints PASS or FAIL for each rule, writesreports/validation.json, and exits with code 1 if any rule fails."""
import jsonimport sysfrom pathlib import Path
import pandas as pd
MODEL = Path("data/model")REPORT = Path("reports/validation.json")TIME_FORMAT = "%Y-%m-%d %H:%M:%S"SPLITS = ["train", "valid", "test"]
# ---------------------------------------------------------------- the contract
# Every column, its type, and whether a value is required.COLUMNS = { "ticket_id": ("text", True), "customer_id": ("text", False), # guests have no customer "created_at": ("datetime", True), "channel": ("text", True), "team": ("text", True), "priority": ("integer", True), "segment": ("text", True), "region": ("text", True), "order_value": ("number", False), # no order (account) or unknown "word_count": ("integer", True), "customer_tenure_days": ("number", False), # guests and deleted customers "prior_tickets_90d": ("integer", True), "created_hour": ("integer", True), "escalated_72h": ("integer", True), # the target}# Known only after the ticket is created: never in the dataset.NEVER = ["priority_now", "first_reply_minutes", "closed_at"]ALLOWED = { "channel": {"email", "chat", "phone", "web_form", "unknown"}, "team": {"delivery", "returns", "payment", "warranty", "account"}, "segment": {"home", "trade", "business", "guest", "unknown"}, "region": {"north", "south", "east", "west", "unknown"},}RANGES = { # inclusive; None means no limit "priority": (1, 3), "order_value": (0, 2000), "word_count": (0, 10_000), "customer_tenure_days": (0, None), "prior_tickets_90d": (0, None), "created_hour": (0, 23), # From the start of the selection to the snapshot minus 72 hours. "created_at": (pd.Timestamp("2026-01-01"), pd.Timestamp("2026-06-28 06:00:00")),}ESCALATION_RATE = (0.05, 0.30)Chaque valeur vient d’une décision prise dans un module précédent :
| Règle | Pourquoi cette valeur |
|---|---|
order_value et customer_tenure_days peuvent manquer |
Les billets liés aux comptes n’ont pas de commande, et le −1 du système de commandes est maintenant une valeur manquante (module 4). Les invités et les clients supprimés n’ont pas de date d’inscription. |
channel permet unknown |
clean.py transforme un canal manquant en une catégorie distincte, unknown (inconnu). |
NEVER |
Ces colonnes sont remplies après la création du billet. Elles révèlent la réponse (module 5). |
created_at jusqu’au 28 juin 2026 à 6 h |
Un billet doit avoir ses 72 heures complètes avant l’instantané, sinon sa cible n’est pas définitive. |
| Taux de transfert de 5 % à 30 % | L’exportation a un taux de 13,7 %. Grace s’attend à des variations selon les changements de politique, mais pas d’un facteur de 3. Un taux hors de cette plage indique un changement du code de la cible, pas du service de soutien. |
Partie 2 : petites fonctions utilitaires
Section intitulée « Partie 2 : petites fonctions utilitaires »# ---------------------------------------------------------------- helpers
results = []
def rule(name, problems): """Record one rule. An empty list of problems means the rule passes.""" results.append({"rule": name, "passed": not problems, "problems": problems}) print(f"{'PASS' if not problems else 'FAIL'} {name}") for problem in problems: print(f" {problem}")
def first(df, mask, column): """The first bad row, as an example: its ticket ID and its value.""" row = df[mask].iloc[0] value = row[column] shown = repr(value) if isinstance(value, str) else value return f"first: {row['ticket_id']}, {shown}"
def n(count, word): """'1 row', '3 rows'.""" return f"{count} {word}" + ("" if count == 1 else "s")
def load(path): """Read every value as text. Types are checked by the rules, not assumed.""" return pd.read_csv(path, dtype=str, keep_default_na=False, na_values=[""])
def convert(text, kind): """Convert a text column to its type. A value that cannot be read becomes missing.""" if kind == "text": return text if kind == "datetime": return pd.to_datetime(text, format=TIME_FORMAT, errors="coerce") numbers = pd.to_numeric(text, errors="coerce") if kind == "integer": numbers = numbers.where(numbers % 1 == 0) # 2.5 is not an integer return numbersrule() est au cœur du script. Chaque contrôle crée une liste de problèmes, et rule() affiche PASS (réussite) ou FAIL (échec), puis les problèmes en dessous. Chaque message indique la colonne, le nombre de lignes et le premier billet incorrect pour vous aider à le trouver.
load() lit chaque valeur comme du texte. Pourquoi ne pas laisser pandas choisir les types? Parce que pandas choisit sans vous avertir. Si une valeur dans order_value est $10.85, pandas lit toute la colonne comme du texte, sans avertissement. convert() essaie ensuite chaque valeur. Une valeur présente mais impossible à convertir constitue un problème de type, que la partie suivante compte.
Partie 3 : règles pour la table de modélisation
Section intitulée « Partie 3 : règles pour la table de modélisation »# ---------------------------------------------------------------- the rules
def check_table(raw): """Rules for the modelling table, one row per ticket. Returns the typed table.""" missing = [c for c in COLUMNS if c not in raw.columns] leaky = [c for c in raw.columns if c in NEVER] extra = [c for c in raw.columns if c not in COLUMNS and c not in NEVER] rule("required columns are present", [f"missing column: {c}" for c in missing]) rule("no column that is known only after creation", [f"{c} must not be in the dataset" for c in leaky] + [f"unexpected column: {c}" for c in extra]) if missing: return None
df = raw.copy() problems = [] for column, (kind, _) in COLUMNS.items(): df[column] = convert(raw[column], kind) bad = raw[column].notna() & df[column].isna() if bad.any(): problems.append(f"{column}: not a valid {kind} in {n(bad.sum(), 'row')} ({first(raw, bad, column)})") rule("every value has its column's type", problems)
problems = [] for column, (_, required) in COLUMNS.items(): if required and raw[column].isna().any(): problems.append(f"{column}: missing in {n(raw[column].isna().sum(), 'row')}") rule("no missing values in required columns", problems)
repeated = df["ticket_id"].duplicated(keep=False) rule("ticket_id is unique", [f"{n(df.loc[repeated, 'ticket_id'].nunique(), 'ticket ID')} repeated " f"(first: {df.loc[repeated, 'ticket_id'].iloc[0]})"] if repeated.any() else [])
problems = [] for column, allowed in ALLOWED.items(): bad = df[column].notna() & ~df[column].isin(allowed) if bad.any(): values = sorted(df.loc[bad, column].unique()) problems.append(f"{column}: values not allowed in {n(bad.sum(), 'row')}: {values}") rule("categories use allowed values only", problems)
problems = [] for column, (low, high) in RANGES.items(): values = df[column] bad = pd.Series(False, index=df.index) if low is not None: bad |= values < low if high is not None: bad |= values > high if bad.any(): problems.append(f"{column}: outside {low} to {high} in {n(bad.sum(), 'row')} ({first(df, bad, column)})") rule("numbers and dates are in their allowed ranges", problems)
bad = ~df["escalated_72h"].isin([0, 1]) rule("the target escalated_72h is 0 or 1", [f"not 0 or 1 in {n(bad.sum(), 'row')} ({first(df, bad, 'escalated_72h')})"] if bad.any() else []) return dfChaque bloc est une règle, et chaque règle trouve d’abord les lignes incorrectes, comme les règles SQL. Deux détails :
duplicated(keep=False)marque chaque copie d’un identifiant répété, pas seulement la deuxième.- La règle des valeurs permises ignore les valeurs manquantes (
notna()). Une valeur manquante relève de la règle des valeurs requises. Ainsi, chaque problème est signalé une seule fois, par la règle qui l’explique le mieux.
Partie 4 : règles pour les séparations et résultat
Section intitulée « Partie 4 : règles pour les séparations et résultat »def rate_problem(name, target): """The escalation rate must be in the expected range, or something upstream changed.""" low, high = ESCALATION_RATE rate = pd.to_numeric(target).mean() if not low <= rate <= high: return [f"{name}: escalation rate {rate:.1%} is outside {low:.0%} to {high:.0%}"] return []
def check_splits(table, parts): """Rules for train, valid and test.""" empty = [name for name, part in parts.items() if len(part) == 0] rule("every split has rows", [f"{name} has no rows" for name in empty]) if empty: return
problems = [] for name, part in parts.items(): if list(part.columns) != list(table.columns): problems.append(f"{name}: the columns are not the same as in tickets_model.csv") together = pd.concat(parts.values()) if len(together) != len(table): problems.append(f"the splits have {len(together)} rows together; tickets_model.csv has {len(table)}") if set(together["ticket_id"]) != set(table["ticket_id"]): problems.append("the splits do not contain the same tickets as tickets_model.csv") rule("together, the splits are exactly the modelling table", problems)
problems = [] for i, a in enumerate(SPLITS): for b in SPLITS[i + 1:]: shared = set(parts[a]["ticket_id"]) & set(parts[b]["ticket_id"]) if shared: problems.append(f"{a} and {b} share {n(len(shared), 'ticket ID')} (first: {sorted(shared)[0]})") rule("no ticket is in two splits", problems)
problems = [] for a, b in zip(SPLITS, SPLITS[1:]): last = pd.to_datetime(parts[a]["created_at"], format=TIME_FORMAT).max() start = pd.to_datetime(parts[b]["created_at"], format=TIME_FORMAT).min() if not last < start: problems.append(f"{a} ends at {last}, which is not before the start of {b} at {start}") rule("the splits are in time order: train, then valid, then test", problems)
problems = rate_problem("all rows", table["escalated_72h"]) for name, part in parts.items(): problems += rate_problem(name, part["escalated_72h"]) rule(f"escalation rate between {ESCALATION_RATE[0]:.0%} and {ESCALATION_RATE[1]:.0%}", problems)
def main(): files = ["tickets_model.csv"] + [f"{name}.csv" for name in SPLITS] missing = [f for f in files if not (MODEL / f).exists()] if missing: print(f"FAIL missing file(s) in {MODEL}: {', '.join(missing)}") print("Run build_features.py and split.py first.") sys.exit(1)
print(f"Checking {MODEL} against the contract") table = check_table(load(MODEL / "tickets_model.csv")) if table is not None: check_splits(table, {name: load(MODEL / f"{name}.csv") for name in SPLITS})
failed = [r for r in results if not r["passed"]] REPORT.parent.mkdir(exist_ok=True) REPORT.write_text(json.dumps({"files": files, "passed": not failed, "rules": results}, indent=2) + "\n", encoding="utf-8") if failed: print(f"{len(failed)} of {len(results)} rules failed. Fix the step that made the data, then run it again.") sys.exit(1) print(f"All {len(results)} rules passed.")
if __name__ == "__main__": main()Les règles de séparation protègent les décisions du module 5. Un billet dans deux ensembles crée une fuite : le modèle voit un billet de test pendant l’entraînement. Des ensembles hors de l’ordre temporel mêlent le futur aux données d’entraînement. La règle du taux s’applique à chaque ensemble, car un ensemble incorrect peut se cacher dans un total correct.
sys.exit(1) termine le script avec le code de sortie 1. Chaque programme retourne un code de sortie au terminal quand il se termine : 0 signifie réussite, tout autre nombre signifie échec. Les autres programmes lisent le code, pas le texte. Dans la dernière leçon de ce module, run_pipeline.py l’utilise pour s’arrêter.
Mei l’exécute sur les fichiers du module 5 :
Checking data/model against the contractPASS required columns are presentPASS no column that is known only after creationPASS every value has its column's typePASS no missing values in required columnsPASS ticket_id is uniquePASS categories use allowed values onlyPASS numbers and dates are in their allowed rangesPASS the target escalated_72h is 0 or 1PASS every split has rowsPASS together, the splits are exactly the modelling tablePASS no ticket is in two splitsPASS the splits are in time order: train, then valid, then testPASS escalation rate between 5% and 30%All 13 rules passed.Le script enregistre aussi les mêmes résultats dans reports/validation.json. Ce fichier est le rapport de qualité que Priya reçoit avec les données.
Ce que la règle de distribution détecte
Section intitulée « Ce que la règle de distribution détecte »Les 13 règles sont respectées, mais fonctionnent-elles vraiment? Une règle qui n’échoue jamais ne prouve rien. Mei teste la règle de distribution avec un vrai bogue. Dans build_features.py, elle tape minutes au lieu de hours :
WINDOW = pd.Timedelta(minutes=72)build_features.py s’exécute sans erreur. Puis elle exécute split.py et validate.py. Deux règles échouent :
FAIL numbers and dates are in their allowed ranges created_at: outside 2026-01-01 00:00:00 to 2026-06-28 06:00:00 in 29 rows (first: T-11374, 2026-06-28 07:14:18)...FAIL escalation rate between 5% and 30% all rows: escalation rate 0.8% is outside 5% to 30% train: escalation rate 0.5% is outside 5% to 30% valid: escalation rate 1.8% is outside 5% to 30% test: escalation rate 0.8% is outside 5% to 30%2 of 13 rules failed. Fix the step that made the data, then run it again.Chaque ligne reste valide : chaque cible vaut 0 ou 1. Mais seulement 11 billets sur 1 398 sont maintenant transférés. Peu de billets atteignent un agent supérieur en 72 minutes. Le bogue a aussi déplacé la limite pour une cible définitive à 72 minutes avant l’instantané. Les 29 billets trop récents sont donc revenus. Sans les règles, Priya entraîne un modèle sur la mauvaise cible, et ses scores semblent normaux. Mei remet hours=72 dans la ligne.
Exercice guidé : exécuter le contrat, puis l’enfreindre
Section intitulée « Exercice guidé : exécuter le contrat, puis l’enfreindre »-
Créez
scripts/validate.pyavec les quatre parties ci-dessus, dans l’ordre, dans un seul fichier. -
Dans le terminal, allez dans le dossier de votre projet et vérifiez que l’environnement est actif.
-
Exécutez le script :
Fenêtre de terminal python scripts/validate.pyVous devriez voir 13 lignes
PASSetAll 13 rules passed.(les 13 règles sont respectées). -
Lisez le code de sortie. Il vaut 0.
Terminal echo $?PowerShell $LASTEXITCODE -
Introduisez volontairement une erreur. Cette commande ajoute la dernière ligne d’entraînement à la fin de
test.csv:Fenêtre de terminal python -c "import pandas as pd; train = pd.read_csv('data/model/train.csv'); train.tail(1).to_csv('data/model/test.csv', mode='a', header=False, index=False)" -
Prédisez quelles règles échoueront. Puis exécutez de nouveau
python scripts/validate.py. Vous devriez voir :FAIL together, the splits are exactly the modelling tablethe splits have 1370 rows together; tickets_model.csv has 1369FAIL no ticket is in two splitstrain and test share 1 ticket ID (first: T-10929)FAIL the splits are in time order: train, then valid, then testvalid ends at 2026-05-31 20:57:12, which is not before the start of test at 2026-04-30 23:54:47PASS escalation rate between 5% and 30%3 of 13 rules failed. Fix the step that made the data, then run it again. -
Relisez le code de sortie, comme à l’étape 4. Il vaut 1.
-
Réparez les fichiers. Exécutez l’étape qui les a créés :
python scripts/split.py. -
Exécutez de nouveau
python scripts/validate.py. Les 13 règles sont respectées.
Une seule ligne incorrecte a enfreint trois règles, et chaque message la signale sous un angle différent. La règle du taux reste respectée : une ligne change peu un taux. Voilà pourquoi un contrat a besoin de règles sur les lignes et de règles de distribution.
À vous de jouer : une règle sur la taille des données d’évaluation
Section intitulée « À vous de jouer : une règle sur la taille des données d’évaluation »Priya ajoute une demande : « Avec moins de 30 billets de soutien escaladés, un seul billet peut beaucoup changer le score de validation ou de test. Je veux au moins 30 billets escaladés dans chacun de ces ensembles. »
- Ajoutez une constante
MIN_ESCALATED = 30dans la partie du contrat. - Ajoutez une règle à la fin de
check_splits()qui compte les billets escaladés dans valid et dans test. - Prédisez : la règle réussit-elle? Puis exécutez le script.
- Changez la valeur pour 40 et prédisez de nouveau.
Afficher la réponse
MIN_ESCALATED = 30 problems = [] for name in ["valid", "test"]: escalated = int(pd.to_numeric(parts[name]["escalated_72h"]).sum()) if escalated < MIN_ESCALATED: problems.append(f"{name}: {escalated} escalated tickets; at least {MIN_ESCALATED} are needed") rule(f"valid and test have at least {MIN_ESCALATED} escalated tickets each", problems)Avec 30, la règle réussit : valid contient 45 billets escaladés, et test en contient 35. La dernière ligne est All 14 rules passed. (les 14 règles ont réussi). Avec 40, elle échoue :
FAIL valid and test have at least 40 escalated tickets each test: 35 escalated tickets; at least 40 are needed1 of 14 rules failed. Fix the step that made the data, then run it again.Les données ne sont pas incorrectes ici : les données de test sont peu nombreuses. Priya et Mei doivent décider de la solution, par exemple une période de test plus longue. Une règle peut échouer parce que les données ne répondent pas à un besoin. C’est utile aussi : elle révèle le problème avant que quelqu’un se fie à un score.
Erreur courante
Une règle qui ne peut pas échouer
Symptôme : la règle sur les canaux autorisés réussit sur l’exportation brute, où vous savez que Email (courriel) et webform (formulaire Web) existent.
Cause : les valeurs autorisées viennent des données que la règle vérifie. Par exemple :
import pandas as pd
df = pd.read_csv("data/extract/tickets.csv", dtype=str, keep_default_na=False, na_values=[""])allowed = set(df["channel"].dropna()) # the mistake: the rule comes from the dataprint(sorted(allowed))print("rows that break the rule:", (~df["channel"].dropna().isin(allowed)).sum())[' chat', 'EMAIL', 'Email', 'chat', 'email', 'phone', 'web_form', 'webform']rows that break the rule: 0Chaque valeur dans les données est « autorisée », parce que la liste est créée à partir des données. La règle ne peut jamais échouer.
Solution :
- Écrivez les valeurs autorisées à la main, selon l’entente avec les personnes qui connaissent les données. Grace connaît les quatre canaux.
- Testez chaque nouvelle règle une fois sur des données qui doivent la faire échouer, comme dans l’exercice guidé. Si elle n’échoue pas, elle ne fonctionne pas.
contract = {"email", "chat", "phone", "web_form"} # the fix: the rule comes from the agreementprint("rows that break the rule:", (~df["channel"].dropna().isin(contract)).sum())rows that break the rule: 40Dans le travail en IA : une vérification qui réussit toujours est pire qu’aucune vérification, parce que les gens s’y fient. Une nouvelle catégorie en production, par exemple un canal social (réseaux sociaux), atteint alors le modèle sans que personne le remarque.
Vérifiez votre compréhension
Pourquoi une règle retourne-t-elle les lignes qui l'enfreignent, et pas seulement vrai ou faux?
Afficher la réponse
Parce que les lignes vous indiquent quoi corriger. « Faux » indique qu’il y a un problème. « 36 lignes, la première est T-10046 avec −1 » indique le problème et où chercher. Le nombre montre aussi l’ampleur du problème. Une seule ligne est souvent une erreur de frappe. Avec 25 lignes, c’est souvent un problème du système, comme les cents de l’ancien formulaire Web.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Prédisez : Tomás remet priority_now dans tickets_model.csv, parce que « ça fonctionne tellement bien ». Quelle règle échoue, et que dit le message?
Afficher la réponse
La règle « aucune colonne connue seulement après la création » échoue avec priority_now must not be in the dataset (priority_now ne doit pas être dans le jeu de données). La colonne figure dans NEVER. Si les ensembles séparés n’ont pas cette colonne non plus, une deuxième règle échoue : « ensemble, les ensembles séparés correspondent exactement à la table de modélisation ». Les colonnes ne sont pas les mêmes. Le contrat transforme la décision du module 5 en vérification. Personne n’a besoin de s’en souvenir.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Vérifiez votre compréhension
Le taux d'escalade est de 11,7 % dans train et de 19,8 % dans valid. Omar propose une règle : chaque ensemble doit rester à moins de 3 points de pourcentage du taux global de 13,7 %. Que se passe-t-il, et est-ce une bonne règle?
Afficher la réponse
Valid échoue : 19,8 % dépasse 13,7 % de 6,1 points. Mais les données sont correctes. Le taux a augmenté après le 1er mai 2026 à cause de la nouvelle politique de garantie. Une séparation temporelle montre volontairement ce changement. Une règle de distribution doit décrire ce qui est normal, selon les personnes qui connaissent les données, comme Grace. Une règle qui échoue sur des données correctes apprend aux gens à ignorer les échecs. La plage de 5 % à 30 % détecte les bogues et permet les changements réels.
Votre réponse contient-elle les points principaux de la nôtre ?
Connectez-vous et inscrivez-vous au cours pour enregistrer vos réponses. Chaque vérification compte pour le certificat.
Récapitulatif
- Un schéma énumère les colonnes et leurs types. Un contrat de données ajoute des règles sur les valeurs. C’est une entente entre la personne qui crée le jeu de données et celles qui l’utilisent.
- Quatre types de règles : valeurs obligatoires, valeurs et plages autorisées, clés uniques et distributions attendues.
- Écrivez chaque règle comme une vérification qui retourne les lignes qui l’enfreignent. Elle réussit avec 0 ligne.
- En SQL,
NOT INexclut les lignes NULL. Précisez comment une règle traite les valeurs manquantes. validate.pylit chaque valeur comme du texte, vérifie 13 règles, écritreports/validation.jsonet termine avec le code 1 en cas d’échec.- Testez chaque règle sur des données qui doivent la faire échouer. Une règle qui ne peut pas échouer est pire qu’aucune règle.
Ensuite, vous noterez la provenance des données et les transformations subies en chemin. Priya pourra ainsi s’y fier et reproduire le processus.
Leçon suivante : Suivre la provenance des données