02 数据预处理:从理论到实践

核心议题:为何数据预处理至关重要?

在任何数据分析或机器学习项目中,我们面对的原始数据往往是’不干净’的。

这引出了一个核心问题:我们如何将混乱的原始数据转化为可供模型使用的、高质量的’燃料’?

’垃圾进,垃圾出’是数据科学的铁律

垃圾进,垃圾出原则 一个流程图,展示了低质量数据输入模型导致低质量输出,高质量数据输入模型导致高质量输出。 混乱的原始数据 (缺失, 异常, ...) 机器学习模型 不可靠的结论 高质量数据 机器学习模型 可靠的商业洞察

模型和算法固然重要,但数据的质量决定了分析结果的上限。

本章目标:掌握数据预处理的核心技术

我们将系统地学习数据预处理的五大核心模块,并结合Python代码进行实践。

理论部分 - 数据缺失值处理 - 数据异常值处理 - 数据类型转换 - 特征选择与降维 - 不平衡数据集处理

实践工具 - pandas - numpy - scikit-learn - matplotlib / seaborn - imbalanced-learn

模块一:数据缺失值处理

在真实的商业场景中,我们拿到的数据几乎不可能是完美的。

  • 用户消费数据: 用户的年龄或收入信息可能为空。
  • 财务报表: 特定季度的某个指标可能因统计口径变化而缺失。

这些缺失的数据点,我们称之为缺失值 (Missing Values)

首先,必须理解缺失值产生的原因

从统计学的角度,缺失值的产生机制可以分为三类。

理解它们,有助于我们选择更合适的处理方法,避免引入新的偏差。

类型一:完全随机缺失 (MCAR)

定义: 数据的缺失与任何值(无论是缺失值本身还是其他变量)都无关。

直观例子: 由于设备随机故障,导致某天的传感器数据未能记录。

这是最理想、最简单的缺失情况,因为缺失样本可以看作是全集数据的一个随机子集。

类型二:随机缺失 (MAR)

定义: 数据的缺失不完全是随机的,它依赖于其他已观测到的变量。

直观例子: 在市场调研中,高收入人群可能更不愿透露具体收入。

  • 收入字段的缺失与’教育水平’、’职业’等其他已知变量相关。
  • 但在同一个教育/职业水平分组内,收入的缺失又是随机的。

类型三:完全非随机缺失 (MNAR)

定义: 数据的缺失与缺失值本身有关。

直观例子: 在评估一款减肥产品的效果时,效果不佳的参与者可能中途退出。

  • 后续的体重数据缺失,恰恰是因为体重没有下降(缺失值本身)。
  • 这种缺失本身就包含了重要信息。这是最难处理的情况。

三种缺失机制的对比

机制类型 缺失是否依赖其他变量? 缺失是否依赖自身数值? 处理难度
MCAR
MAR
MNAR (可能)

正确判断缺失类型是选择处理策略的第一步。

缺失值的处理策略:删除 vs. 插补

处理缺失值主要有两种思路,每种都有其适用场景和风险。

1. 删除法 (Deletion)

  • 核心思想: ‘以空间换纯净’,通过丢弃部分数据换取信息矩阵的完整性。
  • 优点: 简单、直接。
  • 缺点: 可能导致严重的信息损失。

2. 插补法 (Imputation)

  • 核心思想: ‘用估计换完整’,用一个估计值来代替缺失值,以保留数据。
  • 优点: 保留了样本量。
  • 缺点: 可能引入噪声和偏差。

删除法:简单粗暴但需谨慎

  • 删除样本(行): 当某个数据点(行)包含一个或多个缺失值时,直接将该行删除。
    • 适用: 缺失值样本占总样本比例很小(例如 < 5%)且为MCAR。
    • 风险: 缺失比例高时,会丢失大量信息。
  • 删除特征(列): 当某个特征(列)的缺失值比例非常高时(例如 > 50%)。
    • 适用: 该特征有效信息过少,强行填充可能弊大于利。

插补法:更精细的主流选择

插补法是用一个估计值来代替缺失值,是更常用的方法。

