总有刁民想害朕
作者总有刁民想害朕·2017-10-12 15:24
存储工程师·ansteel

机器学习:构建贷款违约预测模型

字数 22136阅读 18351评论 2赞 7

1 项目概述

本项目需解决的问题

本项目通过利用P2P平台Lending Club的贷款数据,进行机器学习,构建贷款违约预测模型,对新增贷款申请人进行预测是否会违约,从而决定是否放款。

建模思路

以下是本次项目机器学习工作流程,实际操作中,其实每个步骤都是反复迭代的过程。

2 场景解析(算法选择)

贷款申请人向Lending Club平台申请贷款时,Lending Club平台通过线上或线下让客户填写贷款申请表,收集客户的基本信息,这里包括申请人的年龄、性别、婚姻状况、学历、贷款金额、申请人财产情况等信息,通常来说还会借助第三方平台如征信机构或FICO等机构的信息。通过这些信息属性来做线性回归 ,生成预测模型,Lending Club平台可以通过预测判断贷款申请是否会违约,从而决定是否向申请人发放贷款。

通过以上的业务逻辑和上一个项目报告《注册会计师带你探索风险分析(EDA)》的数据探索,下面进行场景解析。

1)首先,我们的场景是通过用户的历史行为(如历史数据的多维特征和贷款状态是否违约)来训练模型,通过这个模型对新增的贷款人“是否具有偿还能力,是否具有偿债意愿”进行分析,预测贷款申请人是否会发生违约贷款。这是一个监督学习的场景,因为已知了特征以及贷款状态是否违约(目标列),我们判定贷款申请人是否违约是一个二元分类问题,可以通过一个分类算法来处理,这里选用逻辑斯蒂回归(Logistic Regression)。

2)通过上一个项目报告《注册会计师带你探索风险分析(EDA)》

可以看到,部分数据是半结构化数据,需要进行特征抽象。

现对该业务场景进行总结如下:

● 根据历史记录数据学习并对贷款是否违约进行预测,监督学习场景,选择逻辑斯蒂回归(Logistic Regression)算法。
● 数据为半结构化数据,需要进行特征抽象。

微信图片_20171012133949.jpg

微信图片_20171012133949.jpg

本项目报告,我将如何运用Python操作数据和机器学习的思考过程均记录下来。

3 数据预处理(Pre-Processing Data)

● 前期准备

# Imports

# Numpy,Pandas
import numpy as np
import pandas as pd

# matplotlib,seaborn,pyecharts

import matplotlib.pyplot as plt
plt.style.use('ggplot')  #风格设置近似R这种的ggplot库
import seaborn as sns
sns.set_style('whitegrid')
%matplotlib inline
import missingno as msno

#  忽略弹出的warnings
import warnings
warnings.filterwarnings('ignore')  

pd.set_option('display.float_format', lambda x: '%.5f' % x)

● 数据获取与解析

data = pd.read_csv('LoanStats_2017Q2.csv' , encoding='latin-1',skiprows = 1) #读取数据data.head() #查看表格默认前5行

微信图片_20171012134138.jpg

微信图片_20171012134138.jpg

统计每列属性缺失值的数量。

check_null = data.isnull().sum(axis=0).sort_values(ascending=False)/float(len(data)) #查看缺失值比例
print(check_null[check_null > 0.2]) # 查看缺失比例大于20%的属性。

微信图片_20171012134455.jpg

微信图片_20171012134455.jpg

数据是否有缺失值或乱码一般是判断数据质量的主要因素。

对于缺失值的处理,一般来说先判定缺失的数据是否有意义。从上面信息可以发现,本次数据集缺失值较多的属性对我们模型预测意义不大,例如id和member_id以及url等。因此,我们直接删除这些没有意义且缺失值较多的属性。此外,如果缺失值对属性来说是有意义的,还得细分缺失值对应的属性是数值型变量或是分类类型变量。

thresh_count = len(data)*0.4 # 设定阀值
data = data.dropna(thresh=thresh_count, axis=1 ) #若某一列数据缺失的数量超过阀值就会被删除

再次检查缺失值情况,发现缺失值比较多的数据列已被我们删除。

data.isnull().sum(axis=0).sort_values(ascending=False)/float(len(data))

微信图片_20171012134618.jpg

微信图片_20171012134618.jpg

data.to_csv('loans_2017q2_ml.csv', index = False) # 将初步预处理后的数据转化为csv

再次用pandas解析数据。

loans = pd.read_csv('loans_2017q2_ml.csv',encoding='gb2312')
loans.dtypes.value_counts() # 分类统计数据类型

微信图片_20171012134651.jpg

微信图片_20171012134651.jpg

我们通过Pandas的nunique方法来筛选属性分类为一的变量,剔除分类数量只有1的变量,Pandas方法nunique()返回的是变量的分类数量(除去非空值)。

loans = loans.loc[:,loans.apply(pd.Series.nunique) != 1]

查看数据的行列,发现数据已比之前少了3列。

