4. 数据清洗:处理缺失值、异常值检测与处理、数据对齐与重采样

数据清洗这步,说白了就是给原始数据「洗澡」。我见过太多人,模型调得花里胡哨,结果数据一塌糊涂,最后跑出来的结果连自己都不敢信。嗯,咱们今天就把这块硬骨头啃下来。

核心观点:脏数据进,脏数据出。你花在清洗上的每一分钟,都会在模型性能上得到回报。

4.1 缺失值处理:别让「空」坑了你

美股数据里,缺失值太常见了。比如节假日没交易,或者某些小盘股流动性差,某天直接没成交。我个人习惯,拿到数据第一件事就是检查缺失情况。

import pandas as pd
import numpy as np

# 加载数据
df = pd.read_csv('sp500_data.csv', index_col='Date', parse_dates=True)

# 检查缺失值
print(df.isnull().sum())

# 缺失比例
print(df.isnull().mean() * 100)

处理缺失值,我一般分三种情况:

  • 直接删除:缺失比例小于5%,且数据量够大。简单粗暴,但要注意别把时间序列搞断了。
  • 向前填充:用上一个有效值填充。适合价格数据,因为股价变化是连续的。我在做日内策略时常用这个。
  • 插值法:线性插值或时间插值。适合成交量这类波动较大的指标。

我的经验:千万别用均值填充时间序列数据!我曾经犯过这个错,结果模型学到的全是假规律。股价有趋势,均值填充会破坏这种结构。

# 向前填充(推荐用于价格)
df['Close'].fillna(method='ffill', inplace=True)

# 线性插值(适合成交量)
df['Volume'].interpolate(method='linear', inplace=True)

# 时间插值(更精确)
df['Volume'].interpolate(method='time', inplace=True)

4.2 异常值检测:揪出那些「离谱」的数据点

异常值是什么?就是那些明显偏离正常范围的数据。比如某只股票一天涨了1000%,或者成交量突然变成平时的100倍。这些多半是数据录入错误,或者极端事件。

我常用的方法有三种:

方法 适用场景 优缺点
Z-Score 正态分布数据 简单,但对极端值敏感
IQR(四分位距) 非正态分布 稳健,不受极端值影响
滚动窗口法 时间序列 考虑局部趋势,适合金融数据

我个人最常用的是滚动窗口法。为什么?因为金融数据有波动聚集性,今天涨10%可能正常,但放在过去30天里可能就是异常。

# 滚动窗口异常检测
window = 20
mean = df['Close'].rolling(window=window).mean()
std = df['Close'].rolling(window=window).std()

# 3倍标准差作为阈值
df['is_outlier'] = (abs(df['Close'] - mean) > 3 * std)

# 处理异常值:用中位数替换
df.loc[df['is_outlier'], 'Close'] = df['Close'].rolling(window=window).median()

注意:不要机械地删除所有异常值。有些异常值其实是市场信号,比如财报发布后的跳空缺口。我曾经因为误删这些「真异常」,导致模型错过了重要的价格发现。

4.3 数据对齐:让不同频率的数据「对上话」

做多因子模型时,你经常会遇到这种情况:价格数据是日频的,宏观经济数据是月频的,新闻情绪数据可能是不规则的。怎么把它们对齐?

说白了,就是让所有数据都站在同一个时间点上。我一般这样做:

  • 低频转高频:用向前填充。比如把月度的GDP数据填充到每一天。
  • 高频转低频:用聚合函数。比如把日频收益率聚合成周频。
  • 对齐索引:确保所有数据框的索引一致。
# 假设有两个数据框
price_df = pd.DataFrame(...)  # 日频
macro_df = pd.DataFrame(...)  # 月频

# 低频转高频:向前填充
macro_daily = macro_df.reindex(price_df.index, method='ffill')

# 对齐索引
aligned_df = pd.concat([price_df, macro_daily], axis=1).dropna()

避坑指南:我曾经在回测时用了未来数据,就是因为对齐没做好。月频数据发布有滞后,你拿到数据时已经是下个月了。记得用 shift() 把数据往后挪一期。

4.4 重采样:改变数据的时间粒度

重采样就是把数据从一种频率变成另一种。比如把5分钟K线变成日K线,或者把日数据变成周数据。

为什么需要重采样?

  • 不同策略需要不同频率的数据
  • 降低噪声,看清长期趋势
  • 匹配不同数据源的频率
# 日频转周频
weekly_df = df.resample('W').agg({
    'Open': 'first',
    'High': 'max',
    'Low': 'min',
    'Close': 'last',
    'Volume': 'sum'
})

# 日频转月频
monthly_df = df.resample('M').agg({
    'Open': 'first',
    'High': 'max',
    'Low': 'min',
    'Close': 'last',
    'Volume': 'sum'
})

嗯,这里要注意:重采样时聚合函数的选择很关键。开盘价用第一个,收盘价用最后一个,最高价用最大值,成交量用总和。这些是行业惯例,别搞混了。

知识体系总览

下面这张图,把数据清洗的整个流程串起来了。你想想看,从原始数据到干净数据,每一步都有坑,但每一步也都有解法。

数据清洗流程 原始数据 缺失值处理 删除/填充/插值 异常值检测 Z-Score/IQR/滚动 干净数据 数据对齐 低频↔高频 重采样 日↔周↔月 数据清洗不是一次性的,而是迭代过程 每次添加新数据源,都需要重新走一遍流程

数据清洗这件事,说白了就是「磨刀不误砍柴工」。我见过太多人急着建模,结果被脏数据坑得怀疑人生。你想想看,如果连数据都没洗干净,模型再花哨又有什么用?

最后说一句:清洗后的数据一定要做可视化检查。画个K线图,看看有没有明显的跳空或异常。眼睛有时候比统计指标更管用。

专注资料整理