基于python的信用评分卡制作

项目介绍:

根据借贷者的统计学特征、社会学特征、消费行为特征、兴趣爱好特征等一系列特征快速判断出一

个的信用情况,并为不同特征赋予不同的权重,整合得到可以供业务人员评判的借贷者信用得分情

况。

项目具体内容:

1、数据预处理,处理缺失值和异常值;使用上采样解决样本不均衡的问题。

2、离散特征分箱处理,获取到每个特征的最大 IV 值。

3、使用逻辑回归算法进行建模;将回归算法转化为银行使用的评分卡模型。


数据特征含义:

'SeriousDlqin2yrs':好坏客户

'RevolvingUtilizationOfUnsecuredLines':可用额度比值

'age':年龄

'NumberOfTime30-59DaysPastDueNotWorse': 逾期30-59天笔数

'DebtRatio':负债率

'MonthlyIncome':月收入

'NumberOfOpenCreditLinesAndLoans':信贷数量

'NumberOfTimes90DaysLate':逾期90天笔数

'NumberRealEstateLoansOrLines':固定资产贷款量

'NumberOfTime60-89DaysPastDueNotWorse':逾期60-89天笔数

'NumberOfDependents':家属数量


一、数据预处理

1、查看数据

查看数据信息,共有150000个样本,11个特征

data= pd.read_csv("cs-training.csv",index_col=0)

print(data.info())

print(data.shape)

2、重复数据删除

去除重复值,有609条是重复数据

print("查看是否有重复值")

print(data[data.duplicated().values==True].count())

data.drop_duplicates(inplace=True)

data.index= range(data.shape[0])# 重置索引

3、填补缺失值

共有两个特征有数据缺失:NumberOfDependents,缺失率为0.025;MonthlyIncome,缺失率为0.195601

print("查看缺失值:\n",data.isnull().mean())

3.1 使用均值填充NumberOfDependents

data["NumberOfDependents"].fillna(data["NumberOfDependents"].mean(),inplace=True)

3.2 使用随机森林填充随机森林填充MonthlyIncome

def fill_missing_rf(X,y,to_fill):

    df= X.copy()

    fill= df.loc[:,to_fill]

    df= pd.concat([df.loc[:,df.columns!=to_fill],pd.DataFrame(y)],axis=1)

    #找到测试集和训练集

    Ytrain= fill[fill.notnull()]

    Ytest= fill[fill.isnull()]

    Xtrain= df.iloc[Ytrain.index,:]

    Xtest= df.iloc[Ytest.index,:]

    # 利用随机森林回归树来填补

    from sklearn.ensembleimport RandomForestRegressoras RFR

    rfr= RFR(n_estimators=100)

    rfr.fit(Xtrain,Ytrain)

    Ypredict= rfr.predict(Xtest)

    return Ypredict

X= data.iloc[:,1:]

y= data["SeriousDlqin2yrs"]

y_pred= fill_missing_rf(X,y,"MonthlyIncome")

data.loc[data.loc[:,"MonthlyIncome"].isnull(),"MonthlyIncome"]= y_pred

4. 异常值处理

使用箱线图查看异常值,年龄为0的样本有1个,申请信用卡的人年龄不可能为0,;违约次数超过80次的为异常。直接删除掉。

if True:

    for iin range(1, data.shape[1]):

        plt.figure()

        plt.boxplot(data.iloc[:,i])

        plt.xlabel(data.columns[i])

        plt.savefig("explore/{}.png".format(data.columns[i]))

data= data[data.loc[:,"age"]>0]

data= data[data.loc[:,"NumberOfTime30-59DaysPastDueNotWorse"]<80]

data= data[data.loc[:,"NumberOfTime60-89DaysPastDueNotWorse"]<80]

data= data[data.loc[:,"NumberOfTimes90DaysLate"]<80]

5、样本不均衡的问题

正样本有104578,负样本有6644,使用上采样方法处理样本不均衡的问题

X= data.iloc[:,1:]

y= data.iloc[:,0]

n_0_sample= y.value_counts()[0]

n_1_sample= y.value_counts()[1]

print("样本分布情况",n_0_sample,n_1_sample)

from imblearn.over_samplingimport SMOTE

sm= SMOTE(random_state=42)#实例化

X,y= sm.fit_sample(X,y)

n_0_sample= y.value_counts()[0]

n_1_sample= y.value_counts()[1]

print("上采样后样本分布情况",n_0_sample,n_1_sample)

