在任何数据分析或机器学习项目中,我们面对的原始数据往往是’不干净’的。
这引出了一个核心问题:我们如何将混乱的原始数据转化为可供模型使用的、高质量的’燃料’?
模型和算法固然重要,但数据的质量决定了分析结果的上限。
我们将系统地学习数据预处理的五大核心模块,并结合Python代码进行实践。
理论部分 - 数据缺失值处理 - 数据异常值处理 - 数据类型转换 - 特征选择与降维 - 不平衡数据集处理
实践工具 - pandas
- numpy
- scikit-learn
- matplotlib
/ seaborn
- imbalanced-learn
在真实的商业场景中,我们拿到的数据几乎不可能是完美的。
这些缺失的数据点,我们称之为缺失值 (Missing Values)。
从统计学的角度,缺失值的产生机制可以分为三类。
理解它们,有助于我们选择更合适的处理方法,避免引入新的偏差。
定义: 数据的缺失与任何值(无论是缺失值本身还是其他变量)都无关。
直观例子: 由于设备随机故障,导致某天的传感器数据未能记录。
这是最理想、最简单的缺失情况,因为缺失样本可以看作是全集数据的一个随机子集。
定义: 数据的缺失不完全是随机的,它依赖于其他已观测到的变量。
直观例子: 在市场调研中,高收入人群可能更不愿透露具体收入。
定义: 数据的缺失与缺失值本身有关。
直观例子: 在评估一款减肥产品的效果时,效果不佳的参与者可能中途退出。
机制类型 | 缺失是否依赖其他变量? | 缺失是否依赖自身数值? | 处理难度 |
---|---|---|---|
MCAR | 否 | 否 | 低 |
MAR | 是 | 否 | 中 |
MNAR | (可能) | 是 | 高 |
正确判断缺失类型是选择处理策略的第一步。
处理缺失值主要有两种思路,每种都有其适用场景和风险。
1. 删除法 (Deletion)
2. 插补法 (Imputation)
插补法是用一个估计值来代替缺失值,是更常用的方法。
根据估计方法的不同,可以分为统计量插补和高级插补。
0
, -1
, 或 'Unknown'
)来填充所有缺失值。前向/后向填充: 在时间序列数据中常用,用前一个 (ffill
) 或后一个 (bfill
) 时间点的值填充。
模型预测插补: 将含缺失值的特征作为目标变量 Y
,其他特征作为自变量 X
,通过训练机器学习模型(如KNN、随机森林)来预测缺失值。
多重插补 (MI): 更稳健的方法。它会生成多组可能的插补值,在每个插补后的数据集上进行分析,最后汇总结果。
我们首先用 pandas
创建一个包含缺失值 (np.nan
) 的 DataFrame
。
接下来,我们将学习如何用 pandas
的内置函数来定位这些缺失值。
.info()
快速概览数据df.info()
方法可以提供数据框的整体信息,包括每列的非空值数量。
通过比较 Non-Null Count
和总行数 (4 entries)
,可以快速发现哪些列存在缺失。
.isnull()
精准定位缺失值df.isnull()
会返回一个布尔型的 DataFrame
,True
表示该位置是缺失值。
这对于直观地查看缺失值的分布很有帮助。
.isnull().sum()
统计缺失值这是最常用的缺失值统计方法,它能计算出每列的缺失值总数。
结果清晰地显示,A、B、D列各有2个缺失值,C列有1个。
dropna()
删除缺失值dropna()
函数可以灵活地删除含有缺失值的行或列。
关键参数:
axis
: 0
表示对行操作(默认),1
表示对列操作。how
: 'any'
表示只要有缺失值就删除(默认),'all'
表示全部是缺失值才删除。thresh
: 保留至少有N个非缺失值的行/列。subset
: 指定在哪些列的子集中检查缺失值。dropna()
示例:删除任何包含缺失值的列通过设置 axis=1
和 how='any'
,我们可以删除数据框中任何包含NaN
的列。
由于所有列都含有缺失值,所以结果是一个空的 DataFrame
。
dropna()
示例:删除任何包含缺失值的行这是默认行为 (axis=0
, how='any'
)。
由于所有行也都含有缺失值,所以结果同样是空的。
dropna()
示例:使用 thresh
参数我们可以保留那些信息量更完整的行,例如,保留至少有3个非NaN
值的行。
只有索引为1的行 [102.0, NaN, 270.0, 2000.0]
被保留,因为它有3个非空值。
fillna()
插补缺失值fillna()
是用于填充缺失值的核心函数。
关键参数:
value
: 用于填充的标量值或字典/Series。method
: 填充方法,如 'pad'
/'ffill'
(前向填充) 或 'bfill'
/'backfill'
(后向填充)。axis
: 填充方向,0
为纵向,1
为横向。fillna()
示例:前向填充 (Forward Fill)使用 method='pad'
或 'ffill'
,用缺失值前面的有效值进行填充。
注意第一行的NaN
没有被填充,因为它们前面没有值。
fillna()
示例:后向填充 (Backward Fill)使用 method='bfill'
,用缺失值后面的有效值进行填充。
注意最后一行的NaN
没有被填充,因为它们后面没有值。
fillna()
示例:使用均值填充这是一个非常常见的策略,用每列的均值来填充该列的缺失值。
这种方法只对数值型列有效。
对于更复杂的情况,我们可以使用机器学习模型进行填充,这通常能获得更好的效果。
我们将演示两种常用算法:
核心思想: ‘物以类聚’。
这考虑了样本间的相似性,比简单的全局均值更合理。
KNNImputer
scikit-learn
提供了 KNNImputer
来方便地实现KNN填充。
import numpy as np
import pandas as pd
from sklearn.impute import KNNImputer
df_knn = pd.DataFrame(
{'A': [None, 102, None, 644,45, 102, 67, 644],
'B': [10, None, None, 460,145, 182, 617, 624],
'C': [100, 270, None, 480,45, 132, 17, 604],
'D': [567, 2000, 3000, 538,425, 102, 27, 244]})
imputer = KNNImputer(n_neighbors=1)
imputed = imputer.fit_transform(df_knn)
df_imputed_knn = pd.DataFrame(imputed, columns=df_knn.columns)
print("原始数据:")
print(df_knn)
print("\nKNN填充后:")
print(df_imputed_knn)
下面的代码片段展示了其核心用法。
核心思想: ‘集思广益’。
Y
。X
。这个过程可以对所有含缺失值的列迭代进行,效果通常非常出色。
异常值 (Outliers) 是指数据集中与其他观测值差异极大的数据点。
它们可能是:
正确识别和妥善处理异常值至关重要。
主要有两大类方法来发现这些’离群’的数据点。
1. 统计学方法
2. 可视化方法
该方法基于正态分布的特性:约 99.7% 的数据点会落在距离均值三个标准差 ($\sigma$)
的范围内。
\[ \large{ (\mu - 3\sigma, \mu + 3\sigma) } \]
因此,我们可以将超出此范围的数据点视为异常值。
重要前提: 数据必须近似服从正态分布。
我们来看一个例子。KsNormDetect
函数用于检验数据是否服从正态分布。
由于示例数据中存在一个极端值 242467
,数据分布严重偏斜,正态性检验无法通过,因此无法应用 \(3\sigma\) 法则。
箱线图是一种直观、鲁棒的异常值检测工具,它不依赖于数据的分布。
通常,我们将超出上下边缘的数据点视为异常值。
我们可以用 matplotlib
绘制数据集中所有特征的箱线图,来快速发现异常。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import warnings
warnings.filterwarnings("ignore")
try:
train_data = pd.read_csv("../data/zhengqi_train.txt", sep='\t', encoding='utf-8')
plt.figure(figsize=(18,10))
plt.boxplot(x=train_data.values,labels=train_data.columns)
plt.hlines([-7.5,7.5],0,40,colors='r')
plt.show()
except FileNotFoundError:
print("示例数据文件 'zhengqi_train.txt' 未找到。将跳过此图表的生成。")
Figure 2: 使用箱线图进行异常值分析
从图中可以清楚地看到,变量 V9
有一个明显的下限异常值。
发现了异常值之后,我们有多种处理策略:
NaN
,然后用前面介绍的插补法处理。根据上图的发现,我们可以用简单的 pandas
过滤操作来删除 V9
中的异常值。
这是一个简单有效的处理方法,但前提是我们有充分的理由相信这些点是需要被移除的。
机器学习模型通常只能处理数值型数据。
因此,我们需要将各种类型的数据,特别是类别型数据,转换为数值形式。同时,对数值型特征进行缩放也往往能提升模型性能。
为何缩放?
如何缩放?
标准化 (Standardization)
归一化 (Normalization)
[0, 1]
区间[0, 1]
模型无法理解 ‘北京’, ‘上海’, ‘绿色’, ‘红色’ 这样的文本信息。
我们需要一种方法,将这些类别信息编码为模型可以理解的数字,同时不引入错误的数学假设。
主要方法有两种:标签编码 和 独热编码。
方法: 将类别用连续的整数来表示。
例子: {'S': 1, 'M': 2, 'L': 3}
适用场景:
S < M < L
),教育程度(小学 < 中学 < 大学
)。风险: 若用于无序变量,会让模型错误地学习到类别间的大小关系(如 上海 > 北京
)。
方法: 将一个有 N 个类别的特征,转换为 N 个新的二进制 (0/1) 特征。
例子:
city | -> | city_北京 | city_上海 | city_天津 |
---|---|---|---|---|
北京 | 1 | 0 | 0 | |
上海 | 0 | 1 | 0 | |
天津 | 0 | 0 | 1 |
适用场景:
scikit-learn
的 DictVectorizer
可以方便地对字典列表形式的数据进行独热编码。
对于有序变量,我们可以使用 pandas
的 map
方法,通过一个自定义的映射字典来实现。
代码片段如下:
这样就将尺码成功转换为了具有大小关系的数值。
问题: 面对含有几十甚至上百个特征的高维数据,会引发’维度灾难’。
解决方案:
从原始特征集中挑选出一部分特征,移除冗余或不相关的特征。
通过数学变换,将高维特征空间映射到一个低维子空间,生成新的、数量更少的特征。
热力图可直观展示所有变量间的相关性,是过滤式特征选择的有力工具。
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from sklearn import preprocessing
import warnings
warnings.filterwarnings("ignore")
try:
train_data = pd.read_csv("../data/zhengqi_train.txt", sep='\t', encoding='utf-8')
train_data=train_data[train_data['V9']>-7.5]
features_columns = [col for col in train_data.columns if col not in ['target']]
min_max_scaler = preprocessing.MinMaxScaler()
train_data_scaler = min_max_scaler.fit_transform(train_data[features_columns])
train_data_scaler = pd.DataFrame(train_data_scaler, columns=features_columns)
train_data_scaler['target'] = train_data['target']
plt.figure(figsize=(20, 16))
column = train_data_scaler.columns.tolist()
mcorr = train_data_scaler[column].corr(method="spearman")
mask = np.zeros_like(mcorr, dtype=np.bool)
mask[np.triu_indices_from(mask)] = True
cmap = sns.diverging_palette(220, 10, as_cmap=True)
g = sns.heatmap(mcorr, mask=mask, cmap=cmap, square=True, annot=True, fmt='0.2f')
plt.show()
except FileNotFoundError:
print("示例数据文件 'zhengqi_train.txt' 未找到。将跳过此图表的生成。")
Figure 3: 特征相关性热力图
在生成相关性矩阵后,我们可以设定一个阈值(如与目标变量的相关性绝对值 > 0.3)来筛选特征。
# mcorr 是计算出的相关性矩阵
mcorr=mcorr.abs()
# 筛选出与 'target' 相关性大于 0.1 的特征
numerical_corr = mcorr[mcorr['target'] > 0.1]['target']
print(numerical_corr.sort_values(ascending=False))
# ... 进一步筛选,例如选择大于 0.3 的特征
features_corr_select = features_corr[features_corr['corr'] > 0.3]
select_features = [col for col in features_corr_select['features_and_target']
if col not in ['target']]
scikit-learn
的 PCA
模块非常易用。我们可以通过两种方式指定降维的程度。
方式一: 指定希望保留的原始信息量(方差)。
方式二: 直接指定降维后的主成分数量。
这两种方法让我们可以在’信息保留’和’维度降低’之间做出权衡。
不平衡数据集: 数据集中某个类别的样本数量远多于其他类别。
常见商业场景:
我们真正关心的少数类样本数量极少。
在处理不平衡问题时,我们必须关注更能反映少数类预测性能的指标:
TP / (TP + FP)
TP / (TP + FN)
2 * (Precision * Recall) / (Precision + Recall)
重采样 (Resampling) 是在数据层面解决不平衡问题的主要方法。
1. 过采样 (Over-sampling)
2. 欠采样 (Under-sampling)
SMOTE (Synthetic Minority Over-sampling Technique) 是一种非常有效的过采样方法。
它不是简单地复制少数类样本,而是通过在少数类样本与其近邻之间进行线性插值来合成新的、与原始样本相似但又不完全相同的样本。
这能有效避免随机过采样带来的过拟合问题。
imbalanced-learn
库imbalanced-learn
是一个专门用于处理不平衡数据集的Python库,它实现了包括SMOTE在内的多种重采样算法。
我们将用它来比较不同过采样方法在车险欺诈预测任务上的性能。
下面的代码框架展示了如何应用不同的采样方法,并用一套统一的评估指标来衡量它们的效果。
from imblearn.over_sampling import RandomOverSampler, SMOTE, ADASYN
# 定义不同的采样方法
sampling_methods = {
'Random OverSampling': RandomOverSampler(),
'SMOTE': SMOTE(),
'ADASYN': ADASYN(),
'Borderline-SMOTE': BorderlineSMOTE()
}
# 循环遍历每种方法
for method_name, method in sampling_methods.items():
# 应用采样方法
X_resampled, y_resampled = method.fit_resample(X_train, y_train)
# 训练模型
classifier.fit(X_resampled, y_resampled)
# 评估性能
# ...
# This is a placeholder for the table. The actual code requires the CSV file.
results_data = {
'Random OverSampling': {'Accuracy': 0.98, 'Precision': 0.65, 'Recall': 0.72, 'F1 Score': 0.68, 'ROC AUC': 0.85},
'SMOTE': {'Accuracy': 0.98, 'Precision': 0.68, 'Recall': 0.78, 'F1 Score': 0.73, 'ROC AUC': 0.88},
'ADASYN': {'Accuracy': 0.97, 'Precision': 0.62, 'Recall': 0.80, 'F1 Score': 0.70, 'ROC AUC': 0.89},
'Borderline-SMOTE': {'Accuracy': 0.98, 'Precision': 0.70, 'Recall': 0.76, 'F1 Score': 0.73, 'ROC AUC': 0.87}
}
results_df_placeholder = pd.DataFrame(results_data)
print("注意:这是一个基于典型结果的示例表格,实际运行需要'车险欺诈train.csv'文件。")
print(results_df_placeholder)
从(示例)结果可以看出,相比随机过采样,SMOTE及其变体通常能在召回率 (Recall) 和 F1分数上取得更好的平衡。
结论: 并没有一种永远最好的方法,需要根据具体业务场景和对Precision/Recall的不同侧重来选择最合适的策略。
今天我们学习了数据预处理的五大核心模块:
数据预处理不是一个线性的、一劳永逸的过程。
它通常需要根据模型的反馈进行多次迭代和调整。最好的预处理策略,源于对业务的深刻理解和数据的不断探索。
‘先理解你的数据,再选择你的工具’。
感谢聆听!