在重看我的项目的过程中发现自己对于相关知识点理解并不透彻,希望能理论联系实际,加深自己对基础知识的理解。项目来源于阿里天池学习赛——零基础入门金融风控-贷款违约预测,感兴趣的小伙伴可以自己去原文了解。特征工程是数据挖掘过程中至关重要的一块, 因为数据和特征决定了机器学习的上限,而算法和模型只是逼近这个上限而已,所以特征工程的好坏往往决定着最后的结果。特征工程在我看来分为数据清洗过程以及特征降维&交互&选择的过程。 前者通过重复值、异常值、缺失值的去除/填充,更好地表示出潜在问题的特征;后者或通过构造新特征使得数据的表达能力进一步放大,或根据相关性等进行特征的选择和降维以减少“维度灾难”。
这里尽量总结更多的方法,在后续实际应用中再具体模型具体分析。
2、数据清洗 2.0 时间格式处理 2.0.1 ['issueDate']构造['issueDateDT']代表据最早日期(2007-06-01)的天数,连续型变量
import datatime
#转化成时间格式
for data in [train, testA]:
data['issueDate'] = pd.to_datetime(data['issueDate'],format='%Y-%m-%d')
startdate = datetime.datetime.strptime('2007-06-01', '%Y-%m-%d')
#构造时间特征
data['issueDateDT'] = data['issueDate'].apply(lambda x: x-startdate).dt.days
sns.histplot(data_train['issueDateDT'],kde=True)
构造['issueDateM']代表贷款的月份,可以作为一个离散型变量
for data in [data_train, data_testA]:
data['issueDate'] = pd.to_datetime(data['issueDate'],format='%Y-%m-%d')
data['issueDateM'] = data['issueDate'].dt.month
data_train['issueDateM'].value_counts(dropna=False).sort_index()
1 67280 2 62108 3 74774 4 65147 5 62159 6 59689 7 76969 8 66842 9 54091 10 81035 11 67785 12 62121 Name: issueDateM, dtype: int642.0.2 ['employmentLength']保留数字
#定义一个函数:非零值取空格前的数字,且设置为int8格式
def employmentLength_to_int(s):
if pd.isnull(s):
return s
else:
return np.int8(s.split()[0])
for data in [data_train, data_testA]:
data['employmentLength'].replace(to_replace='10+ years', value='10 years', inplace=True)
data['employmentLength'].replace('< 1 year', '0 years', inplace=True)
data['employmentLength'] = data['employmentLength'].apply(employmentLength_to_int)
data_train['employmentLength'].value_counts(dropna=False).sort_index()
0.0 64237 1.0 52489 2.0 72358 3.0 64152 4.0 47985 5.0 50102 6.0 37254 7.0 35407 8.0 36192 9.0 30272 10.0 262753 NaN 46799 Name: employmentLength, dtype: int642.0.3 ['earliesCreditLine']保留年份
#取最后4个数字作为年份
for data in [data_train, data_testA]:
data['earliesCreditLine'] = data['earliesCreditLine'].apply(lambda s: int(s[-4:]))
data['earliesCreditLine'].value_counts(dropna=False).sort_index()
1944 1
1945 1
1949 1
1950 2
1951 2
...
2011 2490
2012 1676
2013 850
2014 388
2015 50
Name: earliesCreditLine, Length: 69, dtype: int64
2.1 缺失值处理
2.1.1 不处理
部分模型可以自动处理缺失值,如xgboost,不用我们对其进行处理。
2.1.2 填充(fillna)用某个统计量或缺失值前后数据填充
data_train.fillna(0, inplace=True) # 填充 0 data_train.fillna(train_data.mean(),inplace=True) # 填充均值 data_train.fillna(train_data.median(),inplace=True) # 填充中位数 data_train.fillna(train_data.mode(),inplace=True) # 填充众数 data_train.fillna(method='pad', inplace=True) # 填充前一条数据的值 data_train.fillna(method='bfill', inplace=True) # 填充后一条数据的值 data_train.fillna(method='bfill', limit = 2, inplace=True) ## 填充后一条数据的值,最多连着填充两个
用模型预测值填充(以KNN为例)
#填充KNN数据:先利用knn计算临近的k个数据,然后填充他们的均值 from fancyimpute import KNN data_train_x = pd.DataFrame(KNN(k=6).fit_transform(data_train_x), columns=features)
插值法填充
#用插值法拟合出缺失的数据,然后进行填充。
for f in features:
data_train[f] = data_train[f].interpolate()
2.1.3 删除缺失值(dropna)
data_train.dropna(inplace=True)
对于连续型变量,用上面合适的方法都可以,但是对于离散型变量,随意填充可能产生新的类型,感觉上KNN模型填充会是个不错的选择。
2.2 异常值处理发现异常值后,一定要先分清是什么原因导致的异常值,然后再考虑如何处理。首先,如果这一异常值并不代表一种规律性的,而是极其偶然的现象,或者说你并不想研究这种偶然的现象,这时可以将其删除。其次,如果异常值存在且代表了一种真实存在的现象,那就不能随便删除。在现有的欺诈场景中很多时候欺诈数据本身相对于正常数据来说就是异常的,我们要把这些异常点纳入,重新拟合模型,研究其规律。能用监督的用监督模型,不能用的还可以考虑用异常检测的算法来做。
2.2.1 均方差
在统计学中,如果一个数据分布近似正态,那么大约 68%的数据值会在均值的一个标准差范围内,大约95%会在两个标准差范围内,大约99.7%会在三个标准差范围内。
#1判断异常值;
def find_outliers_by_3segama(data,fea): ##增加一列名为fea+'_outliers'来判断异常值/正常值
data_std = np.std(data[fea])
data_mean = np.mean(data[fea])
outliers_cut_off = data_std * 3
lower_rule = data_mean - outliers_cut_off
upper_rule = data_mean + outliers_cut_off
data[fea+'_outliers'] = data[fea].apply(lambda x:str('异常值') if x > upper_rule or x < lower_rule else '正常值')
return data
#2分析变量异常值和目标变量的关系
numerical_fea = list(data_train.select_dtypes(exclude=['object']).columns)
for fea in numerical_fea:
data_train = find_outliers_by_3segama(data_train,fea)
print(data_train[fea+'_outliers'].value_counts())
print(data_train.groupby(fea+'_outliers')['isDefault'].sum())
print('*'*10)
#3删除异常值
for fea in numerical_fea:
data_train = data_train[data_train[fea+'_outliers']=='正常值']
data_train = data_train.reset_index(drop=True) #重置索引
#4进一步删除用于判断异常值的列
out_features = [f for f in data_train.columns if '_outliers' in f]
data_train = data_train.drop(out_features, axis = 1)
2.2.2 箱型图判断异常值
#1判断异常值;
def find_outliers_by_boxplot(data,fea):
data_q1 = data[fea].quantile(0.25)
data_q3 = data[fea].quantile(0.75)
iqr = data_q3 - data_q1
lower_rule = data_q1 - 1.5*iqr
upper_rule = data_q3 + 1.5*iqr
data[fea+'_outliers'] = data[fea].apply(lambda x:str('异常值') if x > upper_rule or x < lower_rule else '正常值')
return data
2.3 重复值处理
data_train.duplicated() #判断是否有重复值 data_train.drop_duplicates() #删除重复值2.4 特征分箱(分桶)
- 特征分箱的目的:
- 从模型效果上来看,特征分箱主要是为了降低变量的复杂性,减少变量噪音对模型的影响,提高自变量和因变量的相关度。从而使模型更加稳定。
- 数据分桶的对象:
- 将连续变量离散化
- 将多状态的离散变量合并成少状态
- 分箱的原因:
- 数据的特征内的值跨度可能比较大,对有监督和无监督中如k-均值聚类它使用欧氏距离作为相似度函数来测量数据点之间的相似度。都会造成大吃小的影响,其中一种解决方法是对计数值进行区间量化即数据分桶也叫做数据分箱,然后使用量化后的结果。
-
分箱的优点:
- 处理缺失值:当数据源可能存在缺失值,此时可以把null单独作为一个分箱。
- 处理异常值:当数据中存在离群点时,可以把其通过分箱离散化处理,从而提高变量的鲁棒性(抗干扰能力)。例如,age若出现200这种异常值,可分入“age > 60”这个分箱里,排除影响。
- 业务解释性:我们习惯于线性判断变量的作用,当x越来越大,y就越来越大。但实际x与y之间经常存在着非线性关系,此时可经过WOE变换。
-
特别要注意一下分箱的基本原则:
- (1)最小分箱占比不低于5%
- (2)箱内不能全部是好客户
- (3)连续箱单调
①间隔均匀的分箱,每个分箱的取值范围都是1000,即[0,10000],[10000,20000]...
data_train['loanAmnt_bin1'] = np.floor_divide(data_train['loanAmnt'], 10000) data_train['loanAmnt_bin1'].value_counts()
1.0 313364 0.0 271377 2.0 149684 3.0 61565 4.0 4010 Name: loanAmnt_bin1, dtype: int64
②对数分箱(通过对数函数映射后再均匀宽度分箱),即[0,10],[10,100],[100,1000]...
data_train['loanAmnt_bin2'] = np.floor(np.log10(data_train['loanAmnt'])) data_train['loanAmnt_bin2'].value_counts()
4.0 528623 3.0 271370 2.0 7 Name: loanAmnt_bin2, dtype: int64
③等距分桶(分成N份)
data_train['loanAmnt_bin3'] = pd.qcut(data_train['loanAmnt'], 10, labels=False) data_train['loanAmnt_bin3'].value_counts()
0 108387 5 92158 3 90313 8 87116 2 78907 4 76408 7 74671 9 71007 6 68195 1 52838 Name: loanAmnt_bin3, dtype: int642.4.2 自定义宽度数据分桶
ages = [20, 22, 25, 27, 21, 23, 37, 31, 61, 45, 41, 32] # 如果按年龄分成18-25, 26-35, 36-60, 61以上的若干组,可以使用pandas中的cut bins = [18, 25, 35, 60, 100] # 定义箱子的边 cats = pd.cut(ages, bins) print(cats) # 这是个categories对象 通过bin分成了四个区间, 然后返回每个年龄属于哪个区间 # codes属性 print(cats.codes) # 这里返回一个数组,指明每一个年龄属于哪个区间 print(cats.categories) print(pd.value_counts(cats)) # 返回结果是每个区间年龄的个数 # 与区间的数学符号一致, 小括号表示开放,中括号表示封闭, 可以通过right参数改变 print(pd.cut(ages, bins, right=False))
[(18, 25], (18, 25], (18, 25], (25, 35], (18, 25], ..., (25, 35], (60, 100], (35, 60], (35, 60], (25, 35]] Length: 12 Categories (4, interval[int64, right]): [(18, 25] < (25, 35] < (35, 60] < (60, 100]] [0 0 0 1 0 0 2 1 3 2 2 1] IntervalIndex([(18, 25], (25, 35], (35, 60], (60, 100]], dtype='interval[int64, right]') (18, 25] 5 (25, 35] 3 (35, 60] 3 (60, 100] 1 dtype: int64 [[18, 25), [18, 25), [25, 35), [25, 35), [18, 25), ..., [25, 35), [60, 100), [35, 60), [35, 60), [25, 35)] Length: 12 Categories (4, interval[int64, left]): [[18, 25) < [25, 35) < [35, 60) < [60, 100)]2.5 特征编码 2.5.1 one-hot编码
hot_features = ['grade','subGrade'] data_train_hot = pd.get_dummies(data_train, columns=hot_features)2.5.2 labelEncode
from sklearn.preprocessing import LabelEncoder le = LabelEncoder() data_train['subGrade'] = le.fit_transform(data_train['subGrade']) data_testA['subGrade'] = le.transform(data_testA['subGrade'])2.5.3 自定义编码
for data in [data_train, data_testA]:
data['grade'] = data['grade'].map({'A':1,'B':2,'C':3,'D':4,'E':5,'F':6,'G':7})
data_train['grade']
3、特征选择&交互&降维
3.1 特征选择
特征选择(排序)对于数据科学家、机器学习从业者来说非常重要。好的特征选择能够提升模型的性能,更能帮助我们理解数据的特点、底层结构,这对进一步改善模型、算法都有着重要作用。但是拿到数据集,一个特征选择方法,往往很难同时完成这两个目的。
特征选择主要有两个功能:
- 减少特征数量、降维,使模型泛化能力更强,减少过拟合
- 增强对特征和特征值之间的理解
通常来说,从两个方面考虑来选择特征:
- 特征是否发散:如果一个特征不发散,例如方差接近于0,也就是说样本在这个特征上基本上没有差异,这个特征对于样本的区分并没有什么用。
- 特征与目标的相关性:这点比较显见,与目标相关性高的特征,应当优选选择。
根据特征选择的形式又可以将特征选择方法分为3种:
- Filter:过滤法,按照发散性或者相关性对各个特征进行评分,设定阈值或者待选择阈值的个数,选择特征。
- Wrapper:包装法,根据目标函数(通常是预测效果评分),每次选择若干特征,或者排除若干特征。
- Embedded:嵌入法,先使用某些机器学习的算法和模型进行训练,得到各个特征的权值系数,根据系数从大到小选择特征。类似于Filter方法,但是是通过训练来确定特征的优劣。
- trick1: 对于数值型特征,方差很小的特征可以不要,因为太小没有什么区分度,提供不了太多的信息,对于分类特征,也是同理,取值个数高度偏斜的那种可以先去掉。
- trick2:根据与目标的相关性等选出比较相关的特征(当然有时候根据字段含义也可以选)
- trick3: 卡方检验一般是检查离散变量与离散变量的相关性,当然离散变量的相关性信息增益和信息增益比也是不错的选择(可以通过决策树模型来评估来看), person系数一般是查看连续变量与连续变量的线性相关关系。
方差选择法(认为方差太小的特征提供不了足够的信息)
方差选择法中,先要计算各个特征的方差,然后根据设定的阈值,选择方差大于阈值的特征
from sklearn.feature_selection import VarianceThreshold #其中参数threshold为方差的阈值 VarianceThreshold(threshold=3).fit_transform(train,target_train)
相关系数法(认为与目标值没有什么相关性的特征值提供不了正确的信息)
Pearson 相关系数是一种最简单的,可以帮助理解特征和响应变量之间关系的方法,该方法衡量的是变量之间的线性相关性。 结果的取值区间为 [-1,1] , -1 表示完全的负相关, +1表示完全的正相关,0 表示没有线性相关。此外对于某些数据来说spearman相关性更合理。
- 连续数据,正态分布,线性关系,用pearson相关系数是最恰当,当然用spearman相关系数也可以,效率没有pearson相关系数高。
- 上述任一条件不满足,就用spearman相关系数,不能用pearson相关系数。
- 两个定序测量数据(顺序变量)之间也用spearman相关系数,不能用pearson相关系数。
- Pearson相关系数的一个明显缺陷是,作为特征排序机制,他只对线性关系敏感。如果关系是非线性的,即便两个变量具有一一对应的关系,Pearson相关性也可能会接近0。
from sklearn.feature_selection import SelectKBest from scipy.stats import pearsonr #选择K个最好的特征,返回选择特征后的数据 #第一个参数为计算评估特征是否好的函数,该函数输入特征矩阵和目标向量, #输出二元组(评分,P值)的数组,数组第i项为第i个特征的评分和P值。在此定义为计算相关系数 #参数k为选择的特征个数 SelectKBest(k=5).fit_transform(train,target_train)
卡方检验(利用假设检验得到特征与输出值之间的相关性)
- 经典的卡方检验是用于检验自变量对因变量的相关性。 假设自变量有N种取值,因变量有M种取值,考虑自变量等于i且因变量等于j的样本频数的观察值与期望的差距。 其统计量如下: χ2=∑(A−T)2T,其中A为实际值,T为理论值
- (注:卡方只能运用在正定矩阵上,否则会报错Input X must be non-negative)
from sklearn.feature_selection import SelectKBest from sklearn.feature_selection import chi2 #参数k为选择的特征个数 SelectKBest(chi2, k=5).fit_transform(train,target_train)
互信息法(从信息熵的角度分析相关性)
经典的互信息也是评价自变量对因变量的相关性的。 在feature_selection库的SelectKBest类结合最大信息系数法可以用于选择特征,相关代码如下:
from sklearn.feature_selection import SelectKBest from minepy import MINE #由于MINE的设计不是函数式的,定义mic方法将其为函数式的, #返回一个二元组,二元组的第2项设置成固定的P值0.5 def mic(x, y): m = MINE() m.compute_score(x, y) return (m.mic(), 0.5) #参数k为选择的特征个数 SelectKBest(lambda X, Y: array(map(lambda x:mic(x, Y), X.T)).T, k=2).fit_transform(train,target_train)3.1.2 Wrapper (Recursive feature elimination,RFE,递归特征消除法 )
递归消除特征法使用一个基模型来进行多轮训练,每轮训练后,消除若干权值系数的特征,再基于新的特征集进行下一轮训练。在feature_selection库的RFE类可以用于选择特征,相关代码如下(以逻辑回归为例):
from sklearn.feature_selection import RFE from sklearn.linear_model import LogisticRegression #递归特征消除法,返回特征选择后的数据 #参数estimator为基模型 #参数n_features_to_select为选择的特征个数 RFE(estimator=LogisticRegression(), n_features_to_select=2).fit_transform(train,target_train)3.1.3 Embedded(基于惩罚项的特征选择法)
使用带惩罚项的基模型:除了筛选出特征外,同时也进行了降维。 在feature_selection库的SelectFromModel类结合逻辑回归模型可以用于选择特征,相关代码如下:
from sklearn.feature_selection import SelectFromModel from sklearn.linear_model import LogisticRegression #带L1惩罚项的逻辑回归作为基模型的特征选择 SelectFromModel(LogisticRegression(penalty="l1", C=0.1)).fit_transform(train,target_train)
基于树模型的特征选择:树模型中GBDT也可用来作为基模型进行特征选择。 在feature_selection库的SelectFromModel类结合GBDT模型可以用于选择特征,相关代码如下:
from sklearn.feature_selection import SelectFromModel from sklearn.ensemble import GradientBoostingClassifier #GBDT作为基模型的特征选择 SelectFromModel(GradientBoostingClassifier()).fit_transform(train,target_train)3.2 特征交互
特征交互可以理解为用房间的‘长’和‘宽’来构造‘面积’这个新的特征
- 交互特征的构造非常简单,使用起来却代价不菲。如果线性模型中包含有交互特征对,那它的训练时间和评分时间就会从 O(n) 增加到 O(n2),其中 n 是单一特征的数量。
for col in ['grade', 'subGrade']:
#将训练数据按col分组,取目标值'isDefault'数据,计算不同'grade'的均值,加上索引列,将col的列名改成col_taregt_mean
temp_dict = train.groupby([col])['isDefault'].agg(['mean']).reset_index().rename(columns={'mean': col + '_target_mean'})
#将temp_dict转化为字典{grade: isDefault_mean}
temp_dict.index = temp_dict[col].values
temp_dict = temp_dict[col + '_target_mean'].to_dict()
#通过字典新建一个col_target_mean列
train[col + '_target_mean'] = train[col].map(temp_dict)
testA[col + '_target_mean'] = testA[col].map(temp_dict)
3.3 特征降维
特征降维方法就是既可以减少我们需要分析的指标,而且尽可能多的保持原来数据信息的方法
3.3.1 PCA(主成分分析,Principal components analysis)数据的方差越大,所获得的的信息量就会越多。PCA找主成分的时候其实在寻找K个尽可能的把样本区分开方向,即方差尽可能大的方向作为主成分,这样就可以做到在保留尽可能多的数据信息的情况下把数据的维度降到了低维。具体原理和计算方法可见白话机器学习算法理论+实战之PCA降维和刘建平的博客。
from sklearn.decomposition import PCA pca = PCA(n_components=4) #4个主成分OR保留0.9的信息 pca_result = pca.fit_transform(df[feat_cols].values)3.3.2 LDA(线性判别分析,Linear Discriminant Analysis)
LDA用于降维,和PCA有很多相同,也有很多不同的地方,因此值得好好的比较一下两者的降维异同点。可以参考sklearn官方文档。
首先我们看看相同点:
- 两者均可以对数据进行降维。
- 两者在降维时均使用了矩阵特征分解的思想。
- 两者都假设数据符合高斯分布。
我们接着看看不同点:
- LDA是有监督的降维方法,而PCA是无监督的降维方法
- LDA降维最多降到类别数k-1的维数,而PCA没有这个限制。
- LDA除了可以用于降维,还可以用于分类。
- LDA选择分类性能最好的投影方向,而PCA选择样本点投影具有最大方差的方向。
这点可以从下图形象的看出,在某些数据分布下LDA比PCA降维较优。
from sklearn.discriminant_analysis import LinearDiscriminantAnalysis LDA = LinearDiscriminantAnalysis(n_components=2) lda_result = LDA.fit_transform(data_train)3.3.3 t-SNE
可视化效果较好的降维方法
from sklearn.manifold import TSNE tsne = TSNE(n_components=3, n_iter=300).fit_transform(df[feat_cols][:6000].values)