loans.shape
out:(105455, 98)

● 缺失值处理——分类型变量

首先,我们查看分类变量缺失值的情况。

objectColumns = loans.select_dtypes(include=["object"]).columns
loans[objectColumns].isnull().sum().sort_values(ascending=False)

微信图片_20171012134902.jpg

微信图片_20171012134902.jpg

我们注意到分类变量中,"int_rate"、"revol_util"、“annual_inc”的属性实质意义是数值,但pandas因为它们含有“%”符号或数字间有逗号而误识别为字符。为了方便后续处理,我们先将他们的数据类型重分类。

loans['int_rate'] = loans['int_rate'].str.rstrip('%').astype('float')
loans['revol_util'] = loans['revol_util'].str.rstrip('%').astype('float')
loans['annual_inc'] = loans['annual_inc'].str.replace(",","").astype('float')
objectColumns = loans.select_dtypes(include=["object"]).columns  # 对objectColumns重新赋值

对分类型变量缺失值来个感性认知。

msno.matrix(loans[objectColumns]) #缺失值可视化

微信图片_20171012135006.jpg

微信图片_20171012135006.jpg

从上图可以直观看出变量“emp_title”、“next_pymnt_d”缺失值较多,同时图的右边分别反映了缺失值最多和最少的行数,分别是第25行和第0行。

msno.heatmap(loans[objectColumns]) #查看缺失值之间的相关性

微信图片_20171012135054.jpg

微信图片_20171012135054.jpg

上图显示了缺失值之间的相关性,当相关性为0时,说明一个变量与另一个变量之间没有影响。相关性接近1或-1说明变量之间呈现正相关或负相关。但我们从图中发现,并不完全如此。policy_code和其他变量之间的相关性比较强,这与我们的期望相反,zop_code一般来说和其他变量没什么关系,有可能表明数据某些行的记录是不完整的。

这个热图对于选择变量对之间的数据完整性关系非常有用,但是当涉及到更大的数据关系时,它的视觉能力是有限的,而且对于非常大的数据集没有特别的支持。

我们使用pandas.fillna()处理文本变量缺失值,为分类变量缺失值创建一个分类“Unknown”。

objectColumns = loans.select_dtypes(include=["object"]).columns # 筛选数据类型为object的数据
loans[objectColumns] = loans[objectColumns].fillna("Unknown") #以分类“Unknown”填充缺失值

再次查看分类变量缺失值的情况,发现缺失值已被清洗干净。

msno.bar(loans[objectColumns]) #可视化

微信图片_20171012135131.jpg

微信图片_20171012135131.jpg

● 缺失值处理——数值型变量

查看数值型变量的缺失值情况。

loans.select_dtypes(include=[np.number]).isnull().sum().sort_values(ascending=False)

微信图片_20171012135329.jpg

微信图片_20171012135329.jpg

numColumns = loans.select_dtypes(include=[np.number]).columns
msno.matrix(loans[numColumns]) #缺失值可视化

微信图片_20171012135358.jpg

微信图片_20171012135358.jpg

pd.set_option('display.max_columns', len(loans.columns))
loans[numColumns]

微信图片_20171012135429.jpg

微信图片_20171012135429.jpg

从表格发现,第105,451行至105,454行的属性值全为NaN,这些空行对我们预测模型的构建没有任何意义,在此先单独删除这些行。

loans.drop([105451,105452,105453,105454], inplace = True)
loans[numColumns].tail() # 默认查看表格倒数5行

微信图片_20171012135500.jpg

微信图片_20171012135500.jpg

对数值型变量的缺失值,我们采用均值插补的方法来填充缺失值,这里使用可sklearn的Preprocessing模块,参数strategy可选项有median或most_frequent以及median,具体详见官方文档sklearn.preprocessing.Imputer。

http://link.zhihu.com/?target=http%3A//scikit-learn.org/stable/modules/generated/sklearn.preprocessing.Imputer.html

from sklearn.preprocessing import Imputerimr = Imputer(missing_values='NaN', strategy='mean', axis=0)  # 针对axis=0 列来处理imr = imr.fit(loans[numColumns])loans[numColumns] = imr.transform(loans[numColumns])
msno.matrix(loans) # 再次检查缺失值情况

微信图片_20171012135711.jpg

微信图片_20171012135711.jpg

● 数据过滤

对于同一份数据基于不同的数据挖掘目的,很多时候不需要把所有数据进行训练。冗余特征重复了包含在一个或多个其他属性中的许多或所有信息。例如,zip_code对于我们借款人的偿债能力并没有任何意义。grade和sub_grade是重复的属性信息。下一步,我们对数据进行过滤。

objectColumns = loans.select_dtypes(include=["object"]).columns
var = loans[objectColumns].columns
for v in var:
    print('\\nFrequency count for variable {0}'.format(v))
    print(loans[v].value_counts())
loans[objectColumns].shape

微信图片_20171012135815.jpg

