Codification COICOP des produits BDF

Documentation de la démarche et du pipeline de codification automatique

À quoi sert ce pipeline ?

L’enquête Budget de Famille (BDF) vise à comprendre la structure et l’évolution de la consommation des ménages et collecte pour cela les dépenses réalisées par les ménages enquêtés. Ces dépenses remontent sous forme de libellés textuels relativement courts, souvent bruités et sémantiquement différents selon leur source (descripitions manuelles, lignes de tickets de caisse, etc.).

Pour exploiter ces données, chaque libellé doit être rattaché à un poste de la nomenclature COICOP (Classification of Individual Consumption According to Purpose). Le faire à la main est coûteux : le pipeline codif-coicop-bdf automatise cette codification en combinant plusieurs approches (règles, similarité de chaînes, machine learning supervisé, RAG) puis en les arbitrant via un LLM.

La nomenclature COICOP en bref

Un code COICOP est une suite de segments séparés par des points, de plus en plus précis :

Niveau Exemple Désignation
1 01 Section (ex. Produits alimentaires et boissons non alcoolisées)
2 01.1 Division
3 01.1.1 Groupe (ex. Pains et céréales)
4 01.1.1.1 Classe (poste le plus fin conservé par le pipeline)
5 01.1.1.1.x Poste — supprimé lors du pruning (trop granulaire)

À côté des codes « produits », des codes techniques servent aux lignes qui ne sont pas un produit consommable :

Code Sens
98.1 Courses / alimentation non détaillée
98.3 Carte bancaire (ligne « Ticket CB »)
98.4 Prélèvement
98.5 Remises / réductions
99.x Divers non codables

Davantage de contexte et échéances

La codification porte sur le millésime BDF 2026 (dont les questionnaires remontent progressivement au cours de l’année 2026).

Les résultats consolidés doivent être transmis à Eurostat en 2028.

Les travaux de mise en place d’un pipeline de codification automatique ont débuté à l’automne 2025, impliquant la MoA de l’enquête, la division IPC et le SSPlab.

À noter que la prochaine enquête BDF aura lieu en 2030 et il convient ainsi de rendre le processus de codification pérenne et reproductible pour pouvoir l’utiliser lors de ce prochain millésime.

Le pipeline étape par étape

Le pipeline de codification est un DAG Argo (argo/codif-pipeline.yaml). Chaque étape lit/écrit sur S3 dans un dossier propre au run :

s3://projet-budget-famille/data/workflow_runs/{run_date}/{run_id}/<étape>/

Le diagramme ci-dessous montre les étapes et les fichiers qui circulent entre elles (tous au format parquet, sauf le livrable final) :

flowchart LR
  input[/"CSV d'entrée<br/>(input_file, obligatoire)"/] --> PRE[build-datasets]
  PRE -- observations.parquet --> REG[classify-regex]
  REG -- raw_test_without_regex.parquet --> PRU[prune-codes]
  REG -- raw_test_without_regex.parquet --> LCS[classify-lcs]
  REG -- raw_test_without_regex.parquet --> TTC[classify-ttc]
  VDB[/"collection Qdrant notices<br/>(classify-rag-notices-collection)"/] -.-> RAG[classify-rag-notices]
  VDBA[/"collection Qdrant annotations<br/>(classify-rag-annotations-collection)"/] -.-> RAGA[classify-rag-annotations]
  PRU -- annotations_test_pruned.parquet --> RAG
  PRU -- annotations_test_pruned.parquet --> RAGA
  PRU -- mapping_lvl4.parquet --> RAG
  LCS -- raw_test_LCS.parquet --> DEC{{"conciliation<br/>reconcile-llm OU reconcile-sirus"}}
  RAG -- predictions.parquet --> DEC
  RAGA -- predictions.parquet --> DEC
  TTC -- predictions.parquet --> DEC
  PRU -- mapping_lvl4.parquet --> DEC
  DEC -- predictions.parquet --> FIN[export-results]
  REG -- REGEX_pred.parquet --> FIN
  DEC -- predictions.parquet --> REP[report]
  FIN --> out[/"CSV livré<br/>(+ predicted_code)"/]
  REP --> html[/report.html/]
  FIN -.-> EVA["evaluate<br/>(si label-column)"]
  DEC -.-> EVA
  EVA -.-> evahtml[/evaluation_report.html/]
  style VDB stroke-dasharray: 5 5
  style VDBA stroke-dasharray: 5 5
  style EVA stroke-dasharray: 5 5

