第1章:Pandas数据处理实战
做量化交易这几年,我越来越觉得Pandas就是咱们的瑞士军刀。数据清洗、特征工程、回测准备,哪一步都离不开它。今天咱们就从头捋一遍,把Pandas在交易策略中的核心用法吃透。
本章核心知识点:DataFrame与Series基础、时间序列处理、数据重采样与对齐、滚动窗口计算、缺失值处理。
1.1 DataFrame与Series基础
说白了,Series就是一列带标签的数据,DataFrame就是多列拼起来的表格。我刚开始做策略回测时,经常把两者搞混,后来发现一个简单的记忆方法:Series是单列,DataFrame是多列。
import pandas as pd
import numpy as np
# 创建一个Series
close_prices = pd.Series([100.5, 101.2, 99.8, 102.1],
index=['2024-01-01', '2024-01-02', '2024-01-03', '2024-01-04'],
name='close')
print(close_prices)
# 创建一个DataFrame
data = {
'open': [100.0, 101.0, 99.5, 102.0],
'high': [101.5, 102.0, 100.5, 103.0],
'low': [99.5, 100.2, 98.8, 101.0],
'close': [100.5, 101.2, 99.8, 102.1],
'volume': [10000, 12000, 9500, 11000]
}
df = pd.DataFrame(data, index=pd.to_datetime(['2024-01-01', '2024-01-02', '2024-01-03', '2024-01-04']))
print(df.head())
我的小技巧:拿到数据后,我习惯先用 df.info() 和 df.describe() 快速扫一眼。这能帮你发现很多隐藏问题,比如数据类型不对、缺失值太多等。
1.2 时间序列数据处理
量化交易里,时间就是命根子。我见过太多人因为时间索引没处理好,导致回测结果完全失真。记住:时间索引必须是DatetimeIndex类型。
# 确保时间索引正确
df.index = pd.to_datetime(df.index)
# 按时间切片
jan_data = df['2024-01-01':'2024-01-03']
# 提取特定时间属性
df['year'] = df.index.year
df['month'] = df.index.month
df['weekday'] = df.index.weekday # 0=周一
# 时间偏移
df['prev_close'] = df['close'].shift(1) # 前一天的收盘价
df['pct_change'] = df['close'].pct_change() # 收益率
我曾经踩过的坑:用 shift() 时一定要注意索引对齐。有一次我shift了2天,结果因为中间有非交易日,数据全对歪了。建议用 df.index = pd.to_datetime(df.index) 先标准化。
1.3 数据重采样与对齐
不同数据源频率不一样,这是家常便饭。日线数据、分钟数据、Tick数据,怎么对齐?重采样就是干这个的。
# 从日线重采样到周线
weekly_df = df.resample('W').agg({
'open': 'first',
'high': 'max',
'low': 'min',
'close': 'last',
'volume': 'sum'
})
# 从分钟数据重采样到小时
# 假设我们有分钟数据 minute_df
# hourly_df = minute_df.resample('H').mean()
# 数据对齐:合并不同频率的数据
# 假设有两个DataFrame,频率不同
df1 = pd.DataFrame({'close': [100, 101, 102]},
index=pd.to_datetime(['2024-01-01', '2024-01-02', '2024-01-03']))
df2 = pd.DataFrame({'volume': [10000, 11000]},
index=pd.to_datetime(['2024-01-01', '2024-01-03']))
aligned = df1.join(df2, how='outer') # 外连接对齐
重采样常用频率:'D'(日)、'W'(周)、'M'(月)、'H'(小时)、'T'或'min'(分钟)。我一般用 resample().agg() 组合,灵活又清晰。
1.4 滚动窗口计算
移动平均线、布林带、RSI……这些技术指标都离不开滚动窗口。说白了,就是在一个滑动的窗口里做计算。
# 20日移动平均线
df['ma20'] = df['close'].rolling(window=20).mean()
# 布林带:20日均线 ± 2倍标准差
df['std20'] = df['close'].rolling(window=20).std()
df['upper'] = df['ma20'] + 2 * df['std20']
df['lower'] = df['ma20'] - 2 * df['std20']
# 滚动相关系数:比如股价与成交量的相关性
df['corr'] = df['close'].rolling(window=20).corr(df['volume'])
# 自定义滚动函数
def rolling_sharpe(returns, window=20):
"""滚动夏普比率"""
return returns.rolling(window).mean() / returns.rolling(window).std() * np.sqrt(252)
df['sharpe'] = rolling_sharpe(df['pct_change'])
我个人的经验:滚动窗口大小选多少?这取决于你的交易周期。短线用5-10天,中线用20-60天,长线用120-250天。别死板,多试试不同参数。
1.5 缺失值处理
真实数据里,缺失值就像路上的坑,不填平就会翻车。停牌、节假日、数据源故障,都会产生NaN。
# 检查缺失值
print(df.isnull().sum())
# 方法1:向前填充(用上一个有效值填充)
df['close'].fillna(method='ffill', inplace=True)
# 方法2:向后填充
df['close'].fillna(method='bfill', inplace=True)
# 方法3:插值法(线性插值)
df['close'].interpolate(method='linear', inplace=True)
# 方法4:删除缺失行
df.dropna(subset=['close', 'volume'], inplace=True)
# 方法5:用统计量填充
df['volume'].fillna(df['volume'].median(), inplace=True)
我曾经犯过的错:有一次回测,我直接用 dropna() 删掉了所有缺失行。结果因为停牌日太多,数据量少了30%,回测结果完全失真。后来我改用 ffill 填充,效果好了很多。记住:删除要谨慎,填充是首选。
1.6 实战小案例:构建一个简单的均线策略数据管道
咱们把上面学的串起来,做一个完整的例子。你想想看,从原始数据到策略信号,中间要经过多少步?
# 完整流程
def prepare_strategy_data(raw_df):
"""
准备均线策略所需的数据
"""
# 1. 确保时间索引
df = raw_df.copy()
df.index = pd.to_datetime(df.index)
# 2. 计算收益率
df['returns'] = df['close'].pct_change()
# 3. 计算均线
df['ma5'] = df['close'].rolling(5).mean()
df['ma20'] = df['close'].rolling(20).mean()
# 4. 处理缺失值(前20行因为rolling会有NaN)
df.fillna(method='bfill', inplace=True)
# 5. 生成信号
df['signal'] = 0
df.loc[df['ma5'] > df['ma20'], 'signal'] = 1 # 金叉买入
df.loc[df['ma5'] < df['ma20'], 'signal'] = -1 # 死叉卖出
return df
# 使用示例
# result = prepare_strategy_data(raw_data)
# print(result[['close', 'ma5', 'ma20', 'signal']].tail())
核心要点总结:
- DataFrame是表格,Series是列,别搞混
- 时间索引必须标准化为DatetimeIndex
- 重采样用
resample().agg(),灵活组合 - 滚动窗口用
rolling(),注意窗口大小 - 缺失值优先填充,谨慎删除
嗯,这一章的内容就到这儿。Pandas这东西,光看文档没用,得多动手。我建议你拿一份真实的股票数据,把上面的代码跑一遍。遇到问题很正常,慢慢调试就好。