4、探索性数据分析(EDA):描述性统计、数据可视化、相关性分析、异常值检测

做投研分析,拿到数据的第一件事是什么?

不是建模,不是调参。是看数据

我见过太多人,上来就扔进模型跑,结果跑出来一堆垃圾。为什么?因为数据里藏着各种坑——缺失值、异常点、分布偏态。你不先摸清楚数据的脾气,后面所有工作都是白搭。

这一章,我们就聊聊EDA。说白了,就是和数据「交朋友」的过程。

核心观点:EDA不是一次性工作,而是贯穿整个分析流程的「体检」。每次建模前,至少做一轮完整的描述性统计+可视化检查。

EDA 核心流程框架 描述性统计 均值、中位数、标准差 数据可视化 直方图、箱线图、散点图 相关性分析 Pearson/Spearman 异常值检测 IQR / Z-score / 箱线图 数据清洗 特征工程 模型选择 → 最终决策

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营收增长率
count1000.01000.01000.01000.0
mean25.123.0214.9810.05
std9.871.484.957.92
min-2.34-1.120.23-15.67
25%18.452.0111.674.78
50%25.083.0015.0210.12
75%31.674.0318.3415.45
max58.928.7632.1138.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.000.12-0.080.05
市净率0.121.000.350.18
ROE-0.080.351.000.22
营收增长率0.050.180.221.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 处理异常值:不是所有异常都要删

检测出来之后怎么办?我的原则是:

  1. 先理解,再处理。 别上来就删。问问自己:这个异常值合理吗?
  2. 如果是录入错误——直接删除或修正
  3. 如果是真实极端值——考虑是否保留。比如做多因子模型,极端值可能会扭曲结果,建议做缩尾处理(Winsorize)
  4. 如果异常值代表一个子群体——比如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。别急着建模,先和数据「聊聊天」。你会发现,数据会告诉你很多意想不到的东西。