数据分析案例(四)——评分卡模型(四)

  • 拖了好久忘记了我的评分卡模型的拟写。这一次稍微好好写一下。本文章主要是写一下评分卡建模的主要流程

一、建模思路

信用评分卡.jpg

二、数据集介绍

givemesomecredit --Kaggle数据集
数据来自Kaggle的Give Me Some Credit,有15万条的样本数据,大致情况如下:
数据属于个人消费类贷款,只考虑信用评分最终实施时能够使用到的数据应从如下一些方面获取数据:
基本属性:包括了借款人当时的年龄。
偿债能力:包括了借款人的月收入、负债比率。
信用往来:两年内35-59天逾期次数、两年内60-89天逾期次数、两年内90天或高于90天逾期的次数。
财产状况:包括了开放式信贷和贷款数量、不动产贷款或额度数量。
贷款属性:暂无。
其他因素:包括了借款人的家属数量(不包括本人在内)。
时间窗口:自变量的观察窗口为过去两年,因变量表现窗口为未来两年。

三、具体步骤与代码

3.1 数据描述

df.rename(columns = {'SeriousDlqin2yrs':'y'},inplace = True)
df.drop(columns = 'Unnamed: 0',inplace = True) #因为id没有什么意义,下面还要直接去重
df.info()
结果.png
  • 发现有缺失值:MonthlyIncome、NumberofDependents
(df['y'].value_counts()[1])/len(df)

0.06684

  • 说明样本非常不平衡。如果两类样本的比例在1:5以上,则无需做样本不平衡处理。这里需要做不平衡处理。

其他的数据描述,例如绘制条形图、相关性检查就不再这里写出来了。

3.2 数据清洗

3.2.1 去重、缺失值处理

# 去重
dfana = df.copy()
dfana.drop_duplicates(inplace = True)
dfana.reset_index(inplace = True)
# 缺失值补充
dfana.isnull().mean()
结果2.png
  • 查看缺失值比例
  • 考虑到后面要进行分箱,如果缺失比例>5%,且好样本率非极端,不做缺失值填补,直接变成一个分箱;
  • 缺失值比例<=5%,或者好坏样本率极端(全好全坏样本),选择随机填补。
dfana[dfana['MonthlyIncome'].isnull()].y.value_counts()
image.png
  • 发现MonthlyIncome非极端,则无需做缺失值填补
# 对NumberOfDependents进行缺失值填补
dfana['NumberOfDependents']=Fillna(dfana['NumberOfDependents'],repval ='random')
  • 对NumberOfDependents进行缺失值进行随机填补。这里的Fillna函数自行定义。思路:可以用np.random.choice(array,n,replace = True),注意不要随机抽到缺失值的了

3.2.2 异常值处理

  • 对于异常值处理,首先要根据业务,绘制box箱线图发现异常变量,进行剔除或者代替。这里具体的发现方法不做描述。
dfana = dfana[dfana.age > 0]
dfana = dfana[(dfana['NumberOfTime30-59DaysPastDueNotWorse'] < 90)]
dfana.loc[(dfana.RevolvingUtilizationOfUnsecuredLines > 30),'RevolvingUtilizationOfUnsecuredLines'] = 0.5
dfana.loc[(dfana.DebtRatio > 2),'DebtRatio'] = 0.5
  • 通过探索发现处理了age、NumberOfTime30-59DaysPastDueNotWorse、RevolvingUtilizationOfUnsecuredLines、DebtRatio这几个变量的异常值。见仁见智。

3.3 不平衡样本处理

注意:不平衡样本处理应该在分箱之前,因为分箱之后某些信息会缺失,所以按照badrate来平衡样本。

  • 方法有很多,这里用的是简单的下采样的方法。
  • 无需抽样成1:1,可以抽样成1:5,所以这里从正样本汇总抽样5倍的坏样本数即可。
# 采用分层抽样,1:5的比例进行下采样
G_train = dfana[dfana.y == 0]
B_train = dfana[dfana.y == 1]
### 对好样本进行抽样,抽样个数选择坏样本个数的5倍
G_train_sample = G_train.sample(n=B_train.shape[0] * 5, frac=None, replace=False, weights=None, random_state=101, axis=0)
dfana_t = []
dfana_t = pd.concat([G_train_sample,B_train])
dfana_t.reset_index(inplace = True,drop = True)
dfana_t.drop(columns = 'index',inplace = True)

