【干货code】R建立评分卡详例

https://zhuanlan.zhihu.com/p/30149571

这篇文章是介绍用R做信用(申请)评分卡,包含了常用的数据处理方法,代码快为如下部分

1. 数据导入

2.数据清洗

3.特征筛选

4.模型训练

5.效果评估

6.评分卡转化


Step 1. 数据导入

示例数据选用klaR包中的GermanCredit,数据太干净了就人为加了少量异常值以便演示数据处理。变量credit_risk代表是否违约 -- ‘good’ 未违约, ‘bad’ 违约。

# 1.数据导入

df <- read.csv("C:/Users/YXS/Desktop/GermanCredit.csv",  stringsAsFactors = F)

# tips: 设置参数strngsAsFactor可防止字符型被自动转为因子型,方便数据处理

## 若从txt导入  read.table()

## 若从数据库直接读取  library(RJDBC); dbConnect()


Step 2. 数据探查与清洗

# 2.0 数据粗探

head(df)  # 查看前5行

str(df)  # 查看各变量类型

summary(df)  # 查看各变量的基础统计信息

# 变量重赋值  -- credit_risk取值为字符型,出于习惯将它转化为y标签值0,1

df$credit_risk <- ifelse(df$credit_risk == 'bad', 1, 0) # credit_risk是否违约

# 2.1检查缺失值

na_num <- apply(df, 2, function(x) sum(is.na(x)))  # 检查每列的缺失情况

sort(na_num, decreasing = T) / nrow(df)  # 缺失百分比

subset(df, is.na(job))  # 发现job变量有缺失,具体看下存在缺失的观测值

# 也可以加载sqldf以sql的方式做数据处理与探查工作,减少学习成本

# library(sqldf); sqldf('select * from df where job is null ')

# 常用的缺失值可视化拓展包有VIM,mice

# library(VIM); aggr(df)

# library(mice) ;  md.pattern(df)

# 2.2 缺失值处理

## 缺失值赋众数    -- 将job有缺失的值附众值

df[is.na(df$job), 'job'] <- names(table(df$job)[which.max(table(df$job))])

sum(is.na(df$job))

## 其它常用缺失值处理方法:

## 缺失值赋均值

#df[which(is.na(df$age), 'age')] <- mean(df$age, na.rm=T)  # na.rm

## 缺失值赋特定值

# for(i in 1:ncol(df)){

#  if(is.character(df[,i])){

#    df[is.na(df[ ,i]), i] <- "missing"

#  }

#  if(is.numeric(df[,i])){

#    df[is.na(df[ ,i]), i] <- -9999

#  }

# }

## 缺失值插补法

# library(DMwR)

# DMwR::knnImputation(data, k = 10, scale = T, meth = "weighAvg",  distData = NULL)

# library(mice)

# mice(data, m=5)

# 2.3 查看特征取值个数

val_num <- data.frame()  # 建立空矩阵用于存储后续数据

for (i in 1:ncol(df)){

t1 <- length(unique(df[,i]))  # dplyr::n_distinct()

t2 <- names(df)[i]

val_num <- rbind(data.frame(variable = t2, num = t1, type = mode(df[,i]),

stringsAsFactors = F), val_num)

}

rm(i,t1,t2); gc()  # garbage collection

## tips:在数据量大的情况下循环非常占资源,R中的循环基本都能用apply做向量化运算。为便于理解本文均采用for循环写法。

# apply(df, 2, function(x) length(unique(x))) 可取代上面的for循环

# 2.3.1 转换数据类型  -- 发现某些离散型变量的数据类型为数值型,将这些转为字符型处理

convert_cols <- val_num[which(val_num$num < 5),'variable']

df[,convert_cols] <- sapply(df[,convert_cols], as.character)

str(df[, val_num[val_num$num < 5, 'variable']])

# 2.4 查看数据分布

# 2.4.1 连续型变量查看各变量分位数

num_distribution <- c(); temp_name <- c()

