4 minutes
探索性数据分析(EDA)
拿到一份干净的数据后,你的第一反应是什么?很多人会立刻跑模型、调参数,但这是本末倒置。在建模之前,你需要先回答两个问题:这些数据长什么样?里面有什么规律?
探索性数据分析(EDA)就是回答这两个问题的过程。它不是一次性的步骤,而是一个反复提问、画图、计算、再提问的循环。好的 EDA 能帮你发现数据质量问题、验证假设、指导特征工程,甚至直接决定选什么模型。
EDA 的核心方法论
EDA 没有固定脚本,但有一个可循的工作流:
- 概览:数据有多大?每列是什么类型?有没有明显的缺失和异常?
- 单变量分析:每个变量各自的分布如何?
- 双变量分析:两个变量之间有什么关系?
- 多变量分析:多个变量综合起来能发现什么?
- 总结与假设:形成分析假设,进入建模阶段。
我们以 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严重右偏,大部分票价集中在低区间,少量头等舱票价极高sibsp和parch大部分为 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()
相关矩阵的关键发现:
survived与fare正相关(0.26),票价越高生存率越高survived与pclass负相关(-0.34),等级越低生存率越低sibsp与parch正相关(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 结束后,你应该能回答:
- 数据是否足够干净? 哪些列需要清洗?
- 目标变量的分布是什么? 是否平衡?(分类)是否有长尾?(回归)
- 哪些特征与目标相关? 排在前 3 的特征是什么?
- 是否存在多重共线性? 两个特征几乎一模一样?
- 数据中有什么惊喜? 某个分组的异常值?反直觉的模式?
把这些发现记录下来,它们会成为特征工程和模型选择的依据。
小结
EDA 不是画几张漂亮的图就完事了。它是一个假设驱动的探索过程:提出问题 -> 画图验证 -> 发现新模式 -> 提出新问题。整个过程要带着对业务的理解,而不是机械地跑代码。
自动化工具能帮你快速扫描数据,但真正的洞察来自你亲自观察、追问和思考。EDA 做得好的分析师,建模时基本不会走弯路。
下一篇文章,我们进入数据可视化的进阶世界——用 Seaborn 和 Plotly 做出既专业又美观的图表。
Summary: EDA 方法论与 Titanic 全流程实战。