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

数据预处理是任何数据分析或机器学习项目中最关键的步骤之一。古人云:“工欲善其事,必先利其器”,在数据科学领域,高质量的数据就是我们最锋利的“器”。原始数据往往是“不干净”的,充满了缺失、异常、格式不一致等问题。本章将系统介绍数据预处理中的核心技术,包括缺失值处理、异常值检测、数据类型转换、特征选择与降维,以及如何应对不平衡数据集。我们将结合理论知识与Python实践,为后续的商业建模与分析打下坚实的基础。

2.1 数据缺失值处理

在真实的商业场景中,我们拿到的数据几乎不可能是完美的。例如,在分析用户消费数据时,某些用户的年龄或收入信息可能为空;在处理财务报表时,特定季度的某个指标可能因为统计口径变化而缺失。这些缺失的数据点,我们称之为“缺失值”(Missing Values)。

2.1.1 缺失值的类型

从统计学的角度,缺失值的产生机制可以分为三类,理解它们有助于我们选择更合适的处理方法:

  1. 完全随机缺失 (Missing Completely at Random, MCAR): 数据的缺失与任何值(无论是缺失值本身还是其他变量)都无关。例如,由于设备随机故障导致某天的传感器数据未能记录。这是最理想的缺失情况。
  2. 随机缺失 (Missing at Random, MAR): 数据的缺失不完全是随机的,它依赖于其他观测到的变量。例如,在市场调研中,高收入人群可能更不愿意透露自己的具体收入,导致收入字段的缺失与“教育水平”、“职业”等其他已知变量相关。
  3. 完全非随机缺失 (Missing Not at Random, MNAR): 数据的缺失与缺失值本身有关。例如,在评估一款减肥产品的效果时,效果不佳的参与者可能中途退出,导致他们后续的体重数据缺失。这种缺失本身就包含了重要信息。

2.1.2 缺失值的处理方法

处理缺失值主要有两种思路:删除插补

2.1.2.1 删除法

删除法简单直接,但可能导致信息损失,是一种“以减少数据来换取信息完整”的策略。

  • 删除样本(行): 当一个数据点(行)包含一个或多个缺失值时,直接将该行数据删除。这种方法适用于缺失值样本占比较小的情况。如果缺失比例过高,删除样本会丢失大量信息。
  • 删除特征(列): 当某个特征(列)的缺失值比例非常高(例如超过50%)时,可以考虑删除整个特征。因为过多的缺失值意味着该特征的有效信息非常有限,强行填充可能会引入更多噪声。

2.1.2.2 插补法 (Imputation)

插补法是用一个估计值来代替缺失值,以保留数据的完整性。这是更常用也更复杂的方法。

  • 统计量插补:
    • 均值/中位数/众数插补: 对数值型特征,使用该列的均值或中位数填充;对类别型特征,使用众数填充。这是最简单的插补法,但会影响数据的原始分布,低估方差。
    • 固定值填充: 用一个特定的常数(如0, -1, 或”Unknown”)来填充所有缺失值。
  • 高级插补法:
    • 前向/后向填充: 在时间序列数据中尤为常用,用前一个或后一个时间点的值来填充缺失值。
    • 模型预测插补: 将含有缺失值的特征作为目标变量(Y),其他特征作为自变量(X),通过训练机器学习模型(如K近邻(KNN)、随机森林、线性回归)来预测缺失值。这种方法考虑了特征间的关系,通常效果更好。
    • 多重插补 (Multiple Imputation, MI): 认为待插补的值是随机的,通过贝叶斯估计等方法生成多组可能的插补值,形成多个完整的数据集。分别在这些数据集上进行分析,最后将结果汇总。这是一种更稳健但计算也更复杂的方法。

2.1.3 Python实践:缺失值处理

接下来,我们通过Python代码来演示如何检测和处理缺失值。

2.1.3.1 缺失值检测与基础处理