for(i in names(df)){

if(is.numeric(df[,i])){

temp <- quantile(df[,i], probs=c(0,0.10,0.25,0.50,0.75,0.90,0.95,0.98,0.99,1), na.rm = T, names = T)

temp_name <- c(temp_name, i)

num_distribution <- rbind(num_distribution, temp)

}

}

row.names(num_distribution) <- temp_name

num_distribution <- as.data.frame(num_distribution)

num_distribution$variable <- temp_name

rm(i, temp, temp_name)

# 2.4.2 离散型变量查看各取值占比

char_distribution <- data.frame(stringsAsFactors = F)

for(i in names(df)){

if(!is.numeric(df[, i])){

temp <- data.frame(Variable = i, table(df[, i]), stringsAsFactors = F)

char_distribution <- rbind(char_distribution, temp)

}

}

char_distribution$Per <- char_distribution$Freq / nrow(df)

rm(i,temp)

# 异常值删除 -- 在变量分布中发现age最小值为0为异常值,这边做删除处理

age_0 <- subset(df, age==0); age_0

df <- df[- which(df$age==0), ]

rm(age_0)

# 2.4.3 查看自变量与应变量联合分布

xy_distribution <- data.frame()

for(i in names(df)){

if(!is.numeric(df[, i])){

temp <- data.frame(variable = i, table(df[, i], df$credit_risk), stringsAsFactors = F)

xy_distribution <- rbind(xy_distribution, temp)

}

}

xy_distribution <- transform(xy_distribution, Percent= xy_distribution$Freq / ifelse(xy_distribution$Var2 == 0, 699, 298))

rm(i,temp)


Step 3. 变量离散化(分箱)

主要用smbinning包的smbinnig进行分箱

library(smbinning)

# 3.1 字符转因子型 -- smbinning包要求离散型变量的数据类型为字符型

for ( i in names(df)){

if(i != 'credit_risk' & is.character(df[,i])) {

df[, i] <- as.factor(df[, i])}

}

str(df)

# 3.2 分箱

data_bak <- df

df$credit_risk <- as.numeric(df$credit_risk)  # 要求y值为数值型

bin_iv <- data.frame(); bin_var <- c()

var_name <- names(df)

for(i in var_name) {

if(is.numeric(df[,i]) & i != 'credit_risk'){

bin_tbl <- smbinning(df, y='credit_risk', x= i)  -- 连续变量用smbinning分箱

bin_iv <- rbind(bin_iv, data.frame(bin_tbl$ivtable, variable=i))

new_var <- paste('bin',i, sep='_')

bin_var <- c(bin_var, new_var)

df <- smbinning.gen(df, bin_tbl, new_var)    # 生成离散后的数据

}

if(is.factor(df[,i])){

# 离散变量用smbinning.factor,主要是计算woe、iv值

bin_tbl <- smbinning.factor(df, y='credit_risk', x= i)

bin_iv <- rbind(bin_iv, data.frame(bin_tbl$ivtable, variable=i))

new_var <-  paste('bin',i, sep='_')

bin_var <- c(bin_var, new_var)

df <- smbinning.factor.gen(df, bin_tbl, new_var)  # 生成离散后的数据

}

}

rm(i, new_var);

write.csv(bin_iv, file='C:/Users/YXS/Desktop/bin_iv.csv') # 存储分箱信息

save(df, file='C:/Users/YXS/Desktop/data_after_bin.rdata') # 数据存储备份

df<- df[, c('credit_risk', bin_var)]

rm(bin_tbl, data_bak, var_name)


Step 4. 特征筛选

# 4.1 通过IV值筛选

library(klaR)

woe_model <- woe(as.factor(df$credit_risk)~., data=df, zeroadj =0.5)  # 计算各段woe值

iv_table <- sort(woe_model$IV, decreasing = T) # woe_model$IV返回IV值,奖序

iv_var <- names(iv_table[iv_table > 0.02])  # 选取iv > 0.02的变量

