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

数据探索性分析,也就是我们常说的 EDA,是整个数据挖掘项目里最「自由」的一个环节。我个人习惯把它比作「相亲」——你刚拿到一个数据集,就像刚认识一个人,得先聊聊、看看对方的生活习惯、脾气秉性,才能决定要不要深入交往。千万别一上来就建模,那跟直接求婚差不多,大概率会翻车。

EDA 的核心目标就四个字:摸清底细。你要搞清楚数据长什么样、有没有脏数据、变量之间有没有关联、有没有极端值在捣乱。下面我按四个步骤来拆解,每一步都有我踩过的坑。

4.1 描述性统计:先给数据「称个体重」

拿到数据后,我第一件事就是跑 df.describe()。这步就像给数据做个体检,看看它的基本指标。

import pandas as pd
import numpy as np

# 假设 df 是你的数据框
print(df.describe())
print(df.info())
print(df.isnull().sum())

输出结果里,我最关注三个东西:

  • 均值 vs 中位数:如果两者差距很大,说明数据有偏态。我在做电商用户消费分析时遇到过,均值被几个大客户拉得很高,但中位数才几十块。这时候用均值代表「典型用户」就很不靠谱。
  • 标准差:数值太大说明数据波动剧烈。嗯,这里要注意,标准差为 0 的列基本是常量,可以直接扔掉。
  • 缺失值比例:超过 50% 的列,我一般会考虑删除。低于 5% 的,直接填充均值或中位数。
我的小技巧:对于分类变量,我喜欢用 df['col'].value_counts(normalize=True) 看占比。如果某个类别占比超过 95%,这列基本没什么区分能力,可以考虑剔除。

4.2 数据分布可视化:用图说话

光看数字不够直观。你想想看,一个偏态分布的数据,只看均值和中位数你很难想象它的形状。所以我习惯画图。

4.2.1 直方图 + 密度图

import matplotlib.pyplot as plt
import seaborn as sns

plt.figure(figsize=(10, 6))
sns.histplot(df['amount'], kde=True, bins=30)
plt.title('交易金额分布')
plt.show()

为什么要加 kde=True?因为密度曲线能帮你判断分布形态。我曾经在金融风控项目里,发现某个特征呈现「双峰分布」——后来一查,原来是两个不同业务线的数据混在一起了。如果不做 EDA,直接建模,模型会学得很痛苦。

4.2.2 箱线图

箱线图是检测异常值的利器。我每次必画。

sns.boxplot(x=df['amount'])
plt.title('交易金额箱线图')
plt.show()

箱线图里的「小圆点」就是异常值。但注意,异常值不等于错误值。比如在反欺诈场景里,异常的大额交易恰恰是我们需要关注的。所以别急着删,先标记出来。

避坑指南:我曾经在医疗数据里看到一堆「异常值」,差点删掉。后来发现那是重症患者的真实数据,删了反而丢失了关键信息。所以,异常值处理一定要结合业务背景。

4.3 相关性分析:找变量之间的「暧昧关系」

相关性分析说白了就是看两个变量之间有没有「默契」。我常用的是皮尔逊相关系数,取值范围在 -1 到 1 之间。

# 计算相关系数矩阵
corr_matrix = df.corr()

# 热力图可视化
plt.figure(figsize=(12, 8))
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', fmt='.2f')
plt.title('特征相关性热力图')
plt.show()

看热力图时,我重点关注两点:

  • 高相关性对(|r| > 0.8):两个特征高度相关,说明信息冗余。比如「房屋面积」和「房间数量」通常强相关。建模时保留一个就行,否则容易引起多重共线性。
  • 与目标变量的相关性:如果某个特征和目标变量的相关系数接近 0,那它对预测基本没帮助。但注意,这不绝对——非线性关系用皮尔逊系数是看不出来的。

一个真实的教训:我在做房价预测时,发现「卫生间数量」和「总价」的相关系数只有 0.2,差点把它扔掉。后来画了散点图才发现,高端住宅的卫生间数量虽然少,但单价极高。这就是典型的非线性关系,皮尔逊系数骗了你。

4.4 异常值初步检测:揪出「捣乱分子」

异常值检测的方法很多,我一般先用两种简单粗暴的方法做初步筛查。

4.4.1 3σ 原则

如果数据近似正态分布,可以用均值 ± 3 倍标准差来界定异常值。

mean = df['amount'].mean()
std = df['amount'].std()
lower_bound = mean - 3 * std
upper_bound = mean + 3 * std

outliers = df[(df['amount'] < lower_bound) | (df['amount'] > upper_bound)]
print(f'异常值数量: {len(outliers)}')

这个方法简单,但有个前提——数据得是正态分布。如果数据偏态严重,3σ 会漏掉很多异常点。

4.4.2 IQR 方法

这个方法更稳健,不依赖分布假设。

Q1 = df['amount'].quantile(0.25)
Q3 = df['amount'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

outliers = df[(df['amount'] < lower_bound) | (df['amount'] > upper_bound)]
print(f'异常值数量: {len(outliers)}')

为什么用 1.5 倍?这是统计学里的经验值。但说实话,我在实际项目中经常调整这个系数。比如在电商场景里,双十一的销售额是平时的 10 倍,用 1.5 倍会误杀太多。这时候我会把系数调到 3 甚至 5。

我的建议:异常值检测不要「一刀切」。先标记出来,然后跟业务方确认。我曾经在用户行为数据里发现一个「异常用户」,每天点击 10000 次——后来发现是爬虫。这种异常值必须剔除。

4.5 总结:EDA 的「黄金三问」

做完上面四步,我会问自己三个问题:

  1. 数据干净吗?——缺失值、异常值、重复值都处理了吗?
  2. 数据长什么样?——分布、偏态、离散程度心里有数吗?
  3. 变量之间怎么关联?——哪些特征冗余?哪些特征和目标强相关?

如果这三个问题都能回答清楚,那 EDA 这步就算到位了。别急着往下走,EDA 做得越扎实,后面的建模就越省心。我见过太多项目,因为 EDA 草草了事,结果模型上线后各种翻车,最后还得回来补课。

记住一句话:EDA 不是走过场,而是给整个项目打地基。地基不稳,楼盖得再高也得塌。