Les cinq tables, les deux sources dont elles proviennent, la couverture réelle de chaque colonne d’enrichissement et la manière d’interroger le résultat.
Cinq tables, exportées en CSV UTF-8 délimité par des barres verticales et en Apache Parquet, avec des colonnes identiques :
position_index.Deux sources, une jointure documentée. L’URL, l’horodatage de téléchargement et le SHA-256 de chaque fichier source sont consignés dans le build_report.json livré avec les données.
categories_tags propres à la source, listes d’ingrédients brutes) sous Open Database License (ODbL).NULL dans toutes les colonnes issues de CosIng, jamais une valeur de remplacement.is_common_allergen. La FDA américaine n’a pas encore publié sa liste d’allergènes parfumants au titre du MoCRA ; ce jeu de données ne comporte donc aucun indicateur américain.rating_source, et valent NULL partout ailleurs.Présentée de deux façons, car les deux chiffres diffèrent d’un ordre de grandeur et n’en citer qu’un serait trompeur :
functions : 10,9% des noms distincts ; 81,5% des occurrences sur les étiquettes.cas_number : 8,3% des noms distincts ; 76,4% des occurrences sur les étiquettes.chemical_description : 8,5% des noms distincts, 3 965 valeurs distinctes.Un corpus d’étiquettes contient bien plus de chaînes distinctes — variantes botaniques, graphies multilingues, tokens marketing — qu’aucun inventaire réglementaire n’en répertorie, d’où la faiblesse du premier chiffre de chaque paire. Les emplacements d’une étiquette réelle sont très majoritairement occupés par des ingrédients que CosIng couvre effectivement, d’où le niveau élevé du second. Aucun des deux chiffres ne décrit à lui seul les données.
Les tokens bruts d’étiquette sont résolus en noms INCI canoniques par une procédure explicite et ordonnée — décodage des entités, suppression de caractères, réduction des espaces, passage en majuscules, puis correspondance exacte, correspondance après nettoyage, une petite table de fautes de frappe sourcée, suppression des pourcentages, suppression des parenthèses, une table de synonymes, séparation des variantes à barre oblique, et découpage des concaténations uniquement lorsque les deux moitiés correspondent à l’inventaire. Tout ce qui échoue à toutes ces étapes est conservé tel quel et enregistré comme unresolved, jamais deviné. La méthode utilisée est stockée ligne par ligne dans ingredient_name_map.method : chaque décision est ainsi auditable et réversible.
import pyarrow.parquet as pq
ingredients = pq.read_table('ingredients.parquet').to_pandas()
links = pq.read_table('product_ingredients.parquet').to_pandas()
# Coverage the way it actually matters: weighted by label occurrence.
merged = links.merge(ingredients, on='ingredient_id', how='left')
print(merged['functions'].notna().mean())
# EU Annex III fragrance allergens
allergens = ingredients[ingredients['is_common_allergen'] == 1]
import duckdb
duckdb.query("""
SELECT f.function, COUNT(*) AS label_slots
FROM 'product_ingredients.parquet' pi
JOIN 'ingredients.parquet' i USING (ingredient_id)
CROSS JOIN UNNEST(string_split(i.functions, ';')) AS f(function)
WHERE i.functions IS NOT NULL
GROUP BY 1 ORDER BY 2 DESC LIMIT 10;
""").show()
# Audit the canonicalisation itself
duckdb.query("""
SELECT method, COUNT(*) AS n
FROM 'ingredient_name_map.parquet'
GROUP BY 1 ORDER BY 2 DESC;
""").show()