4. Pandas进阶与因子计算:从数据清洗到因子构建

各位同学,欢迎来到第四章。说实话,前几章我们还在玩一些基础操作,像是给数据排排队、筛筛选选。但从这一章开始,我们要真正进入量化因子的核心战场了。

我个人习惯把Pandas比作一把瑞士军刀——平时切个水果还行,但真要上战场,你得知道怎么用它的每一把刀刃。滚动窗口、分组聚合、因子构建,这些就是量化分析中最锋利的几把刀。

4.1 DataFrame高级操作:别只会loc和iloc

很多新手拿到DataFrame,翻来覆去就是df.loc[]df.iloc[]。嗯,这当然没错,但实战中远远不够。

条件筛选与布尔索引

我在项目中遇到过这样一个场景:需要筛选出某只股票在过去20天里,涨幅超过5%且成交量放大1.5倍以上的所有交易日。用链式条件写起来其实很简洁:

import pandas as pd
import numpy as np

# 模拟数据
dates = pd.date_range('2024-01-01', periods=100, freq='D')
df = pd.DataFrame({
    'close': np.random.randn(100).cumsum() + 100,
    'volume': np.random.randint(10000, 50000, 100)
}, index=dates)

# 计算涨跌幅和量比
df['return'] = df['close'].pct_change()
df['volume_ratio'] = df['volume'] / df['volume'].rolling(5).mean()

# 条件筛选
condition = (df['return'] > 0.05) & (df['volume_ratio'] > 1.5)
result = df[condition]
print(f"符合条件的交易日数: {len(result)}")
我的小技巧: 条件筛选时,记得用括号把每个条件包起来。不然Python的运算符优先级会给你一个「惊喜」——我曾经因为这个bug排查了整整一个下午。

多级索引与stack/unstack

做因子分析时,数据往往是三维的:时间、股票、因子值。Pandas的多级索引就是为这个场景设计的。

# 构建多级索引DataFrame
arrays = [
    ['2024-01-01', '2024-01-01', '2024-01-02', '2024-01-02'],
    ['000001.SZ', '600001.SH', '000001.SZ', '600001.SH']
]
index = pd.MultiIndex.from_arrays(arrays, names=['date', 'stock'])
df_multi = pd.DataFrame({
    'close': [10.5, 20.3, 10.8, 20.1],
    'volume': [10000, 20000, 12000, 18000]
}, index=index)

# unstack:将股票从行索引转到列索引
df_unstacked = df_multi.unstack('stock')
print(df_unstacked)

说白了,unstack就是把数据从「长格式」变成「宽格式」。我刚开始做因子分析时,经常在这两个格式之间来回转换,后来发现——其实大部分计算在长格式下更高效。

4.2 滚动窗口计算:时间序列的显微镜

滚动窗口,说白了就是给数据加一个「滑动窗口」,只看窗口内的数据。这在量化里太常用了——移动平均、波动率、动量因子,全都离不开它。

基础滚动操作

# 20日滚动均值
df['ma_20'] = df['close'].rolling(window=20).mean()

# 滚动标准差(波动率)
df['volatility_20'] = df['close'].rolling(window=20).std()

# 滚动最大值(用于计算最高价突破)
df['high_20'] = df['close'].rolling(window=20).max()
避坑指南: 我曾经犯过一个低级错误——用滚动窗口计算时,忘记处理前N天的NaN值。结果因子值里全是缺失值,回测直接报错。记住:rolling()默认会返回NaN,记得用dropna()或者fillna()处理。

自定义滚动函数

有时候内置的均值、标准差不够用。比如我想计算「过去20天里,涨幅超过3%的天数占比」,这就需要自定义函数了:

def up_ratio(series):
    """计算上涨比例"""
    return (series > 0.03).mean()

df['up_ratio_20'] = df['return'].rolling(window=20).apply(up_ratio)

你想想看,这种自定义滚动函数,其实就是把「人脑的判断逻辑」翻译成了代码。我经常用这种方式来构建一些「非标准」的因子,效果往往出奇的好。

4.3 分组聚合:多股票场景下的必备技能

单只股票的分析只是热身。真正的量化策略,面对的是成百上千只股票。这时候,groupby就是你的救星。

按股票分组计算因子

# 假设我们有多个股票的数据
df_all = pd.DataFrame({
    'stock': ['000001.SZ']*100 + ['600001.SH']*100,
    'date': pd.date_range('2024-01-01', periods=100, freq='D').tolist() * 2,
    'close': np.random.randn(200).cumsum() + 100,
    'volume': np.random.randint(10000, 50000, 200)
})

