# ... (绘图代码) ...
"output_filename.png") # 先保存
plt.savefig(# 再显示 plt.show()
2 数据预处理:从理论到实践
数据预处理是任何数据分析或机器学习项目中最关键的步骤之一。古人云:“工欲善其事,必先利其器”,在数据科学领域,高质量的数据就是我们最锋利的“器”。原始数据往往是“不干净”的,充满了缺失、异常、格式不一致等问题。本章将系统介绍数据预处理中的核心技术,包括缺失值处理、异常值检测、数据类型转换、特征选择与降维,以及如何应对不平衡数据集。我们将结合理论知识与Python实践,为后续的商业建模与分析打下坚实的基础。
2.1 数据缺失值处理
在真实的商业场景中,我们拿到的数据几乎不可能是完美的。例如,在分析用户消费数据时,某些用户的年龄或收入信息可能为空;在处理财务报表时,特定季度的某个指标可能因为统计口径变化而缺失。这些缺失的数据点,我们称之为“缺失值”(Missing Values)。
2.1.1 缺失值的类型
从统计学的角度,缺失值的产生机制可以分为三类,理解它们有助于我们选择更合适的处理方法:
- 完全随机缺失 (Missing Completely at Random, MCAR): 数据的缺失与任何值(无论是缺失值本身还是其他变量)都无关。例如,由于设备随机故障导致某天的传感器数据未能记录。这是最理想的缺失情况。
- 随机缺失 (Missing at Random, MAR): 数据的缺失不完全是随机的,它依赖于其他观测到的变量。例如,在市场调研中,高收入人群可能更不愿意透露自己的具体收入,导致收入字段的缺失与“教育水平”、“职业”等其他已知变量相关。
- 完全非随机缺失 (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 所示。
import pandas as pd
import numpy as np
= pd.DataFrame(
df '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 代码展示了这两种方法的实现。
#KNN填充
import numpy as np
import pandas as pd
from sklearn.impute import KNNImputer
= pd.DataFrame(
df '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)
= KNNImputer(n_neighbors=1)
imputer = imputer.fit_transform(df)
imputed = pd.DataFrame(imputed, columns=df.columns)
df_imputed print(df_imputed)
#随机森林填充
= pd.DataFrame(
df '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()=df.copy()
data3#获取含有缺失值的特征
=data3.isna().any()[data3.isna().any().values==True].index.tolist()
miss_index#按照缺失值多少,由小至大排序,并返回索引
=np.argsort(data3[miss_index].isna().sum(axis=0)).values
sort_miss_indexprint('sort_miss_index',sort_miss_index)
from sklearn.impute import SimpleImputer
from sklearn.ensemble import RandomForestRegressor
for i in sort_miss_index:
=data3.columns.tolist()#特征名
data3_list=data3.copy()
data3_copy=data3_copy.iloc[:,i]#需要填充缺失值的一列
fillc#从特征矩阵中删除这列,因为要根据已有信息预测这列
=data3_copy.drop(data3_list[i],axis=1)
df#将已有信息的缺失值暂用0填补
=SimpleImputer(missing_values=np.nan,strategy='constant',fill_value=0).fit_transform(df)
df_0
=fillc[fillc.notnull()]#训练集标签为填充列含有数据的一部分
Ytrain=fillc[fillc.isnull()]#测试集标签为填充列含有缺失值的一部分
Ytest
=df_0[Ytrain.index,:]#通过索引获取Xtrain和Xtest
Xtrain=df_0[Ytest.index,:]
Xtest
=RandomForestRegressor(n_estimators=100)#实例化
rfc=rfc.fit(Xtrain,Ytrain)#导入训练集进行训练
rfc=rfc.predict(Xtest)#将Xtest传入predict方法中,得到预测结果
Ypredict#获取原填充列中缺失值的索引
=data3[data3.iloc[:,i].isnull()==True].index.tolist()
the_index=Ypredict#
data3.iloc[the_index,i]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 异常值的处理方法
- 删除: 如果异常值是由于明显的录入错误造成的,可以直接删除。但如果异常值数量较多,删除会损失信息。
- 视为缺失值: 将异常值转换成缺失值,然后采用上一节 sec-missing-values 介绍的方法进行插补。
- 盖帽法 (Capping): 也称为“缩尾”,即用一个设定的分位数(如1%和99%)来替换低于或高于该分位数的数据。例如,将所有低于1%分位数的值替换为1%分位数的值。
- 不处理: 在某些情况下,异常值本身就是分析的重点,比如在金融风控中检测欺诈交易。此时,我们不应处理异常值,而是要深入分析它们。
2.2.3 Python实践:异常值处理
2.2.3.1 基于 \(3\sigma\) 法则的异常值检测
我们首先需要检验数据是否服从正态分布,这里使用 scipy.stats
中的 kstest
(Kolmogorov-Smirnov test)。如果检验通过(通常p值小于0.05),我们就可以应用 \(3\sigma\) 法则。列表 lst-outlier-3sigma 代码封装了这一过程。
#题目一
import numpy as np
import pandas as pd
from scipy.stats import kstest
# 正态分布检验
def KsNormDetect(df):
# 计算均值
= df['value'].mean()
u # 计算标准差
= df['value'].std()
std # 计算P值
=kstest(df, 'norm', (u, std))[1]
res# 判断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):
# 计算均值
= df['value'].mean()
u # 计算标准差
= df['value'].std()
std if ks_res==1:
# 定义3σ法则识别异常值
# 识别异常值
= df[np.abs(df['value'] - u) > 3 * std]
error # 剔除异常值,保留正常的数据
= df[np.abs(df['value'] - u) <= 3 * std]
data_c # 输出异常数据
print(error)
return error
else:
print('请先检测数据是否服从正态分布-----------')
return None
= pd.DataFrame([111, 333, 12, 290, 265, 152, 222, 1213, 242467, 114, 231, 122, 33, 2, 1, 5, 22, 44], columns=["value"])
df = KsNormDetect(df)
ks_res #调用函数OutlierDetection进行异常值检测,结果存储再result中
= OutlierDetection(df, ks_res)
result 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
"ignore")
warnings.filterwarnings(
#%matplotlib inline
="zhengqi_train.txt"
train_data_file = "zhengqi_test.txt"
test_data_file
= pd.read_csv(train_data_file, sep='\t', encoding='utf-8')
train_data = pd.read_csv(test_data_file, sep='\t', encoding='utf-8')
test_data
#异常值分析
=(18,10))
plt.figure(figsize=train_data.values,labels=train_data.columns)
plt.boxplot(x-7.5,7.5],0,40,colors='r')
plt.hlines([
plt.show()
#删除异常值
=train_data[train_data['V9']>-7.5]
train_data
#最大最小值归一化
from sklearn import preprocessing
= [col for col in train_data.columns if col not in ['target']]
features_columns = preprocessing.MinMaxScaler()
min_max_scaler = min_max_scaler.fit(train_data[features_columns])
min_max_scaler = min_max_scaler.transform(train_data[features_columns])
train_data_scaler = min_max_scaler.transform(test_data[features_columns])
test_data_scaler
= pd.DataFrame(train_data_scaler)
train_data_scaler = features_columns
train_data_scaler.columns
= pd.DataFrame(test_data_scaler)
test_data_scaler = features_columns
test_data_scaler.columns
'target'] = train_data['target']
train_data_scaler[
#查看训练集数据和测试集数据分布情况
= 6
dist_cols = len(test_data_scaler.columns)
dist_rows
=(4*dist_cols,4*dist_rows))
plt.figure(figsize
for i, col in enumerate(test_data_scaler.columns):
=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
ax.set_xlabel(col)"Frequency")
ax.set_ylabel(= ax.legend(["train","test"])
ax
plt.show()
= 6
drop_col = 1
drop_row
=(5*drop_col,5*drop_row))
plt.figure(figsize
for i, col in enumerate(["V5","V9","V11","V17","V22","V28"]):
=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
ax.set_xlabel(col)"Frequency")
ax.set_ylabel(= ax.legend(["train","test"])
ax plt.show()
2.2.3.3 错误分析
在上述 图 fig-outlier-boxplot-vis 代码中,脚本末尾使用了 plt.savefig()
函数来保存图片。然而,plt.show()
函数会显示图形并清空当前的图形对象。因此,在 plt.show()
之后调用 plt.savefig()
通常会保存一个空白的图像。
2.2.3.4 正确写法
为了正确保存图像,plt.savefig()
应该在 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-learn
和 pandas
进行独热编码和有序编码。
from sklearn.feature_extraction import DictVectorizer
import pandas as pd
= DictVectorizer()
onehot_encoder = [{'city':'北京'},{'city':'天津'},{'city':'上海'}]#这个可以使用相同的key值city是因为它们属于不同的字典中
instances print(onehot_encoder.fit_transform(instances).toarray())
=pd.DataFrame([['green','M',10.1,'class1'],['red','L',13.5,'class2'],['blue','XL',15.3,'class1'],],
df=['color','size','price','classlabel'])
columnsprint('编码前\n',df)
={'XL':3,'L':2,'M':1}
size_mapping'size']=df['size'].map(size_mapping)#size表示列名
df[print('编码后\n',df)
在 列表 lst-data-transformation 中,DictVectorizer
实现了对城市名称的独热编码。而对于有序变量“size”,我们通过创建一个映射字典 size_mapping
并使用 pandas
的 map
方法,实现了有序的标签编码。
2.4 特征选择与降维
在商业数据分析中,我们常常面对含有几十甚至上百个特征的数据集(高维数据)。高维数据不仅会增加计算成本,还可能因为“维度灾难”而降低模型性能。特征选择和降维是解决这一问题的两种主要方法。
- 特征选择 (Feature Selection): 从原始特征集中挑选出一个子集,使得模型在该子集上的性能最优。被移除的特征被认为是冗余或不相关的。
- 降维 (Dimensionality Reduction): 通过某种数学变换,将高维特征空间映射到一个低维子空间,生成新的、数量更少的特征。每个新特征都是原始特征的某种组合。
2.4.1 特征选择方法
- 过滤式 (Filter): 在模型训练之前,先对特征进行评估和筛选。
- 方差过滤: 移除方差较小或为0的特征,因为这些特征在不同样本间几乎没有变化,包含的信息量很少。
- 相关系数法: 计算特征与目标变量之间的相关系数(如皮尔逊Pearson、斯皮尔曼Spearman),保留相关性高的特征。同时,也可以计算特征之间的相关系数,移除那些高度相关的特征以减少多重共线性。
- 嵌入式 (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
"ignore")
warnings.filterwarnings(from sklearn.decomposition import PCA #主成分分析法
from sklearn import preprocessing
="zhengqi_train.txt"
train_data_file = "zhengqi_test.txt"
test_data_file
= pd.read_csv(train_data_file, sep='\t', encoding='utf-8')
train_data = pd.read_csv(test_data_file, sep='\t', encoding='utf-8')
test_data #根据箱型图删除异常值
=train_data[train_data['V9']>-7.5]
train_data#最大最小值归一化
= [col for col in train_data.columns if col not in ['target']]
features_columns = preprocessing.MinMaxScaler()
min_max_scaler = min_max_scaler.fit(train_data[features_columns])
min_max_scaler = min_max_scaler.transform(train_data[features_columns])
train_data_scaler = min_max_scaler.transform(test_data[features_columns])
test_data_scaler
= pd.DataFrame(train_data_scaler)
train_data_scaler = features_columns
train_data_scaler.columns
= pd.DataFrame(test_data_scaler)
test_data_scaler = features_columns
test_data_scaler.columns
'target'] = train_data['target']
train_data_scaler[
#特征相关性
=(20, 16))
plt.figure(figsize= train_data_scaler.columns.tolist()
column = train_data_scaler[column].corr(method="spearman")
mcorr = np.zeros_like(mcorr, dtype=np.bool)
mask = True
mask[np.triu_indices_from(mask)] = sns.diverging_palette(220, 10, as_cmap=True)
cmap = sns.heatmap(mcorr, mask=mask, cmap=cmap, square=True, annot=True, fmt='0.2f')
g
plt.show()
=mcorr.abs()
mcorr=mcorr[mcorr['target']>0.1]['target']
numerical_corrprint(numerical_corr.sort_values(ascending=False))
= numerical_corr.sort_values(ascending=False).index
index0 print(train_data_scaler[index0].corr('spearman'))
= numerical_corr.sort_values(ascending=False).reset_index()
features_corr = ['features_and_target', 'corr']
features_corr.columns # 筛选出大于相关性大于0.3的特征
= features_corr[features_corr['corr']>0.3]
features_corr_select print('筛选出大于相关性大于0.3的特征',features_corr_select)
= [col for col in features_corr_select['features_and_target'] if col not in ['target']]
select_features = train_data_scaler[select_features+['target']]
new_train_data_corr_select = test_data_scaler[select_features]
new_test_data_corr_select '''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(n_components=0.9)
pca = pca.fit_transform(train_data_scaler.iloc[:,0:-1])
new_train_pca_90 = pca.transform(test_data_scaler)
new_test_pca_90 = pd.DataFrame(new_train_pca_90)
new_train_pca_90 = pd.DataFrame(new_test_pca_90)
new_test_pca_90 'target'] = train_data_scaler['target']
new_train_pca_90[print('保持90%的信息',new_train_pca_90.head())
#PCA方法降维
#保留16个主成分
= PCA(n_components=16)
pca = pca.fit_transform(train_data_scaler.iloc[:,0:-1])
new_train_pca_16 = pca.transform(test_data_scaler)
new_test_pca_16 = pd.DataFrame(new_train_pca_16)
new_train_pca_16 = pd.DataFrame(new_test_pca_16)
new_test_pca_16 'target'] = train_data_scaler['target']
new_train_pca_16[print('保留16个主成分',new_train_pca_16.head())
2.4.3.1 错误分析
在上述 图 fig-feature-selection 代码中,plt.savefig("1.png")
在 plt.show()
之后被调用。plt.show()
函数会显示图形并清除当前图形画布,导致后续的 savefig
保存的是一个空白图像。
2.4.3.2 正确写法
正确的顺序是先保存再显示:
# ... (heatmap 绘图代码) ...
"1.png") # 先保存
plt.savefig(# 后显示 plt.show()
此外,代码中 mask
的 dtype
被设置为 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
库来演示不同的过采样方法,并比较它们在车险欺诈预测任务上的性能。
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
# 加载数据集(请替换为实际的信用卡欺诈数据集)
= pd.read_csv("车险欺诈train.csv")
df
#把时间列policy_bind_date转为数值
'policy_bind_date'] = pd.to_datetime(df['policy_bind_date']).view(np.int64)
df[
#划分数据集,倒数第一列为taget,其余为特征
= df.iloc[:, :-1]
X = df.iloc[:, -1]
y #将进行one-hot编码
= pd.get_dummies(X)
X
# 数据准备
= train_test_split(X, y, test_size=0.2, stratify=y, random_state=42)
X_train, X_test, y_train, y_test
# 采样方法
= {
sampling_methods 'Random OverSampling': RandomOverSampler(sampling_strategy='minority'),
'SMOTE': SMOTE(),
'ADASYN': ADASYN(),
'Borderline-SMOTE': BorderlineSMOTE()
}
# 逻辑回归分类器
= xgb.XGBClassifier(use_label_encoder=False, eval_metric='logloss', random_state=42)
classifier # 使用分层 k 折交叉验证评估
= StratifiedKFold(n_splits=5)
cv
# 比较不同采样方法的性能
= {}
results for method_name, method in sampling_methods.items():
# 应用采样方法
= method.fit_resample(X_train, y_train)
X_resampled, y_resampled
# 训练模型
classifier.fit(X_resampled, y_resampled)
# 在测试集上预测
= classifier.predict(X_test)
y_pred
# 计算性能指标
= accuracy_score(y_test, y_pred)
accuracy = precision_score(y_test, y_pred)
precision = recall_score(y_test, y_pred)
recall = f1_score(y_test, y_pred)
f1 = roc_auc_score(y_test, y_pred)
roc_auc
# 保存结果
= {
results[method_name] 'Accuracy': accuracy,
'Precision': precision,
'Recall': recall,
'F1 Score': f1,
'ROC AUC': roc_auc
}
# 输出结果
= pd.DataFrame(results)
results_df print(results_df)
上述代码需要本地存在名为 车险欺诈train.csv
的数据文件才能成功运行。在执行代码前,请确保已下载该文件并放置在与代码脚本相同的目录下。
表 tbl-resampling-results 表格清晰地展示了不同采样方法对模型性能的影响。在处理不平衡问题时,我们通常最关心召回率 (Recall) 和 F1分数 (F1 Score)。通过比较这些指标,我们可以选择最适合当前业务场景的采样策略,以构建出更可靠的预测模型。