贷款自动审批模型

这个案例的背景:金融领域,申请贷款的人很多,可能银行每天要面对很多贷款申请,其中可能有很多不具备贷款资格的申请,为了缓解审批人员的工作量,可以根据申请人的一些资料,做一个模型自动过滤那些不具备贷款资格申请表。

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.image as mpimge
plt.rcParams['font.sans-serif']=['Simhei']
plt.rcParams['axes.unicode_minus']=False

#多数银行的主要评估标准是:房龄+贷款年限≤50年
#借款人年龄为18-65周岁,ps:一般而言,进入社会工作的中青年(25-40周岁)是银行喜欢的群体。
#年龄过大,以及18岁以上的,贷款不通过的可能性非常高。
#评估还款的能力,不见要对个人的收入进行评估,也要对家庭年收入、家庭存款以及其他资产情况进行审查。
#通常月收入和月供之间的关系是:月供不超过月收入的一半。
#工薪族申请贷款条件很简单,年龄25-55周岁 在现单位连续工作满3个月(部分客户需6个月) 近3个月(部分客户需6个月)的月均收入不低于3000元,
#部分城市(深圳、广州、上海、杭州)不低于4000元 ;个人信用记录良好。
#贷款期限与借款人的年龄之和不得超过60周岁。
#国内银行房贷最长能贷款30年
#二手房已使用的年限与贷款年限的和不能超过30年。2、二手房的房龄不能超过15年

替换表格中的列名 

raw_loan=pd.read_csv('/Users/imacpro/Desktop/kaggle/loan/train_u6lujuX_CVtuZ9i.csv')
raw_loan.columns = ['贷款编号','性别','婚姻状况','子女数','教育','是否自雇','收入' ,'联合贷款人收入',
                   '贷款数','贷款期限' ,'信用状况','产权所在地区','贷款状态']

统计属性中的缺失值 

loan = raw_loan.drop('贷款编号',axis=1)
print('属性中那些有缺失值\n\n',raw_loan.isnull().sum(axis=0)[raw_loan.isnull().sum(axis=0) != 0])

然后开始处理缺失值,其中对性别,子女数,是否自雇,贷款期限,信用状况信息用众数进行填充。对贷款数用中位数进行填充,

婚姻状况一栏,填充考虑收入状况做辅助。

marriage_income=loan.groupby('婚姻状况')['收入'].median()

婚姻状况
No     3750.0
Yes    3854.5
Name: 收入, dtype: float64

所以,设定收入超过3854.5的为已婚,以下为未婚。

loan['婚姻状况'] = loan['婚姻状况'].fillna(loan[loan['婚姻状况'].isnull()]['收入'].
                                       map(lambda x: 'Yes' if float(x) > 3854.5 else 'No'))

整体代码显示如下

def handle_na(loan):
    "此函数主要用于处理缺失值"
    #数据中有些特征有缺失值先处理缺失值
    #对于性别缺失值,就取贷款中频率出现最多的性别作为替代值
    loan['性别'] = loan['性别'].fillna(pd.value_counts(loan['性别']).argmax())
    #对于婚姻状况的缺失值,我考虑是看有没有子女和收入情况,统计下结了婚的人收入大体情况,如果没有的话,就填贷款申请中婚姻出现频率最高的值
    #收入偏斜分布,考虑中位数作为结婚与否作为其平均收入
    marriage_income = loan.groupby('婚姻状况')['收入'].median()
    loan['婚姻状况'] = loan['婚姻状况'].fillna(loan[loan['婚姻状况'].isnull()]['收入'].
                                       map(lambda x: 'Yes' if float(x) > 3854.5 else 'No'))
    #子女数缺省值也用众数替代
    loan['子女数'] = loan['子女数'].fillna(loan['子女数'].mode()[0])
    #是否自雇缺失值处理,也用众数替代
    loan['是否自雇'] = loan['是否自雇'].fillna(loan['是否自雇'].mode()[0])
    #对于贷款数缺失数据,观察期贷款分布图,用中位数替代
    loan['贷款数'] = loan['贷款数'].fillna(loan['贷款数'].median())
    #贷款期限缺失值,也用众数替代
    loan['贷款期限'] = loan['贷款期限'].fillna(loan['贷款期限'].mode()[0])
    #信用缺失值也用众数替代
    loan['信用状况'] = loan['信用状况'].fillna(loan['信用状况'].mode()[0])
    return loan

