数据挖掘实践(金融风控-贷款违约预测)(三):特征工程

数据挖掘实践(金融风控-贷款违约预测)(三):特征工程

1.引言

  特征工程是机器学习领域的一个重要概念,目前并没有普遍接受的定义, 一般可以认为是为机器学习应用而设计特征集的相关工作。主要涉及两个方面的因素:
  a) 了解要解决的问题和要使用的机器学习算法的优势和限制;
  b) 进行实践,通过实验从而更好地掌握哪种特征更为合理,哪种特征并不符合所处理的问题或所选择的机器学习方法
  这两方面的影响因素可以是一个螺旋式迭代过程,对问题自顶而下的理解有助于实验工作的展开;同时在实验中所获得的自底而上的信息者会帮助更好地理解要解决的问题,揭示蕴涵的问题本质。

  目标
  • 学习特征预处理、缺失值、异常值处理、数据分桶等特征处理方法
  • 学习特征交互、编码、选择的相应方法

项目地址:https://github.com/datawhalechina/team-learning-data-mining/tree/master/FinancialRiskControl
比赛地址:https://tianchi.aliyun.com/competition/entrance/531830/introduction

2.特征预处理

  在比赛中数据预处理是必不可少的一部分,对于缺失值的填充往往会影响比赛的结果,在比赛中不妨尝试多种填充然后比较结果选择结果最优的一种, 比赛数据相比真实场景的数据相对要“干净”一些,但是还是会有一定的“脏”数据存在,清洗一些异常值往往会获得意想不到的效果。

2.1缺失值填充

  由于很多数据分析过程不能直接处理缺失值,因此需要在预处理阶段“消除”缺失数据。常用的方法包括忽略含缺失项的记录、将缺失值视为特殊值或者插补缺失值。目前很多数据挖掘与统计分析的软件都提供了对缺失值的处理方法,例如WEKA"用最频繁值与均值替换缺失的离散变量与连续变量,而统计软件spssi1中不仅提供了缺失值的分析还给出了包括均值、中位数以及回归拟合等多种填充方法。下面对一些常用的缺失值处理方法进行简单介绍:

  Listwise deletion:把含有缺失值的记录删除,尽管这种方法在MCAR 数据中能得到相对无偏的参数估计,但它降低了样本的有效大小,会增加估算误差
  Pairwise deletion:仅在需要进行配对的变量有缺失值时才将该条记录删除。这种方法虽然先比前者多保留了一些数据,但仍然不适用于MCAR之外的情况。由于不同变量的丢失率不同,其估算准确度相差也会很大。
  Single imputation:最常用最简单的单一插补法有均值替换和hotdeck插补方法。前者多为设定默认值(如0)或者进行均值插补,后者考虑了属性间的依赖关系,用一组决策属性对数据进行分组,同组内在某些变量上的取值是一样的。复杂一点的统计学方法有回归方法和期望最大化算法等。而机器学习领域的单值填充法包括聚类、分类等方法。
  Multiple imputation:用某一选定算法获得若干个填充值,形成若干完整数据集,然后用任意一个应用于完整数据集的方法来对它们进行分析,综合得到一个结果值。多值填充考虑了插补的不确定性,效果较单值填充好,但时间代价高。

  常见的填充方法
  (1)填充固定值
  选取某个固定值/默认值填充缺失值。

train_data.fillna(0, inplace=True) # 填充 0

  (2)填充均值
  对每一列的缺失值,填充当列的均值。

train_data.fillna(train_data.mean(),inplace=True) # 填充均值

  (3)填充中位数
  对每一列的缺失值,填充当列的中位数。

train_data.fillna(train_data.median(),inplace=True) # 填充中位数

  (4)填充众数
  对每一列的缺失值,填充当列的众数。由于存在某列缺失值过多,众数为nan的情况,因此这里取的是每列删除掉nan值后的众数。

train_data.fillna(train_data.mode(),inplace=True) # 填充众数,该数据缺失太多众数出现为nan的情况
features_mode = {
    
    }
for f in features:
    print f,':', list(train_data[f].dropna().mode().values)
    features_mode[f] = list(train_data[f].dropna().mode().values)[0]
train_data.fillna(features_mode,inplace=True)

  (5)填充上下条的数据
  对每一条数据的缺失值,填充其上下条数据的值。

