| """Additive concern lookup, backed by OFF's EFSA/ANSES-sourced taxonomy. |
| |
| Replaces the substring matching in the old `nutriweb/risk_levels.py`. Lookups |
| are exact on OFF's canonical additive tags (`en:e250`), so "sugar-free" can no |
| longer be flagged as containing sugar. |
| """ |
|
|
| from __future__ import annotations |
|
|
| import json |
| from functools import lru_cache |
| from pathlib import Path |
|
|
| DATA_PATH = Path(__file__).resolve().parent.parent.parent / "data" / "additives.json" |
|
|
| |
| CONCERN_PENALTY = {"high": 6.0, "moderate": 3.0, "watch": 1.5, "none": 0.0} |
|
|
| |
| |
| MAX_ADDITIVE_PENALTY = 18.0 |
|
|
|
|
| @lru_cache(maxsize=1) |
| def _table() -> dict[str, dict]: |
| if not DATA_PATH.exists(): |
| raise FileNotFoundError( |
| f"{DATA_PATH} missing. Run: python pipeline/additives_taxonomy.py" |
| ) |
| return json.loads(DATA_PATH.read_text()) |
|
|
|
|
| def lookup(tag: str) -> dict | None: |
| """Return the concern record for an OFF additive tag, or None if unflagged.""" |
| return _table().get(tag) |
|
|
|
|
| def concerns(additives_tags: list[str] | None) -> list[dict]: |
| """Flagged additives in a product, worst first, for display.""" |
| order = {"high": 0, "moderate": 1, "watch": 2} |
| found = [ |
| {"tag": tag, **meta} |
| for tag in (additives_tags or ()) |
| if (meta := lookup(tag)) and meta["concern"] != "none" |
| ] |
| return sorted(found, key=lambda a: order.get(a["concern"], 9)) |
|
|
|
|
| def penalty(additives_tags: list[str] | None) -> float: |
| """Total health-score penalty from a product's additives, capped.""" |
| total = sum(CONCERN_PENALTY.get(a["concern"], 0.0) for a in concerns(additives_tags)) |
| return min(total, MAX_ADDITIVE_PENALTY) |
|
|
|
|
| def has_non_nutritive_sweetener(additives_tags: list[str] | None) -> bool: |
| """Whether any additive is a non-nutritive sweetener. |
| |
| Drives the Nutri-Score 2023 beverage penalty of 4 negative points. |
| """ |
| return any( |
| (meta := lookup(tag)) and meta.get("non_nutritive_sweetener") |
| for tag in (additives_tags or ()) |
| ) |
|
|