微信图片_20171012135815.jpg

  1. sub_grade:与Grade的信息重复
  2. emp_title :缺失值较多,同时不能反映借款人收入或资产的真实情况
  3. zip_code:地址邮编,邮编显示不全,没有意义
  4. addr_state:申请地址所属州,不能反映借款人的偿债能力
  5. last_credit_pull_d :LendingClub平台最近一个提供贷款的时间,没有意义
  6. policy_code : 变量信息全为1pymnt_plan 基本是n
  7. title: title与purpose的信息重复,同时title的分类信息更加离散
  8. next_pymnt_d : 下一个付款时间,没有意义
  9. policy_code : 没有意义
  10. collection_recovery_fee: 全为0,没有意义
  11. earliest_cr_line : 记录的是借款人发生第一笔借款的时间
  12. issue_d : 贷款发行时间,这里提前向模型泄露了信息
  13. last_pymnt_d、collection_recovery_fee、last_pymnt_amnt: 预测贷款违约模型是贷款前的风险控制手段,这些贷后信息都会影响我们训练模型的效果,在此将这些信息删除

将以上重复或对构建预测模型没有意义的属性进行删除。

drop_list = ['sub_grade', 'emp_title',  'title', 'zip_code', 'addr_state', 
             'mths_since_last_delinq' ,'initial_list_status','title','issue_d','last_pymnt_d','last_pymnt_amnt',
             'next_pymnt_d','last_credit_pull_d','policy_code','collection_recovery_fee', 'earliest_cr_line']loans.drop(drop_list, axis=1, inplace = True)

分类型变量从28列被精减至11列。

loans.select_dtypes(include = ['object']).shape
out:(105448, 11)
loans.select_dtypes(include = ['object']).head() # 再次概览数据

微信图片_20171012135941.jpg

微信图片_20171012135941.jpg

不同算法模型需要不同的数据类型来建立。例如逻辑回归只支持数值型的数据,而随机森林通常对字符型和数值型都支持。由于在场景分析中,我们判定本项目预测贷款违约是一个二元分类问题,我们选择的算法是逻辑回归算法模型,从数据预处理的过程中也发现数据的结构是半结构化,因此需要对特征数据作进一步转换。

4 特征工程(Feature Engineering)

特征工程是机器学习中最重要的步骤。实际工作中,特征工程是个反复迭代的过程,大部分时间也是在分析业务、分析case,不断地找特征。更好的特征意味着只需要用简单的模型,更好的特征也意味着能够获得更好的依据去预测结果。2014年天池比赛,第一名团队的模型并不复杂,但他们特征更贴近业务,对业务场景理解比较好,出来的结果比淘宝负责做推荐的准确率提升16%。

本次项目特征工程主要分4大部分:1、特征衍生 2、特征抽象 3、特征缩放 4、特征选择

1 特征衍生

特征衍生是指利用现有的特征进行某种组合生成新的特征。在风险控制方面,传统银行获得企业的基本财务报表(资产负债表、利润表以及现金流量表),借助于现代成熟的财务管理体系,在不同业务场景的需求下,利用企业财务报表各种项目之间的组合,就可以衍生不同新特征反映企业不同的财务状况,例如资产与负债项目组合能够生成反映企业债务情况的特征,收入与应收账款组合能生成反映应收账款周转率(资金效率)特征等,同时还能利用企业财务报表之间的勾稽关系生成新特征来作证企业报表的质量。在金融风险控制中,要做好以上工作的前提是,你必须熟悉各种业务场景同时精通财务知识。

而Lending Club平台中,"installment"代表贷款每月分期的金额,我们将'annual_inc'除以12个月获得贷款申请人的月收入金额,然后再把"installment"(月负债)与('annual_inc'/12)(月收入)相除生成新的特征'installment_feat',新特征'installment_feat'代表客户每月还款支出占月收入的比,'installment_feat'的值越大,意味着贷款人的偿债压力越大,违约的可能性越大。

loans['installment_feat'] = loans['installment'] / (loans['annual_inc'] / 12)

2 特征抽象(feature abstraction)

特征抽象是指将数据转换成算法可以理解的数据。

#使用Pandas replace函数定义新函数:

def coding(col, codeDict):

    colCoded = pd.Series(col, copy=True)
    for key, value in codeDict.items():
        colCoded.replace(key, value, inplace=True)

    return colCoded

#把贷款状态LoanStatus编码为违约=1, 正常=0:

pd.value_counts(loans["loan_status"])

loans["loan_status"] = coding(loans["loan_status"], {'Current':0,'Fully Paid':0,'In Grace Period':1,'Late (31-120 days)':1,'Late (16-30 days)':1,'Charged Off':1})

print( '\\nAfter Coding:')

pd.value_counts(loans["loan_status"])

微信图片_20171012140651.jpg

微信图片_20171012140651.jpg

