2. 数据探索与理解:数据质量检查、描述性统计分析、数据分布可视化、相关性分析
好,咱们进入第二章。说实话,很多同学拿到数据就急着建模,恨不得马上跑出个漂亮结果。我刚开始做项目时也这样,结果呢?模型跑出来一塌糊涂,调参调到怀疑人生。后来我学乖了——数据探索这一步,省不得。
你想想看,数据就像食材。食材不新鲜,再好的厨子也做不出好菜。数据探索,就是帮你检查食材、了解食材的过程。我个人习惯,花在探索上的时间至少占整个项目的30%。
2.1 数据质量检查
拿到数据第一件事,不是看分布,不是画图——先看数据质量。我见过太多项目,跑了大半天模型,最后发现是数据本身有问题。
2.1.1 缺失值检查
缺失值是最常见的问题。怎么查?很简单:
import pandas as pd
import numpy as np
# 加载数据
df = pd.read_csv('your_data.csv')
# 检查缺失值
print(df.isnull().sum())
# 缺失比例
print(df.isnull().mean() * 100)
嗯,这里要注意:缺失比例超过50%的特征,我一般直接扔掉。为什么?因为信息量太少,补全的意义不大。我在一个电商项目中遇到过,有个特征叫「用户上次投诉时间」,缺失率高达80%。当时团队有人想用均值填充,我说别费劲了,直接删掉。后来证明这个决定是对的。
2.1.2 异常值检测
异常值,说白了就是那些「离谱」的数据点。比如用户年龄填了200岁,或者交易金额是负数。这些数据会严重干扰模型训练。
我常用的方法:
# 使用IQR方法检测异常值
def detect_outliers_iqr(data, column):
Q1 = data[column].quantile(0.25)
Q3 = data[column].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = data[(data[column] < lower_bound) | (data[column] > upper_bound)]
return outliers
# 举个例子
outliers = detect_outliers_iqr(df, 'age')
print(f'发现 {len(outliers)} 个异常值')
但注意,异常值不一定是错误。我在金融风控项目中遇到过,有些大额交易看起来像异常值,其实是正常的业务行为。所以,先理解业务,再决定怎么处理。
2.2 描述性统计分析
数据质量没问题了,接下来就是「认识」你的数据。描述性统计就是帮你快速了解数据全貌的工具。
2.2.1 数值型特征
对于数值型特征,我一般看这几个指标:
| 指标 | 含义 | 我关注什么 |
|---|---|---|
| 均值 | 数据的平均水平 | 是否和业务预期一致 |
| 中位数 | 数据的中间值 | 和均值对比,判断数据是否偏斜 |
| 标准差 | 数据的离散程度 | 数据是否过于集中或分散 |
| 偏度 | 分布的不对称性 | 是否需要做对数变换 |
| 峰度 | 分布的尖峭程度 | 是否存在极端值 |
# 快速获取描述性统计
print(df.describe())
# 更详细的统计信息
print(df.describe(percentiles=[0.01, 0.05, 0.25, 0.5, 0.75, 0.95, 0.99]))
我个人习惯,会特别关注均值和中位数的差异。如果两者相差很大,说明数据分布偏斜严重。比如收入数据,均值往往远大于中位数,因为少数高收入者拉高了平均值。这时候,我通常会考虑做对数变换。
2.2.2 类别型特征
类别型特征怎么看?看频数和占比:
# 类别型特征的频数统计
for col in df.select_dtypes(include=['object']).columns:
print(f'\n{col} 的分布:')
print(df[col].value_counts(normalize=True).head(10))
这里有个坑:类别太多怎么办? 比如「城市」这个特征,可能有几百个取值。我一般会做两件事:一是把出现频率低于1%的类别合并为「其他」;二是看业务上能不能做层级合并,比如把城市归到省份。
2.3 数据分布可视化
光看数字不够,还得画图。一张好图胜过千言万语。我常用的可视化工具:
2.3.1 直方图与密度图
看单个特征的分布:
import matplotlib.pyplot as plt
import seaborn as sns
# 直方图
plt.figure(figsize=(12, 4))
plt.subplot(1, 2, 1)
df['age'].hist(bins=50)
plt.title('年龄分布直方图')
# 密度图
plt.subplot(1, 2, 2)
sns.kdeplot(df['age'], shade=True)
plt.title('年龄分布密度图')
plt.show()
为什么要看分布?举个例子,我在做信用评分模型时,发现「收入」特征的分布严重右偏。如果不处理,模型会对高收入群体过度敏感。后来做了对数变换,模型表现明显提升。
2.3.2 箱线图
箱线图能同时展示分布和异常值:
# 箱线图
plt.figure(figsize=(10, 6))
sns.boxplot(data=df[['age', 'income', 'credit_score']])
plt.title('关键特征箱线图')
plt.show()
嗯,这里要注意:箱线图里的「异常值」不一定要删除。我记得在医疗项目中,某些生理指标的「异常值」恰恰是疾病的表现。所以,先问业务专家,再决定。
2.3.3 类别特征的柱状图
# 类别特征分布
plt.figure(figsize=(10, 6))
df['education'].value_counts().plot(kind='bar')
plt.title('教育程度分布')
plt.xticks(rotation=45)
plt.show()
我一般会关注类别是否均衡。如果某个类别占比超过90%,这个特征对模型贡献可能不大。但也不绝对——比如「是否欺诈」这个特征,正样本可能只有1%,但恰恰是最重要的。
2.4 相关性分析
最后一步,看特征之间的关系。这能帮你发现冗余特征、潜在的多重共线性问题。
2.4.1 皮尔逊相关系数
最常用的方法:
# 计算相关系数矩阵
corr_matrix = df.corr()
# 可视化
plt.figure(figsize=(12, 10))
sns.heatmap(corr_matrix, annot=True, cmap='coolwarm', center=0)
plt.title('特征相关性热力图')
plt.show()
我一般关注相关系数绝对值大于0.8的特征对。比如「房屋面积」和「房间数量」高度相关,我会考虑只保留一个,或者做特征融合。
2.4.2 斯皮尔曼秩相关系数
# 斯皮尔曼相关系数
spearman_corr = df.corr(method='spearman')
# 对比两种方法
linear_vs_rank = pd.DataFrame({
'pearson': corr_matrix['target'],
'spearman': spearman_corr['target']
})
print(linear_vs_rank)
如果皮尔逊和斯皮尔曼结果差异很大,说明特征之间的关系不是线性的。这时候,我会考虑做多项式特征或者使用树模型。
2.4.3 类别特征与数值特征的相关性
对于类别特征和数值特征,不能用相关系数。我一般用方差分析(ANOVA)或者信息价值(IV):
from scipy import stats
# 方差分析:类别特征对数值特征的影响
for cat_col in ['education', 'marital_status']:
groups = [df[df[cat_col] == val]['income'] for val in df[cat_col].unique()]
f_stat, p_val = stats.f_oneway(*groups)
print(f'{cat_col} 对收入的影响:F值={f_stat:.2f}, p值={p_val:.4f}')
p值小于0.05,说明这个类别特征对数值特征有显著影响。嗯,这个信息对特征筛选很有用。
小结
数据探索不是走过场,它是整个特征工程的基石。我个人习惯,每次做完探索都会写一份报告,记录:
- 哪些特征质量差,需要清洗
- 哪些特征分布偏斜,需要变换
- 哪些特征高度相关,需要合并或删除
- 哪些特征和目标变量关系强,值得深入挖掘
下一章,我们会讲特征变换的具体方法。但记住:没有充分的数据探索,再高级的特征工程都是空中楼阁。别急着建模,先把数据摸透了再说。