接下来利用函数创建新的特征,首先来构建收入总额的特征,然后利用特征来构建收入是否大于两倍月供。同时建立月供比特征,月供比越高,代表还款能力越高。代码如下

def create_feature(loan,r=0.6):
    '''此函数用来构建新特征,r为联合贷款人的折扣因子'''
    #先构建一个收入总额的特征,为本人收入 + 联合贷款人收入 * 折扣因子r
    loan['收入总额'] = loan['收入'] + loan['联合贷款人收入'] * r
    #美国目前30年房贷固定利率为3.41%;15年房贷固定利率为2.74%;5年期房贷浮动利率为2.68%(此数据为国外数据)
    #等额还款公式:[贷款本金×月利率×(1+月利率)^还款月数]÷[(1+月利率)^还款月数-1]
    loan_rate = pd.cut(loan['贷款期限'],bins=[0,60,180,480],labels=[0.0268,0.0274,0.0341]).astype(np.float32) / 12#月利率
    month_repay = (loan['贷款数'] * 1000 *  loan_rate *((1+loan_rate)**loan['贷款期限'])) / ((1+loan_rate)**loan['贷款期限']-1)#每月还款数
    #构建一个新的特征,是否月收入大于2倍月还款
    loan['收入大于2倍月供'] = (loan['收入总额'] > 2 * month_repay).astype(np.float32)
    #比例越高,还贷能力越高
    loan['收入月供比'] = loan['收入总额'] / month_repay
    return loan 

接下来剔除异常值。对月供比小于两个月获得审批的,和月供比高于10个月未获得审批的数据进行剔除。

def handle_outlier(loan,t=10):
    '''此函数用来处理异常值,t参数用来控制异常值'''
    #找出数据中的异常值
    outlier_index1 = loan[loan['收入月供比'] <= 2]\
    [loan[loan['收入月供比'] <= 2]['贷款状态']=='Y'].index#收入都小于2倍月供的,但是被批准的可以视为异常数据(银行业务标准)
    #再引入一个系数t,如果收入月供比远大于这个系数,但是贷款没有通过,可以视为异常
    outlier_index2 = loan[loan['收入月供比'] >t][loan[loan['收入月供比'] > t]['贷款状态']=='N'].index
    total_outlier = np.hstack([outlier_index1,outlier_index2])
    loan = loan.drop(total_outlier,axis=0)#剔除异常值
    return loan

对数据进行规范化处理,其中子女数和贷款状态采用标签编码,其他类别采用独热编码。

def data_std(loan):
'''函数功能对数据规范化处理'''
    from sklearn.preprocessing import StandardScaler,LabelEncoder
    loan[['贷款数','收入总额']] = np.log(loan[['贷款数','收入总额']]+1)
    #子女数,贷款状态(标签值)这条属性做标签编码,其他类别数据做独热编码,因为子女数有1,2,3还以有差别的,其他数据值是没有大小可比性的
    le = LabelEncoder()
    loan['子女数'] = le.fit_transform(loan['子女数'])
    loan['贷款状态'] = le.fit_transform(loan['贷款状态'])
    loan = pd.get_dummies(loan)
    return loan
