第四章:金融数据预处理——从原始数据到干净的时间序列

金融预测这件事,说白了就是「垃圾进,垃圾出」。我见过太多人把精力花在模型调参上,结果数据本身就有问题。嗯,今天我们就来聊聊数据预处理这关怎么过。

核心流程:数据获取 → 缺失值处理 → 异常值检测 → 对数收益率计算 → 平稳性检验

数据获取 缺失值处理 异常值检测 对数收益率 ADF检验 不平稳则差分/变换 Yahoo Finance Tushare

4.1 数据获取:从哪里来?

我个人习惯用两个数据源:Yahoo Finance 和 Tushare。前者免费、方便,适合快速验证想法;后者数据质量更高,适合做严肃的回测。

Yahoo Finance 获取方式:

import yfinance as yf

# 下载贵州茅台历史数据
df = yf.download('600519.SS', start='2020-01-01', end='2024-01-01')
print(df.head())

Tushare 获取方式:

import tushare as ts

# 需要先注册获取 token
pro = ts.pro_api('your_token_here')
df = pro.daily(ts_code='600519.SH', start_date='20200101', end_date='20240101')
print(df.head())

我的经验:Yahoo Finance 偶尔会断连,尤其是国内网络环境。我建议你两个源都配好,一个挂了立刻切另一个。另外,Tushare 的 token 别硬编码在代码里,用环境变量存着。

4.2 缺失值处理:别让空值毁了你的模型

金融数据里缺失值很常见——节假日、停牌、数据源抽风。你想想看,如果模型训练时遇到 NaN,直接报错。所以第一步,先看看缺失情况。

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

# 方法一:前向填充(推荐用于金融时间序列)
df.fillna(method='ffill', inplace=True)

# 方法二:线性插值
df.interpolate(method='linear', inplace=True)

# 方法三:直接删除(慎用,会破坏时间连续性)
df.dropna(inplace=True)

避坑指南:我曾经在回测时用了「删除缺失值」的方法,结果发现连续几天缺失导致时间索引错位,回测结果完全失真。后来我改用前向填充,问题就解决了。记住:金融数据的时间连续性比你想的重要得多。

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

什么叫异常值?比如某只股票突然涨了 1000%,或者跌了 90%。这通常是数据错误,不是市场行为。我常用的方法有两种。

方法一:3σ 原则

import numpy as np

def detect_outliers_zscore(data, threshold=3):
    z_scores = np.abs((data - data.mean()) / data.std())
    return z_scores > threshold

outliers = detect_outliers_zscore(df['close'])
print(f"发现 {outliers.sum()} 个异常值")

方法二:IQR 四分位距法

Q1 = df['close'].quantile(0.25)
Q3 = df['close'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = (df['close'] < lower_bound) | (df['close'] > upper_bound)

我的习惯:对于异常值,我不会直接删除。我会先看看是不是除权除息导致的「假异常」。如果是,就保留;如果是数据错误,就用前后均值替换。嗯,这个判断需要一点经验。

4.4 对数收益率计算:为什么不用简单收益率?

简单收益率有个问题——它不对称。涨 10% 再跌 10%,你回不到原点。但对数收益率是时间可加的,而且更接近正态分布。说白了,做金融时间序列建模,对数收益率是标配。

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

# 去掉第一个 NaN
df.dropna(subset=['log_return'], inplace=True)

print(df[['close', 'log_return']].head())
日期 收盘价 简单收益率 对数收益率
2023-01-03 100.00 - -
2023-01-04 105.00 5.00% 4.88%
2023-01-05 99.75 -5.00% -5.13%

关键点:对数收益率可以直接相加得到多期收益率,而简单收益率不行。这在计算累积收益时特别方便。

4.5 平稳性检验(ADF):你的数据「稳」吗?

很多时间序列模型(比如 ARIMA)要求数据是平稳的。什么叫平稳?均值、方差不随时间变化。说白了,就是数据没有明显的趋势或季节性。

ADF 检验是判断平稳性的标准方法。原假设是「序列存在单位根(不平稳)」,p 值小于 0.05 就拒绝原假设,认为序列平稳。

from statsmodels.tsa.stattools import adfuller

def adf_test(series, title=''):
    result = adfuller(series.dropna())
    print(f'ADF 统计量: {result[0]:.4f}')
    print(f'p 值: {result[1]:.4f}')
    print(f'临界值:')
    for key, value in result[4].items():
        print(f'  {key}: {value:.4f}')
    
    if result[1] <= 0.05:
        print(f"结论: {title} 序列平稳")
    else:
        print(f"结论: {title} 序列不平稳,需要差分")

# 测试原始价格序列
adf_test(df['close'], '收盘价')

# 测试对数收益率序列
adf_test(df['log_return'], '对数收益率')

我曾经踩过的坑:有一次我拿价格序列直接建模,ADF 检验显示 p 值 0.99,明显不平稳。我心想「差分一下就好了」,结果差分后还是没通过。后来发现是数据里有结构性断点——某天突然跳空高开。这种情况需要先做断点检测,再分段处理。

如果 ADF 检验显示不平稳,通常的做法是:

  • 一阶差分: df['diff_1'] = df['close'].diff()
  • 对数变换: 我们已经做了对数收益率,通常这一步就够了
  • 季节差分: 如果数据有周期性,比如周内效应

我的建议:别一上来就差分。先看看对数收益率是否已经平稳。我处理过的 A 股数据中,90% 的对数收益率序列在 5% 显著性水平下是平稳的。如果还不平稳,再考虑差分。


好了,数据预处理这块就聊到这儿。记住:干净的数据是模型成功的一半。我见过太多人花 80% 的时间调模型,却只花 20% 的时间处理数据。嗯,方向反了。

蓝海数据掘金营,专注资料整理