
简介基于深度学习LSTM实现蔬菜价格预测的完整项目资源包面向Python学习者、数据挖掘爱好者以及需要进行课程设计、毕业设计或实战项目的人员覆盖从网页数据采集、数据清洗、时间序列建模到Web与微信公众号结果展示的完整流程。项目以菜心、西红柿等蔬菜价格及天气等多源数据为切入点综合运用Scrapy、BeautifulSoup完成数据爬取使用Pandas、NumPy完成预处理以长短期记忆网络LSTM及Sklearn、Statsmodels等模型进行对比预测并借助Flask搭建展示页面技术栈完整、贴近真实分析任务。资源共181个文件包含142个CSV数据文件、25个Python源码文件以及pyc缓存、docx说明文档和md笔记压缩包仅1.66MB便于快速下载、按目录研读与复现实验。目前已有247人学习下载。获取后即可得到可运行的爬虫与预测脚本、LSTM模型实现、数据预处理与模型对比思路以及Web端与公众号对接示例适合作为蔬菜价格预测类数据分析项目的参考模板。1. 蔬菜价格预测为什么 LSTM 是入门时序建模最划算的一条路蔬菜价格预测看着是小事真做起来却让不少人头疼批发价数据噪声大节假日和天气一扰动就失灵。基于深度学习LSTM实现蔬菜价格预测是这类时序问题里最务实的入门方案——不需要海量数据一份按日记录的菜价表加上一个几十行的模型就能预测未来一两天的价格区间直接服务采购备货和定价。适合做这件事的人有两类供应链和采购岗位的从业者想用量化预测替代拍脑袋刚接触时序建模的开发者想在真实数据集上跑通数据清洗、序列构造、LSTM 训练和评估的完整链路。菜价预测从来不是追求百分百准确而是在低信噪比的序列里拿到比均值回归更可靠的参考值这一点先摆正后面很多参数决策就不会走偏。下面按一个可复现的完整流程来写先讲 LSTM 在价格数据里到底学什么再落到数据预处理、模型搭建和训练评估最后把最容易翻车的地方和验证技巧逐个拆开。2. 先搞懂 LSTM 在价格预测里到底在学什么2.1 价格序列和普通回归数据的区别时序依赖从哪来很多第一次做时序预测的人习惯性把数据整理成「特征加标签」的形式就往模型里塞结果发现 LSTM 的表现还不如一个均值预测。这不是模型的问题而是没理解价格数据最核心的性质时序依赖。普通回归任务里每一条样本是独立的。房价预测中某一套房和另一套房之间没有先后顺序把样本顺序打乱完全不改变问题本身。菜价数据完全不同今天的价格里藏着昨天的影子昨天的价格又受前天影响。比如连续阴雨后的第三天菠菜批发价往往才开始明显上涨这种滞后效应如果只看当天的天气和供需数据常规模型根本学不到。LSTM 之所以适合这类数据是因为它在每个时间步维护一个隐藏状态这个状态像一条传送带t-1 的价格信息、t-2 的波动幅度、t-3 的异常值都会通过门控机制被筛选后传递到当前时刻。换句话说LSTM 学的不是「今天输入什么就输出什么」的静态映射而是「过去一段时间的价格轨迹会怎样影响下一站」。这也是后续所有操作的总原则样本之间的先后顺序是数据的一部分任何打乱顺序的操作都在破坏信息结构。后面会反复提到这个原则因为翻车点几乎都出在顺序和切片上。2.2 LSTM 的三个门在菜价数据里的直观对应LSTM 的内部结构很多教程都画过图但换成菜价场景更容易建立直觉。遗忘门决定从历史状态里丢掉什么信息一个月前的降雪导致菜价短期冲高这个影响早该消退遗忘门会把它清掉而三天前批发市场到货量骤减带来的涨价压力保留价值就高得多。输入门决定把当前时间步的哪些新信息写进状态比如今天突发的高温预警对叶菜价格的影响。输出门决定从当前状态里抽出哪些内容作为这一时刻的输出在预测任务里通常就是下一个时间步的价格。这个结构对应到实际调参有一个直接结论不要一上来就用很长的历史窗口。如果 lookback 开得过大门控机制没有把无关信息滤干净模型会把几个月前的价格波动当成当前趋势去学预测精度反而下降。第 3 章会给出一个具体的窗口选择方法。2.3 为什么选 LSTM 而不是 ARIMA、Prophet 或 Transformer很多人会问做菜价预测是不是有更轻量的办法。我通常把几个常见方案拉一张对比表选型逻辑会清楚很多。方案对菜价数据的适配度主要短板ARIMA低线性模型对节假日脉冲和天气突变无能为力且每个品种都要单独做平稳性检验和差分Prophet中擅长趋势和周期性但忽略价格自身的动量依赖昨天的价格对今天的影响学不了Transformer低需要大量数据几百到一千多条日度数据远不够让自注意力机制学出可靠模式LSTM高几百条数据就能收敛天然建模动量依赖训练资源要求低LSTM 恰好落在可行区间对序列长度不敏感数据量小也能训练出有意义的模式门控机制让它可以记住一周前的价格水平也可以忘掉三个月前的异常波动训练一轮在 CPU 上也就是几分钟的事。当然LSTM 内部像个黑匣子中间状态难以直接解释但配合敏感性分析或简单的输入扰动测试完全可以把决策依据粗略还原出来。对菜价预测这个具体任务来说LSTM 是最务实的选择这也是这个项目包的核心原因——代码量小、复现门槛低、效果可预期。3. 把数据集变成 LSTM 能吃的样本清洗、归一化与滑动窗口3.1 原始菜价数据的常见形态与清洗要点拿到数据集后常见的形态是三列date日期、variety品种名称、price批发价或均价有的还会带一列 market市场名称。不管数据来自批发市场官网采集还是手工台账第一件要做的事是预览和清洗。直接把原始 CSV 喂给模型是最常见的翻车姿势缺失值、重复日期和乱序记录都会让后续的滑动窗口构造产生错位。import pandas as pd import numpy as np # 统一日期格式按日期升序排列 df pd.read_csv(vegetable_price.csv) df[date] pd.to_datetime(df[date], format%Y-%m-%d) df df.sort_values(date).reset_index(dropTrue) # 检查缺失值和重复日期 print(缺失值统计\n, df.isnull().sum()) print(重复日期数量, df[date].duplicated().sum()) # 对缺失价格做线性插值连续缺失不超过 3 天 df[price] df[price].interpolate(methodlinear, limit3, limit_directionboth) df df.dropna(subset[price]).reset_index(dropTrue)interpolate 的 limit3 表示只填充连续缺失不超过 3 天的空洞超过 3 天保留 NaN 后整行丢掉。这是一个有意识的取舍菜价数据里连续缺失超过一周往往对应市场休市或统计中断这种空洞用插值补出来反而是污染。limit_directionboth 让序列开头和结尾的缺失也能用单侧插值补齐但这只是兜底如果两端缺失过多建议直接按日期范围截断而非依赖插值。如果你的数据集包含多个品种记得先按品种过滤或分组再对每个品种单独执行清洗和后续步骤。多品种混在一起做插值会引入品种间的价格错位这是新手容易踩的第二个坑。3.2 归一化MinMaxScaler 的正确打开方式与反归一化LSTM 对输入尺度敏感sigmoid 和 tanh 的激活区间决定了原始价格数值直接输入会导致梯度异常。常见做法是用 MinMaxScaler 把价格压缩到 0 到 1 之间。这里有一个几乎所有人都会踩的坑必须先切分训练集和测试集再对训练集 fit最后用同一个 scaler 去 transform 测试集。如果先对整个数据集 fit 再切分测试集的价格边界信息就通过 scaler 泄漏进了训练过程评估结果会虚高部署到真实场景后立刻现原形。from sklearn.preprocessing import MinMaxScaler import joblib # 按时间顺序切分前 80% 训练后 20% 测试 train_size int(len(df) * 0.8) train_df df.iloc[:train_size] test_df df.iloc[train_size:] scaler MinMaxScaler(feature_range(0, 1)) train_scaled scaler.fit_transform(train_df[[price]].values) test_scaled scaler.transform(test_df[[price]].values) # 保存 scaler预测新数据时用同一个对象做反归一化 joblib.dump(scaler, scaler.pkl)feature_range(0, 1) 是默认值一般不需要改。切分比例 0.8 是经验值如果数据量少于 500 条可以考虑提高到 0.9保证训练集足够学出趋势。有一个现象需要提前知会如果价格序列尾部有明显上涨趋势训练集的 max 会小于测试集的 max预测阶段反归一化后价格会被压缩在训练区间内看起来像「预测值永远比真实值低」。这是趋势外推能力弱的正常表现不是 bug。缓解手段是先把价格做对数变换或差分再归一化属于进阶内容。3.3 滑动窗口构造 X/ylookback 怎么选才不翻车LSTM 的输入形状是 (样本数, 时间步数, 特征数)时间步数就是 lookback表示用过去多少天的价格预测下一天。构造滑动窗口的代码不难但窗口大小的选择直接决定模型学到的模式是短期波动还是长期趋势。def create_sequences(data, lookback): 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) lookback 7 X_train, y_train create_sequences(train_scaled, lookback) X_test, y_test create_sequences(test_scaled, lookback) print(训练集形状, X_train.shape, 标签形状, y_train.shape)lookback 的取值我一般按数据频率来定日度数据从 7 开始试因为果蔬价格天然有一周的周期效应周末和节前节后的走货节奏不同然后试 14 和 30在验证集上对比 MAE。有一个经验阈值值得参考lookback 超过数据总量的 10% 后收益递减且过拟合风险上升比如 300 条数据就不建议把 lookback 拉到 60 以上。X_train 的形状是 (样本数, 7, 1)最后一个维度 1 代表单特征价格。如果后续要加入节假日、天气等额外特征把多个特征按列拼进 data 数组即可模型输入维度同步调整。注意 create_sequences 里遍历到 len(data) - lookback 为止末尾 lookback 条数据没有对应的 y这是正常截断。测试集的 X_test 是连续滑动窗口预测目标 y_test 对应每个窗口后一天的价格评估时需要保证索引对齐。4. 基于 LSTM 的蔬菜价格预测最小可运行实现拿到项目包后按项目说明里的步骤走通常是先安装依赖、再运行训练脚本、最后跑预测脚本。下面把核心环节的代码结构和参数选择讲清楚这样你既能跑通也知道每一步为什么这么写。4.1 模型结构从单层到双层的参数选型模型结构是 LSTM 项目里玄学浓度最高的部分网上各种「三层 LSTM 加注意力」的架构一搜一大把但对菜价这种单变量小数据结构越花哨越容易过拟合。我的经验是从基线开始先跑通再加深。基线结构是单层 LSTM 加全连接输出如果训练损失降不下去再加一层 LSTM 和 Dropout同时减小各层 units。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout lookback 7 model Sequential([ LSTM(64, return_sequencesTrue, input_shape(lookback, 1)), Dropout(0.2), LSTM(32, return_sequencesFalse), Dropout(0.2), Dense(1) ]) model.compile(optimizeradam, lossmse, metrics[mae, mape])几个关键参数要解释清楚。第一个 LSTM 的 units64 表示隐藏维度为 64第二个为 32这个组合对单变量价格序列够用。特征数只有 1hidden units 开得再大也只是增加参数量收益很有限反而更容易让模型把训练集上的噪声背下来。return_sequencesTrue 表示当前层输出每个时间步的隐藏状态这样下一层 LSTM 才能继续按时间步展开最后一层 LSTM 用 return_sequencesFalse只输出最后一个时间步的状态再接 Dense(1) 输出下一日价格。Dropout 放在 LSTM 层之后训练时随机丢弃 20% 的神经元输出目的是抑制过拟合。损失函数用 mse因为 MSE 对大偏差的惩罚更重价格预测里一个大的预测误差意味着真金白银的损失。metrics 同时监控 mae 和 mapemape 方便直观理解误差百分比方便和业务方沟通。4.2 训练配置epochs、batch_size、learning rate 怎么搭训练配置决定你在这个项目上要调试多久。菜价数据集量级小几百到几千条样本训练时间不是瓶颈过拟合才是。from tensorflow.keras.callbacks import EarlyStopping early_stop EarlyStopping( monitorval_loss, patience15, restore_best_weightsTrue ) history model.fit( X_train, y_train, epochs100, batch_size32, validation_split0.1, shuffleFalse, callbacks[early_stop], verbose1 )这里最容易被忽略的是 shuffleFalse。Keras 的 fit 默认会打乱训练样本这对图像分类没问题但时序样本一旦打乱时间依赖关系就被破坏模型只能学到价格的均值水平。这是我用一次翻车换来的血泪经验建议直接刻在项目说明的显眼位置。epochs 设 100 搭配 EarlyStoppingEarlyStopping 的 patience15 表示验证损失连续 15 个 epoch 不下降就停止restore_best_weights 会自动回滚到验证集上表现最好的权重这相当于免费帮你做了一次模型选择。validation_split0.1 表示从训练集尾部按顺序切出 10% 做验证不会打乱。batch_size32 是默认值数据量很小时可以减到 16让梯度更新更频繁但训练噪音也会变大。learning rate 没有显式设置用 Adam 的默认值 0.001 起步即可菜价数据不需要额外调学习率优先调 lookback 和模型宽度。4.3 评估指标与预测曲线可视化训练完先看指标再画图。菜的均价预测里MAE 是最直观的指标单位就是元RMSE 对偶尔的大偏差更敏感如果 RMSE 明显大于 MAE说明模型在少数异常日上预测偏差很大常见于节假日和天气突变日MAPE 是相对百分比误差适合横向对比不同品种之间的预测难度。from sklearn.metrics import mean_absolute_error, mean_squared_error y_pred model.predict(X_test, verbose0) # 反归一化回真实价格单位 y_pred_inv scaler.inverse_transform(y_pred) y_test_inv scaler.inverse_transform(y_test.reshape(-1, 1)) mae mean_absolute_error(y_test_inv, y_pred_inv) rmse np.sqrt(mean_squared_error(y_test_inv, y_pred_inv)) print(fMAE: {mae:.3f} 元, RMSE: {rmse:.3f} 元) import matplotlib.pyplot as plt plt.figure(figsize(12, 5)) plt.plot(y_test_inv, label真实价格, linewidth2) plt.plot(y_pred_inv, labelLSTM 预测, linewidth1.5) plt.legend() plt.title(蔬菜价格预测对比) plt.xlabel(测试集天数) plt.ylabel(价格元) plt.show()注意这里有一个对齐细节X_test 的第 i 个样本对应 test_scaled 中第 i 到 ilookback-1 天的窗口预测目标 y_test[i] 是第 ilookback 天的价格。model.predict(X_test) 的输出顺序和 y_test 是天然对齐的直接用 inverse_transform 换回价格单位即可。如果手滑把 predict 的结果和平铺的整个测试序列做差索引会错位一个 lookback画出来的图看起来好像模型整体滞后了一天——这不是模型的问题是索引问题。5. 避坑指南菜价预测最容易翻车的 5 个地方下面五条是我见过和踩过频率最高的坑每一条都按「现象、原因、解决」展开建议对照自己的项目排查一遍。5.1 数据泄漏归一化时用了全量数据的统计量现象训练集和验证集上的 MAE 都很好但模型一上真实新数据就明显偏差或者固定划分的测试集误差比验证集差出好几倍。原因先对整个数据集 fit 了 MinMaxScaler再切分训练集和测试集。测试集的 min 和 max 信息通过 scaler 泄漏进了训练过程相当于模型在训练时偷看了未来价格的边界评估结果虚高。解决严格按「先切分、再 fit、再 transform」的顺序操作。增量更新数据集时每次重新拟合 scaler 并持久化预测时只用预测时刻之前的数据来计算统计量。5.2 反归一化写错导致预测曲线贴地飞行现象预测曲线和真实曲线形状一致但整体偏低尤其在价格持续上行的阶段预测值几乎贴着序列底部走。原因训练区间的价格上限低于测试区间的上限模型输出被压缩在 0 到 1 之间反归一化时用训练集的 min/max 换算超出训练区间的那部分价格永远无法被预测到。解决如果序列有趋势或季节漂移先做一阶差分或对数变换把非平稳成分去掉再归一化更省事的方案是改用第 6 章的滚动训练每个训练窗口重新拟合一次 scaler让统计量跟随最新行情。5.3 节假日和天气突变让模型集体失灵现象模型平时 MAE 在 0.3 元以内一遇到春节前一周、台风警报或连续暴雨预测误差直接飙到 2 元以上且误差集中在某几天。原因菜价数据的大部分方差来自低频高影响的异常事件而不是日常波动。单靠历史价格序列模型没有足够信号识别春节或台风这类外部事件它只能把这些天当成普通的波动来推。解决第一优先把节假日做成特征列加进输入比如节假日当天、节前 N 天、节后 N 天的标志位第二用异常值检测把极端日标记出来评估时分别统计正常日和异常日的误差避免指标被极端日拖垮第三是调整业务预期异常日的预测只能给区间不能给精确值。5.4 多品种混训还是单品种建模现象把菠菜、番茄、土豆十几个品种放进同一个模型训练整体 MAE 看起来还可以分品种一拆部分品类误差极大而另一些品类则非常准。原因不同品种的价格量级和波动模式差别巨大。土豆价格常年在一到三元之间波动菠菜可以在一天内从两元涨到八元。共享一套参数会让 LSTM 花大量容量去协调不同量级和不同波动频率之间的冲突结果两头不讨好。解决按品种分组建模。主力品种单独建模型长尾品种按价格量级和波动率聚类后合并建模。我在业务里的习惯是出货量前五的品种必须单独建模其余品种每 3 到 5 个一组共享模型这样整体误差可控模型数量也不至于爆炸。5.5 训练集乱序时序信息被打散现象训练损失曲线很平滑但预测结果看起来像白噪声预测曲线贴着均值线走MAE 逼近直接用历史均值预测的水平。原因fit 时没有设置 shuffleFalse或者自己写数据生成器时手动打乱了样本顺序。LSTM 的时序信息完全依赖样本之间的先后关系打乱后模型只能学到价格的均值水平学不到任何动态模式。解决模型训练时设置 shuffleFalse。如果你用 DataLoader 自己生成批次确保批次内和批次间的样本顺序都是原始序列顺序。这条写起来简单但翻车概率极高建议把「shuffleFalse」写进项目说明的注意事项第一条。6. 进阶用滚动预测和 walk-forward 验证把模型做扎实前面的流程解决的是「单步预测」——用过去 7 天预测明天。业务上往往要预测未来 3 到 7 天的走势验证方式也不能只依赖一次固定的训练测试切分。这一节给两个进阶做法。6.1 滚动预测多步预测的正确姿势需要预测未来多天时常见的错误做法是把上一步的预测值当作真实值回填到输入序列里继续预测下一天。误差会逐日累积第三天基本就不可信了。更可靠的做法是滚动预测每预测一天后把新预测值拼进窗口丢掉最旧的一天再预测下一天。def rolling_predict(model, last_window, scaler, n_steps): preds [] seq last_window.copy().reshape(1, -1, 1) for _ in range(n_steps): pred model.predict(seq, verbose0)[0, 0] preds.append(pred) # 窗口左移一位把新预测值拼到末尾 seq np.roll(seq, -1, axis1) seq[0, -1, 0] pred return scaler.inverse_transform(np.array(preds).reshape(-1, 1))np.roll 把整个序列向左平移一位最旧的值被移出窗口然后我们把新预测写入最后一个位置。这样窗口长度始终保持 lookback 不变。需要提醒的是多步预测越往后越不可靠第 3 天的预测值要当作趋势参考而不是精确报价。滚动预测的价值在于给采购一个「未来几天价格走势」的区间判断而不是替代人工询价。6.2 walk-forward validation比固定切分更可靠的验证方法固定把前 80% 当训练集、后 20% 当测试集的做法有一个问题只验证了一次结论方差大换一组数据表现就不同。walk-forward validation 的做法是每次用最近一段窗口的数据训练预测下一个时间点然后窗口整体向后滑动重复这个过程把所有预测结果汇总成一个误差分布。window 180 # 每次训练用的历史天数 results [] for i in range(window, len(scaled) - 1): train_data scaled[i - window:i] X, y create_sequences(train_data, lookback) step_model build_model() # 每个窗口重新初始化模型 step_model.fit( X, y, epochs30, batch_size16, shuffleFalse, verbose0, callbacks[EarlyStopping(patience3, restore_best_weightsTrue)] ) # 用最近 lookback 天的数据预测第 i 天 seq_in scaled[i - lookback:i].reshape(1, lookback, 1) pred step_model.predict(seq_in, verbose0)[0, 0] results.append(pred) results scaler.inverse_transform(np.array(results).reshape(-1, 1))这个写法在真实场景里比固定切分可靠得多因为它模拟了模型上线后的使用方式每次只用截至当前时刻的数据训练预测未来一天然后更新训练集。这样得到的误差分布就是模型未来真实表现的近似估计不会出现固定切分里「运气好切到平稳段」导致评估虚高的情况。我现在拿到一个新的菜价数据集第一件事不是跑模型而是先画时间序列图、切一刀 walk-forward 验证、把节假日标记加上然后才碰模型结构。这个习惯帮我省掉了大量「模型改来改去但误差纹丝不动」的无效调试时间。菜价预测的核心不在模型多复杂而在数据和验证流程是否扎实希望帮到你。本文还有配套的精品资源点击获取