3、数据探索性分析(EDA):描述性统计、时间序列可视化、分布分析、相关性分析、缺失值模式识别
拿到金融时间序列数据,别急着建模。我个人的习惯是——先跟数据「聊聊天」。
EDA 就是这场对话。说白了,就是通过统计和图表,摸清数据的脾气。你想想看,如果连数据长什么样都不知道,后面做的模型再花哨,也是空中楼阁。
3.1 描述性统计:先看数字怎么说
描述性统计是 EDA 的第一步。我一般会先跑一个 df.describe(),看看均值、标准差、分位数这些基础指标。
import pandas as pd
import numpy as np
# 加载数据
df = pd.read_csv('stock_data.csv', parse_dates=['date'], index_col='date')
# 描述性统计
desc = df.describe()
print(desc)
输出大概长这样:
| 指标 | close | volume | returns |
|---|---|---|---|
| count | 2520 | 2520 | 2519 |
| mean | 45.67 | 1.23e7 | 0.0003 |
| std | 8.91 | 5.67e6 | 0.021 |
| min | 28.34 | 2.10e6 | -0.098 |
| 25% | 38.12 | 7.89e6 | -0.012 |
| 50% | 44.56 | 1.10e7 | 0.001 |
| 75% | 52.34 | 1.56e7 | 0.014 |
| max | 68.90 | 3.45e7 | 0.112 |
嗯,这里要注意——金融数据经常有「尖峰厚尾」特征。正态分布那一套,在金融数据上往往不适用。我见过不少新手直接用 3σ 原则剔除异常值,结果把真实的极端行情给剔掉了。
3.2 时间序列可视化:一图胜千言
描述性统计只能告诉你「数字是什么」,但时间序列图能告诉你「故事是什么」。
import matplotlib.pyplot as plt
import seaborn as sns
plt.style.use('seaborn-v0_8-darkgrid')
fig, axes = plt.subplots(3, 1, figsize=(12, 10))
# 价格走势
axes[0].plot(df.index, df['close'], color='#2E86AB', linewidth=1.5)
axes[0].set_title('收盘价走势', fontsize=14)
axes[0].set_ylabel('价格')
# 收益率
axes[1].plot(df.index, df['returns'], color='#A23B72', linewidth=0.8, alpha=0.7)
axes[1].set_title('日收益率', fontsize=14)
axes[1].set_ylabel('收益率')
axes[1].axhline(y=0, color='gray', linestyle='--', linewidth=0.5)
# 成交量
axes[2].bar(df.index, df['volume'], color='#F18F01', width=1, alpha=0.6)
axes[2].set_title('成交量', fontsize=14)
axes[2].set_ylabel('成交量')
plt.tight_layout()
plt.show()
看这三张图,你能发现什么?
- 价格走势: 有没有明显的趋势?有没有突然的跳空缺口?
- 收益率: 波动率是否随时间变化?有没有极端值聚集的现象?
- 成交量: 有没有异常放量或缩量的日期?
3.3 分布分析:数据长什么样?
分布分析能告诉你数据的「身材」——是胖是瘦,是左偏还是右偏。
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
# 直方图 + 核密度估计
axes[0].hist(df['returns'], bins=50, density=True, alpha=0.6, color='#2E86AB')
df['returns'].plot.kde(ax=axes[0], color='#A23B72', linewidth=2)
axes[0].set_title('收益率分布', fontsize=14)
axes[0].set_xlabel('收益率')
# Q-Q图
from scipy import stats
stats.probplot(df['returns'].dropna(), dist='norm', plot=axes[1])
axes[1].set_title('Q-Q图(正态性检验)', fontsize=14)
plt.tight_layout()
plt.show()
直方图配合核密度曲线,能直观看出分布形态。Q-Q 图则用来检验数据是否服从正态分布。
金融数据有个特点——峰度通常大于 3。这意味着「肥尾」,极端值出现的概率比正态分布预测的要高。我做过一个统计,A 股日收益率超过 ±5% 的天数,比正态分布预测的多出 3 倍以上。
- 偏度(Skewness): 正偏表示右尾长,负偏表示左尾长。股票收益率通常有轻微的负偏。
- 峰度(Kurtosis): 大于 3 表示尖峰肥尾,小于 3 表示平峰瘦尾。
3.4 相关性分析:变量之间怎么勾搭?
金融数据里,变量之间往往存在复杂的相关性。比如,大盘涨的时候,个股大概率跟着涨。但有些相关性是虚假的,需要警惕。
# 计算相关系数矩阵
corr_matrix = df[['close', 'volume', 'returns', 'ma5', 'ma20', 'volatility']].corr()
# 热力图可视化
plt.figure(figsize=(10, 8))
sns.heatmap(corr_matrix, annot=True, cmap='RdBu_r', center=0,
square=True, linewidths=0.5, fmt='.2f')
plt.title('变量相关系数矩阵', fontsize=14)
plt.show()
看热力图时,我一般关注三点:
- 强相关对: 相关系数绝对值大于 0.7 的,要考虑是否有多重共线性问题。
- 与目标变量的关系: 哪些变量与收益率的相关性较高?这能帮你筛选特征。
- 意外的不相关: 理论上应该相关的变量却不相关,这可能是数据有问题。
3.5 缺失值模式识别:数据里的「黑洞」
缺失值在金融数据里太常见了。停牌、节假日、数据源故障,都会导致缺失。关键是要搞清楚缺失的模式——是随机缺失,还是系统缺失?
# 缺失值统计
missing_count = df.isnull().sum()
missing_ratio = df.isnull().mean() * 100
missing_df = pd.DataFrame({
'缺失数量': missing_count,
'缺失比例(%)': missing_ratio
})
print(missing_df[missing_df['缺失数量'] > 0])
# 缺失值可视化
plt.figure(figsize=(12, 4))
sns.heatmap(df.isnull(), cbar=False, cmap='viridis', yticklabels=False)
plt.title('缺失值模式热力图', fontsize=14)
plt.xlabel('变量')
plt.ylabel('样本')
plt.show()
缺失值热力图能直观展示缺失的模式。我总结了几种常见情况:
| 缺失模式 | 特征 | 常见原因 |
|---|---|---|
| 完全随机缺失(MCAR) | 缺失位置无规律 | 数据采集偶发故障 |
| 随机缺失(MAR) | 缺失与其他变量相关 | 低成交量日缺失某些指标 |
| 非随机缺失(MNAR) | 缺失与自身值相关 | 极端行情导致数据缺失 |
| 块状缺失 | 连续时间段内全部缺失 | 停牌、节假日 |
识别出缺失模式后,才能选择合适的处理方法。比如,MCAR 可以直接删除,MAR 可以用插值,MNAR 则需要更复杂的模型来处理。这些内容,我们会在后面的章节详细展开。
EDA 做得好,后面数据清洗和建模就顺风顺水。做不好,那就是「垃圾进,垃圾出」。我个人觉得,EDA 花的时间至少应该占整个项目时间的 30%。别急着跑模型,先把数据摸透了再说。