首先,我们创建一个包含缺失值(在 numpy 中表示为 np.nan)的 DataFrame,并使用 pandas 库的内置函数进行检测和基础处理。如 列表 lst-na-detection 所示。

列表 2.1
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()#查看数据概况
print('df.isnull()')
print(df.isnull())#查看缺失值情况
print('df.isnull().sum()')
print(df.isnull().sum())
print('检测每行中缺失值的数量')
print(df.isnull().T.sum())

#如果出现缺失值的行/列重要性不大的话,可以直接使用 dropna() 删除带有缺失值的行/列
"""
df.dropna(axis=0,
     how='any',
     thresh=None,
     subset=None,
     inplace=False)
axis:控制行列的参数,0 行,1 列。
how:any,如果有 NaN,删除该行或列;all,如果所有值都是 NaN,删除该行或列。
thresh:指定 NaN 的数量,当 NaN 数量达到才删除。
subset:要考虑的数据范围,如:删除缺失行,就用subset指定参考的列,默认是所有列。
inplace:是否修改原数据,True直接修改原数据,返回 None,False则返回处理后的数据框。
"""
print('如下,指定 axis = 1,如果列中有缺失值,则删除该列')
print(df.dropna(axis=1, how='any'))
print('指定 axis = 0(默认),如果行中有缺失值,则删除该行')
print(df.dropna(axis=0, how='any'))
print('以 ABC 列为参照,删除这三列都是缺失值的行')
print(df.dropna(axis=0, subset=['A', 'B', 'C'], how='all'))
print('保留至少有3个非NaN值的行')
print(df.dropna(axis=0, thresh=3))


"""
使用 fillna() 填补缺失值。
df.fillna(value=None,
     method=None,
     axis=0,
     inplace=False,
     limit=None)
"""
print('指定填充值为 0')
print('用缺失值前的值填充')
"""
当method 值为 ffill 或 pad时,按前一个值进行填充。

当 axis = 0,用缺失值同一列的上一个值填充,如果缺失值在第一行则不填充。

当 axis = 1,用缺失值同一行的上一个值填充,如果缺失值在第一列则不填充。
"""
    
print(df.fillna(axis=0, method='pad'))
"""
按后一个值填充

当method 值为 backfill 或 bfill时,按后一个值进行填充。

当 axis = 0,用缺失值同一列的下一个值填充,如果缺失值在最后一行则不填充。

当 axis = 1,用缺失值同一行的下一个值填充,如果缺失值在最后一列则不填充。
"""
print('用缺失值后的值填充')
print(df.fillna(axis=0, method='bfill'))  
print('指定均值的方法来填充')    
print(df.fillna(df.mean()))

列表 lst-na-detection 的输出中,我们可以清晰地看到 info()isnull()sum() 如何帮助我们定位缺失值。dropna() 函数通过不同的参数组合,可以灵活地实现删除策略。而 fillna() 则展示了如何使用前向填充 (pad)、后向填充 (bfill) 以及均值来插补缺失值。

2.1.3.2 基于模型的缺失值填充

对于更复杂的情况,我们可以使用机器学习模型进行填充。下面我们将演示使用 K近邻(KNN) 和 随机森林(Random Forest) 两种算法来填充缺失值。

KNN填充的思想是:找到与包含缺失值的样本最相似的 K 个样本,用这 K 个样本在该特征上的值的加权平均来填充缺失值。

随机森林填充的思想是:将含有缺失值的列作为预测目标,其他列作为特征,在没有缺失值的数据上训练一个随机森林模型,然后用这个模型来预测缺失值。这个过程可以对所有含缺失值的列迭代进行。

列表 lst-model-imputation 代码展示了这两种方法的实现。

列表 2.2
#KNN填充
import numpy as np
import pandas as pd
from sklearn.impute import KNNImputer
df = 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]})
print(df)


