Portail développeurs

Documentation technique INCIDB

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.

1. Contenu du snapshot

Cinq tables, exportées en CSV UTF-8 délimité par des barres verticales et en Apache Parquet, avec des colonnes identiques :

2. Les sources et la jointure

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.

3. Couverture mesurée

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 :

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.

4. Canonicalisation

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.

5. Démarrage rapide — PyArrow et Pandas

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]

6. Démarrage rapide — DuckDB

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()