# 贷款状态分布可视化
fig, axs = plt.subplots(1,2,figsize=(14,7))
sns.countplot(x='loan_status',data=loans,ax=axs[0])
axs[0].set_title("Frequency of each Loan Status")
loans['loan_status'].value_counts().plot(x=None,y=None, kind='pie', ax=axs[1],autopct='%1.2f%%')
axs[1].set_title("Percentage of each Loan status")
plt.show()

微信图片_20171012140801.jpg

微信图片_20171012140801.jpg

从上一篇报告《 注册会计师带你探索风险分析(EDA)》也提到,平台贷款发生违约的数量占少数。贷款状态为正常的有103,743个,贷款正常状态占比为98.38%。贷款状态将作为我们建模的标签,贷款状态正常和贷款状态违约两者数量不平衡,绝大多数常见的机器学习算法对于不平衡数据集都不能很好地工作,稍后我们将会解决样本不平衡的问题。

object_columns_df =loans.select_dtypes(include=["object"]) #筛选数据类型为object的变量
print(object_columns_df.iloc[0])

微信图片_20171012141528.jpg

微信图片_20171012141528.jpg

对变量“delinq_2yrs”、“total_acc”、“last_pymnt_amnt”、“revol_bal”的数据类型重分类。

loans['delinq_2yrs'] = loans['delinq_2yrs'].apply(lambda x: float(x))
loans['total_acc'] = loans['total_acc'].apply(lambda x: float(x))
loans['revol_bal'] = loans ['revol_bal'].apply(lambda x: float(x))
loans.select_dtypes(include=["object"]).describe().T # 再次检查数据

微信图片_20171012141657.jpg

微信图片_20171012141657.jpg

将变量类型为"object"的数量从30个缩减至7个。

● 多值有序变量(Ordinal Values)

多值有序变量也称顺序数据(rank data),有序多值变量是某一有序类别的非数字型数据。有序多值变量虽然是类别,但这些类别是有序的。比如将产品分为一等品、二等品、三等品、次品等;在上一篇报告中《注册会计师带你探索风险分析(EDA)》,我们得知Lending Club对贷款申请者信用等级分类——A至G,相应地按照不同信用等级匹配贷款利率——等级为A的客户信用评分比等级为B的客户好。

A <B <C < D < E < F < G ; 信用风险从低到高排序

● 多值无序变量(Nominal Values)

多值无序变量又称分类数据(categorical data),多值无序变量是某一类别的非数字型数据。它是对事物进行分类的结果,数据表现为类别,是用文字表述的。例如,借款人按照性别分为男、女两类;分类数据中的分类是无序的,意味着我们并不能像多值有序变量那样将多值无序变量(“purpose”)进行排序。

car < wedding < education < moving < house;这种排序不符合常识,也没有任何意义
在此,我们对分类变量进一步细分。

● 多值有序变量

  1. grade
  2. emp_length
    ● 多值无序变量
  3. term
  4. home_ownership
  5. verification_status
  6. pupose
  7. application_type

对不同分类变量的转换需要使用不同的操作方法进行处理。

● 有序特征的映射

首先,我们对变量“emp_length”、"grade"进行特征抽象化,使用的方法是先构建一个mapping,再用pandas的replace( )进行映射转换,pandas的DataFrame.replace的具体用法,详见官方文档。

http://link.zhihu.com/?target=http%3A//pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.replace.html%3Fhighlight%3Dreplace%23pandas.DataFrame.replace

# 构建mapping,对有序变量"emp_length”、“grade”进行转换
mapping_dict = {
    "emp_length": {
        "10+ years": 10,
        "9 years": 9,
        "8 years": 8,
        "7 years": 7,
        "6 years": 6,
        "5 years": 5,
        "4 years": 4,
        "3 years": 3,
        "2 years": 2,
        "1 year": 1,
        "< 1 year": 0,
        "n/a": 0
    },
    "grade":{
        "A": 1,
        "B": 2,
        "C": 3,
        "D": 4,
        "E": 5,
        "F": 6,
        "G": 7
    }
}

loans = loans.replace(mapping_dict) #变量映射
loans[['emp_length','grade']].head() #查看效果

微信图片_20171012143155.jpg

微信图片_20171012143155.jpg

从上面表格可以看出多值有序变量经过处理后的效果,已经将“emp_length”,“grade”转化为算法可以理解的数据类型。

● 独热编码(one-hot encoding)

接下来,对多值无序变量进行独热编码(one-hot encoding)。

我们使用pandas的get_dummies( )方法创建虚拟特征,虚拟特征的每一列各代表变量属性的一个分类。然后再使用pandas的concat()方法将新建虚拟特征和原数据进行拼接。

get_dummies返回的一组数据是一个稀疏矩阵,但这组数据已经可以带到算法中进行计算。

n_columns = ["home_ownership", "verification_status", "application_type","purpose", "term"] 
dummy_df = pd.get_dummies(loans[n_columns])# 用get_dummies进行one hot编码
loans = pd.concat([loans, dummy_df], axis=1) #当axis = 1的时候,concat就是行对齐,然后将不同列名称的两张表合并

