IAMaîtriser l'IA générative Plan du corpus

04 — Qualité des données

L'IA est très forte pour proposer 30 contrôles de qualité en deux minutes. C'est toi qui décides lesquels ont un sens et à quel seuil ils alertent.

Temps de lecture : 17 min | Niveau : Intermédiaire

Ce que tu sauras faire après

  • Faire profiler un jeu de données et lire le profil sans te faire raconter d'histoires.
  • Faire générer des règles de contrôle à partir de règles métier écrites en français.
  • Écrire des tests dbt et des Expectations Great Expectations sans clé inventée.
  • Distinguer une anomalie réelle d'un artefact de collecte.
  • Rédiger un rapport d'incident qualité lisible par le métier.

1. La règle de base : l'IA ne voit pas tes données

Un modèle de langage ne lit pas ta table. Il lit ce que tu lui colles. Donc :

  • il ne peut pas détecter une anomalie tout seul ;
  • il peut très bien écrire le code qui la détecte ;
  • il peut interpréter un profil que tu lui fournis.

Tout le travail consiste donc à lui donner de la matière : un profil, un échantillon anonymisé, des règles métier. Pas « regarde si mes données sont bonnes ».


2. Profiler un dataset

Profiler, c'est produire la carte d'identité chiffrée d'une table : volumétrie, types, taux de valeurs manquantes, cardinalités, valeurs extrêmes.

En pandas, les méthodes de base documentées :

df.head()        # premières lignes
df.tail(3)       # dernières lignes
df.describe()    # résumé statistique rapide
df.dtypes        # types des colonnes
df.columns       # noms des colonnes
df.index         # index
pd.isna(df)      # masque booléen des valeurs manquantes

En SQL, le profil minimal d'une table tient en une requête :

SELECT
  COUNT(*)                                      AS nb_lignes,
  COUNT(DISTINCT commande_id)                   AS nb_commandes_distinctes,
  COUNTIF(commande_id IS NULL)                  AS nb_id_null,
  COUNTIF(montant_ttc IS NULL)                  AS nb_montant_null,
  MIN(date_commande)                            AS date_min,
  MAX(date_commande)                            AS date_max,
  MIN(montant_ttc)                              AS montant_min,
  MAX(montant_ttc)                              AS montant_max,
  APPROX_QUANTILES(montant_ttc, 100)[OFFSET(50)] AS montant_median
FROM analytics.fct_commandes
WHERE date_commande BETWEEN '2026-08-01' AND '2026-08-31';

Le résultat de cette requête, lui, tu le colles dans le prompt. À ce moment-là, le modèle a quelque chose de réel à interpréter.

AVANT / APRÈS

AVANT :

Est-ce que mes données de commandes sont propres ?

Le modèle n'a rien. Il répondra par des généralités : « vérifiez les doublons, les valeurs nulles… ». Inutile.

APRÈS :

Voici le profil d'une table de commandes sur août 2026.

<profil>
nb_lignes: 1 482 903
nb_commandes_distinctes: 1 482 110
nb_id_null: 0
nb_montant_null: 12 447
date_min: 2026-08-01
date_max: 2026-08-31
montant_min: -1890.00
montant_max: 428000.00
montant_median: 78.40
</profil>

<contexte_metier>
Table : commandes e-commerce, une ligne = une commande.
Panier moyen attendu : entre 60 et 120 euros.
Les avoirs sont censés être dans une table séparée.
</contexte_metier>

Liste les anomalies que ce profil révèle, classées par gravité.
Pour chacune : le constat chiffré, l'hypothèse la plus probable,
et la requête SQL exacte qui permet de trancher.
N'affirme rien que le profil ne montre pas.

Pourquoi c'est mieux : le modèle repère nb_lignes > nb_commandes_distinctes (793 doublons), un montant_min négatif alors que les avoirs sont censés être ailleurs, un montant_max à 428 000 euros face à une médiane de 78 euros, et 12 447 montants nuls. Quatre pistes concrètes, chacune vérifiable par une requête.


3. Traduire une règle métier en test

C'est l'usage le plus rentable au quotidien. Tu écris la règle en français, le modèle te rend le test.

