简介:这份资源是面向数据挖掘学习者与互联网金融风控从业者的课程资料包,聚焦如何用R语言对海量互金数据做深入分析并构建有效风控模型,适合具备一定统计与编程基础、希望提升实战能力的中高级学员。包内共4个文件,包含R程序源代码、R历史记录、PDF讲义与PPT课件,分别用于动手实践、复现操作过程以及系统梳理知识点,压缩包约10.15MB,轻量便于下载学习。课程内容覆盖数据导入清洗与转换、特征工程、信用评分卡与逻辑回归、决策树、随机森林、支持向量机等模型构建,以及交叉验证、网格搜索调参和AUC、ROC曲线等评估指标,并配有从数据清洗到建模的完整实战演练。目前已有266人学习,读者可借助源码与课件快速理解互金风控建模全流程,积累可迁移到实际业务中的分析与排错经验。
1. 互联网金融风控模型到底在挖什么:从一份课程资料说起
很多人第一次接触「大数据挖掘之互联网金融风控模型」这个方向,是被一份打包资料吸引进来的——里面通常有 R 语言程序源代码、PPT 课件,还有一份 .rar 压缩包。但真正打开之后,最常见的困惑不是代码跑不起来,而是不知道这些代码在业务上到底对应什么动作。互联网金融风控模型的核心,其实就三件事:判断这笔借款该不该放、额度给多少、利率定多高。数据挖掘在这里的角色,是把历史借贷行为、还款表现、多头借贷记录这些原始字段,变成能排序的分数。
这套东西适合谁?一是刚转行做风控建模、手里只有 R 基础的数据分析人员;二是做信贷产品、需要看懂模型输出但不想被算法黑匣子绕晕的产品和运营;三是想拿一套完整案例练手的在校学生。它解决的不是「预测未来」这种玄学问题,而是把「这个人过去的行为像不像会逾期的人」变成一个可复现的流程。下面我按一份典型课程资料的结构,把从数据到评分卡的路径拆开讲,代码用 R,参数和坑都标清楚。
2. 用 R 把原始借贷流水变成建模宽表:字段清洗与标签定义
2.1 先搞清楚哪些字段能进模型,哪些必须扔
拿到一份互联网金融风控的原始数据,第一眼看到的往往是几十个字段:用户 ID、申请时间、借款金额、期限、利率、还款状态、历史逾期次数、近 30 天申请次数等等。不是所有字段都能直接喂给模型。我一般会先做三件事:把主键和时间戳单独拎出来、把标签列锁定、把明显属于「事后信息」的字段剔除。
所谓事后信息,就是放款之后才知道的字段,比如「实际还款日期」「催收记录」。这些字段如果在训练集里出现,模型离线指标会好得离谱,上线就翻车。常见做法是只保留申请时点及之前能拿到的变量。下面这段 R 代码做的是读入数据、剔除事后字段、并把字符型日期转成标准格式。
# 读入原始借贷流水,假设为 csv 格式 raw <- read.csv("loan_raw.csv", stringsAsFactors = FALSE, encoding = "UTF-8") # 事后字段清单:这些字段放款后才有,训练阶段必须剔除 post_loan_cols <- c("repay_date", "collection_flag", "settle_amount") # 只保留申请时点可见的字段 model_data <- raw[, !(names(raw) %in% post_loan_cols)] # 日期字段统一转换,避免后续计算逾期天数时格式错乱 model_data$apply_time <- as.POSIXct(model_data$apply_time, format = "%Y-%m-%d %H:%M:%S") # 检查缺失率,超过 60% 的字段先标记出来 missing_rate <- sapply(model_data, function(x) sum(is.na(x)) / length(x)) high_missing <- names(missing_rate[missing_rate > 0.6]) print(high_missing)这段代码的逻辑很直白:先做字段级过滤,再做格式统一,最后用缺失率筛一遍。参数上,encoding = "UTF-8"是为了防止中文列名乱码;缺失率阈值 0.6 不是死规定,业务上如果某个字段很重要但缺失高,可以保留并单独做缺失值填充,而不是一刀切删掉。
2.2 标签怎么定义,直接决定模型是「能用」还是「白做」
风控模型的标签通常定义为「未来某段时间内是否逾期超过 X 天」。这里有两个参数必须写死:观察窗口和表现窗口。观察窗口是拿多久的行为数据来预测,表现窗口是放款后多久看结果。常见组合是观察窗口 6 个月、表现窗口 6 个月,也就是用前 6 个月的申请和行为,预测放款后 6 个月内会不会出现 M1+(逾期 1 期以上)。
# 定义表现窗口:放款后 180 天内是否出现逾期超过 30 天 model_data$label <- ifelse( !is.na(model_data$overdue_days) & model_data$overdue_days > 30, 1, 0 ) # 剔除表现期不足的样本,避免标签不成熟 model_data <- model_data[model_data$observation_days >= 180, ] # 查看好坏样本比例 table(model_data$label) prop.table(table(model_data$label))这里最容易踩的坑是「标签不成熟」:有些样本放款才两个月,还没到表现窗口结束,就被当成好样本,结果模型学到的全是噪声。所以observation_days >= 180这个过滤条件不能省。另外,好坏样本比例如果低于 1:20,后续要做采样或调权重,否则模型会倾向于全预测为好样本。
2.3 宽表落地前,把类别变量和缺失值处理干净
R 里跑逻辑回归或评分卡,类别变量不能直接扔进去。常见做法是对高基数类别变量做 WOE 编码,对低基数做哑变量。缺失值方面,数值型用中位数填充并加一个缺失指示列,类别型把缺失单独当成一类。下面这段代码演示一个简化版的处理流程。
# 数值型缺失用中位数填充,并生成缺失指示列 num_cols <- c("age", "income", "apply_cnt_30d") for (col in num_cols) { na_flag <- is.na(model_data[[col]]) model_data[[paste0(col, "_is_na")]] <- as.integer(na_flag) model_data[[col]][na_flag] <- median(model_data[[col]], na.rm = TRUE) } # 类别型缺失统一填 "MISSING" cat_cols <- c("city_level", "education", "marital_status") for (col in cat_cols) { model_data[[col]][is.na(model_data[[col]])] <- "MISSING" } # 保存宽表,供后续分箱和建模使用 write.csv(model_data, "model_wide_table.csv", row.names = FALSE)参数说明:中位数填充比均值填充更稳,因为收入这类字段常有极端值;加缺失指示列是为了让模型知道这个值原本是缺失的,而不是真实的中位数。类别型填"MISSING"而不是众数,是因为缺失本身可能携带信息,比如不愿意填学历的人,风险表现往往和填了的人不一样。
3. 评分卡建模全流程:分箱、WOE、逻辑回归与分数转换
3.1 分箱不是随便切,先看单调性再定切点
变量分箱是评分卡最耗时的一步。常见做法有等频分箱、等距分箱和决策树分箱。我一般先用等频分箱跑一版,再看每个箱的坏样本率是否单调。如果不单调,就手动合并相邻箱。R 里可以用smbinning包做初步分箱,也可以用下面这段自定义函数快速看分布。
# 自定义等频分箱函数,返回每个箱的样本量和坏样本率 equal_freq_bin <- function(df, var, label, bins = 5) { breaks <- quantile(df[[var]], probs = seq(0, 1, length.out = bins + 1), na.rm = TRUE) breaks <- unique(breaks) # 防止重复切点 df$bin <- cut(df[[var]], breaks = breaks, include.lowest = TRUE) result <- aggregate(df[[label]], by = list(df$bin), FUN = function(x) { c(count = length(x), bad_rate = mean(x)) }) result } # 对年龄字段做 5 等频分箱 bin_result <- equal_freq_bin(model_data, "age", "label", bins = 5) print(bin_result)逻辑说明:quantile按分位点切,保证每箱样本量接近;cut生成箱标签;aggregate统计每箱的样本数和坏样本率。参数bins = 5是经验值,样本量小的时候可以降到 3,样本量大可以升到 10。关键看坏样本率是否随箱号递增或递减,如果中间某箱突然跳变,就要考虑合并。
3.2 WOE 和 IV 的计算,以及怎么用 IV 筛变量
WOE(Weight of Evidence)是把每个箱的坏样本率转换成对数值,IV(Information Value)是 WOE 的加权和。IV 用来衡量变量的预测能力,通常 IV 小于 0.02 的变量直接扔掉,0.02 到 0.1 弱,0.1 到 0.3 中等,大于 0.3 要警惕过拟合。下面代码计算 WOE 和 IV。
# 计算每个箱的 WOE 和 IV calc_woe_iv <- function(df, var, label) { df$bin <- as.factor(df[[var]]) total_bad <- sum(df[[label]] == 1) total_good <- sum(df[[label]] == 0) stats <- do.call(rbind, lapply(levels(df$bin), function(b) { sub <- df[df$bin == b, ] bad <- sum(sub[[label]] == 1) good <- sum(sub[[label]] == 0) woe <- log((bad / total_bad) / (good / total_good)) iv <- (bad / total_bad - good / total_good) * woe data.frame(bin = b, bad = bad, good = good, woe = woe, iv = iv) })) stats } # 假设 age_bin 是已经分好箱的变量 woe_table <- calc_woe_iv(model_data, "age_bin", "label") print(woe_table) print(paste("IV =", sum(woe_table$iv)))参数说明:total_bad和total_good是全样本的好坏数量;woe公式里分子是箱内坏样本占比,分母是箱内好样本占比;iv是每个箱的贡献,求和得到变量总 IV。注意如果某个箱的好样本或坏样本为 0,WOE 会变成无穷大,实际处理时要把这个箱和相邻箱合并。
3.3 逻辑回归建模与分数转换,把概率变成 300 到 850 的评分
变量筛完之后,用逻辑回归拟合。R 里用glm函数,family 设为 binomial。拟合完不能直接上线,要把预测概率转换成标准评分。常见公式是score = A - B * log(odds),其中A和B由基准分和 PDO(Points to Double the Odds)决定。
# 逻辑回归建模,假设已选入 5 个变量 final_vars <- c("age_bin_woe", "income_bin_woe", "apply_cnt_30d_woe", "overdue_cnt_hist_woe", "city_level_woe") formula <- as.formula(paste("label ~", paste(final_vars, collapse = " + "))) lr_model <- glm(formula, data = model_data, family = binomial(link = "logit")) # 分数转换:基准分 600,PDO 50,基准 odds 为 1:1 A <- 600 + 50 * log(1) / log(2) B <- 50 / log(2) # 计算每个样本的评分 model_data$prob <- predict(lr_model, type = "response") model_data$odds <- model_data$prob / (1 - model_data$prob) model_data$score <- round(A - B * log(model_data$odds)) # 查看评分分布 summary(model_data$score)逻辑说明:glm拟合逻辑回归,predict输出概率;A和B的推导基于「odds 翻倍时分数下降 PDO」这个约定。参数上,基准分 600 和 PDO 50 是行业常见起点,实际项目里要根据通过率和坏账率反推。评分越高代表风险越低,通常 600 以上算较好客户,550 以下要人工复核。
4. 模型验证与监控:AUC、KS 和 PSI 到底怎么看
4.1 AUC 和 KS 的计算,以及为什么不能只看 AUC
AUC 衡量的是模型把好样本排在坏样本前面的能力,KS 衡量的是好坏样本累计分布的最大差值。两个指标要一起看:AUC 高但 KS 低,说明模型整体排序可以,但区分度不够尖锐。下面代码用pROC包计算 AUC 和 KS。
library(pROC) # 计算 AUC roc_obj <- roc(model_data$label, model_data$prob) auc_value <- auc(roc_obj) print(paste("AUC =", round(auc_value, 4))) # 计算 KS ks_value <- max(roc_obj$sensitivities + roc_obj$specificities - 1) print(paste("KS =", round(ks_value, 4)))参数说明:roc的第一个参数是真实标签,第二个是预测概率;sensitivities和specificities是 ROC 曲线上的灵敏度和特异度。经验上,AUC 低于 0.6 的模型基本不能用,KS 低于 0.2 说明区分度不足。但这两个指标都是在训练集或测试集上算的,上线后要看 PSI。
4.2 PSI 监控:模型上线后有没有「漂移」
PSI(Population Stability Index)用来比较训练集和当前样本的分数分布差异。PSI 小于 0.1 说明稳定,0.1 到 0.25 要关注,大于 0.25 说明分布明显漂移,模型需要重新训练。下面代码计算两个时间段的 PSI。
# 计算 PSI,expected 是训练集分数分布,actual 是当前样本分布 calc_psi <- function(expected, actual, bins = 10) { breaks <- quantile(expected, probs = seq(0, 1, length.out = bins + 1), na.rm = TRUE) breaks <- unique(breaks) exp_pct <- table(cut(expected, breaks, include.lowest = TRUE)) / length(expected) act_pct <- table(cut(actual, breaks, include.lowest = TRUE)) / length(actual) psi <- sum((act_pct - exp_pct) * log(act_pct / exp_pct)) psi } # 假设 train_score 和 oot_score 是两个时间段的评分 psi_value <- calc_psi(train_score, oot_score) print(paste("PSI =", round(psi_value, 4)))逻辑说明:先按训练集分数切分箱,再分别统计训练集和当前样本落在每个箱的比例,最后按 PSI 公式求和。参数bins = 10是常用值,样本量小的时候可以降到 5。注意如果某个箱在 actual 里占比为 0,log 会出问题,实际代码里要加一个极小值保护。
4.3 跨时间验证:OOT 样本比随机切分更接近真实上线
很多课程资料只做随机切分训练集和测试集,但风控模型必须做跨时间验证。常见做法是按申请时间切分:前 8 个月做训练,后 2 个月做 OOT(Out of Time)测试。下面代码演示按时间切分并分别计算 AUC。
# 按申请时间排序 model_data <- model_data[order(model_data$apply_time), ] # 前 80% 做训练,后 20% 做 OOT cut_point <- floor(nrow(model_data) * 0.8) train_data <- model_data[1:cut_point, ] oot_data <- model_data[(cut_point + 1):nrow(model_data), ] # 在训练集上建模,在 OOT 上验证 lr_train <- glm(formula, data = train_data, family = binomial(link = "logit")) oot_data$prob <- predict(lr_train, newdata = oot_data, type = "response") # 计算 OOT 的 AUC roc_oot <- roc(oot_data$label, oot_data$prob) print(paste("OOT AUC =", round(auc(roc_oot), 4)))参数说明:cut_point按 80% 切分是常见做法,但更严格的做法是按固定时间点切,比如用 1 到 8 月训练,9 到 10 月验证。OOT AUC 通常比随机测试集低 0.02 到 0.05,如果低太多,说明模型对时间不稳定,要检查变量里有没有随时间漂移严重的字段。
5. 避坑与排查:R 语言风控建模里最容易翻车的 5 个地方
5.1 现象:模型 AUC 0.85,上线后坏账率反而升高
原因:训练集里混入了事后字段,或者标签定义时用了放款后的信息。比如「催收次数」这种字段,只有逾期的人才有,模型学到的是「有催收记录的人会逾期」,而不是「会逾期的人有催收记录」。
解决:把所有字段按「申请时点是否可见」过一遍,凡是放款后产生的字段一律剔除。可以用下面这段代码做字段审计。
# 字段审计:列出所有字段名,人工标记是否申请时可见 field_audit <- data.frame( field = names(raw), visible_at_apply = NA # 人工填写 TRUE/FALSE ) # 导出后逐字段确认,FALSE 的字段从模型数据中删除 write.csv(field_audit, "field_audit.csv", row.names = FALSE)5.2 现象:WOE 计算时出现 Inf 或 NaN
原因:某个分箱里好样本或坏样本数量为 0,导致 log 里出现 0 或无穷大。
解决:在计算 WOE 前,检查每个箱的好坏样本数,如果小于 5,就和相邻箱合并。下面代码做合并前的检查。
# 检查每个箱的好坏样本数 bin_check <- aggregate(model_data$label, by = list(model_data$age_bin), FUN = function(x) { c(bad = sum(x == 1), good = sum(x == 0)) }) print(bin_check) # 如果某个箱 bad 或 good 小于 5,手动调整分箱切点5.3 现象:逻辑回归系数符号和业务常识相反
原因:变量之间存在多重共线性,或者 WOE 编码时把方向搞反了。比如收入越高应该风险越低,但系数是正的。
解决:先算变量之间的相关系数矩阵,把相关系数大于 0.7 的变量挑出来,只保留 IV 更高的那个。然后检查 WOE 编码的方向,确保坏样本率高的箱 WOE 也高。
# 计算变量间相关系数 cor_matrix <- cor(model_data[, final_vars], use = "pairwise.complete.obs") # 找出相关系数大于 0.7 的变量对 high_cor <- which(abs(cor_matrix) > 0.7 & upper.tri(cor_matrix), arr.ind = TRUE) print(high_cor)5.4 现象:评分分布集中在 600 附近,区分度不够
原因:分数转换时 A 和 B 的参数设得不对,或者模型本身区分度不足。
解决:先看 AUC 和 KS,如果 AUC 正常但分数集中,调整 PDO 和基准分。PDO 越小,分数分布越分散。可以画评分分布直方图确认。
# 画评分分布直方图 hist(model_data$score, breaks = 50, main = "Score Distribution", xlab = "Score", col = "lightblue") # 如果分布太集中,把 PDO 从 50 降到 20 或 305.5 现象:OOT 样本上 PSI 超过 0.25,但 AUC 没降
原因:分数分布漂移但排序能力还在,可能是某个变量的分布变了,但变量本身的区分度没变。
解决:逐变量计算 PSI,找出漂移最严重的变量,判断是业务变化还是数据采集问题。如果某个变量 PSI 持续偏高,考虑在下一版模型里替换掉。
# 逐变量 PSI 计算 var_psi <- sapply(final_vars, function(v) { calc_psi(train_data[[v]], oot_data[[v]]) }) print(sort(var_psi, decreasing = TRUE))6. 把课程资料变成自己的东西:从跑通代码到改一版能用的评分卡
一份打包资料最大的价值不是代码本身,而是它提供了一条从原始数据到评分卡的完整链路。但直接跑通只能算「看过」,真正变成自己的东西,得做三件事:换数据、改参数、加监控。
换数据是指找一份自己的借贷流水,哪怕只有几千条,按第 2 章的流程重新走一遍字段清洗和标签定义。这一步会逼着你面对真实数据里的各种脏情况,比如同一个用户多条申请记录、时间戳格式不统一、标签列有缺失。改参数是指把分箱数量、IV 阈值、PDO 这些参数按自己的数据分布调一遍,观察 AUC 和 KS 的变化。加监控是指把第 4 章的 PSI 计算封装成一个函数,每月跑一次,看分数分布有没有漂移。
下面这段代码把前面几章的核心步骤串成一个可复用的函数框架,方便你替换数据后直接跑。
# 可复用的评分卡建模框架 build_scorecard <- function(data, label_col, candidate_vars, bins = 5) { # 1. 分箱和 WOE 计算 woe_tables <- list() for (v in candidate_vars) { data[[paste0(v, "_bin")]] <- equal_freq_bin(data, v, label_col, bins)$bin woe_tables[[v]] <- calc_woe_iv(data, paste0(v, "_bin"), label_col) } # 2. 按 IV 筛选变量 iv_values <- sapply(woe_tables, function(x) sum(x$iv)) selected_vars <- names(iv_values[iv_values > 0.02]) # 3. 逻辑回归建模 formula <- as.formula(paste(label_col, "~", paste(selected_vars, collapse = " + "))) model <- glm(formula, data = data, family = binomial(link = "logit")) # 4. 分数转换 A <- 600 + 50 * log(1) / log(2) B <- 50 / log(2) data$prob <- predict(model, type = "response") data$score <- round(A - B * log(data$prob / (1 - data$prob))) # 5. 返回模型、评分和 IV 表 list(model = model, data = data, iv = iv_values, woe = woe_tables) } # 使用示例 result <- build_scorecard(model_data, "label", c("age", "income", "apply_cnt_30d")) print(result$iv)这个框架里,candidate_vars是候选变量列表,bins是分箱数量。实际使用时,先把所有可能用到的变量列进去,让 IV 筛选自动淘汰弱变量。注意equal_freq_bin和calc_woe_iv需要提前定义,就是第 3 章里的那两个函数。
最后说一个我自己的习惯:每次跑完模型,先把评分分布、AUC、KS、PSI 四个数记在一张表里,再开始调参。调参过程中只改一个参数,改完重新记录。这样即使某次调参翻车了,也能快速回退到上一版。风控建模没有一步到位的玄学,靠的就是这种笨办法一点点逼近可用状态。希望帮到你。
本文还有配套的精品资源,点击获取