简介: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_index=True) # 按食物名称聚合可食部分量 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_index=True决定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 调整前后的数值对比表
正文给出的调整后数据整理成表如下,男女生分开列出:
| 指标 | 调整前男生 | 调整后男生 | 调整前女生 | 调整后女生 | 参考摄入量近似值 |
|---|---|---|---|---|---|
| 能量(千卡) | 2268 | 2191 | 1870 | 1802 | 2200 / 1800 |
| 蛋白质(克) | 81.6 | 112.3 | 64.6 | 86.1 | 110 / 85 |
| 脂肪(克) | 74.7 | 47.8 | 54.5 | 36.8 | 50 / 40 |
| 碳水化合物(克) | 292.3 | 238.3 | 248.9 | 184.1 | 250 / 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 # 读取附件3,keep_default_na=False避免空值被读成NaN df = pd.read_excel("附件 3.xlsx", keep_default_na=False) # 把每个字段转成字典,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, lowBound=0, cat="Continuous") # 目标:总费用最低 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] == '是',只有允许半份购买的食物才把摄入量上限压到半份。
参数说明:lowBound=0保证所有食物的摄入量非负。cat="Continuous"表示连续变量,如果换成"Integer"就变成整数规划,求解速度会明显下降,而这个场景下食物重量本来就是连续的,没必要取整。nutrient_map[col][f] / 100.0这一步是把"每100克含量"换算成"每克含量",漏掉这一步,能量约束会变成2400*100,结果直接无解。prob.status返回1表示最优解,-1表示无可行解(Infeasible),0表示未求解,-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_index=True)把两份数据纵向粘在一起,后面直接对合并结果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明细翻给评委看,每个食物一行,克数、价格、营养素列全展开,比空口解释有说服力得多。从那以后我每次跑这类食谱优化题,都会强制走一遍"约束方向自检、单位换算验证、回代检查"三个步骤,几乎没再翻过车,希望帮到你。
本文还有配套的精品资源,点击获取