4. 数据概览与EDA:先摸清数据的脾气
做固收量化,拿到数据的第一件事是什么?
我个人习惯,不是急着写策略,而是先跟数据「聊聊天」。说白了,就是做数据概览和探索性分析(EDA)。这一步做扎实了,后面能省下80%的坑。
4.1 快速扫描:df.info() 与 df.describe()
这两个函数,是我每天都要敲的。就像医生看病先量体温、测血压一样自然。
4.1.1 df.info() —— 看骨架
它会告诉你:数据有多少行、多少列、每列的类型、有没有缺失值。我一般这么用:
import pandas as pd
# 假设我们加载了信用债的日频数据
df = pd.read_csv('credit_bond_daily.csv')
df.info()
输出大概长这样:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 125000 entries, 0 to 124999
Data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 bond_code 125000 non-null object
1 trade_date 125000 non-null object
2 close_price 124800 non-null float64
3 volume 124500 non-null float64
4 ytm 123200 non-null float64
5 duration 122000 non-null float64
6 rating 125000 non-null object
7 issuer_type 125000 non-null object
8 coupon_rate 125000 non-null float64
9 remaining_m 125000 non-null float64
10 spread 121000 non-null float64
11 z_score 118000 non-null float64
dtypes: float64(8), object(4)
memory usage: 11.4 MB
关键信息解读:
- 12.5万行数据,12列 —— 规模适中
- close_price 有200个缺失值,ytm有1800个缺失值 —— 需要处理
- trade_date 是 object 类型 —— 记得转 datetime
- rating 和 issuer_type 是 object —— 分类变量,后面要编码
我的小习惯:每次加载完数据,先跑 df.info() 看一眼。如果发现某列缺失值超过30%,我会特别警惕。曾经有一次,我发现 spread 列缺失了40%,后来一查,是因为某些债券没有成交,报价缺失。这种情况,直接填充均值会出大问题。
4.1.2 df.describe() —— 摸脉搏
这个函数给出数值型列的统计摘要:均值、标准差、分位数、最大最小值。我一般会关注:
df.describe()
输出示例:
| close_price | volume | ytm | duration | spread | |
|---|---|---|---|---|---|
| count | 124800.0 | 124500.0 | 123200.0 | 122000.0 | 121000.0 |
| mean | 101.23 | 5200.45 | 3.45 | 4.12 | 1.23 |
| std | 2.15 | 3800.20 | 1.02 | 1.85 | 0.56 |
| min | 85.00 | 0.00 | 0.50 | 0.10 | -0.30 |
| 25% | 99.80 | 1200.00 | 2.80 | 2.50 | 0.85 |
| 50% | 101.50 | 4500.00 | 3.40 | 4.00 | 1.20 |
| 75% | 102.80 | 7800.00 | 4.10 | 5.60 | 1.60 |
| max | 115.00 | 25000.00 | 8.50 | 12.00 | 4.50 |
注意! 你看 volume 的最小值是0,这合理吗?在固收市场,有些债券当天确实没有成交,volume=0 是正常的。但 ytm 的最小值0.5%,最大值8.5%,跨度很大。我曾经遇到过一个极端值,ytm 显示为 -2%,后来发现是数据录入错误。所以,光看 describe 还不够,得结合业务逻辑判断。
4.2 缺失值可视化:哪里漏了,一目了然
df.info() 告诉我们缺失值的数量,但我想知道缺失值在时间轴上的分布。比如,是不是某段时间集中缺失?还是随机缺失?
我推荐用 missingno 库,几行代码就能画出缺失值矩阵:
import missingno as msno
import matplotlib.pyplot as plt
# 缺失值矩阵图
msno.matrix(df.sample(500)) # 样本太多时,抽一部分展示
plt.show()
# 缺失值柱状图
msno.bar(df)
plt.show()
矩阵图里,白色条纹就是缺失的位置。如果白色条纹集中在某几行,说明那几天的数据采集出了问题。如果白色条纹随机分布,那可能是个别债券的数据缺失。
避坑指南: 我曾经处理过一批银行间债券数据,用 missingno 一看,发现2018年6月到8月之间,spread 列几乎全是缺失的。后来一查,是因为那段时间市场流动性枯竭,很多债券没有报价。这种情况下,你不能简单填充,得考虑用插值法或者干脆剔除那段时间的数据。
4.3 分布图:看看数据长什么样
描述性统计只能给数字,但分布图能给你「感觉」。我一般会画直方图和核密度图:
import seaborn as sns
fig, axes = plt.subplots(2, 3, figsize=(15, 8))
# 画 ytm 的分布
sns.histplot(df['ytm'].dropna(), bins=50, kde=True, ax=axes[0,0])
axes[0,0].set_title('YTM 分布')
# 画 duration 的分布
sns.histplot(df['duration'].dropna(), bins=50, kde=True, ax=axes[0,1])
axes[0,1].set_title('久期分布')
# 画 spread 的分布
sns.histplot(df['spread'].dropna(), bins=50, kde=True, ax=axes[0,2])
axes[0,2].set_title('信用利差分布')
# 画 volume 的分布(注意取对数)
sns.histplot(np.log1p(df['volume'].dropna()), bins=50, kde=True, ax=axes[1,0])
axes[1,0].set_title('成交量(log)分布')
plt.tight_layout()
plt.show()
看分布图的时候,我主要关注三点:
- 是否正态? 如果偏态严重,后面建模可能需要做变换
- 有没有双峰? 比如 ytm 出现双峰,可能意味着市场上有两个不同的信用等级群体
- 有没有异常值? 分布尾巴特别长,就要小心了
实战经验: 我记得有一次做城投债分析,发现 ytm 的分布有两个明显的峰。一个在3%左右,一个在6%左右。后来一查,3%的是省级平台,6%的是区县级平台。这个发现直接影响了后面的特征工程——我把 issuer_type 拆成了更细的行政级别。
4.4 箱线图:揪出异常值的好帮手
箱线图能直观地展示数据的四分位数和异常值。我一般用它来快速筛查:
fig, axes = plt.subplots(1, 3, figsize=(15, 5))
sns.boxplot(y=df['ytm'], ax=axes[0])
axes[0].set_title('YTM 箱线图')
sns.boxplot(y=df['duration'], ax=axes[1])
axes[1].set_title('久期箱线图')
sns.boxplot(y=df['spread'], ax=axes[2])
axes[2].set_title('信用利差箱线图')
plt.show()
箱线图里,那些「小圆圈」就是异常值。但注意,在固收领域,有些「异常值」其实是合理的。比如,某只债券因为信用事件,ytm 突然飙升到8%,这在箱线图里会被标记为异常,但它反映了真实的市场情况。
千万不要机械地删除异常值! 我曾经犯过这个错。当时看到 spread 有几个点超过3个标准差,直接删掉了。结果回测的时候,策略表现特别好,但实盘一跑就崩。后来才发现,那些「异常值」恰恰是市场极端行情下的真实数据,删掉它们等于删掉了风险场景。
4.5 知识体系总览
下面这张图,是我自己总结的EDA流程。每次拿到新数据,我就按这个步骤走一遍:
你看,整个流程是环环相扣的。从概览到缺失值,再到分布和异常值,最后形成清洗策略。每一步都在为下一步提供信息。
4.6 本章小结
EDA不是走过场,它是你跟数据建立信任的过程。我做了这么多年固收量化,最大的体会就是:数据不会骗人,但人容易骗自己。老老实实把 df.info()、df.describe()、缺失值可视化、分布图、箱线图都跑一遍,很多坑在开始建模之前就能发现。
嗯,下一节我们会讲如何处理这些发现的问题——缺失值填充、异常值处理、数据变换。但今天,先把EDA这一步做扎实了。