
简介这份资源是2023年全国大学生数学建模竞赛C题的Python实现面向备赛学生与想用数学建模解决商超果蔬定价和补货问题的学习者。围绕果蔬易腐、季节性强、消费者需求不确定等特点代码完整覆盖了数据采集、清洗预处理、价格预测、补货预测与相关性分析等关键环节涉及时间序列、回归分析和最优化建模思路适合对照赛题复盘和实际数据演练。包内共18个文件其中11个Python脚本承担数据抓取、分类和回归预测等任务6个MATLAB脚本用于数据拟合、蔬菜分类与Pearson相关性检验另有README文档说明整体结构压缩包仅16KB轻量但流程完整便于快速定位和复现每一步模型。已有310人学习适合数学建模初学者用于理解从数据到决策的完整链路也可作为课程设计或比赛准备的参考实现。1. 2023数学建模C题Python源码这份包能拆出什么才是关键2023年全国大学生数学建模竞赛C题的商超果蔬定价与补货问题对当年参赛的人来说绝对不轻松——它不像A题、B题那样有明确的物理机理可循而是丢给你一堆商超销售流水让你自己从数据里找规律。这份基于Python实现的源代码包覆盖了过去参赛者从数据清洗、果蔬分类、价格序列构建到价格预测和补货预测的完整建模链路既包含Python脚本也保留了MATLAB对照实现适合想复现C题全流程、或者想直接借鉴预测与回归代码的建模选手。我拆了一遍包内文件后可以负责任地说这套源码的价值不在某个单点模型而在于它把C题的三个核心任务串了起来果蔬类商品的分类规律、单品价格与销量的回归关系、以及后续的价格预测和补货量决策。你不需要指望它直接给你一份国奖论文但它能省掉你大量写数据处理脚本的时间——尤其是t01_*系列脚本几乎把数据清洗和分类的工作做完了。适合的人群很明确正在备战国赛/美赛的学生、想把商超数据建模完整跑通一遍的练习者、以及需要一份价格预测与补货预测代码做基线的人。理论你自己补代码它给到八成。2. 数据预处理这条管线价格和销量数据先合规模型才不会翻车2.1 t01_reptile.py 与 t01_reptile_after.py拿到原始数据后的第一道工序包内最早的脚本是 t01_reptile.py 和 t01_reptile_after.py。这两份脚本的名字虽然带reptile爬虫但实际作用更偏向“数据获取与落地后的预处理”。在C题场景里官方给出的销售流水通常是Excel或CSV包含销售日期、单品编码、商品名称、销量、原价、折扣价等字段。常见做法是先把原始表读进来按日期和单品编码做一次透视形成“日期 × 单品”的价格和销量矩阵再进行后续清洗。import pandas as pd # 读取销售流水编码坑比较多GBK和UTF-8都试一下 df pd.read_csv(sale_data.csv, encodinggbk) # 统一日期格式C题数据里的日期经常带时分秒或类型混用 df[sale_date] pd.to_datetime(df[sale_date]).dt.date # 按日期单品编码聚合得到日维度的销量和均价 daily df.groupby([sale_date, product_id]).agg( sales_qty(sales_qty, sum), avg_price(discount_price, mean) ).reset_index() # 检查缺失日期很多商超数据会跳过某些日期需要补全 date_range pd.date_range(daily[sale_date].min(), daily[sale_date].max()) daily daily.set_index(sale_date).reindex(date_range).reset_index()这段代码对应包内最基础的清洗逻辑。groupby之后做日期补全非常关键——后续无论是做时间序列预测还是回归日期不连续都会导致模型错位。我第一次跑这类数据时就是没补日期ARIMA把断档当成了规律预测结果直接偏了一个量级。2.2 商品名称清洗t01_jieba.py 和 t01_remove.py 解决的问题商超数据里商品名称脏得超出想象同一款“菠菜”可能写成“菠菜精品”“菠菜新鲜”“本地菠菜”等不同文本。包内的 t01_jieba.py 引入了结巴分词配合 t01_remove.py 做停用词删除目标就是把同一品类下的不同叫法归并到同一类。import jieba # 分词并过滤掉无意义的规格描述词 def clean_product_name(name): words jieba.lcut(name) # 去掉括号、单位、品相等干扰词保留核心果蔬名 stop_words [精品, 新鲜, 本地, , , 优质] cleaned [w for w in words if w not in stop_words and w.strip()] # 取第一个词作为品类关键词比如“紫皮洋葱”切出来后是“洋葱” return .join(cleaned) df[clean_name] df[product_name].apply(clean_product_name)调用分词库处理商品名的思路比单纯用字符串匹配鲁棒得多。需要注意结巴分词对生鲜词库的覆盖率一般如果词库里没有“秋葵”“娃娃菜”这类词得手动往jieba词典里加词否则切分会把完整词切碎。包内 t01_jieba.py 跑完后建议打印一份清洗前后的商品名对照表人工复核一遍漏切和错切的情况。2.3 t01_delete.m 和 t02_add_sale_price.py清洗与价格序列的补全t01_delete.m 是MATLAB版本的数据剔除脚本和t01_remove.py功能类似主要是把销量为0、价格为空、折扣价高于原价等明显异常的行删掉。t02_add_sale_price.py 则更有意思——它做的事情是“补价格”同一单品在部分日期没有销售记录但为了后续建模需要完整的价格序列它把前后日的价格做线性插值补进去。# 对每个单品在缺失日期上线性插值价格 def fill_price_series(group): group group.sort_values(sale_date) group[avg_price] group[avg_price].interpolate( methodlinear, limit_directionboth ) # 边界日期没有前后值用离它最近的可用价格填充 group[avg_price] group[avg_price].fillna(methodffill).fillna(methodbfill) return group daily daily.groupby(product_id, group_keysFalse).apply(fill_price_series)注意插值只适用于“该商品今天确实没卖出去”的场景不能用于“该商品今天下架了”的场景。如果商品出现长时间断档比如连续7天无销量再用插值补出来的价格就是纯属编造模型会学到一个假规律。遇到这种单品宁可把整条序列剔除也不要补。3. 品类归并与相关性分析把分类账做平预测才有意义3.1 t01_vegetable_classification.m 与 t01_add_category.m蔬菜分类的两种口径C题的核心任务之一是分析不同蔬菜品类的销售规律差异这就离不开品类标签。包内 t01_vegetable_classification.m 负责给每个单品打上品类标签叶菜类、根茎类、茄果类等t01_add_category.m 则是把分类结果写回原数据表生成一个带category_id的新表。分类逻辑参考了蔬菜学上的可食用部位分法处理“精品菠菜”和“上海青”这种同类别但价格差很大的商品时是按单品名称关键词映射到品类再按品类汇总销量。实操时我一般会在分类脚本里留一个显式映射表品类关键词示例分类依据叶菜类菠菜、生菜、油麦菜可食部位为叶片茄果类番茄、茄子、辣椒可食部位为果实根茎类土豆、胡萝卜、山药可食部位为根或茎花菜类西兰花、菜花可食部位为花球映射表的好处是比赛时评委很容易看懂你的分类逻辑而且后续如果发现分类结果不合理只需要改关键词不需要重写整份脚本。3.2 pearson1.m相关系数到底该看哪几个值pearson1.m 是包里的相关性分析脚本用Pearson相关系数计算价格、销量、折扣率之间的线性关联。建模时这一步不是走过场——C题后续的回归模型选特征、判断是否存在多重共线性基本都靠这一步的结论。% pearson1.m 核心逻辑示意 % data: 每列分别为 销量、原价、折扣价、折扣率 R corrcoef(data); fprintf(销量与折扣价相关系数: %.4f\n, R(1,3)); fprintf(原价与折扣价相关系数: %.4f\n, R(2,3));跑完后重点看两个数第一个是销量与折扣价的相关系数如果绝对值在0.8以上说明价格驱动的销量关系非常明显回归模型可以大胆用价格做自变量第二个是原价与折扣价的相关系数如果这个数接近1说明折扣价完全由原价按固定比例打折得到此时原价和折扣价不能同时放进回归模型里否则会引发多重共线性系数估计会变得极不稳定。3.3 t03_corresponding.py从“商品日销售”对齐到“品类日销售”t03_corresponding.py 是包内跨度最大的一步它把清洗后的单品级数据按品类汇总成“品类-日期-销量-均价”为后续的预测模型准备面板数据。品类归并之后数据量会从几百个单品缩减到几个品类每一类的销量和价格序列稳定得多时间序列模型和回归模型的拟合效果会明显提升。# 按品类加日期汇总销量和加权均价 category_daily df.groupby([category_id, sale_date]).apply( lambda x: pd.Series({ sales_qty: x[sales_qty].sum(), avg_price: (x[avg_price] * x[sales_qty]).sum() / x[sales_qty].sum() }) ).reset_index()加权均价是个容易忽略的细节直接用平均价会把畅销品和滞销品同等对待导致价格序列失真。按销量加权之后价格才能反映真实现金流方向。包内这一步处理得比较规范值得沿用。4. 回归与预测双模块价格拟合和补货量预测的分工逻辑4.1 t02_fitting.mMATLAB拟合脚本在拟合什么t02_fitting.m 是包内的拟合脚本用MATLAB的cftool或者fit函数对销量-价格关系做曲线拟合。C题场景里销量和价格往往不是线性关系而是类似“价格越低销量越高但边际递减”的形态。常见做法是尝试二次多项式和对数线性两种形式对比R²后选更优的。% 拟合销量与价格的关系二次多项式 f fit(price, qty, poly2); % 拟合对数线性模型ln(Q) a * ln(P) b f_log fit(log(price), log(qty), poly1);二次多项式的好处是能捕捉“中间价最优”的倒U型关系这在蔬菜水果里很常见——太便宜了消费者怀疑不新鲜太贵了又买不下手。对数线性模型的解释性更强系数直接就是需求价格弹性写论文时更好展开分析。拟合完记得把两条曲线的残差图都打印出来看是否出现系统性的偏差比如低价段总是高估销量。4.2 t02_regression.py多元回归的自变量选择和参数解读t02_regression.py 承担了回归建模的正活把我前面提到的价格、品类、折扣率、星期几等变量统一丢进一个多元回归模型。包里没有用特别复杂的模型而是走了线性回归加正则化的路线——在这个数据量级下复杂模型容易过拟合线性模型的可解释性在比赛中反而更值钱。from sklearn.linear_model import Ridge from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler # 构造特征价格、折扣率、星期、品类哑变量 X df[[price, discount_rate, is_weekend]] X pd.get_dummies(df[[category_id]]).join(X) # 标准化后做岭回归alpha默认取1.0比赛里够用 scaler StandardScaler() X_scaled scaler.fit_transform(X) model Ridge(alpha1.0) model.fit(X_scaled, df[sales_qty])参数说明alpha1.0 是岭回归的默认惩罚系数如果跑出来训练集R²很高但测试集R²掉得厉害把alpha往上调到5或10试试。is_weekend这个特征在果蔬销售里非常有用周末销量通常比工作日高20%以上不加这个特征的话残差里会带着明显的星期周期。4.3 t02_prediction.py 与 t03_prediction.py价格预测在前补货预测在后包内两个预测脚本分工明确。t02_prediction.py 做的是价格预测——用时间序列模型预测未来几天的单品或品类价格走势依赖的是第2章构造的连续价格序列。t03_prediction.py 做的是补货预测——在给定未来价格的前提下用第4章的回归模型反推预期销量再结合损耗率给出补货建议。from statsmodels.tsa.arima.model import ARIMA # 价格预测ARIMA(2,1,2)差分一次消除趋势 model ARIMA(price_series, order(2, 1, 2)) result model.fit() forecast result.forecast(steps7)ARIMA的阶数选择不要迷信自动定阶。C题数据通常只有一年左右长度几百条p和q超过3就非常容易过拟合。先画ACF和PACF图辅助定阶不行就默认(2,1,2)对比AIC值选小的。补货预测那边就更直接了把预测价格代入回归模型乘一个安全系数就是补货量——包内没有显式写安全系数但我建议你加上因为回归给出的是平均预期生鲜商品宁可多补5%也不愿意断货。5. 避坑清单跑这份C题源码最容易翻车的五个点5.1 日期格式不一致导致groupby结果错乱现象t01系列脚本跑完后输出的日销售表中出现重复日期行或者同一日期下出现NaN销量。原因原始数据中日期字段一部分是datetime类型、一部分是字符串类型直接用pd.to_datetime转换后部分日期变成了带时间的格式groupby时“2023-06-01”和“2023-06-01 00:00:00”被当成两个不同的值。解决在读取数据后立刻统一执行df[sale_date] pd.to_datetime(df[sale_date]).dt.normalize()强制去掉时间部分。检查一下转换后的dtype是否为datetime64如果不是就检查是否有无法解析的脏值先把那些行单独打印出来看。5.2 价格插值补出负数或荒谬的高价现象t02_add_sale_price.py 跑完后个别商品的价格序列出现负值或者原价的两倍以上。原因线性插值在边界条件下会用前向填充或后向填充如果商品的最后一条记录价格是异常值比如折扣价录错成1元ffill会把1元一路带到序列末尾显得价格波动极其夸张。解决插值之前先做一次价格合理性过滤把单价低于0.5元或高于某个分位数比如99分位的记录标记成缺失而不是直接删除。缺失值交给插值处理异常值交给业务判断处理两件事不能混在一起。5.3 pearson1.m算出的相关系数高得离谱回归系数却符号相反现象相关性分析显示价格与销量高度负相关比如-0.95但回归模型的系数是正数模型的业务解释完全站不住脚。原因这是典型的多重共线性问题——原价、折扣价、折扣率三个变量内部相关性极高回归模型在拆分它们各自的贡献时发生了系数翻转。Pearson相关分析只看了两两关系没有控制第三个变量的影响。解决回归建模时原价、折扣价、折扣率只保留一个我一般保留折扣价因为它才是消费者实际付的钱。如果要三个都放进去就用岭回归或PCA先降维别用普通最小二乘法硬跑。5.4 t03_prediction.py 预测的补货量是一条几乎水平的直线现象未来7天的补货量预测值变化极小基本都在均值附近波动完全看不出周末效应和价格波动的联动。原因时间序列模型ARIMA只用了价格序列自身的历史进行外推而价格序列本身的波动幅度远小于销量序列。ARIMA捕捉到了价格惯性但没有捕捉价格变化对销量的传导效应——这个信息在回归模型里不在价格序列里。解决不要用纯时间序列模型做补货预测。把回归模型的预测销量作为外生变量传入带exog的ARIMAX模型或者简单一点直接对回归模型的输出做一次残差修正。包内t03_prediction.py单独跑出来的直线预测恰恰说明销量预测不能脱离价格变量单独做。5.5 MATLAB和Python混跑时的文件路径与编码问题现象t02_fitting.m 读取从Python导出的CSV时报错“未定义函数或变量”或者读取中文表头时出现乱码。原因MATLAB的readtable默认编码是系统区域设置和Python导出的UTF-8编码不匹配另外Python脚本和MATLAB脚本混跑时工作目录没有统一相对路径指向了不同的文件夹。解决第一个问题在MATLAB里读文件时显式指定readtable(data.csv, Encoding, UTF-8)第二个问题把所有脚本和数据文件放进同一个主目录用绝对路径或cd先切到主目录再跑。如果还是报错把Python输出的CSV表头改成英文再导入MATLAB能省掉一半的编码纠结。6. 验证与收尾残差检验和滚动回测是交卷前的最后一道保险模型跑通只是第一步预测结果能不能经得起评委推敲靠的是验证这一步。包内源码对验证部分没有给出特别完整的实现我一般会补两段检查代码。第一段是残差自相关检查。用Durbin-Watson统计量或者直接画残差的ACF图如果残差在滞后1阶或7阶还有明显自相关说明模型漏掉了短期或每周周期性的信息这时候要回头加滞后项或星期哑变量而不是加大模型阶数硬吞。import statsmodels.api as sm # 计算Durbin-Watson统计量接近2说明残差无自相关 dw sm.stats.durbin_watson(model.resid) print(fDW统计量: {dw:.3f}) # 如果小于1.5或者大于2.5残差还有周期信息没提取干净第二段是滚动回测。用前60%的数据训练后40%的数据按天滚动验证每次只预测未来一天不断把真实值拼进训练集再预测下一天。这样评估出来的MAE和MAPE才是模型真正的泛化能力一次性划分训练集测试集评估出来的指标通常乐观15%到20%。# 滚动回测示意每次推进一天预测次日销量 history list(train_data) predictions [] for t in range(len(test_data)): model ARIMA(history, order(2, 1, 2)) fit model.fit() yhat fit.forecast(steps1)[0] predictions.append(yhat) history.append(test_data.iloc[t])从那以后我每次跑这类商超数据建模题无论时间多紧都强制自己走一遍残差检查和滚动回测这两个步骤——前者拦得住结构性错误后者拦得住“看着好看、实际没用”的虚假高精度。比赛最后两个小时这两步救过我很多次翻车。这份源码包给了你一个完整的框架剩下的细节补全和验证才是决定你能不能从“跑通”到“获奖”的距离。希望帮到你。本文还有配套的精品资源点击获取