第二章:Python数据处理基础:Pandas入门、NumPy数组操作、金融时间序列处理、数据清洗实战
说实话,金融数据处理这活儿,80%的时间都在跟数据较劲。不是格式不对,就是缺胳膊少腿。我刚开始做量化策略的时候,光是把交易所的Level-2行情数据洗干净,就熬了三个通宵。嗯,从那以后我就明白了——工具链再花哨,数据处理的基本功不过关,一切都是白搭。
这一章,咱们就聊聊Python里最核心的几个数据处理工具。Pandas、NumPy,还有金融时间序列那点事儿。我会把我在实战中踩过的坑、总结的经验,一股脑儿倒给你。
2.1 NumPy:金融计算的基石
NumPy这东西,说白了就是Python里的C语言。速度快,内存省。我见过不少新手用Python列表做金融计算,跑个回测要等半小时。换成NumPy数组,几秒钟搞定。为什么?因为底层是C写的,还用了SIMD指令集。
创建数组的几种姿势
import numpy as np
# 从列表创建
prices = np.array([100.5, 101.2, 99.8, 102.1])
# 全零数组,常用于初始化
zeros = np.zeros((5, 3)) # 5行3列
# 等差数列,金融里算时间轴常用
time_axis = np.arange(0, 10, 0.5) # 从0到10,步长0.5
# 随机数,蒙特卡洛模拟必备
np.random.seed(42) # 固定种子,保证可复现
returns = np.random.randn(1000) # 标准正态分布
我个人习惯,在写策略回测框架时,所有价格数据都用np.float64。精度够用,速度也快。别用float128,除非你算的是天文数字。
索引与切片:别小看这个
金融数据经常要取某一段。比如取最近20个交易日的收盘价。NumPy的切片效率极高,因为它不复制数据,只是创建一个视图。
data = np.array([100, 102, 101, 103, 105, 104, 106])
# 取最后3个
last_3 = data[-3:] # [105, 104, 106]
# 布尔索引,筛选出大于102的值
mask = data > 102
filtered = data[mask] # [103, 105, 104, 106]
# 花式索引,取第1、3、5个
indices = [0, 2, 4]
selected = data[indices] # [100, 101, 105]
广播机制:让不同形状的数组做运算
这个机制太实用了。比如你有一个股票组合的权重向量,和一个收益率矩阵。想算组合收益率?直接乘就行。
weights = np.array([0.3, 0.4, 0.3]) # 3只股票的权重
returns = np.array([[0.01, 0.02, -0.01], # 3个时间点,3只股票
[0.02, 0.01, 0.03],
[-0.01, 0.03, 0.02]])
# 广播:weights自动扩展成3x3
portfolio_returns = returns @ weights # 矩阵乘法
# 结果:[0.008, 0.019, 0.014]
你想想看,如果没有广播,你得写个循环,一行一行算。又慢又丑。
2.2 Pandas:金融数据的瑞士军刀
Pandas就是为表格数据而生的。金融数据天然就是表格——时间戳、开盘价、收盘价、成交量...一行行排下来。用Pandas处理,顺手得不行。
DataFrame与Series
Series是一列数据,DataFrame是多列。我习惯把DataFrame想象成Excel表格,但比Excel灵活一万倍。
import pandas as pd
# 从字典创建DataFrame
data = {
'date': ['2024-01-02', '2024-01-03', '2024-01-04'],
'open': [100.5, 101.2, 99.8],
'close': [101.2, 99.8, 102.1],
'volume': [10000, 12000, 9500]
}
df = pd.DataFrame(data)
# 查看前几行
print(df.head())
# 基本统计
print(df.describe())
df.set_index('date', inplace=True) 一步搞定。
分组聚合:按股票、按日期汇总
金融数据经常要分组。比如按股票代码分组,算每只股票的日均收益率。Pandas的groupby就是干这个的。
# 假设有多只股票的数据
df = pd.DataFrame({
'stock': ['AAPL', 'AAPL', 'GOOG', 'GOOG', 'MSFT', 'MSFT'],
'date': ['2024-01-02', '2024-01-03', '2024-01-02', '2024-01-03', '2024-01-02', '2024-01-03'],
'return': [0.01, -0.02, 0.03, 0.01, -0.01, 0.02]
})
# 按股票分组,算平均收益率
avg_return = df.groupby('stock')['return'].mean()
print(avg_return)
# stock
# AAPL -0.005
# GOOG 0.020
# MSFT 0.005
嗯,这里要注意。groupby之后的操作,尽量用内置的聚合函数(mean, sum, std)。别自己写循环,慢得离谱。
合并与连接:把多张表拼起来
做金融分析,经常要把行情数据和财务数据合并。Pandas的merge就像SQL里的JOIN。
# 行情数据
prices = pd.DataFrame({
'date': ['2024-01-02', '2024-01-03'],
'close': [100, 102],
'stock': 'AAPL'
})
# 财务数据
financials = pd.DataFrame({
'date': ['2024-01-02', '2024-01-03'],
'pe_ratio': [25.3, 24.8],
'stock': 'AAPL'
})
# 内连接
merged = pd.merge(prices, financials, on=['date', 'stock'], how='inner')
print(merged)
df.duplicated() 先检查一下。
2.3 金融时间序列处理
金融数据最特殊的地方,就是时间。不是所有时间都有数据——周末、节假日都是空的。而且不同市场的时间还不一样。A股和美股的时间戳,差了12个小时。
重采样:把高频数据降频
我经常要把分钟级数据变成日线。Pandas的resample就是干这个的。
# 假设有分钟级数据
idx = pd.date_range('2024-01-02 09:30', '2024-01-02 16:00', freq='1min')
df = pd.DataFrame({'price': np.random.randn(len(idx)).cumsum() + 100}, index=idx)
# 重采样为日线,取收盘价
daily = df.resample('D').last()
# 或者取OHLC
ohlc = df.resample('D').ohlc()
我个人习惯,重采样时用 .last() 取收盘价,用 .first() 取开盘价。别用 .mean(),那算出来的是均价,不是金融里常用的OHLC。
滑动窗口:算移动平均线
技术分析里最常用的就是移动平均线。Pandas的rolling函数,一行代码搞定。
# 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']
center=True 参数。但注意,这会引入未来信息,回测时千万别用。
时区处理:跨市场数据对齐
做全球资产配置时,时区是个大麻烦。A股收盘是北京时间15:00,美股开盘是北京时间21:30。怎么对齐?
import pytz
# 设置时区
df_asia = df_asia.tz_localize('Asia/Shanghai')
df_us = df_us.tz_localize('US/Eastern')
# 统一转换到UTC
df_asia_utc = df_asia.tz_convert('UTC')
df_us_utc = df_us.tz_convert('UTC')
# 现在可以对齐了
aligned = pd.concat([df_asia_utc, df_us_utc], axis=1)
我曾经在做一个跨市场套利策略时,因为时区没处理好,信号早了8个小时触发。嗯,那次回测结果漂亮得不像真的——后来发现是数据对齐出了问题。
2.4 数据清洗实战
真实世界的金融数据,脏得超乎你想象。缺失值、异常值、重复数据、格式不一致...我敢说,任何一个金融数据集,至少10%的数据需要清洗。
缺失值处理
缺失值在金融数据里太常见了。停牌、节假日、数据源故障...处理方法就几种:删除、填充、插值。
# 检查缺失值
print(df.isnull().sum())
# 删除有缺失的行
df_clean = df.dropna()
# 向前填充(用上一个有效值填充)
df_filled = df.fillna(method='ffill')
# 线性插值
df_interpolated = df.interpolate(method='linear')
异常值检测
异常值可能是数据错误,也可能是真实的极端行情(比如闪崩)。怎么区分?我一般用两种方法。
# 方法1:3σ原则
mean = df['return'].mean()
std = df['return'].std()
outliers = df[(df['return'] < mean - 3*std) | (df['return'] > mean + 3*std)]
# 方法2:IQR(四分位距)
Q1 = df['return'].quantile(0.25)
Q3 = df['return'].quantile(0.75)
IQR = Q3 - Q1
outliers_iqr = df[(df['return'] < Q1 - 1.5*IQR) | (df['return'] > Q3 + 1.5*IQR)]
我个人更倾向于IQR方法。因为金融收益率不是正态分布,3σ原则会漏掉很多尾部风险。
重复数据去重
数据源偶尔会重复推送。比如某一天的数据出现了两次。去重很简单,但要注意判断标准。
# 基于所有列去重
df_unique = df.drop_duplicates()
# 基于特定列去重(比如日期+股票代码)
df_unique = df.drop_duplicates(subset=['date', 'stock'])
# 保留最后一个重复值
df_unique = df.drop_duplicates(subset=['date', 'stock'], keep='last')
keep='last' 保留最新的那条。
数据类型转换
从CSV读进来的数据,经常是字符串。比如价格列是'100.5'这种字符串。不转成float,没法做计算。
# 查看数据类型
print(df.dtypes)
# 转换类型
df['price'] = pd.to_numeric(df['price'], errors='coerce')
df['date'] = pd.to_datetime(df['date'])
# 处理千分位分隔符
df['volume'] = df['volume'].str.replace(',', '').astype(float)
嗯,这里要注意。errors='coerce' 会把无法转换的值变成NaN。这样你就能看到哪些数据有问题了。
金融数据对齐:不同频率的数据怎么合并
这是实战里最头疼的问题。你有日线数据,还有季度财报数据。怎么把两者对齐?
# 日线数据
daily = pd.DataFrame({'price': [100, 101, 102]},
index=pd.date_range('2024-01-02', periods=3))
# 季度数据
quarterly = pd.DataFrame({'eps': [1.2, 1.5]},
index=pd.to_datetime(['2024-03-31', '2024-06-30']))
# 向前填充:用最新的季度数据填充到每一天
daily['eps'] = quarterly['eps'].reindex(daily.index, method='ffill')
# 或者用merge_asof:按时间最近匹配
aligned = pd.merge_asof(daily, quarterly, left_index=True, right_index=True, direction='backward')
我强烈推荐 merge_asof。它比普通的merge更智能,会自动找到最近的时间点。做因子分析时,这个函数救了我无数次。
df.info() 和 df.describe() 摸清底细,然后对症下药。别一上来就咔咔删数据,有时候那些「异常值」恰恰是最值钱的信息。
公众号:蓝海资料掘金营,微信deep3321