评分卡开发流程全解析---python版

一、背景

从去年暑假到今年2月份曾在两家互金公司实习,接触了很多金融信贷场景下的知识,其中最基础的当属评分卡的开发了。接下来将结合我在实习中的工作讲一下评分卡的开发全流程,有兴趣的童鞋可以看一下,有任何不对的地方希望大家批评指正。

二、正文

首先评分卡的开发流程大致包含下面这些步骤:

(一)定义问题

确定两类重要的问题:观察期和表现期、好坏客户的定义。

观察期相当于X变量,即搜集用户的行为;表现期相当于Y变量,即用户的表现,在评分卡中通常就是好坏两类。观察期从观察时点起向前回溯的最远时长,表现期从观察时点起根据账龄分析来决定。

从图中可看出,在9个月以后的坏账率趋于稳定,说明坏客户重复暴露的时间为9个月,那么就定义表现期为9个月。

好坏客户通过滚动率和迁移率来分析。滚动率分析就是从某个观察点之前的一段时间(称为观察期)的最坏的状态向观察点之后的一段时间(称为表现期)的最坏状态的发展变化情况,如下所示:

从上看出,在观察期正常还款的在表现期还正常还款的概率为96%,在观察期逾期3个月的客户在表现期逾期4个月的概率为61%,说明M3的用户有很大概率会更坏变为M4,故定义M3为坏客户,即逾期60-90的人。

(二)数据准备和预处理

贯穿全文的包---scorecardpy

根据定义的观察期和表现期拉取数据,预处理主要是对异常值、缺失值,有些情况还需要特征缩放即归一化和标准化处理,比如时间和金额等等和其他字段的取值相差很大时,可以利用已有的方法也可以自定义,如下所示:

#相当于数据预处理, 设定要删除的规则,主要是指缺失率,iv<0.02等等

dt_s = sc.var_filter(data, y="flagy")

print("变量预处理前后变化:",data.shape[1],"->",dt_s.shape[1])

(三)特征工程

可以从数据来源、业务类型、时间范围和统计单位4个维度来衍生,将4个维度作笛卡尔积交叉组合特征。

除此之外维度指标有频次(近3个月的f贷款申请次数)、总和、平均、占比、一致性(身份证和手机号所在的省份是否一致)、距离、波动、占比趋势和对比趋势等。还有一些可以结合具体的业务来,如在信用卡业务中对于还款有近1、3、6、12、24个月的还款次数、最大单笔还款距今时长、最后一次还款距今时长等等。

(四)变量筛选

在评分卡中由于需要业务具有很强的解释性,通常入模的变量为8~16个,需要筛选变量。变量筛选的方式有以下几种:

如果不使用预测模型,有监督的可以使用信息值、卡方统计量和gini系数;无监督的可以使用相关性、聚类和PCA等

若使用预测模型,可以使用逐步回归、正则化(AIB,BIC,lasso)、交叉验证和机器学习如xgb等自带筛选特征,在经过预处理后保留下来的变量有707个,故必须要筛选变量,采用了两种方法lasso和xgb,xgb的方法如下:

# 进行Xgboost之前需要处理数据,要预先处理数据 都要转化成float类型,而且要没有缺失值

import re

from sklearn.model_selection import cross_val_score

from sklearn.model_selection import KFold

from sklearn.model_selection import train_test_split

def float_1(x):

    try:

        return float(x)

    except:

        return np.nan

y = dt_s['flagy']

col = set(dt_s.columns)

X = dt_s.ix[:, [x for x in col if not re.search('flagy', x)]]

X = X.applymap(float_1)

X = X.astype(np.float)

X = X.fillna(0)

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=10)

选择合适的参数

import xgboost as xgb

dtrain = xgb.DMatrix(X_train, y_train)

params={

    'booster':'gbtree',

    'objective': 'binary:logistic',

        'eval_metric': 'auc',

    'max_depth': 3,

    'lambda': 5,

        'subsample': 0.8,

        'colsample_bytree': 0.8,

        'min_child_weight': 20,

        'eta': 0.025,

    'nthread': 8

        }

# 交叉验证选出test-auc-mean最高时对应的迭代次数

cv_log = xgb.cv(params,dtrain,num_boost_round=500,

                nfold=5,

                metrics='auc',

                early_stopping_rounds=10)

bst_auc = cv_log['test-auc-mean'].max()

cv_log['nb'] = cv_log.index

cv_log.index = cv_log['test-auc-mean']

nround = cv_log.nb.to_dict()[bst_auc]

lasso方法如下:

# 交叉验证拟合Lasso模型

from sklearn.linear_model import LassoCV

lassocv = LassoCV()

lassocv.fit(X,y)

# 交叉验证选择的参数alpha

print(lassocv.alpha_)

# 最终Lasso模型中的变量系数

print(lassocv.coef_[:10])

# Lasso细筛出的变量个数

print(np.sum(lassocv.coef_ > 0))

然后可以将两者的特征交集作为初步的变量。

#将两种方法取交集,得到的变量入模

features_chosen = ['pc_rcnt_income', 'location_cell_stab', 'location_cell_use_days',

                  'als_m6_id_nbank_cons_allnum', 'ir_id_x_cell_notmat_days',

                  'pc_business_type', 'als_m12_id_caoff_orgnum', 'pc_noincome_lst_mons',

                  'stab_mail_num', 'pc_long_income', 'als_m12_id_cooff_orgnum',

                  'pc_mobile_cons', 'pc_regincome_sta', 'als_fst_cell_nbank_inteday',

                  'location_online_fre', 'als_m6_cell_nbank_p2p_allnum',

                  'als_m6_cell_bank_min_inteday', 'als_m12_id_nbank_max_inteday',

                  'location_id_attr', 'als_m12_cell_nbank_nsloan_allnum',

                  'cons_max_m12_pay', 'cons_tot_m3_visits', 'cons_tot_m3_pay', 'als_m12_id_nbank_allnum'

]

