第2章:数据获取与清洗:如何获取历史价格数据?数据清洗的常见问题与处理方法
做农产品季节性分析,第一步就是搞到干净的数据。
这话听起来简单,但我见过太多人,模型建得飞起,结果数据源就有问题。最后分析出来的季节性规律,全是假的。嗯,咱们今天就把这第一步走扎实。
2.1 历史价格数据从哪里来?
我个人习惯,把数据源分成三类:官方、商业、爬虫。各有各的坑。
2.1.1 官方免费数据源
国内最常用的,就是这几个:
- 农业农村部官网:全国农产品批发市场价格信息系统。每天更新,品种全,免费。但格式嘛……经常变。我遇到过好几次,上个月还是CSV,下个月就变成PDF了。
- 国家统计局:月度、季度、年度数据。频率低,但权威。适合做长周期验证。
- 各商品交易所:比如大商所、郑商所。期货价格数据,分钟级、日级都有。API接口相对稳定。
2.1.2 商业数据源
说白了,就是花钱买省心。比如:
- 卓创资讯:农产品价格数据很全,历史数据能追溯到十几年前。API接口也规范。
- Wind、同花顺:金融终端,期货、现货都有。但贵,一年几万块。
- Bloomberg:国际农产品数据。做全球套利的人常用。
商业数据的好处是清洗过,缺失值少。但也不是100%靠谱。我曾经发现卓创的某个品种,连续三天价格一模一样——明显是系统故障。所以,拿到数据后,第一件事不是建模,而是看一眼。
2.1.3 爬虫获取数据
如果你预算有限,或者需要特定网站的数据,爬虫是唯一选择。
但这里有个大坑:反爬虫机制。很多农产品网站,比如“一亩田”、“惠农网”,都有反爬。你爬得太快,IP直接封。
我建议的做法是:
- 用
requests+BeautifulSoup做简单爬虫 - 加随机延时,比如
time.sleep(random.uniform(1, 3)) - 实在不行,上Selenium模拟浏览器
import requests
from bs4 import BeautifulSoup
import time
import random
def fetch_price(url):
headers = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
}
time.sleep(random.uniform(1, 3)) # 随机延时,别太急
resp = requests.get(url, headers=headers)
soup = BeautifulSoup(resp.text, 'html.parser')
# 解析逻辑省略...
return price
2.2 数据清洗:常见问题与处理方法
数据拿到手,你以为就能用了?太天真了。
我做过一个统计:从公开源拿到的农产品价格数据,平均有15%左右的问题。要么缺失,要么异常,要么格式不对。
2.2.1 缺失值处理
缺失值是最常见的。比如节假日没交易,或者网站当天没更新。
处理方法有三种:
- 直接删除:如果缺失比例小于5%,且是随机缺失,直接删行。简单粗暴。
- 向前填充:用上一个交易日的数据填充。适合节假日缺失。比如周六日没数据,用周五的。
- 插值法:线性插值或时间序列插值。适合中间某天缺失。
import pandas as pd
# 向前填充
df['price'].fillna(method='ffill', inplace=True)
# 线性插值
df['price'].interpolate(method='linear', inplace=True)
2.2.2 异常值检测
什么叫异常值?比如鸡蛋价格突然从4块跳到40块,那肯定是数据录入错误。
我常用的方法:
- 3σ原则:超过均值±3个标准差的值,标记为异常。
- 箱线图法:超过1.5倍四分位距的值,视为异常。
- 业务规则:比如猪肉价格不可能低于5元/公斤,也不可能高于100元/公斤。超出这个范围,直接剔除。
# 3σ法
mean = df['price'].mean()
std = df['price'].std()
df = df[(df['price'] > mean - 3*std) & (df['price'] < mean + 3*std)]
# 箱线图法
Q1 = df['price'].quantile(0.25)
Q3 = df['price'].quantile(0.75)
IQR = Q3 - Q1
df = df[(df['price'] > Q1 - 1.5*IQR) & (df['price'] < Q3 + 1.5*IQR)]
2.2.3 日期格式统一
不同数据源的日期格式五花八门。有的用“2024-01-15”,有的用“2024/01/15”,还有的用“2024年1月15日”。
我的习惯是:统一转为 YYYY-MM-DD 格式,并设为索引。
df['date'] = pd.to_datetime(df['date'], format='%Y-%m-%d')
df.set_index('date', inplace=True)
2.2.4 重复值处理
有时候同一个日期会出现两条数据。可能是爬虫重复抓取,也可能是数据源本身的问题。
处理方法:
- 先检查重复:
df.duplicated().sum() - 保留第一条,删除后续重复:
df.drop_duplicates(keep='first')
2.3 数据清洗流程图
下面这张图,是我做数据清洗的标准流程。每次拿到新数据,都走一遍。
2.4 实战:一个完整的清洗案例
咱们拿鸡蛋价格举个例子。假设我从三个源拿到了数据,合并后长这样:
| 日期 | 价格(元/公斤) | 数据源 |
|---|---|---|
| 2024-01-01 | 9.5 | 农业农村部 |
| 2024-01-01 | 9.6 | 卓创资讯 |
| 2024-01-02 | NaN | 农业农村部 |
| 2024-01-02 | 9.7 | 卓创资讯 |
| 2024-01-03 | 50.0 | 农业农村部 |
你看,问题很明显:
- 1月1日有重复数据
- 1月2日有缺失值
- 1月3日价格50元,明显异常
处理步骤:
- 先按日期去重,取多个源的平均值
- 1月2日缺失,用1月1日和1月3日的均值插补
- 1月3日的50元,用3σ法检测后剔除,再用插值补上
# 完整清洗代码
import pandas as pd
import numpy as np
# 原始数据
data = {
'date': ['2024-01-01', '2024-01-01', '2024-01-02', '2024-01-02', '2024-01-03'],
'price': [9.5, 9.6, np.nan, 9.7, 50.0],
'source': ['gov', 'commercial', 'gov', 'commercial', 'gov']
}
df = pd.DataFrame(data)
df['date'] = pd.to_datetime(df['date'])
# 1. 按日期分组,取均值
df_clean = df.groupby('date')['price'].mean().reset_index()
# 2. 缺失值插值
df_clean['price'] = df_clean['price'].interpolate()
# 3. 异常值检测(3σ)
mean = df_clean['price'].mean()
std = df_clean['price'].std()
df_clean = df_clean[(df_clean['price'] > mean - 3*std) &
(df_clean['price'] < mean + 3*std)]
print(df_clean)
date price
2024-01-01 9.55
2024-01-02 9.65
2024-01-03 9.75 ← 异常值已被替换为插值结果
2.5 总结一下
数据获取与清洗,说白了就是“垃圾进,垃圾出”。你花80%的时间在清洗上,一点都不夸张。
我个人习惯是:每次拿到新数据,先画个分布图,看一眼。如果价格曲线像锯齿一样乱跳,那肯定有问题。别急着建模,先查数据源。
记住:干净的数据,是季节性分析的地基。地基不稳,楼盖得再高也得塌。