根据估计方法的不同,可以分为统计量插补高级插补

统计量插补:简单快速的基础方法

  • 均值/中位数/众数插补:
    • 数值型特征: 使用该列非缺失数据的均值中位数填充。
    • 类别型特征: 使用该列非缺失数据的众数(出现次数最多的类别)填充。
  • 固定值填充:
    • 用一个特定的常数(如 0, -1, 或 'Unknown')来填充所有缺失值。

高级插补法:考虑特征间的关系

  • 前向/后向填充: 在时间序列数据中常用,用前一个 (ffill) 或后一个 (bfill) 时间点的值填充。

  • 模型预测插补: 将含缺失值的特征作为目标变量 Y,其他特征作为自变量 X,通过训练机器学习模型(如KNN随机森林)来预测缺失值。

  • 多重插补 (MI): 更稳健的方法。它会生成多组可能的插补值,在每个插补后的数据集上进行分析,最后汇总结果。

Python实践:缺失值检测

我们首先用 pandas 创建一个包含缺失值 (np.nan) 的 DataFrame

import pandas as pd
import numpy as np

df = pd.DataFrame(
  {'A': [None, 102, None, 644],
   'B': [10, None, None, 460], 
   'C': [100, 270, None, 480],
   'D': [None, 2000, 3000, None]})

接下来,我们将学习如何用 pandas 的内置函数来定位这些缺失值。

使用 .info() 快速概览数据

df.info() 方法可以提供数据框的整体信息,包括每列的非空值数量。

import pandas as pd
import numpy as np

df = pd.DataFrame(
  {'A': [None, 102, None, 644],
   'B': [10, None, None, 460], 
   'C': [100, 270, None, 480],
   'D': [None, 2000, 3000, None]})
print('df.info(): ')
df.info()

通过比较 Non-Null Count 和总行数 (4 entries),可以快速发现哪些列存在缺失。

使用 .isnull() 精准定位缺失值

df.isnull() 会返回一个布尔型的 DataFrameTrue 表示该位置是缺失值。

print('df.isnull()')
print(df.isnull())

这对于直观地查看缺失值的分布很有帮助。

使用 .isnull().sum() 统计缺失值

这是最常用的缺失值统计方法,它能计算出每列的缺失值总数。

print('df.isnull().sum()')
print(df.isnull().sum())

结果清晰地显示,A、B、D列各有2个缺失值,C列有1个。

Python实践:使用 dropna() 删除缺失值

dropna() 函数可以灵活地删除含有缺失值的行或列。

关键参数:

  • axis: 0 表示对行操作(默认),1 表示对列操作。
  • how: 'any' 表示只要有缺失值就删除(默认),'all' 表示全部是缺失值才删除。
  • thresh: 保留至少有N个非缺失值的行/列。
  • subset: 指定在哪些列的子集中检查缺失值。

dropna() 示例:删除任何包含缺失值的列

通过设置 axis=1how='any',我们可以删除数据框中任何包含NaN的列。

print(df.dropna(axis=1, how='any'))
print('指定 axis = 1,如果列中有缺失值,则删除该列')
print(df.dropna(axis=1, how='any'))

由于所有列都含有缺失值,所以结果是一个空的 DataFrame

dropna() 示例:删除任何包含缺失值的行

这是默认行为 (axis=0, how='any')。

print(df.dropna(axis=0, how='any'))
print('指定 axis = 0(默认),如果行中有缺失值,则删除该行')
print(df.dropna(axis=0, how='any'))

由于所有行也都含有缺失值,所以结果同样是空的。

dropna() 示例:使用 thresh 参数

我们可以保留那些信息量更完整的行,例如,保留至少有3个非NaN值的行。

print(df.dropna(axis=0, thresh=3))
print('保留至少有3个非NaN值的行')
print(df.dropna(axis=0, thresh=3))

只有索引为1的行 [102.0, NaN, 270.0, 2000.0] 被保留,因为它有3个非空值。

Python实践:使用 fillna() 插补缺失值

fillna() 是用于填充缺失值的核心函数。

