2023国赛C题源码拆解:从数据预处理到补货预测的完整建模流程
简介基于Python实现的2023年全国大学生数学建模竞赛C题源码包面向需要掌握商超果蔬定价与补货预测建模方法的学生和参赛者。围绕价格预测、补货预测等核心任务完整覆盖数据爬取、清洗、分词、特征分类、时间序列分析、回归预测与可视化环节并提供MATLAB辅助脚本用于对照验证。压缩包共18个文件包含11个Python脚本负责数据爬取、jieba分词去噪、类别添加、回归与预测、6个MATLAB脚本完成数据拟合、类别划分及Pearson相关性分析及1个说明文档整体仅16KB结构精简、注释清晰便于快速运行调试。已有310人学习下载。读者可获得一套可运行的赛题基线代码理解ARIMA与回归模型在真实商业数据上的应用思路以及爬虫、分类和预测模块的代码组织方式适合课程设计、竞赛复现或二次开发参考。1. 这套国赛 C 题源码真正值钱的是数据预处理2023 年国赛 C 题有个比较反常识的地方真正卡住大多数队伍的并不是最后的预测模型而是前两天的数据预处理。这套源代码包正好把这条最苦的路完整走了一遍——从爬虫抓数、jieba 分词给商品归类到皮尔逊相关性分析、MATLAB 曲线拟合、Python 回归最后落到 t03 预测脚本整条链路是通的。它要解决的核心问题是商超 6 个蔬菜品类、大量销售流水里打折促销对销量的扭曲和品类间的替代关系怎么量化以及未来一周的补货量和定价怎么给。适合备战国赛 C 题的队伍也适合想复现一套完整销量预测流程的从业者新手可以按文件顺序跑熟手可以直接去看 t02 和 t03 的参数设计。2. 先看懂技术路线t01、t02、t03 三个阶段在分工什么2.1 两个工具链的分工Python 当数据加工厂MATLAB 当拟合计算器拿到压缩包第一件事先看文件后缀。.py和.m混着来说明作者没有死守单一语言。Python 这边负责爬虫、分词、回归和预测理由是 requests、jieba、pandas、scikit-learn 这套生态处理文本和表格数据确实顺手MATLAB 那边负责分类标记、曲线拟合和相关性分析fit 函数一条命令能出图交互式调参比 Python 快。我的判断是作者先用了 Python 把零零散散的原始数据整理成规整的 csv然后用 MATLAB 做探索性分析中间结果再喂回 Python 做回归和预测。两套工具之间靠 csv 文件传数据这个接口是整条链路最容易出问题的地方后面第 5 章专门讲。2.2 从文件命名还原处理顺序每个脚本在链路里的位置文件名里 t01、t02、t03 是阶段标记不是随便编的序号。我按自己的理解整理了一张表对照着看整个项目就不乱了。阶段文件在链路里干什么t01 数据准备t01_reptile.py爬取外部补充数据比如天气、节假日t01 数据准备t01_reptile_after.py爬虫结果的后处理去重、统一格式t01 数据准备t01_jieba.py对商品名做分词提取品类关键词t01 数据准备t01_remove.py / t01_delete.m删缺失、删异常记录t01 数据准备t01_getNum.py从文本里提取数量、统一商品编码t01 数据准备t01_vegetable_classification.m按规则给蔬菜分品类t01 数据准备t01_add_category.m把品类标签写回数据表t02 分析建模pearson1.m计算销量与价格、折扣率的皮尔逊相关系数t02 分析建模t02_add_data.m / t02_add_sale_price.py / t02_add_classification.py拼接价格、销量、分类特征t02 分析建模t02_fitting.m对销量-折扣率关系做曲线拟合t02 分析建模t02_regression.py用回归模型替代拟合曲线引入多维特征t02 分析建模t02_prediction.py中间版本的预测脚本验证模型t03 预测输出t03_prediction.py最终预测未来一周销量/补货量t03 预测输出t03_corresponding.py把预测结果对应回单品编码和品类这个顺序和国赛 C 题的常规解法是对上的先处理数据再找关系最后预测。t02_prediction.py 和 t03_prediction.py 同时存在说明作者先做了中间验证最终版本才整理到 t03。2.3 先跑哪一步我建议按 t01 → t02 → t03 的顺序完整走一遍README.md 里没有写太细的执行说明但我看完文件命名后建议你第一次跑的时候严格按 t01、t02、t03 的顺序来不要跳。原因是后面每个脚本都依赖前面生成的 csv 字段比如 t02_regression.py 要用到 t01 阶段加的品类标签t03_prediction.py 要用到 t02 阶段拟合出来的参数。先跑 t01 里的爬虫和 jieba 脚本确认生成的 csv 字段齐全再跑 pearson1.m 和 t02_fitting.m看相关性方向和拟合曲线是否符合业务直觉最后跑 t03 两个脚本检查输出的补货建议表。Python 侧依赖 pandas、jieba、scikit-learn、requestsMATLAB 侧只需要基础功能readtable、corrcoef、fit 都是自带函数不需要额外工具箱。3. 数据预处理爬虫、jieba 分词与品类归约决定后面模型上限3.1 爬虫脚本与后续处理t01_reptile.py 和 t01_reptile_after.py竞赛附件虽然给了销售流水但缺外部环境特征。做补货预测时天气和节假日对蔬菜销量影响很大暴雨天叶菜销量会明显变化这就是爬虫脚本存在的意义。我一般会这样组织爬虫逻辑# t01_reptile.py 思路简化版 import requests import pandas as pd from bs4 import BeautifulSoup def fetch_weather(start_date, end_date): records [] for day in pd.date_range(start_date, end_date, freqD): # 实际使用中要加 headers、限速和重试 resp requests.get( fhttps://example.com/weather?date{day:%Y%m%d}, timeout10, headers{User-Agent: Mozilla/5.0} ) soup BeautifulSoup(resp.text, html.parser) records.append({ date: day, temp: soup.select_one(.temp).text, weather: soup.select_one(.weather).text, }) return pd.DataFrame(records) weather_df fetch_weather(2023-01-01, 2023-06-30) weather_df.to_csv(weather.csv, indexFalse, encodingutf-8-sig)逻辑说明按天遍历日期请求天气页面把温度和天气现象解析出来存成 csv。这里要注意 timeout 必须设置否则某个请求卡住整个脚本就停了utf-8-sig编码是为了后面导入 Excel 或 MATLAB 时不乱码。t01_reptile_after.py 做的是爬虫的后处理去重、补缺失日期、把天气现象映射成数值。比如“晴1、多云2、雨3”因为回归模型不能直接吃中文字符串。这个脚本的价值在于把不规则的外部数据对齐到销售流水的日期索引上对齐错了后面全白做。3.2 jieba 分词做商品名归类t01_jieba.py 的词典与停用词C 题附件里的商品名不是干净的品类名而是类似“云南韭菜花精品400g”“山东小黄瓜水嫩500g”这种带产地、带规格、带宣传语的混合文本。直接按字符串匹配效率很低所以作者用了 jieba 分词先把商品名切成词再去匹配品类关键词。我简化后的核心逻辑是这样# t01_jieba.py 思路先加载自定义词典再对商品名分词取关键词 import jieba # 自定义词典freq 调高避免被拆碎 custom_words [韭菜花, 上海青, 红彩椒, 白玉菇, 金针菇, 西兰花] for w in custom_words: jieba.add_word(w, freq200) category_keywords { 花叶类: [韭菜, 菠菜, 生菜, 油麦菜], 水生根茎类: [莲藕, 土豆, 山药, 莴笋], 茄果类: [番茄, 茄子, 黄瓜], 辣椒类: [青椒, 红椒, 小米椒], 食用菌: [香菇, 金针菇, 杏鲍菇, 白玉菇], } def classify_product(name): parts jieba.lcut(name, cut_allFalse, HMMTrue) for w in parts: for category, keywords in category_keywords.items(): if w in keywords: return category return 未分类参数说明cut_allFalse是精确模式适合关键词匹配场景HMMTrue让 jieba 用隐马尔可夫模型识别词典外的新词对生鲜商品名里的网络词、简称有一定帮助。freq200这个值不是随便给的默认词库里“韭菜”可能被切成“韭/菜”调高频率才能让自定义词优先成词。这里有个业务细节分类规则本身也是建模的一部分。6 个品类的划分标准来自题目的附件说明分类错了后面按品类汇总的销量规律全错。所以 t01_jieba.py 输出的结果我建议人工抽检 50 条确认分类准确率在 95% 以上再继续。3.3 删除与重编码t01_remove.py / t01_delete.m / t01_getNum.py原始数据永远是脏的。t01_remove.py 负责清掉三类问题缺失值、销量为 0 的异常记录、同一商品同一天重复的流水。删除原则是“宁可删异常不要带脏数据进模型”因为后续拟合和回归对离群点都很敏感。# t01_remove.py 思路 import pandas as pd df pd.read_csv(sales.csv, parse_dates[date]) # 缺失销量或价格直接删除 df df.dropna(subset[qty, price]) # 销量为 0 且没有折扣记录的视为异常 df df[df[qty] 0] # 同一商品同一天只保留最后一条 df df.drop_duplicates(subset[product_id, date], keeplast) # 统一商品编码 df[product_code] df[product_name].astype(category).cat.codes df.to_csv(sales_clean.csv, indexFalse, encodingutf-8-sig)逻辑说明dropna只针对建模要用的两列产地、规格这些描述性字段缺失不影响drop_duplicates的keeplast是假设后写入的记录覆盖前面astype(category).cat.codes把商品名映射成整数编码但要注意这个编码在不同批次运行时会变所以一定要保存映射表。t01_delete.m 和 t01_getNum.py 是同一目的的 MATLAB 和 Python 两个版本。t01_getNum.py 做的是更细的字段提取比如从“500g”这种规格文本里抽出重量数值后续做单价对比时要用。这些脚本单看都不难但它们决定了后面所有模型的输入质量。4. 相关性与回归把打折和销量之间的非线性关系量化出来4.1 pearson1.m皮尔逊相关性到底在算什么数据洗干净之后第一个要回答的问题是打折到底有没有用折扣率和销量之间是什么方向的关系pearson1.m 用皮尔逊相关系数把这个关系量化成数值。代码逻辑很直接% pearson1.m data readtable(sales_clean.csv); qty data.qty; discount_rate data.discount_rate; % 统一为折后价/原价范围 0~1 % 计算销量与折扣率的相关系数 R corrcoef(qty, discount_rate, Rows, complete); fprintf(相关系数: %.3f\n, R(1, 2));逻辑说明corrcoef返回相关系数矩阵Rowscomplete表示遇到 NaN 的行直接跳过避免把缺失值带进计算。相关系数接近 -1 或 1 说明线性关系强接近 0 说明没有线性关系。但这里有业务陷阱折扣率和销量的关系往往不是线性的。折扣从 95 折打到 9 折销量可能只涨 5%从 7 折打到 5 折销量可能翻倍。这种边际递增关系用皮尔逊相关系数只能看出方向和粗略强度真正要建模得靠下一节的拟合。所以 pearson1.m 的输出只用来做方向判断不做预测。4.2 t02_fitting.m从相关到拟合曲线注意 StartPoint 这个坑t02_fitting.m 是在相关性分析基础上做的曲线拟合。针对销量随折扣加深而边际递增的特点指数形式a*exp(b*x)c比线性拟合更贴近业务直觉。% t02_fitting.m x data.discount_rate; y data.qty; % 指数拟合销量随折扣率降低而边际递增 ft fittype(a*exp(b*x)c, independent, x); f fit(x, y, ft, StartPoint, [10 -1 1]); % 拟合效果可视化 plot(f, x, y); % 输出拟合参数 coeffvalues(f)参数说明StartPoint是拟合的迭代起点给不好直接返回 NaN这是 MATLAB 拟合最常见的坑。[10 -1 1]的意思是初始销量 10、衰减系数 -1、偏置 1你拿到自己的数据后要先用scatter画散点图观察量级再调起点。fittype里的independent, x显式声明自变量避免 MATLAB 把字段名猜错。t02 阶段还有三个辅助脚本t02_add_data.m、t02_add_sale_price.py、t02_add_classification.py。它们做的事情是把原始流水和价格表、品类表做 join拼成一张宽表让后面的回归模型有完整的特征输入。拼接时要注意一对多关系一个商品编码可能对应多条价格记录join 之前必须确认键的唯一性。4.3 t02_regression.py把拟合升级成回归模型处理多维特征拟合曲线只用了折扣率一个自变量但实际销量还受价格、星期、节假日、上周销量影响。t02_regression.py 就是把这些特征全部放进回归模型用 scikit-learn 实现。我简化后的核心逻辑# t02_regression.py 思路 from sklearn.linear_model import Ridge from sklearn.preprocessing import StandardScaler features [price, discount_rate, last_week_qty, is_weekend] X df[features].values y df[qty].values # 标准化避免价格量级过大主导模型 scaler StandardScaler() X_scaled scaler.fit_transform(X) # L2 正则处理价格与折扣率的共线性 model Ridge(alpha1.0) model.fit(X_scaled, y) print(model.coef_, model.intercept_)逻辑说明价格和折扣率高度相关普通最小二乘回归的系数估计会很不稳定所以用 Ridge 的 L2 正则压缩系数方差。alpha1.0控制正则强度调参时先跑一组[0.1, 1.0, 10.0]对比验证集误差再定。StandardScaler必须和模型一起保存预测新数据时用同一套均值方差做变换否则输入分布不一致。t02_prediction.py 是这个回归模型的直接应用把拟合好的模型对历史数据做回测。这里我建议你关注的不是预测精度而是残差如果某个品类的残差明显大于其他品类说明特征设计漏掉了这个品类的特殊因素比如叶菜受天气影响大这时候应该回去加天气特征而不是调正则参数。5. 避坑双栈协作最容易翻车的五个地方5.1 Python 导出的 csv 被 MATLAB 读错列现象用 pandas 写出的 csvreadtable读进 MATLAB 后日期列变成 double中文列名变成 blank。原因pandas 默认写入 utf-8 编码MATLAB 旧版本对 utf-8 的中文列名支持不好日期列被识别成文本后又自动转成了序列号。文件在 Python 侧看着没问题一过 MATLAB 就变了。解决pandas 写文件时统一用encodingutf-8-sig日期列先转成YYYY-MM-DD字符串再导出MATLAB 读的时候用readtable(xxx.csv, PreserveVariableNames, true)保留原始列名。从那以后我所有跨工具传文件都强制走这个配置。5.2 时间特征当数值用造成数据泄漏现象回归模型在训练集上 R² 高达 0.95但预测未来一周的销量结果离谱预测值一路下滑。原因特征里加了“日期序号”或“当年第几周”这种单调递增的数值模型学到的是时间趋势而不是业务关系遇到未来的日期序号就外推失效。这是时间序列预测里最常见的泄漏。解决特征里只保留周期性时间变量比如星期几、月份、是否节假日删掉日期序号。验证时用时间窗口切分拿最后 7 天做 backtest而不是随机打乱后交叉验证。5.3 jieba 把商品名切碎分类准确率掉到 70% 以下现象“小米椒”被切成“小米/椒”“韭菜花”被切成“韭菜/花”分类规则匹配不到大量商品归到“未分类”。原因jieba 默认词典覆盖不了生鲜领域的商品名复合词被拆成单字词语高频词权重不够。分类错乱会让后续所有按品类的汇总分析失真。解决把品类关键词全部加进自定义词典add_word时把freq调到 200 以上让 jieba 优先组词。再加一道保险分词前先做一轮完整商品名的直接匹配命中就跳过分词只有未命中的才走分词流程。5.4 MATLAB 曲线拟合返回 NaN不收敛现象fit运行完coeffvalues(f)输出全 NaN或者拟合曲线是一根水平的直线完全不符合散点分布。原因指数拟合是非线性优化问题对初值敏感。StartPoint给得太离谱优化过程发散或者数据本身量级太大比如销量上千、折扣率 0~1函数计算溢出。解决先scatter看散点的纵轴量级再给StartPoint。也可以用polyfit先试一次多项式拟合拿到粗略趋势后再换非线性模型把多项式系数当非线性拟合的起点。5.5 折扣率口径不统一模型特征自相矛盾现象同一个商品在不同日期的discount_rate有的记录是 0.8有的是 -20%还有的是 80模型训练直接报错或者系数混乱。原因数据来源不同有的字段记录的是折后价/原价有的是降价百分比还有的是“比原价低 20%”的反向表达。坑就坑在数据仓库里的字段名都一样。解决在 t01 清洗阶段就统一成“折后价/原价”范围裁剪到 0.1~1.0 之间超出范围的视为异常置为缺失。这个口径确认要写进数据字典里不然后面换个人接手他大概率会在这个字段上重新踩一遍。6. 预测落地t03 脚本怎么把数字变回可执行的补货建议t03_prediction.py 是最终预测脚本它把 t02 阶段训练好的回归模型拿过来对每个单品生成未来 7 天的销量预测。t03_corresponding.py 是它配套的对应脚本把预测结果映射回商品编码、品类名称和原始商品名输出一张可以直接给商超采购看的表。对应关系是这里的关键模型内部用的是整数编码的product_code但业务上需要的是“云南韭菜花精品400g”这种可读名称两个脚本一个管算、一个管翻译。补货量不能直接用预测销量要考虑损耗率。2023 年 C 题背景里蔬菜有损耗率参数实际补货要按“预测销量 / (1 - 损耗率)”来算否则到晚上就缺货了。对应逻辑可以这样做# t03_prediction.py 到 t03_corresponding.py 的衔接思路 pred result.merge( products[[product_code, product_name, loss_rate]], onproduct_code, howleft ) pred[补货量] (pred[pred_qty] / (1 - pred[loss_rate])).clip(lower0).round()说明merge的howleft保证以预测结果为准防止商品编码对不上时丢行clip(lower0)是给预测值兜底防止极端情况下出现负补货量。验证预测结果我习惯从三个维度做。第一是时间后验拿最后一周的数据当测试集看模型预测值和实际值的误差是不是稳定第二是业务合理性补货量不能为负、预测价格不能低于批发价这些约束条件写进脚本里做硬校验第三是数值稳定性检查输出表里有没有 NaN 和极端离群值这两个指标异常说明上游特征拼接有问题。这套流程我拆完最大的感受是国赛 C 题拿高分的关键不一定是最新的模型而是每一步数据都有据可查。从那以后我每次跑完预测都会强制走一遍“数据口径核对 → 时间窗口切分 → 业务约束校验”这三步用这套代码复现的时候也建议你把这个习惯保留下来希望帮到你。本文还有配套的精品资源点击获取