woe_model <- woe(as.factor(df$credit_risk)~., data = df[, c('credit_risk', iv_var)], zeroadj =0.5, appont =T)

traindata <- predict(woe_model, newdata=df[, c('credit_risk', iv_var)])  # 用woe值代替原来的变量取值

# 4.2 逐步回归筛选

library(leaps)

regfit <- regsubsets(credit_risk~., data = traindata, method = 'back', nvmax = 10) #向后逐步回归

reg_summary <- summary(regfit)

plot(reg_summary$bic)  # 9个变量后bic就基本不下降了,选最好的9个变量入模

reg_summary

# 筛选入模变量

feature_in <- c('bin_status', 'bin_credit_history', 'bin_duration'

,'bin_savings','bin_purpose','bin_personal_status_sex',

'bin_other_debtors', 'bin_installment_rate')

feature_in <- paste('woe', feature_in, sep='.')


Step 5. Logistic 模型训练

# 5. 逻辑回归训练

glmodel <- glm(credit_risk~., traindata[,c('credit_risk', feature_in)], family = binomial)

summary(glmodel)

# 5.1 相关性检验

corelation <- cor(traindata[,feature_in])

library(lattice)

levelplot(corelation)

rm(corelation)

# 5.2 VIF 共线性检验

library(car)

vif(glmodel, digits =3 )


Step 6. 模型评估

# 6.3 模型评估

# ROC/AUC

pred <- predict(glmodel, newdata = traindata,type = "response")

library(ROCR)

t <- prediction(pred, traindata[, 'credit_risk'])

t_roc <- performance(t, 'tpr', 'fpr')

plot(t_roc)

t_auc <- performance(t, 'auc')

t_auc@y.values

title(main = 'ROC Curve')

# KS 值

ks <- max(attr(t_roc, "y.values")[[1]] - (attr(t_roc, "x.values")[[1]])); print(ks)


Step 7. 制作评分卡

# 7.1 计算factor和offset

# 620 = offset + factor * log(15*2)

# 600 = offset + factor * log(15) # 按好坏比15为600分, 翻一番加20

factor <- 20/log(2)  # 比例因子

offset <- 600-factor*log(15)  # 偏移量

# 7.2提取所需 woe、逻辑回归系数、截距项、特征个数

glm_coef <- data.frame(coef(glmodel))

NamesWoE <- row.names(glm_coef)[-1] <- gsub('woe.', replacement = '', row.names(glm_coef)[-1])

a = glm_coef[1,1]  # 截距

Beta <- glm_coef$coef.glmodel.[-1]    # 系数

names(Beta) <- row.names(glm_coef)[-1]; Beta # 系数名

glm_coef$Variables  <-  row.names(glm_coef)

feature_num <- nrow(glm_coef) - 1 # 特征数目

Score_card <- data.frame()

# Score_card  <-  data.frame(WoE = c(NA),  Score = c(NA),  Variable = c(NA),  Beta = c(1), Band = c(NA))

# Score_card <- na.omit(Score_card) # delte na cases

# 7.3 计算最终评分

for (i in NamesWoE) # 循环变量,计算每个变量取值下的分数

{

WoEEE <- data.frame(woe_model$woe[i])

# 评分公式

Score <- data.frame(-(Beta[i]*WoEEE + a/(feature_num)) * factor + offset/(feature_num))

Temp <- cbind(WoEEE,  Score)

Temp$Variable <- i

Temp$Beta <- Beta[i]

Temp$Value <- row.names(Temp)

names(Temp)[1] <- "WoE"

names(Temp)[2] <- "Score"

Score_card <- rbind(Temp,  Score_card)

}

rm(i,WoEEE, NamesWoE, feature_num, glm_coef, Temp, Score)

write.table(Score_card, file='C:/Users/YXS/Desktop/Scorecard.csv', sep  =  ",  ", col.names  =  NA)

数据源与整体code见iking8023/Score-Card

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

推荐阅读更多精彩内容