☰
数学建模C题源码解析:蔬菜销量预测与定价补货完整链路
2026/10/10 1:03:40 网站建设 项目流程

简介: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, lookback=7): 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, lookback=7) 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, activation='relu', return_sequences=True, input_shape=(X.shape[1], 1)), Dropout(0.2), LSTM(32, activation='relu'), Dense(1) ]) model.compile(optimizer='adam', loss='mse') model.fit(X_train, y_train, epochs=50, batch_size=32, validation_split=0.1, verbose=0)

这段代码的核心逻辑是两步:先用 7 天的滑动窗口构造监督学习样本,再用两层 LSTM 提取时间依赖。lookback=7的含义是用前一周的销量预测下一天,这个值对应蔬菜销售的自然周周期——周中的销量和周末的销量有明显差异,7 天窗口刚好覆盖一个完整周期。return_sequences=True让第一层 LSTM 输出完整的时间步序列给第二层,二层网络能学到更抽象的时序模式。validation_split=0.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(f"MAPE: {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_ub=A_ub, b_ub=b_ub, bounds=bounds, method='highs') 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的默认行为是分批随机取样本,需要用shuffle=False显式关闭,或者在构造X, y时就保证样本按时间递增排列。

4.4 坑四:预测输出取期望值,而不是区间,订货量永远不够

现象:回测时缺货次数频繁,即使预测的 MAPE 只有 15%,最终毛利依然低于基线方案。

原因:销量预测是概率分布,期望值只是中心点。订货量期望值能满足约 50% 的需求场景,剩下 50% 的场景都会缺货,损失销售机会。这是零售补货里的经典问题。

解决:用预测区间上沿做补货量计算。实践做法是在 LSTM 预测时对同一输入多次采样(开启model.train_on_batch的随机性),取 75 或 90 分位数作为补货需求基准,牺牲一点库存周转、换取更低的缺货率。

提示:这四个坑不是互相独立的。粒度不对会导致后续所有计算结果整体偏移,损耗率错误会掩盖预测模型本身的问题。排查顺序建议是:先核对粒度,再检查时序切分,最后回头验证损耗率和分位数。

5. 用论文里的图表反推代码:一个快速验证源码是否靠谱的技巧

拿到这套源码后,最怕的是“代码能跑但结果对不上论文”。与其逐行读代码猜逻辑,不如用论文里的图表当基准,反向核对代码实现。

第一个验证点是训练测试划分。论文里的预测对比图通常会标注“训练集”“测试集”,拿这个时间去代码里找split的位置,看切分比例和日期是否一致。常见问题是为了好看,论文里展示的是训练集上的拟合效果,代码里默认的切分点却把同样的时间段放在了测试集里——这两种情况不能同时成立。

第二个验证点是预测指标。论文正文里一般会给出 MAPE、RMSE 的具体数值,直接在代码里找到mape = ...那行,把它的计算方式和数据处理路径对齐。注意看它是按品类分别计算再平均,还是把所有品类的预测值和真实值堆在一起算一个总数——两种方式最后得到的数字差别不小,论文里通常会写清楚是哪种。

第三个验证点是补货量的波动特征。回测跑完补货模块后,把每天的订货量画出来,和论文里展示的补货计划表对照。如果论文里的补货量有明显的周期性波动(比如周一和周四进货量偏高),但代码跑出来的是一条平线,说明价格弹性或损耗率参数没有参与计算,直接锁定到对应参数即可。

我平时拆这类赛题源码,最后一步都是强制走一遍“论文图表复刻”——从代码里重新生成论文里的那张预测对比图,叠加真实销量曲线。重合度达到八成以上,才敢说这份资源真的吃透了。

从那以后,我每次拿到赛题源码包,都会先看有没有可复现的图表脚本,而不是先去读模型定义。没有图表脚本的话,就自己写一段回测代码,把论文里的关键数字逐项复现,对上了再继续深挖参数细节。这个习惯帮我避开了不少“看着完美、跑起来翻车”的坑,希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询