Règle métier en françaisTraduction en test
« Une commande ne peut pas avoir deux lignes avec le même identifiant »unique sur commande_id
« Un montant est toujours renseigné »not_null sur montant_ttc
« Le statut fait partie d'une liste fermée »accepted_values
« Tout client d'une commande existe dans la table clients »relationships
« Le CA d'un jour ne varie jamais de plus de 50 % par rapport à la veille »test singulier en SQL

Les quatre premiers sont des tests génériques dbt, livrés d'origine. Le cinquième n'existe pas d'origine : c'est un test singulier, un fichier .sql dans le dossier tests/ qui renvoie les lignes fautives. La doc dbt le définit ainsi : un test passe si la requête ne renvoie aucune ligne.

-- tests/assert_variation_ca_journalier_raisonnable.sql
with ca_jour as (
    select
        date_commande,
        sum(montant_ttc) as ca
    from {{ ref('fct_commandes') }}
    group by 1
),

avec_veille as (
    select
        date_commande,
        ca,
        lag(ca) over (order by date_commande) as ca_veille
    from ca_jour
)

select
    date_commande,
    ca,
    ca_veille
from avec_veille
where ca_veille is not null
  and abs(ca - ca_veille) > 0.5 * ca_veille

Détail de la doc dbt à ne pas oublier : dans un fichier de test singulier, on omet le point-virgule final. La doc en donne la raison : « Omit semicolons (;) at the end of the SQL statement in your singular test files, as they can cause your data test to fail. » Un point-virgule oublié et ton test tombe en erreur sans que la donnée n'ait rien à se reprocher.


4. Écrire les tests dbt

Rappel de la syntaxe vérifiée dans la doc dbt (voir aussi la fiche 03-dbt-et-modelisation) :

models:
  - name: fct_commandes
    columns:
      - name: commande_id
        data_tests:
          - unique
          - not_null
      - name: statut
        data_tests:
          - accepted_values:
              arguments:
                values: ['placed', 'shipped', 'completed', 'returned']
      - name: client_id
        data_tests:
          - relationships:
              arguments:
                to: ref('dim_clients')
                field: client_id

Choisir entre alerte et blocage

Un test dbt peut bloquer la chaîne (error) ou simplement avertir (warn). C'est le réglage severity :

        data_tests:
          - unique:
              config:
                severity: error   # ou 'warn'

La doc mentionne aussi error_if et warn_if pour des seuils personnalisés. La règle de bon sens :

  • error pour tout ce qui casse un calcul en aval : clé primaire dupliquée, clé étrangère orpheline.
  • warn pour ce qui est suspect mais pas bloquant : un taux de valeurs manquantes qui monte.

Un projet où tous les tests sont en error finit par être désactivé en entier au premier incident. Un projet où tout est en warn n'alerte plus personne. Dose.

Garder les lignes fautives

Le drapeau --store-failures, ou la configuration correspondante, enregistre les enregistrements en échec dans une table :

data_tests:
  +store_failures: true

Les résultats sont stockés par défaut dans un schéma suffixé dbt_test__audit, et chaque exécution remplace les échecs précédents. C'est ce qui te permet de passer de « le test est rouge » à « voici les 12 lignes coupables ».

Lancer les tests

dbt test                                 # tous les tests
dbt test --select customers              # un modèle
dbt test --select "source:*"             # toutes les sources
dbt test --select "test_type:data"       # tous les tests de données
dbt test --select "test_type:generic"    # les tests génériques seulement
dbt test --select "test_type:singular"   # les tests singuliers seulement
dbt test --select "test_type:unit"       # les tests unitaires seulement

test_type:data couvre à la fois les génériques et les singuliers. C'est le sélecteur large ; les trois autres servent à isoler une famille quand une suite devient longue.

La fraîcheur des sources

Une donnée juste mais vieille de trois jours est une donnée fausse pour le métier. dbt gère ça à part, avec freshness :

sources:
  - name: jaffle_shop
    database: raw
    config:
      freshness:
        warn_after: {count: 12, period: hour}
        error_after: {count: 24, period: hour}
      loaded_at_field: _etl_loaded_at
    tables:
      - name: orders
        config:
          freshness:
            warn_after: {count: 6, period: hour}
            error_after: {count: 12, period: hour}

Les clés exactes : freshness, warn_after, error_after, loaded_at_field, count, period. Le réglage au niveau de la table l'emporte sur celui de la source. Pour désactiver le contrôle sur une table précise, la doc utilise freshness: null.

