暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

LR评分卡建模流程实操——Kaggle项目GiveMeSomeCredit实战

数据风控学习笔记 2022-08-31
1183

 

LR评分卡建模流程实操

Kaggle项目GiveMeSomeCredit实战

  逮噶猴,在第一次实战(详见推送:Xgboost简单建模——Kaggle项目GiveMeSomeCredit实战)中,我们初步得到了一个Xgboost模型,并且提到将在后续更新中进行风控评分卡建模实操,因此从本次更新开始,我们就来一步一步完成这个小目标。

     在数据竞赛中,尤其对于表格型的数据集,我们常采用xgboost,或者LightGBM,catboost等模型进行训练,而在实际的风控场景中,尤其是对于银行等金融机构来说,逻辑回归即LR模型由于可解释性强、特征权重直观等原因,仍然是实际工作中的主流模型。由于评分卡模型涉及要素很多,今天,我们就先来对LR评分卡模型的整体建模流程作一个全面的了解,并进行简单的代码实操。后续,我们再对流程中的每个环节进行详细的拆解~

数据源:give me some credit数据集

数据源介绍:

来源于kaggle官网,主要为个人在银行的基本信息及信贷信息。

数据源地址:https://www.kaggle.com/c/GiveMeSomeCredit/data

注:也可通过在公众号后台回复“GiveMeSomeCredit数据集”获取数据

建模目标:通过某客户在过去的个人信息及信贷信息信贷信息预测该客户在未来两年经理财务困境的概率,为银行的授信决策提供依据。

评价标准:AUC

关注重点:LR评分卡建模流程实操

01

