Zum Hauptinhalt springen

Zutatenmengenschätzer GFM

Das Zutatenmengenschätzer Gap Filling Module schätzt die Menge jeder Zutat in einem Lebensmittelprodukt basierend auf Nährwertdaten. Es verwendet konvexe Optimierung (CVXPY), um die Differenz zwischen deklarierten Nährwerten auf der Produktverpackung und berechneten Werten aus einzelnen Zutatennährstoffen zu minimieren.

Kurzreferenz

EigenschaftBeschreibung
Läuft aufFoodProcessingActivityNode mit Nährwerten auf übergeordneten Knoten
AbhängigkeitenUnitWeightConversionGapFillingWorker, AddClientNodesGapFillingWorker, MatchProductNameGapFillingWorker, IngredientSplitterGapFillingWorker, NutrientSubdivisionGapFillingWorker, LinkTermToActivityNodeGapFillingWorker
SchlüsseleingabeProdukt-Nährwertdeklaration, Zutatenliste mit Nährwertprofilen
AusgabeGeschätzte Zutatenmengen (kg pro Einheit des übergeordneten Produkts)
AuslöserProdukt hat Nährwerte und Zutatendeklaration

Wann es läuft

Das Modul wird ausgelöst, wenn:

  1. Der Knoten ein FoodProcessingActivityNode ist
  2. Übergeordnete Knoten Nährwerte verfügbar haben (aus API oder Datenbank)
  3. Das Produkt eine Zutatendeklaration hat, die vom Zutatensplitter GFM geparst wurde
  4. Alle erforderlichen Abhängigkeits-GFMs abgeschlossen sind

Schlüsselausgabe

Das Modul erzeugt:

  • Zutatenmengen: Geschätztes Gewicht jeder Zutat in kg pro Einheit des übergeordneten Produkts
  • Lösungsstatus: Optimierungslöser-Status (optimal, nicht erfüllbar, unbegrenzt)
  • Fehlerquadrate: Pro-Nährstoff quadrierte Fehler zur Qualitätsbewertung
  • Geschätzte Nährstoffe: Berechnete Nährwerte basierend auf geschätzten Mengen

Wissenschaftliche Methodik

Der Zutatenmengenschätzer löst ein beschränktes Optimierungsproblem, um Zutatenanteile zu finden, die am besten zu den deklarierten Nährwerten passen, während rechtliche und physikalische Beschränkungen eingehalten werden.

Problemformulierung

Der Algorithmus minimiert die normalisierte Differenz zwischen berechneten und deklarierten Nährstoffen:

minimiere ||A_norm * x - b_norm||

Wobei:

  • A: M x N Nährstoffmatrix (M Nährstoffe, N Zutaten)
  • x: N x 1 Vektor der Zutatenmengen (Anteile des Gesamten, summieren sich zu 1)
  • b: M x 1 Vektor der deklarierten Nährwerte
  • A_norm, b_norm: Normalisierte Versionen unter Verwendung der Standardabweichung

Normalisierung

Jeder Nährstoff wird durch seine Populationsstandardabweichung normalisiert, um gleichmäßige Gewichtung sicherzustellen:

A_norm = (A.T / norm_vector).T
b_norm = b / norm_vector

Diese Normalisierung verwendet Statistiken aus tausenden Produktdeklarationen:

NährstoffDurchschnittStandardabweichungMedian
Energie (Kilokalorie)284,84170,00275,0
Fett (g)12,6112,677,4
Gesättigtes Fett (g)5,486,292,5
Kohlenhydrate (g)27,4724,1916,2
Zucker/Saccharose (g)15,3617,057,3
Protein (g)6,945,765,8
Natriumchlorid (g)0,770,850,4
Ballaststoffe (g)2,882,592,2
Natrium (mg)302,92938,0715,74
Chlor (mg)461,02515,61242,64

Akzeptierte Nährstoffe

Nur Nährstoffe mit vollständigen Daten in der Eaternity-Datenbank werden für die Optimierung verwendet:

ACCEPTED_NUTRIENTS = {
"energy",
"fat",
"saturated_fat",
"carbohydrates",
"water",
"sucrose",
"protein",
"sodium",
"chlorine",
"fibers",
}

