Problème
Le uid d'un marché est construit ainsi (src/tasks/clean.py:48) :
lf = lf.with_columns((pl.col("acheteur_id") + pl.col("id")).alias("uid"))
uid = acheteur_id + id. Or le champ id publié par les acheteurs n'est pas garanti unique : un même acheteur peut réutiliser le même id pour deux marchés différents. Le uid n'est donc pas un identifiant fiable d'un marché.
Exemple concret
Acheteur 21310555400017 (Ville de Toulouse), deux contrats distincts publiés avec id = "2023V" → même uid = 213105554000172023V :
|
marché A |
marché B |
| objet |
22V0320AO — extension groupe scolaire Jolimont |
23V0141MN — LOT 9 électricité CFO-CFA |
| codeCPV |
45210000 (bâtiment) |
45311200 (électricité) |
| procédure |
Appel d'offres ouvert |
Sans publicité ni mise en concurrence |
| dureeMois |
31 |
52 |
| lieuExecution |
31500 |
31000 |
Mêmes acheteur_id, id, uid et dateNotification — mais deux contrats réellement différents.
Conséquences
Comme modification_id est un rang dense de dateNotification sur uid (sort_modifications, src/tasks/transform.py:97), les deux contrats se retrouvent avec le même (uid, modification_id), avec plusieurs impacts :
modification_id corrompu : deux contrats indépendants sont fusionnés dans l'historique de modifications d'un seul marché.
- Perte de données silencieuse dans la table
marches publiée : src/tasks/output.py:216 fait unique(subset=["uid", "modification_id"]) → pour chaque paire en collision, un des deux marchés réels est supprimé de la sortie finale.
- Démultiplication (fan-out) dans
detect_montant_anomalies : la fonction regroupe par toutes les colonnes non-titulaire (donc les 2 contrats restent séparés) puis rejoint les titulaires sur (uid, modification_id) seul → produit cartésien des titulaires entre les deux contrats. Symptôme visible : les deux logs height df diffèrent (~3,12 M en entrée vs ~3,18 M en sortie).
Ampleur
Sur un run complet (~2,52 M de couples (uid, modification_id)) :
- 5 806 couples
(uid, modification_id) couvrent plus d'un objet distinct
- 3 374 couples couvrent plus d'un
codeCPV distinct
Soit ~0,2 % des marché-versions, mais avec perte de données réelle à la publication.
Pistes de correction
- Rendre le
uid réellement unique (intégrer p. ex. un hash de objet/codeCPV/dateNotification, ou un suffixe de désambiguïsation). ⚠️ modifie les valeurs de uid publiées → changement cassant pour les consommateurs.
- Vérifier au préalable si le
id non-unique est un problème de donnée source ou de parsing (le vrai id du marché semble être 22V0320AO / 23V0141MN, présent dans objet, alors que le id extrait vaut 2023V — possible troncature/mauvais champ).
Note
Bug préexistant, indépendant des optimisations de performance récentes. Mis en évidence par les logs de height restaurés dans detect_montant_anomalies.
Problème
Le
uidd'un marché est construit ainsi (src/tasks/clean.py:48) :uid = acheteur_id + id. Or le champidpublié par les acheteurs n'est pas garanti unique : un même acheteur peut réutiliser le mêmeidpour deux marchés différents. Leuidn'est donc pas un identifiant fiable d'un marché.Exemple concret
Acheteur
21310555400017(Ville de Toulouse), deux contrats distincts publiés avecid = "2023V"→ mêmeuid = 213105554000172023V:22V0320AO— extension groupe scolaire Jolimont23V0141MN— LOT 9 électricité CFO-CFAMêmes
acheteur_id,id,uidetdateNotification— mais deux contrats réellement différents.Conséquences
Comme
modification_idest un rang dense dedateNotificationsuruid(sort_modifications,src/tasks/transform.py:97), les deux contrats se retrouvent avec le même(uid, modification_id), avec plusieurs impacts :modification_idcorrompu : deux contrats indépendants sont fusionnés dans l'historique de modifications d'un seul marché.marchespubliée :src/tasks/output.py:216faitunique(subset=["uid", "modification_id"])→ pour chaque paire en collision, un des deux marchés réels est supprimé de la sortie finale.detect_montant_anomalies: la fonction regroupe par toutes les colonnes non-titulaire (donc les 2 contrats restent séparés) puis rejoint les titulaires sur(uid, modification_id)seul → produit cartésien des titulaires entre les deux contrats. Symptôme visible : les deux logsheight dfdiffèrent (~3,12 M en entrée vs ~3,18 M en sortie).Ampleur
Sur un run complet (~2,52 M de couples
(uid, modification_id)) :(uid, modification_id)couvrent plus d'unobjetdistinctcodeCPVdistinctSoit ~0,2 % des marché-versions, mais avec perte de données réelle à la publication.
Pistes de correction
uidréellement unique (intégrer p. ex. un hash deobjet/codeCPV/dateNotification, ou un suffixe de désambiguïsation).uidpubliées → changement cassant pour les consommateurs.idnon-unique est un problème de donnée source ou de parsing (le vraiiddu marché semble être22V0320AO/23V0141MN, présent dansobjet, alors que leidextrait vaut2023V— possible troncature/mauvais champ).Note
Bug préexistant, indépendant des optimisations de performance récentes. Mis en évidence par les logs de
heightrestaurés dansdetect_montant_anomalies.