Consumer Values¶
Overview¶
Many of the most interesting questions for food-opt ask how diets respond
to environmental or health pricing — for example, “what would people eat under
a $50/tCO2-eq price?”. Naively letting the optimizer choose the
cheapest macronutrient-and-food-group-feasible diet gives implausible answers:
the unconstrained model will gladly replace half of today’s food consumption
with whatever is cheapest to grow. We need a way to bake revealed consumer
preferences into the objective.
This page documents how the model derives those preferences from a baseline solve and feeds them back as a piecewise utility curve in subsequent solves. The full workflow has three steps:
Solve a baseline scenario with consumption fixed to the observed diet via
validation.enforce_baseline_diet: true(see Current Diets).Extract consumer values as the dual variables of the per-(food, country) consumption equality constraints in that solve.
Calibrate piecewise utility blocks centred on baseline consumption, using the extracted duals as marginal utilities at the baseline quantity.
In subsequent scenarios the diet is freed (enforce_baseline_diet: false)
and the calibrated blocks are applied via food_utility_piecewise.enabled:
true. Consumption then deviates from baseline only when the GHG/health
savings outweigh the consumer-value cost of the deviation.
Tutorial Part 2 (Tutorial) walks through this workflow end-to-end with a small config; the present page focuses on the interpretation of the extracted values and on the model preconditions that make them meaningful.
What the duals encode¶
When enforce_baseline_diet is on, every food consumption link gets an
equality constraint \(p = p_{\mathrm{set}}\) pinning consumption to the
processed baseline diet (see Baseline Diet Estimation). The dual
variable \(\mu_{\text{p\_set}}\) of this equality is the marginal change
in objective per unit of relaxed consumption:
Sign convention in extract_consumer_values.py:
value_bnusd_per_mt = -mu_p_set so that positive values mean consumption
is valuable to the consumer (the model would pay this much per Mt to be
allowed to consume more).
Read carefully, a positive dual is not a “preference” in the everyday sense — it is whatever marginal cost the supply chain has to incur to deliver the next Mt of that food. It bundles together land, water, fertilizer, processing, trade, and emissions costs net of any byproduct value. The calibration relies on the assumption that at the baseline quantity, this marginal supply cost is a reasonable proxy for the consumer’s willingness to pay — i.e. that observed consumption is approximately at the equilibrium of supply cost and demand value. That is a common revealed-preference assumption in food-system modelling.
Negative duals are floored at zero. A negative mu_p_set would mean
the consumer pays the model to take more of the food, which is semantically
backwards as a preference signal — it always indicates a supply-side
artifact (e.g. forced co-product disposal, L1 production-stability dragging
production toward baseline through binding caps elsewhere). The extractor
floors these at zero so downstream consumers see consistent non-negative
values. The clipped count and the most-negative foods are logged for
traceability; the Preconditions section below catalogs the structural
issues that produce them.
Visualisation¶
The figure below shows the per-(food, country) consumer values from the documentation baseline solve, ordered by food group. Each row is one food; the boxen shows the spread across countries. Colours follow the food-group palette used elsewhere in the documentation.
Distribution of consumer values (USD2024 per kg) across 175 countries for each modelled food, derived from the dual variables of the per-(food, country) consumption equalities in the documentation baseline solve. Foods are ordered by food group (group label on the right margin) and within each group by within-group median value. The x-axis is symlog with a linear region near zero; the vertical line at zero separates foods the consumer would pay to consume more of (right) from foods that cost nothing or less to consume more of (left).¶
A few patterns are worth flagging.
Animal products (red meat, processed meat, dairy, eggs, poultry) sit at the high end. This is consistent with their high supply cost — they consume large amounts of crop and grassland feed, land, and emit substantial GHGs.
Cereals and starchy vegetables sit in the low-positive range. They are cheap calorie sources at baseline.
A few oils and seeds cluster near zero. These come from co-products of larger commodity flows (e.g. coconut oil and meal from copra-based coconut production), and their marginal cost is dominated by the byproduct-value side of the balance sheet. The extractor floors these at zero (see above), so any food whose raw dual was negative shows as zero in the figure.
Preconditions for sensible duals¶
Three model details are easy to overlook and each can pollute the extracted
duals if it goes wrong. They are the reason the documentation baseline
configuration looks the way it does (see docs/config/doc_figures.yaml and
config/central.yaml’s baseline scenario).
The fixed diet must be supplyable. If the model cannot deliver the baseline consumption of food f in country c through real production pathways, the food consumption equality is closed by food slack at
validation.slack_marginal_cost(default 50 bn USD/Mt). The dual then saturates at exactly that price with the wrong sign — it reflects the slack penalty rather than any consumer preference.This is most likely to happen for foods that are forced co-products of commodity demands the model represents only partially. Cottonseed oil is the textbook case: cotton is grown for fiber demand (
enforce_fiber_demand), the ginning pathway has fixed coefficients (cotton-lint 0.38, cottonseed oil 0.083, oilseed meal 0.275), and at the global fiber-demand level the joint cottonseed oil output exceeds baseline-diet absorption. Without an outlet the surplus exits via food slack and the cottonseed-oil dual saturates at −50 USD/kg in every country.The mitigation is to give surplus a route to the energy sector via
biomass.disposal_foods(see disposal foods). Foods currently on this list — cottonseed oil, the sesame and groundnut oils and seeds, coconut and coconut oil, foxtail millet — were each identified from a baseline solve where their dual sat at the slack price or had a strongly negative median across countries.The L1 deviation penalty pulls in the same direction. When
deviation_penaltyis enabled withpenalty_mode: "l1"(typical for the central and GSA configurations), the objective gains a term \(l_1 \cdot \sum |a - a_{\mathrm{baseline}}|\) on harvested area per crop. If the modelled outlets for some crop’s production cannot absorb its baseline area, the L1 term drags the corresponding food consumption duals negative — relaxing the consumption equality lets the model grow more of the upstream crop and reduces L1 deviation, so the marginal value of consumption is negative (the consumer would “save” the L1 penalty per extra Mt consumed).Empirically this affected sesame, groundnut, coconut, foxtail-millet, chickpea and gram in earlier baseline solves: each was under-produced relative to its baseline area by 1–6 Mha. The fix is the same as in point 1 — provide a missing real-world outlet (biomass disposal, feed routing, or both).
Redundant constraints can leak into duals. If the diet is enforced per-food via
enforce_baseline_dietand within-group ratios are simultaneously enforced viafood_groups.fix_within_group_ratios, the second set of constraints is mathematically redundant (the per-food p_set already implies the within-group shares) but can split the marginal value across the two constraint families in unpredictable ways. Keepfix_within_group_ratios.enabled: falsewheneverenforce_baseline_dietis on. The same goes for any additional constraint that further pins what is already pinned.
Interpreting disposal flows as a residual diagnostic¶
A useful side benefit of the disposal-route mechanism is that the amount routed to biomass in a baseline solve is the gap between baseline production and what the modelled diet absorbs. Reading these flows answers “what real demand am I missing for this food?”:
Cottonseed oil: ~1.8 Mt globally, all in cotton-fiber-producing countries — the forced co-product story.
Foxtail-millet: ~2.8 Mt — birdseed and forage demand outside of the East and South Asian food markets.
Coconut oil: ~3 Mt — coir/charcoal/husk uses are the missing demand; the L1 baseline is calibrated against total coconut area but the modelled outlets are only food and oil.
Sesame oil and groundnut oil: ~0.8 and ~0–4 Mt respectively — partly post- harvest losses beyond food-group waste factors, partly under-attribution of these oils in the FBS-derived diet.
Where these flows are large or geographically concentrated, they point to
specific model improvements: an explicit non-food demand term (analogous to
fiber_demand for cotton), a finer split between competing pathways, or
revised loss/waste factors. Until those are in place the disposal route is
the pragmatic choice — it lets the L1 baseline reflect total observed area
without poisoning the consumer-value duals.
How the calibrated blocks use the duals¶
The calibrate_food_utility_blocks rule reads values.csv together with
the per-(food, country) baseline consumption levels and emits a piecewise
diminishing-marginal-utility curve per (food, country). The block containing
baseline consumption uses the extracted dual as its marginal utility; blocks
below baseline are more valuable (decline_factor < 1) and blocks above
baseline are less valuable, all parameterised by:
food_utility_piecewise.n_blocks— number of steps per side (default: 4).food_utility_piecewise.decline_factor— geometric ratio between successive block values (default: 0.7, i.e. each step is worth 70% of the previous one).food_utility_piecewise.total_width_multiplier— total width of the curve relative to baseline (default: 2.0, so the curve spans 0 to 2× baseline).
See Configuration and Tutorial for the full configuration reference and a worked example.
Workflow Integration¶
- Snakemake rules:
extract_consumer_values— produces<results>/{name}/consumer_values/{baseline}/values.csvcalibrate_food_utility_blocks— produces<results>/{name}/consumer_values/{baseline}/utility_blocks.csvplot_consumer_values_comparison— produces consumption, objective and consumer-value comparison figures
- Inputs:
Solved baseline network with
mu_p_setduals on food consumption links.
- Configuration parameters:
consumer_values.baseline_scenario— name of the scenario whose duals are extracted (default:"baseline").food_utility_piecewise.enabled— settruefor scenarios that should respond to consumer values; must befalsein any scenario that also setsenforce_baseline_diet(the validation layer rejects the combination).food_utility_piecewise.n_blocks,decline_factor,total_width_multiplier— block geometry described above.
- Output schema (
values.csv): food, food_group, country, value_bnusd_per_mt, adjustment_bnusd_per_mt.value_bnusd_per_mtandadjustment_bnusd_per_mtdiffer only in sign (the latter is what gets added to the marginal cost of the consumption link in subsequent solves).