Beschränkungssystem

Die Optimierung umfasst mehrere Beschränkungstypen:

1. Summenbeschränkung

Alle Zutatenanteile auf jeder Hierarchieebene müssen sich zu 1 (100%) summieren:

F @ x == g

Wobei F die Gleichheitsbeschränkungsmatrix ist, die sicherstellt, dass Zutaten auf jeder Ebene korrekt summieren.

2. Abnehmende Reihenfolgenbeschränkung

Gemäß EU-Lebensmittelkennzeichnungsvorschriften müssen Zutaten in abnehmender Reihenfolge nach Gewicht aufgelistet werden:

C @ x <= d

Wobei C eine Beschränkungsmatrix ist, die durchsetzt:

  • Zutat[i] >= Zutat[i+1] für Zutaten auf derselben Ebene

Ausnahme: Diese Beschränkung gilt nicht für Unterteilungen (Nährstoffvarianten derselben Zutat).

3. Feste Prozentsatzbeschränkungen

Wenn Prozentsätze auf der Verpackung deklariert sind:

# Exakte Prozentsatzbeschränkung
x[ingredient_idx] == fixed_percentage * x[parent_idx]

# Oder für Zutaten auf Stammebene
x[ingredient_idx] == fixed_percentage

4. Minimum/Maximum-Prozentsatzbeschränkungen

Wenn nur Grenzen angegeben sind:

# Minimumprozentsatz: Zutat >= min_percentage * Eltern
x[col_idx] - min_pct * x[parent_col_idx] >= 0

# Maximumprozentsatz: Zutat <= max_percentage * Eltern
x[col_idx] - max_pct * x[parent_col_idx] <= 0

5. Nicht-Negativität

Alle Zutatenmengen müssen nicht-negativ sein:

x >= 0

Implementierungsdetails

Solver-Konfiguration

Das Modul verwendet den ECOS-Solver über CVXPY:

problem.solve(solver="ECOS")

ECOS (Embedded Conic Solver) wurde wegen seiner Effizienz bei Programmen zweiter Ordnung und seiner Fähigkeit, die Least-Squares-Zielfunktion zu behandeln, gewählt.

Behandlung der Zutatenhierarchie

Der Algorithmus behandelt verschachtelte Zutatendeklarationen mit einem Level-Tupel-System:

# Beispiel-Hierarchie:
# "Schokolade (Kakao 30%, Zucker), Milchpulver (Milch, Laktose)"
# Level-Tupel:
# (0,) -> Schokolade
# (0, 0) -> Kakao (30% von Schokolade)
# (0, 1) -> Zucker
# (1,) -> Milchpulver
# (1, 0) -> Milch
# (1, 1) -> Laktose

Die Beschränkungsmatrix stellt sicher:

  • Unterzutaten summieren sich zur Menge ihrer übergeordneten Zutat
  • Reihenfolgenbeschränkungen gelten innerhalb jeder Ebene

Spezialbehandlung

Fermentationsanpassung

Für alkoholische Produkte passt der Algorithmus Nährstoffe an, um die Fermentation zu berücksichtigen:

# Alkoholumwandlung (Gay-Lussac-Gleichung)
# 180g Zucker -> 92g Ethanol + 88g CO2
alc_as_sugar = alc.value / 0.47

# Zucker und Kohlenhydrate anpassen
sugar.value += alc_as_sugar
carbs.value += alc_as_sugar

# Energie anpassen (Alkohol: 7 kcal/g, Zucker: 4 kcal/g)
energy.value += alc_as_sugar * 4.0 # Zuckerkalorien wieder hinzufügen
energy.value -= alc.value * 7.0 # Alkoholkalorien entfernen

Natriumchlorid-Aufteilung

Wenn Natriumchlorid (Salz) deklariert ist, wird es für die Optimierung in Natrium und Chlor aufgeteilt:

NA_PERCENT_IN_NACL = 39.34  # 39,34% Natrium
CL_PERCENT_IN_NACL = 60.66 # 60,66% Chlor

new_sodium = old_sodium + (sodium_chloride * 39.34 / 100)
new_chlorine = old_chlorine + (sodium_chloride * 60.66 / 100)