Lancer le contrôle :

dbt freshness --resource-type source   # les sources seulement
dbt freshness                          # sources et modèles

Point de version. Ces deux commandes sont celles documentées pour dbt v2.0 et au-delà. Sur une version antérieure, la commande historique est dbt source freshness. Vérifie ce que ta version accepte avant de l'écrire dans un DAG. Dans les deux cas, les résultats partent dans target/freshness.json, et dans target/sources.json pour compatibilité quand des sources sont incluses.


5. Great Expectations : quand dbt ne suffit pas

Great Expectations (GX) sert quand tu veux contrôler des données avant qu'elles n'entrent dans l'entrepôt, ou dans un DataFrame pandas au milieu d'un pipeline Python. dbt teste ce qui est déjà dans la base ; GX teste ce qui est en transit.

La documentation GX Core (version 1.23.2 au moment de la consultation) donne cet enchaînement :

import great_expectations as gx
import pandas as pd

# df est ton DataFrame, celui qui circule dans ton pipeline.
df = pd.read_csv("commandes_du_jour.csv")

context = gx.get_context()

data_source = context.data_sources.add_pandas("pandas")
data_asset = data_source.add_dataframe_asset(name="pd dataframe asset")
batch_definition = data_asset.add_batch_definition_whole_dataframe("batch definition")
batch = batch_definition.get_batch(batch_parameters={"dataframe": df})

expectation = gx.expectations.ExpectColumnValuesToBeBetween(
    column="passenger_count", min_value=1, max_value=6, severity="warning"
)

validation_result = batch.validate(expectation)

Le vocabulaire à retenir, trois mots :

  • Expectation : une attente exprimée sur les données. Par exemple ExpectColumnValuesToBeBetween.
  • Batch : le lot de données à contrôler.
  • batch.validate(expectation) : l'exécution, qui rend le résultat.

Piège à vérifier sur ta version. Un problème signalé de longue date dans les discussions de la communauté GX, et que je n'ai pas retrouvé confirmé dans la documentation officielle : selon les versions, une faute de frappe dans le nom d'une colonne peut produire un succès avec zéro ligne évaluée au lieu d'un échec. Prends l'habitude de contrôler le nombre de lignes évaluées dans le résultat de validation, pas seulement le booléen de succès. Le coût est nul, le bénéfice potentiel est énorme.

Quand tu demandes du code GX à l'IA, donne la version exacte de ta bibliothèque. L'API a beaucoup changé entre les versions 0.x et 1.x : sans cette précision, tu recevras du code d'une ancienne API qui ne s'exécutera pas.


6. Rédiger un rapport d'incident qualité

Quand un chiffre faux est parti chez le métier, tu dois écrire. Et tu dois écrire vite, clairement, sans te défendre. L'IA est très bonne pour structurer ça — à condition que tu lui donnes les faits.

La structure qui fonctionne, en six blocs :

  1. Quoi : quel indicateur, quelle période, quel écart chiffré.
  2. Qui est touché : quels tableaux de bord, quelles décisions déjà prises.
  3. Quand : depuis quand, détecté quand, par qui.
  4. Pourquoi : la cause technique, en une phrase compréhensible par un non-technicien.
  5. Corrigé : ce qui a été fait, et à partir de quand les chiffres sont justes.
  6. Prévention : le test ajouté pour que ça ne se reproduise pas.

Le bloc 6 est celui qui restaure la confiance. Un incident sans test ajouté, c'est un incident qui reviendra.


🧰 Templates à copier-coller

Template A — Profiler et détecter les anomalies

Voici le profil d'une table.

<profil>
[COLLE_LE_RESULTAT_DE_TA_REQUETE_DE_PROFILAGE_OU_DE_df.describe()]
</profil>

<contexte_metier>
Table : [NOM_ET_ROLE_DE_LA_TABLE]
Granularité : une ligne par [GRANULARITE]
Période couverte : [PERIODE]
Ordres de grandeur attendus : [EX_PANIER_MOYEN_ENTRE_60_ET_120_EUROS]
Règles connues : [EX_LES_AVOIRS_SONT_DANS_UNE_AUTRE_TABLE]
</contexte_metier>