print(X.info())

二、数据探索

多变量分析就是对各个变量之间的相关性进行探索,线性回归模型中的特征之间由于存在精确相关关系或高度相关关系而使模型估计失真或难以估计准确,所以需要查看是否存在高度相关的变量,可以看到,各个特征间相关性都不高。

三、数据分箱处理

3.1 特征分箱离散化处理

计算每个特征中箱子的woe值

def get_woe(num_bins):

    """

    通过num_bins获取箱子的woe值"""

    columns= ["min","max","count_0","count_1"]

    df= pd.DataFrame(num_bins,columns=columns)

    df["total"]= df.count_0+ df.count_1# 一个箱子中总的样本数

    df["percentage"]= df.total/ df.total.sum()# 一个箱子中样本数,占总的样本数的比例

    df["bad_rate"]= df.count_1/ df.total# 一个箱子中坏样本数,占一个箱子中的比例

    df["good%"]= df.count_0/ df.count_0.sum()# 一个箱子中好样本,占总的好样本数的比例

    df["bad%"]= df.count_1/ df.count_1.sum()# 一个箱子中坏样本,占总的坏样本数的比例

    df["woe"]= np.log(df["good%"]/ df["bad%"])

return df

计算特征的IV值

def get_iv(bins_df):

    """

    通过箱子获取每个特征的iv值"""

    rate= bins_df["good%"]- bins_df["bad%"]

    iv= np.sum(rate* bins_df.woe)

return iv

自动合并箱子

def combine_bins(numbins_,n):

    """

    自动合并箱子,得到最大的iv值 

    """

    bins_df= None

    IV= []# 存储不同箱子的IV

    axisx= []# 存储箱子个数

    while len(numbins_)> n:

        pvs= []

         # 获取 num_bins_两两之间的卡方检验的置信度(或卡方值)

        for iin range(len(numbins_)- 1):

                x1= numbins_[i][2:]# (正样本数,负样本数) 做卡方检验

                x2= numbins_[i+ 1][2:]# (正样本数,负样本数) 做卡方检验

                # 0返回chi2值,1返回p值

                pv= scipy.stats.chi2_contingency([x1, x2])[1]

                pvs.append(pv)

        # 通过p值进行处理,合并p值最大的两组

        i= pvs.index(max(pvs))# p值越大,越不相关

        numbins_[i:i+ 2]= [(

                numbins_[i][0],

                numbins_[i+ 1][1],

                numbins_[i][2]+ numbins_[i+ 1][2],

                numbins_[i][3]+ numbins_[i+ 1][3])]

        bins_df= get_woe(numbins_)

        IV.append(get_iv(bins_df))

        axisx.append(len(numbins_))

    return bins_df, IV, axisx

通过IV曲线确定最优箱子个数

def graphforbestbin(DF,x,y,n=5,q=20,graph=True):

    """

    自动最优分箱,基于卡方检验的分箱"""

    # 连续性变量离散化

    DF= DF[[x,y]].copy()

    DF["qcut"], updown= pd.qcut(DF[x],retbins=True,q=q,duplicates="drop")#等频分箱函数

    print("{} 原始分箱 qcut:\n".format(x),DF["qcut"])

    print("{} 原始分箱的组距 updown:\n".format(x),updown)

    #统计每个分箱0、1的个数

    coount_y0= DF[DF[y]== 0].groupby(by="qcut").count()[y]

    coount_y1= DF[DF[y]== 1].groupby(by="qcut").count()[y]

    numbins= [*zip(updown,updown[1:],coount_y0,coount_y1)]

    print("{} 原始分箱:\n".format(x),numbins)

    #确保每个箱子中都有至少一个0样本,一个1样本

    #卡方检验、合并箱体、画出IV曲线

    numbins_= numbins.copy()# 复制一份数据

    bins_df, IV, axisx= combine_bins(numbins_,n)

    if graph:

        plt.figure()

        plt.plot(axisx,IV,'mo:')

        plt.xticks(axisx)

    plt.title(x)

    plt.xlabel("number of box")

    plt.ylabel("IV")

    plt.savefig("explore/iv_{}.png".format(x))

return bins_df,IV[0]

自动通过IV曲线得到的特征如下:

auto_col_bins= {

"RevolvingUtilizationOfUnsecuredLines":6,

"age":5,

"DebtRatio":4,

"MonthlyIncome":3,

"NumberOfOpenCreditLinesAndLoans":5

}

