4、探索性数据分析(EDA):描述性统计、数据可视化、相关性分析、异常值检测
做投研分析,拿到数据的第一件事是什么?
不是建模,不是调参。是看数据。
我见过太多人,上来就扔进模型跑,结果跑出来一堆垃圾。为什么?因为数据里藏着各种坑——缺失值、异常点、分布偏态。你不先摸清楚数据的脾气,后面所有工作都是白搭。
这一章,我们就聊聊EDA。说白了,就是和数据「交朋友」的过程。
核心观点:EDA不是一次性工作,而是贯穿整个分析流程的「体检」。每次建模前,至少做一轮完整的描述性统计+可视化检查。
4.1 描述性统计:先给数据「称个体重」
描述性统计,就是给数据做个体检。看看它胖不胖(均值)、分布均匀不(标准差)、有没有极端值(最大最小值)。
我个人习惯,拿到DataFrame第一件事就是跑 df.describe()。几秒钟,心里就有底了。
import pandas as pd
import numpy as np
# 模拟一份投研数据
np.random.seed(42)
data = {
'市盈率': np.random.normal(25, 10, 1000),
'市净率': np.random.normal(3, 1.5, 1000),
'ROE': np.random.normal(15, 5, 1000),
'营收增长率': np.random.normal(10, 8, 1000)
}
df = pd.DataFrame(data)
# 描述性统计
desc = df.describe()
print(desc)
输出结果长这样:
| 指标 | 市盈率 | 市净率 | ROE | 营收增长率 |
|---|---|---|---|---|
| count | 1000.0 | 1000.0 | 1000.0 | 1000.0 |
| mean | 25.12 | 3.02 | 14.98 | 10.05 |
| std | 9.87 | 1.48 | 4.95 | 7.92 |
| min | -2.34 | -1.12 | 0.23 | -15.67 |
| 25% | 18.45 | 2.01 | 11.67 | 4.78 |
| 50% | 25.08 | 3.00 | 15.02 | 10.12 |
| 75% | 31.67 | 4.03 | 18.34 | 15.45 |
| max | 58.92 | 8.76 | 32.11 | 38.90 |
小技巧:看均值和中位数。如果两者差距大,说明数据有偏态。比如市盈率均值25.12,中位数25.08,差不多——说明分布还算对称。
嗯,这里要注意。市盈率最小值是-2.34?负的市盈率意味着公司亏损。这在A股里常见,但如果你做的是价值因子选股,负市盈率的股票通常要剔除。
4.2 数据可视化:一张图胜过千行代码
描述性统计只能告诉你「数字」,但图能告诉你「形状」。
我习惯用Matplotlib打底,Seaborn美化。Seaborn的默认配色和统计功能,做EDA简直不要太顺手。
4.2.1 直方图 + 密度曲线:看分布
import matplotlib.pyplot as plt
import seaborn as sns
plt.figure(figsize=(10, 6))
sns.histplot(df['市盈率'], kde=True, bins=30, color='steelblue')
plt.title('市盈率分布直方图', fontsize=14)
plt.xlabel('市盈率')
plt.ylabel('频数')
plt.show()
这张图能告诉你什么?
- 数据是不是正态分布?——投研数据很少完美正态,但至少别太离谱
- 有没有双峰?——双峰可能意味着数据里藏着两个不同的群体
- 尾巴长不长?——长尾意味着极端值多
避坑指南:我曾经遇到一个数据集,营收增长率看起来很正常,但画了直方图才发现——数据在0附近有个巨大的尖峰,原来是很多公司营收为0(刚成立或已退市)。如果只看描述性统计,根本发现不了这个问题。
4.2.2 箱线图:一眼揪出异常值
plt.figure(figsize=(10, 6))
sns.boxplot(data=df[['市盈率', '市净率', 'ROE', '营收增长率']], palette='Set2')
plt.title('各指标箱线图', fontsize=14)
plt.show()
箱线图的好处是——异常值直接给你标成小圆点。哪个指标异常点多,一目了然。
4.2.3 散点图矩阵:看变量间的关系
sns.pairplot(df, diag_kind='kde', plot_kws={'alpha': 0.5})
plt.show()
这个图我几乎每次EDA都会用。对角线是分布,非对角线是两两散点图。一眼就能看出哪些变量线性相关,哪些有非线性关系。
4.3 相关性分析:谁和谁「有关系」?
做投研,我们经常想知道:市盈率和ROE有关系吗?营收增长率和市净率呢?
相关性分析就是回答这个问题的。
# 计算相关系数矩阵
corr_matrix = df.corr(method='pearson')
print(corr_matrix)
# 热力图可视化
plt.figure(figsize=(8, 6))
sns.heatmap(corr_matrix, annot=True, cmap='RdBu_r', center=0,
square=True, linewidths=0.5)
plt.title('Pearson相关系数热力图', fontsize=14)
plt.show()
| 市盈率 | 市净率 | ROE | 营收增长率 | |
|---|---|---|---|---|
| 市盈率 | 1.00 | 0.12 | -0.08 | 0.05 |
| 市净率 | 0.12 | 1.00 | 0.35 | 0.18 |
| ROE | -0.08 | 0.35 | 1.00 | 0.22 |
| 营收增长率 | 0.05 | 0.18 | 0.22 | 1.00 |
关键解读:
- 相关系数范围[-1, 1]。0.7以上算强相关,0.3-0.7中等,0.3以下弱相关
- ROE和市净率相关系数0.35——有点关系,但不算强。这符合直觉:高ROE的公司通常市净率也高
- 市盈率和ROE是负的(-0.08)——几乎没关系。说明市盈率更多反映的是市场情绪,而非盈利能力
个人经验:别只看Pearson。如果数据不是线性的,试试Spearman秩相关系数。我在分析某个行业数据时,Pearson显示相关性为0.1,但Spearman是0.6——原来是存在单调非线性关系。
# Spearman相关系数
corr_spearman = df.corr(method='spearman')
print(corr_spearman)
4.4 异常值检测:揪出「害群之马」
异常值,说白了就是「不太正常」的数据点。在投研里,异常值可能是:
- 数据录入错误(比如市盈率写成9999)
- 特殊事件(比如某公司突然暴雷,市盈率变负)
- 真实的极端值(比如茅台和垃圾股放在一起)
怎么检测?我常用的方法有三种。
4.4.1 Z-score方法
Z-score衡量一个数据点偏离均值多少个标准差。通常认为|Z| > 3就是异常。
from scipy import stats
z_scores = np.abs(stats.zscore(df['市盈率']))
outliers_z = df[z_scores > 3]
print(f"Z-score方法检测到 {len(outliers_z)} 个异常值")
4.4.2 IQR方法(四分位距法)
这个方法更稳健,不受极端值影响。IQR = Q3 - Q1,异常值定义为:
- 小于 Q1 - 1.5 * IQR
- 大于 Q3 + 1.5 * IQR
Q1 = df['市盈率'].quantile(0.25)
Q3 = df['市盈率'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers_iqr = df[(df['市盈率'] < lower_bound) | (df['市盈率'] > upper_bound)]
print(f"IQR方法检测到 {len(outliers_iqr)} 个异常值")
避坑指南:我曾经处理一个金融数据,用Z-score检测出200多个异常值,吓我一跳。后来发现——数据本身是厚尾分布(金融数据常见),Z=3的标准太严格了。对于金融数据,我建议用IQR方法,或者把Z-score阈值放宽到3.5甚至4。
4.4.3 可视化检测:箱线图 + 散点图
嗯,其实前面画的箱线图已经帮你标出异常值了。但有时候,异常值不是孤立的点,而是一小群——这时候用散点图更有效。
plt.figure(figsize=(10, 6))
sns.scatterplot(x='ROE', y='市盈率', data=df, alpha=0.6)
plt.axhline(y=upper_bound, color='r', linestyle='--', label='上界')
plt.axhline(y=lower_bound, color='r', linestyle='--', label='下界')
plt.title('ROE vs 市盈率 - 异常值检测', fontsize=14)
plt.legend()
plt.show()
你想想看,如果某个点ROE很高(30%+),但市盈率很低(5倍以下),这正常吗?可能正常——说明市场低估了它。也可能不正常——数据错了。需要结合业务判断。
4.5 处理异常值:不是所有异常都要删
检测出来之后怎么办?我的原则是:
- 先理解,再处理。 别上来就删。问问自己:这个异常值合理吗?
- 如果是录入错误——直接删除或修正
- 如果是真实极端值——考虑是否保留。比如做多因子模型,极端值可能会扭曲结果,建议做缩尾处理(Winsorize)
- 如果异常值代表一个子群体——比如ST股票和正常股票混在一起,建议分开分析
# 缩尾处理:将超出上下界的值替换为边界值
def winsorize_series(s, limits=(0.01, 0.01)):
lower = s.quantile(limits[0])
upper = s.quantile(1 - limits[1])
return s.clip(lower, upper)
df['市盈率_缩尾'] = winsorize_series(df['市盈率'])
总结一下:EDA不是走过场,它是整个投研流程的基石。描述性统计让你「心中有数」,可视化让你「眼见为实」,相关性分析帮你「理清关系」,异常值检测帮你「排雷」。每一步都做扎实了,后面的建模才能稳。
我个人习惯,每次拿到新数据,至少花30%的时间做EDA。别急着建模,先和数据「聊聊天」。你会发现,数据会告诉你很多意想不到的东西。