train_data.fillna(method='pad', inplace=True) # 填充前一条数据的值,但是前一条也不一定有值
train_data.fillna(0, inplace=True)
 
train_data.fillna(method='bfill', inplace=True) # 填充后一条数据的值,但是后一条也不一定有值
train_data.fillna(0, inplace=True)

  (6)填充插值得到的数据
  用插值法拟合出缺失的数据,然后进行填充。

for f in features: # 插值法填充
    train_data[f] = train_data[f].interpolate()
    
train_data.dropna(inplace=True)

  (7)填充KNN数据
  填充近邻的数据,先利用knn计算临近的k个数据,然后填充他们的均值。(安装fancyimpute)除了knn填充,fancyimpute还提供了其他填充方法。

from fancyimpute import KNN
 
train_data_x = pd.DataFrame(KNN(k=6).fit_transform(train_data_x), columns=features)

  (8)填充模型预测的值
  把缺失值作为新的label,建立模型得到预测值,然后进行填充。这里选择某个缺失值数量适当的特征采用随机森林RF进行拟合,其他缺失特征采用均值进行填充。

new_label = 'SNP46'
new_features = []
for f in features:
    if f != new_label:
        new_features.append(f)
        
new_train_x = train_data[train_data[new_label].isnull()==False][new_features]
new_train_x.fillna(new_train_x.mean(), inplace=True) # 其他列填充均值
new_train_y = train_data[train_data[new_label].isnull()==False][new_label]
 
new_predict_x = train_data[train_data[new_label].isnull()==True][new_features]
new_predict_x.fillna(new_predict_x.mean(), inplace=True) # 其他列填充均值
new_predict_y = train_data[train_data[new_label].isnull()==True][new_label]
 
rfr = RandomForestRegressor(random_state=666, n_estimators=10, n_jobs=-1)
rfr.fit(new_train_x, new_train_y)
new_predict_y = rfr.predict(new_predict_x)
 
new_predict_y = pd.DataFrame(new_predict_y, columns=[new_label], index=new_predict_x.index)
new_predict_y = pd.concat([new_predict_x, new_predict_y], axis=1)
new_train_y = pd.concat([new_train_x, new_train_y], axis=1)
new_train_data = pd.concat([new_predict_y,new_train_y]) 
 
train_data_x = new_train_data[features]
train_data_y = train_data['label']

2.2时间格式处理

  (1)时间本身的特征
  形式:日期、时间戳等。
  方法:将时间变量作为类别变量处理

  (2)时间变量之间的组合特征
  方法:根据两个或多个时间变量的含义,进行特征组合

  (3)时间序列相关特征
  目的:基于历史数据预测未来信息。
  方法:滞后特征、滑动窗口统计特征

2.3类别特征处理

  1. 对于类别有序的特征,比如age等,是可以当成数值型变量处理。对于非类别有序变量,比如仓库编号,推荐使用one-hot,虽然one-hot会增加内存开销以及训练开销。
  2. 类别型变量在范围较小的时候(如[10-100]),推荐使用。

  xgboost是不支持category特征的,所以在训练模型前,需要进行预处理,可以根据特征的具体形式来选择:

  • 无序特征:one-hot encoding,比如城市
  • 有序特征:label encoding,比如版本号

3.异常值处理

  异常值也称为离群点,就是那些远离绝大多数样本点的特殊群体,通常这样的数据点在数据集中都表现出不合理的特性。如果忽视这些异常值,在某些建模场景下就会导致结论的错误(如线性回归模型、K均值聚类等),所以在数据的探索过程中,有必要识别出这些异常值并处理好它们。

  当你发现异常值后,一定要先分清是什么原因导致的异常值,然后再考虑如何处理首先,如果这一异常值并不代表一种规律性的,而是极其偶然的现象,或者说你并不想研究这种偶然的现象,这时可以将其删除。其次,如果异常值存在且代表了一种真实存在的现象,那就不能随便删除。在现有的欺诈场景中很多时候欺诈数据本身相对于正常数据勒说就是异常的,我们要把这些异常点纳入,重新拟合模型,研究其规律。能用监督的用监督模型,不能用的还可以考虑用异常检测的算法来做。

  • 注意test的数据不能删。