关键参数:

  • value: 用于填充的标量值或字典/Series。
  • method: 填充方法,如 'pad'/'ffill' (前向填充) 或 'bfill'/'backfill' (后向填充)。
  • axis: 填充方向,0为纵向,1为横向。

fillna() 示例:前向填充 (Forward Fill)

使用 method='pad''ffill',用缺失值前面的有效值进行填充。

print(df.fillna(axis=0, method='pad'))
print('用缺失值前的值填充')
print(df.fillna(axis=0, method='pad'))

注意第一行的NaN没有被填充,因为它们前面没有值。

fillna() 示例:后向填充 (Backward Fill)

使用 method='bfill',用缺失值后面的有效值进行填充。

print(df.fillna(axis=0, method='bfill'))
print('用缺失值后的值填充')
print(df.fillna(axis=0, method='bfill'))  

注意最后一行的NaN没有被填充,因为它们后面没有值。

fillna() 示例:使用均值填充

这是一个非常常见的策略,用每列的均值来填充该列的缺失值。

print(df.fillna(df.mean()))
print('指定均值的方法来填充')    
print(df.fillna(df.mean()))

这种方法只对数值型列有效。

Python实践:基于模型的缺失值填充

对于更复杂的情况,我们可以使用机器学习模型进行填充,这通常能获得更好的效果。

我们将演示两种常用算法:

  1. K近邻 (KNN)
  2. 随机森林 (Random Forest)

模型填充思想:K近邻 (KNN)

核心思想: ‘物以类聚’。

  1. 找到与包含缺失值的样本最相似的 K 个样本(邻居)。
  2. 用这 K 个样本在该特征上的值的加权平均来填充缺失值。

这考虑了样本间的相似性,比简单的全局均值更合理。

Python实践:使用 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)
Figure 1

下面的代码片段展示了其核心用法。

from sklearn.impute import KNNImputer

# 假设 df 是我们含有缺失值的 DataFrame
# ...

# 初始化一个 KNNImputer,这里我们使用1个最近邻
imputer = KNNImputer(n_neighbors=1)

# 应用 imputer 来填充数据
imputed = imputer.fit_transform(df)

# 将结果转回 DataFrame
df_imputed = pd.DataFrame(imputed, columns=df.columns)

模型填充思想:随机森林

核心思想: ‘集思广益’。

  1. 将含有缺失值的列作为预测目标 Y
  2. 将其余列作为特征 X
  3. 在没有缺失值的数据上训练一个随机森林模型。
  4. 用训练好的模型来预测缺失值。

这个过程可以对所有含缺失值的列迭代进行,效果通常非常出色。

模块二:数据异常值处理

异常值 (Outliers) 是指数据集中与其他观测值差异极大的数据点。

它们可能是:

  • 错误: 数据录入错误、测量设备故障。
  • 真实的极端事件: ’双十一’的销售额、市场崩盘时的股价。

正确识别和妥善处理异常值至关重要。

异常值的检测方法

主要有两大类方法来发现这些’离群’的数据点。

1. 统计学方法

  • \(3\sigma\) 法则 (Pauta Criterion)
  • 基于数据的统计分布特性。
  • 优点: 客观、量化。
  • 缺点: 通常对数据分布有假设(如正态分布)。

2. 可视化方法

  • 箱线图 (Box Plot)
  • 通过图表直观地发现异常点。
  • 优点: 直观、对分布不敏感。
  • 缺点: 判断标准可能略带主观。

统计学方法:\(3\sigma\) 法则

该方法基于正态分布的特性:约 99.7% 的数据点会落在距离均值三个标准差 ($\sigma$) 的范围内。

\[ \large{ (\mu - 3\sigma, \mu + 3\sigma) } \]

因此,我们可以将超出此范围的数据点视为异常值。

重要前提: 数据必须近似服从正态分布。

Python实践:\(3\sigma\) 法则的局限性

我们来看一个例子。KsNormDetect 函数用于检验数据是否服从正态分布。

