
这些年做数据分析项目农产品价格这个方向一直被很多人低估。不少朋友觉得不就是把批发市场的价格抄进Excel再画个折线图嘛能有什么技术含量但真正接过这类活儿之后你才会发现数据源头五花八门、单位不统一、品种名称乱写、缺失值和异常值满地都是。用Python做一套“采集—清洗—分析—可视化”的自动化系统恰恰能把这种脏活累活变成一条稳定流水线。这篇文章就围绕一个实际项目来拆解数据从哪来、怎么清洗、怎么分析波动和季节性规律、怎么把结果做成能看的图表和简易看板。适合正在学Python数据分析、想找一个完整练手项目或者想做农业信息化相关工具的朋友参考。1. 项目整体设计与思路拆解1.1 动手之前先想清楚三个问题很多初学者拿到这个题目第一反应是下载数据、import pandas、画图结果做着做着发现做不下去了。原因很简单需求没想清楚。我在做这个项目之前先强迫自己回答三个问题。第一个问题数据到底从哪来农产品价格不是一个集中的标准数据集它分散在批发市场、政府公开平台、行业网站甚至在一些Excel附件和PDF里面。你必须先明确是抓别人的公开接口还是下载历史报表还是人工录入。没有稳定数据源后面所有分析都是空中楼阁。第二个问题分析到什么粒度是看全国均价还是分市场看是看日度波动还是看月度趋势是只分析一个品种还是几十个品种一起跑粒度决定了数据表怎么设计也决定了代码的复杂度。我建议第一次做先锁定“一个品种、一个市场、最近三个月”的小闭环跑通了再扩展。第三个问题做给谁看、在哪里看如果只给自己做研究Jupyter Notebook里出几张图就够了如果要做成日报给运营或管理层那就要考虑自动生成HTML报告如果还想做成可视化大屏那又要换一种输出形式。这三个问题直接决定了你的技术选型和工作量。把这些问题写在项目文档第一页后面每一步选择都有了依据。这一步看起来不写代码但反而是整个项目里最省时间的环节。1.2 技术栈选型为什么是Python加pandas加可视化库有人可能会问现在BI工具这么多PowerBI、Tableau拖拽一下就能出图为什么还要拿Python写一遍我的回答是BI工具在“数据已经干净、字段已经明确”的时候确实高效但在农产品价格这个场景里数据清洗占了70%的工作量而这一步恰恰是BI工具的短板。Python的pandas做数据清洗、字段对齐、分组聚合效率非常高可视化部分又可以用matplotlib做基础图、pyecharts做交互图切换成本很低。这套系统的核心链路大概是requests采集、pandas清洗、matplotlib和pyecharts可视化。选这些工具的核心原因有三点。第一pandas处理表格型数据是最顺手的尤其groupby、pivot_table、rolling这类操作BI工具里写起来反而别扭。第二可视化库的灵活度高想画箱线图、热力图、组合图都很自由不受模板约束。第三脚本化以后可以扔到服务器上定时跑每天自动出报告这是手工BI操作做不到的。另外一个容易被忽略的点是生态。pandas的文档和案例极多遇到问题基本一搜就有答案。农产品价格分析本身不涉及太复杂的算法绝大多数需求用基础统计就能解决完全没有必要为了“显得高级”引入深度学习或者复杂的预测模型。1.3 项目目录与数据流设计我建议把项目拆成四层采集层、存储层、分析层、可视化层。每一层之间通过文件和DataFrame传递数据解耦以后调试起来非常舒服。price_analysis/ ├── data/ │ ├── raw/ # 原始数据 │ ├── cleaned/ # 清洗后数据 │ └── output/ # 图表和报告 ├── src/ │ ├── collect.py # 数据采集 │ ├── clean.py # 数据清洗 │ ├── analyze.py # 数据分析 │ ├── visualize.py # 数据可视化 │ └── config.py # 公共配置 ├── main.py # 主入口 └── requirements.txt数据流按顺序走collect.py拿到原始数据存到data/rawclean.py读取后统一格式输出到data/cleanedanalyze.py基于清洗后的数据计算指标visualize.py读取指标结果生成图片或者HTML报告。main.py负责把这几步串起来传参控制在最小范围。这样设计的好处是哪一步出了问题可以单独重跑不需要从头再来。比如数据源临时变了只改collect.py汇报口径变了只改analyze.py。这种模块化思维比代码本身更值得学习。2. 环境准备与数据采集把数据装进DataFrame2.1 基础环境与依赖安装项目基于Python 3.8以上版本建议直接用Anaconda或者手动创建虚拟环境不要图省事装到系统Python里。虚拟环境的好处是依赖隔离后面装错包不会影响其他项目。核心依赖其实就这几个pip install pandas matplotlib pyecharts requests openpyxlpandas负责数据处理matplotlib负责静态图表pyecharts负责交互式图表requests用于抓取接口和网页openpyxl用于读写Excel。如果安装过程比较慢可以临时换国内镜像源命令写成pip install -i https://pypi.tuna.tsinghua.edu.cn/simple pandas matplotlib pyecharts requests openpyxl这里有个细节经验不要把绘图库一股脑全装上。seaborn、plotly、bokeh都很好但一个项目里选一两个主力的就够了安装包多、引入依赖多、出问题概率也高。我后期发现pyecharts和matplotlib已经能覆盖绝大多数场景其他库基本用不上。2.2 数据来源与合规的采集方式农产品价格数据的公开渠道不少常见的有政府价格监测平台、大型批发市场官网、农业行业数据网站。如果你只是自己分析优先找提供结构化数据的接口或历史报表这样省去大量解析成本。我用requests写过一个简单采集脚本逻辑是请求公开接口拿到JSON后转成DataFrame。核心代码大概长这样import requests import pandas as pd url https://example.com/api/price # 替换为实际接口 params {market: 某批发市场, date: 2025-01-01} resp requests.get(url, paramsparams, timeout10) data resp.json() df pd.DataFrame(data[data]) df.to_csv(data/raw/price_20250101.csv, indexFalse, encodingutf-8)如果目标网站没有接口也可以解析HTML。解析之前一定要先看网站的robots协议和使用条款只采集公开数据控制请求频率不要对对方服务器造成压力。我的习惯是每次请求之间至少间隔2秒并且只拉取近期增量数据不做全量狂抓。还有一个非常实用的兜底方案很多数据分析平台提供CSV或Excel下载手动下载整理后放到data/raw目录collect.py直接跳过抓取步骤从本地文件读取。项目第一版完全可以用这种方式跑通等逻辑没问题了再补自动采集。2.3 数据入库与字段规范采集到的原始数据通常很乱字段名可能是中文拼音、英文缩写甚至乱码。我复盘过数十份不同来源的农产品价格数据最后把标准字段统一为这样一张表字段名类型说明示例datedatetime行情日期2025-01-01marketstr市场名称某批发市场categorystr品类蔬菜varietystr品种黄瓜low_pricefloat最低价元/公斤3.2high_pricefloat最高价元/公斤4.5avg_pricefloat平均价元/公斤3.8unitstr原始单位元/公斤把数据统一到这个结构非常重要。注意unit这一列单位字段必须保留原始值哪怕你已经统一换算过也不要删掉。原因很实际数据源后续可能会补充新的单位格式保留原始单位可以反查换算逻辑是否出错。import pandas as pd def normalize_columns(df): df.columns [col.strip().lower() for col in df.columns] df df.rename(columns{ 日期: date, 市场: market, 品名: variety, 最低价: low_price, 最高价: high_price, 平均价: avg_price, 单位: unit }) return df这一步看着基础其实是整个系统最值得花时间的地方。字段规范一旦立起来后续扩展新数据源时只需要写一个适配函数把新字段映射过来分析代码完全不用动。3. 核心分析模块从一组价格里读出规律3.1 数据清洗决定分析结果靠谱的第一步数据清洗听起来不性感但它直接决定后面所有结论能不能信。我做这个项目时清洗环节踩的坑比分析环节多得多。首先是缺失值。农产品价格数据经常出现某几天没报价的情况。处理缺失值最忌讳的是全局填充比如把所有NaN都填成均值这样做会抹平品种之间的差异。正确做法是按品种分组再在组内向后填充模拟“缺的那天沿用最近一次报价”的业务逻辑df df.sort_values([variety, date]) df[avg_price] df.groupby(variety)[avg_price].transform( lambda x: x.ffill() )其次是异常值。实际数据里出现过平均价低于最低价、价格为负数、单日价格暴涨10倍这类情况。我一般先用describe看整体范围再用百分位数判断离群点for variety in df[variety].unique(): sub df[df[variety] variety] lower sub[avg_price].quantile(0.01) upper sub[avg_price].quantile(0.99) df.loc[(df[variety] variety) ((df[avg_price] lower) | (df[avg_price] upper)), avg_price] None这里只把异常值置空而不直接删除整行。原因很简单一条记录里可能有多个字段价格异常不代表品种信息和市场信息也是错的置空后再用组内填充补上信息损失最小。最后是单位统一。不同数据源给出的单位可能是元/斤、元/公斤甚至元/吨。我建了一个映射表统一转成元/公斤unit_map {元/公斤: 1, 元/斤: 2, 元/吨: 0.001} df[avg_price] df.apply( lambda row: row[avg_price] * unit_map.get(row[unit], 1), axis1 ) df[unit] 元/公斤清洗完成后一定要做一次“数据体检”按品种打印记录数、时间范围和均值肉眼扫一遍有没有明显不合理的地方。这一步我每次都会做因为程序判断终归有盲区。3.2 价格趋势与波动分析环比、均线、市场对比清洗完数据正式进入分析环节。最基础也最常用的分析是看价格趋势。先把时间序列排好序然后算几种典型指标。描述性统计是最先看的。对黄瓜这个品种执行describe能快速得到均价、标准差、25%和75%分位数对整体价格水平有个判断。直接一行代码df[df[variety] 黄瓜][avg_price].describe()接下来是移动平均。为什么用移动平均因为农产品价格受短期供需扰动非常明显今天涨5%明天跌3%很正常直接看原始序列很难抓住趋势。我习惯用7日移动平均原因是农产品价格有很强的周内周期性周末消费旺、周初批发清货7天窗口能平滑掉这种周期。df[ma7] df.groupby(variety)[avg_price].transform( lambda x: x.rolling(7, min_periods1).mean() )环比涨跌幅是另一个高频指标。它有明确的业务含义今天比昨天涨了还是跌了涨了几个百分点。df[pct_change] df.groupby(variety)[avg_price].pct_change() * 100做环比分析时有个容易忽略的坑法定节假日前后价格波动特别大而且不同年份的节假日日期不同直接做同比会失真。我一般会在结果里把节假日数据单独标记出来避免业务方误读。跨市场对比用的是透视表。比如想看黄瓜在三个市场的价格差异pivot df.pivot_table( indexdate, columnsmarket, valuesavg_price ) pivot.plot(figsize(12, 5))透视表做出来之后能非常直观地看到哪个市场长期偏高、哪个市场波动剧烈。这些都是业务方真正关心的问题。3.3 季节性规律与涨跌榜趋势分析解决“最近在涨还是在跌”季节分析解决“每年这个时候大概是什么水平”。按月聚合是计算季节性规律最简单也最有效的方式。df[month] df[date].dt.month monthly_mean df.groupby([variety, month])[avg_price].mean().unstack()更直观的做法是计算季节指数。具体方法是先算每个品种的整体月均价格再用每个月均价格除以整体均值乘以100。指数大于100说明该月价格高于平均低于100说明低于平均。seasonal monthly_mean.div(monthly_mean.mean(axis1), axis0) * 100这个指数很有用。比如黄瓜的季节指数如果显示6到8月数值常年偏低那就可以推测这个时段是黄瓜供应旺季价格容易往下走。基于这个信息采购方可以提前安排囤货节奏种植户也可以调整排期。涨跌榜是另一个业务方特别喜欢看的输出。按周维度统计哪些品种涨得最多、跌得最多用nlargest和nsmallest就能实现week_change df.groupby(variety)[pct_change].mean() top_gainers week_change.nlargest(5) top_losers week_change.nsmallest(5)这里要提醒一句周维度涨跌幅榜随时可能出现“上周跌20%这周涨30%”的品种这种极端波动往往意味着背后有天气、物流或集中上市的结构性因素不是单纯的统计游戏。榜单只是线索不能直接把结论甩给业务方。4. 可视化模块让价格变化“看得见”4.1 先用matplotlib跑通静态图表数据分析做得再好如果不能直观展示价值就打折扣。我习惯先用matplotlib把静态图表跑通因为它是基础能力出图快、可定制性强、不依赖浏览器环境。第一张图永远是价格走势叠加7日移动平均。代码很简单import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False fig, ax plt.subplots(figsize(12, 5)) sub df[df[variety] 黄瓜].tail(90) ax.plot(sub[date], sub[avg_price], label日均价, linewidth1) ax.plot(sub[date], sub[ma7], label7日均线, linewidth2) ax.set_title(黄瓜近90天价格走势) ax.legend() plt.xticks(rotation45) plt.tight_layout() plt.savefig(data/output/cucumber_trend.png, dpi150, bbox_inchestight)这段代码里最关键的是两个rcParams设置。第一个设置中文字体SimHei否则图上的中文全部变成方框第二个设置负号正常显示否则坐标轴上的负号会显示成方块。这两个问题几乎所有人都遇到过最好一开始就写上。第二张图可以用箱线图看价格分布。不同品种的均价范围差异很大箱线图能同时展示中位数、四分位距和离群点比单纯看均线丰富得多import seaborn as sns sns.boxplot(datadf[df[category] 蔬菜], xvariety, yavg_price) plt.xticks(rotation90)对于一个日报型项目这两张图已经能覆盖大多数汇报场景。静态图的优势是稳定随便用什么工具都能打开塞进Word和PPT都很方便。4.2 用pyecharts做交互式图表和简易看板静态图够用但交互式图表对业务方的说服力强得多。鼠标悬停能看到具体数值、可以缩放区间、图例可以点击隐藏这些体验远超一张静态PNG。我在项目里用pyecharts做交互图主要是看中它纯Python生成HTML不需要写一行前端代码。画一个基础折线图from pyecharts.charts import Line from pyecharts import options as opts sub df[df[variety] 黄瓜].tail(90) line ( Line() .add_xaxis(sub[date].dt.strftime(%Y-%m-%d).tolist()) .add_yaxis(日均价, sub[avg_price].round(2).tolist(), is_smoothTrue) .set_global_opts( title_optsopts.TitleOpts(title黄瓜价格走势), tooltip_optsopts.TooltipOpts(triggeraxis), ) ) line.render(data/output/cucumber_line.html)如果想一屏展示多个图表最省钱的做法是pyecharts的Tab。把趋势图、涨跌榜柱状图、月度箱线图拼成一个Tab输出一个HTML文件就有一个简易看板的样子了。from pyecharts.charts import Tab tab Tab() tab.add(line, 趋势) tab.add(bar, 涨跌榜) tab.add(boxplot, 月度分布) tab.render(data/output/report.html)如果业务方提出“要一个可视化大屏”不要慌先用Tab做一个多图表页面顶住绝大多数情况下已经够用。真正的大屏项目涉及的屏幕适配、实时刷新、后端推送那是另一个量级的工程不建议在这个阶段一上来就铺开做。5. 常见问题与排查技巧实录5.1 数据读取和编码那些坑用pandas读CSV报乱码是农产品价格项目里最经典的问题。国内很多公开数据文件是GBK或者GB2312编码直接read_csv默认用UTF-8解码就会变成乱码。解决办法是尝试指定编码df pd.read_csv(data/raw/price.csv, encodinggbk)但实际工作中同一个文件夹里的CSV可能一半UTF-8一半GBK更稳妥的方式是写一个自动检测逻辑with open(data/raw/price.csv, rb) as f: raw f.read() # 尝试常见编码顺序 for enc in [utf-8, gbk, gb2312, latin1]: try: df pd.read_csv(data/raw/price.csv, encodingenc) print(读取成功编码为, enc) break except UnicodeDecodeError: continue另一个高频坑是Excel表头不在第一行。很多原始报表前面有标题行、单位行、说明行需要skiprows参数跳过df pd.read_excel(data/raw/price.xlsx, skiprows2)读进来之后还要留意日期列究竟是不是datetime类型。很多情况下日期被读成了字符串后面做resample就报错。标准检查方式是打印df.dtypes看到date列显示object就说明需要做转换。5.2 图表显示和字体问题matplotlib中文乱码这个问题前面埋过伏笔这里再补充一种情况。如果你在自己电脑上设置SimHei没问题但把脚本部署到Linux服务器SimHei字体大概率不存在图表中文照样变方块。解决办法有两种一种是服务器上安装中文字体另一种是完全用英文标签。我的经验是如果报告是给国内业务方看的还是装字体更靠谱。Debian/Ubuntu上装中文字体命令apt-get install -y fonts-wqy-zenhei然后在代码里指定plt.rcParams[font.sans-serif] [WenQuanYi Zen Hei, SimHei]pyecharts这边常见的报错是地图渲染空白。这个问题的根源是pyecharts从v1.0开始把地图数据拆成了单独的包使用地图前必须安装地图扩展包pip install echarts-countries-pypkg不过单纯做价格分析很少需要地图有需要时再装不迟。另外一个pyecharts的问题是Jupyter Notebook里图表不显示。解决办法是渲染成HTML后用IFrame嵌入或者确认已经调用render_notebook。如果还不行换个浏览器试试大多数时候是本地渲染环境的问题。5.3 数据量变大之后怎么办第一版系统按单个品种三个月的数据跑pandas毫无压力。但扩展到几十个品种、十几个市场、跨三五年历史数据之后内存占用和计算速度都会出现明显变化。这时候优先做三件事。第一读取时指定列类型和使用小范围数据。read_csv的usecols参数可以只读需要的列dtype参数可以提前把字符串列设为category减少内存df pd.read_csv( data/cleaned/price_clean.csv, usecols[date, market, variety, avg_price], dtype{market: category, variety: category}, parse_dates[date] )第二把函数化改造做彻底。不要在一个脚本里从上到下写一千行而是把清洗、分析、可视化都拆成函数每个函数输入输出明确。这样内存不够的时候可以做链式处理用完一个结果就释放一个。第三引入增量更新和定时调度。每天只要拉当天的新数据追加到历史数据后面不需要每天全量重算。定时任务用Linux的crontab或者Windows的任务计划程序都能实现命令就一行0 8 * * * cd /path/to/price_analysis python main.py如果数据量进一步增长到几百万行pandas也能凑合跑但更稳的方案是引入SQLite或者PostgreSQL把清洗好的数据放数据库里用SQL做聚合pandas只负责最后一公里的分析和图表。这个扩展路径很平滑因为我前面要求规范字段名和数据类型切到数据库时几乎不用改分析逻辑。做完这套东西我最大的一个体会是农产品价格分析真正的难点不在画图也不在算法而在数据治理。一开始我也花了不少时间研究各种“高级”统计方法后来发现把单位统一、把品种名对齐、把缺失值处理好比任何模型都能更显著地提升结果可信度。如果你也要动手做类似项目我建议先拿一个品种、一个市场、三个月的数据把整个链路跑通再慢慢扩展品种和数据源每加一个数据源都要重新检查一遍清洗逻辑。另外出图表之前多问自己一句“谁会看这张图、他看完要做什么决定”能帮你砍掉不少没用的功能。数据这种事情慢就是快底子打扎实了后面都是顺手的活。