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倍。一开始以为是数据错误,后来一查,是个企业采购账号。这个「异常值」恰恰是最有价值的客户。

所以,处理异常值之前,先问自己三个问题:

  1. 这个值在业务上合理吗?
  2. 它是测量错误还是真实情况?
  3. 删除后对模型影响大吗?
# 用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比赛里用过的特征技巧,保证实用。