NutriWeb / views /insights.py
Krish264's picture
Deploy: Pin Streamlit to 1.61.1; add deployment parity tests
89d9642
Raw
History Blame Contribute Delete
6.2 kB
"""Data insights — what is actually in the catalog, and how it was built.
This page exists because the honest answer to "how good are these
recommendations?" is "as good as the coverage underneath them". Rather than
hide that, it is measured and shown.
"""
from __future__ import annotations
import plotly.express as px
import plotly.graph_objects as go
import streamlit as st
from nutriweb.data import catalog
st.markdown(
"""<div class="nw-hero">
<h1>What's in the catalog</h1>
<p>Built from the Open Food Facts product database on Hugging Face,
filtered to US products and scored with the official Nutri-Score 2023 algorithm.</p>
</div>""",
unsafe_allow_html=True,
)
@st.cache_data(show_spinner=False)
def _stats() -> dict:
return catalog.stats()
@st.cache_data(show_spinner=False)
def _grade_distribution():
return catalog.connect().execute("""
SELECT nutriscore_grade AS grade, nutriscore_source AS source, count(*) AS n
FROM catalog WHERE nutriscore_grade IS NOT NULL
GROUP BY 1, 2 ORDER BY 1
""").fetchdf()
@st.cache_data(show_spinner=False)
def _top_categories(limit: int = 12):
return catalog.connect().execute(f"""
SELECT replace(split_part(primary_category, ':', 2), '-', ' ') AS category,
count(*) AS products, round(avg(health_score), 1) AS avg_health
FROM catalog
WHERE primary_category IS NOT NULL AND health_score IS NOT NULL
GROUP BY 1 HAVING count(*) > 200
ORDER BY products DESC LIMIT {limit}
""").fetchdf()
@st.cache_data(show_spinner=False)
def _nutrient_profile(limit: int = 8):
return catalog.connect().execute(f"""
WITH top AS (
SELECT primary_category FROM catalog
WHERE primary_category IS NOT NULL AND health_score IS NOT NULL
GROUP BY 1 ORDER BY count(*) DESC LIMIT {limit}
)
SELECT replace(split_part(c.primary_category, ':', 2), '-', ' ') AS category,
median(c.proteins_100g) AS protein,
median(c.carbohydrates_100g) AS carbs,
median(c.sugars_100g) AS sugars,
median(c.fat_100g) AS fat,
median(c.fiber_100g) AS fibre,
median(c.salt_derived) AS salt
FROM catalog c JOIN top USING (primary_category)
GROUP BY 1 ORDER BY 1
""").fetchdf()
stats = _stats()
cols = st.columns(4)
cols[0].metric("US products", f"{stats['products']:,}")
cols[1].metric("With a health score", f"{stats['scored']:,}")
cols[2].metric("Graded by NutriWeb", f"{stats['graded_by_nutriweb']:,}",
help="Products where Open Food Facts publishes no Nutri-Score and we computed one.")
cols[3].metric("With a photo", f"{stats['with_image']:,}")
st.divider()
left, right = st.columns(2, gap="large")
with left:
st.markdown("#### Nutri-Score distribution, by source")
grades = _grade_distribution()
grades["source"] = grades["source"].map(
{"off": "Published by Open Food Facts", "nutriweb": "Computed by NutriWeb"}
)
figure = px.bar(
grades, x="grade", y="n", color="source",
category_orders={"grade": ["a", "b", "c", "d", "e"]},
color_discrete_sequence=["#0F8A5F", "#9CC5B4"],
labels={"grade": "Nutri-Score", "n": "Products", "source": ""},
)
figure.update_layout(
legend=dict(orientation="h", y=-0.22), margin=dict(l=0, r=0, t=6, b=0), height=340,
)
st.plotly_chart(figure, width='stretch')
st.caption(
"Computing grades ourselves lifts scored coverage well beyond what Open "
"Food Facts publishes for the US market."
)
with right:
st.markdown("#### Largest categories")
categories = _top_categories()
figure = px.bar(
categories.sort_values("products"), x="products", y="category",
orientation="h", color="avg_health",
color_continuous_scale=["#E63E11", "#FECB02", "#038141"],
labels={"products": "Products", "category": "", "avg_health": "Avg health"},
)
figure.update_layout(margin=dict(l=0, r=0, t=6, b=0), height=340)
st.plotly_chart(figure, width='stretch')
st.caption("Colour shows the average NutriWeb health score for the category.")
st.divider()
st.markdown("#### Nutrient profile of the largest categories")
st.caption(
"Median grams per 100 g. Axes are clipped at the 95th percentile so a few "
"extreme products cannot flatten the shape."
)
profile_data = _nutrient_profile()
nutrients = ["protein", "carbs", "sugars", "fat", "fibre", "salt"]
maxima = {n: max(profile_data[n].max(), 1e-6) for n in nutrients}
radar = go.Figure()
for _, row in profile_data.iterrows():
radar.add_trace(
go.Scatterpolar(
r=[row[n] / maxima[n] for n in nutrients],
theta=[n.capitalize() for n in nutrients],
fill="toself",
name=str(row["category"])[:28],
)
)
radar.update_layout(
polar=dict(radialaxis=dict(visible=True, range=[0, 1], showticklabels=False)),
height=470, margin=dict(l=40, r=40, t=20, b=20),
legend=dict(orientation="h", y=-0.12),
)
st.plotly_chart(radar, width='stretch')
st.divider()
with st.expander("How the health score is built"):
st.markdown(
"""
**Health score (0–100)** = 70% Nutri-Score 2023 + 30% NOVA processing group,
minus a capped penalty for additives flagged by EFSA or ANSES.
**Nutri-Score** is computed with the official 2023 algorithm, ported from Open
Food Facts' reference implementation. Where Open Food Facts already publishes a
grade we use theirs; where it does not, we compute one and label it. Validated
against Open Food Facts' own grades on 317,265 products: **98.4% exact agreement**,
99.7% within one letter.
**Where a signal is missing** a neutral prior is substituted rather than
rescaling the others, so every product stays on the same scale and remains
comparable. Products where we had to assume the category are marked *low
confidence* on the product page.
Data: [openfoodfacts/product-database](https://huggingface.co/datasets/openfoodfacts/product-database),
licensed ODbL. Product data © Open Food Facts contributors.
"""
)