4、pandas数据处理基础:DataFrame核心操作;时间序列处理;重采样与缺失值处理;滚动计算与窗口函数
做量化回测,说白了就是跟数据打交道。我刚开始接触这行时,总觉得策略逻辑写好了就万事大吉。结果呢?数据格式不对、时间索引乱了、缺失值没处理——回测结果一塌糊涂。嗯,从那以后我学乖了:数据处理是回测系统的地基,地基不稳,楼盖得再高也得塌。
今天咱们就聊聊pandas里最常用的几个数据处理技巧。这些是我在实盘和回测中反复用到的,希望能帮你少走些弯路。
4.1 DataFrame核心操作:你每天都在用的那些
DataFrame,说白了就是个带标签的二维表格。你从交易所拿到的行情数据、从数据库导出的交易记录,基本都是这个格式。
创建与查看
我个人习惯用字典来创建DataFrame,直观又好理解:
import pandas as pd
import numpy as np
# 模拟5天的行情数据
data = {
'date': ['2024-01-02', '2024-01-03', '2024-01-04', '2024-01-05', '2024-01-08'],
'open': [100.5, 101.2, 102.0, 101.8, 103.5],
'high': [102.0, 102.5, 103.2, 102.8, 104.0],
'low': [99.8, 100.5, 101.0, 101.2, 102.5],
'close': [101.0, 102.3, 101.5, 102.0, 103.8],
'volume': [15000, 18000, 12000, 16000, 20000]
}
df = pd.DataFrame(data)
print(df.head()) # 看前5行,心里有个数
选列与选行
这里有个坑,我刚开始时经常搞混:
df['close']返回一个Series(单列)df[['close', 'volume']]返回一个DataFrame(多列)df.iloc[0]按位置选行(第0行)df.loc[0]按索引标签选行(索引为0的行)
.loc 和 .iloc。直接用 df['close'] 虽然方便,但容易在链式赋值时踩坑。你想想看,如果写 df[df['close'] > 102]['volume'] = 0,pandas会给你一个警告——因为它可能修改的是副本而不是原数据。
条件筛选
这是回测中最常用的操作之一:
# 找出所有收盘价大于102的交易日
up_days = df[df['close'] > 102]
print(up_days)
# 多条件:涨幅超过1%且成交量大于15000
df['return'] = df['close'].pct_change()
active_days = df[(df['return'] > 0.01) & (df['volume'] > 15000)]
df[df['return'] > 0.01 & df['volume'] > 15000],结果报错——因为运算符优先级搞错了。
4.2 时间序列处理:回测的灵魂
量化交易的数据,几乎全是时间序列。K线数据、订单流、资金曲线……都带着时间戳。pandas对时间序列的支持非常强大,我甚至可以说,学会时间序列处理,你就掌握了回测系统的一半。
把日期列变成索引
这是第一步,也是最重要的一步:
df['date'] = pd.to_datetime(df['date']) # 先转成datetime类型
df.set_index('date', inplace=True) # 设为索引
print(df.index) # 看看是不是DatetimeIndex
为什么要设成索引?因为pandas的时间序列功能,比如重采样、滚动计算、时间偏移,都是基于DatetimeIndex的。你不设索引,很多功能用不了。
时间切片
设好索引后,选数据就变得非常优雅:
# 选2024年1月4日之后的数据
after_jan4 = df['2024-01-04':]
# 选1月3日到1月5日
range_data = df['2024-01-03':'2024-01-05']
# 选整个1月份(如果有更多数据)
jan_data = df['2024-01']
我曾经在回测一个日内策略时,需要只保留9:30到11:30的数据。用时间切片一行搞定:df.between_time('09:30', '11:30')。要是用循环去过滤,代码得写十几行。
生成日期范围
有时候你需要生成一个完整的交易日历:
# 生成2024年1月的所有交易日(跳过周末)
trading_days = pd.bdate_range(start='2024-01-01', end='2024-01-31')
print(trading_days)
# 输出:DatetimeIndex(['2024-01-01', '2024-01-02', ..., '2024-01-31'], dtype='datetime64[ns]', freq='B')
4.3 重采样与缺失值处理:让数据对齐
做回测时,你经常会遇到数据频率不一致的情况。比如你拿到的行情是1分钟K线,但你的策略信号是日频的。怎么办?重采样。
重采样:从高频到低频
把1分钟数据聚合成日数据,这是最常见的场景:
# 假设df_min是1分钟K线,索引是DatetimeIndex
df_daily = df_min.resample('D').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
})
这里有个细节:resample('D') 是按自然日聚合。但A股市场只有交易日,所以更准确的做法是用 resample('B')(工作日)。不过要注意,如果遇到节假日,'B' 也会把节假日算进去,所以最好还是用自定义的交易日历。
缺失值处理:别让NaN毁了你的策略
数据缺失在实盘中太常见了。停牌、数据源故障、节假日……都会产生NaN。我的经验是:先搞清楚为什么缺失,再决定怎么处理。
# 检查缺失值
print(df.isnull().sum())
# 向前填充(用上一个有效值填充)
df.fillna(method='ffill', inplace=True)
# 向后填充(用下一个有效值填充)
df.fillna(method='bfill', inplace=True)
# 插值法(线性插值)
df.interpolate(method='linear', inplace=True)
# 直接删除有缺失的行
df.dropna(inplace=True)
ffill 填充了缺失的tick数据。结果策略在实盘时表现完全不一样——因为实盘中的缺失是真正的「无交易」,而不是「上一个价格还在」。从那以后,我对缺失值的处理变得非常谨慎:高频数据用dropna,低频数据用ffill。
4.4 滚动计算与窗口函数:策略信号的核心
滚动计算,说白了就是「看过去N天的数据,算个统计量」。移动平均线、布林带、RSI……这些经典指标,底层都是滚动计算。
滚动窗口基础
# 5日移动平均线
df['ma5'] = df['close'].rolling(window=5).mean()
# 20日标准差(布林带用)
df['std20'] = df['close'].rolling(window=20).std()
# 滚动求和(比如5日成交量总和)
df['volume_sum5'] = df['volume'].rolling(window=5).sum()
自定义滚动函数
有时候内置的mean、std不够用,你需要自己写逻辑:
# 计算滚动窗口内的最大值与最小值之差(波动幅度)
def rolling_range(x):
return x.max() - x.min()
df['range_5'] = df['close'].rolling(window=5).apply(rolling_range, raw=False)
扩展窗口:从第一天算到现在
滚动窗口是固定长度的,扩展窗口是从起点到当前点:
# 从上市第一天到现在的累计收益率
df['cum_return'] = (df['close'] / df['close'].iloc[0] - 1)
# 用expanding实现同样的效果
df['cum_return_v2'] = df['close'].expanding().apply(lambda x: x.iloc[-1] / x.iloc[0] - 1)
.apply()。内置函数是用C语言实现的,速度比Python循环快几十倍。我回测5年1分钟数据时,用 .apply() 要跑3分钟,换成内置函数后只要5秒。
4.5 本章知识体系
下面这张图是我自己梳理的pandas数据处理核心脉络,你可以把它当作一个快速索引:
这四个模块不是孤立的。在实际的回测系统中,你经常需要组合使用:先用时间序列处理把数据对齐,然后用重采样统一频率,接着用滚动计算生成信号,最后用DataFrame操作把信号和交易逻辑结合起来。
好了,这一章的内容就到这里。pandas的数据处理能力远不止这些,但上面讲的这些,已经能覆盖回测系统80%以上的需求了。剩下的20%,等你真正遇到问题时,再回来查文档也不迟。
公众号:蓝海资料掘金营,微信deep3321