imputer = KNNImputer(n_neighbors=1)
imputed = imputer.fit_transform(df)
df_imputed = pd.DataFrame(imputed, columns=df.columns)
print(df_imputed)
#随机森林填充

df = 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]})
print(df)
print('df.info()')
df.info()
data3=df.copy()
#获取含有缺失值的特征
miss_index=data3.isna().any()[data3.isna().any().values==True].index.tolist()
#按照缺失值多少,由小至大排序,并返回索引
sort_miss_index=np.argsort(data3[miss_index].isna().sum(axis=0)).values
print('sort_miss_index',sort_miss_index)
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestRegressor
for i in sort_miss_index:
  data3_list=data3.columns.tolist()#特征名
  data3_copy=data3.copy()
  fillc=data3_copy.iloc[:,i]#需要填充缺失值的一列
  #从特征矩阵中删除这列,因为要根据已有信息预测这列
  df=data3_copy.drop(data3_list[i],axis=1)
  #将已有信息的缺失值暂用0填补
  df_0=SimpleImputer(missing_values=np.nan,strategy='constant',fill_value=0).fit_transform(df)

  Ytrain=fillc[fillc.notnull()]#训练集标签为填充列含有数据的一部分
  Ytest=fillc[fillc.isnull()]#测试集标签为填充列含有缺失值的一部分

  Xtrain=df_0[Ytrain.index,:]#通过索引获取Xtrain和Xtest
  Xtest=df_0[Ytest.index,:]

  rfc=RandomForestRegressor(n_estimators=100)#实例化
  rfc=rfc.fit(Xtrain,Ytrain)#导入训练集进行训练
  Ypredict=rfc.predict(Xtest)#将Xtest传入predict方法中,得到预测结果
  #获取原填充列中缺失值的索引
  the_index=data3[data3.iloc[:,i].isnull()==True].index.tolist()
  data3.iloc[the_index,i]=Ypredict#
print('随机森林填充结果 \n',data3)

2.2 数据异常值处理

异常值(Outliers)是指数据集中与其他观测值差异极大的数据点。在商业分析中,异常值可能是数据录入错误,也可能代表着真实但极端的商业事件,如“双十一”的销售额、市场崩盘时的股价等。因此,识别和妥善处理异常值至关重要。

2.2.1 异常值的检测方法

2.2.1.1 统计学方法

  • \(3\sigma\)法则 (Pauta Criterion): 该方法基于正态分布。我们知道,在正态分布中,约99.7%的数据点会落在距离均值三个标准差(\(\sigma\))的范围内。因此,我们可以将超出 \((\mu - 3\sigma, \mu + 3\sigma)\) 范围的数据点视为异常值。前提条件是数据近似服从正态分布。

2.2.1.2 可视化方法

  • 箱线图 (Box Plot): 箱线图是一种非常直观的异常值检测工具。它展示了数据的四分位数(Quartiles)。箱体的上边缘是上四分位数(Q3,75%分位点),下边缘是下四分位数(Q1,25%分位点)。箱体的高度被称为四分位距(IQR = Q3 - Q1)。通常,我们将低于 Q1 - 1.5 * IQR 或高于 Q3 + 1.5 * IQR 的数据点定义为异常值。

2.2.2 异常值的处理方法

  1. 删除: 如果异常值是由于明显的录入错误造成的,可以直接删除。但如果异常值数量较多,删除会损失信息。
  2. 视为缺失值: 将异常值转换成缺失值,然后采用上一节 sec-missing-values 介绍的方法进行插补。
  3. 盖帽法 (Capping): 也称为“缩尾”,即用一个设定的分位数(如1%和99%)来替换低于或高于该分位数的数据。例如,将所有低于1%分位数的值替换为1%分位数的值。
  4. 不处理: 在某些情况下,异常值本身就是分析的重点,比如在金融风控中检测欺诈交易。此时,我们不应处理异常值,而是要深入分析它们。

