赞
踩
本文结合《机器学习算法竞赛实战》学习笔记4.特征工程_全自动学习机器的博客-CSDN博客进行了重现整理。仅供自己学习。
目录
尽量得到标准、干净、连续的数据,供数据统计、数据挖掘等使用,视情况尝试对缺失值进行处理,比如是否要填充,填充什么。此外,有些竞赛提供的数据集以及对应的存储方式可能使得需要占用超过参赛者本身硬件条件的内存,故有必要进行一定的内存优化,这也有助于在有限的内存空间对更大的数据集进行操作。
除了XGBoost和LightGBM等算法在训练时可以直接处理缺失值以外,其他很多例如LR、DNN、CNN、RNN等都并不能对缺失值进行直接处理。故而在数据准备阶段,要比构建算法阶段花更多时间,因为像填补缺失值这样的操作需要细致处理。
Python中可用“.info()”直接查看Non-Null Count;如果你用kaggle,dataset上传后会自动可视化
首先,需找到缺失值表现形式。除了None、NA和NaN外,还有例如-1或-999来填充的缺失值。
还有一种看上去像缺失值,但实际上有实际意义的业务,此时需特殊对待。例如没有填“婚姻状态”的用户可能是对自己隐私比较敏感,应为其单独设为一个分类;没有“驾龄”可能是没有车,为其填充0比较合适。
数据缺失可以分为类别特征的缺失和数值特征的缺失两种。
填充方法总结如下:
实际数据中可能会发现某个或某些字段(特征)根据某个变量(如时间序列问题中的时间)排序后,经观察存在一些数值远高于或低于其一定范围内的其他数值。
还有些不合理的存在,e.g.年龄不在0-130,这些都可以视作异常值,他们可能会给算法性能带来负面影响。
首先,找到异常值,总结了两种方法:
常见:seaborn库的箱型图
扩展
离散型异常值(离散属性定义范围以外的所有值均为异常值)、知识型异常值(如大学牲脱发情况:从无)等,都可以当做类别缺失值来处理。
数据集太大而自己的硬件条件有限就有可能会因为内存不够导致代码出现memory error,介绍Python的内存回收机制和数值类型优化这两种有助于优化内存的常见方法。
- import pandas as pd
- df_csv = pd.read_csv('XXXX.csv')
- df_txt = pd.read_txt('XXXX.txt')
- df_excel = pd.read_excel('XXXX.xlsx')
pandas中的许多数据类型都具有多个子类型,可以使用较少的字节表示不同数据。
我们可以用np.iinfo类来确认每一个int型子类型的最大值和最小值
- import numpy as np
- np.iinfo(np.int8).min
- np.iinfo(np.int8).max
在不允许影响模型泛化性能的情况下:
指将不同规格的数据转换到同一规格。
常见无量纲化方法有标准化和区间缩放法。标准化的前提是特征值服从正态分布,标准化后,特征值服从标准正态分布。区间缩放法利用了边界信息,将特征的取值区间缩放到某个特定的范围,例如[0,1]。
单特征转换是构建一些模型(如线性回归、KNN、神经网络)的关键,对于决策树相关模型并无影响。还有些纯粹的工程原因,即在进行回归预测时,对目标取对数处理,不仅可以缩小数据范围,而且压缩变量尺度使数据更平稳。
然而,数据要求不仅是通过参数化方法施加的。如果特征没有被规范化,例如当一个特征的分布位于0附近且范围不超过(-1,1),而另一个特征的分布范围在数十万数量级时,会导致分布于0附近的特征变得完全无用。
进行log变换可以将倾斜数据变得接近正态分布,因为大多数机器学习模型不能很好地处理非正态分布数据,比如右倾数据。可以应用log(x+1)变换来修正倾斜,其中+1的目的是防止数据等于0,同时保证x都是正的。取对数不会改变数据的性质和相关关系,但是压缩了变量的尺度,不仅数据更加平稳,还削弱了模型的共线性、异方差性等。
扩展:cbox-cox变换,一种自动寻找最佳正态分布变换函数的方法。竞赛不常用。
离散化后的特征对异常数据有很强的健壮性,更便于探索数据的相关性。
常用的离散化分为无监督和有监督两种。
在实际数据中,特征并不总是数值,还有可能是类别。对于离散型的类别特征进行编码,一般分为两种情况:自然数编码(特征有顺序意义)和独热(one-hot)编码(特征无顺序意义)。
一列有意义的类别特征(即有顺序关系)可以使用自然数进行编码,利用自然数的大小关系可以保留其顺序关系。以下介绍两种自然数编码的常用方式。
①调用sklearn中函数:
- from sklearn import preprocessing
- from f in columns:
- le = preprocessing.LableEncoder()
- le.fit(data[f})
②自定义实现(速度快)
- for f in columns:
- data[f] = data[f].fillna(-999)
- data[f] = data[f].map(dict(zip(data[f].unique(),range(0,data[f].nunique()))))
当类别特征没有意义(即无顺序关系)时,需要使用独热编码。例如,红>蓝>绿不代表任何东西,进行独热编码后,每个特征的取值对应一维特征,最后得到一个样本数×类别数大小的0~1矩阵。可直接调用sklearn中API
不规则特征可能包含样本的很多信息,比如身份证号。各段表示不同的信息。
机器学习模型很难识别复杂的模式,特别是很难学习到不同特征组合交叉的信息,所以我们可以基于对数据集的直觉分析和业务理解创建一些特征来帮助模型有效学习。下面我们将介绍结构化数据的特征提取方式。(结构化数据由明确定义的数据类型组成,非结构化数据由音频、视频和图片等不容易搜索的数据组成。)
类别特征又可以称为离散特征,除了每个类别属性的特定含义外,还可以构造连续型的统计特征,以挖掘更多有价值的信息,比如构造目标编码、count、nunique、和ratio等特征。另外,也可以通过类别特征间的交叉组合构造更加细粒度的特征。
目标编码可以理解为用目标变量(标签)的统计量对类别特征进行编码,即根据目标变量进行有监督的特征构造。如果是分类问题,可以统计正样本个数、负样本个数或者正负样本的比例;如果是回归问题,可以统计目标均值、中位数和最值。目标编码的方式可以很好地替代类别特征,或者作为新特征。
使用目标变量时,非常重要的一点是不能泄露任何验证集的信息。所有基于目标编码的特征都应该在训练集上计算,测试集则由完整的训练集来构造。更严格一点,我们在统计训练集的特征时,需要采用K折交叉统计法构造目标编码特征,从而最大限度地防止信息泄露。如用五折交叉统计构造特征时,我们将样本划分为五份,对于其中每一份数据,我们都将用另外四份来计算每个类别取值对应目标变量的频次、比例或者均值,简单来说就是未知的数据(一份)在已知的数据(四份)里面取特征。
目标编码方法对于基数较低的类别特征通常很有效,但对于基数较高的类别特征,可能会有过拟合的风险。?因为会存在一些类别出现频次非常低,统计出来的结果不具有代表性。一般会加入平滑性来降低过拟合风险。在处置妥当的情况下,无论是线性模型,还是非线性模型,目标编程都是最佳的编码方式和特征构造方式。
五折交叉统计的代码实现:?
- folds = KFold(n_splits=5,shuffle=True,random_state=2020)
- for col in columns:
- colname = col+'_kfold'
- for fold_,(trn_idx,val_idx) in enumerate(folds.split(train,train)):
- tmp = train.iloc[trn_idx]
- order_label = tmp.groupby([col])['label'].mean()
- train[colname] = train[col].map(order_label)
- order_label = train.groupby([col])['label'].mean()
- test[colname] = test[col].map(order_label)
交叉组合能够描述更细粒度的内容。对类别特征进行交叉组合在竞赛中是一项非常重要的工作,这样可以进行很好的非线性特征拟合。如用户年龄和用户性别可以组合成“年龄_性别”这样的新特征。一般我们可以对两个类别或三个类别特征进行组合,也称作二阶组合或三阶组合。简单来讲,就是对两个类别特征进行笛卡尔积的操作,产生新的类别特征。
并非所有组合都是需要考虑的,我们会从两个方面进行分析。
这里所说的数值特征,我们认为是连续的。数值特征的大小是有意义的,通常不需要处理就可以直接“喂”给模型进行训练。除了之前对数值特征进行各种变换外,还存在一些其他常见的数值特征构造方式。
在实际数据中,通常给出的时间特征是时间戳属性,所以首先需要将其分离成多个维度,比如年月日小时分钟秒钟。
如果你的数据源来自于不同的地理数据源,还需要利用时区将数据标准化。
除了分离出来的基本时间特征外,还可以构造时间差特征,即计算出各个样本的时间与未来某一个时间的数值差距,这样这个差距是UTC的时间差,从而将时间特征转换为连续值,比如用户首次行为日期与用户注册日期的时间差、用户当前行为与用户上次行为的时间差等。
在竞赛中,可能会遇到某一列特征中每行都包含多个属性的情况,这就是多值特征。
例如广告大赛中的兴趣类目,其中包含5个兴趣特征组,每个兴趣特征组都包含若干个兴趣ID。
对于多值特征,通常可以进行稀疏化或者向量化的处理,这种操作一般出现在自然语言处理中,比如文本分词后使用TF-IDF(词频-逆文档频率)、LDA(隐含狄利克雷分布)、NMF(非负矩阵分解)等方式进行处理,这里则可以将多值特征看作文本分词后的结果,然后做相同的处理。
对多值特征最基本的处理办法是完全展开,即把这列特征所包含的n个属性展开成n维稀疏矩阵。使用sklearn中的CountVectorizer函数,可以方便地将多值特征展开,只考虑每个属性在这个特征的出现频次。
还有一种情况,比如在广告算法大赛中,需要根据用户的历史点击行为预测用户的属性标签。这时候用户的点击序列尤为重要,当我们构造好用户对应的历史点击序列后,除了使用上述的TF-IDF等方法外,还可以提取点击序列中商品或广告的嵌入表示,比如用Word2Vec、DeepWalk等方法获取embedding向量表示。因为要提取用户单个特征,所以可以对序列中的嵌入向量进行聚合统计,这种方法本质上是假设用户点击过的商品或广告等同重要,是一种比较粗糙的处理方式。我们可以引入时间衰减因素,或者使用序列模型,如RNN、LSTN、GRU,套用NLP的方法进行求解。
另外:空间特征、时序特征、聚类和降维的方法,后续会介绍。
特征选择算法用于从数据中识别并删除不需要、不相关以及冗余的特征,这些特征可能会降低模型的准确度和性能。
特征选择的方法主要有先验的特征关联性分析以及后验的特征重要性分析。
使用统计量为特征之间的相关性进行评分,按照排序进行删除或者保留。
通常是针对单变量的,并且独立考虑特征或者因变量。?
常见方法:皮尔逊相关系数、卡方检验、互信息法、信息增益等。
优缺点:速度快、使用方便,但是忽略了特征之间的关系,以及特征和模型之间的关系。?
不仅可以衡量变量之间的线性相关性,解决共线变量问题,还可以衡量特征与标签的相关性。
共线变量是指变量之间存在高度相关关系,这会降低模型的学习可用性,可解释性以及测试集的泛化性能。但这三个特性都是我们想增加的,所以删除共线变量是一个有价值的步骤。
我们将为删除共线变量建立一个基本的阈值(根据想要保留的特征数量决定)。
下面代码用于解决特征与标签不具有相关性的问题,根据皮尔逊相关系数的计算提取top300的相似特征:
- def feature_select_pearson(train,features):
- featureSelect = features[:]
- #进行皮尔逊相关性计算
- corr=[]
- for feat in featureSelect:
- corr.append(abs(train[[feat,'target']].fillna(0).corr().values[0][1]))
- se = pd.Series(corr,index=featureSelect).sort_values(ascending=False) # 降序
- feature_select = se[:300].index.tolist() # 选择与标签值相关性最高的前300个特征
- #返回特征选择后的训练集
- return train[feature_select]
-
用于检验特征变量与因变量的相关性。
对于分类问题,一般假设与标签独立的特征为无关特征,而卡方检验恰好可以进行独立性检验,所以使用与特征选择。
如果检验结果是某个特征与标签独立,则可以去除该特征。
互信息是对一个联合分布中两个变量之间相互影响关系的度量,也可以用于评价两个变量之间的相关性。互信息法之所以能够用于特征选择,可以从两个角度进行解释:基于KL散度和基于信息增益。互信息越大说明两个变量相关性越高。
但是想把互信息直接用于特征选择其实不太方便,由于:
在实际竞赛中,经常用到的一种特征选择方法是基于树模型评估特征的重要性分数。特征的重要性分数越高,说明特征在模型中被用来构建决策树的次数越多。
这里我们以XGBoost为例来介绍树模型评估特征重要性的三种计算方法(weight、gain和cover)。(LightGBM也可以返回特征重要性)
该方式比较简单,计算特征在所有树中被选为分裂特征的次数,并将以此作为评估特征重要性的依据
- params ={
- 'max_depth':10,
- 'subsample':1,
- 'verbose_eval':True,
- 'seed':12,
- 'objective':'binary:logistic'
- }
- xgtrain = xgb.DMatrix(x,label=y)
- bst = xgb.train(params,xgtrain,numm_boost_round=10)
- importance = bst.get_score(fmap='',importance_type='weight')
gain表示平均增益。在进行特征重要性评估时,使用gain表示特征在所有树中作为分裂节点的信息增益之和再除以该特征出现的频次。
importance =bst.get_score(fmap='',importance_type='gain')
cover是特征对每棵树的覆盖率,即特征被分到该节点的样本的二阶导数之和,而特征度量的标准就是平均覆盖率值。
importance = bst.get_score(fmap='',importance_type='cover')
技巧:虽然特征重要性可以帮助我们快速分析特征在模型训练过程中的重要性,但不能将其当做绝对的参考依据。一般而言,只要特征不会导致过拟合,我们就可以选择重要性高的特征进行分析和扩展,对于重要性低的特征,可以考虑将之从特征集中移除,然后观察线下效果,再做进一步判断。
比较耗时的特征选择方法。可以将一组特征的选择视作一个搜索问题,在这个问题中,通过准备、评估不同的组合并对这些组合进行比较,从而找出最优的特征子集,搜索过程可以是系统性的,比如最佳优先搜索;也可以是随机的,比如随机爬山算法,或者启发式方法,比如通过向前和向后搜索来添加和删除特征(类似前剪枝和后剪枝算法)
分为前向搜索和后向搜索。
前向搜索是每次增量地从剩余未选中的特征中选出一个并将其加入特征集中,待特征集中的特征数量达到初设阈值时,意味着贪心选出了错误率最小的特征子集。既然有增量加,就会有增量减,后者称为后向搜索,即从特征全集开始,每次删除其中的一个特征并评价,知道特征集中的特征数量达到初设阈值,就选出了最佳的特征子集。
因为启发式方法会导致局部最优,所以加入模拟退火方式进行改善,这种方式不会因为新加入的特征不能改善效果而舍弃该特征,而是对其添加权重后放入已选特征集。
这种启发式方法是很耗时间、耗资源的。
用一个基模型来进行多轮训练,每轮训练都会先消除若干权值系数的特征,再基于新特征集进行下一轮训练。可以使用feature_selection库的RFE类来进行特征选择
- from sklearn.feature_selection import RFE
- from sklearn.linear_model import LogisticRegression
- #递归消除特征法,返回特征选择后的数据
- #参数estimator为基模型
- #参数n_feature_to_select 为选择的特征个数
- RFE(estimator=LogisticRegression(),n_features_to_select=2).fit_transform(data,target)
技巧:在使用封装方法进行特征选择时,用全量数据训练并不是最明智的选择。应先对大数据进行采样,再对小数据使用封装方法
以上三种特征选择方法按需使用或组合使用,建议优先考虑特征重要性,其次是特征关联性。
此外还有null importance(kaggle经常使用)。
其思想:将构建好的特征和正确的标签喂给树模型得到一个特征重要性分数,再将特征和打乱后的标签喂给树模型得到一个特征重要性分数,然后对比两个分数,如果前者没有超过后者,那么这个特征就是一个无用的特征。
数据清洗:缺失值和异常值。
下面的代码先进行数据读取,然后删除缺失比例大于50%的特征列,并对object型的缺失特征进行填充。
- test = pd.read_csv("test.csv")
- train = pd.read_csv("train.csv")
- ntrain = train.shape[0]
- ntest = test.shape[0]
-
- data = pd.concat([train, test], axis=0, sort=False)
- # 删除缺失值比例大于50%的特征列
- missing_cols = [c for c in data if data[c].isna().mean()*100 > 50]
- data = data.drop(missing_cols, axis=1)
-
- # 对object型(类别型)的缺失特征进行填充
- object_df = data.select_dtypes(include=['object'])
- numerical_df = data.select_dtypes(exclude=['object'])
-
- object_df = object_df.fillna('unknow')
接下来,对数值型特征用中位数进行填充:
- # 数值型列采用中位数填充
- missing_cols = [c for c in numerical_df if numerical_df[c].isna().sum() > 0]
- for c in missing_cols:
- numerical_df[c] = numerical_df[c].fillna(numerical_df[c].median())
对于特征中(类别型)属性的分布极其不均衡,比如存在某个属性所占比例超过95%,也要将其删除。(参见前文类别型特征的分布图代码)
object_df = object_df.drop(['Heating','RoofMatl','Condition2','Street','Utilities'],axis=1)
从多个角度进行具体的特征构造。
常识推理:e.g. 房屋建筑年龄也是影响房屋价格的一个因素,在构建时,发现存在销售日期(YrSold)早于建筑日期(YrBuilt)的数据,将异常的销售日期替换为数据集中的最晚 销售日期(2009)。
- numerical_df.loc[numerical_df['YrSold'] < numerical_df['YearBuilt'], 'YrSold' ] = 2009
- numerical_df['Age_House']= (numerical_df['YrSold'] - numerical_df['YearBuilt'])
业务相关:e.g. 浴池和半浴池求和,全浴和半浴求和,一楼面积和二楼面积以及地下室面积求和
- numerical_df['TotalBsmtBath'] = numerical_df['BsmtFullBath'] + numerical_df['BsmtHalfBath']*0.5
- numerical_df['TotalBath'] = numerical_df['FullBath'] + numerical_df['HalfBath']*0.5
- numerical_df['TotalSA'] = numerical_df['TotalBsmtSF'] + numerical_df['1stFlrSF'] + numerical_df['2ndFlrSF']
主要是object型(类别型)进行编码。
存在大小关系的序数特征,进行0-N的映射转换,即自然数编码;
没有大小关系的,进行独热编码,或者频次编码。?
- bin_map = {'TA':2,'Gd':3, 'Fa':1,'Ex':4,'Po':1,'None':0,
- 'Y':1,'N':0,'Reg':3,'IR1':2,'IR2':1,
- 'IR3':0,"None" : 0,"No" : 2, "Mn" : 2,
- "Av": 3,"Gd" : 4,"Unf" : 1, "LwQ": 2, \
- "Rec" : 3,"BLQ" : 4, "ALQ" : 5, "GLQ" : 6}
- object_df['ExterQual'] = object_df['ExterQual'].map(bin_map)
- object_df['ExterCond'] = object_df['ExterCond'].map(bin_map)
- object_df['BsmtCond'] = object_df['BsmtCond'].map(bin_map)
- object_df['BsmtQual'] = object_df['BsmtQual'].map(bin_map)
- object_df['HeatingQC'] = object_df['HeatingQC'].map(bin_map)
- object_df['KitchenQual'] = object_df['KitchenQual'].map(bin_map)
- object_df['FireplaceQu'] = object_df['FireplaceQu'].map(bin_map)
- object_df['GarageQual'] = object_df['GarageQual'].map(bin_map)
- object_df['GarageCond'] = object_df['GarageCond'].map(bin_map)
- object_df['CentralAir'] = object_df['CentralAir'].map(bin_map)
- object_df['LotShape'] = object_df['LotShape'].map(bin_map)
- object_df['BsmtExposure'] = object_df['BsmtExposure'].map(bin_map)
- object_df['BsmtFinType1'] = object_df['BsmtFinType1'].map(bin_map)
- object_df['BsmtFinType2'] = object_df['BsmtFinType2'].map(bin_map)
-
- PavedDrive = {"N" : 0, "P" : 1, "Y" : 2}
相关性评估。过滤掉相似性大于一定阈值的特征,减少特征冗余。
下面的代码将相关性高的特征筛选出来。
- def correlation(data, threshold):
- col_corr = set()
- corr_matrix = data.corr() # dataframe的corr()计算相关系数
- for i in range(len(corr_matrix.columns)):
- for j in range(i):
- if abs(corr_matrix.iloc[i, j]) > threshold: # 相似性分数与阈值对比
- colname = corr_matrix.columns[i] # 获取列名
- col_corr.add(colname)
- return col_corr
-
- all_cols = [c for c in data.columns if c not in ['SalePrice']] # 去掉标签的其他列
- corr_features = correlation(data[all_cols], 0.9)
对于阈值的确定很难量化,可以从总特征量和整体相似性分数这两个角度进行考虑。也可以保留不同阈值的特征集进行训练,辅助提升模型融合效果。
1.在对数值型特征进行缺失值填充时,该如何从平均数、中位数和众数中选择?
可以从数值的分布来判断,如果数据分布极其不平衡,可以选择中位数或者众数;如果比较分散可以选择平均数;如果看到中位数值和均值相差不多,此时选择均值比较好。如果中位数和均值相差较多,此时选择中位数比较好,因为中位数比较稳定,不会受到数值偏离的影响。
2.在特征选择中,还有一类是基于惩罚项的特征选择法,里面包含L1正则和L2正则,那么这两种正则有什么区别?该如何选择?
正则化项L1和L2的区别 - 岁月静好--lyr - 博客园
L1L2正则化的选择_小智rando的博客-CSDN博客_l2正则化参数选择
3.整个特征工程主要分为几个部分?每个部分主要包含哪些内容?
数据预处理,特征变换,特征提取,特征选择
4.数值型特征、类别型特征与不规则特征各自是怎么定义的?请举例说明。
5.在数据清洗过程中,应该如何选择异常值处理方式?
6.为什么要优化内存,可供选择的优化方式是什么?
7.请举例说明count、nunique、 ratio 三种统计特征的含义。
8.在什么情况下,我们需要进行特征变换?
9.在进行特征选择时,该选择特征关联性分析方法还是特征重要性分析方法呢?
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。