Liste les anomalies que ce profil révèle, classées par gravité.
Pour chacune :
- le constat, avec le chiffre exact tiré du profil ;
- l'hypothèse la plus probable ;
- la requête SQL exacte qui permet de trancher ;
- l'impact si l'hypothèse est vraie.

N'affirme rien que le profil ne montre pas. Si tu as besoin d'une mesure
supplémentaire pour conclure, demande-la au lieu de supposer.

Template B — Traduire des règles métier en tests dbt

Version de dbt : [VERSION_EXACTE]

<modele>
[COLLE_LE_SQL_DU_MODELE_OU_SA_LISTE_DE_COLONNES]
</modele>

<regles_metier>
1. [REGLE_EN_FRANCAIS]
2. [REGLE_EN_FRANCAIS]
3. [REGLE_EN_FRANCAIS]
</regles_metier>

Pour chaque règle :
- dis si elle se traduit par un test générique (unique, not_null,
  accepted_values, relationships) ou par un test singulier ;
- écris le code correspondant : bloc YAML sous `data_tests:` avec `arguments:`
  pour les tests paramétrés, ou fichier .sql complet pour un test singulier
  (sans point-virgule final) ;
- propose une severity (error ou warn) ET justifie ce choix en une phrase.

Rends un tableau récapitulatif : | règle | type de test | severity | fichier |
Si une règle n'est pas testable avec les données du modèle, dis-le clairement.

Template C — Écrire des Expectations Great Expectations

Version de great_expectations installée : [VERSION_EXACTE]
Cette précision est impérative : l'API a changé entre les versions majeures.
Si tu n'es pas certain de la syntaxe de cette version, dis-le au lieu de deviner.

Données : DataFrame pandas nommé `df`.
Colonnes (nom | type | contrainte métier) :
[COLLE_LA_LISTE]

Écris le script de validation :
- création du contexte, de la source de données, du batch ;
- une Expectation par contrainte métier listée ;
- la validation et l'affichage du résultat ;
- un contrôle explicite du nombre de lignes évaluées, pas seulement du
  booléen de succès, pour détecter une colonne mal orthographiée ;
- que faire si la validation échoue : [LEVER_UNE_EXCEPTION | JOURNALISER | ROUTER_VERS_TABLE_DE_REJETS].

Template D — Rapport d'incident qualité

Rédige un rapport d'incident qualité de données, en français, pour
[DESTINATAIRE_EX_LE_COMITE_DE_DIRECTION], qui n'est pas technique.

<faits>
- Indicateur touché : [NOM_INDICATEUR]
- Période touchée : [DU_AU]
- Écart constaté : [VALEUR_ERRONEE] au lieu de [VALEUR_CORRIGEE], soit [ECART_%]
- Détecté le : [DATE] par : [QUI_OU_QUEL_TEST]
- Cause technique : [DECRIS_LA_CAUSE_REELLE]
- Tableaux de bord impactés : [LISTE]
- Décisions déjà prises sur ces chiffres : [LISTE_OU_AUCUNE]
- Correction appliquée le : [DATE]
- Test ajouté : [DECRIS_LE_TEST]
</faits>

Structure en six blocs : Quoi / Qui est touché / Quand / Pourquoi /
Corrigé / Prévention.

Ton : factuel, sobre, sans jargon technique, sans justification défensive.
Maximum 400 mots. Commence par l'écart chiffré, pas par le contexte.
N'ajoute aucun fait qui ne soit pas dans le bloc <faits>.

A retenir

  • L'IA ne voit pas tes données : donne-lui un profil chiffré, pas une demande vague.
  • Quatre tests génériques dbt couvrent 80 % des besoins : unique, not_null, accepted_values, relationships ; le reste passe par un test singulier qui renvoie les lignes fautives.
  • Dose severity : tout en error fait désactiver la suite de tests, tout en warn ne réveille personne.
  • --store-failures transforme un test rouge en liste de lignes coupables, stockée dans un schéma dbt_test__audit.
  • La fraîcheur est une dimension de qualité à part entière : warn_after, error_after, loaded_at_field.
  • Pour Great Expectations, donne toujours ta version exacte : l'API a beaucoup bougé entre les versions majeures.

Sources

Corpus personnel de formation · genere le 26/09/2026 · source : 04-qualite-des-donnees.md