Le nœud de conciliation est exclusif : le paramètre reconciliation (llm ou sirus) décide lequel des deux tourne, l’autre est skippé. evaluate est en pointillé parce qu’elle est facultative : elle ne tourne que si le fichier d’entrée porte une colonne d’étiquettes (label-column).

Les deux nœuds en pointillé ne sont pas des étapes de ce workflow : ce sont des collections Qdrant construites à l’avance, par deux workflows d’indexation autonomes, et désignées ici par leur nom via les paramètres classify-rag-notices-collection et classify-rag-annotations-collection. L’étape prune-codes, elle, tourne à chaque run : elle produit le jeu à coder pruné et la table de mapping dont l’aval a besoin.

NoteTrois workflows Argo, et pourquoi

Le dépôt contient trois workflows :

Workflow Ce qu’il produit
argo/index-notices-pipeline.yaml une collection Qdrant des notices COICOP prunées (prune-codes --only nomenclature → index-notices)
argo/index-annotations-pipeline.yaml une collection Qdrant des produits déjà annotés (build-datasets → prune-codes --only kb → index-annotations)
argo/codif-pipeline.yaml la codification proprement dite, qui interroge ces deux collections

L’indexation a été sortie du pipeline de codification pour deux raisons. D’abord, embarquer toute la nomenclature (et toute la base d’exemples) n’a aucune raison d’être repayé à chaque codification. Ensuite et surtout, tant que les collections portaient un nom fixe partagé par tous les runs, une réindexation détruisait la base que lisait un run concurrent. Chaque indexation crée désormais une collection au nom unique ; on recopie ce nom dans le fichier de paramètres du pipeline de codification — même pratique que classify-ttc-model-uri et reconcile-sirus-model-uri.

Le pipeline de codification n’a qu’un seul mode : il codifie le fichier désigné par input_file (obligatoire) et le livre par export-results. Si ce fichier porte une colonne d’étiquettes, la désigner par label-column déclenche l’étape evaluate, qui mesure la qualité du run. Chaque étape des trois workflows clone le dépôt sur la branche donnée par le paramètre git-branch (défaut : main).

# Étape Rôle Page
1 build-datasets Consolide toute la base annotée (la KB) et prépare les observations à coder →
2 classify-regex Code les libellés évidents par règles regex ; point unique d’échantillonnage →
3 prune-codes Troncature niveau 4 + élagage des hiérarchies linéaires : produit tous les artefacts prunés →
4 classify-lcs Code par similarité de chaîne (LCS) sur un pool de référence →
5 classify-ttc Classifieur neuronal basique (caisses + fine-tuning BDF 2024) →
6 classify-rag-notices Codification sémantique par RAG sur les notices COICOP prunées (collection bâtie par index-notices) →
7 classify-rag-annotations Codification par RAG sur exemples déjà codifiés, few-shot (collection bâtie par index-annotations) →
8 reconcile-llm Arbitrage final LLM-as-judge entre LCS / RAG notices / RAG annotations / TTC →
8bis reconcile-sirus Conciliation alternative par règles interprétables. Exclusive de reconcile-llm : le paramètre reconciliation choisit →
9 export-results Assemble le résultat utilisateur (regex + code concilié) →
10 report Rapport Quarto de production →
11 evaluate Rapport d’évaluation, facultatif (label-column) →

Lancer le pipeline

Un script d’aide est présent dans le repo : codif-coicop-bdf/argo/argo_helper.md.

Il donne les commandes d’installation et de lancement. Par ailleurs, le fichier de paramétrage du workflow se trouve ici codif-coicop-bdf/argo/params.yaml : il permet notamment de choisir sur quel fichier s’applique la codification, quelle conciliation tranche (reconciliation), s’il faut mesurer la qualité du run (label-column), ou coder sur un échantillon de produits.