2.2.3 Python实践:异常值处理

2.2.3.1 基于 \(3\sigma\) 法则的异常值检测

我们首先需要检验数据是否服从正态分布,这里使用 scipy.stats 中的 kstest (Kolmogorov-Smirnov test)。如果检验通过(通常p值小于0.05),我们就可以应用 \(3\sigma\) 法则。列表 lst-outlier-3sigma 代码封装了这一过程。

列表 2.3
#题目一
import numpy as np
import pandas as pd
from scipy.stats import kstest
# 正态分布检验
def KsNormDetect(df):
    # 计算均值
    u = df['value'].mean()
    # 计算标准差
    std = df['value'].std()
    # 计算P值
    res=kstest(df, 'norm', (u, std))[1]
    # 判断p值是否服从正态分布,p<=0.05 则服从正态分布,否则不服从。
    if res<=0.05:
        print('该列数据服从正态分布------------')
        print('均值为:%.3f,标准差为:%.3f' % (u, std))
        print('------------------------------')
        return 1
    else:
        return 0
# 异常值检测并删除
def OutlierDetection(df, ks_res):
    # 计算均值
    u = df['value'].mean()
    # 计算标准差
    std = df['value'].std()
    if ks_res==1:
        # 定义3σ法则识别异常值
        # 识别异常值
        error = df[np.abs(df['value'] - u) > 3 * std]
        # 剔除异常值,保留正常的数据
        data_c = df[np.abs(df['value'] - u) <= 3 * std]
        # 输出异常数据
        print(error)
        return error
    else:
        print('请先检测数据是否服从正态分布-----------')
        return None
df = pd.DataFrame([111, 333, 12, 290, 265, 152, 222, 1213, 242467, 114, 231, 122, 33, 2, 1, 5, 22, 44], columns=["value"])
ks_res = KsNormDetect(df)
#调用函数OutlierDetection进行异常值检测,结果存储再result中
result = OutlierDetection(df, ks_res)
print(result)
代码解读

列表 lst-outlier-3sigma 中,KsNormDetect 函数首先执行正态性检验。由于示例数据中存在一个极端值 242467,导致数据分布严重偏斜,kstest 的结果会显示数据不服从正态分布(返回值ks_res为0)。因此 OutlierDetection 函数会提示数据不服从正态分布,并返回 None。这恰好说明了直接应用 \(3\sigma\) 法则的局限性,它对数据的分布有严格要求。

2.2.3.2 基于箱线图的异常值分析与处理

箱线图法不依赖于数据的特定分布,因此具有更强的普适性。图 fig-outlier-boxplot-vis 展示了如何使用 matplotlib 绘制箱线图来可视化数据分布和异常点。

#题目二
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns


from scipy import stats


import warnings
warnings.filterwarnings("ignore")
 
#%matplotlib inline


train_data_file ="zhengqi_train.txt"
test_data_file = "zhengqi_test.txt"


