拿到一份干净的数据后,你的第一反应是什么?很多人会立刻跑模型、调参数,但这是本末倒置。在建模之前,你需要先回答两个问题:这些数据长什么样?里面有什么规律?

探索性数据分析(EDA)就是回答这两个问题的过程。它不是一次性的步骤,而是一个反复提问、画图、计算、再提问的循环。好的 EDA 能帮你发现数据质量问题、验证假设、指导特征工程,甚至直接决定选什么模型。

EDA 的核心方法论

EDA 没有固定脚本,但有一个可循的工作流:

  1. 概览:数据有多大?每列是什么类型?有没有明显的缺失和异常?
  2. 单变量分析:每个变量各自的分布如何?
  3. 双变量分析:两个变量之间有什么关系?
  4. 多变量分析:多个变量综合起来能发现什么?
  5. 总结与假设:形成分析假设,进入建模阶段。

我们以 Kaggle 的 Titanic 数据集为例,一步步走完 EDA 流程。

环境与数据加载

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')

# 设置中文字体
plt.rcParams['font.sans-serif'] = ['SimHei']
plt.rcParams['axes.unicode_minus'] = False

sns.set_theme(style='whitegrid', palette='muted')

我们直接加载 Titanic 数据集:

# 从 seaborn 加载
df = sns.load_dataset('titanic')
print('数据集大小:', df.shape)
print('\n前 5 行:')
print(df.head())

第一步:快速概览

# 基本信息
print(df.info())

# 数值统计
print(df.describe())

# 分类统计
print(df.describe(include=['object', 'category']))

# 缺失值
missing = df.isnull().sum()
missing_pct = (missing / len(df) * 100).round(2)
missing_df = pd.DataFrame({'缺失数': missing, '缺失比例(%)': missing_pct})
print(missing_df[missing_df['缺失数'] > 0].sort_values('缺失比例(%)', ascending=False))

输出的关键发现:

  • 共 891 行,12 列
  • age 有约 20% 缺失,cabin 有 77% 缺失
  • embarked 只有 2 个缺失值

这就是 EDA 的第一个产出:了解数据质量现状

单变量分析

单变量分析关注每个变量自己的故事。

数值型变量

fig, axes = plt.subplots(2, 3, figsize=(15, 8))

num_cols = ['age', 'fare', 'sibsp', 'parch']
for i, col in enumerate(num_cols):
    row, col_idx = divmod(i, 3)

    # 直方图 + KDE
    axes[row, col_idx].hist(df[col].dropna(), bins=30, density=True, alpha=0.6)
    df[col].dropna().plot(kind='kde', ax=axes[row, col_idx], color='red')
    axes[row, col_idx].set_title(f'{col} 分布')

# 箱线图汇总
sns.boxplot(data=df[num_cols], ax=axes[1, 2])
axes[1, 2].set_title('数值变量箱线图')
axes[1, 2].tick_params(axis='x', rotation=45)

plt.tight_layout()
plt.show()

从这些图中我们可以观察到:

  • age 大致呈正态分布,集中在 20-40 岁
  • fare 严重右偏,大部分票价集中在低区间,少量头等舱票价极高
  • sibspparch 大部分为 0,说明乘客多数独自出行

分类变量

fig, axes = plt.subplots(2, 2, figsize=(12, 8))

cat_cols = ['sex', 'class', 'embarked', 'alone']
for i, col in enumerate(cat_cols):
    row, col_idx = divmod(i, 2)
    counts = df[col].value_counts()
    axes[row, col_idx].bar(counts.index, counts.values)
    axes[row, col_idx].set_title(f'{col} 频数分布')
    for j, v in enumerate(counts.values):
        axes[row, col_idx].text(j, v + 5, str(v), ha='center')

plt.tight_layout()
plt.show()

单变量 EDA 的产出:每个变量的分布特征、集中趋势、离散程度、类别平衡性

双变量分析

EDA 的精华在于发现变量之间的关系。

分类 vs 目标变量(生存率)

# 性别与生存
pd.crosstab(df['sex'], df['survived'], normalize='index').plot(kind='bar', stacked=True)
plt.title('性别 vs 生存率')
plt.ylabel('比例')
plt.legend(['未幸存', '幸存'])
plt.show()

