尧图精选

2024电工杯B题:膳食营养评价与线性规划食谱优化完整代码解析

🕒 发布时间:2026/10/2 15:37:11 📁 来源:尧图网络
简介2024年电工杯B题数学建模完整过程与代码解析面向参加电工杯及营养膳食优化类建模竞赛的高校学生。内容以大学生一日食谱为对象完整演示了膳食营养评价、食谱调整改进、优化模型建立与求解全流程包含能量与各类营养素摄入量计算公式、男女学生调整前后的对比结论、Python数据处理代码示例以及基于平衡膳食原则的改进策略如增加优质蛋白与维生素、减少脂肪和碳水化合物摄入。整个方案条理清晰可作为竞赛论文写作和代码实现的直接参考。包体为单个docx文档体积仅32KB虽小巧但覆盖了从问题概述、步骤拆解到代码实现的关键内容适合快速阅读与复用。该资源已有892人学习下载受到建模参赛者关注。对于缺少思路或希望验证自身建模过程的同学这份全析全解能帮助节省大量查阅资料和调试代码的时间值得参考。1. 2024电工杯B题完整代码把膳食营养评价变成可复现的计算链2024电工杯B题真正卡人的不是建模思路而是把四份附件串成一条能跑通的计算链。附件1、附件2是男女两份一日食谱附件3是食堂食物信息附件4是营养参考摄入量标准题目要求先按附件4去评价前两份食谱再做调整最后用附件3搭线性规划优化出日食谱和周食谱。这份完整过程与完整代码的价值在于它把能量怎么算、营养素达不达标怎么判、线性规划怎么建、三种目标怎么比整条路径都铺好了适合想拿一套可复现模板的竞赛选手也适合想看看pandas聚合和PuLP求解器怎么配合的一线数据分析者。本文按实际执行顺序拆开讲重点落在参数含义和那些容易让人翻车的点上。2. 膳食营养评价五步法从能量到微量元素的逐项核算2.1 五步法的执行顺序为什么不能乱附件4给的是一套完整的平衡膳食参考标准附件1和附件2分别是男生和女生的一日食谱。评价流程拆成五步先算能量再算蛋白质、脂肪、碳水化合物三大产能营养素然后看食物类别结构接着排查维生素和矿物质的缺乏或过量最后才进入调整环节。这个顺序有讲究——能量和三大营养素决定食谱的整体框架膳食结构决定食物来源是否多样微量营养素决定要不要定点补充。如果一上来就盯着钙和铁很容易忽略能量超标这个更严重的问题。以正文给出的评价结果为例附件1男生能量2268千卡、女生1870千卡均高于参考摄入量蛋白质男生81.6克、女生64.6克又低于参考值脂肪和碳水化合物也双双偏高。这个结果就是典型的能量过剩但蛋白质不足——食堂食谱普遍主食和油脂给得多优质蛋白和蔬果给得少。读评价结论的正确姿势是把所有指标分成超标和不达标两列超标项决定砍什么不达标项决定补什么。五步法的另一个作用是给后续建模阶段提供约束来源。评价结论里哪些营养素缺口最大、哪些已经超标直接决定了优化模型里上下界该怎么设。所以这一步不是走过场而是整个建模的输入条件。2.2 核心计算公式与参数约定正文给出的计算通式是Energy Σ (Food_amount_i × Energy_per_100g_i × Edible_part_rate_i × Portion_i)蛋白质、脂肪、碳水化合物、维生素和矿物质的计算结构完全一致只是把每100克含量换成对应的营养素列。四个参数里最容易翻车的是可食部分率和食用份数Food_amount 表示一次取用了多少克Edible_part_rate 表示其中能吃的比例比如带骨鸡腿的可食率大约0.7整条鱼更低Portion 表示这份食物一天吃了几次。三者相乘才是真正进到嘴里的量。注意附件4的营养成分表里每100克含量通常是基于可食部分的。如果某列数据本身已经标成每份含量就不要再乘可食部分率否则会重复折算数值直接翻倍。另外还要确认单位能量一般是千卡蛋白质脂肪碳水是克维生素A是微克维生素C是毫克。混用单位是这类题目里最常见的低级错误我在第5章会专门讲一个量级差100倍的案例。2.3 用pandas把一日食谱聚合出营养素总量正文代码的第一步是读取附件1和附件2并合并然后按食物名称聚合。这里有个细节值得提合并之前要确认两份附件列名完全一致否则concat之后会出现NaN。我一般会先打印列名对齐一次再执行聚合import pandas as pd import numpy as np male pd.read_excel(附件 1.xlsx) female pd.read_excel(附件 2.xlsx) # 先检查列名是否一致避免合并后字段错位 print(附件1列名:, male.columns.tolist()) print(附件2列名:, female.columns.tolist()) students pd.concat([male, female], ignore_indexTrue) # 按食物名称聚合可食部分量 food_intake students.groupby(食物名称).agg({可食部分量: np.sum}) # 按食物名称聚合能量 energy_intake students.groupby(食物名称).agg({能量: np.sum}) # 按食物名称聚合营养素 nutrient_intake students.groupby(食物名称).agg({ 蛋白质: np.sum, 脂肪: np.sum, 膳食纤维: np.sum, 维生素 A: np.sum, 维生素 C: np.sum, 钙: np.sum, 铁: np.sum }) print(能量摄入:, energy_intake.sum()[能量], 千卡) print(蛋白质摄入:, nutrient_intake.sum()[蛋白质], 克)逻辑说明先把两份Excel读进来再按食物名称分组求和。groupby(食物名称)会把同名食物合并成一行比如两餐都出现的米饭会被累加Sum的结果就是该食物全天摄入总量。agg字典里每个键对应一个聚合函数键名必须和Excel表头完全一致多一个空格都会报KeyError。参数说明ignore_indexTrue决定concat后行索引是重新从0编号还是保留原附件索引数据处理阶段建议设为True避免后续merge时索引冲突。np.sum传的是函数对象而不是sum()的调用结果新手容易把括号带上写成np.sum()那样会在分组前就完成求和然后agg拿到一个标量直接报错。如果题目要求分别评价男生和女生就要把代码拆开对male和female分别执行groupby而不是合并后再算一次。正文原代码里合并后输出的其实是男女合计值标题却写成附件1的评价这个混淆我在避坑章里再展开说。算完的数字本身没有意义必须拿附件4的参考摄入量去比超标项和不达标项分开列才能进入下一步调整。3. 食谱调整改进四条策略与二次评价的量化对比3.1 从评价结论反推调整方向调整不是多吃菜少吃饭这种口号而是针对评价结果做定点替换。正文给出的四条策略是增加高蛋白食物鸡胸肉、鸡蛋、瘦牛肉增加维生素来源水果、蔬菜、全谷类换成低脂食材鱼类、豆类、脱脂奶换成低GI主食燕麦、红薯、全麦面包。这四条正好对应上一章诊断出的三类问题蛋白质不足、脂肪偏高、碳水偏高蔬菜和水果的补充则同时解决维生素A和钙缺乏的问题。实际操作时我会先列一份问题清单再对着附件3的食物信息表找替换物。比如原食谱里的猪肉包子脂肪偏高可以用鸡胸肉三明治替白米饭GI值高可以用燕麦或红薯替。每替换一样就要重新累加一次营养素直到所有指标落进附件4的允许区间。替换的粒度可以细到同一类食物换品种也可以粗到整个餐次重排看附件3里可选食物有多少。这里有个经验替换时优先保蛋白质来源因为蛋白质是食堂食谱里最容易缺的硬指标。脂肪和碳水可以通过调烹饪方式和主食种类来压蛋白质不足则必须靠实打实的高蛋白食物来补。虾、瘦牛肉、鸡胸肉、鸡蛋、豆腐这些在附件3里通常都有价格差异也不大。3.2 调整前后的数值对比表正文给出的调整后数据整理成表如下男女生分开列出指标调整前男生调整后男生调整前女生调整后女生参考摄入量近似值能量千卡22682191187018022200 / 1800蛋白质克81.6112.364.686.1110 / 85脂肪克74.747.854.536.850 / 40碳水化合物克292.3238.3248.9184.1250 / 180参考摄入量在附件4里本身是一个区间或随体重浮动的值表里取的是正文实际使用的近似数。调整后的特点是能量从超标压回接近区间蛋白质从不足提上来脂肪和碳水同时降下去。这就是结构修正的含义——四类指标联动变化而不是只改某一个。只看单一指标永远不会发现能量和蛋白质其实是绑定在一起的蛋白质不足常常伴随能量超标因为高蛋白食物被高油脂主食挤掉了份额。判断达标的标准不要盯着完全等于参考值附件4给的本身就是区间落在区间内就算合格。比区间略高或略低一点也可以接受关键是不能出现能量超了但蛋白质不足这种结构性问题。3.3 新评价要重算一遍而不能只改结论调整完必须重新跑一遍第2章的计算代码因为食物替换会连锁改变所有营养素不可能只动一栏。做法是把附件1和附件2里被替换的食物行改掉重新执行groupby再对照附件4判断达标情况。正文给的结论是调整后能量、蛋白质、脂肪、碳水化合物都接近参考摄入量但维生素A和钙仍有缺乏需要根据个人情况补充。这说明微量元素单靠换食材不一定补得够。维生素A在动物肝脏里含量高但食堂未必有钙在奶制品里含量高但部分学生乳糖不耐受这种情况下可以考虑走强化食品或额外补充的路线。对建模的意义在于进入优化阶段时微量元素的约束上下界要留有余量不要定死成必须达标否则线性规划很可能无解。如果附件3里没有题目指定的那些食物我的做法是找同类替代——鸡胸肉没有就用去皮鸡腿肉燕麦没有就用玉米或紫薯脱脂奶没有就用无糖豆浆。替代的原则是尽量保持蛋白质和膳食纤维接近脂肪和碳水尽量低。这一步不需要写复杂代码一张Excel手工替换表就够重点是把替换前后的差异记下来方便论文里写清楚调整依据。4. 日食谱优化建模三种目标函数的PuLP实现4.1 为什么用线性规划而不是穷举或遗传算法附件3提供的食物种类有限但组合方式仍然是指数级的穷举不现实。遗传算法对这类线性约束问题属于杀鸡用牛刀每次运行结果还不稳定竞赛论文里很难解释清楚收敛性。线性规划是教科书级的选择目标函数和约束都是线性的PuLP配合CBC求解器可以直接拿到全局最优解数值稳定且可复现。正文一共给了三种目标函数一是蛋白质氨基酸评分最大二是用餐费用最经济三是兼顾评分与经济性。三者共用同一组约束——能量、蛋白质、脂肪、碳水化合物的摄入量上下界以及半份购买限制。区别只在目标函数的写法评分最大用LpMaximize费用最小用LpMinimize兼顾模型里引入权重λ把两个目标压成一个。这里必须提醒一个正文里的笔误评分最大模型的约束写成b_i x_i ≤ B1但B1定义为最低摄入量。≤ 最低摄入量在数学上允许摄入量低于下限求解器为了最大化评分会把食物量压到最低得到一个最优但物理上错误的解。正确写法应该是≥ B1并把能量、蛋白质、脂肪、碳水都设为既有下限又有上限的双向约束。4.2 三种目标函数怎么在PuLP里表达费用最小化在PuLP里最直观LpProblem设置成LpMinimize目标函数是价格乘以摄入量的求和。氨基酸评分最大化则把problem类型换成LpMaximize目标函数换成评分系数乘以摄入量。兼顾模型写作max Σ a_i x_i - λ Σ p_i x_iλ越大费用惩罚越重解越偏向经济型λ越接近0解越偏向评分型。代码实现上三种模型共用同一套约束块只是目标函数不同。我的习惯是把约束写成列表函数再分别新建三个problem实例而不是反复修改同一个problem。这样后面比较三组解时数据不会互相污染论文里要贴三组结果也方便。注意PuLP的变量对象不能跨problem共用每次都要重新LpVariable.dicts生成一批新变量。4.3 费用最小化模型的完整可运行代码下面这版把正文代码整理成了可以直接跑的形式做了两个关键优化一是把df.loc[df[食物名称]f]这种循环查询换成字典映射数据量大时速度差几十倍二是把上下界集中到bounds字典里管理改参考摄入量只需要改一处import pandas as pd import numpy as np from pulp import LpProblem, LpVariable, LpMinimize, lpSum, value # 读取附件3keep_default_naFalse避免空值被读成NaN df pd.read_excel(附件 3.xlsx, keep_default_naFalse) # 把每个字段转成字典key是食物名称避免循环里反复loc price_map df.set_index(食物名称)[价格].to_dict() amount_map df.set_index(食物名称)[可食部分量].to_dict() half_map df.set_index(食物名称)[是否可以购买半份].to_dict() nutrient_cols [能量, 蛋白质, 脂肪, 碳水化合物, 维生素 A, 维生素 B1, 维生素 B2, 维生素 C, 钙, 铁] nutrient_map {col: df.set_index(食物名称)[col].to_dict() for col in nutrient_cols} foods list(price_map.keys()) # 费用最小化模型 prob LpProblem(Diet_MinCost, LpMinimize) x LpVariable.dicts(food, foods, lowBound0, catContinuous) # 目标总费用最低 prob lpSum([x[f] * price_map[f] for f in foods]) # 约束每种营养素的上下界格式为 (下限, 上限) bounds { 能量: (2400, 2800), 蛋白质: (100, 150), 脂肪: (80, 100), 碳水化合物: (500, 600), 维生素 A: (1000, 3000), 维生素 B1: (1.2, 1.8), 维生素 B2: (1.3, 1.8), 维生素 C: (100, 200), 钙: (800, 1200), 铁: (15, 20), } for col, (lo, hi) in bounds.items(): expr lpSum([x[f] * nutrient_map[col][f] / 100.0 for f in foods]) prob expr lo prob expr hi # 半份约束只有允许半份购买的食物才设上限 for f in foods: if half_map[f] 是: prob x[f] 0.5 * amount_map[f] prob.solve() print(求解状态:, prob.status) print(总费用:, round(value(prob.objective), 2), 元) for f in foods: v x[f].varValue if v and v 0.01: print(f, round(v, 2), 克费用, round(v * price_map[f], 2), 元)逻辑说明先把附件3的每个字段转成以食物名称为键的字典后面写nutrient_map[col][f]就是一次字典查询加变量相乘比循环里反复df.loc[df[食物名称]f]快得多也不会因为食物名称重复而取错行。约束部分用bounds字典统一管上下界循环里对每一列加两条约束 lo和 hi。半份约束先判断half_map[f] 是只有允许半份购买的食物才把摄入量上限压到半份。参数说明lowBound0保证所有食物的摄入量非负。catContinuous表示连续变量如果换成Integer就变成整数规划求解速度会明显下降而这个场景下食物重量本来就是连续的没必要取整。nutrient_map[col][f] / 100.0这一步是把每100克含量换算成每克含量漏掉这一步能量约束会变成2400*100结果直接无解。prob.status返回1表示最优解-1表示无可行解Infeasible0表示未求解-2表示无界-3表示数值错误这是排查的第一步。注意附件3里营养素列如果用的是每份含量而不是每100克含量/ 100.0就不要加。跑之前先打印nutrient_map[能量][米饭]看一眼量级米饭每100克能量大约115千卡如果打印出来是11500说明单位是每千克或每份需要重新确认表头。三种目标的切换很简单把LpMinimize改成LpMaximize目标函数换成lpSum([x[f] * score_map[f] for f in foods])score_map就是每种食物的氨基酸评分系数兼顾模型则写成LpMaximize加lpSum([x[f] * score_map[f] for f in foods]) - lam * lpSum([x[f] * price_map[f] for f in foods])λ自行设定后跑三遍结果分开存。注意变量对象不能复用每次新建problem都要重新建变量。5. 避坑食谱建模与数据处理的五个翻车现场5.1 约束方向写反线性规划给了个最优但错误的解现象用蛋白质氨基酸评分最大化模型求解得到的食谱能量只有几百千卡明显低于参考摄入量。检查目标函数没问题约束也都加了但结果就是不合理。原因正文第一个优化模型里约束写成b_i x_i ≤ B1而B1是最低摄入量。≤ 最低摄入量允许摄入量低于下限求解器为了最大化评分当然会把所有食物量压到最便宜的组合上得到一个数学上最优、物理上错误的解。线性规划遇到这种情况不会报错它会正常返回一个最优解所以特别隐蔽。解决把能量、蛋白质、脂肪、碳水这几项核心约束统一写成≥ 参考下限和≤ 参考上限两组。参考下限来自附件4上限取下限的1.2到1.5倍。我用bounds字典集中管理这些值注释里写清≥是硬约束≤是安全余量避免约束散落在代码各处改漏一处。5.2 每100克含量与食用份量混用营养素量级差100倍现象求解结果里能量动辄几万千卡或者钙只有个位数毫克怎么看都不像人能吃的食谱。原因附件3的营养成分表约定的是每100克可食部分的含量。代码里直接写x[f] * nutrient_map[能量][f]x单位是克能量单位是每100克千卡相乘后放大100倍。蛋白质、脂肪等同样受影响只有价格这项不受影响所以结果看起来费用合理但营养离谱。解决定义nutrient_map时统一除以100把列名改成_per_g再参与计算或者在约束表达式里显式写/ 100.0。跑之前先手工验证一行比如100克米饭能量约115千卡看代码输出是否在这个量级。这个检查30秒就能做完能省下后面一小时的排查时间。5.3 半份约束误伤不能半份的主食现象部分食物求解出来只有几克食堂根本不可能按这个量卖比如馒头算出2.3克。原因半份约束被无条件写成x[f] 0.5 * amount_map[f]但附件3里是否可以购买半份列标着否的食物也进了这个约束。另外amount_map[f]存的是每份可食部分量而非可购买量如果一份米饭200克半份应该是100克碰上带骨鸡腿这种一份300克、可食率0.7的食物可食部分量和可购买量就完全不是一回事。解决先判断half_map[f] 是才加半份约束。然后把可食部分量和可购买量分开两个字典存约束里用可购买量营养素计算里用可食部分量。更稳妥的做法是对不能半份的食物不设上界求解后单独检查单品种最小购买量如果算出来低于一份量round到一份再人工复核一遍食谱是否合理。5.4 concat与merge后索引错位评价对象张冠李戴现象附件1和附件2合并后算出来的男生能量摄入和女生能量摄入完全一样或者食物总量翻倍。原因pd.concat([male, female], ignore_indexTrue)把两份数据纵向粘在一起后面直接对合并结果groupby得到的是男女合计值而不是各自的值。而merge多表关联时如果食物名称在附件3里有重复行行数会膨胀聚合结果直接翻倍这属于典型的索引错位。解决男女生分开读、分开聚合最后再拼到一张评价表。如果一定要走concat就加一列性别作为groupby的分组键。凡是用到merge先df.duplicated(食物名称).sum()检查是否有重复名称有重复就先按名称聚合再merge不要带着重复行去关联。5.5 求解器报Infeasible先查哪类约束现象prob.solve()返回Infeasible所有变量值都是None输出结果全是0整个人懵在原地。原因大部分情况是上下界互相打架。比如能量上限2800千卡蛋白质下限150克如果附件3里没有足够多高蛋白低能量的食物选项这两个条件同时成立就可能无解。另一个常见原因是半份约束与最低摄入量冲突——把每个食物都限死在半份以内又没有足够的食物种类去凑够营养素下限组合空间直接被砍没。解决用松弛法排查。把所有≥约束临时改成≥ 0逐步恢复每次恢复一条就看prob.status是否回到1卡在哪条就是哪条的问题。或者把某条约束的下限改成0再求解三分钟内能定位到冲突约束。定位后通常两个选择放宽某一个上下界或者从附件3里剔除那些价格异常低但营养成分单一的食物缩小变量空间反而更容易出解。6. 周食谱扩展与验证轮换法、λ敏感性与回代检查6.1 从日食谱到周食谱的扩展思路周食谱不推荐把7天×3餐×食物种类一次性丢进线性规划变量数会膨胀到几百个CBC求解慢不说结果还因为约束过紧变得不可解释。常见做法是先让日食谱模型跑出最优解再用同类别轮换生成周食谱——周一用鸡胸肉周二换瘦牛肉周三换豆腐蛋白质总量和氨基酸评分都能控制在±10%以内波动。如果题目明确要求以周为目标建模另一种做法是把7天约束叠加同一目标函数拉成7段但权重系数要对齐代码量翻倍论文里解释起来也更费劲。6.2 三种模型的比较与回代验证正文要求对三种目标得到的日食谱做比较分析核心看两个维度氨基酸评分和用餐费用。把三组解拉成对比表最经济的解评分低但便宜评分最大的解贵但营养结构好兼顾模型在λ取中间值时落在前两者之间。λ从0到1变化时可以画一条评分-费用的帕累托前沿这一步在论文里是加分项。验证代码的思路是回代把求解得到的每种食物克数代回第2章的评价公式重新计算总能量和各营养素与附件4参考值比对。如果回代结果和求解器输出对不上优先怀疑单位换算而不是求解器。6.3 结果可视化与收尾习惯最后把三组食谱的营养素达标情况做成柱状图或雷达图横轴是参考摄入量的百分比一眼能看出短板在哪个营养素。答辩时直接把Excel明细翻给评委看每个食物一行克数、价格、营养素列全展开比空口解释有说服力得多。从那以后我每次跑这类食谱优化题都会强制走一遍约束方向自检、单位换算验证、回代检查三个步骤几乎没再翻过车希望帮到你。本文还有配套的精品资源点击获取
上一篇/下一篇内容由系统自动关联 返回资讯列表 →