3.天池金融风控-贷款违约预测新人赛之特征工程

第三部分主要是对数据做特征工程,这也是影响数据挖掘模型好坏的非常重要的一步。
我们的学习目标有:
1.学习数据的预处理,包括特征的预处理、缺失值和异常值的处理、数据分桶等
2. 学习特征交互、编码、选择的相应方法

1.数据预处理

第一步先导入我们需要的库

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')

#训练集测试集导入
train_data = pd.read_csv('train.csv')
test_A_data = pd.read_csv('testA.csv')
#分离出数值型特征和类别型特征
numerical_fea = list(train_data.select_dtypes(exclude = ['object']).columns)
category_fea = list(train_data.select_dtypes(include = ['object']).columns)
label = 'isDefault'
numerical_fea.remove(label)

前面我们通过EDA对特征有了一个大致的了解,我们可以一一解决刚刚碰到的一些比较明显的问题

1.1特征预处理

1.1.1缺失值处理

初始数据中难免会有缺失值,如何填充缺失值会影响到最后的结果,所以我们可以用多种填充法然后选取最优的一种。
先介绍一些简单的缺失值的填充办法:

#所有缺失值填充0
train_data = train_data.fillna(0)

#用缺失值上处的数据填充
train_data = train_data.fillna(axis = 0,method = 'ffill')

#纵向用缺失值下面的值替换缺失值,且设置最多只填充两个连续的缺失值
train_data = train_data.fillna(axis = 0,method = 'bfill',limit = 2)

我们先来看看缺失值的基本情况

train_data.isnull().sum()

'''
results:
id                        0
loanAmnt                  0
term                      0
interestRate              0
installment               0
grade                     0
subGrade                  0
employmentTitle           1
employmentLength      46799
homeOwnership             0
annualIncome              0
verificationStatus        0
issueDate                 0
isDefault                 0
purpose                   0
postCode                  1
regionCode                0
dti                     239
delinquency_2years        0
ficoRangeLow              0
ficoRangeHigh             0
openAcc                   0
pubRec                    0
pubRecBankruptcies      405
revolBal                  0
revolUtil               531
totalAcc                  0
initialListStatus         0
applicationType           0
earliesCreditLine         0
title                     1
policyCode                0
n0                    40270
n1                    40270
n2                    40270
n2.1                  40270
n4                    33239
n5                    40270
n6                    40270
n7                    40270
n8                    40271
n9                    40270
n10                   33239
n11                   69752
n12                   40270
n13                   40270
n14                   40270
dtype: int64
'''

我们发现n0-n14以及employLength特征缺失值较多,employmentTitle,postCode,dti,pubRecBankruptcies,revolUtil,title有较少的缺失,我们这里采用的方法是对于数值型变量,我们取中位数,对于类别型变量,我们使用众数来填充缺失值:

train_data[numerical_fea] = train_data[numerical_fea].fillna(train_data[numerical_fea].median())
train_data[category_fea] = train_data[category_fea].fillna(train_data[category_fea].mode())

#重新查看一下缺失值的情况
train_data.isnull().sum()

我们发现还剩下employmentLength这个特征的缺失值没有被填充,之后我们再处理。
我们先来对类别型特征做一些预处理

1.1.2时间格式处理
  • 我们发现isissueDate(贷款发放的月份)这个特征是一个时间特征,我们处理的方式是计算借款日与最文件中最小的日期的距离的天数来构造一个新的特征:
#最早的日期
startdate = datetime.datetime.strptime('2007-06-01','%Y-%m-%d')
#先转换格式再用日期-最早的日期得出天数为新的特征issueDateDT
for data in [train_data,test_A_data]:
    data['issueDate'] =pd.to_datetime(data['issueDate'],format = '%Y-%m-%d')
    #构造时间特征
    data['issueDateDT'] = data['issueDate'].apply(lambda x: x - startdate).dt.days
  • 接下来将employmentLength的数据做一些预处理:空值依然返回空值,>10年的全部分到10这个类别,<1年的分到0这个类别。
def employmentLength_to_int(s):
    if pd.isnull(s):
        return s
    else:
        return np.int8(s.split(' ')[0])
    
for data in [train_data,test_A_data]:
    data['employmentLength'].replace(to_replace='< 1 year',value='0 year',inplace=True)
    data['employmentLength'].replace(to_replace='10+ years',value='10 years',inplace=True)
    data['employmentLength'] = data['employmentLength'].apply(employmentLength_to_int)
  • 对earliesCreditLine(借款人最早报告的信用额度开立的月份)进行预处理:我们直接提取出年份