查看变量“home_ownership”经过独热编码处理的前后对比,我们发现“home_ownership”每一个分类均创建了一个新的虚拟特征,虚拟特征的每一列代表变量“home_ownership”属性的一个值。从下表可以看出 ,我们已成功将n1_columns的变量转化成算法可理解的数据类型,这里就不逐个展示。

loans.loc[:,loans.columns.str.contains("home_ownership")].head() #筛选包含home_ownership的所有变量

微信图片_20171012143407.jpg

微信图片_20171012143407.jpg

loans = loans.drop(n_columns, axis=1)  #清除原来的分类变量

用pandas的info( )的方法作最后检查,发现已经将所有类型为object的变量作转化,所有数据类型均满足下一步算法的要求。

loans.info() #查看数据信息

微信图片_20171012143459.jpg

微信图片_20171012143459.jpg

3 特征缩放(peature scaling)

特征缩放(peature scaling)是指将变量数据经过处理之后限定到一定的范围之内。特征缩放本质是一个去量纲的过程,同时可以加快算法收敛的速度。目前,将不同变量缩放到相同的区间有两个常用的方法:归一化(normalization)和标准化(standardization)。

我们采用的是标准化的方法,与归一化相比,标准化的方法保持了异常值所包含的有用信息,并且使得算法受到异常值的影响较小。想了解归一化和标准化的比较详见Standardization VS Normalization。

http://link.zhihu.com/?target=http%3A//scientificdg.com/index.php/en/2017/01/07/standardization-vs-normalization/

变量标准化的具体操作可以采用scikit-learn模块preprocessing的子模块StandardScaler,具体用法详见scikit-learn官网。

http://link.zhihu.com/?target=http%3A//scikit-learn.org/stable/modules/preprocessing.html%23encoding-categorical-features

col = loans.select_dtypes(include=['int64','float64']).columnslen(col)out:78 #78个特征col = col.drop('loan_status') #剔除目标变量loans_ml_df = loans # 复制数据至变量loans_ml_df###################################################################################from sklearn.preprocessing import StandardScaler # 导入模块sc =StandardScaler() # 初始化缩放器loans_ml_df[col] =sc.fit_transform(loans_ml_df[col]) #对数据进行标准化loans_ml_df.head() #查看经标准化后的数据

微信图片_20171012143552.jpg

微信图片_20171012143552.jpg

目前为止,我们已经对特征进行进行抽象化处理,使得变量的数据类型让算法可以理解,同时也将不同变量的规格缩放至同一规格。接下来,我们需要了解不同特征对目标变量的影响程度并对特征进行选择。

4 特征选择(feature selection)

维基百科对Feature selection的定义:“特征选择是从给定的集合中选择出相关特征子集的过程。”

通常来说,对特征集合做选择主要有2个原因:首先,优先选择与目标相关性较高的特征,不相关特征包含对于数据挖掘任务完全没用的信息,不相关特征可能会降低分类的准确率,因此为了增强模型的泛化能力,我们需要从原有特征集合中挑选出最佳的部分特征。其次,去除不相关特征可以降低学习的难度(less is more),能够简化分类器的计算,同时帮助了解分类问题的因果关系。

在上一个报告《注册会计师带你探索风险分析(EDA)》中,我们对Lending Club平台2017年Q2的业务数据进行了EDA,通过不同维度的变量分析,探索数据的过程可以指导我们选择特征的方向,而我们这次选用另一种方法来选择特征。

一般来说,根据特征选择的思路将特征选择分为3种方法:嵌入方法(embedded approach)、过滤方法(filter approach)、包装方法(wrapper approacch)。

  1. 过滤方法(filter approach): 通过自变量之间或自变量与目标变量之间的关联关系选择特征。
  2. 嵌入方法(embedded approach): 通过学习器自身自动选择特征。
  3. 包装方法(wrapper approacch): 通过目标函数(AUC/MSE)来决定是否加入一个变量。

对于三种特征选择的方法,scikit-learn官网也有相应的工程实现方法,具体详见Feature selection。

http://link.zhihu.com/?target=http%3A//scikit-learn.org/stable/modules/feature_selection.html

本次项目,我采用Filter、Embedded和Wrapper三种方法组合进行特征选择。

#构建X特征变量和Y目标变量x_feature = list(loans_ml_df.columns)x_feature.remove('loan_status')x_val = loans_ml_df[x_feature]y_val = loans_ml_df['loan_status']len(x_feature) # 查看初始特征集合的数量


x_val.describe().T # 初览数据

微信图片_20171012143739.jpg

微信图片_20171012143739.jpg

● Wrapper

首先,选出与目标变量相关性较高的特征。这里采用的是Wrapper方法,通过暴力的递归特征消除 (Recursive Feature Elimination)方法筛选30个与目标变量相关性最强的特征,逐步剔除特征从而达到首次降维,自变量从104个降到30个。