Wasserentfernung

Wassergehalt wird von der direkten Optimierung ausgeschlossen, da er implizit durch Nährstoffunterteilung behandelt wird:

incoming_nutrients.quantities = {
k: v for k, v in incoming_nutrients.quantities.items()
if k != get_nutrient_term("water").uid
}

Nicht-Lebensmittel-Zutaten

Nicht-Lebensmittel-Zutaten (Zusatzstoffe, Konservierungsmittel, E-Nummern) erhalten die Menge null:

for uid in non_food_ingredient_uids:
calc_graph.apply_mutation(
PropMutation(
node_uid=uid,
prop_name="amount",
prop=QuantityProp(
value=0.0,
unit_term_uid=kilogram_term.uid,
),
)
)

Beschränkungsmatrix-Konstruktion

Nährstoffmatrix (A)

Die M x N Nährstoffmatrix ordnet Zutaten-Nährstoffprofile der Optimierung zu:

def make_nutrient_matrix(flat_nutrients, product_order, nutrient_order, M, N):
matrix = np.zeros((M, N))
for iy, product_key in enumerate(product_order):
if flat_nutrients[product_key] is None:
continue # Spalte bei null lassen
for ix, nutrient_key in enumerate(nutrient_order):
if nutrient_key in flat_nutrients[product_key].quantities:
matrix[ix, iy] = flat_nutrients[product_key].quantities[nutrient_key].value
matrix[np.isnan(matrix)] = 0.0
return matrix

Reihenfolgenbeschränkungsmatrix (C)

Erzwingt absteigende Zutatenreihenfolge:

# Beispiel für [(0,), (1,), (1,0), (1,1), (2,)]
# Matrixstruktur:
# [[-1, 1, 0, 0, 0], # Zutat[0] >= Zutat[1]
# [ 0, -1, 0, 0, 1], # Zutat[1] >= Zutat[4]
# [ 0, 0, -1, 1, 0], # sub[0] >= sub[1]
# [ 0, 0, 0, -1, 0],
# [ 0, 0, 0, 0, -1]]

Gleichheitsmatrix (F)

Stellt sicher, dass Zutaten auf jeder Ebene korrekt summieren:

# Beispiel: F @ x = g
# [[1, 1, 0, 0, 1], # Oberste Ebene summiert zu 1
# [0, -1, 1, 1, 0]] # Unterzutaten summieren zum Eltern
# g = [1, 0]

Berechnungsbeispiel

Szenario: Schokoladenriegel mit Deklaration:

  • "Kakaomasse (45%), Zucker, Kakaobutter, Milchpulver (5%)"
  • Deklarierte Nährstoffe: 530 kcal, 32g Fett, 20g gesättigtes Fett, 52g Kohlenhydrate, 48g Zucker, 6g Protein

Schritt 1: Zutatenhierarchie erstellen

Level-Tupel:
(0,) -> Kakaomasse [45% fixiert]
(1,) -> Zucker
(2,) -> Kakaobutter
(3,) -> Milchpulver [5% fixiert]

Schritt 2: Matrizen konstruieren

Nährstoffmatrix A (pro 100g jeder Zutat):

KakaomasseZuckerKakaobutterMilchpulver
Energie228400884496
Fett14099,826,7
Ges. Fett8,1060,516,7
Kohlenhydrate11,5100038,4
Zucker0,5100038,4
Protein19,60026,3

Beschränkungsmatrix C (Reihenfolgenbeschränkungen):

[[-1, 1, 0, 0],    # x[0] >= x[1]
[0, -1, 1, 0], # x[1] >= x[2]
[0, 0, -1, 1], # x[2] >= x[3]
[0, 0, 0, -1]]

Schritt 3: Optimierung lösen

Mit festen Beschränkungen x[0] = 0,45 und x[3] = 0,05:

minimiere ||A_norm @ x - b_norm||
unter:
x[0] + x[1] + x[2] + x[3] = 1
x[0] = 0,45
x[3] = 0,05
x[0] >= x[1] >= x[2] >= x[3]
x >= 0

Schritt 4: Lösung

Kakaomasse:   45,0%  (fixiert)
Zucker: 30,2% (geschätzt)
Kakaobutter: 19,8% (geschätzt)
Milchpulver: 5,0% (fixiert)

