多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

数学建模C题源码解析:蔬菜销量预测与定价补货完整链路

数学建模C题源码解析:蔬菜销量预测与定价补货完整链路 简介2023年高教社杯全国大学生数学建模竞赛C题围绕蔬菜类商品的自动定价与补货决策展开这份赛题源码与论文资料包面向参赛学生、毕业设计者及课程设计学习者。包内包含模型训练代码、神经网络权重文件h5/pkl/joblib、数据分析表格xlsx、可视化截图png以及论文文档pdf/doc等共110个文件压缩包约72.74MB目录结构清晰便于按模块查阅。代码附有详细注释并覆盖辣椒类、食用菌、茄类、花菜类、花叶类、水生根茎类等多个蔬菜品类的建模实现新手也能快速理解定价与补货的完整思路简单部署即可运行。目前已有852人学习下载适合用于竞赛复盘、毕设参考或课程设计项目。通过该资源可系统掌握从数据预处理、模型训练到结果可视化的全流程并获得高分论文范例与可复用的工程实现。1. 为什么这份C题源码值得拆开看从“进多少菜”到“定什么价”的完整决策链2023年高教社杯全国大学生数学建模竞赛C题表面上是给一家超市的蔬菜区做自动定价与补货决策实际上是一个把“销量预测”和“运筹优化”串起来打完整闭环的赛题。拿到手的是一段三年维度的蔬菜销售流水六个大类、几十个单品每天都有销量、单价、损耗率在变。真正让多数参赛队伍翻车的不是模型选得不够深而是没意识到这个题的本质链条——先预测明天卖多少再决定今天进多少、标什么价。这套源码加论文资料把“数据清洗 → 销量预测 → 定价补货 → 结果回写”的完整流程做成了可以直接跑的项目工程适合三类人准备数学建模竞赛的本科生、做供应链或零售数据分析的入门者、以及想把学术解法落成可执行代码的毕业生。2. 先把C题的底牌摸清三个数据文件与一层决策逻辑的技术拆解2.1 数据文件先认全六类蔬菜的销售流水、损耗率与批发价格C题的数据不是一张大宽表而是按业务含义拆开的多个文件。常见的数据包组织方式是一张销售流水明细表、一张损耗率表、一张批发价格表外加一个需要提交的结果表模板。先不要急着建模把这几个文件的功能边界认清楚后面才不会在特征工程里自己骗自己。文件角色典型字段在决策链路中的用途销售流水明细销售日期、单品编码、单品名称、销量、销售单价销量预测的训练数据按SKU单品粒度组织损耗率表品类、损耗率补货时扣除损耗造成的实际可售量损耗批发价格表日期、单品编码、批发单价定价决策的成本基准毛利率计算的基础结果表模板日期、单品编码、建议订货量、建议定价最终决策输出的统一出口这几个文件中最容易被忽略的是损耗率表。C题场景里蔬菜生鲜的损耗不是均匀发生的叶菜类和食用菌类的损耗率通常显著高于根茎类。很多新手直接用一个固定损耗率去做所有品类的补货修正结果补货量整体偏移。我一般会在预处理阶段把损耗率表单独存一份按品类和季节分组看统计量再决定是取均值还是做分段处理。数据粒度也是一个关键选择。流水表是日级别的SKU粒度但赛题要求的是品类级别的决策输出。这意味着你必须在“按单品预测再汇总”和“直接按品类预测”之间做选择。直接按品类聚合预测数据更平稳模型更容易收敛但会丢掉单品维度的价格差异信息按单品预测再汇总信息保留完整但计算量成倍上升而且冷门单品的序列太稀疏预测容易震荡。常见做法是先按品类聚合验证整体效果再决定要不要下钻到单品粒度。2.2 从流水到决策感知、预测、定价、补货的四段链路设计把C题还原成业务问题本质是一条四段链路。第一段是感知从流水表里读历史销量和价格清洗缺失值和异常值第二段是预测基于历史序列预测未来一周的日销量第三段是定价结合批发价格和毛利率目标算出每个品类的最优售价第四段是补货用预测销量、损耗率和现有库存共同决定明天进货多少。这条链路的设计顺序不能乱。很多队伍第一步就去调LSTM的参数量却忘了先确认预测的粒度、区间和输出格式是否和后面定价模块对齐。判断标准很简单预测模块的输出是“未来7天每天每个品类的销量期望值”定价模块就要能吃掉这个格式且能回写结果表。我一般先确定结果表的列结构再反向决定预测模块的接口——这样可以避免后期联调时大量的字段重命名和格式转换。链路里还有一个隐性的业务约束蔬菜的可售周期短补货决策必须考虑损耗率。也就是说预测出的销量是“需求侧”但订货量必须除以1-损耗率才能保证实际到货后有足够的可售量。这个折算逻辑应该在补货模块里写而不是在预测模块里写——预测模块只负责需求侧的估计补货模块负责供给侧的现实修正。提示链路设计阶段最重要的产出不是“流程图”而是一张字段映射表——明确每个模块的输入字段和输出字段确保预测模块的输出就是定价模块的输入。后面对代码、查错这张表是最快的定位工具。3. 核心链路落地销量预测与定价补货的双引擎实现3.1 销量预测用LSTM把六个类别的日销量拆开预测C题场景下的销量预测我见过 ARIMA、Prophet、LSTM 三种主流方案。ARIMA 对单条平稳序列效果好但面对六类蔬菜的周期性波动和节日扰动需要逐个品类调参工程量大Prophet 对节假日和周期性内建支持好适合快速出一个还不差的基线LSTM 的优势是能同时捕捉序列的短期依赖和非线性关系在样本量足够三年日度数据的情况下效果上限更高。这份源码的预测模块走的是 LSTM 路线下面这版代码是核心骨架注意看序列构造部分——这是整个模型成败的关键。import numpy as np import pandas as pd from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout from sklearn.preprocessing import MinMaxScaler def build_sequences(data, lookback7): X, y [], [] for i in range(len(data) - lookback): X.append(data[i:i lookback]) y.append(data[i lookback]) return np.array(X), np.array(y) df pd.read_csv(sales_flow.csv, parse_dates[sale_date]) category 花叶类 single df[df[category] category].sort_values(sale_date) scaler MinMaxScaler(feature_range(0, 1)) scaled scaler.fit_transform(single[[sales_volume]].values) X, y build_sequences(scaled, lookback7) split int(len(X) * 0.8) X_train, X_test X[:split], X[split:] y_train, y_test y[:split], y[split:] model Sequential([ LSTM(64, activationrelu, return_sequencesTrue, input_shape(X.shape[1], 1)), Dropout(0.2), LSTM(32, activationrelu), Dense(1) ]) model.compile(optimizeradam, lossmse) model.fit(X_train, y_train, epochs50, batch_size32, validation_split0.1, verbose0)这段代码的核心逻辑是两步先用 7 天的滑动窗口构造监督学习样本再用两层 LSTM 提取时间依赖。lookback7的含义是用前一周的销量预测下一天这个值对应蔬菜销售的自然周周期——周中的销量和周末的销量有明显差异7 天窗口刚好覆盖一个完整周期。return_sequencesTrue让第一层 LSTM 输出完整的时间步序列给第二层二层网络能学到更抽象的时序模式。validation_split0.1是在训练集内部再切一份做早停监控避免过拟合。这里有个容易被忽略的问题MinMaxScaler 必须在整个序列上拟合不能只对训练集拟合。代码里对全量数据做归一化是假设测试集的数值范围在训练集范围之内——对销量序列来说这个假设成立。如果你想更严谨可以先只对训练集 fit再 transform 测试集但要保证测试集出现远超训练集的新峰值时不至于失真。模型训完后预测要记得做逆变换把归一化后的数值还原成真实销量pred_scaled model.predict(X_test) pred_sales scaler.inverse_transform(pred_scaled) actual_sales scaler.inverse_transform(y_test.reshape(-1, 1)) mape np.mean(np.abs((actual_sales - pred_sales) / actual_sales)) * 100 print(fMAPE: {mape:.2f}%)MAPE 是这类预测任务最直观的指标但注意它有个缺陷当真实销量接近 0 时MAPE 会被极小值放大。蔬菜品类在清货日可能会有接近 0 的销量所以我会同时看 RMSE两个指标一起判断模型质量。注意LSTM 的随机性来自权重初始化和训练过程同一份代码跑两次结果会有细微差异。正式提交前固定随机种子如np.random.seed(42)、tf.random.set_seed(42)保证论文里的结果可复现。3.2 定价与补货用线性规划吃掉预测结果给出明日订货量预测模块的输出是“未来每天的销量期望”但赛题真正要的是“明天进多少货、标什么价”。这一步是典型的约束优化问题在库存、损耗率、最低陈列量、价格变动幅度等约束下最大化毛利。这份源码的定价补货模块用的是线性规划核心是把业务规则翻译成数学约束。from scipy.optimize import linprog import numpy as np pred_sales np.array([120, 150, 100, 80, 130]) # 示例某品类未来5天预测销量 wholesale 3.5 # 批发价单位元/斤 loss_rate 0.15 # 损耗率15% max_price 6.0 # 最高定价 min_price 4.5 # 最低定价 # 决策变量x[0] 为订货量x[1] 为定价 # 目标函数最大化毛利 (定价 - 批发价) * 可售量 # 这里化简为最小化负毛利因为 linprog 只做最小化 c [0, 0] # 占位实际目标在约束中处理 A_ub [] b_ub [] # 约束1订货量 预测销量 / (1 - 损耗率) A_ub.append([-1, 0]) b_ub.append(-pred_sales.sum() / (1 - loss_rate)) # 约束2定价不能超过上限 A_ub.append([0, 1]) b_ub.append(max_price) # 约束3定价不能低于下限通过 -x[1] -min_price 实现 A_ub.append([0, -1]) b_ub.append(-min_price) bounds [(0, None), (min_price, max_price)] res linprog(c, A_ubA_ub, b_ubb_ub, boundsbounds, methodhighs) order_qty res.x[0] optimal_price res.x[1]代码里pred_sales.sum()是用未来 5 天的总算需求除以(1 - loss_rate)得到考虑损耗后的订货量下界。如果订货量低于这个值到货扣掉损耗后就不够卖会出现缺货。定价的上下界约束来自实际业务——价格不能无限高也不能低过成本太多。这个简化版做了两件事保证订货量够覆盖需求、保证定价落在合理区间。但它还没有真正把毛利最大化写进目标函数因为目标函数里的c被占位成了 0。真正要最大化毛利目标函数应该是-( pricing - wholesale ) * 可售量决策变量出现在乘积里会变成非线性项。常见的处理方式有两种一是将定价离散化枚举候选价格再用整数规划选最优二是用线性近似假设可售量为常数。在这类赛题要求的规模下我一般用枚举法——把定价从下限到上限按 0.1 元步长切分枚举每个候选价下的毛利取最大。这样做能绕开非线性又符合“零售定价按角变动”的真实业务习惯best_profit -np.inf best_price min_price best_qty 0 for price in np.arange(min_price, max_price 0.1, 0.1): demand pred_sales.sum() * (1 - 0.05 * (price - wholesale)) demand max(demand, 0) qty demand / (1 - loss_rate) profit (price - wholesale) * demand if profit best_profit: best_profit profit best_price price best_qty qty这里的demand pred_sales.sum() * (1 - 0.05 * (price - wholesale))是价格弹性假设——定价每高出批发价 1 元需求下降 5%。这个弹性系数不是拍脑袋而是从流水数据里拟合价格与销量的关系得到的。赛题场景下很多队伍忽略这个环节直接把预测销量当定值去算利润最后算出的最优价格永远是最低价——因为你没把需求对价格的反馈放进去。4. 复现与实战常见的坑四个最容易翻车的位置4.1 坑一预测用品类汇总补货却按单品算量级对不上现象预测模块输出的是品类日销量补货模块直接把这个值拿去和单品批发价相乘计算毛利得出订货量小得离谱。原因品类下包含多个单品品类销量是单品销量之和。补货决策针对单品但预测输出是品类级两个模块的粒度没有对齐。解决在预测模块和补货模块之间加一层“品类到单品”的下钻映射。简单做法是按历史销量占比把品类预测值分摊到单品再分别做补货计算严谨做法是直接按单品粒度做预测。C题场景下前者工程成本更低且能满足赛题精度要求。4.2 坑二损耗率直接取平均值叶菜类补货量系统性偏低现象结果表里叶菜类连续多天订货量小于实际销量缺货率明显高于其他品类。原因不同品类损耗率差异很大叶菜类损耗率可能到 20% 以上根茎类不到 5%直接取平均会让所有品类用同一个中间值去折算损耗高的品类被低估。解决按品类分别维护损耗率补货量计算时使用品类专属损耗率。代码层面就是用loss_dict {花叶类: 0.2, 食用菌: 0.15, ...}这类字典结构替代单个常量。4.3 坑三LSTM 预测时把时序打乱泄露了未来信息现象模型的训练损失很低测试 MAPE 也很低但一到实际回测就崩——预测值严重滞后于真实值。原因构造训练样本时用了全局随机打乱把时间顺序破坏了。LSTM 学到的“规律”里混入了未来数据测试集和训练集不再独立。解决序列数据只能按时间顺序切分禁止 shuffle。代码中model.fit的默认行为是分批随机取样本需要用shuffleFalse显式关闭或者在构造X, y时就保证样本按时间递增排列。4.4 坑四预测输出取期望值而不是区间订货量永远不够现象回测时缺货次数频繁即使预测的 MAPE 只有 15%最终毛利依然低于基线方案。原因销量预测是概率分布期望值只是中心点。订货量期望值能满足约 50% 的需求场景剩下 50% 的场景都会缺货损失销售机会。这是零售补货里的经典问题。解决用预测区间上沿做补货量计算。实践做法是在 LSTM 预测时对同一输入多次采样开启model.train_on_batch的随机性取 75 或 90 分位数作为补货需求基准牺牲一点库存周转、换取更低的缺货率。提示这四个坑不是互相独立的。粒度不对会导致后续所有计算结果整体偏移损耗率错误会掩盖预测模型本身的问题。排查顺序建议是先核对粒度再检查时序切分最后回头验证损耗率和分位数。5. 用论文里的图表反推代码一个快速验证源码是否靠谱的技巧拿到这套源码后最怕的是“代码能跑但结果对不上论文”。与其逐行读代码猜逻辑不如用论文里的图表当基准反向核对代码实现。第一个验证点是训练测试划分。论文里的预测对比图通常会标注“训练集”“测试集”拿这个时间去代码里找split的位置看切分比例和日期是否一致。常见问题是为了好看论文里展示的是训练集上的拟合效果代码里默认的切分点却把同样的时间段放在了测试集里——这两种情况不能同时成立。第二个验证点是预测指标。论文正文里一般会给出 MAPE、RMSE 的具体数值直接在代码里找到mape ...那行把它的计算方式和数据处理路径对齐。注意看它是按品类分别计算再平均还是把所有品类的预测值和真实值堆在一起算一个总数——两种方式最后得到的数字差别不小论文里通常会写清楚是哪种。第三个验证点是补货量的波动特征。回测跑完补货模块后把每天的订货量画出来和论文里展示的补货计划表对照。如果论文里的补货量有明显的周期性波动比如周一和周四进货量偏高但代码跑出来的是一条平线说明价格弹性或损耗率参数没有参与计算直接锁定到对应参数即可。我平时拆这类赛题源码最后一步都是强制走一遍“论文图表复刻”——从代码里重新生成论文里的那张预测对比图叠加真实销量曲线。重合度达到八成以上才敢说这份资源真的吃透了。从那以后我每次拿到赛题源码包都会先看有没有可复现的图表脚本而不是先去读模型定义。没有图表脚本的话就自己写一段回测代码把论文里的关键数字逐项复现对上了再继续深挖参数细节。这个习惯帮我避开了不少“看着完美、跑起来翻车”的坑希望帮到你。本文还有配套的精品资源点击获取
返回列表