def log_convert_display(loan,log):
    '''显示数据对数变换前后的分布图,log参数表示数据是否做了对数变换'''
    #统计下有些数据的分布情况
    loan = handle_na(loan)
    loan = create_feature(loan,r=0.4)
    if log:
        loan = handle_outlier(loan,t=30)
        loan = data_std(loan)
        foo = loan[['贷款数','收入总额']]
        foo.hist(bins=50,figsize=(10,3),xlabelsize=10)
        plt.suptitle('对数变换后数据分布图',fontsize=15,y=1.1) 
    else:
        foo = loan[['贷款数','收入总额']]
        foo.hist(bins=50,figsize=(10,3),xlabelsize=10)
        plt.suptitle('对数变换前数据分布图',fontsize=15,y=1.1) 
    plt.show()

这里采用对数交换的方法,使得整体数据更为集中。

扫描二维码关注公众号,回复: 6731951 查看本文章

下面开始训练模型

from sklearn.model_selection import train_test_split
from sklearn.ensemble import RandomForestClassifier
from sklearn.svm import SVC
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import f1_score
from sklearn.preprocessing import StandardScaler
from xgboost import XGBClassifier

def main(loan):
    while True:
        print('请选择要使用算法模型,可以选择的算法:\n\
        rfc: 随机森林,svc:支持向量机,lr:逻辑斯蒂回归,xgb:xgboost')
        choose =  input('请选择:')
        if choose not in ['rfc','svc','lr','xgb']:
            continue
        break
    raw_loan = pd.read_csv('/Users/imacpro/Desktop/kaggle/loan/train_u6lujuX_CVtuZ9i.csv')#读入数据
    raw_loan.columns = ['贷款编号','性别','婚姻状况','子女数','教育','是否自雇','收入' ,'联合贷款人收入',
           '贷款数','贷款期限' ,'信用状况','产权所在地区','贷款状态']
    loan = raw_loan.drop('贷款编号',axis=1)
    loan = handle_na(loan)#处理缺失值
    loan = create_feature(loan,r=0.3)#构建特征
    loan = handle_outlier(loan,t=30)#处理异常值
    loan = data_std(loan)#数据规范化
    y = loan['贷款状态']
    x = loan.drop(['贷款状态','收入月供比'],axis=1)
    x_train,x_test,y_train,y_test = train_test_split(x,y,test_size=0.3,stratify=y,random_state=33)#在类分布不平衡是用到
    sc = StandardScaler()
    x_train_std = sc.fit_transform(x_train)
    x_test_std = sc.transform(x_test)
    #多个算法模型构建
    rfc = RandomForestClassifier(random_state=33)
    svc = SVC(random_state=33)
    lr = LogisticRegression(random_state=33)
    xgb = XGBClassifier(random_state=33)
    clf_dict = {'rfc':rfc,'svc':svc,'lr':lr,'xgb':xgb}
    clf = clf_dict[choose]
    clf.fit(x_train_std,y_train)
    test_score = clf.score(x_test_std,y_test)
    train_score = clf.score(x_train_std,y_train)
    y_pred_test = clf.predict(x_test_std)
    y_pred_train = clf.predict(x_train_std)
    test_f1 = f1_score(y_test,y_pred_test)
    train_f1 = f1_score(y_train,y_pred_train)
    print('{}=====>:测试/训练f1分数为:{:.3f} / {:.3f}'.format(choose.upper(),test_f1,train_f1))
    print('{}=====>:测试/训练准确度为:{:.3f} / {:.3f}'.format(choose.upper(),test_score,train_score))
    return (x_train_std,y_train,x_test_std,y_test)

之后对模型进行调参,采用网格法。

param_grid = {'C':[0.2,0.3,0.4],'kernel':['rbf'],'gamma':['auto',0.02,0.03,0.01]}
svc  = SVC(random_state=33)
gs = GridSearchCV(svc,cv=5,param_grid=param_grid)
gs.fit(x_train_std,y_train)
from sklearn.grid_search import GridSearchCV

随机森林的确存在很高的过拟合现象,逐步调参之后会稳定在0.85左右。

猜你喜欢

转载自blog.csdn.net/weixin_42307828/article/details/86656418