Schritt 5: Ausgabe in Graph

Jeder Zutatenknoten erhält eine Menge in kg:

# Für 1kg Produkt:
cocoa_mass.amount = 0,450 kg
sugar.amount = 0,302 kg
cocoa_butter.amount = 0,198 kg
milk_powder.amount = 0,050 kg

Fallback-Mechanismen

Wenn Optimierung fehlschlägt

Wenn die Optimierung "nicht erfüllbar" oder "unbegrenzt" zurückgibt:

  1. Fallback ohne Schätzung versuchen: Wenn alle Prozentsätze festgelegt sind, diese direkt verwenden
  2. Unterteilungen behandeln: Unterteilungsmengen basierend auf übergeordneter Produktionsmenge zuweisen
  3. Datenfehler protokollieren: Fehler für manuelle Überprüfung erfassen
if len(flat_nutrients) == len(fixed_percentages):
if set(flat_nutrients.keys()) == set(fixed_percentages.keys()):
self.handle_fixed_percentages(flat_nutrients, fixed_percentages, calc_graph)
return

Fehlende Nährstoffe

Wenn Blattzutaten keine Nährstoffdaten haben:

  • Wenn alle Prozentsätze bekannt sind, feste Prozentsätze verwenden
  • Andernfalls Fehler auslösen und Wissenschaftsteam-Daten anfordern

Qualitätsmetriken

Fehlerquadrate

Das Modul berechnet pro-Nährstoff quadrierte Fehler zur Qualitätsbewertung:

error_squares[nutrient] = ((geschätzt - deklariert) / STD)^2

Dieser normalisierte Fehler ermöglicht Vergleiche über Produkte unabhängig von der Skala.

Lösungsstatus

Der Solver gibt einen Status zurück, der die Lösungsqualität anzeigt:

  • optimal: Gültige Lösung gefunden
  • optimal_inaccurate: Lösung gefunden, aber möglicherweise numerische Probleme
  • infeasible: Keine gültige Lösung existiert (widersprüchliche Beschränkungen)
  • unbounded: Problem nicht richtig beschränkt

Bekannte Einschränkungen

Datenabdeckung

  • Erfordert Nährstoffprofile für alle Zutaten in der Eaternity-Datenbank
  • Nur 10 Nährstoffe für Optimierung verwendet (nicht alle deklarierten Nährstoffe)
  • Nährstoffstatistiken basieren auf europäischen Produktdaten

Modellannahmen

  • Zutaten in absteigender Reihenfolge aufgelistet (EU-Verordnung)
  • Unterzutaten-Prozentsätze relativ zum Eltern (kann in der Praxis variieren)
  • Lineare Beziehung zwischen Zutatenmengen und Nährstoffen
  • Keine verarbeitungsbedingten Nährstoffänderungen direkt modelliert

Numerische Überlegungen

  • Sehr kleine negative Lösungen (> -1e-5) werden auf 1e-10 begrenzt
  • NaN-Lösungen lösen Fehler aus
  • Solver kann bei schlecht konditionierten Problemen fehlschlagen

Verwandte Gap Filling Modules

ModulBeziehung
ZutatensplitterParst Zutatendeklarationen in strukturierte Hierarchie
NährstoffunterteilungErstellt getrocknete/modifizierte Varianten für Wasserverlustmodellierung
Produktnamen-MatchingVerknüpft Zutaten mit Datenbankeinträgen mit Nährstoffprofilen
EinheitsgewichtumrechnungRechnet Mengen zwischen Einheiten um

Referenzen

  1. CVXPY-Dokumentation. https://www.cvxpy.org/

  2. ECOS Solver. Domahidi, A., Chu, E., & Boyd, S. (2013). ECOS: An SOCP solver for embedded systems. European Control Conference.

  3. EU-Lebensmittelkennzeichnungsverordnung. Verordnung (EU) Nr. 1169/2011

  4. Mahalanobis-Distanz. https://de.wikipedia.org/wiki/Mahalanobis-Distanz

  5. EuroFIR Lebensmittelzusammensetzungsdaten. http://www.eurofir.org/