for colin auto_col_bins:

    bins_df,iv= graphforbestbin(model_data,col,"SeriousDlqin2yrs",n=auto_col_bins[col],q=20,graph=False)

    bins_list= sorted(set(bins_df["min"]).union(bins_df["max"]))

    bins_list[0],bins_list[-1]= -np.inf, np.inf

    print("{} 分箱上下限,正负无穷替换后:\n".format(col), bins_list)

    #存放自动分箱的结果

    bins_of_col[col]= bins_list

对于无法自动分箱的特征,采用手动分箱

# 手动分箱

hand_bins= {

"NumberOfTime30-59DaysPastDueNotWorse":[0,1,2,13],

"NumberOfTimes90DaysLate":[0,1,2,17],

"NumberRealEstateLoansOrLines":[0,1,2,4,54],

"NumberOfTime60-89DaysPastDueNotWorse":[0,1,2,8],

"NumberOfDependents":[0,1,2,3]

}

hand_bins= {k:[-np.inf,*v[:-1],np.inf]for k,vin hand_bins.items()}

合并自动和手动分箱

bins_of_col.update(hand_bins)

3.2 使用woe值替换原数据集

def get_woe1(df,col,y ,bins):

    df= df[[col,y]].copy()

     df["cut"]= pd.cut(df[col],bins)# 手动分箱

    # print("{} 手动分箱:\n".format(col),df)

    bins_df= df.groupby("cut")[y].value_counts().unstack()

    bins_df["good%"]= bins_df[0]/ bins_df[0].sum()

    bins_df["bad%"]= bins_df[1]/ bins_df[1].sum()

    bins_df["woe"]= np.log(bins_df["good%"]/bins_df["bad%"])

    return bins_df

woeall= {}

ivall= {}

for colin bins_of_col:

    bins_df= get_woe1(model_data,col,"SeriousDlqin2yrs",bins_of_col[col])

    woeall[col]= bins_df["woe"]

    ivall[col]= get_iv(bins_df)

model_woe= pd.DataFrame(index=model_data.index)

for colin bins_of_col:

    model_woe[col]= pd.cut(model_data[col],bins_of_col[col]).map(woeall[col])

    model_woe["SeriousDlqin2yrs"]= model_data["SeriousDlqin2yrs"]

四、数据建模

使用逻辑回归进行数据建模

                                        g(x) = \frac{1}{(1+e^-x)}

X= model_woe.iloc[:,:-1]

y= model_woe.iloc[:,-1]

vali_X= vali_woe.iloc[:,:-1]

vali_y= vali_woe.iloc[:,-1]

from sklearn.linear_modelimport LogisticRegressionas LR

lr= LR(max_iter=3000).fit(X,y)

score= lr.score(vali_X,vali_y)

vali_pred=lr.predict(vali_X)

print("线性回归评分1",score)

模型评估:测试在预测坏客户的召回率为0.7965

精准率:[0.78804166 0.75787516]

召回率:[0.74829608 0.79651945]

confusion_matrix=confusion_matrix(vali_y,vali_pred)

print("混淆矩阵:\n",confusion_matrix)

print("精准率:\n",precision_score(vali_y, vali_pred,average=None))

print("召回率:\n",recall_score(vali_y, vali_pred,average=None))

五、制作评分卡

建模完毕,接下来就是把逻辑回归转化成评分卡中的分数,评分卡分数由下面公式计算:

                                    Score = A-B\log(odds)

其中A与B 是常数,A叫做常数,B叫做"刻度"。log(odds)代表一个人违约的可能性。

                                  p = \frac{1}{(1+e^-\theta ^Tx ) }

                                   log(odds)=log\frac{p}{1-p} =\theta ^T x

                                   Score=A-B* \theta ^T x

1、指定某个特定违约率的分数

2、指定违约概率翻倍的分数(POD)

                                   600 =A-B*log(\frac{1}{60} )

                                    620 = A -B*log(\frac{1}{30} )

求解得:

              B= 20/np.log(2)=28.85390081777927

               A= 600+B*np.log(1/60)=481.8621880878296

file= "ScoreData.csv"

with open(file,"w")as fdata:

    fdata.write("base_score,{}\n".format(base_score))

for i,colin enumerate(X.columns):

    print(i,col,type(i),type(col))

    score= woeall[col]*(-B*lr.coef_[0][i])

    score.name= "Score"

    score.index.name= col

    score.to_csv(file,header=True,mode="a")

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

推荐阅读更多精彩内容