NutriWeb / README.md
Krish264's picture
Deploy: Pin Streamlit to 1.61.1; add deployment parity tests
89d9642
|
Raw
History Blame Contribute Delete
5.98 kB
---
title: NutriWeb
emoji: πŸ₯—
colorFrom: green
colorTo: blue
sdk: streamlit
sdk_version: 1.61.1
app_file: app.py
pinned: false
license: agpl-3.0
---
# NutriWeb
Find a packaged food, and get a **healthier alternative of the same kind** that
respects your allergens, your diet and your health profile.
Built on the full [Open Food Facts product
database](https://huggingface.co/datasets/openfoodfacts/product-database)
(4.66M products), filtered to **796,542 US products**.
---
## Why this was rebuilt
The previous version ran on a hand-cleaned CSV subset of unknown provenance,
downloaded from Google Drive at startup. Its recommendations weren't
trustworthy, and the data turned out to be only one of three causes:
| Problem | Old approach | Now |
|---|---|---|
| **Coverage** | A static CSV subset, no lineage | 796,542 US products from the live upstream dataset, rebuildable in one command |
| **Ingredient / allergen logic** | ~90 hardcoded substrings matched with `if key in name`, so `"sugar"` fired on *"sugar-free"* | Exact matching on Open Food Facts' canonical `allergens_tags`, `additives_tags`, `ingredients_analysis_tags` |
| **Additive risk** | A hand-written opinion list | EFSA overexposure ratings and the ANSES watch list, via the OFF additives taxonomy |
| **Retrieval** | FAISS index rebuilt **on every query**, with a positional slice applied to a filtered frame that misaligned scores against products | Category-constrained SQL candidate generation, ranked in NumPy |
| **Relevance** | No category constraint β€” a soda could return a candy bar | Candidates must share a category tag; regression-tested |
| **Blood-pressure rule** | `sodium_100g > 5.0` β€” five *grams* per 100 g, a level no food reaches, so it never fired | FDA-derived per-100g ceilings for salt and saturated fat |
| **Passwords** | Stored and compared in plaintext | bcrypt via passlib |
| **Dependencies** | torch + transformers + faiss β‰ˆ 2 GB | None of them; ~40 MB of pure Python and DuckDB |
## What it does
**Health score (0–100)** β€” the single quantity used for ranking:
70% Nutri-Score 2023 + 30% NOVA processing group, minus a capped penalty for
additives flagged by EFSA or ANSES.
**Nutri-Score, computed where Open Food Facts has none.** Only 44% of US
products carry an OFF grade, because Nutri-Score is a European scheme and OFF
usually cannot assign one without a category. NutriWeb ports the official 2023
algorithm from OFF's own reference implementation and computes the rest:
| | products | share |
|---|---|---|
| Published by Open Food Facts | 352,295 | 44.2% |
| **Computed by NutriWeb** | **290,908** | **36.5%** |
| **Total graded** | **643,203** | **80.7%** |
Validated against OFF's own grades on **317,265 products where both exist**:
**98.37% exact agreement**, 99.73% within one letter, 95.99% exact numeric score.
Grades are never silently merged β€” the UI labels every grade with its source,
and grades computed for products with no category are marked *low confidence*.
**Recommendations** β€” three stages:
1. **Candidates** β€” products sharing a category tag, choosing the most specific
tag whose pool clears a minimum size. Falls back to ingredient-tag overlap
for the 49% of US products with no category.
2. **Hard filters** β€” allergens (including traces), diet, and health-condition
ceilings, applied in SQL *before* the limit so the pool is filtered, not truncated.
These exclude; they are never traded off against a good score.
3. **Ranking** β€” 45% health gain, 30% macro similarity, 20% ingredient overlap,
5% popularity. Every component is shown to the user.
## Running it
```bash
python -m venv .venv && source .venv/bin/activate
pip install -r requirements.txt
# Build the catalog (one-off; ~7.2 GB download, then about a minute)
python pipeline/01_download.py # fetch food.parquet from the Hub
python pipeline/02_curate.py # filter, flatten, pivot -> DuckDB
python pipeline/03_score.py # Nutri-Score + health score + validation
python pipeline/04_audit.py # coverage report β€” read this
streamlit run app.py
```
`01_download.py` needs a Hugging Face token (`hf auth login`, or `HF_TOKEN`):
anonymous requests get rate-limited on a file that size.
### Deploying to Spaces
Publish the built catalog so the Space doesn't rebuild from 7.2 GB on cold start:
```bash
python pipeline/05_publish.py <your-username>/nutriweb-us-catalog
```
Then set on the Space:
- `NUTRIWEB_CATALOG_REPO` = `<your-username>/nutriweb-us-catalog`
- `MONGODB_URI` *(optional)* β€” as a **repository secret**, never in a file.
Without it the app runs with session-only profiles.
## Layout
```
app.py entry point; st.navigation
views/ search Β· product Β· recommend Β· compare Β· profile Β· insights
components/ badges, cards, nutrient tables
nutriweb/
data/catalog.py DuckDB access, FTS search
scoring/ nutriscore.py (2023 algorithm) Β· health_score.py Β· additives.py
reco/ engine.py Β· filters.py Β· similarity.py
profile/ model.py Β· auth.py
pipeline/ 01_download β†’ 02_curate β†’ 03_score β†’ 04_audit β†’ 05_publish
tests/ 64 tests: algorithm, filters, engine
```
## Tests
```bash
pytest
```
Covers Nutri-Score variant selection and grade boundaries, the safety filters
(an allergen must never reach a result), and engine guarantees β€” every
recommendation is strictly healthier, shares the source category, and a soda
never returns confectionery.
## Data and licence
Product data Β© Open Food Facts contributors, licensed
[ODbL](https://opendatacommons.org/licenses/odbl/). The Nutri-Score
implementation is ported from
[openfoodfacts-server](https://github.com/openfoodfacts/openfoodfacts-server)
(`lib/ProductOpener/Nutriscore.pm`), AGPL-3.0. This project is AGPL-3.0.
NutriWeb is an informational tool, not medical or dietary advice.