from sklearn.feature_selection import RFE
from sklearn.linear_model import LogisticRegression
# 建立逻辑回归分类器
model = LogisticRegression()
# 建立递归特征消除筛选器
rfe = RFE(model, 30) #通过递归选择特征,选择30个特征
rfe = rfe.fit(x_val, y_val)
# 打印筛选结果
print(rfe.support_)
print(rfe.ranking_) #ranking 为 1代表被选中,其他则未被代表未被选中

微信图片_20171012143820.jpg

微信图片_20171012143820.jpg

col_filter = x_val.columns[rfe.support_] #通过布尔值筛选首次降维后的变量
col_filter # 查看通过递归特征消除法筛选的变量

微信图片_20171012143847.jpg

微信图片_20171012143847.jpg

● Filter

正常情况下,影响目标变量的因数是多元性的;但不同因数之间会互相影响(共线性 ),或相重叠,进而影响到统计结果的真实性。下一步,我们在第一次降维的基础上,通过皮尔森相关性图谱找出冗余特征并将其剔除;同时,可以通过相关性图谱进一步引导我们选择特征的方向。

colormap = plt.cm.viridis
plt.figure(figsize=(12,12))
plt.title('Pearson Correlation of Features', y=1.05, size=15)
sns.heatmap(loans_ml_df[col_filter].corr(),linewidths=0.1,vmax=1.0, square=True, cmap=colormap, linecolor='white', annot=True)

微信图片_20171012143921.jpg

微信图片_20171012143921.jpg

drop_col = ['funded_amnt', 'funded_amnt_inv','installment', 'out_prncp', 'out_prncp_inv',
                       'total_pymnt_inv', 'total_rec_prncp', 'total_rec_int', 'home_ownership_OWN',
                       'num_sats','application_type_JOINT',  'home_ownership_RENT' ,
                       'term_ 36 months', 'total_pymnt', 'verification_status_Source Verified', 'purpose_credit_card','int_rate']
col_new = col_filter.drop(drop_col) #剔除冗余特征

目前为止,特征子集包含的变量从30个降维至13个。

len(col_new)

微信图片_20171012144026.jpg

微信图片_20171012144026.jpg

col_new # 查看剩余的特征

微信图片_20171012144104.jpg

微信图片_20171012144104.jpg

colormap = plt.cm.viridis
plt.figure(figsize=(12,12))
plt.title('Pearson Correlation of Features', y=1.05, size=15)
sns.heatmap(loans_ml_df[col_new].corr(),linewidths=0.1,vmax=1.0, square=True, cmap=colormap, linecolor='white', annot=True)

微信图片_20171012144249.jpg

微信图片_20171012144249.jpg

● Embedded

很多时候我们需要了解每个特征对目标的影响程度,在特定的业务场景下,不同的特征权重对业务的决策带来不同的影响。例如,在Lending Club的业务数据中,能够反映借款人资产状况或现金流的特征都对我们构建预测违约贷款模型十分关键。因此,我们需要对特征的权重有一个正确的评判和排序,就可以通过特征重要性排序来挖掘哪些变量是比较重要的,降低学习难度,最终达到优化模型计算的目的。这里,我们采用的是随机森林算法判定特征的重要性,工程实现方式采用scikit-learn的featureimportances 的方法,具体操介绍详见官方文档Feature importances with forests of trees。

http://link.zhihu.com/?target=http%3A//scikit-learn.org/stable/auto_examples/ensemble/plot_forest_importances.html

names = loans_ml_df[col_new].columns
from sklearn.ensemble import RandomForestClassifier
clf=RandomForestClassifier(n_estimators=10,random_state=123)#构建分类随机森林分类器
clf.fit(x_val[col_new], y_val) #对自变量和因变量进行拟合
names, clf.feature_importances_
for feature in zip(names, clf.feature_importances_):
    print(feature)

微信图片_20171012144325.jpg

微信图片_20171012144325.jpg

plt.style.use('fivethirtyeight')
plt.rcParams['figure.figsize'] = (12,6)

## feature importances 可视化##
importances = clf.feature_importances_
feat_names = names
indices = np.argsort(importances)[::-1]
fig = plt.figure(figsize=(20,6))
plt.title("Feature importances by RandomTreeClassifier")
plt.bar(range(len(indices)), importances[indices], color='lightblue',  align="center")
plt.step(range(len(indices)), np.cumsum(importances[indices]), where='mid', label='Cumulative')
plt.xticks(range(len(indices)), feat_names[indices], rotation='vertical',fontsize=14)
plt.xlim([-1, len(indices)])
plt.show()

微信图片_20171012144457.jpg

微信图片_20171012144457.jpg

上图是根据特征在特征子集中的相对重要性绘制的排序图,这些特征经过特征缩放后,其特征重要性的和为1.0。

由上图我们可以得出的结论:基于决策树的计算,特征子集上最具判别效果的特征是“loan_amnt”。贷款金额约高,意味着借款人固定流出的现金也越多,随着借款人偿还债务压力增大,贷款违约的风险也随之增加。

5 模型训练

