Skip to content
Back to the work

Optimization

Pricing the cost of variety in hospital meal planning

A mixed-integer program that feeds patients within clinical limits, on budget — and tells the manager exactly what menu variety costs.

Context
MMA 861 — Queen’s University
Role
Model formulation, constraint design and sensitivity analysis
Timeline
2026
Mixed-Integer ProgrammingPythonSciPy (HiGHS solver)PlotlyShadow Price Analysis
11
constraint families encoded
9
meals per optimization block
400
patient baseline modelled

Problem

Manual meal planning fails on at least one of cost, clinical compliance or variety — and managers have no way to quantify what fixing one costs them on the others.

Approach

A mixed-integer linear program over three-day, nine-meal blocks: binary ingredient selection plus continuous portion sizing, under clinical, culinary and rotation constraints.

Result

Least-cost compliant menus, plus a quantified “cost of variety” and shadow prices identifying which nutritional requirement is most expensive to satisfy.

The trilemma

A hospital food service manager is asked to do three incompatible things every day:

  1. Minimise ingredient cost — the budget is fixed and food is a large line item.
  2. Meet strict clinical nutritional profiles — a diabetic patient needs 90–132g of protein daily and carbohydrate control within each meal, not just across the day.
  3. Keep patients eating — repetitive menus cause food fatigue, and a patient who stops eating is a clinical problem, not a catering one.

Manual planning routinely fails on at least one. You either blow the budget, miss the clinical target, or serve chicken and rice for the fourth time this week. And critically, a manager who trades one off against another has no number telling them what the trade cost.

That’s what makes this an optimization problem rather than a scheduling chore. The goal isn’t only to produce a menu — it’s to produce a menu and a price tag on each rule that constrained it.

Why mixed-integer, not linear

The model needs two fundamentally different kinds of decision at once:

  • x — binary: is ingredient i on the plate for meal j? Yes or no.
  • q — continuous: if it is, how many grams?

You cannot express “exactly one fruit per meal” or “oatmeal and beef never share a plate” in a purely continuous model. Those are logical rules and they need integer variables. But portion sizing is genuinely continuous — 142g of chicken is a legitimate answer.

The two are linked by a constraint that does a lot of quiet work:

qMin(i,j) · x(i,j)  ≤  q(i,j)  ≤  qMax(i,j) · x(i,j)

If the ingredient isn’t selected, x = 0 forces the portion to exactly zero. If it is selected, the portion is bounded into a range that’s realistic for that meal. Without it, the solver cheerfully serves 5g of chicken or 500g of rice to hit a nutrient target.

Objective: minimise total ingredient cost — the sum of unit cost × quantity across all ingredients and meals.

The constraints are the actual work

Eleven constraint families, and each one exists because the solver did something stupid without it. That’s the honest story of building a MILP: you are not writing rules, you are closing loopholes.

Constraint The loophole it closes
C1 Daily nutrient bounds — protein, fat, carbs, fibre, vitamins, minerals within clinical range Without it, “cheapest” means nutritionally void
C2 Per-meal calorie split — breakfast 35%, lunch 40%, dinner 25% The solver dumps all calories into one giant meal and starves the other two
C3 Diabetic per-meal carbs, 45–60g Daily carb bounds alone let it concentrate carbs into one spike
C4 Exactly one fruit per meal; 1–4 non-fruit items Otherwise it optimises to a plate of one cheap ingredient
C5 Culinary incompatibilities — no oatmeal with beef, no granola with salmon A solver has no palate. It will pair anything if it’s cheap
C6 Breakfast: ≥1 protein, exactly 1 grain, exactly 1 fat Nutritionally valid ≠ recognisable as breakfast
C7 Lunch/dinner: ≥1 protein, ≥1 vegetable, 1–2 grains Plate-ratio structure per nutritional guidance
C8 Portion bounds, meal-specific Prevents absurd serving sizes; links q to x
C9 Day-occurrence tracking Bookkeeping that makes rotation rules expressible
C10 An ingredient appears at most once per day No chicken at both lunch and dinner
C11 Each ingredient on at most MaxDays of 3 days Forces rotation instead of reusing the cheapest items daily

C5 is my favourite illustration of why domain knowledge can’t be skipped. The solver has no concept of a meal being edible together. It sees nutrients and prices. Every culinary rule a human applies unconsciously has to be written down as an explicit inequality, or the “optimal” answer is granola with salmon.

The managerial output: what does variety cost?

This is the part that makes it a business tool rather than a homework problem.

Variety is enforced by the rotation constraints (C10, C11). Relax them and cost falls — the solver reuses the cheapest compliant ingredients every day. Tighten them and cost rises. The difference between those two solutions is the price of patient satisfaction, in dollars.

That single number reframes the conversation. A manager arguing for menu variety is no longer making a soft appeal; they’re saying “rotation costs us X per patient per week, and here’s the food-waste and clinical cost of not paying it.”

Run across all five clinical profiles, the number is consistent: guaranteeing no ingredient repeats more than once every two days raises 3-day food cost by 37% on average — from $6.81 to $9.34 per patient — over the cost-only optimum. It also roughly doubles distinct-ingredient count, from the high teens to the mid-twenties per 3-day block.