ImportantTout argo submit passe d’abord par un run « smoke »

Le DAG complet tourne deux fois : une première passe sur smoke-observations lignes (100 par défaut, ~8 min), puis le vrai run. Si le smoke échoue, le vrai run ne démarre pas — c’est ce qui évite de découvrir une erreur de paramétrage après plusieurs heures de codification.

Les deux passes écrivent sous des run_id distincts (<workflow>-smoke et <workflow>), et le smoke logue dans son propre espace MLflow : il ne pollue donc ni les sorties S3 ni les métriques du run réel.

Échappatoire, pour une urgence ou un smoke en faux positif qui immobilise la production : -p skip-smoke=true.

Fil rouge

Quatre lignes réelles du CSV sont suivies de bout en bout dans les pages suivantes :

Libellé (NAT_DEP) Enseigne Montant Devenir attendu
Bagu. Tradition U Blé Bretagne U (Liffré) 1,00 € produit alimentaire → pain 01.1.1.x (via modèles)
Illisible — 0,00 € non codable → retiré, ou capté par la règle ^illisible$ → 98.4
Ticket CB Station Netto 40,24 € ligne technique mais non captée par les regex (libellé non standardisé) → passe aux modèles
Max garden flowers balle surprise Action 3,99 € produit ambigu → arbitrage LLM
Astuce

Les règles regex sont surtout ancrées (^…$) et ne reconnaissent que des libellés standardisés (carte bancaire, prelevement, courses, restaurant…). Sur cet extrait BDF, les libellés réels (Ticket CB, Prlv Sepa Bouygues…) ne correspondent pas exactement : la plupart des lignes franchissent donc l’étape regex et sont codées par les modèles puis arbitrées par le LLM. Voir 2. Codif regex.

Chaque page d’étape reprend la même structure : Rôle · Entrées · Traitement · Exemple sur le fil rouge · Sorties.

# 1. (une fois, ou quand la nomenclature / la base d'annotations change)
#    Construire les deux collections Qdrant. Chacune affiche en fin d'exécution
#    la ligne exacte à recopier dans argo/params.yaml.
argo submit argo/index-notices-pipeline.yaml
argo submit argo/index-annotations-pipeline.yaml            # la KB = toute la base annotée

# 2. Pipeline complet (input_file et les collections viennent de params.yaml)
argo submit argo/codif-pipeline.yaml --parameter-file argo/params.yaml

# Tout passer en ligne de commande : fichier d'entrée et mappage de colonnes
argo submit argo/codif-pipeline.yaml \
  -p input_file=s3://projet-budget-famille/data/workflow_inputs/mon_fichier.csv \
  -p text_column=NAT_DEP -p shop_column=MAG_DEP -p budget_column=MONT_DEP \
  -p classify-rag-notices-collection=coicop_notices__2026-09-02__index-notices-a7k2p \
  -p classify-rag-annotations-collection=coicop_annotations__2026-09-02__index-annotations-b3x9q

# Limiter le jeu à coder à N observations (sampling centralisé à classify-regex,
# hérité par les quatre classifieurs)
argo submit argo/codif-pipeline.yaml --parameter-file argo/params.yaml -p sample-observations=100

# Plafonner la BASE indexée, elle, se fait au moment de l'indexation :
argo submit argo/index-annotations-pipeline.yaml -p kb-sample-size=1000
Important

classify-rag-notices-collection et classify-rag-annotations-collection sont obligatoires et n’ont pas de valeur par défaut. Argo ne sait pas exprimer « paramètre requis » : la garde est donc faite deux fois, dans le script du conteneur et dans l’argparse Python, et un nom manquant fait échouer l’étape en quelques secondes. C’est délibéré — mieux vaut cet échec immédiat qu’un repli silencieux sur l’index d’un autre run, ou qu’un plantage après deux heures de calcul.

Pour relancer ou déboguer une seule étape (via Argo ou en local), voir Lancer une étape individuellement. Les termes et colonnes du pipeline sont définis dans le glossaire. Des présentations résument le pipeline pour différents publics.