● 处理样本不平衡

前面提到,目标变量“loans_status”正常和违约两种类别的数量差别较大,会对模型学习造成困扰。举例来说,假如有100个样本,其中只有1个是贷款违约样本,其余99个全为贷款正常样本,那么学习器只要制定一个简单的方法:所有样本均判别为正常样本,就能轻松达到99%的准确率。而这个分类器的决策对我们的风险控制毫无意义。因此,在将数据代入模型训练之前,我们必须先解决样本不平衡的问题。

非平衡样本常用的解决方式有2种:1、过采样(oversampling),增加正样本使得正、负样本数目接近,然后再进行学习。2、欠采样(undersampling),去除一些负样本使得正、负样本数目接近,然后再进行学习。

本次处理样本不平衡采用的方法是过采样,具体操作使用SMOTE(Synthetic Minority Oversampling Technique),SMOET的基本原理是:采样最邻近算法,计算出每个少数类样本的K个近邻,从K个近邻中随机挑选N个样本进行随机线性插值,构造新的少数样本,同时将新样本与原数据合成,产生新的训练集。更详细说明参考CMU关于SMOTE: Synthetic Minority Over-sampling Technique的介绍。

http://link.zhihu.com/?target=http%3A//www.cs.cmu.edu/afs/cs/project/jair/pub/volume16/chawla02a-html/chawla2002.html

# 构建自变量和因变量
X = loans_ml_df[col_new]
y = loans_ml_df["loan_status"]

n_sample = y.shape[0]
n_pos_sample = y[y == 0].shape[0]
n_neg_sample = y[y == 1].shape[0]
print('样本个数:{}; 正样本占{:.2%}; 负样本占{:.2%}'.format(n_sample,
                                                   n_pos_sample / n_sample,
                                                   n_neg_sample / n_sample))
print('特征维数:', X.shape[1])

微信图片_20171012144624.jpg

微信图片_20171012144624.jpg

from imblearn.over_sampling import SMOTE # 导入SMOTE算法模块
# 处理不平衡数据
sm = SMOTE(random_state=42)    # 处理过采样的方法
X, y = sm.fit_sample(X, y)
print('通过SMOTE方法平衡正负样本后')
n_sample = y.shape[0]
n_pos_sample = y[y == 0].shape[0]
n_neg_sample = y[y == 1].shape[0]
print('样本个数:{}; 正样本占{:.2%}; 负样本占{:.2%}'.format(n_sample,
                                                   n_pos_sample / n_sample,
                                                   n_neg_sample / n_sample))

微信图片_20171012144705.jpg

微信图片_20171012144705.jpg

● 构建分类器进行训练

初始化分类器。

from sklearn.linear_model import LogisticRegression
clf1 = LogisticRegression() # 构建逻辑回归分类器
clf1.fit(X, y)

微信图片_20171012145519.jpg

微信图片_20171012145519.jpg

predicted1 = clf.predict(X) # 通过分类器产生预测结果

查看预则结果的准确率。

from sklearn.metrics import accuracy_score
print("Test set accuracy score: {:.5f}".format(accuracy_score(predicted1, y,)))

微信图片_20171012145649.jpg

微信图片_20171012145649.jpg

借助混淆矩阵进一步比较。

from sklearn.metrics import confusion_matrix
m = confusion_matrix(y, predicted1)
m

微信图片_20171012145725.jpg

微信图片_20171012145725.jpg

上面是混淆矩阵对分类器产生不同类型的正误数量的统计。为了更加直观,我们对混淆矩阵进行可视化。

plt.figure(figsize=(5,3))
sns.heatmap(m) # 混淆矩阵可视化

微信图片_20171012145809.jpg

微信图片_20171012145809.jpg

热图颜色越浅代表数量越多,从上图可以看出真阳性的数量最多,而假阳性的数量最少。根据混淆矩阵,我们可以分别计算precision、recall、f1-score的值,这里我们采用sklearn.metrics子模块classification_report快速查看混淆矩阵precision、recall、f1-score的计算值。

from sklearn.metrics import classification_report
print(classification_report(y, predicted1))

微信图片_20171012145837.jpg

微信图片_20171012145837.jpg

from sklearn.metrics import roc_auc_score
roc_auc1 = roc_auc_score(y, predicted1)
print("Area under the ROC curve : %f" % roc_auc1)

微信图片_20171012150009.jpg

微信图片_20171012150009.jpg

以上只是一个baseline模型,接下来我们继续优化模型。

6 模型评估与优化

在上一个步骤中,我们的模型训练和测试都在同一个数据集上进行,这会产生2个问题:

1、很可能导致学习器把训练样本学得“太好”,把训练样本自身的特点当做所有潜在样本都会具有的一般性质。
2、模型在同一个数据集上进行训练和测试,使得测试集的样本属性提前泄露给模型。

以上2个问题都会导致模型的泛化能力下降,这种现象我们称之为“过拟合”(overfitting)。因此,我们需要将数据集划分为测试集和训练集,让模型在训练集上学习,在测试集上测试模型的判别能力。

