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流程。每次拿到新数据,我就按这个步骤走一遍:

固收数据EDA核心流程 1. 数据加载 2. 数据概览 df.info() 看骨架 df.describe() 摸脉搏 3. 缺失值分析 missingno 矩阵图 缺失模式分析 4. 分布分析 直方图 + KDE 箱线图 5. 清洗策略决策 输出:清洗方案 + 特征工程方向

你看,整个流程是环环相扣的。从概览到缺失值,再到分布和异常值,最后形成清洗策略。每一步都在为下一步提供信息。

4.6 本章小结

EDA不是走过场,它是你跟数据建立信任的过程。我做了这么多年固收量化,最大的体会就是:数据不会骗人,但人容易骗自己。老老实实把 df.info()、df.describe()、缺失值可视化、分布图、箱线图都跑一遍,很多坑在开始建模之前就能发现。

嗯,下一节我们会讲如何处理这些发现的问题——缺失值填充、异常值处理、数据变换。但今天,先把EDA这一步做扎实了。