# 客舱等级与生存
sns.barplot(data=df, x='class', y='survived')
plt.title('客舱等级 vs 平均生存率')
plt.show()

# 年龄分段 vs 生存
df['age_group'] = pd.cut(df['age'], bins=[0, 12, 18, 35, 60, 100], 
                          labels=['儿童', '青少年', '青年', '中年', '老年'])
sns.barplot(data=df, x='age_group', y='survived')
plt.title('年龄组 vs 平均生存率')
plt.show()

交叉表分析更精确:

# 性别 + 等级 的生存率
ct = pd.crosstab([df['sex'], df['class']], df['survived'], normalize='index')
print(ct.round(3))

# 热力图可视化
ct_pivot = df.pivot_table(values='survived', index='sex', columns='class', aggfunc='mean')
sns.heatmap(ct_pivot, annot=True, cmap='RdYlGn', fmt='.2f')
plt.title('性别×等级 生存率热力图')
plt.show()

发现:头等舱女性生存率最高(97%),三等舱男性最低(14%)。这是 Titanic 数据最经典的发现。

数值 vs 数值(相关性)

# 散点图
sns.scatterplot(data=df, x='age', y='fare', hue='survived', alpha=0.6)
plt.title('年龄 vs 票价(按生存着色)')
plt.show()

# 相关矩阵
corr = df.select_dtypes(include=[np.number]).corr()
print(corr.round(3))

# 热力图
sns.heatmap(corr, annot=True, cmap='coolwarm', fmt='.2f', 
            linewidths=0.5, square=True)
plt.title('数值变量相关矩阵')
plt.show()

相关矩阵的关键发现:

  • survivedfare 正相关(0.26),票价越高生存率越高
  • survivedpclass 负相关(-0.34),等级越低生存率越低
  • sibspparch 正相关(0.41),家庭成员数量相关

分类 vs 数值

# 箱线图比较
fig, axes = plt.subplots(1, 2, figsize=(12, 4))

sns.boxplot(data=df, x='survived', y='age', ax=axes[0])
axes[0].set_title('生存 vs 年龄')

sns.boxplot(data=df, x='survived', y='fare', ax=axes[1])
axes[1].set_title('生存 vs 票价')

plt.tight_layout()
plt.show()

# 小提琴图(更详细的分布)
sns.violinplot(data=df, x='class', y='age', hue='survived', split=True)
plt.title('客舱等级中年龄的生存分布')
plt.show()

多变量分析

两个变量不够?三个变量一起看。

Pair Plot

pairplot 是最强大的 EDA 工具之一,一口气展示所有数值变量的两两关系:

sns.pairplot(df, vars=['age', 'fare', 'sibsp', 'parch'], 
             hue='survived', diag_kind='kde', corner=True)
plt.show()

当数据维度不多时,pairplot 几乎能替代十几张单独的图表。

平行坐标

适合观察多个连续变量上的分类模式:

from pandas.plotting import parallel_coordinates

# 标准化后画平行坐标
df_std = df[['age', 'fare', 'sibsp', 'parch', 'survived']].dropna()
for col in ['age', 'fare', 'sibsp', 'parch']:
    df_std[col] = (df_std[col] - df_std[col].mean()) / df_std[col].std()

plt.figure(figsize=(10, 5))
parallel_coordinates(df_std, class_column='survived', color=['#FF6B6B', '#4ECDC4'])
plt.title('平行坐标图(标准化后)')
plt.show()

平行坐标上,生存者(一条线)的票价普遍更高。

Facet Grid(分面网格)

按类别拆分观察模式:

g = sns.FacetGrid(df, col='class', row='sex', hue='survived', height=3)
g.map(sns.histplot, 'age', alpha=0.6)
g.add_legend()
plt.show()

这个图一口气展示了性别×等级×年龄×生存率的四维关系。这才是多变量分析的威力——在细分中找模式

自动 EDA 工具

当数据列数很多(30+)时,手动画图效率太低。自动 EDA 工具可以一键生成完整的分析报告。

pandas-profiling(ydata-profiling)

# 安装: pip install ydata-profiling
from ydata_profiling import ProfileReport

profile = ProfileReport(df, title='Titanic EDA Report', explorative=True)
profile.to_file('titanic_eda_report.html')

打开生成的 HTML 文件,你会看到:

  • 数据集概览(行数、列数、缺失率、重复率)
  • 每个变量的详细分析(分布、统计量、异常值)
  • 变量间的相关性矩阵
  • 缺失值关联分析
  • 交互式图表

