4. Python高频数据处理:Pandas与NumPy在毫秒级数据上的高效操作

做高频因子提取,说白了就是跟时间赛跑。

我刚开始接触毫秒级数据时,第一反应是「这玩意儿用Python能搞定?」。毕竟大家印象里Python慢,C++快。但后来我发现,只要把Pandas和NumPy用对了,处理百万级tick数据完全不是问题。关键在于——你得知道哪些操作是「快」的,哪些是「坑」。

4.1 数据加载:别让IO成为瓶颈

先说说数据怎么读进来。我个人习惯用pd.read_parquet(),而不是CSV。为什么?

  • Parquet是列式存储,读部分列时速度极快
  • 自带压缩,磁盘占用小
  • 支持按行组读取,适合分块处理

我在项目中遇到过这样的情况:一个月的tick数据,CSV要读40秒,Parquet只要3秒。差距就是这么明显。

核心原则: 能用二进制格式就别用文本格式。Parquet > HDF5 > Feather > CSV。
import pandas as pd
import numpy as np

# 推荐做法:只读需要的列
df = pd.read_parquet(
    'tick_data.parquet',
    columns=['time', 'price', 'volume', 'bid', 'ask']
)

# 设置时间索引
df['time'] = pd.to_datetime(df['time'])
df.set_index('time', inplace=True)

4.2 向量化操作:告别for循环

这是最容易被忽视的一点。很多人写量化代码,习惯用for i in range(len(df))。嗯,这在低频数据上还能忍,但在毫秒级数据上——你想想看,100万行数据,每行循环一次,光循环开销就够你喝一壶的。

NumPy和Pandas的核心优势就是向量化。说白了,就是用C语言级别的循环代替Python级别的循环。

我的经验: 能用df['col'].values就别用df['col']。直接操作NumPy数组比操作Pandas Series快3-5倍。
# ❌ 慢:Python循环
mid_prices = []
for i in range(len(df)):
    mid_prices.append((df['bid'].iloc[i] + df['ask'].iloc[i]) / 2)

# ✅ 快:向量化操作
mid_prices = (df['bid'].values + df['ask'].values) / 2

4.3 分组聚合:用groupby的诀窍

高频数据里,我们经常要按时间窗口做聚合。比如计算每秒的成交量加权均价(VWAP)。

我建议用pd.Grouper配合groupby,而不是手动切分时间窗口。为什么?因为Grouper底层做了优化,比你自己写循环快得多。

# 按秒计算VWAP
df['vwap'] = (df['price'] * df['volume']).groupby(
    pd.Grouper(freq='1s')
).transform('sum') / df['volume'].groupby(
    pd.Grouper(freq='1s')
).transform('sum')

这里有个坑:transformagg慢,但能保留原始行数。如果你不需要保留原始行数,用agg更快。

我曾经踩过的坑:resample做高频聚合时,如果数据有缺失时间戳,记得用fill_method或者先asfreq补全。否则结果会莫名其妙地少几行。

4.4 滚动窗口:别用rolling做高频

嗯,这里我要说点不一样的。df['price'].rolling(100).mean()看起来很优雅,但在毫秒级数据上,它其实很慢。为什么?因为rolling每次都要重新计算窗口内的值。

我个人的做法是:用NumPy的np.convolve或者np.cumsum来实现滑动窗口。

# 用cumsum实现快速滚动均值
def fast_rolling_mean(arr, window):
    cumsum = np.cumsum(np.insert(arr, 0, 0))
    return (cumsum[window:] - cumsum[:-window]) / window

# 比rolling快5倍以上
prices = df['price'].values
df['ma_100'] = np.concatenate([
    np.full(window-1, np.nan),
    fast_rolling_mean(prices, 100)
])

4.5 内存优化:别让数据撑爆内存

高频数据最头疼的问题就是内存。一天的数据可能就几个G,一个月呢?一年呢?

我建议做两件事:

  • 降精度: 价格用float32代替float64,成交量用int32代替int64
  • 分块处理:pd.read_parquet(..., filters=...)按日期过滤,一次只处理一天的数据
# 内存优化示例
df = df.astype({
    'price': 'float32',
    'volume': 'int32',
    'bid': 'float32',
    'ask': 'float32'
})

# 内存占用直接减半
print(f"内存占用: {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB")

4.6 核心知识体系

下面这张图是我自己总结的高频数据处理流程,你可以看看:

高频数据处理核心流程 数据加载 Parquet / HDF5 向量化操作 NumPy数组运算 分组聚合 Grouper / resample 滚动窗口 内存优化 降精度 / 分块处理 因子提取 高频因子计算 每个环节都有对应的优化技巧,核心是减少Python循环,多用C层运算 性能对比(100万行tick数据) ❌ for循环: ~12秒 ✅ 向量化: ~0.3秒 ❌ rolling: ~8秒 ✅ cumsum: ~0.1秒

4.7 实战:提取tick级因子

最后,我们把这些技巧串起来,写一个完整的因子提取函数。

def extract_tick_factors(df: pd.DataFrame) -> pd.DataFrame:
    """
    从tick数据中提取高频因子
    """
    # 1. 提取NumPy数组(加速)
    prices = df['price'].values.astype('float32')
    volumes = df['volume'].values.astype('int32')
    bids = df['bid'].values.astype('float32')
    asks = df['ask'].values.astype('float32')
    
    # 2. 计算中间价
    mid_prices = (bids + asks) / 2
    
    # 3. 计算价格变化(向量化)
    price_changes = np.diff(prices, prepend=prices[0])
    
    # 4. 计算成交量加权价格(用cumsum加速)
    cum_price_vol = np.cumsum(prices * volumes)
    cum_vol = np.cumsum(volumes)
    vwap = cum_price_vol / cum_vol
    
    # 5. 计算买卖价差
    spread = asks - bids
    
    # 6. 组装结果
    factors = pd.DataFrame({
        'mid_price': mid_prices,
        'price_change': price_changes,
        'vwap': vwap,
        'spread': spread
    }, index=df.index)
    
    return factors

# 使用示例
factors = extract_tick_factors(df)
print(f"因子提取完成,耗时: {time.time() - start:.3f}秒")
小技巧: 如果你发现某个因子计算特别慢,试试用%timeit逐行分析。我经常发现瓶颈在某个不起眼的.apply()调用上——换成向量化后速度提升几十倍。

好了,以上就是高频数据处理的核心技巧。说白了就是三句话:用二进制格式读数据,用向量化代替循环,用NumPy数组代替Pandas Series。做到这三点,你的代码就能在毫秒级数据上跑得飞快。

记住,高频数据处理的本质不是「写更快的Python」,而是「让C语言帮你干活」。你想想看,NumPy和Pandas底层都是C写的,你只要把数据喂给它们,剩下的交给底层就好。

专注资料整理