数据清洗与预处理:处理停牌、异常值、缺失值,统一时间索引,计算收益率、对数收益率

在量化分析中,数据是策略的基石。原始行情数据往往包含停牌导致的长时间无交易、异常跳空、缺失记录、时间索引错位等问题。如果直接将这些数据送入回测框架,轻则导致信号计算错误,重则产生未来函数或幸存者偏差。本章将系统讲解数据清洗的标准流程,并深入解析收益率与对数收益率的计算逻辑及其在量化建模中的适用场景。

数据清洗与预处理核心链路 原始行情数据 日线/分钟线/复权因子 数据清洗 停牌识别 | 异常值剔除 缺失值填充 | 时间索引统一 指标计算 简单收益率 对数收益率 回测 就绪 停牌处理策略 前向填充 / 剔除 / 标记 取决于策略频率与逻辑 异常值识别 MAD / Z-Score / 涨跌停 结合市场规则判断 统一时间索引 交易日历对齐 → 重采样 → 去重 → 排序 简单收益率:R = (P_t - P_{t-1}) / P_{t-1} | 对数收益率:r = ln(P_t / P_{t-1})

上图展示了从原始数据到回测就绪数据的完整链路。数据清洗并非简单的“删除空值”,而是需要结合市场规则、策略频率和金融逻辑进行综合判断。下面我们将逐一拆解每个环节的核心原理与实操方法。

4.1 停牌数据处理:识别、标记与填充策略

4.1.1 核心原理

停牌是指股票因重大事项、涨跌停或临时公告等原因暂停交易。在日线数据中,停牌通常表现为当日无成交记录(成交量、成交额均为0或NaN),但部分数据源会保留一条“空记录”或直接缺失该行。停牌数据对量化策略的影响主要体现在三个方面:

  • 信号失真:基于均线、动量等指标计算的信号在停牌期间无法真实反映市场状态。
  • 收益率计算错误:若直接使用停牌日收盘价计算收益率,会产生大量0收益率或异常跳变。
  • 未来函数风险:若未正确处理停牌,回测时可能错误地使用停牌日之后的数据进行交易决策。

4.1.2 判断方法与操作流程

处理停牌数据的第一步是准确识别。推荐使用以下流程:

  1. 检查成交量与成交额:若某交易日成交量为0或NaN,且非节假日,则判定为停牌。
  2. 检查价格变动:若当日收盘价与前一日完全相同且成交量极低(如低于历史均值的1%),需结合公告确认是否停牌。
  3. 使用交易日历对齐:将数据与交易所官方交易日历对齐,缺失的交易日即为停牌日。

识别停牌后,根据策略需求选择处理方式:

策略类型推荐处理方式原因
日频低频策略前向填充(ffill)假设停牌期间价格不变,保持数据连续性
事件驱动策略剔除停牌日避免停牌事件干扰信号计算
高频/分钟策略标记并单独处理分钟级停牌需精确到时间戳
风险提示:前向填充仅适用于价格数据,对于成交量、成交额等应填充为0或NaN,不可直接填充前值,否则会严重高估流动性。

4.1.3 案例拆解:处理连续停牌股票

假设我们获取了某股票2024年1月1日至3月31日的日线数据,其中1月15日至1月26日因重大资产重组停牌10个交易日。原始数据中,这10天完全缺失。我们的处理步骤如下:

import pandas as pd
import numpy as np

# 假设df包含date, open, high, low, close, volume列
df['date'] = pd.to_datetime(df['date'])
df.set_index('date', inplace=True)

# 生成完整交易日历(使用交易所日历)
trade_cal = pd.date_range(start='2024-01-01', end='2024-03-31', freq='B')  # 简化示例,实际应使用exchange_calendars

# 重索引,缺失日期自动变为NaN
df = df.reindex(trade_cal)

# 识别停牌日(volume为NaN或0)
df['is_suspended'] = df['volume'].isna() | (df['volume'] == 0)

# 价格前向填充,成交量填充0
df['close'] = df['close'].ffill()
df['volume'] = df['volume'].fillna(0)

# 标记停牌日,供后续策略判断
df.loc[df['is_suspended'], 'signal'] = 0  # 停牌期间不产生交易信号

通过上述操作,我们保留了停牌期间的价格连续性,同时明确标记了停牌状态,避免策略在停牌日发出错误交易指令。复盘经验:在回测中,务必在停牌日禁止任何开仓或平仓操作,否则会产生“幽灵成交”。

4.2 异常值检测与处理:识别跳空、涨跌停与数据错误

4.2.1 核心原理

异常值是指明显偏离正常分布的数据点,可能源于数据源错误、除权除息未复权、或真实的极端行情(如涨停/跌停)。区分“真实异常”与“数据错误”是本节的关键。常见的异常类型包括:

  • 价格跳空:单日涨跌幅超过20%(A股主板),可能为数据错误或极端事件。
  • 负价格/零价格:明显的数据错误。
  • 成交量突变:成交量突然放大或缩小至历史均值的100倍以上。

4.2.2 检测方法

推荐使用“规则+统计”双通道检测:

方法公式/阈值适用场景
涨跌停规则涨跌幅 > 10%(主板)或 > 20%(创业板/科创板)快速识别极端行情
MAD(中位差)|x - median| > 5 * MAD稳健的统计异常检测
Z-Score|z| > 3正态分布假设下的检测
经验提醒:对于A股数据,涨跌停是正常现象,不应直接剔除。正确的做法是保留涨跌停数据,但需检查是否伴随成交量异常(如涨停但成交量极小,可能为数据错误)。