3.1 检测异常的方法一:正态分布法

  在统计学中,如果一个数据分布近似正态,那么大约 68% 的数据值会在均值的一个标准差范围内,大约 95% 会在两个标准差范围内,大约 99.7% 会在三个标准差范围内。

  根据正态分布的定义可知,数据点落在偏离均值正负1倍标准差(即sigma值)内的概率为68.2%;数据点落在偏离均值正负2倍标准差内的概率为95.4%;数据点落在偏离均值正负3倍标准差内的概率为99.6%。

  所以,换个角度思考上文提到的概率值,如果数据点落在偏离均值正负2倍标准差之外的概率就不足5%,它属于小概率事件,即认为这样的数据点为异常点。同理,如果数据点落在偏离均值正负3倍标准差之外的概率将会更小,可以认为这些数据点为极端异常点。为使读者直观地理解文中提到的概率值,可以查看标准正态分布的概率密度图,如下图所示:

在这里插入图片描述

  进一步,基于上图的结论,可以按照下表中的判断条件,识别出数值型变量的异常点和极端异常点,如下表所示:

在这里插入图片描述
  利用正态分布的知识点,结合pyplot子模块中的plot函数绘制折线图和散点图,并借助于两条水平参考线识别异常值或极端异常值。

3.2 检测异常的方法二:箱型图

  箱线图技术实际上就是利用数据的分位数识别其中的异常点,该图形属于典型的统计图形,在学术界和工业界都得到广泛的应用。箱线图的形状特征如下图所示:

在这里插入图片描述

  图中的下四分位数指的是数据的25%分位点所对应的值(Q1);中位数即为数据的50%分位点所对应的值(Q2);上四分位数则为数据的75%分位点所对应的值(Q3);上须的计算公式为Q3+1.5(Q3-Q1);下须的计算公式为Q1-1.5(Q3-Q1)。其中,Q3-Q1表示四分位差。如果采用箱线图识别异常值,其判断标准是,当变量的数据值大于箱线图的上须或者小于箱线图的下须时,就可以认为这样的数据点为异常点。

  所以,基于上方的箱线图,可以定义某个数值型变量中的异常点和极端异常点,它们的判断表达式如下表所示:

加粗样式

  • 总结一句话:四分位数会将数据分为三个点和四个区间,IQR = Q3 -Q1,下触须=Q1 − 1.5x IQR,上触须=Q3 + 1.5x IQR

3.3异常值的处理方法

1.删除含有异常值的记录
2.将异常值视为缺失值,交给缺失值处理方法来处理
3.用平均值来修正
4.不处理

4.数据分桶

  • 特征分箱的目的:
      从模型效果上来看,特征分箱主要是为了降低变量的复杂性,减少变量噪音对模型的影响,提高自变量和因变量的相关度。从而使模型更加稳定

  • 数据分桶的对象:
      将连续变量离散化
      将多状态的离散变量合并成少状态

  • 分箱的原因:
      数据的特征内的值跨度可能比较大,对有监督和无监督中如k-均值聚类它使用欧氏距离作为相似度函数来测量数据点之间的相似度。都会造成大吃小的影响,其中一种解决方法是对计数值进行区间量化即数据分桶也叫做数据分箱,然后使用量化后的结果。

  • 分箱的优点:
      处理缺失值:当数据源可能存在缺失值,此时可以把null单独作为一个分箱。
      处理异常值:当数据中存在离群点时,可以把其通过分箱离散化处理,从而提高变量的鲁棒性(抗干扰能力)。例如,age若出现200这种异常值,可分入“age > 60”这个分箱里,排除影响。
      业务解释性:我们习惯于线性判断变量的作用,当x越来越大,y就越来越大。但实际x与y之间经常存在着非线性关系,此时可经过WOE变换。

  • 特别要注意一下分箱的基本原则:
      (1)最小分箱占比不低于5%
      (2)箱内不能全部是好客户
      (3)连续箱单调

  1. 固定宽度分箱
      当数值横跨多个数量级时,最好按照 10 的幂(或任何常数的幂)来进行分组:09、1099、100999、10009999,等等。固定宽度分箱非常容易计算,但如果计数值中有比较大的缺口,就会产生很多没有任何数据的空箱子。
# 通过除法映射到间隔均匀的分箱中,每个分箱的取值范围都是loanAmnt/1000
data['loanAmnt_bin1'] = np.floor_divide(data['loanAmnt'], 1000)
## 通过对数函数映射到指数宽度分箱
data['loanAmnt_bin2'] = np.floor(np.log10(data['loanAmnt']))
  1. 分位数分箱