通常来说,将数据集划分为训练集和测试集有3种处理方法:
1、留出法(hold-out)
2、交叉验证法(cross-validation)
3、自助法(bootstrapping)

本次项目我们采用交叉验证法划分数据集,将数据划分为3部分:训练集(training set)、验证集(validation set)和测试集(test set)。让模型在训练集进行学习,在验证集上进行参数调优,最后使用测试集数据评估模型的性能。

微信图片_20171012150034.jpg

微信图片_20171012150034.jpg

模型调优我们采用网格搜索调优参数(grid search),通过构建参数候选集合,然后网格搜索会穷举各种参数组合,根据设定评定的评分机制找到最好的那一组设置。

结合cross-validation和grid search,具体操作我们采用scikit learn模块model_selection中的GridSearchCV方法。具体可以参看sklearn官网关于GridSearchCV的介绍。

http://link.zhihu.com/?target=http%3A//scikit-learn.org/stable/modules/generated/sklearn.model_selection.GridSearchCV.html

自己做了个GridSearchCV的流程图:

微信图片_20171012150130.jpg

微信图片_20171012150130.jpg

● cross-validation+grid search

from sklearn.model_selection import GridSearchCV
from sklearn.cross_validation import train_test_split 

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, random_state = 0) # random_state = 0 每次切分的数据都一样
# 构建参数组合
param_grid = {'C': [0.01,0.1, 1, 10, 100, 1000,],
                            'penalty': [ 'l1', 'l2']}

grid_search = GridSearchCV(LogisticRegression(),  param_grid, cv=10) # 确定模型LogisticRegression,和参数组合param_grid ,cv指定5折
grid_search.fit(X_train, y_train) # 使用训练集学习算法

微信图片_20171012151257.jpg

微信图片_20171012151257.jpg

results = pd.DataFrame(cl2.cv_results_)
best = np.argmax(results.mean_test_score.values)

微信图片_20171012151330.jpg

微信图片_20171012151330.jpg

从上面能够直观可出,模型最优参数组合为l2和C=1000。

● 模型性能评估

print("Best parameters: {}".format(grid_search.best_params_))
print("Best cross-validation score: {:.5f}".format(grid_search.best_score_))

微信图片_20171012151421.jpg

微信图片_20171012151421.jpg

results# 查看分析报告

微信图片_20171012151444.jpg

微信图片_20171012151444.jpg

scores = np.array(results.mean_test_score).reshape(2, 6)
heatmap(scores, ylabel='penalty', yticklabels=param_grid['penalty'],
                      xlabel='C', xticklabels=param_grid['C'], cmap="viridis")

微信图片_20171012151501.jpg

微信图片_20171012151501.jpg

上图模型在不同参数组合下跑出的分数热力图,我们可以从热力图明显看出,l2比l1的表现普遍要好,当C小于1时,模型表现较差。同时,我可以利用热力图来寻找参数调优的方向,进一步选择更优的参数。而实际操作中,模型调参是一个反复迭代的过程。

print("Best estimator:\\n{}".format(grid_search.best_estimator_))#grid_search.best_estimator_ 返回模型以及他的所有参数(包含最优参数)

微信图片_20171012151837.jpg

微信图片_20171012151837.jpg

现在,我们使用经过训练和调优的模型在测试集上测试。

y_pred = grid_search.predict(X_test)
print("Test set accuracy score: {:.5f}".format(accuracy_score(y_test, y_pred,)))

微信图片_20171012151903.jpg

微信图片_20171012151903.jpg

print(classification_report(y_test, y_pred))

微信图片_20171012151942.jpg

微信图片_20171012151942.jpg

m2 = confusion_matrix(y_test, y_pred) 
plt.figure(figsize=(5,3))
sns.heatmap(m2) # 混淆矩阵可视化

微信图片_20171012152017.jpg

微信图片_20171012152017.jpg

roc_auc2 = roc_auc_score(y_test, y_pred)
print("Area under the ROC curve : %f" % roc_auc2)

微信图片_20171012152040.jpg

微信图片_20171012152040.jpg

从上面数字可以看出,经过我们对模型进行训练和参数调优后,虽然精确率平均分降低了,但模型的精确率表现更稳定,同时模型的准确率和AUC分数都有很大的提升,其中准确率从0.55762提升至0.62775,升幅达到12.58%,ACU的分数也从0.557623提升至0.627906,升幅达到12.60%!

本文转自微信公众号:Python爱好者社区

如果觉得我的文章对您有用,请点赞。您的支持将鼓励我继续创作!

7

添加新评论2 条评论

xiguaxigua软件开发工程师banker
2019-10-10 08:49
很棒
spring初心spring初心其它华东师范大学
2018-04-29 17:28
您好,请问这个数据怎么获得?能否分享一下
Ctrl+Enter 发表

作者其他文章

相关文章

相关问题

相关资料

X社区推广