3.4 分箱处理

  • 用最小卡方法(具体的方法查看评分卡模型(二)

    具体的代码这里不写出来了。主要的点:
  1. 生成交叉表,计算badrate:
    dftbl = pd.crosstab(dfana[colname], dfana[target])
    dftbl.reset_index(inplace = True)
    dftbl['badrate'] = dftbl[1] / (dftbl[0] + dftbl[1])
  1. 需要提前处理badrate极端的组别,badrate = 0 or 1,这部分提前与上下组别合并。
  2. 最好定义一个dataframe记录需要转变前的colname,与需要转变后的colname
# 初始化的转换表格就是自己本身
dfres = pd.DataFrame({colname : dftbl[colname], 'trans' : dftbl[colname]})
# 找到需要转换的变量特征,利用dfres将原来的table转变一下
dfres.loc[i, 'trans'] = dfres.loc[i - 1, 'trans']
dftbl['anacol'] =  dfres.trans
# 转变了之后利用groupby再次合并一次
dftbl = dftbl.groupby('anacol', as_index = False, 
                              observed = True).agg('sum')
  1. 计算相邻两个单元格的卡方值
for i in range(N_levels - 1): 
        dftbl.loc[i, 'chi2'] = ss.chi2_contingency(dftbl.loc[i : i + 1, [0, 1]])[0] 
  1. 找到最小的卡方值,进行向上或者向下合并,合并了之后还需要重新计算其卡方值
dftbl.loc[minindex, 'chi2'] = ss.chi2_contingency(dftbl.loc[minindex : minindex + 1,
                                                                    [0, 1]])[0]

3.5 WOE\IV值

新增一个计算WOE和IV值的函数,计算分箱之后的woe值,和整体变量的iv值,然后进行反复筛选。
主要代码:

    dftbl = pd.crosstab(colname.fillna('NA'), target, normalize = 'columns') #normalize是计算target中的各项频率!数量/列的总和
    # 也就是goodpct,badpct
    dftbl.columns = ['goodpct', 'badpct']
    dftbl['WOE'] = np.log(dftbl.goodpct / dftbl.badpct) * 100
    IV = sum((dftbl.goodpct - dftbl.badpct) * dftbl.WOE) / 100
  • 经过多次尝试,需要把分箱尽量单调,同时合并只能合并最近的箱体(已经排序过了)


    image.png

    image.png
image.png
image.png
image.png
image.png
image.png
image.png
  • 某些特殊的分箱不满足单调性,只需要能在业务上解释就可以了
image.png
image.png

3.6 切分训练集和测试集

  • 在所有数值变换之后,就可以切分数据集和测试集了,一定要保证训练集和测试集的变换是一样的
from sklearn.model_selection import train_test_split
train_df, test_df = train_test_split(dfana_fit,test_size = 0.3)
train_df.shape, test_df.shape

3.7 特征选择(选用)

  • 这里选用的是逐步回归法,但是数据集本身的特征就不是很多,所以这里可以不需要用这个方法,同时IV值都已经满足了要求了。
  • 逐步回归法就是例如有6个特征,一个model
    f1 + model
    f2 + model
    ...
    f6 + model
    选取其中评判标注最好的一个特征加入模型,例如是f2
    selected = f2
    然后
    f1 +model+selected
    f3 + model + selected
    ...
    依次类推
image.png
  • 发现剔除了'NumberOfOpenCreditLinesAndLoans_WOE
  • 下面计算VIF值,这个值是判断多重共线的,一般大于10就是有多重共线性了,需要剔除变量
  • 我们发现所有的系数都为负,这个是因为我们计算WOE的值的时候是goodrate / badrate,所以与y关系是负相关,同时我们发现所有的系数都是一个方向,这样的模型才效果更好。
from statsmodels.stats.outliers_influence import variance_inflation_factor
colnames = list(model.params.index)
colnames.remove('Intercept')
train_X_M = np.matrix(train_df[colnames])

VIF_list = [variance_inflation_factor(train_X_M, i) for i in range(train_X_M.shape[1])]
VIF_list

[1.345451226908903,
1.2336645504356645,
1.203776578215364,
1.2737738281637017,
1.1334798511412756,
1.0174816425613178,
1.0462200835734954,
1.0972902825775086,
1.0547503741366757]

  • 结果发现都在小于2,说明没有共线性,无需剔除变量

3.8 评价模型

  • 计算AUC
from sklearn.metrics import auc,roc_curve, roc_auc_score
### 计算AUC值
print('训练集的auc为:{}'.format(roc_auc_score(train_df.y,train_predict)))
print('测试集的auc为:{}'.format(roc_auc_score(test_df.y, predictions)))

训练集的auc为:0.8320949945565318
测试集的auc为:0.8307774122803272

  • 说明模型没有过拟合,在训练集和测试集的auc都不错

3.9 转换成分值

利用公式进行求解:评分卡模型(三)
按照公式求出Factor和offset,当然自己要规定PDO(odds比增加一倍时候要增加的分数),β是建模的系数。

    factor = pdo/np.log(2)
    offset = basescore - factor * np.log(baseodds)
    dfres['cardscore'] = round(offset / n - factor * (para.Intercept / n + dfres.beta * dfres.WOE))
  • 注意,这里先把每一个箱体的分数计算出来,再计算每一个id的总分数,总分数就是每一个箱体相加
分数特征.png
  • 最后计算的分数特征是,577为分数均值,最高分为690,最低分为92.

3.10 KS指标

  • 横坐标是分数,纵坐标是坏人/好人的累计占比


    KS指标.png
  • 最后绘制两条曲线,KS为两条曲线的差值。发现最大的差值是在572分的时候KS为51分左右,说明在572的时候好坏的占比大约占一半。
  • KS值一般大于40,就是可以用的模型。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342