环境配置

    # 基本功能
    import pandas as pd
    import numpy as np


    # 画图
    import matplotlib
    import matplotlib.pyplot as plt
    import seaborn as sns


    # 数据处理
    from sklearn.model_selection import train_test_split
    from sklearn.model_selection import KFold, cross_validate
    from sklearn.preprocessing import OneHotEncoder
    import imblearn


    # 模型算法
    import statsmodels.api as sm
    from sklearn.linear_model import LogisticRegression as LR


    # 模型评价
    from sklearn.metrics import roc_curve
    from sklearn.metrics import auc,roc_auc_score
    from sklearn.metrics import accuracy_score,confusion_matrix,f1_score
    from sklearn.model_selection import cross_val_score


    # 风控
    import toad
    from toad.plot import  bin_plot,badrate_plot


    # pd显示配置
    pd.set_option('precision',3)
    pd.set_option('display.float_format',lambda x:'{:.4f}'.format(x))


    # sns风格与装饰
    sns.set(style='whitegrid',font='Simhei')


    # 系统路径,定义为数据集所在地址
    import os
    os.chdir(r'C:\Users\28779\Desktop\GiveMeSomeCredit')


    # 输出每个cell代码返回的所有结果
    from IPython.core.interactiveshell import InteractiveShell
    InteractiveShell.ast_node_interactivity = 'all'


    # 屏蔽提示
    import warnings
    warnings.filterwarnings('ignore')


    02

    读取数据

      # 训练数据示例
      data = pd.read_csv('cs-training.csv')

      数据字典信息如下:

        # 查看尾部两行
        data.tail(2)
        # 查看&重命名列
        data.columns
        data.drop(columns='Unnamed: 0',inplace=True)
        column = ['target', 'RevUtilOfUnsecLines', 'age',
              '30-59DPD', 'DebtRatio', 'MonthlyIncome',
              'NumOfOpCreAndLoans', 'NumOf90DPD',
              'NumReaEstLoansOrLines', '60-89DPD',
              'NumberOfDependents']
        data.columns = column
        data.head()

          # 区分特征与标签
          fea = ['RevUtilOfUnsecLines''age',
                '30-59DPD', 'DebtRatio', 'MonthlyIncome',
                'NumOfOpCreAndLoans', 'NumOf90DPD',
                'NumReaEstLoansOrLines', '60-89DPD',
                'NumberOfDependents']
          label = 'target'
          ex_lis = []
          03

          数据查看


            # 样本概览
            data.info()
            data.shape
            RangeIndex: 150000 entries, 0 to 149999
            Data columns (total 11 columns):
            #   Column                 Non-Null Count   Dtype  
            ---  ------                 --------------   -----  
            0   target                 150000 non-null  int64  
            1   RevUtilOfUnsecLines    150000 non-null  float64
            2   age                    150000 non-null  int64  
            3   30-59DPD               150000 non-null  int64  
            4   DebtRatio              150000 non-null  float64
            5   MonthlyIncome          120269 non-null  float64
            6   NumOfOpCreAndLoans     150000 non-null  int64  
            7   NumOf90DPD             150000 non-null  int64  
            8   NumReaEstLoansOrLines  150000 non-null  int64  
            9   60-89DPD               150000 non-null  int64  
            10  NumberOfDependents     146076 non-null  float64
            dtypes: float64(4), int64(7)
            memory usage: 12.6 MB
            可以看到,样本数量为150000,特征数为10个,特征值均为数值型,标签值无缺失,数据大小是12.6MB,不用压缩可以直接拿来分析
              # 标签分析
              data.target.value_counts()
              print('坏样本浓度为{:.4f}%'.format(100*data.target.mean()))
              0    139974
              1 10026
              Name: target, dtype: int64
              坏样本浓度为6.6840%
                # 特征分析
                data.describe()

                查看特征的非空数量、均值、方差、极大值、极小值和四分位值

                04

                数据预处理


                1.重复值处理

                  # 查看数据重复情况
                  data.duplicated().sum()
                  # 查看重复数据明细
                  data[data.duplicated()]
                  # 查看重复值中坏样本数量
                  data[data.duplicated()].target.sum()
                  609
                  17
                  通过明细和重复值数量可以看出,拥有重复特征值的数据并没有表现出异常,可能是自然存在的,因此这里不做处理

                  2.缺失值处理
                  之前的《Xgboost简单建模——Kaggle项目GiveMeSomeCredit实战一文中,我们对缺失值进行了查看,但并没有做处理,这是由于Xgboost中自带缺失值处理函数,我们将带有缺失值的数据直接扔进模型,模型也可以支持处理。而LR模型是不支持缺失值自动处理的,所以一定要记得先对缺失值做好处理再把数据丢进模型,不然模型训练时就会报错喔~
                    # 查看数据缺失情况
                    missing_num = data.isnull().sum()
                    missing_rate = data.isnull().sum()/(data.isnull().count())
                    df_missing = pd.DataFrame({'num':missing_num,'rate':missing_rate})
                    df_missing.rate = df_missing.rate.apply(lambda x :'{:.4f}%'.format(100*x))
                    df_missing

                    图表显示,有两个字段存在缺失,分别是MonthlyIncome和

                    NumberOfDependents,其中NumberOfDependents字段的缺失率比较低,我们可以直接丢掉,MonthlyIncome的缺失值接近20%,我们对缺失值进行填补,此处采用均值填补法,缺失值的处理其实方法有很多,先放一个坑在这里,我们在后续推送中再单独进行详细说明~
                      # 丢弃NumberOfDependents字段值为空的数据
                      data.dropna(subset=['NumberOfDependents'],inplace=True)
                      # 使用均值填补法对MonthlyIncome字段的缺失值进行填补
                      data['MonthlyIncome'].fillna(data['MonthlyIncome'].mean(),inplace=True)

                      3.异常值处理
                        # 查看异常数据情况
                        plt.figure(figsize=(20,15),dpi=100)
                        for i in range(len(fea)):
                           plt.subplot(3,4,i+1)
                           sns.boxplot(y=data[fea[i]],orient='v',width=0.5)
                           plt.title(fea[i])
                        plt.tight_layout()
                        plt.show()

                        对每个特征的分布和极值情况进行观察,结合对字段的理解,我们将三个逾期字段30-59DPD,60-89DPD,和NumOf90DPD中逾期次数超过90的数据去除,同时也去除掉RevUtilOfUnsecLines字段值大于50000的数据

                          data = data[(data['RevUtilOfUnsecLines']<50000)&(data['30-59DPD']<90)]

                          05

                          数据探索性分析


                          在这一步中,我们可以通过画图了解特征总体分布情况,并逐个查看特征的具体情况,也可以尝试对特征进行变形,组合或衍生
                            # 画出数据分布图
                            data.hist(figsize=(20,15))

                            06

                            数据划


                            在进行特征分箱之前,我们需要对数据集进行划分,将80%的数据作为训练集,用于训练模型,剩下的20%作为验证集,用于验证模型效果,实际工作中,其实有时还会设置额外的数据,作为进一步的测试集
                              # 抽取30%的验证集
                              X_train,X_test,y_train,y_test = train_test_split(  
                                 data[fea],data[label],test_size=0.3,random_state=42)
                              data_training = pd.concat([X_train,y_train],axis=1)
                              data_test = pd.concat([X_test,y_test],axis=1
                              07

                              特征分箱


                              特征分箱是评分卡建模的关键步骤之一。不同于Xgboost,LR模型作为广义线性模型,对数据的极端值十分敏感,如果把未分箱的数据直接丢进模型,特征中的一些极端值会使模型跑偏,降低模型的泛化能力,影响模型的预测效果。因此在LR评分卡建模流程中,特征分箱步骤是必不可少的,也就是根据特征值将每个特征划分成不同的箱子,每个箱子对应不同的特征值范围,每个特征值都能找到得到所属的箱子。
                              在这里,我们直接调用toad库中的卡方分箱方法进行分箱,关于分箱的含义,不同分箱方法的比较和分箱函数的实现,我们也将在后续单独进行详细介绍~
                                # 生成分箱实例
                                combiner_chi = toad.transform.Combiner()
                                # 进行卡方分箱
                                combiner_chi.fit(data_training,data_training[label],method='chi',min_samples = 0.05,\
                                empty_separate=True,exclude=label)
                                # 导出分箱节点
                                bins_chi = combiner_chi.export()

                                bins_chi返回一个字典,里面包含了所有特征的切分点信息

                                这里我们看到,NumOf90DPD仅仅只划分出了1箱,NumOf90DPD甚至1箱也没分出,这是由于这两个特征本身的特征值较少,且与与30-59DPD存在一定的相关性导致的。此处,我们重新观察这两个特征的特征值分布,调用toad库的节点调整方法重新对分箱节点进行调整:

                                  # 对上一步得到的节点直接进行调整
                                  adj_bin = {'RevUtilOfUnsecLines': [0.061872817999999996,0.299745148,0.48757002,0.8826939290000001],
                                  'age': [36, 44, 51, 56, 58, 63, 68],
                                  '30-59DPD': [1, 2],
                                  'DebtRatio': [0.016196761, 0.40660066, 0.7264729409999999, 3.9728435277938843],
                                  'MonthlyIncome': [5409.0, 7332.0],
                                  'NumOfOpCreAndLoans': [2.5],
                                   'NumOf90DPD': [1,4],
                                  'NumReaEstLoansOrLines': [1, 3],
                                   'NumOf90DPD': [1,3],
                                  'NumberOfDependents': [1.0, 2.0]
                                  }
                                  combiner_chi.set_rules(adj_bin)

                                  将修改后的节点应用于分箱

                                    # 根据节点实施分箱
                                    data_training_bins_adj = combiner_chi.transform(data_training)
                                    # 验证集同步划分
                                    data_test_bins_adj = combiner_chi.transform(data_test[data_training.columns])

                                    data_training_bins_adjdata_test_bins_adj分别返回一个DataFrame,通过上图结果可以看到其中的特征值已经被箱子编号替代

                                      # 查看各箱标签分布
                                      for col in fea:
                                      bin_plot(data_training_bins_adj,x=col,target=label)

                                      以age特征为例,图中的横坐标代表箱体编号,蓝色柱形对应左侧纵坐标,表示每个箱体中的样本数占总样本数的比例,红色线条对应右侧纵坐标,表示每个箱体中坏样本占此箱体样本数量的比例,左上角代表age特征在这中划分方式下的IV值。
                                      关于WOE和IV值计算的理解可以参考下面这几张图:
                                      WOE的计算公式:
                                      IV的计算公式:
                                      WOE&IV计算实例:
                                      通过WOE转换,我们可以把自变量x与y之间的非线性关系转换为线性关系
                                        # WOE映射处理
                                        # 生成WOETransformer实例
                                        transer = toad.transform.WOETransformer()
                                        # 训练集、验证集WOE同步转换
                                        data_training_woe = transer.fit_transform(data_training_bins_adj, data_training_bins_adj[label], exclude=label)
                                        data_test_woe = transer.transform(data_training_bins_adj)

                                        data_training_bins_chidata_test_bins_chi分别返回一个DataFrame,通过上图结果可以看到,原本的箱体编号已经被箱子对应的WOE值替代
                                        详细的WOE和IV计算表我们也将在后续单独作为一块展开说说~
                                        08

                                        特征筛选


                                        常见的特征筛选方式包括空值率筛选、相关性筛选、方差膨胀系数筛选、SelectFromModel 筛选,以及stepwise递归筛选等,这里主要使用相关性筛选和stepwise筛选进行处理

                                        1.相关性筛选:
                                          # 计算特征相关性
                                          data_training_woe_corr = data_training_woe.corr()
                                          # 设置画布大小
                                          plt.figure(figsize=(10,10),dpi=100)
                                          # 绘制热力图
                                          sns.heatmap(data=data_training_woe_corr,annot=True)

                                          可以看到图中并没有相关性过强的特征,因此本轮筛选无需丢弃特征

                                          2.stepwise筛选:
                                            # 实施双向stepwise筛选
                                            data_training_woe_slct_stp,drop_lst_stp = toad.selection.stepwise(data_training_woe,
                                                                                          data_training_woe[label],
                                            intercept = True,
                                            return_drop=True)
                                            # 查看drop的特征
                                            drop_lst_stp
                                            # 查看剩余特征数据
                                            data_training_woe_slct_stp
                                            # 保持验证集与训练集同步处理
                                            data_test_woe_slct_stp = data_test_woe_slct[data_training_woe_slct_stp.columns]
                                            # 更新特征列表,去除筛除的特征
                                            fea = data_training_woe_slct_stp.columns
                                            ['NumOfOpCreAndLoans']

                                            这一轮筛选掉了NumOfOpCreAndLoans这个特征,其余特征都保留了下来
                                            09

                                            LR建模


                                            数据已经完成了清洗、预处理、分箱、WOE转换和特征筛选,接下来就是建模的环节啦~
                                              # 生成LR模型实例
                                              model_lr  = LR(random_state=2022, solver='saga', penalty='l2', C=1.0, max_iter=500)
                                              # LR模型训练
                                              model_lr.fit(data_training_woe_slct[fea],data_training_woe[label])
                                              # LR模型预测
                                              y_pre_lr = model_lr.predict_proba(data_test_woe_slct[fea])[:,1]
                                              # 查看权重值
                                              model_lr.coef_
                                              array([[0.62382961, 0.47817517, 0.52780345, 0.90017021, 0.04968211,
                                              0.04383911, 0.53562691, 0.51525742, 0.43927112, 0.19261411]])
                                              和之前的xgboost模型一样,还是使用auc指标对预测结果进行评估
                                                # 定义y_test
                                                y_test = data_test_woe[label]
                                                # 计算auc值
                                                roc_auc_score(y_test,y_pre_lr)
                                                0.8528142565736914
                                                可以看到,这个结果相比于xgboost还是略低的,但相差不多
                                                  # 绘制roc曲线
                                                  # 计算fpr_ci,tpr_ci
                                                  fpr,tpr,thresholds=roc_curve(y_test,y_pre_lr,pos_label=None,\
                                                  sample_weight=None,drop_intermediate=True)
                                                  # plot
                                                  plt.figure()
                                                  plt.plot([0,1],[0,1],lw=2,linestyle='--')
                                                  plt.plot(fpr,tpr,label='ROC Curve')
                                                  plt.title('ROC Curve')
                                                  plt.xlabel('False Positive Rate')
                                                  plt.ylabel('True Positive Rate')
                                                  plt.legend()
                                                  plt.show()

                                                  同步计算一下KS值

                                                    max(tpr_ci-fpr_ci)
                                                    0.5503761434091169
                                                    这个结果相比于xgboost同样也是略低,但相差不多
                                                    10

                                                    评分卡映射


                                                    评分卡映射公式:

                                                    Odds = p/(1-p)
                                                    Score = A + B * ln(Odds)

                                                    公式1:这里的p,也就是我们在上一步中通过模型预测出的y_pre_lr值,表示样本为坏(逾期)的概率,1-p相反标示着样本为好(不逾期)的概率,Odds整体表示坏好比
                                                    公式2:为了计算出这里的A和B两个参数值,我们需要首先制订一套评分标准:一个基本分base_score,一个基本得分对应的Odds,以及当Odds变动一个单位时,引起base_score变动的值PDO

                                                    在这里,我们初始设定base_score=600,Odds=1:50,PDO=20,根据公式2:
                                                    600 = A + B * ln(1/50)
                                                    600 - 20 = A + B * ln(2/50)
                                                    通过计算可以得到A和B的参数值
                                                      # 计算参数A、B的值
                                                      A = base_score + PDO * np.log(Odds) np.log(2)
                                                      B = PDO/np.log(2)
                                                      # 定义评分卡映射函数
                                                      def scorecard(p,A,B):
                                                      p = min(p,0.9999)
                                                      odds = p (1 - p)
                                                      score = A - B * np.log(odds)
                                                      score = max(300, score)
                                                      score = min(1000, score)
                                                          return score
                                                      # 评分映射
                                                      df_score = pd.DataFrame()
                                                      df_score['pre'] = y_pre_lr
                                                      df_score['score'] = df_score['pre'].apply(lambda x: scorecard(p=x,A=A,B=B))
                                                      df_score
                                                       

                                                        # 绘制评分分布图
                                                        sns.distplot(df_score['score'])

                                                        更具体地分析这个评分卡,我们可以得到下面这个表格:

                                                        从中可以看到每个特征在各个箱体的阈值,和对应的得分,举个栗子,比如一个样本的年龄为50岁,那么该样本在age项的得分就是2.6833分,如该样本有过2次60-89DPD逾期,那么该样本在60-89DPD项的得分就是25.4469,我们对这个样本在所有特征上得到的分数进行求和,就是这个样本最后的评分卡得分啦~

                                                        在Xgboost简单建模一文中,Xgboost模型表现如下:

                                                        测试集:ks约为0.570,auc约为0.858

                                                        在Xgboost模型调参一文中Xgboost模型表现如下:

                                                        测试集:ks约为0.585,auc约为0.867

                                                        在本文中LR模型表现如下:

                                                        测试集:ks约为0.550,auc约为0.853


                                                        以上,我们建立了简单的LR模型,并且基于模型建立了评分卡,但其实这个过程中还有许多值得进一步探讨的具体细节,例如缺失值的处理、数据的探索、特征筛选、特征分箱、WOE和IV计算、评分变量得分计算等,小记也会持续和大家分享~





                                                        数据风控学习笔记
                                                        微信公众号:datascience_notebook



                                                        文章转载自数据风控学习笔记,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

                                                        评论