ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

农产品价格数据分析与可视化系统:Python全链路实践

农产品价格数据分析与可视化系统:Python全链路实践 如果你经常跑菜市场或者习惯手机买菜应该会对价格的波动特别敏感——菠菜今天的报价可能还是5块钱一把过两天突然蹿到8块同一片区域的土豆不同摊位间价格差出一块钱也不稀奇。价格波动背后是产地、天气、运输成本、批发商库存等各种因素搅在一起。但普通消费者想看到的其实是“这个东西现在多少钱、最近涨了还是跌了、大概会涨到什么程度”而这类信息恰恰分散在各个批发市场网站、农业信息平台里格式不一、单位不同、更新频率也乱。我今天分享的就是一个基于Python的农产品价格数据分析与可视化系统。它负责把分散的农产品价格数据抓下来清洗成统一口径的数据表再做价格走势、涨跌排名、波动统计等分析最后通过静态图表和网页可视化大屏把结果展示出来。这个项目不算大但把数据分析最常见的链路——采集、清洗、存储、分析、展示——完整地走了一遍。如果你正在学Python数据分析想找一个能写在简历上的实操项目或者你是做农业、供应链相关工作的想自己搭一套价格监控看板那这套系统的思路可以直接借鉴。1. 项目概述与需求拆解1.1 农产品价格数据的“散”和“乱”到底有多严重农产品价格数据最大的问题不是拿不到而是拿到了不知道怎么用。国内有大量农业信息平台每天在发布批发市场的价格行情但每个网站的发布格式完全不一样。有的用表格列出品名、批发价、单位、市场名称有的直接是一段纯文本夹杂着“西红柿 3.5/公斤”这样的碎片甚至同一个省份的不同市场计价单位都可能在“斤”“公斤”“吨”之间反复横跳。数据乱还不只是格式问题。品名同样不统一这边写“西红柿”那边写“番茄”还有人写“大红番茄”产地信息有时有、有时没有价格后面的单位有时是“元/斤”有时写“元/kg”也不标注清楚。这些对人工浏览来说不算大事但对程序分析来说每一个不一致都是麻烦。更关键的是时间维度有的平台每天更新有的平台周末就停更数据缺失和口径错位几乎是家常便饭。我当初搭这个系统核心出发点就是为了解决这种“散、乱、缺”的问题。通过爬虫将多个来源统一采集再用Pandas做清洗标准化最后把价格数据按照“菜品日期市场”三个维度组织起来。整个数据链路跑通之后分析才真正有地基。否则前面数据是脏的后面画出来的图表再好看也是在错误结论上盖高楼。1.2 系统的实际使用场景和最适合的人群这个系统能做的事说起来也简单定期抓取农产品价格自动生成三类结果——价格趋势折线图、涨跌榜、价格分布统计。如果你把爬虫做成定时任务挂在服务器上跑甚至能每天往Telegram、企业微信或者本地数据库里推一份行情摘要。它最适合三类人。第一类是正在学Python数据分析的初学者需要一个覆盖完整链条、不是只调调库就能交差的练习项目这个系统能逼你处理真实世界里的脏数据第二类是做农业信息化相关工作的朋友比如批发市场的信息员、农产品电商平台的选品运营他们需要快速看到哪些品类在涨价、最近供应端有什么异动第三类是想做数据可视化作品集的人用同一个数据集折腾出多张图表和一个展示页面比拿着网上下载的干净数据做十几个demo更有说服力。在这个项目里我不会用特别复杂的算法也不会上一套重量级框架核心就是用Python生态里的Requests、Pandas、Flask、ECharts把一个实际问题的闭环跑通。下面我会按数据流顺序把每一步的设计思路和代码细节都拆开讲。2. 技术选型与整体架构设计2.1 为什么最终选了Python这一套技术栈刚开始规划的时候我也考虑过直接用Excel或者商业BI工具来做这个分析。Excel天然适合做小规模的数据透视Power BI和Tableau在可视化交互上也很成熟理论上不需要写代码把数据下载下来拖拽几步就能出图。但它们的共同问题是整个流程里最关键的数据采集环节光靠手动下载根本撑不住长期自动化的需求。你不可能每天打开七八个网站把数据一个个复制粘贴到Excel里再手动刷新透视表。Python把采集、清洗、分析、可视化这四个环节统一到了一个生态里。爬虫用Requests加BeautifulSoup解析整个网页的表格数据大概二十行代码数据清洗交给Pandas处理缺失值、统一单位、按日期排序这些操作都有现成方法可视化端有Matplotlib和Seaborn出静态图表再往Web上走Flask配ECharts做交互式大屏也毫不费力。整个项目只需要一套开发环境就能跑通不需要在两个工具之间来回切换、手动搬运数据。另外一个原因是社区生态成熟。农产品价格分析虽然是一个比较垂直的领域但核心操作方法——处理时序数据、计算环比同比、做滚动均值、聚类分组——这些都有大量现成的代码和踩坑经验可参考。路上遇到问题搜索Python数据分析、可视化相关的资料大概率能找到解决方案这对一个需要长期维护的小系统来说很重要。2.2 数据从采集到展示的三层流转结构整个系统按数据流可以切分成三层采集层、分析层、展示层。我用表格把这层结构梳理一下每个层的任务边界清楚之后代码写起来就不容易乱。层级核心任务用到的库/工具产出物采集层定时抓取多个行情页面解析表格文本Requests、BeautifulSoup原始CSV文件分析层数据清洗、单位统一、指标计算Pandas、NumPy标准化数据库展示层生成静态图表、提供API数据、渲染交互页面Matplotlib、Flask、ECharts图表与可视化大屏采集层追求的是“不阻塞、能容错”。每个网站请求之间要控制频率捕抓失败要能跳过不能因为某个页面短暂超时就把整套流程卡死。这一层产生的原始数据我建议先落成CSV存档保留采集原貌尽量不要在采集阶段就做太激进的清洗。分析层是整个系统的核心也是工作量和坑最多的地方。它负责把不同来源的单位统一成“元/斤”把“西红柿”和“番茄”归并成同一条品名记录再处理缺失日期、重复采集等问题。清洗完成后按“日期、品名、价格”的明细表结构存入SQLite供后续查询。展示层则是把分析结果翻译成人眼更容易看懂的形态。静态图表适合放在周报、PPT里动态大屏适合放在办公室、直播间侧边。两层可以共用同一种标准化的数据接口分析层算好指标展示层只负责取数、绘图。这种三层结构的好处是每一层都能独立测试、独立重跑。采集出了问题不用动分析逻辑展示样式要改不影响底层数据。对个人项目来说这意味着维护成本断崖式下降。3. 数据采集与清洗的实操细节3.1 从行情网页中抓取表格数据一段可复用代码采集环节我选的目标是公开的农产品批发市场行情页面这类网站的结构相对简单价格信息基本都在一个HTML表格里很适合初学者拿来练习。写爬虫之前建议先打开目标网页用浏览器开发者工具确认一下表格的HTML结构看看数据是静态渲染的还是Ajax动态加载的。绝大多数农业信息平台的行情页是静态表格用Requests拿到HTML再用BeautifulSoup定位就可以。下面这段代码展示了最基本的页面抓取和表格解析逻辑import requests from bs4 import BeautifulSoup import time HEADERS { User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 } def fetch_market_prices(url): resp requests.get(url, headersHEADERS, timeout10) resp.encoding resp.apparent_encoding soup BeautifulSoup(resp.text, html.parser) rows [] for tr in soup.select(table tr): cells [td.get_text(stripTrue) for td in tr.find_all(td)] if len(cells) 4: rows.append(cells) return rows if __name__ __main__: url https://example-market.example/price data fetch_market_prices(url) for row in data[:5]: print(row) time.sleep(2)有几个细节值得提醒。resp.encoding那一段不能省很多政府信息平台用的是GBK或GB2312编码Requests默认根据响应头猜测编码猜错的话解析出来就是乱码。用resp.apparent_encoding可以根据页面内容重新判断准确率会高很多。另外time.sleep(2)不是走形式对目标网站保持礼貌的请求频率既是合规的做法也是防止IP被临时限制的实操手段。采集到的原始数据里字段大概会有“品名、市场、单位、批发价、日期”这几列。不同页面顺序可能还不一样所以不建议在爬虫里硬编码列位置而是保存成原始CSV后在分析阶段用Pandas统一重命名列名。3.2 清洗和标准化把脏数据变成能分析的样子采集完成后数据分析过程中最耗时的一步才刚刚开始。原始数据里常见几类问题品名大小写混用、单位不统一、价格字段偶发缺失、同一菜品在同一个市场同一天可能采集了两条记录。这些问题不解决后面算出来的涨跌幅没有任何意义。我的清洗流程分四步走。第一步去除空行和纯重复记录第二步做品名和单位的标准化第三步用统一口径计算标准化价格第四步处理缺失值。下面给出核心代码import pandas as pd import numpy as np df pd.read_csv(price_raw.csv) df.columns [name, market, unit, price, date] # 1. 去重同一市场、同一品名、同一天只需要一条记录 df df.drop_duplicates(subset[name, market, date]) # 2. 品名标准化去空格、转小写、常见别名映射 name_map { 西红柿: 番茄, 小番茄: 圣女果, 土豆: 马铃薯, } df[name] df[name].str.strip().str.lower() df[name] df[name].replace(name_map) # 3. 单位统一所有价格折算为“元/斤” unit_factor {斤: 1.0, 公斤: 0.5, kg: 0.5, 千克: 0.5, 吨: 1 / 2000} df[factor] df[unit].map(unit_factor) df[price_std] df[price] * df[factor] # 4. 对缺失价格先做向前填充再取同菜品的均价兜底 df[price_std] df.groupby(name)[price_std].transform( lambda x: x.ffill().bfill() ) df df.dropna(subset[price_std])注意步骤4的缺失值处理不是万能的。如果一个菜品连续一周没数据前面的记录也被丢光了那向前填充出来的价格其实是“虚构”的会让趋势线失真。所以我会在后面做一层兜底如果某个菜品在近7天内有效数据少于3天就把它从趋势分析中临时剔除不画图。这是很多人会忽略的细节。清洗完成后的数据建议转成长表格式——每条记录一个日期、一个品名、一个市场、一个价格。这种结构对Pandas的分组聚合最友好也方便后续直接写入数据库。4. 数据存储与价格指标的底层逻辑4.1 SQLite和CSV到底选哪种存储方案初期数据量不大的时候很多人会把清洗后的结果继续存成CSV好处是直观、方便打开核对Excel直接就能看。但跑了一段时间你就会发现随着日期累积每天抓几百行一个月上万行CSV的麻烦开始冒头每次分析都要重复读整个文件想按日期切片不够灵活多个菜品同时筛选时代码也变得啰嗦。我的建议是原始数据留CSV做备份清洗后的明细数据存SQLite。SQLite是单文件数据库不需要安装服务器Python原生支持sqlite3模块非常适合这个量级的数据分析项目。用SQL查询“某菜品的最近30天均价”这类需求一行SQL比在Pandas里反复切片要清晰得多。建表和写入的代码很简单import sqlite3 conn sqlite3.connect(agri_prices.db) df.to_sql(price_daily, conn, if_existsappend, indexFalse) # 建立索引按品名和日期查询会快很多 conn.execute( CREATE INDEX IF NOT EXISTS idx_name_date ON price_daily(name, date) ) conn.commit() conn.close()关键一步是给name和date建立联合索引。虽然系统数据量不大但每次可视化请求都要按品名过滤、按日期排序没有索引的话数据多了以后接口响应会肉眼可见地变慢。索引在个人项目里是性价比最高的优化手段。4.2 价格分析里最有用的四个指标数据进库之后就进入分析层。农产品价格分析不一定要做机器学习预测先把这些基础指标算扎实远比糊一个玄学预测模型靠谱。第一个是周环比变化率。(今日价格 - 7日前价格) / 7日前价格它比日环比更能反映短期趋势抹平了单日波动。计算时用Pandas的shift函数就可以df df.sort_values([name, date]) df[prev_price] df.groupby(name)[price_std].shift(7) df[week_change] (df[price_std] - df[prev_price]) / df[prev_price]第二个是7日滚动均线。这一招在处理农产品价格时特别有效。单日价格受供需情绪影响很大走势图会像锯齿一样上下跳加上7日均线之后你才能真正看到“这东西最近整体是涨还是跌”。实现用的是rolling方法Pandas里一行解决。第三个是价格分位数和标准差。农产品价格有明显的季节性西红柿2块钱的时候可能已经处于近期低位4块钱的时候可能就是高位。通过计算近30天价格的分位数可以把当前价格映射成“低于25%分位”“高于75%分位”直接判断当前价格处在什么水平。这个比单纯看绝对值更有参考价值。第四个是价格离散度。同一菜品同一天在不同市场的价格差异反映了各地供需差异和信息不对称的程度。如果看到某个菜品在不同市场的报价差超过20%那可能意味着产地集中、运输成本高或者市场之间流通不畅这也是分析里很有价值的信息。这些指标计算看起来简单但它们组合起来就能支撑起一个像样的价格监控系统。比如自动扫描全品类找出周环比涨幅最大的前十个菜品生成“涨价榜”或者找出价格波动率最高的菜品提示关注背后核心用的就是这些基础指标。5. 可视化方案与展示页面实现5.1 用Matplotlib和Seaborn出一组可读性强的静态图表数据分析结果最终要靠图表来呈现。我个人会把图表分成基础款和进阶款两种思路。基础款是价格走势折线图加7日均线、涨跌幅度柱状图进阶款是用热力图看不同菜品在不同时间段的价格水平用箱线图看价格分布。用Matplotlib画折线图有一个必须提前处理的坑中文乱码。默认字体不支持中文出现的就是一堆方框。解决办法是显式指定中文字体代码里加一行就行import matplotlib matplotlib.rc(font, familyMicrosoft YaHei) matplotlib.rc(axes, unicode_minusFalse)第一行指定字体为微软雅黑第二行是避免坐标轴负号显示成方块。Mac和Linux环境请换成系统自带的中文字体比如“PingFang SC”或者“Noto Sans CJK SC”。下面是一段画菠菜价格走势的完整代码同时绘制了原始价格和7日均线import matplotlib.pyplot as plt import pandas as pd def plot_price_trend(df, item_name): item df[df[name] item_name].sort_values(date) item item.set_index(date) item[ma7] item[price_std].rolling(7).mean() plt.figure(figsize(12, 6)) plt.plot(item.index, item[price_std], label当日价格, linewidth1.2) plt.plot(item.index, item[ma7], label7日均线, linewidth2) plt.title(f{item_name} 价格走势) plt.xlabel(日期) plt.ylabel(价格元/斤) plt.legend() plt.xticks(rotation45) plt.tight_layout() plt.savefig(f{item_name}_trend.png, dpi150) plt.show()画完之后会发现原始价格线毛刺很多但均线能平滑地反映走势。当两根线交叉、均线开始掉头向下的时候通常就是一个中期趋势的转折信号。这个规律放到农产品上同样适用只是周期性比股票更强、更规律。除了折线图我推荐用热力图看“不同菜品在不同周的价格水平”。做法是先把日期归一化成“周”然后按每个菜品的周均价做透视表再用Seaborn画热力图。这种图放在周报里非常有信息量一眼就能看出哪些菜在哪些周处于高位。5.2 用ECharts把分析结果变成可视化大屏静态图适合打印和贴文档但如果要给同事、客户展示一个能交互的网页大屏会更有冲击力。我这里用的是Flask加ECharts的组合。Flask负责提供数据API前端ECharts负责画图。整个链路不复杂但有一种“自己搭了个数据产品”的实感。后端代码里先准备一个接口从SQLite读数据并转成JSONfrom flask import Flask, jsonify, render_template import sqlite3 import pandas as pd app Flask(__name__) def query_price(item_name, days30): conn sqlite3.connect(agri_prices.db) sql SELECT date, name, price_std FROM price_daily WHERE name ? ORDER BY date DESC LIMIT ? df pd.read_sql_query(sql, conn, params(item_name, days)) conn.close() return df.sort_values(date) app.route(/api/price) def api_price(): df query_price(菠菜) return jsonify({ dates: df[date].astype(str).tolist(), prices: df[price_std].tolist() }) app.route(/) def index(): return render_template(index.html)前端页面里通过Fetch请求这个接口把拿到的数据填进ECharts的折线图配置里fetch(/api/price) .then(res res.json()) .then(data { const chart echarts.init(document.getElementById(chart)); chart.setOption({ tooltip: { trigger: axis }, xAxis: { type: category, data: data.dates }, yAxis: { type: value, name: 元/斤 }, series: [{ name: 菠菜价格, type: line, data: data.prices, smooth: true }] }); });这样一个最简单的可视化页面就跑起来了。如果想做大屏思路是完全一致的——不同的图表区域配置不同的ECharts实例每个实例从不同的API接口取数。比如左上角放全国主要市场均价对比右上角放涨价榜底部放一个价格热力日历图。数据时统一走API前端只需要关心渲染。做这个环节时我最强烈的感受是前面所有清洗和存储的功夫都是为这一层服务的。如果数据结构设计得稀烂写前端接口的时候会痛苦到怀疑人生反过来只要存储层规整写一套JSON接口通常一两个小时就能搞定。6. 常见问题与排查技巧实录6.1 数据口径不一致最容易颠覆结论的问题做数据清洗的过程中我踩过最大的坑不是代码报错而是“数据都对但结论错了”。第一次分析时我直接用爬虫抓下来的price字段算涨幅没注意有的市场报价是“元/公斤”有的市场直接报“元/斤”两类数据混在一起画图画出来一个荒谬的曲线——西红柿价格直接翻倍因为单位从公斤变成了斤。从那之后我在清洗流程里强制加了单位映射表并且会做一轮校验检查同一个菜品同一天的价差范围如果不同市场之间的比值超过2倍大概率是单位或者录入口径出了问题。这个校验逻辑帮我在后续数据中抓出了好几个不规范报价。品名映射也是一个长期维护的活。可以把西红柿和番茄做归并但还有更多别名比如土豆与马铃薯、青椒与柿子椒、花菜与花椰菜。建议在项目开始就准备一张别名映射表后续遇到新别名随时补充。不要指望一条正则能把所有问题解决现实中就是靠一张表不断维护。6.2 数据缺失与时间对齐三个避免曲线断崖的技巧农产品价格数据最讨厌的缺失模式是节假日和周末。很多市场周末不更新一旦你按自然日画日频折线图上就会出现一个个缺口。如果你直接用上一日价格填充趋势看起来会平但拉长时间后可能掩盖“周末之后价格跳涨”这个实际现象。我目前的处理策略有三个层面。第一层分析周期拉长到周级别用周均价替代日频价天然规避周末缺失。第二层如果必须保留日频就用ffill加bfill做有限填充同时记录填充标记列画图时单独用虚线标出填充区间。第三层对于连续缺失超过一周的菜品直接在展示层隐藏防止用户被误导。6.3 爬虫被反爬限制与系统性能优化个人项目里最容易被忽视的就是爬虫的合规和礼貌。即使目标网站没有明确封禁每次抓取间隔太短也会给对方服务器造成压力。我在代码里强制加了随机延时每次请求之间等待1到3秒并且在User-Agent里声明了真实的浏览器信息。如果采集任务要对多个市场跑可以做串行加延时单线程足够用了。最有用的排查工具是浏览器的开发者工具网络面板看网页是静态还是动态加载避免浪费时间去解析一堆JavaScript渲染后的空表格。性能方面这个量级的数据用不到分布式、用不到消息队列。我做过的优化就两类一是数据库索引上文已经提到过二是把高频查询结果缓存起来。比如涨价榜这种按天计算的全局指标不用每次打开页面都重算一遍可以在每天采集完成后触发生成一份JSON缓存Flask接口直接读缓存文件响应速度从几百毫秒降到十几毫秒。这个思路在小型数据分析系统里特别实用——少做重复计算比堆机器更有效。最后说一点个人体会。做完这个系统之后我对“数据分析项目”这件事的理解变了很多。以前我也跟着教程跑过Yelp评论情感分析、Kaggle房价预测但数据是现成的、目标是清晰的整个过程更像是在调包。而自己从零做一个农产品价格分析系统你不得不处理数据采集、脏数据、口径不一致、缺失值这类真实世界的磨人问题反而是在这些环节里收获最大。如果你也想尝试我建议不要一上来就追求大而全。先选一个市场、选十个你常买的菜品把采集到展示的链路跑通再去扩展多市场、多品类、定时任务这些花活。这个项目后续可以往哪个方向扩展呢我个人觉得最值得做的是加一个价格预警功能——某个菜品突破历史分位数时自动推送通知比单纯画图实用得多。从一个简单的起点开始慢慢把一个真正能用的系统养起来这大概就是数据项目最有成就感的地方了。
返回列表