第二章:数据获取与清洗——用pandas搞定金融数据
做量化交易,有一句老话叫「垃圾进,垃圾出」。
你策略再牛,模型再花哨,数据一塌糊涂,结果就是白忙活。我入行那会儿,就吃过这个亏——跑回测赚得盆满钵满,实盘一上线直接亏麻。后来一查,原来是数据里混了一堆未来函数和异常值。
所以这一章,咱们就踏踏实实把数据基础打牢。说白了,就是学会用pandas把金融数据收拾得服服帖帖。
2.1 获取金融数据——从哪来,怎么拿
金融数据的来源,我常用的就这几类:
- 本地CSV文件:最原始,也最可控。很多券商、数据商都提供CSV导出。
- 在线API接口:比如Tushare、AKShare、Yahoo Finance。实时数据、历史数据都能拉。
- 数据库:MySQL、InfluxDB,适合团队协作和大规模存储。
我个人习惯,刚开始做研究时先用CSV,简单直接。等策略稳定了,再迁移到数据库。
举个例子,用pandas读CSV:
import pandas as pd
# 读取本地CSV
df = pd.read_csv('stock_600519.csv', parse_dates=['date'], index_col='date')
print(df.head())
这里有个小坑——parse_dates参数一定要加。不然日期列会被当成字符串,后面做时间序列分析时各种报错。我曾经因为这个排查了整整一下午。
如果是拉在线数据,AKShare的写法也很简单:
import akshare as ak
# 获取贵州茅台日线数据
df = ak.stock_zh_a_hist(symbol="600519", period="daily",
start_date="20200101", end_date="20231231")
df['日期'] = pd.to_datetime(df['日期'])
df.set_index('日期', inplace=True)
2.2 处理缺失值——别让NaN毁了你的策略
金融数据里,缺失值太常见了。停牌、节假日、数据源抽风,都会导致NaN。
你想想看,一个策略如果遇到NaN就直接报错,那还怎么跑?
处理缺失值,我一般分三步走:
- 先看看缺失情况:
df.isnull().sum()一目了然。 - 判断缺失原因:是随机缺失,还是结构性缺失?
- 选择处理方法:删除、填充、还是插值。
代码示例:
# 查看缺失值
print(df.isnull().sum())
# 方法一:直接删除(慎用)
df_drop = df.dropna()
# 方法二:向前填充(我最常用)
df_fill = df.fillna(method='ffill')
# 方法三:线性插值
df_interp = df.interpolate(method='linear')
为什么我偏爱向前填充?因为金融数据有很强的时序相关性。今天的收盘价不知道,用昨天的代替,比凭空猜一个要靠谱得多。
2.3 处理异常值——揪出那些「离谱」的数据
异常值,就是那些明显不符合常理的数据点。比如某股票一天涨了1000%,或者成交量突然少了几个零。
嗯,这里要注意:异常值不一定是错误,也可能是真实事件(比如借壳上市)。所以处理前一定要先确认。
我常用的检测方法:
| 方法 | 适用场景 | 代码示例 |
|---|---|---|
| Z-score法 | 数据近似正态分布 | (df - df.mean()) / df.std() |
| IQR四分位法 | 数据分布未知,有离群点 | Q3 + 1.5*IQR |
| 固定阈值法 | 有业务常识(如涨跌幅限制) | df[df > 0.1] = np.nan |
实战中,我一般先用IQR法做个初筛,再结合业务逻辑人工复核。举个例子:
# IQR法检测异常值
Q1 = df['close'].quantile(0.25)
Q3 = df['close'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# 标记异常值
df['is_outlier'] = (df['close'] < lower_bound) | (df['close'] > upper_bound)
# 处理:替换为NaN,然后填充
df.loc[df['is_outlier'], 'close'] = np.nan
df['close'].fillna(method='ffill', inplace=True)
2.4 数据对齐——让不同频率的数据「对上话」
做量化交易,你经常需要把不同来源、不同频率的数据合并到一起。比如日线数据和5分钟数据,或者股票数据和宏观经济数据。
数据对齐,说白了就是让它们的时间索引保持一致。
pandas里最常用的就是merge和join:
# 两个不同频率的DataFrame
df_daily = pd.DataFrame({'close': [100, 102, 101]},
index=pd.date_range('2023-01-01', periods=3, freq='D'))
df_minute = pd.DataFrame({'volume': [1000, 1500, 1200]},
index=pd.date_range('2023-01-01 09:30', periods=3, freq='5T'))
# 对齐到日线频率
df_aligned = df_minute.resample('D').sum().join(df_daily, how='outer')
print(df_aligned)
这里有个细节:how='outer'会保留所有时间点,缺失的填NaN。how='inner'只保留交集。我建议先用outer看看数据覆盖情况,再决定怎么处理。
2.5 重采样——把数据「变」成你想要的频率
重采样,就是把数据从一种频率转换成另一种。比如把1分钟数据合成日线,或者把日线数据降采样到周线。
说白了,就是数据频率的「翻译官」。
pandas的resample方法,我几乎天天用:
# 1分钟数据 -> 日线数据
df_daily = df_1min.resample('D').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
})
# 日线数据 -> 周线数据
df_weekly = df_daily.resample('W').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
})
常用的重采样规则:
'D':日线'W':周线(默认周日结束)'M':月线'Q':季线'5T':5分钟线'H':小时线
我记得有一次,一个同事把周线的开盘价写成了mean,结果回测出来的策略收益高得离谱。一查才发现,他把一周五天的开盘价平均了,相当于用了未来数据。嗯,这种错误,犯一次就长记性了。
小结
这一章的内容,说白了就是数据清洗的「三板斧」:
- 获取数据:CSV、API、数据库,选你顺手的
- 处理缺失值:ffill优先,dropna慎用
- 处理异常值:IQR法+业务复核,保守为上
- 数据对齐:检查时区,选对合并方式
- 重采样:选对聚合函数,别用未来数据
下一章,咱们聊聊特征工程——怎么从原始数据里挖出有用的信号。到时候你会发现,数据清洗做得越扎实,后面的工作就越轻松。
公众号:蓝海资料掘金营,微信deep3321