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/