3-day food cost, by clinical profile and rotation rule
3-day food cost by clinical profile, across rotation strictness Grouped bar chart. Five clinical profiles on the x-axis. Three bars per profile: Cost Only, Hard Constraint 3-day max, Hard Constraint 2-day max. Cost rises roughly 25-40 percent as rotation rules tighten. $0 $3 $6 $9 $12 Normal Male — Cost Only: $7.05 Normal Male — Hard Constraint (3-day max): $9.93 Normal Male — Hard Constraint (2-day max): $9.81 Normal Male Normal Female — Cost Only: $7.31 Normal Female — Hard Constraint (3-day max): $10.67 Normal Female — Hard Constraint (2-day max): $10.67 Normal Female Diabetic — Cost Only: $7.88 Diabetic — Hard Constraint (3-day max): $9.57 Diabetic — Hard Constraint (2-day max): $9.30 Diabetic High Cholesterol — Cost Only: $5.75 High Cholesterol — Hard Constraint (3-day max): $8.28 High Cholesterol — Hard Constraint (2-day max): $8.28 High Cholesterol DASH — Cost Only: $6.06 DASH — Hard Constraint (3-day max): $8.63 DASH — Hard Constraint (2-day max): $8.63 DASH Cost Only Hard Constraint (3-day max) Hard Constraint (2-day max)
Output of the notebook's solver-comparison pass — three MILP runs per clinical profile, identical constraints except rotation strictness. Every profile gets more expensive as ingredient repeats are restricted, which is exactly the trade the model exists to price.

Sensitivity: which clinical rule is expensive?

To go further, I fixed the optimal binary selections from the MILP and ran an LP relaxation on the continuous portion variables. Integer programs don’t yield meaningful shadow prices — duality doesn’t hold cleanly with integrality constraints — so this two-stage approach recovers usable dual values on the nutritional constraints while respecting the discrete menu the MILP chose.

The shadow prices answer a question the menu itself can’t: which clinical requirement is disproportionately driving total cost? If the protein floor carries a large dual value, that’s a direct signal to renegotiate protein sourcing — the highest-leverage procurement action available, identified analytically rather than by intuition.

The compliance chart below is the check that has to pass before any of that matters: does the menu actually land inside its clinical bounds, or does it hug the edges in a way that’s technically feasible but clinically fragile?

Nutrient compliance — diabetic profile, 2-day rotation solver
Nutrient compliance, Diabetic profile, 2-day rotation solver Ten nutrients shown as a bar from the clinical minimum to the actual 3-day average, positioned within the allowed min-max range. Potassium and fibre sit near the top of their range; sodium and protein sit comfortably low. Protein Protein: 94.4g (range 90-132g) — 10% of allowed range 94.4g [90–132] Carbs Carbs: 176.2g (range 130-240g) — 42% of allowed range 176.2g [130–240] Fat Fat: 62.4g (range 60-90g) — 8% of allowed range 62.4g [60–90] Sat. Fat Sat. Fat: 12.3g (range 0-18g) — 68% of allowed range 12.3g [0–18] Fibre Fibre: 30.2g (range 30-42g) — 2% of allowed range 30.2g [30–42] Calcium Calcium: 861mg (range 800-1000mg) — 30% of allowed range 861mg [800–1000] Sodium Sodium: 623mg (range 0-2300mg) — 27% of allowed range 623mg [0–2300] Potassium Potassium: 3116mg (range 2080-3120mg) — 100% of allowed range 3116mg [2080–3120] Iron Iron: 10.8mg (range 8-45mg) — 8% of allowed range 10.8mg [8–45] Vit. D Vit. D: 8.3mcg (range 5-33mcg) — 12% of allowed range 8.3mcg [5–33] Min 50% Max
Each bar is the 3-day average, positioned between the clinical min and max. Potassium sits at the ceiling — the binding constraint for this profile — while sodium and protein have slack. That's the shadow-price story in one chart: potassium is where sourcing has the least room to move.

Data

Input Source
Nutritional profiles per gram USDA food databases
Clinical daily min/max intake Health Canada guidelines, scaled to a 400-patient baseline
Ingredient unit costs Wholesale market averages (synthetic — no supplier data required)

Using synthetic costs was deliberate. The model’s validity rests on its structure, and synthetic-but-realistic pricing lets it be tested and demonstrated without a procurement NDA.

Limitations

  • Scoped to a nine-meal block. The proposal was written around Excel Solver’s variable limits; the build outgrew that and runs on SciPy’s HiGHS MILP backend instead, but the nine-meal window stayed — a production version needs it extended to a full week.
  • Profiles solved independently, not jointly. Each of the five clinical profiles gets its own MILP run. A real hospital runs many diet types out of one kitchen sharing a single ingredient pool — the harder and more interesting version is a shared-ingredient, multi-profile model solved simultaneously.
  • Deterministic costs. Food prices move seasonally; the plan is optimal for the prices you fed it.