3、探索性数据分析(EDA):Pandas与Matplotlib实战、数据分布分析、缺失值与异常值处理策略
好,咱们进入第三章。说实话,很多同学觉得EDA就是画几张图、看看统计量,没什么技术含量。但我得说,这种想法很危险。我在项目里见过太多人,模型怎么调都不收敛,最后发现是数据分布本身就有问题。EDA做得好,后面能省80%的麻烦。
这一章,我会带着你走一遍完整的EDA流程。从Pandas的基本操作,到Matplotlib的可视化,再到数据分布分析、缺失值和异常值的处理。嗯,都是实战中摸爬滚打出来的经验。
3.1 Pandas实战:数据加载与初步探索
拿到数据后,我习惯先做三件事:看形状、看类型、看统计量。别急着建模,先摸摸数据的底。
import pandas as pd
import numpy as np
# 加载数据
df = pd.read_csv('customer_data.csv')
# 第一步:看形状
print(f'数据集大小:{df.shape}')
# 输出:(10000, 15) —— 1万条记录,15个字段
# 第二步:看字段类型
print(df.dtypes)
# 看看有没有object类型,那通常是文本或分类变量
# 第三步:看统计量
print(df.describe())
# 数值型字段的均值、标准差、四分位数
这里有个小技巧。我一般会加一句 df.describe(include='all'),这样连分类字段的频次也能看到。为什么?因为有时候你以为的数值型,其实是分类编码,比如性别用1和2表示。这种坑我踩过不止一次。
df.info()。它能告诉你每列的非空数量,一眼就能看出哪些字段有缺失。比一个个数快多了。
3.2 数据分布分析:别被平均值骗了
平均值是个好东西,但也很容易骗人。我记得有一次做用户消费分析,平均消费额是500元,看起来很正常。但画了分布图才发现,数据是双峰的——一部分用户只花50元,另一部分花2000元。平均值500,其实代表不了任何一群人。
所以,我强烈建议你每次都要看分布。用直方图和箱线图,这两个最直观。
import matplotlib.pyplot as plt
import seaborn as sns
# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False
# 直方图:看整体分布形态
plt.figure(figsize=(10, 5))
plt.subplot(1, 2, 1)
sns.histplot(df['amount'], bins=50, kde=True)
plt.title('消费金额分布')
# 箱线图:看异常值和四分位
plt.subplot(1, 2, 2)
sns.boxplot(y=df['amount'])
plt.title('消费金额箱线图')
plt.tight_layout()
plt.show()
你想想看,直方图告诉你数据是左偏还是右偏,箱线图直接标出异常点。这两个图配合起来,数据分布基本就摸清了。
- 如果直方图呈正态分布(钟形),恭喜你,很多模型可以直接用
- 如果严重偏态,考虑做对数变换或Box-Cox变换
- 如果出现多峰,说明数据可能来自不同的群体,需要分层分析
3.3 缺失值处理:不是所有缺失都要填补
缺失值怎么处理?这个问题我问过很多面试者,答案大多是「用均值填补」或者「删除」。嗯,这两种做法在特定场景下是对的,但更多时候是错的。
我个人习惯先搞清楚缺失的原因。是随机缺失?还是系统缺失?举个例子,在信贷风控项目中,用户的「收入」字段经常缺失。为什么?因为高收入人群可能不愿意填,低收入人群也可能不填。这两种缺失背后的逻辑完全不同。
# 查看缺失情况
missing_ratio = df.isnull().sum() / len(df) * 100
missing_df = pd.DataFrame({
'缺失比例(%)': missing_ratio,
'数据类型': df.dtypes
})
print(missing_df[missing_df['缺失比例(%)'] > 0].sort_values('缺失比例(%)', ascending=False))
拿到缺失比例后,我的处理策略是这样的:
| 缺失比例 | 处理策略 | 我踩过的坑 |
|---|---|---|
| < 5% | 直接删除,或者用均值/中位数填补 | 曾经用均值填补,结果把分布拉偏了 |
| 5% - 20% | 用中位数填补(对异常值更鲁棒),或用模型预测 | 中位数比均值靠谱,尤其是数据偏态时 |
| 20% - 50% | 单独作为一个类别,或者用多重插补 | 单独成类有时效果出奇好,试试看 |
| > 50% | 建议删除该特征,信息量太少了 | 别舍不得,留着只会增加噪声 |
3.4 异常值处理:别一刀切
异常值这个话题,我特别想多说两句。很多同学一看到异常值就删,这是不对的。异常值可能是噪声,也可能是宝贵的信号。
我记得有一次做电商数据分析,发现某个用户的购买金额是其他人的100倍。一开始以为是数据错误,后来一查,是个企业采购账号。这个「异常值」恰恰是最有价值的客户。
所以,处理异常值之前,先问自己三个问题:
- 这个值在业务上合理吗?
- 它是测量错误还是真实情况?
- 删除后对模型影响大吗?
# 用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)} 个异常值')
# 处理方式:截断(Winsorize)
df['amount_clipped'] = df['amount'].clip(lower=lower_bound, upper=upper_bound)
这里我推荐用截断而不是直接删除。为什么?因为删除会改变样本量,而截断保留了样本数量,只是把极端值拉回到合理范围。对于树模型来说,截断和删除差别不大;但对于线性模型,截断能显著提升稳定性。
3.5 实战案例:完整EDA流程
最后,我给你一个完整的EDA代码模板。这是我每次做项目都会跑的流程,你可以直接拿来用。
def eda_pipeline(df):
"""完整的EDA流程"""
print('='*50)
print('1. 数据概览')
print('='*50)
print(f'形状: {df.shape}')
print(f'字段列表: {list(df.columns)}')
print('\n' + '='*50)
print('2. 缺失值分析')
print('='*50)
missing = df.isnull().sum()
missing = missing[missing > 0].sort_values(ascending=False)
if len(missing) > 0:
print(missing)
else:
print('没有缺失值,完美!')
print('\n' + '='*50)
print('3. 数值型字段分布')
print('='*50)
num_cols = df.select_dtypes(include=[np.number]).columns
for col in num_cols[:5]: # 只看前5个,避免输出太长
plt.figure(figsize=(8, 3))
plt.subplot(1, 2, 1)
sns.histplot(df[col].dropna(), bins=30, kde=True)
plt.title(f'{col} 分布')
plt.subplot(1, 2, 2)
sns.boxplot(y=df[col])
plt.title(f'{col} 箱线图')
plt.tight_layout()
plt.show()
print('\n' + '='*50)
print('4. 相关性分析')
print('='*50)
corr = df[num_cols].corr()
sns.heatmap(corr, annot=True, cmap='coolwarm', fmt='.2f')
plt.title('特征相关性热力图')
plt.show()
return df.describe()
# 运行
summary = eda_pipeline(df)
这个流程跑下来,你对数据就有了80%的了解。剩下的20%,是在建模过程中慢慢发现的。嗯,EDA不是一次性的工作,它应该贯穿整个项目周期。
- EDA的核心是「了解你的数据」,而不是「完成任务」
- 分布分析要结合业务背景,别只看统计量
- 缺失值和异常值处理没有银弹,具体情况具体分析
- 把EDA流程自动化,能节省大量重复劳动
下一章,我们会进入特征工程。说白了,就是把原始数据变成模型能理解的语言。到时候我会分享一些我在Kaggle比赛里用过的特征技巧,保证实用。