4.2.3 实战场景:处理除权除息导致的“假异常”

某股票在2024年5月20日实施10送5派2元的分红方案,除权后股价从20元变为13.2元,单日跌幅达34%。若未使用复权数据,该日会被误判为异常值。解决方案:

# 使用前复权或后复权数据
# 若使用前复权,历史价格会被调整,保证收益率计算正确
df_adj = df['close'] * df['adj_factor']  # adj_factor为复权因子

# 检测异常时,应基于复权后的价格计算涨跌幅
daily_ret = df_adj.pct_change()
outliers = daily_ret[abs(daily_ret) > 0.2]  # 复权后仍超过20%才视为异常

复盘经验:在数据清洗阶段,务必先完成复权处理,再进行异常值检测。否则,除权除息日会被误判为异常值而错误剔除,导致后续收益率计算出现偏差。

4.3 缺失值处理与时间索引统一

4.3.1 缺失值来源与处理策略

缺失值不仅来自停牌,还可能源于数据源本身的不完整(如部分交易日数据未下载)。处理策略需根据缺失比例和位置决定:

  • 少量缺失(<1%):线性插值或前向填充。
  • 连续缺失(>5个交易日):需检查是否停牌,若为停牌按4.1处理。
  • 关键位置缺失(如月末):若影响财务指标计算,建议重新获取数据。

4.3.2 统一时间索引的完整流程

  1. 标准化日期格式:统一为YYYY-MM-DD格式。
  2. 对齐交易日历:使用交易所官方日历,剔除周末和节假日。
  3. 去重与排序:检查是否有重复日期,按时间升序排列。
  4. 重采样:若需将日线转为周线或月线,使用resample方法。
# 统一时间索引示例
df.index = pd.to_datetime(df.index)
df = df[~df.index.duplicated(keep='first')]  # 去重
df = df.sort_index()  # 排序

# 对齐交易日历(使用exchange_calendars库)
import exchange_calendars as xcals
cn_cal = xcals.get_calendar('XSHG')
valid_days = cn_cal.sessions_in_range('2024-01-01', '2024-12-31')
df = df.reindex(valid_days)
关键点:统一时间索引是防止未来函数的重要屏障。若数据中存在非交易日记录(如周末数据),必须剔除或重采样,否则回测时会在非交易日产生虚假信号。

4.4 收益率与对数收益率的计算及应用

4.4.1 核心原理与数学定义

收益率是量化分析中最基础的指标。简单收益率(算术收益率)和对数收益率各有优劣:

类型公式优点缺点
简单收益率R_t = (P_t - P_{t-1}) / P_{t-1}直观,易于理解不可加性(多期收益率需连乘)
对数收益率r_t = ln(P_t / P_{t-1})可加性(多期收益率直接相加)数值较小,解释性稍弱

在量化建模中,对数收益率更受青睐,原因在于:

  • 统计性质:对数收益率近似正态分布,便于使用参数化模型(如GARCH)。
  • 时间可加性:计算多期收益率时,只需将单期对数收益率相加,避免连乘误差。
  • 数值稳定性:对数变换可压缩极端值的影响。

4.4.2 计算实操与注意事项

# 计算简单收益率
df['simple_ret'] = df['close'].pct_change()

# 计算对数收益率
df['log_ret'] = np.log(df['close'] / df['close'].shift(1))

# 多期收益率计算
# 简单收益率: (1+R1)*(1+R2) - 1
multi_simple = (1 + df['simple_ret']).prod() - 1
# 对数收益率: sum(r1 + r2)
multi_log = df['log_ret'].sum()
易错点:计算收益率前必须确保价格序列已复权。若使用未复权数据,除权除息日会产生虚假的负收益率,导致策略信号错误。另外,pct_change()默认会删除NaN,但需注意首日收益率为NaN,应使用dropna()或fillna(0)处理。

4.4.3 案例拆解:收益率计算在回测中的应用

假设我们构建了一个简单的双均线策略(5日均线上穿20日均线买入),回测周期为2024年1月至6月。关键步骤:

  1. 数据清洗后,计算对数收益率作为策略绩效评估基准。
  2. 策略信号基于复权后的收盘价计算均线。
  3. 交易成本按单边0.1%计算,在收益率中扣除。
# 策略绩效计算
df['strategy_ret'] = df['signal'].shift(1) * df['log_ret']  # 信号滞后一天避免未来函数
df['strategy_ret'] -= 0.001  # 扣除交易成本(简化处理)

# 累计收益
cumulative_ret = df['strategy_ret'].sum()
# 年化波动率
annual_vol = df['strategy_ret'].std() * np.sqrt(252)

复盘经验:使用对数收益率计算累计收益时,直接求和即可;若使用简单收益率,则需连乘。在回测框架中,统一使用对数收益率可以简化多资产组合的收益聚合计算。

4.5 常见误区与总结复盘

误区后果正确做法
直接删除所有含NaN的行丢失停牌信息,导致时间轴断裂区分停牌与缺失,分别处理
未复权就计算收益率除权日产生虚假负收益先复权,再计算
使用未对齐的时间索引信号与价格错位,产生未来函数统一交易日历,严格对齐
忽略涨跌停数据策略在极端行情下失效保留涨跌停,但需检查流动性
核心总结:数据清洗的终极目标是确保回测结果的真实性与可复现性。每一步处理都应记录在案(如添加is_suspended列),以便后续审计。收益率计算中,对数收益率因其可加性和统计优良性,应作为默认选择。最后,务必在清洗完成后进行数据可视化检查(如绘制价格曲线),从宏观层面确认数据质量。