Lesson 7 — À l'échelle

Mesurer la précision par champ

5 min read

This content is not translated yet. Showing the original version.

Mesurer la précision par champ

Ce que l'on mesure

Pour chaque champ annoté de chaque document : la valeur extraite est-elle égale à la valeur attendue ? La précision par champ est la part de réponses justes pour ce champ (par exemple date_reunion : 4 justes sur 5 = 80 %).

Une moyenne globale cache tout ; le détail par champ dit quoi corriger :

flowchart LR
  A["jeu_annote.jsonl"] --> C["Pour chaque document et chaque champ annoté"]
  R["resultats.jsonl"] --> C
  C --> P["projeter : ne garder que les clés annotées"]
  P --> N["norm : casse, accents, ponctuation, ordre"]
  N --> E{"égal ?"}
  E --> S["scores par (type, champ)"]
  E -->|"non"| L["liste des erreurs"]
  S --> T["tableau + total"]

Comparer ce qui doit l'être

"Électrotechnique" et "electrotechnique" sont la même compétence ; ["SQL", "Python"] et ["Python", "SQL"] la même liste. Sans normalisation, l'évaluation compterait faux des réponses justes.

files/evaluer.py (extrait)
def norm(v):
    """Rend deux valeurs comparables : casse, accents, ponctuation, ordre des listes."""
    if isinstance(v, str):
        v = unicodedata.normalize("NFKD", v).encode("ascii", "ignore").decode()
        return re.sub(r"[^a-z0-9]", "", v.lower())
    if isinstance(v, list):
        return sorted((norm(x) for x in v), key=repr)
    if isinstance(v, dict):
        return tuple(sorted((k, norm(x)) for k, x in v.items()))
    return v


def projeter(predit, attendu):
    """Ne garde dans les objets prédits que les clés annotées (ex. pas le texte libre `tache`)."""
    if isinstance(attendu, list) and attendu and isinstance(attendu[0], dict) and isinstance(predit, list):
        cles = attendu[0].keys()
        return [{k: p.get(k) for k in cles} for p in predit if isinstance(p, dict)]
    return predit
NormalisationRend égauxNe rend PAS égaux (voulu)
Casse, accentsÉlectrotechnique / electrotechnique
Ponctuation, espaces06 11 22 33 44 / 06.11.22.33.44+33 6… / 06… (format différent)
Ordre des listes[SQL, Python] / [Python, SQL][SQL] / [SQL, Python]
None reste Nonenull / [] / ""

Le calcul

files/evaluer.py (extrait)
def evaluer(annotes: list[dict], resultats: dict[str, dict]):
    """Renvoie {(type, champ): [corrects, total]} et la liste des erreurs."""
    scores, erreurs = defaultdict(lambda: [0, 0]), []
    for doc in annotes:
        donnees = (resultats.get(doc["id"]) or {}).get("donnees") or {}
        for champ, attendu in doc["attendu"].items():
            predit = projeter(donnees.get(champ), attendu)
            ok = norm(predit) == norm(attendu)
            s = scores[(doc["type"], champ)]
            s[0] += ok
            s[1] += 1
            if not ok:
                erreurs.append((doc["id"], champ, attendu, predit))
    return scores, erreurs

Un document absent ou invalide (donnees = null) compte faux sur tous ses champs : un pipeline qui abandonne ne doit pas améliorer son score.

Pourquoi un document dont l'extraction a échoué compte-t-il faux sur tous ses champs, au lieu d'être exclu du calcul ?

Quelle normalisation serait une erreur pour le champ telephone ?

Exercice : une précision par type de document

Ajoutez à main un récapitulatif par type (cv, commande, compte_rendu) : nombre de champs corrects sur le total.

Voir la solution
evaluer.py (ajout dans main, après le tableau)
par_type = defaultdict(lambda: [0, 0])
for (t, _), (ok, n) in scores.items():
    par_type[t][0] += ok
    par_type[t][1] += n
for t, (ok, n) in sorted(par_type.items()):
    print(f"{t:13} {ok}/{n}  {ok / n:.0%}")

Sur l'exécution Vertex AI ci-dessous : compte_rendu 26/30 87%, commande 29/30 97%, cv 35/35 100%. Les comptes rendus, avec leurs dates relatives, sont le point faible.

Résultat obtenu

python evaluer.py jeu_annote.jsonl resultats_vertex.jsonl, extraction par gemini-2.5-flash sur Vertex AI, le 2026-10-15 :

sortie réelle, 2026-10-15
type          champ              précision
commande      client             5/5  100%
commande      contact            5/5  100%
commande      date_livraison     5/5  100%
commande      lignes             5/5  100%
commande      mode               5/5  100%
commande      urgence            4/5  80%
compte_rendu  actions            3/5  60%
compte_rendu  date_reunion       4/5  80%
compte_rendu  participants       5/5  100%
compte_rendu  prochaine_reunion  4/5  80%
compte_rendu  projet             5/5  100%
compte_rendu  statut             5/5  100%
cv            annees_experience  5/5  100%
[... 6 lignes cv à 5/5]

TOTAL : 90/95 champs corrects (94.7%)

Erreurs :
  cmd-01 urgence: attendu='normale' obtenu='urgente'
  cr-02 actions: attendu=[{'responsable': 'Léa', 'echeance': '2026-10-02'}, {'responsable': 'Hugo', 'echeance': None}] obtenu=[{'responsable': 'Léa', 'echeance': None}, {'responsable': 'Hugo', 'echeance': None}]
  cr-05 date_reunion: attendu='2026-10-14' obtenu='2014-10-26'
  cr-05 actions: attendu=[{'responsable': 'Max', 'echeance': '2026-10-19'}, {'responsable': 'Zoé', 'echeance': '2026-10-23'}] obtenu=[{'responsable': 'Max', 'echeance': '2014-10-27'}, {'responsable': 'Zoé', 'echeance': None}]
  cr-05 prochaine_reunion: attendu='2026-10-28' obtenu='2014-10-28'

Le total de 94,7 % semble bon ; le détail montre que toutes les erreurs sauf une portent sur des dates dans des comptes rendus. 14/10/26 a été lu « 26 octobre 2014 », et la réparation a seulement rendu les échéances cohérentes avec cette mauvaise date. Une action précise s'en déduit : donner l'année courante dans la consigne, ou exiger des dates absolues dans les comptes rendus.

Je sais faire

  • Calculer une précision par champ à partir d'un jeu annoté
  • Normaliser sans masquer de vraies erreurs
  • Évaluer des listes d'objets en ne comparant que les clés annotées
  • Lire une liste d'erreurs pour en déduire une correction
Next