for data in [train_data,test_A_data]:
    data['earliesCreditLine'] = data['earliesCreditLine'].apply(lambda x : x[-4:])
1.1.3纯类别型特征处理

一般来熟对于类别型特征处理有两种思路:

1.若这个变量是有大小关系的,比如该数据中的grade,可以使用数值映射
('A':1,'B':2,'C':3,'D':4,'E':5,'F':6,'G':7)
2.若变量之间是没有什么关系的,我们可以用独热编码(One-hot-encode)
# 部分类别特征
cate_features = ['grade', 'subGrade', 'employmentTitle', 'homeOwnership', 'verificationStatus', 'purpose', 'postCode', 'regionCode', \
                 'applicationType', 'initialListStatus', 'title', 'policyCode']
for f in cate_features:
    print(f, '类型数:', data[f].nunique())

#对于grade这种有大小,优劣区分的特征
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 [train_data,test_A_data]:
    data = pd.get_dummies(data,columns=['subGrade', 'homeOwnership', 'verificationStatus','regionCode'],drop_first= True)

1.2异常值处理

  • 当你发现异常值后,一定要先分清是什么原因导致的异常值,然后再考虑如何处理。首先,如果这一异常值并不代表一种规律性的,而是极其偶然的现象,或者说你并不想研究这种偶然的现象,这时可以将其删除。
  • 其次,如果异常值存在且代表了一种真实存在的现象,那就不能随便删除。在现有的欺诈场景中很多时候欺诈数据本身相对于正常数据勒说就是异常的,我们要把这些异常点纳入,重新拟合模型,研究其规律。能用监督的用监督模型,不能用的还可以考虑用异常检测的算法来做。
  • 注意:test的数据不能删。
  1. 检测异常的一种方法:3 σ \sigma σ
    在统计学中,如果一个总体近似正态分布,那么大约68%的数据会落在一个标准差范围内,大约95%的数据会落在两个标准差范围内,大约99.7%的数据会落在三个标准差范围内,所以,我们可以把超过3 σ \sigma σ 的数据标为异常值。
#写一个判断异常/正常值的函数,并生成一个新的列fea_outliers
def find_outliers_by_3sigma(data,fea):
    data_mean = np.mean(data[fea])
    data_std = np.std(data[fea])
    lower_rule = data_mean - 3 * data_std
    upper_rule = data_mean + 3 * data_std
    data[fea + '_outliers'] = data[fea].apply(lambda x:str('异常值') if x < lower_rule and x > upper_rule else '正常值' )
    return data
#得到特征的异常值后可以进一步分析变量异常值和目标变量的关系
for fea in numerical_fea:
    train_data = find_outliers_by_3sigma(train_data,fea)
    print(train_data[fea + '_outliers'].value_counts())
    print(train_data.groupby(fea+'_outliers')['isDefault'].sum())
    print('*' * 20)

#删除异常值
for fea in numerical_fea:
    data_train = data_train[data_train[fea+'_outliers']=='正常值']
    data_train = data_train.reset_index(drop=True) 
  1. 检测异常的方法二:箱型图
  • 总结一句话:四分位数会将数据分为三个点和四个区间,IQR = Q3 -Q1,下触须=Q1 − 1.5x IQR,上触须=Q3 + 1.5x IQR;

1.3数据分桶

  • 特征分箱的目的:
    1.从模型效果上来看,特征分箱主要是为了降低变量的复杂性减少变量噪音对模型的影响,提高自变量和因变量的相关度。从而使模型更加稳定
  • 数据分桶的对象:
    1.将连续变量离散化
    2.将多状态的离散变量并成少状态
  • 分箱的原因:
    1.数据的特征内的值跨度可能比较大,对有监督和无监督中如k-均值聚类它使用欧氏距离作为相似度函数来测量数据点之间的相似度。都会造成大吃小的影响,其中一种解决方法是对计数值进行区间量化即数据分桶也叫做数据分箱,然后使用量化后的结果。
  • 分箱的优点:
    1.处理缺失值上,可以将null单独作为一个分箱
    2.处理异常值上,可以把其通过分箱离散化处理,从而提高变量的鲁棒性(抗干扰能力)。例如,age若出现200这种异常值,可分入“age > 60”这个分箱里,排除影响。
    3.业务解释性上,我们习惯于线性判断变量的作用,当x越来越大,y就越来越大。但实际x与y之间经常存在着非线性关系,此时可经过WOE(Weight of Evidence)变换。
  • 特别要注意一下分箱的基本原则:
    1.最小分箱占比不低于5%
    2.箱内不能全是好客户
    3.连续箱单调
  1. 固定宽度分箱
    当数值横跨多个数量级时,最好按照 10 的幂(或任何常数的幂)来进行分组。固定宽度分箱非常容易计算,但如果计数值中有比较大的缺口,就会产生很多没有任何数据的空箱子。
