金融数据预处理实战:数据清洗与特征工程
说实话,做量化模型这么多年,我踩过最大的坑往往不是模型选得不对,而是数据本身就有问题。你想想看,垃圾数据喂进去,再牛的模型也白搭。今天我们就来聊聊金融数据预处理这档子事。
数据清洗:别让脏数据毁了你的模型
金融数据有多脏?我刚开始做量化那会儿,从Wind拉了一堆日线数据,跑回测时发现某只股票某天涨了500%。当时我还挺兴奋,以为抓到妖股了。结果一看,原来是除权除息日没做复权处理。嗯,这种坑我踩过不止一次。
缺失值处理
金融数据缺失很常见。停牌、节假日、数据源问题,都会导致缺失。我个人习惯分三步走:
- 先看缺失比例——缺失超过30%的字段,直接扔掉
- 再看缺失模式——是随机缺失还是系统性缺失
- 最后选填充方法——前向填充、插值、还是模型预测
核心原则:股票数据用前向填充(ffill)最稳妥。因为停牌期间,价格其实没变。用均值填充反而会引入偏差。
import pandas as pd
import numpy as np
# 加载数据
df = pd.read_csv('stock_data.csv', index_col='date', parse_dates=True)
# 检查缺失
print(df.isnull().sum())
# 前向填充(我最常用的方式)
df.fillna(method='ffill', inplace=True)
# 如果开头还有缺失,用后向填充
df.fillna(method='bfill', inplace=True)
# 实在不行,用插值
df.interpolate(method='linear', inplace=True)
我的经验:千万别对收益率数据做插值。收益率缺失往往意味着停牌,插值会制造出虚假的交易信号。我曾经因为这个原因,回测曲线漂亮得不行,实盘直接崩了。
异常值处理
异常值在金融数据里太常见了。涨跌停、乌龙指、数据录入错误,都会产生异常。我一般用两种方法检测:
| 方法 | 适用场景 | 阈值 |
|---|---|---|
| Z-score | 正态分布的数据 | |Z| > 3 |
| IQR四分位距 | 偏态分布的数据 | 超出Q1-1.5IQR或Q3+1.5IQR |
def detect_outliers_iqr(data, column):
Q1 = data[column].quantile(0.25)
Q3 = data[column].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = data[(data[column] < lower_bound) |
(data[column] > upper_bound)]
return outliers
# 检测涨跌幅异常
returns = df['close'].pct_change()
outliers = detect_outliers_iqr(returns, 'close')
print(f'发现 {len(outliers)} 个异常值')
注意:金融数据的异常值不一定要删除。比如2015年股灾那几天,异常波动本身就是市场信息。我建议先标记,再根据策略需求决定是否保留。
特征工程基础:技术指标计算
特征工程说白了,就是把原始数据变成模型能理解的语言。技术指标是量化里最常用的特征。我个人习惯先算三个最基础的:MA、RSI、MACD。这三个指标组合起来,能覆盖大部分趋势和动量信息。
移动平均线(MA)
MA是最简单的趋势指标。但你别小看它,很多量化策略的核心就是均线交叉。我一般算5日、20日、60日三条均线:
# 计算移动平均线
df['MA5'] = df['close'].rolling(window=5).mean()
df['MA20'] = df['close'].rolling(window=20).mean()
df['MA60'] = df['close'].rolling(window=60).mean()
# 均线交叉信号
df['MA_cross'] = np.where(df['MA5'] > df['MA20'], 1, -1)
相对强弱指标(RSI)
RSI用来判断超买超卖。我记得有次做期货策略,RSI参数调了半天,最后发现14天周期在A股效果最好。为什么?因为A股的交易节奏和美股不一样。
def calculate_rsi(data, window=14):
delta = data.diff()
gain = (delta.where(delta > 0, 0)).rolling(window=window).mean()
loss = (-delta.where(delta < 0, 0)).rolling(window=window).mean()
rs = gain / loss
rsi = 100 - (100 / (1 + rs))
return rsi
df['RSI'] = calculate_rsi(df['close'])
# 超买超卖信号
df['RSI_signal'] = 0
df.loc[df['RSI'] < 30, 'RSI_signal'] = 1 # 超卖,买入信号
df.loc[df['RSI'] > 70, 'RSI_signal'] = -1 # 超买,卖出信号
MACD指标
MACD是趋势跟随指标,由快线、慢线和柱状线组成。说实话,MACD在震荡市里表现一般,但在趋势行情中特别好用。
# 计算MACD
exp12 = df['close'].ewm(span=12, adjust=False).mean()
exp26 = df['close'].ewm(span=26, adjust=False).mean()
df['MACD'] = exp12 - exp26
df['MACD_signal'] = df['MACD'].ewm(span=9, adjust=False).mean()
df['MACD_hist'] = df['MACD'] - df['MACD_signal']
# MACD金叉死叉
df['MACD_cross'] = np.where(df['MACD'] > df['MACD_signal'], 1, -1)
避坑指南:计算技术指标时,前N个数据会有NaN值。我建议直接dropna(),或者用bfill填充。千万别用0填充,那会严重扭曲指标值。
数据标准化与归一化
为什么要做标准化?你想想看,股价可能是几百块,RSI是0-100,成交量是几千万。这些量纲完全不同的数据放到一起,模型会优先关注数值大的特征。说白了,就是不公平。
标准化(Z-score)
标准化假设数据服从正态分布。我个人习惯用标准化,因为它保留了数据的分布形态:
from sklearn.preprocessing import StandardScaler
scaler = StandardScaler()
features = ['MA5', 'MA20', 'RSI', 'MACD', 'volume']
df_scaled = df.copy()
df_scaled[features] = scaler.fit_transform(df[features])
print(f'标准化后均值: {df_scaled[features].mean().round(2)}')
print(f'标准化后方差: {df_scaled[features].var().round(2)}')
归一化(Min-Max)
归一化把数据压缩到[0,1]区间。适合数据有明确上下界的场景,比如RSI本身就是0-100:
from sklearn.preprocessing import MinMaxScaler
minmax_scaler = MinMaxScaler(feature_range=(0, 1))
df_normalized = df.copy()
df_normalized[features] = minmax_scaler.fit_transform(df[features])
| 方法 | 公式 | 输出范围 | 适用场景 |
|---|---|---|---|
| 标准化 | (x - μ) / σ | 无固定范围 | 数据近似正态分布 |
| 归一化 | (x - min) / (max - min) | [0, 1] | 数据有明确边界 |
重要提醒:一定要先拆分训练集和测试集,再用训练集的数据拟合scaler,然后transform测试集。千万别对整个数据集做标准化,那叫数据泄露。我见过有人犯这个错,回测结果好得离谱,实盘直接翻车。
本章知识体系
下面这张图总结了数据预处理的完整流程。你可以把它当作操作清单,每次做数据预处理时对照检查:
数据预处理这件事,说白了就是「垃圾进,垃圾出」的反面——你想让模型输出金子,就得先确保输入的是金子。我做了这么多年量化,越来越觉得预处理比模型本身更重要。模型选错了可以换,数据搞砸了,整个项目都得重来。
最后说一句:别嫌预处理麻烦。我见过太多人花80%的时间调模型参数,却只花20%的时间处理数据。结果呢?模型在训练集上表现完美,一到实盘就露馅。数据质量,才是量化模型的真正护城河。