# 正态分布检验
def KsNormDetect(df):
    # ...
    res=kstest(df, 'norm', (u, std))[1]
    # 判断p值是否服从正态分布,p<=0.05 则服从正态分布
    if res<=0.05:
        return 1
    else:
        return 0

df = pd.DataFrame([..., 242467, ...], columns=["value"])
ks_res = KsNormDetect(df) # ks_res 会是 0
result = OutlierDetection(df, ks_res) # 函数会提示不服从正态分布

由于示例数据中存在一个极端值 242467,数据分布严重偏斜,正态性检验无法通过,因此无法应用 \(3\sigma\) 法则。

可视化方法:箱线图 (Box Plot)

箱线图是一种直观、鲁棒的异常值检测工具,它不依赖于数据的分布。

箱线图结构解释 一张图解释箱线图的各个组成部分,包括最小值、Q1、中位数、Q3、最大值和异常值。 异常值 下边缘 (Q1 - 1.5*IQR) Q1 (25%) 中位数 (Q2) Q3 (75%) 上边缘 (Q3 + 1.5*IQR) 异常值 四分位距 (IQR = Q3 - Q1)

通常,我们将超出上下边缘的数据点视为异常值。

Python实践:使用箱线图发现异常

我们可以用 matplotlib 绘制数据集中所有特征的箱线图,来快速发现异常。

Code
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 有一个明显的下限异常值。

异常值的处理方法

发现了异常值之后,我们有多种处理策略:

  1. 删除: 如果确定是录入错误,可以直接删除。
  2. 视为缺失值: 将其转换成 NaN,然后用前面介绍的插补法处理。
  3. 盖帽法 (Capping): 用设定的分位数(如1%和99%)来替换极端值。
  4. 不处理: 如果异常值代表了重要的真实事件(如欺诈交易),则应保留并深入分析。

Python实践:删除箱线图发现的异常值

根据上图的发现,我们可以用简单的 pandas 过滤操作来删除 V9 中的异常值。

# 原始数据有 N 行
# ...

# 删除 V9 列中值小于 -7.5 的行
train_data = train_data[train_data['V9'] > -7.5]

# 新数据行数 < N

这是一个简单有效的处理方法,但前提是我们有充分的理由相信这些点是需要被移除的。

模块三:数据转换

机器学习模型通常只能处理数值型数据

因此,我们需要将各种类型的数据,特别是类别型数据,转换为数值形式。同时,对数值型特征进行缩放也往往能提升模型性能。

数值型数据转换:为何以及如何缩放?

为何缩放?

  • 很多模型(如SVM、KNN、逻辑回归、神经网络)对特征的尺度非常敏感。
  • 尺度差异过大的特征会导致模型训练时,梯度下降方向被大尺度特征主导,影响收敛速度和精度。

如何缩放?

  • 标准化 (Standardization)归一化 (Normalization)

标准化 vs. 归一化

标准化 (Standardization)

  • 目标: 均值为0,标准差为1
  • 公式: \(z = (x - \mu) / \sigma\)
  • 范围: 无固定范围
  • 适用: 适用于数据近似正态分布,或模型对距离敏感的情况(如SVM, PCA)。