# 通过除法映射到间隔均匀的分箱中,每个分箱的取值范围都是loanAmnt/1000
data['loanAmnt_bin1'] = np.floor_divide(data['loanAmnt'],1000)

# 通过对数函数映射到指数宽度分箱
data['loanAmnt_bin1'] = np.floor(np.log10(data['loanAmnt'])
                                 
# 分位数分箱
data['loanAmnt_bin3'] = pd.qcut(data['loanAmnt'], 10, labels=False)

#卡方分箱及其他自学

1.4特征交互(需要自己深入学习)

  • 交互特征的构造非常简单,使用起来却代价不菲。如果线性模型中包含有交互特征对,那它的训练时间和评分时间就会从 O(n) 增加到 O(n2),其中 n 是单一特征的数量。
for col in ['grade', 'subGrade']: 
    temp_dict = train_data.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()

    train_data[col + '_target_mean'] = train_data[col].map(temp_dict)
    train_data[col + '_target_mean'] = test_A_data[col].map(temp_dict)
    
    
# 其他衍生变量 mean 和 std
for df in [train_data, test_A_data]:
    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')

1.5特征编码

labelEncoder 直接放入模型中

##label-encode:subGrade,postCode,title
# 高维类别特征需要进行转换
from sklearn.preprocessing import LabelEncoder
#tqdm是一个看程序进程的函数
for col in tqdm(['subGrade','postCode','title','employmentTitle']):
    le = LabelEncoder()
    # astype()函数可用于转化dateframe某一列的数据类型,values方法返回结果是数组
    le.fit(list(train_data[col].astype(str).values) + list(test_A_data[col].astype(str).values))
    print(le.classes_)
    train_data[col] = le.transform(list(train_data[col].astype(str).values))
    test_A_data[col] = le.transform(list(test_A_data[col].astype(str).values))
print('Label Encoding Finished!')

某些算法需要单独增加的特征工程

  • 对特征做归一化,去掉相关性高的特征
  • 归一化目的是让训练过程更好更快的收敛,避免特征大吃小的问题
  • 去除相关性是增加模型的可解释性,加快预测过程。
#写一个归一化的函数
for fea in tqdm(['需要归一化的列']):
	data[fea] = (data[fea] - min(data[fea]))/(max(data[fea]) - min(data[fea]))

1.6特征选择

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

1.filter(过滤法)

  1方差选择法
  2相关系数法(pearson 相关系数)
  3卡方检验
  4互信息法

2.Wrapper(包裹法)

1.RFE(递归特征消除)
2.RFECV(递归特征消除交叉验证)

3.Embedded

1.基于惩罚项的特征选择法
2.基于树模型的特征选择

接下来对以上三种方法进行介绍:
1.filter法

#1.方差选择法
#该方法要计算各个特征的方差,设置一个阈值,仅选择大于阈值的特征(方差小说明数据之间变化不大所以可能对变量影响不大)
from sklearn.feature_selection import VarianceThreshold
#threshold就是阈值
VarianceThreshold(threshold=3).fit_transform(train,target_train)


#2.相关系数法
#根据Pearson相关系数计算每个特征与目标的线性相关性
#结果的取值区间为 [-1,1] , -1 表示完全的负相关, +1表示完全的正相关,0 表示没有线性相关。
#结合Pearson相关系数选取最好的K个特征
#注意: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)


#3.chi2检验
#同样是用于检验自变量与因变量之间的相关性
#注:卡方只能运用在正定矩阵上,否则会报错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)

#4.互信息法
#也是检验自变量与因变量之间的相关性
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)

2.Wrapper(RFE)
递归特征消除法(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.Embedded(惩罚法)
基于惩罚项的特征选择法 使用带惩罚项的基模型,除了筛选出特征外,同时也进行了降维

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)

2.汇总处理

# 删除不需要的数据
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')

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)

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')
roc_auc_score(testA_result['isDefault'].values, lgb_test)
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值