data['loanAmnt_bin3'] = pd.qcut(data['loanAmnt'], 10, labels=False)
  1. 卡方分箱及其他分箱方法的尝试

5.特征交互

  • 交互特征的构造非常简单,使用起来却代价不菲。如果线性模型中包含有交互特征对,那它的训练时间和评分时间就会从 O(n) 增加到 O(n2),其中 n 是单一特征的数量。
for col in ['grade', 'subGrade']: 
    temp_dict = data_train.groupby([col])['isDefault'].agg(['mean']).reset_index().rename(columns={
    
    'mean': col + '_target_mean'})
    temp_dict.index = temp_dict[col].values
    temp_dict = temp_dict[col + '_target_mean'].to_dict()

    data_train[col + '_target_mean'] = data_train[col].map(temp_dict)
    data_test_a[col + '_target_mean'] = data_test_a[col].map(temp_dict)
# 其他衍生变量 mean 和 std
for df in [data_train, data_test_a]:
    for item in ['n0','n1','n2','n2.1','n4','n5','n6','n7','n8','n9','n10','n11','n12','n13','n14']:
        df['grade_to_mean_' + item] = df['grade'] / df.groupby([item])['grade'].transform('mean')
        df['grade_to_std_' + item] = df['grade'] / df.groupby([item])['grade'].transform('std')

6.特征编码

6.1 labelEncode 直接放入树模型中

#label-encode:subGrade,postCode,title
# 高维类别特征需要进行转换
for col in tqdm(['employmentTitle', 'postCode', 'title','subGrade']):
    le = LabelEncoder()
    le.fit(list(data_train[col].astype(str).values) + list(data_test_a[col].astype(str).values))
    data_train[col] = le.transform(list(data_train[col].astype(str).values))
    data_test_a[col] = le.transform(list(data_test_a[col].astype(str).values))
print('Label Encoding 完成')

6.2 逻辑回归等模型要单独增加的特征工程

  • 对特征做归一化,去除相关性高的特征。
  • 归一化目的是让训练过程更好更快的收敛,避免特征大吃小的问题
  • 去除相关性是增加模型的可解释性,加快预测过程
# 举例归一化过程
#伪代码
for fea in [要归一化的特征列表]:
    data[fea] = ((data[fea] - np.min(data[fea])) / (np.max(data[fea]) - np.min(data[fea])))

7.特征选择

  • 特征选择技术可以精简掉无用的特征,以降低最终模型的复杂性,它的最终目的是得到一个简约模型,在不降低预测准确率或对预测准确率影响不大的情况下提高计算速度。特征选择不是为了减少训练时间(实际上,一些技术会增加总体训练时间),而是为了减少模型评分时间。

  特征选择的方法:
    1 Filter
      o 方差选择法
      o 相关系数法(pearson 相关系数)
      o 卡方检验
      o 互信息法
    2 Wrapper (RFE)
      o 递归特征消除法
    3 Embedded
      o 基于惩罚项的特征选择法
      o 基于树模型的特征选择

7.1 Filter

基于特征间的关系进行筛选

  (1)方差选择法

  • 方差选择法中,先要计算各个特征的方差,然后根据设定的阈值,选择方差大于阈值的特征

  (2)相关系数法

  • Pearson 相关系数 皮尔森相关系数是一种最简单的,可以帮助理解特征和响应变量之间关系的方法,该方法衡量的是变量之间的线性相关性。 结果的取值区间为 [-1,1] , -1 表示完全的负相关, +1表示完全的正相关,0 表示没有线性相关。

  (3)卡方检验

  • 经典的卡方检验是用于检验自变量对因变量的相关性。 假设自变量有N种取值,因变量有M种取值,考虑自变量等于i且因变量等于j的样本频数的观察值与期望的差距。 其统计量如下: χ2=∑(A−T)2T,其中A为实际值,T为理论值。
    (注:卡方只能运用在正定矩阵上,否则会报错Input X must be non-negative)

  (4)互信息法

  • 经典的互信息也是评价自变量对因变量的相关性的。 在feature_selection库的SelectKBest类结合最大信息系数法可以用于选择特征。

7.2 Wrapper (Recursive feature elimination,RFE)

  递归特征消除法

  • 递归消除特征法使用一个基模型来进行多轮训练,每轮训练后,消除若干权值系数的特征,再基于新的特征集进行下一轮训练。 在feature_selection库的RFE类可以用于选择特征。