归一化 (Normalization)

  • 目标: 缩放到 [0, 1] 区间
  • 公式: \(x' = (x - x_{min}) / (x_{max} - x_{min})\)
  • 范围: [0, 1]
  • 适用: 适用于需要将特征缩放到固定范围的场景(如神经网络)。对异常值敏感。

类别型数据转换:核心挑战

模型无法理解 ‘北京’, ‘上海’, ‘绿色’, ‘红色’ 这样的文本信息。

我们需要一种方法,将这些类别信息编码为模型可以理解的数字,同时不引入错误的数学假设。

主要方法有两种:标签编码独热编码

标签编码 (Label Encoding)

方法: 将类别用连续的整数来表示。

例子: {'S': 1, 'M': 2, 'L': 3}

适用场景:

  • 有序变量 (Ordinal Variables),即类别本身具有明确的顺序关系。
  • 例如:衣服尺码 (S < M < L),教育程度(小学 < 中学 < 大学)。

风险: 若用于无序变量,会让模型错误地学习到类别间的大小关系(如 上海 > 北京)。

独热编码 (One-Hot Encoding)

方法: 将一个有 N 个类别的特征,转换为 N 个新的二进制 (0/1) 特征。

例子:

city -> city_北京 city_上海 city_天津
北京 1 0 0
上海 0 1 0
天津 0 0 1

适用场景:

  • 名义变量 (Nominal Variables),即类别间没有顺序关系。
  • 例如:城市名称、商品品类。

Python实践:独热编码

scikit-learnDictVectorizer 可以方便地对字典列表形式的数据进行独热编码。

from sklearn.feature_extraction import DictVectorizer

onehot_encoder = DictVectorizer()
instances = [{'city':'北京'},{'city':'天津'},{'city':'上海'}]
# 输出结果是一个稀疏矩阵,.toarray() 将其转为密集数组
print(onehot_encoder.fit_transform(instances).toarray())
from sklearn.feature_extraction import DictVectorizer
onehot_encoder = DictVectorizer()
instances = [{'city':'北京'},{'city':'天津'},{'city':'上海'}]
print(onehot_encoder.fit_transform(instances).toarray())

Python实践:有序标签编码

对于有序变量,我们可以使用 pandasmap 方法,通过一个自定义的映射字典来实现。

import pandas as pd
df=pd.DataFrame([['green','M',10.1,'class1'],['red','L',13.5,'class2'],['blue','XL',15.3,'class1'],],
                columns=['color','size','price','classlabel'])
print('编码前\n',df)

size_mapping={'XL':3,'L':2,'M':1}
df['size']=df['size'].map(size_mapping)#size表示列名
print('编码后\n',df)

代码片段如下:

# 假设 df 中有 'size' 列,值为 'M', 'L', 'XL'
size_mapping = {'XL':3, 'L':2, 'M':1}
df['size'] = df['size'].map(size_mapping)

这样就将尺码成功转换为了具有大小关系的数值。

模块四:特征选择与降维

问题: 面对含有几十甚至上百个特征的高维数据,会引发’维度灾难’。

  • 计算成本急剧增加。
  • 模型更容易过拟合,泛化能力下降。

解决方案:

  • 特征选择 (Feature Selection): 挑出一个最优的特征子集
  • 降维 (Dimensionality Reduction): 将特征变换到一个更低的维度。

特征选择:做’减法’的艺术

从原始特征集中挑选出一部分特征,移除冗余或不相关的特征。

  • 过滤式 (Filter):
    • 方差过滤: 移除方差很小的特征。
    • 相关系数法: 保留与目标变量相关性高的特征,移除特征间相关性过高的特征。
  • 嵌入式 (Embedded):
    • L1正则化 (Lasso): 通过惩罚项使不重要特征的系数变为0。
    • 基于树模型的特征重要性: 使用决策树、随机森林等模型输出的特征重要性得分进行筛选。

降维:做’变换’的艺术

通过数学变换,将高维特征空间映射到一个低维子空间,生成新的、数量更少的特征。

  • 主成分分析 (PCA):
    • 最经典的线性降维方法。
    • 旨在找到一组新的正交基(主成分),使得数据在这些基上的投影方差最大化
    • 新生成的每个主成分都是原始特征的线性组合。

Python实践:相关性热力图进行特征选择

热力图可直观展示所有变量间的相关性,是过滤式特征选择的有力工具。

Code
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: 特征相关性热力图

Python实践:基于相关性阈值筛选特征

在生成相关性矩阵后,我们可以设定一个阈值(如与目标变量的相关性绝对值 > 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']]

Python实践:使用PCA进行降维

scikit-learnPCA 模块非常易用。我们可以通过两种方式指定降维的程度。

方式一: 指定希望保留的原始信息量(方差)。

from sklearn.decomposition import PCA

# 实例化PCA,保留90%的方差
pca = PCA(n_components=0.9)
new_train_pca_90 = pca.fit_transform(train_data_scaler.iloc[:,0:-1])

方式二: 直接指定降维后的主成分数量。

# 实例化PCA,保留16个主成分
pca = PCA(n_components=16)
new_train_pca_16 = pca.fit_transform(train_data_scaler.iloc[:,0:-1])

这两种方法让我们可以在’信息保留’和’维度降低’之间做出权衡。

模块五:不平衡数据集处理

不平衡数据集: 数据集中某个类别的样本数量远多于其他类别。

常见商业场景:

  • 客户流失预测 (流失客户 vs. 留存客户)
  • 信用卡欺诈检测 (欺诈交易 vs. 正常交易)
  • 贷款违约预测 (违约用户 vs. 正常用户)

我们真正关心的少数类样本数量极少。

为何不平衡数据是个严重问题?

  • 模型偏见:
    • 模型为了最小化总体分类错误率,会倾向于将所有样本都预测为多数类
    • 这会导致模型在少数类上的召回率 (Recall) 极低,失去了商业价值。
  • 评估指标误导:
    • 准确率 (Accuracy) 会失效。例如,99%是正常用户,模型把所有人都预测为正常,准确率高达99%,但这个模型毫无用处,因为它一个违约用户也找不到。

应对不平衡数据,需要新的评估指标

在处理不平衡问题时,我们必须关注更能反映少数类预测性能的指标:

  • 精确率 (Precision): TP / (TP + FP)
    • 在所有被预测为正例的样本中,真正是正例的比例。
  • 召回率 (Recall): TP / (TP + FN)
    • 在所有真正的正例中,被成功预测出来的比例。(通常最关键!
  • F1分数 (F1-Score): 2 * (Precision * Recall) / (Precision + Recall)
    • 精确率和召回率的调和平均数。
  • ROC AUC: ROC曲线下的面积,综合评估模型排序能力。

处理不平衡数据集的核心思想:重采样

重采样 (Resampling) 是在数据层面解决不平衡问题的主要方法。

1. 过采样 (Over-sampling)

  • 目标: 增加少数类样本的数量。
  • 方法:
    • 随机过采样 (简单复制)
    • SMOTE (合成新样本)
    • ADASYN, Borderline-SMOTE

2. 欠采样 (Under-sampling)

  • 目标: 减少多数类样本的数量。
  • 方法:
    • 随机欠采样 (随机丢弃)
    • Tomek Links (清洗类别边界)

过采样明星算法:SMOTE

SMOTE (Synthetic Minority Over-sampling Technique) 是一种非常有效的过采样方法。

它不是简单地复制少数类样本,而是通过在少数类样本与其近邻之间进行线性插值来合成新的、与原始样本相似但又不完全相同的样本。

这能有效避免随机过采样带来的过拟合问题。

Python实践:使用 imbalanced-learn

imbalanced-learn 是一个专门用于处理不平衡数据集的Python库,它实现了包括SMOTE在内的多种重采样算法。

我们将用它来比较不同过采样方法在车险欺诈预测任务上的性能。

Python实践:比较不同采样方法

下面的代码框架展示了如何应用不同的采样方法,并用一套统一的评估指标来衡量它们的效果。

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)
    # 评估性能
    # ...

结果分析:哪种方法更好?

Table 1: 不同过采样方法在XGBoost模型上的性能比较
# 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的不同侧重来选择最合适的策略。

总结:数据预处理是一项系统工程

今天我们学习了数据预处理的五大核心模块:

  • 缺失值处理: 理解缺失机制,选择删除或插补。
  • 异常值处理: 使用统计或可视化方法检测,谨慎处理。
  • 数据转换: 对数值型数据缩放,对类别型数据编码。
  • 特征工程: 通过选择或降维,应对维度灾难。
  • 不平衡处理: 使用重采样方法,关注正确评估指标。

关键启示:迭代与探索

数据预处理不是一个线性的、一劳永逸的过程。

它通常需要根据模型的反馈进行多次迭代和调整。最好的预处理策略,源于对业务的深刻理解数据的不断探索

先理解你的数据,再选择你的工具’。

问答环节 (Q&A)

感谢聆听!