# 按股票分组,计算每只股票的20日滚动均值
df_all['ma_20'] = df_all.groupby('stock')['close'].transform(
    lambda x: x.rolling(20).mean()
)

这里有个关键点:transformapply的区别。我刚开始用的时候也搞混过——transform返回的是和原DataFrame一样长度的结果,而apply返回的是聚合后的结果。做因子计算时,99%的情况用transform就够了。

多因子分组聚合

# 按行业和日期分组,计算行业内各因子的排名
df_all['rank_momentum'] = df_all.groupby(['industry', 'date'])['momentum'].rank(pct=True)

嗯,这里要注意:rank(pct=True)返回的是百分位排名,范围是0到1。我习惯用这个来做「截面标准化」——说白了,就是看某只股票在同行中处于什么位置。

4.4 因子构建实战:从想法到代码

好了,前面铺垫了这么多,终于到了最核心的部分——因子构建。我给大家演示一个完整的因子构建流程,从原始数据到最终因子值。

案例:构建「量价背离因子」

这个因子的逻辑很简单:当价格上涨但成交量萎缩时,说明上涨动力不足,后续可能反转。反之亦然。

def build_volume_price_divergence_factor(df, window=20):
    """
    构建量价背离因子
    正值表示量价背离(上涨缩量),负值表示量价配合
    """
    # 计算价格变化方向
    df['price_direction'] = np.sign(df['close'].pct_change(window))
    
    # 计算成交量变化方向
    df['volume_direction'] = np.sign(
        df['volume'] / df['volume'].rolling(window).mean() - 1
    )
    
    # 量价背离因子 = 价格方向 × (-成交量方向)
    # 上涨(+1)且缩量(-1) → 背离(+1)
    # 上涨(+1)且放量(+1) → 配合(-1)
    df['divergence_factor'] = df['price_direction'] * (-df['volume_direction'])
    
    return df

# 应用因子
df = build_volume_price_divergence_factor(df)
print(df[['close', 'volume', 'divergence_factor']].tail(10))
核心要点: 因子构建的本质,就是把你的交易逻辑「翻译」成数学表达式。不要追求复杂的公式,简单有效的因子往往更稳定。我在实盘中见过太多「花里胡哨」的因子,回测曲线漂亮得不行,一上实盘就崩。

因子标准化处理

构建完因子后,通常还需要做标准化处理,让不同股票的因子值具有可比性:

def standardize_factor(df, factor_name, group_col='date'):
    """
    截面标准化:在每个时间截面上,将因子值转换为Z-score
    """
    df[factor_name + '_zscore'] = df.groupby(group_col)[factor_name].transform(
        lambda x: (x - x.mean()) / x.std()
    )
    return df

# 应用标准化
df = standardize_factor(df, 'divergence_factor')

你想想看,为什么要做截面标准化?因为不同股票的因子值分布可能完全不同。比如大盘股的波动率天然比小盘股低,如果不做标准化,你拿到的因子值其实没有可比性。

4.5 本章知识体系总览

为了让大家更直观地理解本章的知识结构,我画了一张图:

第四章:Pandas进阶与因子计算知识体系 DataFrame高级操作 • 条件筛选与布尔索引 • 多级索引构建 • stack/unstack转换 • 长格式与宽格式互转 滚动窗口计算 • 移动平均/标准差 • 滚动最大值/最小值 • 自定义滚动函数 • NaN值处理技巧 分组聚合 • groupby基础用法 • transform vs apply • 多因子分组排名 • 截面标准化处理 因子构建实战 • 量价背离因子案例 • 因子逻辑→数学表达 • Z-score标准化 • 因子值可比性处理 基础操作 → 时间序列分析 → 多股票处理 → 因子落地

这张图把本章的四个核心模块串起来了。你从左上角开始,先掌握DataFrame的高级操作,然后学习滚动窗口计算,接着搞定分组聚合,最后把这些技能组合起来构建因子。每一步都是下一步的基础。

说实话,因子挖掘这个领域,入门容易精通难。但只要你把Pandas的这些进阶操作练熟了,至少能解决80%的日常需求。剩下的20%,靠的是你对市场的理解和不断的试错。

好了,这一章的内容就到这里。记住:代码可以复制,但经验需要积累。多动手、多思考,你也能成为因子挖掘的高手。


公众号:蓝海资料掘金营,微信deep3321