简介:针对某商超蔬菜价格与补货策略的建模分析压缩包,适合数学建模参赛者、数据分析师及商超运营人员,可用于赛题复现、业务复盘或教学参考。资源围绕数据采集清洗、时间序列预测、回归定价、库存优化等展开,内含源码、数据集、可视化图表和论文模板,可复现从数据预处理到策略输出的完整流程。包体共94个文件,约11.59MB,核心为22个xlsx数据表、6个ipynb分析笔记、2个Python脚本及2份md说明,另有59张png结果图与tex/cls论文排版模板,目录划分清晰,便于按问题定位。已有240人浏览学习。通过阅读src脚本与notebooks交互文档,可掌握Prophet销量预测、成本加成定价、排队论调度等模型的落地写法;同时附带的多个品类相关性、利润率和批发价预测结果表,能为复盘论文图表和进一步拓展实验提供直接参考。
1. 商超蔬菜价格与补货策略研究:别人给你源码和数据,你该怎么落地
拿到一个名为"某商超蔬菜价格和补货策略研究内含源码和数据集.zip"的压缩包,第一反应不该是双击解压然后跑 main.py。这类项目在生鲜零售里属于典型的"需求预测 + 价格弹性 + 库存优化"三合一问题,源码只是最后 10% 的工程量,前面 90% 的时间花在数据清洗、特征构造和参数标定上。这篇文章按一个数据工程师拿到这个 zip 后的完整路径来讲:先立清楚价格弹性和补货模型各自的适用范围,再给能直接改参数跑的 Python 代码,最后拆解源码里最容易出问题的几个环节。适合刚接触零售数据分析或准备做生鲜类项目的开发者,也适合想从 Excel 转向 Python 做决策支持的运营同学。
2. 数据集预处理:商超蔬菜流水账怎么变成训练样本
2.1 先看字段,别急着画分布图
商超蔬菜数据集的字段再乱,也绕不开这几类:SKU 编码、销售日期、销量(公斤或把)、零售价(元/公斤)、批发进价、促销标记(0/1)、当日到货量、报损量。有些数据集会附带天气和节假日,没有的就得自己想办法补。"某商超蔬菜价格和补货策略研究"既然配套了数据集,第一步就是做字段字典,确定哪些是外生变量、哪些是决策变量。常见做法是写一个 Python 脚本把 CSV 的结构和类型打印出来,同时做一次完整性的 CRC 校验,防止解压时数据损坏——压缩包传输过程中出现半个字节的丢失,后面所有回归结果都会变得不可信。
| 字段名 | 类型 | 常见问题 | 处理建议 |
|---|---|---|---|
| sku | str | 编码不统一 | 统一转大写,去空格 |
| date | str/datetime | 格式混杂 | 统一为 YYYY-MM-DD |
| sales | float | 出现负值 | 分退货与报损,分别处理 |
| price | float | 相邻日跳变超 30% | 用 7 日中位数回填 |
| promo | int | 0/1 标记缺失 | 缺失按 0 处理,别填充 |
| arrival | float | 与销量匹配不上 | 保留原值,单独建列 |
import pandas as pd df = pd.read_csv('vegetable_sales.csv', encoding='utf-8') print(df.shape) print(df.dtypes) print(df.isnull().sum()) # CRC 校验下载完整性 import zlib with open('vegetable_sales.csv', 'rb') as f: data = f.read() print(hex(zlib.crc32(data)))这段代码做两件事:第一遍扫描数据集的形状和缺失情况,第二遍用 zlib 的 crc32 计算文件校验值。CRC 校验在数据集下载场景里很实用,尤其是从网盘或邮件附件拿到的 zip,解压工具报"error read zip archive"或提示找不到 EOCD 记录时,多半是文件没传输完整,重新下载比修复快得多,不需要绕到压缩包密码破解工具那一步。
2.2 销量负值和价格跳变是常态
生鲜销售流水表里,销量出现负值通常不是数据错误,而是退货、报损或盘点差异。直接删掉会损失信息,全保留又会污染模型。处理逻辑一般是:销量为负且绝对值小于当日正销量 5% 的,视为退货运差,按 0 处理;超过 5% 的,标记为报损事件单独建列。价格跳变则要区分是真实调价还是录入错误——同一 SKU 相邻两天零售价变动超过 30%,基本可以判定为录入问题,用前后 7 天中位数回填。
df['sales_clean'] = df['sales'].where(df['sales'] >= 0, 0) daily = df.groupby(['sku', 'date'])['sales_clean'].sum() report_loss = df[df['sales'] < 0].groupby(['sku', 'date'])['sales'].abs() df['is_loss_event'] = df['sales'] < 0 # 价格跳变修正:变动超过 30% 用 7 日中位数回填 price_delta = df['price'].pct_change() df.loc[price_delta.abs() > 0.3, 'price'] = ( df['price'].rolling(7, center=True).median() )参数说明:销量负值的 5% 阈值和价格跳变的 30% 阈值是生鲜行业的经验值,不是固定真理。叶菜类价格波动本来就大,30% 可能会误伤真实调价;根茎类价格稳定,可以收紧到 15%。先跑分布统计再定阈值,比拍脑袋可靠。注意rolling(7, center=True)用了中心窗口,意味着回填时会用到后 3 天的数据,在离线分析场景没问题,如果要做线上实时预处理,得改成rolling(7)并手动对齐。
2.3 特征工程:把日期变成模型能消化的信号
蔬菜销售和天气、节气、节假日强相关。数据集里如果没有这些字段,用 Python 的第三方库去补,工作日、周末、农历节气、当地最高低温、降雨量,每一样都要做滞后特征。补货决策真正依赖的不是"今天下雨",而是"去年同期下雨时销量涨了多少",所以特征要落在平移和比率上。
df['weekday'] = pd.to_datetime(df['date']).dt.weekday df['is_weekend'] = df['weekday'] >= 5 df['holiday'] = df['date'].isin(holiday_list) # 滞后7天销量均值,捕捉周期 df['sales_lag7'] = df.groupby('sku')['sales_clean'].transform( lambda x: x.shift(7).rolling(7).mean() ) # 价格比:当日价格 / 前7天均价 df['price_ratio'] = df['price'] / df.groupby('sku')['price'].transform( lambda x: x.shift(1).rolling(7).mean() )这里的关键参数是滞后窗口选 7 而不是 3。生鲜蔬菜存在明显的周周期,周末销量高峰、周中低谷,7 天滞后能平滑掉单日噪音。价格比这个特征是把绝对价格转换成相对价格,方便后面做价格弹性分析时排除整体物价上涨的影响。免费 Python 源码大全里常见的是直接用日期做 one-hot,那样会制造出几十个稀疏列,对树模型不友好,对回归更是灾难。
3. 价格弹性分析:找到"涨价多少会少卖多少"的量化答案
3.1 模型选型:为什么用对数线性而不是线性回归
商超定价的核心问题是价格弹性系数——价格上涨 1%,销量下降百分之几。对蔬菜这种需求价格弹性不恒定的商品,线性模型假设弹性固定,在低价区间会高估销量、高价区间会低估,所以主流做法是对销量和价格同时取对数,做对数线性回归。弹性系数就是这个模型里价格的回归系数。这个项目的源码里如果用的是普通线性回归跑价格系数,第一件事就是把它改成 OLS-log 模型。
import statsmodels.api as sm import numpy as np df['log_sales'] = np.log(df['sales_clean'] + 1) df['log_price'] = np.log(df['price']) df['log_lag7'] = np.log(df['sales_lag7'] + 1) X = df[['log_price', 'log_lag7', 'holiday', 'is_weekend']] X = sm.add_constant(X) model = sm.OLS(df['log_sales'], X).fit() print(model.summary())逻辑说明:对销量加 1 再取对数是为了避免销量为 0 时出现负无穷;log_lag7放进模型是为了控制销量惯性,如果不控制它,价格系数的绝对值会被高估。运行 summary 后需要看三列:coef是弹性系数,p 值要小于 0.05,R-squared 在这个场景下到 0.4 以上就是可用的——时间序列横截面混合数据很难跑出很高的 R²,别追求教材里的 0.9。
3.2 分组跑弹性:叶菜和根茎是两个物种
把全部 SKU 放一起算一个弹性系数是整个分析里最容易出错的操作。菠菜、油菜这类叶菜,保鲜期短、替代品多,价格弹性系数通常在 -1.5 到 -2.5 之间,涨一点价销量就明显下滑;土豆、胡萝卜这类根茎,耐储存、刚需属性强,弹性大概率在 -0.3 到 -0.6 之间。合并建模会把两组商品的平均效应算出来,导致每个 SKU 的定价建议都是错的。
results = {} for category in df['category'].unique(): sub = df[df['category'] == category].copy() sub['log_sales'] = np.log(sub['sales_clean'] + 1) sub['log_price'] = np.log(sub['price']) X = sm.add_constant(sub[['log_price', 'holiday', 'is_weekend']]) model = sm.OLS(sub['log_sales'], X).fit() results[category] = model.params['log_price'] print(results)按品类分组跑完之后,每个类别会得到一个弹性系数表。这个表的直接用途是定价决策:弹性绝对值大于 1 的商品,降价促销能带来销售额增长;小于 1 的,降价只会压毛利。商超蔬菜的定价策略基本就是"高弹性单品用低价引流,低弹性单品做毛利担当"。这个结论反过来会指导补货模型里的单品分层,所以分组弹性计算要跑在补货策略之前。
3.3 促销变量必须单独建模
数据集里有促销标记的,不要把它并进价格变量里。蔬菜促销往往是"每斤减两元"而不是"打八折",如果直接用零售价做变量,促销期间的销量抬升会被误判成低价敏感,弹性系数会虚高。处理办法是加一个促销虚拟变量,同时把价格拆成"常规价"和"促销价"两个字段。源码里如果只有一个 price 列,跑出来的弹性系数大概率偏大,要对这份源码的输出保持怀疑,这一条能省下后面很多无效调参的时间。
4. 补货策略实战:报童模型在蔬菜订货上的落地写法
4.1 报童模型为什么适合生鲜
补货决策的本质是"多订了怕损耗,少订了怕缺货"的权衡。报童模型(Newsvendor Model)正好解决这个问题,它把最优订货量定义为边际成本等于边际收益的点:多进一份货能卖出去赚的毛利,和卖不出去亏的进价,两者相等时的订货量就是最优值。蔬菜和报纸的高度相似在于都是当日损耗、当日报损,今天卖不掉明天基本不能卖。源码里如果补货部分用的是简单移动平均预测,可以在这个基础上叠加报童模型来修正订货量。
import scipy.stats as st import numpy as np # 参数:毛利率、损耗率、服务水平 gross_margin = 0.35 cost = 3.2 price = 4.9 unit_profit = price - cost unit_loss = cost # 服务水平 = 单位收益 / (单位收益 + 单位损失) service_level = unit_profit / (unit_profit + unit_loss) # 用历史销量的均值和标准差近似需求分布 forecast_mean = 150 forecast_std = 30 z = st.norm.ppf(service_level) optimal_qty = forecast_mean + z * forecast_std print(f"服务水平: {service_level:.3f}, 最优补货量: {optimal_qty:.0f}")逻辑说明:service_level就是报童模型的最优缺货概率,它由毛利率结构决定。毛利率越高,缺货的机会成本越大,服务水平就越高,订货量越偏向多进;毛利率低、损耗率高的商品,服务水平就低,订货量保守。forecast_mean和forecast_std来自历史销量预测,实际项目中用上一节加的滞后特征去拟合每日均值,再算残差的标准差,不要直接拿原始销量算。gross_margin在这里只用来理解服务水平的形成,订货量计算本身不需要它,真正决定订货量的是均值和标准差的估计质量。
4.2 预测和补货为什么要分离
一个常见的错误是把预测模型和补货模型揉在一起,用一个神经网络直接输出订货量。高频小幅度的日常补货不建议这么干,生鲜数据量小、噪音大,黑盒模型的误差难定位。工程上建议保持"预测 + 决策"两段式:先预测需求分布(均值加方差),再用报童模型把分布转成订货量。这样当天预测偏了,能分清是预测的锅还是订货量计算的问题。
# 两段式实现:预测均值/方差 -> 报童订货量 from statsmodels.tsa.holtwinters import ExponentialSmoothing model = ExponentialSmoothing( series, seasonal_periods=7, trend='add', seasonal='add' ).fit() mean_forecast = model.forecast(1)[0] residuals = model.resid std_forecast = residuals.std() def newsvendor_qty(mean, std, profit, loss): sl = profit / (profit + loss) z = st.norm.ppf(sl) return max(0, mean + z * std)参数说明:Holt-Winters 的三次指数平滑在这里比 ARIMA 更实用,因为生鲜需求有明确的一周季节性,而且对短期预测来说,指数平滑的延迟特性反而是优点。seasonal_periods=7固定了周周期,trend='add'表示允许销量缓慢增长。残差的标准差作为需求波动估计,这一步决定了安全库存的大小,波动估计偏小会导致缺货率飙升。newsvendor_qty里对最终结果做了max(0, ...)截断,防止负订货量。
4.3 安全库存不是拍脑袋的"多进 10%"
很多运营习惯用"预测值乘以 1.1"来做安全库存,这在生鲜品类是有问题的。10% 的加量在需求波动大的叶菜上远远不够,在土豆这种耐储商品上又造成库存积压。安全库存的正确计算方式是 z 值乘以需求标准差再乘以补货周期 lead time 的平方根。每天凌晨配送到店的商品,lead time 接近 0,安全库存可以很小;但如果仓库要隔天到货,lead time 变成 1,安全库存就要乘以 1.41。
lead_time_days = 1 safety_stock = st.norm.ppf(0.95) * std_forecast * np.sqrt(lead_time_days) order_qty = max(0, round(mean_forecast + safety_stock - stock_on_hand))注意这个公式算的是"今天该订多少",不是"今天该到多少"。如果日清模式已经包含在 lead_time 里,stock_on_hand就是当日开门前的可用库存。95% 的服务水平对应 z 值约 1.65,换算成缺货率是 5%,商超通常取 90% 到 95% 之间,高了损耗受不了,低了顾客抱怨。最终参数取多少,要用历史数据回测一张表格来定,不能凭感觉。
| 服务水平 | z 值 | 适用场景 | 预期缺货率 |
|---|---|---|---|
| 85% | 1.04 | 低毛利、高损耗叶菜 | 15% |
| 90% | 1.28 | 常规蔬菜 | 10% |
| 95% | 1.65 | 高毛利、引流单品 | 5% |
| 97% | 1.88 | 招牌菜、会员单品 | 3% |
5. 拿到 zip 后的验证清单和参数敏感性调试
5.1 解压后不要急着跑,先做三件事
第一件事是校验压缩包完整性,用unzip -t在 Linux 上测试,Windows 下用 7-Zip 的"测试"按钮。如果报错指向 EOCD 缺失或 read zip archive 失败,多半是 zip 没有传输完整,这类问题跟压缩包密码无关,直接重新下载就行。第二件事是检查依赖版本,运行pipreqs或直接看 requirements.txt 里的 numpy、pandas、statsmodels 版本,statsmodels 在不同版本之间的 API 差异很大,sm.add_constant和结果对象的字段名都变过。第三件事是把数据集拆成训练集和验证集,按时间切分而不是随机切分——生鲜数据有强时序性,随机抽样会让模型偷偷看到未来信息,回测结果虚高。
unzip -t 某商超蔬菜价格和补货策略研究内含源码和数据集.zip python -m pipreqs . --force5.2 用滚动回测调服务水平和弹性分组
import pandas as pd import numpy as np def backtest(service_level, df): total_profit = 0 total_spoilage = 0 total_shortage = 0 for date, day_data in df.groupby('date'): # 模拟当日订货与销售 order = predict_and_order(day_data, service_level) sales = day_data['sales_clean'].sum() arranged = min(order, sales) shortage = max(0, sales - order) spoilage = max(0, order - sales) * 0.5 # 部分可退货 total_profit += arranged * unit_profit - spoilage * unit_loss return total_profit for sl in [0.85, 0.90, 0.95]: profit = backtest(sl, df) print(f"服务水平 {sl:.0%} 时,回测毛利 {profit:.0f}")回测输出的几组数字放在一起看才有意义:毛利、损耗量和缺货次数。服务水平从 0.90 调到 0.95,毛利可能只增加 3%,损耗却涨了 15%,那就不值得提服务水平。这个权衡没有标准答案,取决于门店对缺货的口碑敏感度。调参的时候顺带把弹性分析里的分组结果当成先验信息:高弹性组用低服务水平保毛利,低弹性组用高服务水平保口碑。
源码里的注释和数据集字段名可以当参考,但模型结构建议按自己的数据质量重写。生鲜补货的坑大多不在算法而在数据:价格跳变、销量负值、促销混淆这三个问题处理干净,报童模型和弹性分析的结果就基本可信了。把跑通的参数组合沉淀成一个 JSON 配置文件,每次调完价格或换季时直接改配置,比改代码快得多,也方便不同门店直接复制同一套策略。
本文还有配套的精品资源,点击获取