第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
注意: 爬虫有法律风险。尤其是商业网站,robots.txt 里明确禁止的,就别碰了。我一般只爬政府公开数据。

2.2 数据清洗:常见问题与处理方法

数据拿到手,你以为就能用了?太天真了。

我做过一个统计:从公开源拿到的农产品价格数据,平均有15%左右的问题。要么缺失,要么异常,要么格式不对。

2.2.1 缺失值处理

缺失值是最常见的。比如节假日没交易,或者网站当天没更新。

处理方法有三种:

  1. 直接删除:如果缺失比例小于5%,且是随机缺失,直接删行。简单粗暴。
  2. 向前填充:用上一个交易日的数据填充。适合节假日缺失。比如周六日没数据,用周五的。
  3. 插值法:线性插值或时间序列插值。适合中间某天缺失。
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)]
避坑指南: 我曾经处理过一批大蒜数据,用3σ法直接删掉了30%的数据。后来才发现,大蒜价格波动本来就大,3σ太严格了。后来改用业务规则+人工复核,才保住数据。

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 数据清洗流程图

下面这张图,是我做数据清洗的标准流程。每次拿到新数据,都走一遍。

农产品价格数据清洗流程图 1. 数据获取 2. 日期格式统一 3. 缺失值处理 4. 异常值检测 5. 重复值处理 ✅ 干净数据

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月2日缺失,用1月1日和1月3日的均值插补
  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%的时间在清洗上,一点都不夸张。

我个人习惯是:每次拿到新数据,先画个分布图,看一眼。如果价格曲线像锯齿一样乱跳,那肯定有问题。别急着建模,先查数据源。

记住:干净的数据,是季节性分析的地基。地基不稳,楼盖得再高也得塌。


蓝海数据掘金营,专注资料整理