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
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) :
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.
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.
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 |
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=1000classify-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.