简介2024电工杯B题数学建模完整过程与代码解析面向数学建模竞赛参赛者针对大学生一日食谱的膳食营养评价与优化问题提供从数据分析到建模求解的全流程参考。压缩包内含1个docx文档大小约32KB内容结构清晰当前已有892人学习。文档先对附件1、附件2食谱进行能量与营养素评价涵盖蛋白质、脂肪、碳水化合物、维生素及矿物质等指标并给出调整改进方案增加优质蛋白、减少高脂高糖食物等随后展示调整前后对比数据与营养素计算公式附Python代码示例便于读者复现计算过程。此外还介绍了建立线性规划等优化模型求解最优食谱的思路以及论文正文与附录的撰写建议适合备赛冲刺、学习建模方法和快速查漏补缺。1. 电工杯B题为什么“完整代码”比“获奖论文”更值钱每年电工杯成绩公布后总有一批人下载一堆获奖论文却发现没有一篇能照着复现公式跳步、参数不写、数据集口径模糊。而真正让人成长的内容反而是那些把“从读题到提交”逐行展开、每个参数都讲清楚出处的完整代码全析。B题的特点是工程背景重——数据表格里带着明显的设备状态或能耗特征模型结论是要给“解决实际运行问题”这个目标服务的不是算出一个数就完事。这类题光看懂思路没用你得能跑通、能改参数、能回答“为什么用这个模型而不是那个模型”。这篇内容按竞赛完整流程来写拿到题怎么拆、数据怎么清洗、特征怎么构造、模型怎么落地、结果怎么验。新手可以照着代码逐步跑通熟手可以直接跳到调参和避坑部分核对细节。我会把每一段代码背后的选型理由和参数边界写清楚因为真正让你在赛场上拉开差距的就是对边界条件的理解。2. 读题与建模方向拿到B题后先拆解哪些信息2.1 把题目拆成“变量、目标、约束”三张清单很多参赛者拿到B题就急着找数据这是翻车的起点。我通常先花两到三个小时把题目从头到尾读三遍边读边在纸上列三张清单变量、目标、约束。变量是题目里所有能测到或能计算的量目标是你最终要优化的那个标量或向量约束是那些“不能超过”“必须满足”的边界条件。以某工业设备能耗建模的模拟项目X为例题目给出的原始字段可能包括设备启停状态、负载率、工作时间、环境温度等十几个列。如果我一开始就直奔相关性分析很容易被噪声带偏。正确做法是先定义清楚“能耗”这个目标到底是以15分钟为粒度的负荷曲线还是一个班次的累计电量。粒度定义不同后面的特征工程和模型结构完全是两套方案。建议在第一张清单上给每个变量标注类型连续/离散/时间、量纲、缺失风险这会给第3章的预处理减少大量返工。再把题目里的约束逐条翻译成数学表达。比如“设备最长连续运行时间不得超过X小时”这在优化类问题里就是一条硬约束而在预测类问题里它可能只是特征工程里的一个截断条件。这里有一个容易被忽略的点约束还要区分硬约束和软约束硬约束违反就判无效软约束违反只是扣分或降低可行性评分。把这两类分开写建模时才不会被一个并不存在的强限制卡住选型。2.2 三类主流建模路线回归预测、优化调度、综合评价电工杯B题历年方向集中在三类一类是让参赛者预测某个量能耗、负荷、状态趋势一类是让参赛者给出调度或配置方案设备组合、时序安排一类是让参赛者对多个方案做综合评价与排序。这三类路线的建模工具差异很大我列了一个选型参考表路线类型典型问题信号常用模型工具交付物形式回归/预测型“预测未来…”“估计…的变化趋势”线性回归、树模型、LSTM预测值序列 误差曲线优化调度型“如何安排…使…最小/最大”线性规划、整数规划、遗传算法最优方案表 收敛曲线综合评价型“对…进行评价/排序/分级”层次分析法、熵权法、TOPSIS权重表 综合得分排序如果你不确定题目属于哪一类有一个快速判断法题目里真正要交的“答案”是一个数字序列还是一个方案表格。B题很少要求纯机理推导几乎都要落到可执行的结果上。选型时我只坚持一条原则先保证模型可解释再考虑精度。竞赛论文的评阅者通常是电气工程背景而不是机器学习背景他们更信任能讲清物理含义的模型。2.3 构建最小可行基线先跑通再谈优化一个常被低估的做法是在拿到干净数据后的第一个小时先做一个“笨但完整”的基线模型。比如预测任务先用线性回归或决策树不做任何复杂特征只把核心字段塞进去目标是把整个训练-预测-提交的代码管道打通。基线模型的作用不是拿分而是验证你对数据的理解是否和代码一致——有没有读错列、时间对齐有没有错位、标签有没有泄漏。基线跑通后我才会开始尝试复杂模型或精细特征。这样做的好处非常实际基线给你一条分数下限后续每加一个特征、换一个模型你都能看到相对基线的增益。很多队伍一上来就上一个复杂网络跑完发现分数反而比简单模型差却说不清差在哪里因为没有基线做参照。在我的习惯里基线结果会单独存一份CSV后续所有实验都拿它做对比这条习惯在竞赛中帮我规避了很多无效调参。3. 数据预处理与特征工程像处理真实设备数据一样处理赛题数据3.1 数据读入与清洗从原始表格到可分析数据电工杯B题给的数据通常是CSV或Excel字段名可能是中文也可能是英文缩写。读入阶段要特别注意编码和分隔符问题中文CSV用Pandas默认的UTF-8读入时偶尔会出现列名乱码或分隔符错位。我一般会用Python的pd.read_csv并显式指定参数先把数据跑通。import pandas as pd import numpy as np # 读入原始数据sep指定分隔符encoding处理中文编码parse_dates指定时间列 df pd.read_csv( raw_data.csv, sep,, encodingutf-8-sig, # 带BOM的UTF-8避免列名出现\xef\xbb\xbf parse_dates[timestamp], # 直接解析为datetime类型 infer_datetime_formatTrue ) # 第一步查看数据形状、字段类型、缺失量分布 print(df.shape) print(df.dtypes) print(df.isnull().sum()) # 第二步删除全空列和重复行 df df.dropna(axis1, howall) df df.drop_duplicates().reset_index(dropTrue)这段代码看起来基础但两个细节值得说明。encodingutf-8-sig是为了处理Windows下Excel导出的CSV带BOM的问题不带这个参数第一列列名会出现一个不可见的乱码字符后面所有按列名取数的代码都会报KeyError。parse_dates指定时间列则会让你后续做时间对齐时省掉大量字符串转换的麻烦。缺失值的处理策略取决于缺失比例和字段性质。连续型字段缺失比例低于5%我用中位数填充因为中位数对异常值不敏感缺失比例在5%到20%之间我优先用同组均值或前后时刻插值超过20%的字段除非业务上不可替代否则直接丢弃。离散型字段则按众数填充但要额外生成一个“是否缺失”的指示列因为很多时候“数据没记录”本身就是一个有信息量的状态。3.2 异常值识别与处理别让极端值主导你的模型设备数据里异常值不是简单的“错误”而是可能代表真实工况的边缘情况。直接删掉很危险全部保留又会把模型参数拉偏。我采用两层判断先用四分位距IQR找出统计离群点再结合业务规则判断是否物理上可能。# 基于IQR识别异常值 def detect_outliers_iqr(series, k1.5): q1 series.quantile(0.25) q3 series.quantile(0.75) iqr q3 - q1 lower q1 - k * iqr upper q3 k * iqr return (series lower) | (series upper) # 以负载率字段为例先看异常样本数量 outlier_mask detect_outliers_iqr(df[load_rate]) print(f异常样本数: {outlier_mask.sum()}) # 对于物理上不可能的值如负载率超过100%或为负直接修正为NaN再填充 df.loc[df[load_rate] 100, load_rate] np.nan df.loc[df[load_rate] 0, load_rate] np.nan这里的k1.5是Tukey提出的标准参数适合对称分布如果数据明显右偏我会把k调大到2.5或3避免把正常的高负载工况误杀。负载率超过100%的场景在真实设备里可能意味着过载运行但在竞赛数据集里这种值通常是传感器标定错误或录入错误按NaN处理再用前向填充修复比直接删行更稳妥。处理完异常值后务必重新检查目标变量的分布。如果发现目标变量存在长尾做一次对数变换会让后续模型收敛更稳定但要注意预测结果需要做指数变换还原且还原后的误差指标不能直接用原尺度对比。3.3 特征构造与数据集切分防止数据泄漏的三个细节特征构造是B题拉开分数差距的关键段落。对于时间序列类数据我常用的特征包括三类滑动窗口统计特征过去N个时刻的均值、标准差、最大值、差分特征当前值相对上一时刻的变化量、时间上下文特征小时、星期几、是否工作日。这些特征能有效捕捉设备运行的模式性变化。# 构造滑动窗口统计特征 df[load_rate_mean_6h] df[load_rate].rolling(window6, min_periods1).mean() df[load_rate_std_6h] df[load_rate].rolling(window6, min_periods1).std() df[load_rate_diff] df[load_rate].diff().fillna(0) # 构造时间上下文特征 df[hour] df[timestamp].dt.hour df[day_of_week] df[timestamp].dt.dayofweek df[is_weekend] (df[day_of_week] 5).astype(int)min_periods1确保窗口在起始阶段不会全为NaN代价是前几个样本的均值项基于更少的数据这在特征重要性上会造成轻微偏差但对于竞赛场景可以接受。时间上下文特征中hour对于能耗这类强周期变量往往是最重要的单因子几乎所有B题能耗相关任务里它的特征重要性都排在前三。三个防泄漏细节必须刻在脑子里。第一滑动窗口特征只能用历史数据绝不能用未来数据——用shift()把窗口整体后移一步。第二归一化参数必须只用训练集拟合再用同一参数转换验证集和测试集否则验证集的误差会被低估。第三如果数据按设备ID分组切分数据时不要随机切要按组切分否则同一设备的不同时间段会同时出现在训练集和验证集里造成伪高精度。4. 核心模型落地训练代码全解析与调参要点4.1 从特征到模型拆分数据集并建立训练主循环在模型选型上我针对B题场景默认优先试LightGBM或XGBoost这类梯度提升树模型原因有三能处理表格数据中的非线性关系、对异常值有一定鲁棒性、训练速度快方便做多组实验。只有题目明确要求预测长序列趋势时我才会切到LSTM或Transformer类模型。以下是基于LightGBM的完整训练主循环这个结构可以直接改造成XGBoost或CatBoost。import lightgbm as lgb from sklearn.model_selection import train_test_split from sklearn.metrics import mean_absolute_error # 特征列与目标列特征列显式列出避免把id列和时间列带进模型 feature_cols [ load_rate_mean_6h, load_rate_std_6h, load_rate_diff, hour, day_of_week, is_weekend ] target_col energy_consumption X df[feature_cols].values y df[target_col].values # 按时间顺序切分前80%训练后20%验证 split_idx int(len(X) * 0.8) X_train, X_val X[:split_idx], X[split_idx:] y_train, y_val y[:split_idx], y[split_idx:] # 定义LightGBM参数这里用的是偏保守的一组初始参数 params { objective: regression, metric: mae, learning_rate: 0.05, num_leaves: 63, max_depth: 7, min_child_samples: 20, subsample: 0.8, colsample_bytree: 0.8, n_jobs: -1, seed: 42 } train_set lgb.Dataset(X_train, labely_train) val_set lgb.Dataset(X_val, labely_val, referencetrain_set) model lgb.train( params, train_set, num_boost_round500, valid_sets[val_set], callbacks[ lgb.early_stopping(stopping_rounds50), lgb.log_evaluation(period100) ] ) # 预测并评估 y_pred model.predict(X_val, num_iterationmodel.best_iteration) mae mean_absolute_error(y_val, y_pred) print(f验证集MAE: {mae:.4f})这段代码里有三个参数直接决定模型质量。num_leaves控制树的复杂度我把它和max_depth配合使用数据量少于一万条时num_leaves设在31以下超过三万条时可以放宽到127。min_child_samples是防止过拟合的第一道防线竞赛数据通常有噪声设到20比默认的1稳定得多。subsample和colsample_bytree都设为0.8相当于给每棵树注入随机性能有效降低方差。early_stopping的stopping_rounds50意味着如果连续50轮验证集误差没有下降训练就提前终止。这个机制避免了你手动寻找最优迭代次数同时也防止模型在训练集上无限拟合。这里有一个很多人不知道的细节model.predict时要传入num_iterationmodel.best_iteration否则默认用所有训练轮次的模型做预测结果会偏差。4.2 必调的三个参数从初始参数走向稳定高精度模型初始参数只是起点真正让模型拿到好成绩的是后续的针对性调参。我在B题实验里最常调的三个参数依次是learning_rate、num_leaves、feature_fraction。调参顺序不是随机的而是遵循“先降误差、再控复杂度、最后加随机性”的逻辑。第一轮把learning_rate从0.05降到0.02同时把num_boost_round从500提到2000早停轮数不变。学习率降低后每棵树贡献的增量变小模型需要更多树才能达到同等精度但每一步都更精细最终误差通常会下降。代价是训练时间变长不过对B题这种规模的数据集完全可接受。第二轮观察验证集和训练集的误差差距如果差距超过20%说明过拟合优先调大min_child_samples到50或调小num_leaves如果差距小于5%说明模型还有潜力可以把num_leaves调大。第三轮固定前两个参数后再扫feature_fraction从1.0降到0.7左右每棵树只用70%的特征增加模型鲁棒性。建议用网格搜索配合五折交叉验证但只在最终阶段做避免前期实验花大量时间在同一组参数上。4.3 结果落盘与可视化交卷时的图表就是你的门面模型的输出不能只是一个预测数值你需要把预测结果与真实值对比图、误差分布图、特征重要性图都整理出来。这些图在论文里占据的版面可能比公式还多而且评阅人看图的耐心只有几秒钟图必须一眼能看懂。import matplotlib.pyplot as plt # 特征重要性横向柱状图展示Top10特征 lgb.plot_importance(model, max_num_features10, figsize(8, 4)) plt.title(Feature Importance (LightGBM)) plt.tight_layout() plt.savefig(feature_importance.png, dpi300) plt.close() # 预测值与真实值对比散点图贴近对角线说明预测越准 plt.figure(figsize(6, 6)) plt.scatter(y_val, y_pred, s5, alpha0.3) plt.plot([y_val.min(), y_val.max()], [y_val.min(), y_val.max()], r--) plt.xlabel(True Value) plt.ylabel(Predicted Value) plt.title(Prediction vs Ground Truth) plt.tight_layout() plt.savefig(pred_vs_true.png, dpi300) plt.close()保存图片时统一用dpi300保证放入论文后依然清晰。对比图的散点如果过于密集可以加alpha0.3降低重叠造成的视觉误导。特征重要性图要向评阅人传递的核心信息是你的模型不是黑匣子它依赖的确实是跟问题强相关的物理量。如果Top10特征里混进了明显不该有的字段你就要回头检查是不是数据泄漏了。5. 竞赛实战避坑5个让人翻车的隐蔽错误5.1 样例数据跑通、全量数据崩掉的隐藏Bug现象用题目给的样例数据跑通完整流程后换到全量数据训练时程序直接报错或内存溢出而且报错信息隐晦难懂。原因样例数据通常只有几十行全量数据可能有几十万行。常见的问题包括某列在全量数据中包含样例里没出现的字符串值导致pd.get_dummies后维度爆炸某列缺失比例在样例和全量中差异巨大导致填充逻辑失效全量数据时间跨度大日期解析出现NaN。解决拿到全量数据的第一个小时先跑一个df.describe(includeall)对比样例数据和全量数据的字段分布、缺失比例、唯一值数量。凡是在样例数据上做过的硬编码判断全部改成基于全量数据统计结果的动态判断。我在某次模拟项目X中就因为样例数据的设备ID范围是0到9在全量数据中出现了10到99而代码里用了if device_id 10的分支逻辑结果全量数据的半数列被错误分类。这个教训之后我写代码一律不写死范围。5.2 日期字段解析后数据全部错位的编码问题现象训练时所有指标都正常但把预测结果按时间轴画出来发现曲线在某个日期后整体平移了一段或者出现大量尖峰。原因时间列解析时把月和日颠倒了或者是UTC时间和本地时间混用导致的时间偏移。某些CSV里日期格式是dd/mm/yyyyPandas默认按mm/dd/yyyy解析这种错误不会报错但结果全错。解决读入数据后第一件事就是打印df[timestamp].head(20)和.tail(20)肉眼核对首尾时间和题目给定的时间范围是否一致。如果发现日期解析错位在read_csv中显式传入dayfirstTrue或者用pd.to_datetime指定format%d/%m/%Y %H:%M:%S。另外统一在读取阶段就把时间列转为datetime64类型不要在分析中途再做转换避免不同节点对同一列做多次解析产生不一致。5.3 验证集误差低得离谱过拟合与数据泄漏的双重陷阱现象验证集上的误差指标比初赛基准低好几个数量级你觉得自己模型效果好得惊人但换一批真实数据进行测试时发现预测一塌糊涂。原因这是数据泄漏最典型的症状。具体来说有三种可能特征里包含了未来信息比如用未来时段的均值来预测当前值切分时没按时间顺序而是随机切分归一化时用了全量数据的均值和标准差。解决检查特征是否用了shift(-1)这类未来函数检查train_test_split是否设置了shuffleFalse并按时间边界切分检查标准化器是否只fit在训练集上。此外我会强制要求自己每一次实验后都把模型的区里和榜单分数做对比如果差异超过阈值立即回头排查数据管道而不是继续调参。5.4 提交格式错误精度再高也会被判定无效现象模型跑出理想结果但提交到竞赛系统后被判定格式错误或者分数栏显示异常。原因电工杯B题的提交格式通常有严格规定包括文件名、列名顺序、小数位数、编码方式。最常见的问题包括列名大小写不一致、预测结果包含NaN、多提交了一列不需要的字段、CSV文件用Excel打开后另存为改变了编码。解决在生成提交文件时直接通过代码来固化格式确保每次输出完全一致。用to_csv指定indexFalse避免多出索引列对预测值做round处理并统一填充NaN提交前用脚本校验文件的列名、行数、缺失值。养成这个习惯以后格式问题基本不会再犯。5.5 代码版本混乱实验了几十组参数后无法复现最优结果现象做了几十组实验后某一次跑出了很好的分数但回头想复现时发现代码已经被改得面目全非怎么都调不回当时的结果。原因没有做版本管理或者没有记录实验对应的参数组合。竞赛期间代码迭代频繁文件保存为final_1.py、final_2.py、final_real.py这种命名最后根本分不清哪个是哪个。解决用Git做版本管理每次跑出有效结果前给代码和对应的输出打一个标签。如果对Git不熟也可以用最笨的办法每次实验保存一份完整的params.json和一份当前代码副本命名含日期和时间。我在所有竞赛中都坚持这一条最后写论文时能从记录中直接回溯每一个图表对应的实验版本效率提升非常明显。6. 把完整代码变成论文加分项可复现清单与交付习惯竞赛论文的附录空间有限评阅人不可能把你的代码一行行看完但他们一定会做一件事翻代码附件看结构是否清晰、注释是否到位、能否按说明复现。这个环节经常被参赛者忽略却直接影响论文的完成度评分。我的做法是给代码附件建一个“复现清单”用三层结构组织第一层是环境依赖Python版本、核心库版本第二层是执行顺序从数据预处理到结果输出的脚本顺序第三层是每段代码的预期输出。这样评委从打开文件夹到跑完流程不会产生任何困惑。这里有一个值得养成的交付习惯把“数据读取-预处理、特征工程-模型训练、结果可视化”拆成三个独立脚本而不是把所有环节堆在一个文件里。拆分后评委可以只跑第3个脚本来快速验证模型代码也可以跳过训练直接查看预训练好的结果。哪怕竞赛不需要提交代码我也建议你用这套逻辑整理自己的工程目录——它迫使你把流程想清楚坏处只是前期多花十分钟。另一个具体技巧是给模型的随机种子做统一管理。我在所有代码开头固定写入seed 42并在每个可能引入随机性的函数中调用它包括数据切分、模型初始化、特征采样。这样做的直接好处是实验结果可复现间接好处是后期对比不同模型时不会因为随机噪声而误判。团队协作时如果有人改掉了种子代码评审阶段就能立刻发现。竞赛结束后的第二天我总会补一份简短的赛后复盘记录三件事哪一步消耗的时间最多、哪个参数带来的提升最大、哪次翻车造成的损失最严重。这个方法让我每次竞赛都比上次更快进入状态。作为过来人我想说竞赛的分数确实重要但真正能被你带走的是遇到问题时自然反应的排查思路——学会把“瞎试”变成“有序实验”这份能力远比你收集的那堆获奖论文有价值。希望帮到你。本文还有配套的精品资源点击获取