7.3 Embedded

  ==基于惩罚项的特征选择法 ==

  • 使用带惩罚项的基模型,除了筛选出特征外,同时也进行了降维。 在feature_selection库的SelectFromModel类结合逻辑回归模型可以用于选择特征。

8.实战

# 导入包并读取数据
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import datetime
from tqdm import tqdm
from sklearn.preprocessing import LabelEncoder
from sklearn.feature_selection import SelectKBest
from sklearn.feature_selection import chi2
from sklearn.preprocessing import MinMaxScaler
import xgboost as xgb
import lightgbm as lgb
from catboost import CatBoostRegressor
import warnings
from sklearn.model_selection import StratifiedKFold, KFold
from sklearn.metrics import accuracy_score, f1_score, roc_auc_score, log_loss
warnings.filterwarnings('ignore')

data_train = pd.read_csv('./train.csv')
data_test_a = pd.read_csv('./testA.csv')

# 特征预处理
# 首先我们查找出数据中的对象特征和数值特征
numerical_fea = list(data_train.select_dtypes(exclude=['object']).columns)
category_fea = list(filter(lambda x: x not in numerical_fea, list(data_train.columns)))
label = 'isDefault'
numerical_fea.remove(label)

# 缺失值填充
# 查看缺失值情况
print(data_train.isnull().sum())

# 按照平均数填充数值型特征
data_train[numerical_fea] = data_train[numerical_fea].fillna(data_train[numerical_fea].median())
data_test_a[numerical_fea] = data_test_a[numerical_fea].fillna(data_train[numerical_fea].median())
# 按照众数填充类别型特征
data_train[category_fea] = data_train[category_fea].fillna(data_train[category_fea].mode())
data_test_a[category_fea] = data_test_a[category_fea].fillna(data_train[category_fea].mode())
print(data_train.isnull().sum())

# 查看类别特征
print(category_fea)

# 时间格式处理
# 转化成时间格式
for data in [data_train, data_test_a]:
    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
print(data_train['employmentLength'].value_counts(dropna=False).sort_index())

# 对象类型特征转换到数值
def employmentLength_to_int(s):
    if pd.isnull(s):
        return s
    else:
        return np.int8(s.split()[0])
for data in [data_train, data_test_a]:
    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)
print(data['employmentLength'].value_counts(dropna=False).sort_index())

# 对earliesCreditLine进行预处理
print(data_train['earliesCreditLine'].sample(5))

for data in [data_train, data_test_a]:
    data['earliesCreditLine'] = data['earliesCreditLine'].apply(lambda s: int(s[-4:]))

# 类别特征处理
# 部分类别特征
cate_features = ['grade', 'subGrade', 'employmentTitle', 'homeOwnership', 'verificationStatus', 'purpose', 'postCode', 'regionCode', \
                 'applicationType', 'initialListStatus', 'title', 'policyCode']
for f in cate_features:
    print(f, '类型数:', data[f].nunique())

