深度学习LSTM蔬菜价格预测:从爬虫到Web展示的完整工程实现

发布时间:2026/10/3 14:24:46
深度学习LSTM蔬菜价格预测:从爬虫到Web展示的完整工程实现
简介面向蔬菜价格预测与时间序列建模学习者的完整Python项目采用LSTM深度学习模型以爬虫从公开网站抓取菜心、西红柿等多种蔬菜价格及天气数据经pandas清洗后完成时间序列预测并与sklearn、statsmodels等传统方法对比优劣预测结果通过Flask搭建Web页面同时结合微信公众号提供查询入口适合有Python基础、想系统实战数据采集、建模与部署的开发者。压缩包共181个文件、约1.66MB以142个csv蔬菜价格与天气数据集、25个py源码文件为核心辅以pyc、docx和md说明文档内有项目说明、环境依赖与复现步骤py脚本覆盖爬虫、数据处理、模型训练、结果可视化和Web展示等环节csv数据可直接用于复现实验目录结构清晰方便按模块对照学习。项目已有247人学习浏览可作为课程设计、毕业设计或入门深度学习的完整实战案例。1. 基于深度学习的LSTM蔬菜价格预测这个python源码包到底能做什么标题写着“深度学习LSTM蔬菜价格预测”拆开看这是一套从数据采集到Web展示的完整闭环四个环节用不同技术栈串起来爬虫负责从网站上抓蔬菜价格和天气数据pandas做清洗和时序对齐LSTM做预测Flask把结果展示成网页。数据集里包含本地菜心、云南小瓜、小塘白菜、本地芹菜、矮脚白菜、青皮冬瓜、西红柿、红尖椒这10个品类的历史价格csv文件。这个项目对农贸市场的价格波动分析和电商采购定价有实际参考价值也能让想入门LSTM时间序列的人看到一条完整的工程链路。适合两类人——一类是正在做课设、需要能跑通并讲出原理的在校生另一类是已经在用传统统计模型做销量预测、想试试LSTM但又不想从零搭爬虫和数据管道的从业者。2. 数据管道搭建从爬虫抓取到可用的时间序列2.1 爬虫设计价格和天气必须一起抓不能只抓价格蔬菜价格预测的核心难点在于价格波动受外部因素影响大同一品种在雨天和晴天的批发价能差出百分之二三十。所以抓数据时不能只抓价格字段要把天气、节假日这些可能影响菜价的变量一起存下来。这个项目用的爬虫框架是scrapy配合beautifulsoup做页面解析数据落地到MongoDB。常见做法是给每个蔬菜品类建一个单独的爬虫用价格公告页面里带日期标识的分页URL做增量抓取。import scrapy from bs4 import BeautifulSoup from pymongo import MongoClient class VegPriceSpider(scrapy.Spider): name veg_price # 按蔬菜品类分别定义入口URL start_urls [ http://example-market.com/price/本地菜心/, http://example-market.com/price/西红柿/, ] def parse(self, response): # 解析价格表格提取日期、品名、价格、单位 soup BeautifulSoup(response.text, html.parser) rows soup.select(table.price-table tr) client MongoClient(localhost, 27017) db client.veg_price_db for row in rows[1:]: # 跳过表头 tds row.select(td) if len(tds) 3: continue item { date: tds[0].text.strip(), name: tds[1].text.strip(), price: float(tds[2].text.strip()), unit: tds[3].text.strip(), } # 如果当天该品类已有记录做更新而不是追加 db.price_data.update_one( {date: item[date], name: item[name]}, {$set: item}, upsertTrue ) yield item这段代码的逻辑是逐行解析价格表用date加name做联合主键去重。关键点在upsert那段——很多爬虫项目翻车就翻在重复抓取同一天的行情导致数据里出现两行完全相同但日期一样的数据后面做时间序列时索引重排会直接报错。爬虫层面还应该注意分页深度和抓取频率一般设置每页延时一到两秒避免被目标站点封IP。天气数据的爬取方式是一样的但要额外增加一个城市字段因为同一个市场里不同产地的蔬菜受不同地区天气影响比如本地芹菜受本地降雨影响大而青皮冬瓜如果是从外省调运的就得看产地天气。2.2 数据清洗与对齐日期索引、缺失值和重复文件的处理爬虫跑完拿到的原始数据通常包含三类脏数据日期格式不统一、同一日期重复条目、价格字段出现明显异常值。这个项目里的清洗工作集中在pandas里完成核心流程是标准化日期、按品类切片、去重去空、按日重采样对齐。import pandas as pd def clean_veg_data(csv_path, veg_name): # 读取原始csv实际爬取结果可能含有多余表头 df pd.read_csv(csv_path, skiprows1) # 统一日期格式并设为索引 df[date] pd.to_datetime(df[date], errorscoerce) df df.dropna(subset[date, price]) df df.sort_values(date) # 去除同一天内的重复记录保留最后一次更新的价格 df df.drop_duplicates(subset[date], keeplast) # 设置日期索引并按天重采样 df df.set_index(date) df df.resample(D).asfreq() # 连续缺失超过2天的用前后向填充补上 df[price] df[price].interpolate(limit2).ffill().bfill() # 清洗明显异常值价格大于0且小于50的才保留 df df[(df[price] 0) (df[price] 50)] df[name] veg_name return df.reset_index()参数说明resample(D)表示按自然日重采样中间断档的日期会变成NaNinterpolate(limit2)是线性插值最多连续补两天。这个限制很重要——如果一个品种连续一周没有价格很可能是季节性下架盲目插值会把“缺货”拟合得像“低价”模型学到的规律是错的。价格上限50元的设定也是主动清洗的一种方式某些菜品的价格波动特别剧烈红尖椒在节假日前后价格翻倍但超过50基本可以断定是录入错误。清洗完的数据结构统一为date、price、name三列这个结构后面喂给LSTM时可直接用pivot_table转成宽表。注意项目压缩包里你可以看到的矮脚白菜.csv出现了重复文件这是打包时粗心导致的。实际使用时以最近修改日期的那份为准两份文件内容如果一致就直接删掉一份如果不同说明打包者把两个阶段的清洗结果混在包里了建议用上面的代码重新清洗一遍不要直接合并读取。3. LSTM核心实现时序重构、归一化与模型训练3.1 把价格序列转成监督学习样本look_back窗口怎么选LSTM不能直接吃一维价格序列它需要把任务转成“用前N天预测第N1天”的监督学习格式。look_back窗口就是N的大小。蔬菜价格有明确的周效应——周末和周一的价格波动规律不同所以窗口一般选7的倍数。这个项目里默认用7天窗口预测第8天价格。import numpy as np from pandas import DataFrame def series_to_supervised(data, n_in7, n_out1): 将一维价格序列转为监督学习样本 data: 单列价格序列numpy array 或 pandas Series n_in: 输入窗口长度即用多少天的历史数据 n_out: 输出步长预测未来多少天 df DataFrame(data) cols, names [], [] # 构造输入特征列 for i in range(n_in, 0, -1): cols.append(df.shift(i)) names.append(ft-{i}) # 构造预测目标列 for i in range(0, n_out): cols.append(df.shift(-i)) names.append(ft{i1}) # 拼接为宽表 agg pd.concat(cols, axis1) agg.columns names # 删除包含NaN的行 agg.dropna(inplaceTrue) return agg这段代码的核心是shift函数的正负方向。shift正数取前面的历史值shift负数取未来的目标值。窗口大小直接影响训练样本数量——n_in7时一个500天的序列能产生493个样本n_in30时只能产生470个样本样本量缩小但每个样本的上下文更长。这个项目里的数据量级在500到1000天之间7到14天窗口是稳妥区间。窗口设小了模型抓不住周规律设大了训练集太少容易过拟合。3.2 MinMaxScaler归一化为什么训练集和测试集必须分开fit归一化是LSTM项目里最容易被忽视但影响最大的一步。常见误区是对全量数据做一次MinMaxScaler再用归一化后的数据拆分训练集、测试集这对菜价预测来说会造成信息泄漏。测试集在归一化时已经知道了全局最小值和最大值模型看到的测试分布被压缩RMSE会虚低百分之二十到三十。from sklearn.preprocessing import MinMaxScaler def normalize_data(data): # 70/30切分先切分再归一化 train_size int(len(data) * 0.7) train_raw, test_raw data[:train_size], data[train_size:] scaler_train MinMaxScaler(feature_range(0, 1)) train_scaled scaler_train.fit_transform(train_raw.reshape(-1, 1)) scaler_test MinMaxScaler(feature_range(0, 1)) test_scaled scaler_test.fit_transform(test_raw.reshape(-1, 1)) return train_scaled, test_scaled, scaler_train, scaler_test这段代码分两次做归一化训练集用fit_transform测试集只用transform——不能用测试集的信息去计算最小值最大值。但这里有个现实问题如果测试集的价格区间远高于历史区间或者某天突然暴涨超过训练集最大值transform后该点会大于1模型预测结果失真。在菜价场景里这种情况确实会发生比如台风天叶菜价格翻了倍。我一般会在归一化时给最大值加一个松弛系数比如max_val * 1.2作为归一化的上限给尾部波动留空间代价是整体分辨率稍微下降但预测稳定性明显提升。3.3 模型搭建与训练units、epochs、batch_size怎么定LSTM模型本身搭起来并不复杂Keras里几行代码的事真正决定预测质量的是超参数和训练策略。这个项目的核心训练代码是标准的单层LSTM加全连接输出。from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense from tensorflow.keras.callbacks import EarlyStopping def build_lstm_model(n_steps, n_features1, units50, lr0.001): model Sequential() # 单层LSTMunits50是隐藏单元数量return_sequencesFalse model.add(LSTM(units, activationtanh, input_shape(n_steps, n_features))) model.add(Dense(1)) model.compile(optimizeradam, lossmse, metrics[mae]) return model # 构建训练数据: [样本数, 时间步, 特征数] X_train train_supervised.iloc[:, :7].values.reshape((-1, 7, 1)) y_train train_supervised.iloc[:, 7:].values model build_lstm_model(n_steps7, units50) early_stop EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue) history model.fit( X_train, y_train, epochs100, batch_size16, validation_split0.2, callbacks[early_stop], verbose1 )参数说明units50是LSTM隐藏单元数菜价序列特征维度低、数据量不大50个单元足够epochs设100但配合EarlyStoppingpatience10表示验证损失连续10轮不下降就提前停止避免过拟合batch_size16是因为价格序列有周规律一个batch里最好包含至少两组完整的周样本。输入格式里reshape的三维参数依次是样本数、时间步长、特征数。时间步是7特征数是1——只有价格。如果要加入天气特征特征数变成2或3但时间步不变这要求天气数据必须按日和价格对齐在同一条记录里。训练完成后要保存模型权重方便Web端调用时直接加载。model.save(lstm_veg_price.h5)4. 多模型对比验证ARIMA、线性回归与LSTM哪个更靠谱4.1 传统基线模型ARIMA和线性回归的实现说一个模型好必须有对照。LSTM项目里最常用的基线模型是statsmodels库的ARIMA和sklearn的线性回归。ARIMA适合捕捉线性自相关线性回归适合看趋势两者都跑一遍能和LSTM形成明显对照。from statsmodels.tsa.arima.model import ARIMA import warnings warnings.filterwarnings(ignore) def arima_predict(train, test): model ARIMA(train, order(5, 1, 0)) fitted model.fit() # 预测长度对齐测试集 pred fitted.forecast(stepslen(test)) return pred from sklearn.linear_model import LinearRegression def linreg_predict(train_supervised, test_supervised): # 监督学习格式前7天做特征第8天做目标 X_train train_supervised.iloc[:, :7].values y_train train_supervised.iloc[:, 7].values X_test test_supervised.iloc[:, :7].values lr LinearRegression() lr.fit(X_train, y_train) pred lr.predict(X_test) return predARIMA的order参数(5,1,0)表示自回归项5、差分阶数1、移动平均项0。蔬菜价格有很弱的一阶差分平稳性d1是合理的。线性回归部分用的是监督学习格式和LSTM吃的东西完全一样只是没有时间步权重把7天价格直接当7个特征。这种统一输入的好处是最后对比结果时差异完全来自模型结构本身。4.2 评估指标与结果解读RMSE在蔬菜价格里意味着什么from sklearn.metrics import mean_squared_error, mean_absolute_error def evaluate(true, pred): rmse np.sqrt(mean_squared_error(true, pred)) mae mean_absolute_error(true, pred) return rmse, mae跑完三个模型典型的结果会是这样一张表不同品种数据有差异但数量级一致模型RMSE元/斤MAE元/斤训练时长ARIMA0.420.332秒线性回归0.380.293秒LSTM0.310.2445秒RMSE是均方根误差MAE是平均绝对误差。在蔬菜价格这个场景里0.3元/斤的误差意味着预测零售价时已经能区分“便宜”和“偏贵”但离精准报价还差得远。LSTM在误差上比ARIMA低四分之一左右但训练时间多了二十多倍。实际使用时如果预测的是批发市场整体行情ARIMA够用如果针对单品做零售端定价LSTM的精度优势才划算。有一个观察是LSTM在价格尖峰附近的响应速度明显优于ARIMA——比如西红柿在暴雨天气前后价格连续两天跳涨ARIMA会滞后一天才跟上LSTM通常能提前半步。5. 避坑指南LSTM跑菜价预测最容易翻车的5个地方5.1 归一化泄漏验证集误差小得离谱现象测试集RMSE只有0.1元/斤比训练集还低画图看预测曲线和真实曲线完全重合好得不正常。原因归一化时对全量数据做了fit测试集的最小最大值已经被吸纳进scaler里相当于模型提前知道了测试集的价格区间。解决把切分放在归一化之前训练集单独fit_transform测试集只transform。加了这一步之后RMSE可能会从0.1涨到0.3别慌这才是真实水平。5.2 预测曲线整体滞后一天现象预测价格的走势完全正确但整条曲线比真实价格晚一天拐点全部错位RMSE却不高。原因LSTM用的是“前7天预测第8天”的样本结构如果爬虫数据当天的价格是晚市收盘价那么用t-1到t-7预测的其实是第t天的收盘价而不是当天的凌晨价格时间语义错位了。解决检查爬虫时间字段的定义。如果是下午4点抓取当天价格那么预测目标应该定义为“第t1天的价格”。把shift(-1)改成shift(0)或者调整爬虫抓取时间点让预测目标和训练特征的时间边界一致。5.3 多个csv合并时索引错乱现象把10种蔬菜的csv读进来做多头预测结果训练时报错“shape不匹配”检查数据发现有的一百行有的三百行日期窗口完全对不上。原因打包者导出的csv日期范围不一致本地芹菜可能只记录了三个月西红柿记录了一年直接用concat会得到大量NaN。解决合并前先检查每个文件的min(date)和max(date)只保留公共时间范围内的数据或者把缺数据的品种直接剔除。这个项目里矮脚白菜.csv重复出现也是打包粗心的一个信号务必先做文件完整性校验。5.4 加了天气特征反而掉点现象把降雨量和温度作为第二、第三个特征喂进LSTMRMSE不降反升。原因天气特征没有对齐。天气数据是按天的但爬虫抓到的价格数据里有节假日的非交易日记录天气和价格的索引不完全重合pandas在merge时产生了错位样本。解决用“日期左连接”把天气对齐到价格上缺失天气的日期直接删除整行不要用0填充。另外天气影响菜价通常有1到3天滞后期把天气做shift(1)、shift(2)后再作为特征输入效果比当天天气不挪更好。5.5 Arctic训练时loss出现NaN现象训练进行到十几轮的时候loss变成NaN无法继续。原因价格序列里有极端值没有被清洗干净。前文清洗代码里设了0到50的过滤但如果某个csv里有一个价格是“--”或者“None_”pandas读入时变成NaN归一化时报错。还有一种情况是价格出现了负值部分市场报价单里会用负数表示退款。解决清洗时加一行条件过滤把所有非正数直接drop同时检查csv文件是否含有多余表头。我遇到过文件里有两行表头的情况Python读进来第一行变成了数据第二行才是字段名直接影响后续所有操作。6. 把预测结果真正用起来Flask接口与多段式验证技巧模型训练好只是第一步这个项目的Web显示部分用Flask实现把训练好的模型封装成HTTP接口前端通过接口拿预测结果。Flask部分的代码不复杂但有几个小细节值得注意。from flask import Flask, jsonify, request from tensorflow.keras.models import load_model import numpy as np import pandas as pd app Flask(__name__) model load_model(lstm_veg_price.h5) app.route(/predict, methods[POST]) def predict(): data request.get_json() veg_name data[veg_name] recent_prices data[recent_prices] # 最近7天价格列表 # 输入校验必须7个数值 if len(recent_prices) ! 7: return jsonify({error: 需要提供7天价格数据}), 400 # 转成LSTM输入格式1,7,1 x_input np.array(recent_prices, dtypefloat).reshape(1, 7, 1) pred model.predict(x_input)[0][0] return jsonify({ veg_name: veg_name, pred_price: round(float(pred), 2) }) if __name__ __main__: app.run(host0.0.0.0, port5000)这段接口代码有两个实战细节一是输入长度严格校验前端必须传7个价格少了返回400错误不让脏数据进入模型二是pred_price做round处理菜价显示到小数点后两位就够了。配合微信公众号展示时用户输入蔬菜名称和最近几天价格后端返回预测价这个接口可以直接复用。验证环节我一般会做一个强制流程训练完成后必须画一张“真实价格、训练预测、测试预测”三条线的对比图人工肉眼检查最后一个月的预测曲线。重点看的不是误差值而是拐点位置——真实价格在哪个日期开始涨预测曲线是不是也在附近时间开始涨。如果LSTM预测在拐点上普遍滞后超过两天说明窗口或者特征需要重新调。这个项目的实际价值在于整套流程能跑通从爬虫到预测到展示一条线。菜价预测的终极目标是给批发商找采购窗口——提前一到两天预判叶菜价格涨跌比事后解释规律有用得多。从那以后我每次拿到新的菜品数据都强制走一遍“重复文件检查、日期对齐、趋势图可视化”这三件事再决定要不要跑LSTM数据不对模型再花哨也是白费。希望帮到你。本文还有配套的精品资源点击获取