- 拖了好久忘记了我的评分卡模型的拟写。这一次稍微好好写一下。本文章主要是写一下评分卡建模的主要流程
一、建模思路
二、数据集介绍
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()
- 发现有缺失值: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()
- 查看缺失值比例
- 考虑到后面要进行分箱,如果缺失比例>5%,且好样本率非极端,不做缺失值填补,直接变成一个分箱;
- 缺失值比例<=5%,或者好坏样本率极端(全好全坏样本),选择随机填补。
dfana[dfana['MonthlyIncome'].isnull()].y.value_counts()
- 发现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 分箱处理
- 用最小卡方法(具体的方法查看评分卡模型(二)
)
具体的代码这里不写出来了。主要的点:
- 生成交叉表,计算badrate:
dftbl = pd.crosstab(dfana[colname], dfana[target])
dftbl.reset_index(inplace = True)
dftbl['badrate'] = dftbl[1] / (dftbl[0] + dftbl[1])
- 需要提前处理badrate极端的组别,badrate = 0 or 1,这部分提前与上下组别合并。
- 最好定义一个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')
- 计算相邻两个单元格的卡方值
for i in range(N_levels - 1):
dftbl.loc[i, 'chi2'] = ss.chi2_contingency(dftbl.loc[i : i + 1, [0, 1]])[0]
- 找到最小的卡方值,进行向上或者向下合并,合并了之后还需要重新计算其卡方值
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
-
经过多次尝试,需要把分箱尽量单调,同时合并只能合并最近的箱体(已经排序过了)
- 某些特殊的分箱不满足单调性,只需要能在业务上解释就可以了
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
...
依次类推
- 发现剔除了'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的总分数,总分数就是每一个箱体相加
- 最后计算的分数特征是,577为分数均值,最高分为690,最低分为92.
3.10 KS指标
-
横坐标是分数,纵坐标是坏人/好人的累计占比
- 最后绘制两条曲线,KS为两条曲线的差值。发现最大的差值是在572分的时候KS为51分左右,说明在572的时候好坏的占比大约占一半。
- KS值一般大于40,就是可以用的模型。