File size: 6,200 Bytes
89d9642
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
"""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.
"""
    )