train_data = pd.read_csv(train_data_file, sep='\t', encoding='utf-8')
test_data = pd.read_csv(test_data_file, 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()


#删除异常值
train_data=train_data[train_data['V9']>-7.5]


#最大最小值归一化
from sklearn import preprocessing
features_columns = [col for col in train_data.columns if col not in ['target']]
min_max_scaler = preprocessing.MinMaxScaler()
min_max_scaler = min_max_scaler.fit(train_data[features_columns])
train_data_scaler = min_max_scaler.transform(train_data[features_columns])
test_data_scaler = min_max_scaler.transform(test_data[features_columns])


train_data_scaler = pd.DataFrame(train_data_scaler)
train_data_scaler.columns = features_columns


test_data_scaler = pd.DataFrame(test_data_scaler)
test_data_scaler.columns = features_columns


train_data_scaler['target'] = train_data['target']


#查看训练集数据和测试集数据分布情况
dist_cols = 6
dist_rows = len(test_data_scaler.columns)


plt.figure(figsize=(4*dist_cols,4*dist_rows))


for i, col in enumerate(test_data_scaler.columns):
  ax=plt.subplot(dist_rows,dist_cols,i+1)
  ax = sns.kdeplot(train_data_scaler[col], color="Red", shade=True)
  ax = sns.kdeplot(test_data_scaler[col], color="Blue", shade=True)
  ax.set_xlabel(col)
  ax.set_ylabel("Frequency")
  ax = ax.legend(["train","test"])
plt.show()


drop_col = 6
drop_row = 1


plt.figure(figsize=(5*drop_col,5*drop_row))


for i, col in enumerate(["V5","V9","V11","V17","V22","V28"]):
  ax =plt.subplot(drop_row,drop_col,i+1)
  ax = sns.kdeplot(train_data_scaler[col], color="Red", shade=True)
  ax= sns.kdeplot(test_data_scaler[col], color="Blue", shade=True)
  ax.set_xlabel(col)
  ax.set_ylabel("Frequency")
  ax = ax.legend(["train","test"])
plt.show()
图 2.1
潜在错误提示

2.2.3.3 错误分析

在上述 图 fig-outlier-boxplot-vis 代码中,脚本末尾使用了 plt.savefig() 函数来保存图片。然而,plt.show() 函数会显示图形并清空当前的图形对象。因此,在 plt.show() 之后调用 plt.savefig() 通常会保存一个空白的图像。

2.2.3.4 正确写法

为了正确保存图像,plt.savefig() 应该在 plt.show() 之前调用:

# ... (绘图代码) ...
plt.savefig("output_filename.png") # 先保存
plt.show() # 再显示

2.2.3.5 重要提醒

为通过平台检测,在线练习时仍需按原始错误代码输入。

图 fig-outlier-boxplot-vis 的箱线图中,我们可以清楚地看到变量 V9 有一个明显的下限异常值。代码随后通过 train_data=train_data[train_data['V9']>-7.5] 这一行将其删除。后续的核密度估计图(KDE plot)则用于比较处理和归一化后,训练集和测试集的数据分布是否一致,这是确保模型泛化能力的重要步骤。

2.3 数据转换:数值型与类别型

机器学习模型通常只能处理数值型数据。因此,我们需要将各种类型的数据,特别是类别型数据,转换为数值形式。同时,对数值型特征进行缩放(Scaling)也往往能提升模型的收敛速度和性能。

2.3.1 数值型数据转换

  • 标准化 (Standardization): 将数据转换为均值为0,标准差为1的正态分布。计算公式为 \(z = (x - \mu) / \sigma\)。标准化后的数据没有固定的取值范围,适用于那些对距离敏感且假设数据呈正态分布的模型,如SVM、逻辑回归。
  • 归一化 (Normalization): 将数据缩放到一个固定的区间,通常是 [0, 1][-1, 1]。计算公式为 \(x' = (x - x_{min}) / (x_{max} - x_{min})\)。归一化适用于那些对数据范围敏感的模型,如神经网络。

2.3.2 类别型数据转换

  • 标签编码 (Label Encoding): 将类别型数据用连续的整数来表示,如 {'S': 0, 'M': 1, 'L': 2}。这种方法适用于本身具有顺序关系的类别(有序变量),如衣服尺码、教育程度。
  • 独热编码 (One-Hot Encoding): 将一个具有 N 个类别的特征转换为 N 个二进制(0或1)特征。每个新特征代表一个原始类别。这种方法适用于没有顺序关系的类别(名义变量),如城市名称、商品品类。它可以避免模型错误地学习到类别间的顺序关系。

2.3.3 Python实践:数据转换

列表 lst-data-transformation 代码演示了如何使用 scikit-learnpandas 进行独热编码和有序编码。

列表 2.4
from sklearn.feature_extraction import DictVectorizer
import pandas as pd

onehot_encoder = DictVectorizer()
instances = [{'city':'北京'},{'city':'天津'},{'city':'上海'}]#这个可以使用相同的key值city是因为它们属于不同的字典中
print(onehot_encoder.fit_transform(instances).toarray())


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)

列表 lst-data-transformation 中,DictVectorizer 实现了对城市名称的独热编码。而对于有序变量“size”,我们通过创建一个映射字典 size_mapping 并使用 pandasmap 方法,实现了有序的标签编码。

2.4 特征选择与降维

在商业数据分析中,我们常常面对含有几十甚至上百个特征的数据集(高维数据)。高维数据不仅会增加计算成本,还可能因为“维度灾难”而降低模型性能。特征选择和降维是解决这一问题的两种主要方法。

  • 特征选择 (Feature Selection): 从原始特征集中挑选出一个子集,使得模型在该子集上的性能最优。被移除的特征被认为是冗余或不相关的。
  • 降维 (Dimensionality Reduction): 通过某种数学变换,将高维特征空间映射到一个低维子空间,生成新的、数量更少的特征。每个新特征都是原始特征的某种组合。

2.4.1 特征选择方法

  1. 过滤式 (Filter): 在模型训练之前,先对特征进行评估和筛选。
    • 方差过滤: 移除方差较小或为0的特征,因为这些特征在不同样本间几乎没有变化,包含的信息量很少。
    • 相关系数法: 计算特征与目标变量之间的相关系数(如皮尔逊Pearson、斯皮尔曼Spearman),保留相关性高的特征。同时,也可以计算特征之间的相关系数,移除那些高度相关的特征以减少多重共线性。
  2. 嵌入式 (Embedded): 将特征选择过程与模型训练过程融为一体。
    • L1正则化 (Lasso): 在模型的损失函数中加入L1惩罚项,可以使得不重要的特征的系数变为0,从而实现自动特征选择。
    • 基于树模型的特征重要性: 决策树、随机森林、梯度提升树等模型在训练后可以输出每个特征的重要性得分,据此进行筛选。

2.4.2 降维方法

  • 主成分分析 (Principal Component Analysis, PCA): 一种最经典的线性降维方法。PCA旨在找到一组新的正交基(主成分),使得数据在这些基上的投影方差最大化。它通过保留方差最大的前k个主成分来实现降维,关注的是数据本身的方差。

2.4.3 Python实践:特征选择与降维

我们将通过一个综合案例来展示如何应用相关性分析进行特征选择,并使用PCA进行降维。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
import warnings
warnings.filterwarnings("ignore")
from sklearn.decomposition import PCA  #主成分分析法
from sklearn import preprocessing

train_data_file ="zhengqi_train.txt"
test_data_file = "zhengqi_test.txt"

train_data = pd.read_csv(train_data_file, sep='\t', encoding='utf-8')
test_data = pd.read_csv(test_data_file, 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()
min_max_scaler = min_max_scaler.fit(train_data[features_columns])
train_data_scaler = min_max_scaler.transform(train_data[features_columns])
test_data_scaler = min_max_scaler.transform(test_data[features_columns])

train_data_scaler = pd.DataFrame(train_data_scaler)
train_data_scaler.columns = features_columns

test_data_scaler = pd.DataFrame(test_data_scaler)
test_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()

mcorr=mcorr.abs()
numerical_corr=mcorr[mcorr['target']>0.1]['target']
print(numerical_corr.sort_values(ascending=False))

index0 = numerical_corr.sort_values(ascending=False).index
print(train_data_scaler[index0].corr('spearman'))

features_corr = numerical_corr.sort_values(ascending=False).reset_index()
features_corr.columns = ['features_and_target', 'corr']
# 筛选出大于相关性大于0.3的特征
features_corr_select = features_corr[features_corr['corr']>0.3] 
print('筛选出大于相关性大于0.3的特征',features_corr_select)
select_features = [col for col in features_corr_select['features_and_target'] if col not in ['target']]
new_train_data_corr_select = train_data_scaler[select_features+['target']]
new_test_data_corr_select = test_data_scaler[select_features]
'''from statsmodels.stats.outliers_influence import variance_inflation_factor #多重共线性方差膨胀因子

#多重共线性
new_numerical=['V0', 'V2', 'V3', 'V4', 'V5', 'V6', 'V10','V11', 
             'V13', 'V15', 'V16', 'V18', 'V19', 'V20', 'V22','V24','V30', 'V31', 'V37']
X=np.matrix(train_data_scaler[new_numerical])
VIF_list=[variance_inflation_factor(X, i) for i in range(X.shape[1])]
print(VIF_list)
'''

#PCA方法降维
#保持90%的信息
pca = PCA(n_components=0.9)
new_train_pca_90 = pca.fit_transform(train_data_scaler.iloc[:,0:-1])
new_test_pca_90 = pca.transform(test_data_scaler)
new_train_pca_90 = pd.DataFrame(new_train_pca_90)
new_test_pca_90 = pd.DataFrame(new_test_pca_90)
new_train_pca_90['target'] = train_data_scaler['target']
print('保持90%的信息',new_train_pca_90.head())

#PCA方法降维
#保留16个主成分
pca = PCA(n_components=16)
new_train_pca_16 = pca.fit_transform(train_data_scaler.iloc[:,0:-1])
new_test_pca_16 = pca.transform(test_data_scaler)
new_train_pca_16 = pd.DataFrame(new_train_pca_16)
new_test_pca_16 = pd.DataFrame(new_test_pca_16)
new_train_pca_16['target'] = train_data_scaler['target']
print('保留16个主成分',new_train_pca_16.head())
图 2.2
潜在错误提示

2.4.3.1 错误分析

在上述 图 fig-feature-selection 代码中,plt.savefig("1.png")plt.show() 之后被调用。plt.show() 函数会显示图形并清除当前图形画布,导致后续的 savefig 保存的是一个空白图像。

2.4.3.2 正确写法

正确的顺序是先保存再显示:

# ... (heatmap 绘图代码) ...
plt.savefig("1.png") # 先保存
plt.show() # 后显示

此外,代码中 maskdtype 被设置为 np.bool,这是一个已弃用的别名,推荐使用 np.bool_ 或 Python 内置的 bool

2.4.3.3 重要提醒

为通过平台检测,在线练习时仍需按原始错误代码输入。

该代码首先绘制了如 图 fig-feature-selection 所示的特征相关性热力图,直观展示了所有变量间的斯皮尔曼相关性。然后,通过设定阈值(与目标变量相关性>0.3)来筛选特征。最后,代码演示了两种PCA的应用方式:一种是指定保留的主成分数量(n_components=16),另一种是指定希望保留的原始信息量(n_components=0.9,即保留90%的方差)。

2.5 不平衡数据集处理

在很多商业问题中,我们关心的小概率事件,如客户流失、信用卡欺诈、贷款违约等,其样本数量远少于正常样本。这种“正负样本比例严重失衡”的数据集被称为不平衡数据集。如果直接用不平衡数据训练模型,模型会倾向于预测样本量多的类别,导致对我们真正关心的少数类的预测性能很差。

2.5.1 不平衡数据集的缺陷

  • 模型偏见: 模型为了最小化总体分类错误率,会“牺牲”少数类,导致模型在少数类上的召回率(Recall)极低。
  • 评估指标误导: 在严重不平衡数据下,准确率(Accuracy)会失效。例如,99%的样本为负例,模型即使将所有样本都预测为负例,准确率也能达到99%,但这样的模型毫无用处。因此,我们需要关注精确率(Precision)召回率(Recall)F1分数(F1-Score)ROC AUC等指标。

2.5.2 处理不平衡数据集的方法

处理不平衡问题主要在数据层面进行,核心思想是重采样(Resampling)

  • 过采样 (Over-sampling): 增加少数类样本的数量。
    • 随机过采样: 简单地重复抽样少数类样本。缺点是可能导致过拟合。
    • SMOTE (Synthetic Minority Over-sampling Technique): 一种改进的过采样方法。它不是简单复制样本,而是通过在少数类样本与其近邻之间进行线性插值来合成新的、与原始样本相似但又不完全相同的样本。
    • ADASYN, Borderline-SMOTE: 都是SMOTE的变体,它们会更关注那些在分类边界上、更难学习的少数类样本,并为它们生成更多的新样本。
  • 欠采样 (Under-sampling): 减少多数类样本的数量。
    • 随机欠采样: 随机地从多数类中丢弃一部分样本。缺点是可能丢失多数类的重要信息。
    • Tomek Links: 一种清洗方法,它会找到属于不同类别但互为最近邻的“Tomek Links”对,并移除其中的多数类样本,从而使类别边界更清晰。

2.5.3 Python实践:不平衡数据集处理

我们将使用 imbalanced-learn 库来演示不同的过采样方法,并比较它们在车险欺诈预测任务上的性能。

表 2.1: 不同过采样方法在XGBoost模型上的性能比较
import pandas as pd
import numpy as np
import xgboost as xgb
from sklearn.model_selection import StratifiedKFold, train_test_split
from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score, roc_auc_score
from imblearn.over_sampling import RandomOverSampler, SMOTE, ADASYN, BorderlineSMOTE
 
# 加载数据集(请替换为实际的信用卡欺诈数据集)
df = pd.read_csv("车险欺诈train.csv")
 
#把时间列policy_bind_date转为数值
df['policy_bind_date'] = pd.to_datetime(df['policy_bind_date']).view(np.int64)
 
#划分数据集,倒数第一列为taget,其余为特征
X = df.iloc[:, :-1]
y = df.iloc[:, -1]
#将进行one-hot编码
X = pd.get_dummies(X)
 
 
# 数据准备
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)
 
# 采样方法
sampling_methods = {
    'Random OverSampling': RandomOverSampler(sampling_strategy='minority'),
    'SMOTE': SMOTE(),
    'ADASYN': ADASYN(),
    'Borderline-SMOTE': BorderlineSMOTE()
}
 
# 逻辑回归分类器
classifier = xgb.XGBClassifier(use_label_encoder=False, eval_metric='logloss', random_state=42)
# 使用分层 k 折交叉验证评估
cv = StratifiedKFold(n_splits=5)
 
# 比较不同采样方法的性能
results = {}
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)
    
    # 在测试集上预测
    y_pred = classifier.predict(X_test)
    
    # 计算性能指标
    accuracy = accuracy_score(y_test, y_pred)
    precision = precision_score(y_test, y_pred)
    recall = recall_score(y_test, y_pred)
    f1 = f1_score(y_test, y_pred)
    roc_auc = roc_auc_score(y_test, y_pred)
    
    # 保存结果
    results[method_name] = {
        'Accuracy': accuracy,
        'Precision': precision,
        'Recall': recall,
        'F1 Score': f1,
        'ROC AUC': roc_auc
    }
 
# 输出结果
results_df = pd.DataFrame(results)
print(results_df)
运行环境提示

上述代码需要本地存在名为 车险欺诈train.csv 的数据文件才能成功运行。在执行代码前,请确保已下载该文件并放置在与代码脚本相同的目录下。

表 tbl-resampling-results 表格清晰地展示了不同采样方法对模型性能的影响。在处理不平衡问题时,我们通常最关心召回率 (Recall)F1分数 (F1 Score)。通过比较这些指标,我们可以选择最适合当前业务场景的采样策略,以构建出更可靠的预测模型。