(五)相关性分析

可以利用皮尔森相关系数>0.7,和方差扩大因子vif>3来筛选变量。

#将vif>3的去掉,statsmodels不允许有缺失值

col = np.array(data[features_chosen])

from statsmodels.stats.outliers_influence import variance_inflation_factor as vif

data[features_chosen] = data[features_chosen].fillna(0)

for i in range(len(features_chosen)):

    print('{} VIF是{}'.format(features_chosen[i],vif(col,i)))

(六)WOE/IV

在计算WOE和IV值之前先要划分训练集、测试集和跨时间验证样本,比例为5:2:3,接下来是分箱,分箱应该在训练集上而不是整个数据上,因为如果在整个数据集上先分箱,然后再用其中的一部分作为测试集,将会导致过拟合。

dt_var = data[features_chosen]

dt_var['flagy'] = data['flagy']

train, test = sc.split_df(dt_var,'flagy').values()

print("训练集、测试集划分比例为",train.shape[0],":",test.shape[0])

然后接下来分箱

#bins是对训练集作的分箱

bins_train = sc.woebin(train, y="flagy",save_breaks_list=True)

#测试集以在训练集上分箱的情况作为标准

bins_test = sc.woebin(test, y="flagy",breaks_list=breaks_list)

train_woe = sc.woebin_ply(train, bins_train)

test_woe = sc.woebin_ply(test, bins_test)

y_train = train_woe.loc[:,'flagy']

X_train = train_woe.loc[:,train_woe.columns != 'flagy']

y_test = test_woe.loc[:,'flagy']

X_test = test_woe.loc[:,train_woe.columns != 'flagy']

X_train.to_csv('X_train_final.csv',index=False,sep=',',header=None)

y_train.to_csv('y_train_final.csv',index=False,sep=',',header=None)

计算的IV值

分箱的结果,对于非单调的可以手动分箱:

(七)模型构建

用到LR和LightGBM,部分代码如下:

LR代码:

from sklearn.model_selection import GridSearchCV

from sklearn.linear_model import LogisticRegression

#需要调优的参数

penaltys = ['l1','l2']

Cs = [0.001, 0.01, 0.1, 1, 10, 100, 1000]

tuned_parameters = dict(penalty = penaltys, C = Cs)

lr_penalty= LogisticRegression()

# GridSearchCV(estimator, param_grid, ... cv=None, ...)

# estimator: 模型, param_grid:字典类型的参数, cv:k折交叉验证

grid= GridSearchCV(lr_penalty, tuned_parameters, cv=5)

grid.fit(X_train,y_train) # 网格搜索训练

grid.cv_results_ #训练的结果

# examine the best model

print(grid.best_score_)  # 最好的分数

print(grid.best_params_) # 最好的参数

from sklearn.linear_model import LogisticRegression

lr = LogisticRegression(penalty='l1',C=0.1,solver='saga',n_jobs=-1)

lr.fit(X_train,y_train)

对于变量还要计算其显著性,如P值、Z值,然后将P>|Z|的变量去掉

from scipy import stats


def stat_model(x, y, y_pred, const, coef):

    '''

    '''

    x = x.copy()

    x.reset_index(drop=True,inplace=True)


    y = y.copy()

    y.reset_index(drop=True,inplace=True)


    newX = pd.DataFrame({"Constant": np.ones(len(x))}).join(pd.DataFrame(x))

    MSE = (sum((y - y_pred)**2))/(len(newX)-len(newX.columns))#分母应该是N

    params = np.append(const,coef)

    var_b = MSE*(np.linalg.inv(np.dot(newX.T,newX)).diagonal())

    st_error = np.sqrt(var_b)

    t_v = params/st_error

    p_values = [2*(1-stats.t.cdf(np.abs(i),len(newX)-1)) for i in t_v]

    result = pd.DataFrame({'Coefficients':params,

                          'Standard Errors':st_error,

                          't values':t_v,

                          'P_values':p_values}) \

                        .apply(lambda x: round(x,3)) \

                        .assign(Variable = newX.columns.tolist())


    return result

#intercept_是截距,coef_是训练后的输入端模型系数,如果label有两个,即y值有两列。

summary = stat_model(X_train,y_train,train_pred,lr.intercept_,lr.coef_)

summary[['Variable','Coefficients','P_values']]

#得到的constant是截距项,不用管

LightGBM代码(找不到了...):

(八)模型评估

模型评估使用ks、auc和psi来评价

train_perf = sc.perf_eva(y_train, train_pred, title = "train")

test_perf = sc.perf_eva(y_test, test_pred, title = "test")

最终模型的分值分布如下:

(九)模型部署

在风控后台上配置模型规则,对于复杂的模型还需要将模型文件作转换,封装成类,由其他代码来调用

(十)模型监控

前期主要监控模型的稳定性psi,变量的psi,也要比较模型每日的拒绝率和线下拒绝率

后期等到用户有一定表现后可以用auc、ks和iv等来对比线上和线下的区别。

三、结语

在互金实习了两段,最终还是选择了互联网,这也是对自己实习部分工作的回顾和总结。评分卡建模流程基本如上,欢迎大家指正交流。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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