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
| Eigenschaft | Beschreibung |
|---|---|
| Läuft auf | FoodProcessingActivityNode mit Nährwerten auf übergeordneten Knoten |
| Abhängigkeiten | UnitWeightConversionGapFillingWorker, AddClientNodesGapFillingWorker, MatchProductNameGapFillingWorker, IngredientSplitterGapFillingWorker, NutrientSubdivisionGapFillingWorker, LinkTermToActivityNodeGapFillingWorker |
| Schlüsseleingabe | Produkt-Nährwertdeklaration, Zutatenliste mit Nährwertprofilen |
| Ausgabe | Geschätzte Zutatenmengen (kg pro Einheit des übergeordneten Produkts) |
| Auslöser | Produkt hat Nährwerte und Zutatendeklaration |
Wann es läuft
Das Modul wird ausgelöst, wenn:
- Der Knoten ein
FoodProcessingActivityNodeist - Übergeordnete Knoten Nährwerte verfügbar haben (aus API oder Datenbank)
- Das Produkt eine Zutatendeklaration hat, die vom Zutatensplitter GFM geparst wurde
- 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ährstoff | Durchschnitt | Standardabweichung | Median |
|---|---|---|---|
| Energie (Kilokalorie) | 284,84 | 170,00 | 275,0 |
| Fett (g) | 12,61 | 12,67 | 7,4 |
| Gesättigtes Fett (g) | 5,48 | 6,29 | 2,5 |
| Kohlenhydrate (g) | 27,47 | 24,19 | 16,2 |
| Zucker/Saccharose (g) | 15,36 | 17,05 | 7,3 |
| Protein (g) | 6,94 | 5,76 | 5,8 |
| Natriumchlorid (g) | 0,77 | 0,85 | 0,4 |
| Ballaststoffe (g) | 2,88 | 2,59 | 2,2 |
| Natrium (mg) | 302,92 | 938,07 | 15,74 |
| Chlor (mg) | 461,02 | 515,61 | 242,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):
| Kakaomasse | Zucker | Kakaobutter | Milchpulver | |
|---|---|---|---|---|
| Energie | 228 | 400 | 884 | 496 |
| Fett | 14 | 0 | 99,8 | 26,7 |
| Ges. Fett | 8,1 | 0 | 60,5 | 16,7 |
| Kohlenhydrate | 11,5 | 100 | 0 | 38,4 |
| Zucker | 0,5 | 100 | 0 | 38,4 |
| Protein | 19,6 | 0 | 0 | 26,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:
- Fallback ohne Schätzung versuchen: Wenn alle Prozentsätze festgelegt sind, diese direkt verwenden
- Unterteilungen behandeln: Unterteilungsmengen basierend auf übergeordneter Produktionsmenge zuweisen
- 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
| Modul | Beziehung |
|---|---|
| Zutatensplitter | Parst Zutatendeklarationen in strukturierte Hierarchie |
| Nährstoffunterteilung | Erstellt getrocknete/modifizierte Varianten für Wasserverlustmodellierung |
| Produktnamen-Matching | Verknüpft Zutaten mit Datenbankeinträgen mit Nährstoffprofilen |
| Einheitsgewichtumrechnung | Rechnet Mengen zwischen Einheiten um |
Referenzen
-
CVXPY-Dokumentation. https://www.cvxpy.org/
-
ECOS Solver. Domahidi, A., Chu, E., & Boyd, S. (2013). ECOS: An SOCP solver for embedded systems. European Control Conference.
-
EU-Lebensmittelkennzeichnungsverordnung. Verordnung (EU) Nr. 1169/2011
-
Mahalanobis-Distanz. https://de.wikipedia.org/wiki/Mahalanobis-Distanz
-
EuroFIR Lebensmittelzusammensetzungsdaten. http://www.eurofir.org/