# 像等级这种类别特征,是有优先级的可以labelencode或者自映射
for data in [data_train, data_test_a]:
    data['grade'] = data['grade'].map({
    
    'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5, 'F': 6, 'G': 7})
# 类型数在2之上,又不是高维稀疏的,且纯分类特征
for data in [data_train, data_test_a]:
    data = pd.get_dummies(data, columns=['subGrade', 'homeOwnership', 'verificationStatus', 'purpose', 'regionCode'], drop_first=True)

# 异常值处理
# 检测异常的方法一:均方差
def find_outliers_by_3segama(data,fea):
    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
# 得到特征的异常值后可以进一步分析变量异常值和目标变量的关系
data_train = data_train.copy()
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)
# 删除异常值
for fea in numerical_fea:
    data_train = data_train[data_train[fea+'_outliers'] == '正常值']
    data_train = data_train.reset_index(drop=True)

# 固定宽度分箱
# 通过除法映射到间隔均匀的分箱中,每个分箱的取值范围都是loanAmnt/1000
data['loanAmnt_bin1'] = np.floor_divide(data['loanAmnt'], 1000)
# 通过对数函数映射到指数宽度分箱
data['loanAmnt_bin2'] = np.floor(np.log10(data['loanAmnt']))

# 分位数分箱
data['loanAmnt_bin3'] = pd.qcut(data['loanAmnt'], 10, labels=False)

# 特征交互
for col in ['grade', 'subGrade']:
    temp_dict = data_train.groupby([col])['isDefault'].agg(['mean']).reset_index().rename(columns={
    
    'mean': col + '_target_mean'})
    temp_dict.index = temp_dict[col].values
    temp_dict = temp_dict[col + '_target_mean'].to_dict()

    data_train[col + '_target_mean'] = data_train[col].map(temp_dict)
    data_test_a[col + '_target_mean'] = data_test_a[col].map(temp_dict)
# 其他衍生变量 mean 和 std
for df in [data_train, data_test_a]:
    for item in ['n0', 'n1', 'n2', 'n2.1', 'n4', 'n5', 'n6', 'n7', 'n8', 'n9', 'n10', 'n11', 'n12', 'n13', 'n14']:
        df['grade_to_mean_' + item] = df['grade'] / df.groupby([item])['grade'].transform('mean')
        df['grade_to_std_' + item] = df['grade'] / df.groupby([item])['grade'].transform('std')

# 特征编码
# labelEncode 直接放入树模型中
# label-encode:subGrade,postCode,title
# 高维类别特征需要进行转换
for col in tqdm(['employmentTitle', 'postCode', 'title', 'subGrade']):
    le = LabelEncoder()
    le.fit(list(data_train[col].astype(str).values) + list(data_test_a[col].astype(str).values))
    data_train[col] = le.transform(list(data_train[col].astype(str).values))
    data_test_a[col] = le.transform(list(data_test_a[col].astype(str).values))
print('Label Encoding 完成')

# 特征选择
# 删除不需要的数据
for data in [data_train, data_test_a]:
    data.drop(['issueDate', 'id'], axis=1, inplace=True)
# "纵向用缺失值上面的值替换缺失值"
data_train = data_train.fillna(axis=0, method='ffill')
numerical_fea.remove('id')
print(numerical_fea)
# x_train = data_train.drop(['isDefault', 'id'], axis=1)
# # 计算协方差
# data_corr = x_train.corrwith(data_train.isDefault)  # 计算相关性
# result = pd.DataFrame(columns=['features', 'corr'])
# result['features'] = data_corr.index
# result['corr'] = data_corr.values
# 当然也可以直接看图
data_numeric = data_train[numerical_fea]
correlation = data_numeric.corr()
f, ax = plt.subplots(figsize=(7, 7))
plt.title('Correlation of Numeric Features with Price', y=1, size=16)
sns.heatmap(correlation, square=True, vmax=0.8)
plt.show()

features = [f for f in data_train.columns if f not in ['id', 'issueDate', 'isDefault'] and '_outliers' not in f]
x_train = data_train[features]
x_test = data_test_a[features]
y_train = data_train['isDefault']


def cv_model(clf, train_x, train_y, test_x, clf_name):
    folds = 5
    seed = 2020
    kf = KFold(n_splits=folds, shuffle=True, random_state=seed)

    train = np.zeros(train_x.shape[0])
    test = np.zeros(test_x.shape[0])

    cv_scores = []

    for i, (train_index, valid_index) in enumerate(kf.split(train_x, train_y)):
        print('************************************ {} ************************************'.format(str(i + 1)))
        trn_x, trn_y, val_x, val_y = train_x.iloc[train_index], train_y[train_index], train_x.iloc[valid_index], \
                                     train_y[valid_index]

        if clf_name == "lgb":
            train_matrix = clf.Dataset(trn_x, label=trn_y)
            valid_matrix = clf.Dataset(val_x, label=val_y)

            params = {
    
    
                'boosting_type': 'gbdt',
                'objective': 'binary',
                'metric': 'auc',
                'min_child_weight': 5,
                'num_leaves': 2 ** 5,
                'lambda_l2': 10,
                'feature_fraction': 0.8,
                'bagging_fraction': 0.8,
                'bagging_freq': 4,
                'learning_rate': 0.1,
                'seed': 2020,
                'nthread': 28,
                'n_jobs': 24,
                'silent': True,
                'verbose': -1,
            }

            model = clf.train(params, train_matrix, 50000, valid_sets=[train_matrix, valid_matrix], verbose_eval=200,
                              early_stopping_rounds=200)
            val_pred = model.predict(val_x, num_iteration=model.best_iteration)
            test_pred = model.predict(test_x, num_iteration=model.best_iteration)

            # print(list(sorted(zip(features, model.feature_importance("gain")), key=lambda x: x[1], reverse=True))[:20])

        if clf_name == "xgb":
            train_matrix = clf.DMatrix(trn_x, label=trn_y)
            valid_matrix = clf.DMatrix(val_x, label=val_y)

            params = {
    
    'booster': 'gbtree',
                      'objective': 'binary:logistic',
                      'eval_metric': 'auc',
                      'gamma': 1,
                      'min_child_weight': 1.5,
                      'max_depth': 5,
                      'lambda': 10,
                      'subsample': 0.7,
                      'colsample_bytree': 0.7,
                      'colsample_bylevel': 0.7,
                      'eta': 0.04,
                      'tree_method': 'exact',
                      'seed': 2020,
                      'nthread': 36,
                      "silent": True,
                      }

            watchlist = [(train_matrix, 'train'), (valid_matrix, 'eval')]

            model = clf.train(params, train_matrix, num_boost_round=50000, evals=watchlist, verbose_eval=200,
                              early_stopping_rounds=200)
            val_pred = model.predict(valid_matrix, ntree_limit=model.best_ntree_limit)
            test_pred = model.predict(test_x, ntree_limit=model.best_ntree_limit)

        if clf_name == "cat":
            params = {
    
    'learning_rate': 0.05, 'depth': 5, 'l2_leaf_reg': 10, 'bootstrap_type': 'Bernoulli',
                      'od_type': 'Iter', 'od_wait': 50, 'random_seed': 11, 'allow_writing_files': False}

            model = clf(iterations=20000, **params)
            model.fit(trn_x, trn_y, eval_set=(val_x, val_y),
                      cat_features=[], use_best_model=True, verbose=500)

            val_pred = model.predict(val_x)
            test_pred = model.predict(test_x)

        train[valid_index] = val_pred
        test = test_pred / kf.n_splits
        cv_scores.append(roc_auc_score(val_y, val_pred))

        print(cv_scores)

    print("%s_scotrainre_list:" % clf_name, cv_scores)
    print("%s_score_mean:" % clf_name, np.mean(cv_scores))
    print("%s_score_std:" % clf_name, np.std(cv_scores))
    return train, test

def lgb_model(x_train, y_train, x_test):
    lgb_train, lgb_test = cv_model(lgb, x_train, y_train, x_test, "lgb")
    return lgb_train, lgb_test

def xgb_model(x_train, y_train, x_test):
    xgb_train, xgb_test = cv_model(xgb, x_train, y_train, x_test, "xgb")
    return xgb_train, xgb_test

def cat_model(x_train, y_train, x_test):
    cat_train, cat_test = cv_model(CatBoostRegressor, x_train, y_train, x_test, "cat")

lgb_train, lgb_test = lgb_model(x_train, y_train, x_test)


# testA_result = pd.read_csv('./testA_result.csv')
# print(roc_auc_score(testA_result['isDefault'].values, lgb_test))

# cv_model[['id', 'issueDate', 'isDefault']].to_csv('data_for_model.csv', index=None)

# 10. 保存处理好的特征
data_train.to_csv('./train_data_v1.csv', index=False)
data_test_a.to_csv('./test_data_v1.csv', index=False)

data_train.to_csv('data_for_model.csv', index=None)

结果

在这里插入图片描述

在这里插入图片描述

9.总结

  特征工程是机器学习,甚至是深度学习中最为重要的一部分,在实际应用中往往也是所花费时间最多的一步。各种算法书中对特征工程部分的讲解往往少得可怜,因为特征工程和具体的数据结合的太紧密,很难系统地覆盖所有场景。本章主要是通过一些常用的方法来做介绍,例如缺失值异常值的处理方法详细对任何数据集来说都是适用的。但对于分箱等操作本章给出了具体的几种思路,需要读者自己探索。在特征工程中比赛和具体的应用还是有所不同的,在实际的金融风控评分卡制作过程中,由于强调特征的可解释性,特征分箱尤其重要。学有余力同学可以自行多尝试,希望大家在本节学习中有所收获。

参考资料

  1. 数据挖掘实践(金融风控)
  2. 零基础入门数据挖掘 - 贷款违约预测

猜你喜欢

转载自blog.csdn.net/weixin_42691585/article/details/108738531