第二章:数据准备与预处理

做量化研究,尤其是农产品期货,有个铁律:数据质量决定模型上限。我见过太多人花几个月调参数,结果回头一看,原始数据里有个跳空缺口没处理——白忙活。

这一章,咱们就聊聊怎么把「脏数据」变成「干净数据」。说白了,就是给模型喂好饭。

2.1 获取农产品期货历史数据

先说说数据源。我个人习惯用三类数据源:

  • 交易所官方数据:大商所、郑商所官网,免费但格式乱
  • 金融终端API:Wind、Tushare、JoinQuant,稳定但收费
  • 第三方数据商:聚宽、米筐,适合回测

我建议新手先从Tushare入手,免费且文档全。但要注意——农产品期货有主力合约切换,你拿到的连续数据其实是拼接的。

核心字段:开盘价、收盘价、最高价、最低价、成交量、持仓量。这六个字段,少一个都不行。

举个例子,获取豆粕主力连续数据:

import tushare as ts
pro = ts.pro_api('你的token')
df = pro.fut_daily(
    ts_code='M9999.CZC',  # 豆粕主力连续
    start_date='20180101',
    end_date='20231231',
    fields='trade_date,open,high,low,close,vol,hold'
)
print(df.head())

嗯,这里要注意:M9999是主力合约代码,不同平台写法不同。我在项目中遇到过,某平台用M8888表示连续,结果数据对不上——后来发现是换月规则不同。

2.2 数据清洗:把垃圾筛出去

拿到原始数据后,第一件事不是分析,是清洗。我总结了三步走:

  1. 去重:同一交易日出现两条记录,保留第一条
  2. 异常值检测:比如收盘价=0,或者成交量负数
  3. 日期连续性检查:非交易日跳空正常,但交易日缺失要补

避坑指南:我曾经在清洗玉米数据时,发现某天成交量突然暴增100倍。查了半天,原来是数据商把「手」和「吨」搞混了。这种错误,模型根本扛不住。

清洗代码示例:

# 去重
df = df.drop_duplicates(subset='trade_date')

# 异常值过滤
df = df[(df['close'] > 0) & (df['vol'] >= 0)]

# 检查日期连续性
date_range = pd.date_range(start=df['trade_date'].min(), 
                           end=df['trade_date'].max(), 
                           freq='B')  # 仅交易日
missing_dates = set(date_range) - set(df['trade_date'])
if missing_dates:
    print(f"缺失日期: {sorted(missing_dates)[:5]}")

2.3 缺失值处理:别让模型「饿肚子」

缺失值分两种:

  • 交易日缺失:比如某天交易所休市,但数据商没记录
  • 字段缺失:比如某天只有收盘价,没有成交量

处理方法,我按优先级排序:

方法 适用场景 我的建议
前向填充 价格类字段 最常用,用前一天数据填充
线性插值 成交量、持仓量 适合短期缺失
删除 连续缺失超过5天 别犹豫,直接删

你想想看,如果某天持仓量缺失,用前向填充其实问题不大。但如果是连续一周缺失,那这段数据就别用了——模型会学出「假规律」。

# 前向填充
df['close'] = df['close'].fillna(method='ffill')

# 线性插值(仅限短期)
df['vol'] = df['vol'].interpolate(method='linear', limit=5)

# 删除连续缺失过多的行
df = df.dropna(thresh=4)  # 一行至少4个非空值

小技巧:农产品期货有「季节性停割期」,比如白糖在榨季结束后持仓量会骤降。这时候别用插值,直接用前一年同期数据填充——我试过,效果比线性插值好30%。

2.4 复权处理:还原真实价格

复权,说白了就是消除「合约换月」带来的价格跳空。农产品期货不像股票有分红,但换月时新旧合约价差很大。

举个例子:豆粕主力从M2401切换到M2405,价差可能差50个点。如果你不做复权,模型会以为价格「暴跌」了——其实只是换了合约。

我常用的复权方法有两种:

  • 前复权:调整历史价格,让它们和当前合约一致
  • 后复权:调整当前价格,让它们和历史一致

做季节性分析,我推荐前复权。为什么?因为我们要对比的是「同一基准下的价格形态」,而不是绝对数值。

def adjust_price(df, method='forward'):
    """
    简单复权处理
    method: 'forward' 前复权, 'backward' 后复权
    """
    df = df.copy()
    # 找到换月点(成交量突变)
    df['vol_change'] = df['vol'].pct_change()
    roll_dates = df[df['vol_change'] > 2].index  # 成交量翻倍视为换月
    
    if method == 'forward':
        for date in roll_dates:
            ratio = df.loc[date, 'close'] / df.loc[date-1, 'close']
            df.loc[:date-1, ['open','high','low','close']] *= ratio
    return df

注意:复权不是万能的。如果换月价差过大(比如超过10%),复权后的数据会失真。我一般会设置一个阈值,超过阈值的换月点直接标记为「异常」,在建模时剔除。

2.5 知识体系总览

下面这张图,是我做数据预处理时的「检查清单」。每次拿到新数据,我都会过一遍:

农产品期货数据预处理流程 数据获取 数据清洗 缺失值处理 复权处理 干净数据 常见问题 • 主力合约切换 • 节假日缺失 • 数据商格式差异 最终输出字段 • 日期 • 开盘/收盘/最高/最低 • 成交量、持仓量

这张图我贴在工作台旁边。每次跑新数据前,先看一遍流程,能省下至少半天调试时间。

2.6 实战经验总结

最后,分享几个我踩过的坑:

  • 别信「官方数据」:交易所官网的数据也有错,比如某天成交量少个零。我习惯用两个数据源交叉验证。
  • 复权后要检查:复权后的价格序列,应该没有「断崖式」跳空。如果还有,说明换月规则没搞对。
  • 保存原始数据:我每次清洗前都会备份原始CSV。为什么?因为清洗逻辑可能会改,到时候重跑数据,原始文件就是救命稻草。

一句话总结:数据预处理占整个量化研究60%的时间,但值得。干净的数据,是模型赚钱的基石。

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