Sweetviz

# 安装: pip install sweetviz
import sweetviz as sv

report = sv.analyze(df, target_feat='survived')
report.show_html('titanic_sweetviz.html')

Sweetviz 的特色是自动对比目标变量的分布差异。

什么时候用自动 EDA?

  • 数据量大、列数多时作为初筛
  • 快速了解陌生数据集
  • 生成报告给非技术人员看

但自动 EDA 不能替代 手动分析——它对业务上下文一无所知,无法发现你领域特有的模式。

完整 EDA 工作流模板

这是一个可复用的 EDA 模板:

def eda_pipeline(df, target=None):
    """完整的 EDA 流程"""
    print('=' * 50)
    print('1. 数据概览')
    print('=' * 50)
    print(f'Shape: {df.shape}')
    print(f'内存: {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB')

    print('\n' + '=' * 50)
    print('2. 缺失值分析')
    print('=' * 50)
    missing = df.isnull().sum()
    missing = missing[missing > 0].sort_values(ascending=False)
    if len(missing) > 0:
        print(pd.DataFrame({
            '缺失数': missing,
            '比例(%)': (missing / len(df) * 100).round(2)
        }))
    else:
        print('无缺失值')

    print('\n' + '=' * 50)
    print('3. 重复值分析')
    print('=' * 50)
    print(f'重复行数: {df.duplicated().sum()}')

    print('\n' + '=' * 50)
    print('4. 数值变量统计')
    print('=' * 50)
    print(df.describe().round(2))

    print('\n' + '=' * 50)
    print('5. 分类变量统计')
    print('=' * 50)
    cat_cols = df.select_dtypes(include=['object', 'category']).columns
    for col in cat_cols:
        print(f'\n{col}:')
        print(df[col].value_counts())

    # 6. 绘图
    fig = plt.figure(figsize=(15, 10))
    gs = fig.add_gridspec(3, 3)

    # 相关矩阵
    num_cols = df.select_dtypes(include=[np.number]).columns
    if len(num_cols) > 0:
        ax = fig.add_subplot(gs[0, :2])
        corr = df[num_cols].corr()
        sns.heatmap(corr, annot=True, cmap='coolwarm', fmt='.2f', ax=ax)
        ax.set_title('相关矩阵')

        # 目标变量分布
        if target and target in num_cols:
            ax = fig.add_subplot(gs[0, 2])
            df[target].hist(ax=ax, bins=30)
            ax.set_title(f'{target} 分布')

    # 分类柱状图
    for i, col in enumerate(cat_cols[:3]):
        if i < 3:
            ax = fig.add_subplot(gs[1, i])
            df[col].value_counts().plot(kind='bar', ax=ax)
            ax.set_title(f'{col} 频数')
            ax.tick_params(axis='x', rotation=45)

    plt.tight_layout()
    plt.show()

    return {
        'shape': df.shape,
        'missing': missing.to_dict() if len(missing) > 0 else {},
        'duplicates': df.duplicated().sum(),
    }

# 使用
report = eda_pipeline(df, target='survived')

EDA 完成后你应该知道什么?

好的 EDA 结束后,你应该能回答:

  1. 数据是否足够干净? 哪些列需要清洗?
  2. 目标变量的分布是什么? 是否平衡?(分类)是否有长尾?(回归)
  3. 哪些特征与目标相关? 排在前 3 的特征是什么?
  4. 是否存在多重共线性? 两个特征几乎一模一样?
  5. 数据中有什么惊喜? 某个分组的异常值?反直觉的模式?

把这些发现记录下来,它们会成为特征工程和模型选择的依据。

小结

EDA 不是画几张漂亮的图就完事了。它是一个假设驱动的探索过程:提出问题 -> 画图验证 -> 发现新模式 -> 提出新问题。整个过程要带着对业务的理解,而不是机械地跑代码。

自动化工具能帮你快速扫描数据,但真正的洞察来自你亲自观察、追问和思考。EDA 做得好的分析师,建模时基本不会走弯路。

下一篇文章,我们进入数据可视化的进阶世界——用 Seaborn 和 Plotly 做出既专业又美观的图表。

Summary: EDA 方法论与 Titanic 全流程实战。