简介:这份资料面向银行风控建模人员、金融数据分析师与信贷策略岗,围绕风控决策引擎在商业银行联合贷与小微贷场景中的落地展开,可帮助读者建立从模型设计、规则准入到贷后预警的完整方法论框架。包内为1个PDF文档,压缩包约613KB,内容以案例讲解与图表数据为主,适合作为建模项目参考手册或内部分享材料。目前已吸引951人学习下载。资源核心是两则建模案例:案例一聚焦某城商行「网商贷」申请评分卡与行为评分卡,给出好坏客户定义、表现期与观察期设计、有征信与无征信模型细分方案,并列出KSAUC、精确率、准确率、召回率、PSI等评估结果,行为卡还梳理了账户、余额、还款、逾期四类变量衍生思路;案例二以某农信小微「税金贷」为例,展示14条准入剔除规则、基于税务与工商司法等多源数据筛选出的1100个变量如何经缺失值、单一值、IV与共线性处理压缩至10个入模变量,并配套额度测算与贷后预警设计,便于读者直接对照自身项目复用。
1. 从一笔被拒的放款说起:风控决策引擎在银行里到底做什么
一笔线上消费贷申请,从点击提交到页面返回「额度 38000 元、利率 7.2%、12 期」通常不会超过两秒。这两秒里,风控决策引擎要做的事包括:拉取征信与多头数据、跑十几条准入规则、调用申请评分卡、判定是否命中反欺诈名单、按矩阵算出额度与定价,最后把决策结果和命中明细一并落库留痕。银行做建模,真正难的不是把 KS 做到 0.4,而是让模型在线上以毫秒级稳定执行,并且任何一条策略都能被人解释、被人回滚。决策引擎就是承接这件事的容器:规则、评分卡、决策表、决策流四样东西串起来,才叫一套能上生产的策略体系。下面按贷前到贷后的顺序,把建模案例和引擎落地拆开讲,重点放在数据口径、评分卡转换和决策流编排这些真要动手的环节。
2. 风控决策引擎的数据底座:贷前贷中贷后的评分卡分工与指标口径
决策引擎本身不产生判断力,判断力来自喂给它的特征和模型。很多团队上引擎失败,不是引擎不好用,而是拿到的变量口径混乱:同一个「近 3 个月查询次数」,在离线表里是自然月、在实时接口里是滚动 90 天,上线后模型分直接漂移。所以建模案例的第一步永远是先把数据底座和指标口径定死,再谈规则和评分卡怎么编排。
2.1 贷前贷中贷后:三类评分卡的样本与用途
银行风控里常说的 A 卡、B 卡、C 卡,分别对应申请、行为、催收三个决策点,它们的样本空间和表现期完全不同,混用是常见事故。A 卡用于新客准入和初始额度,样本是申请件,表现期一般看首贷后 6 到 12 期是否出现 DPD30+;B 卡用于存量客户的调额、预警和定价调整,样本是已有账户,观察近 6 个月行为;C 卡用于逾期客户的催收强度分配,表现期更短。
| 评分卡 | 决策场景 | 观察期 | 表现期 | 常用评估门槛 | 更新频率 |
|---|---|---|---|---|---|
| A 卡 申请评分 | 准入、初始额度、定价 | 申请时点 | 6~12 期 | KS ≥ 0.35,AUC ≥ 0.75 | 月度或季度 |
| B 卡 行为评分 | 调额、早期预警、交叉营销 | 近 6 个月 | 未来 3~6 期 | KS ≥ 0.40 | 月度 |
| C 卡 催收评分 | 催收队列、委外分配 | 逾期时点 | 未来 3 期回款 | KS ≥ 0.35,回款率提升 | 季度 |
| 反欺诈评分 | 申请实时拦截 | 申请时点 | 首期或首次支用 | 抓准率、误杀率 | 周或月 |
这张表里的门槛是行业里比较常见的取值,不是硬性标准。实际项目里更该关注的是同一张卡在不同客群上的表现分化,比如新客和存量客的 KS 差异,往往比整体 KS 更能说明问题。
2.2 特征取数:从征信变量到衍生变量
征信、多头、设备、收入负债是四类核心变量。取数时最容易被忽略的是时间对齐:所有特征只能用到申请时点之前的信息,任何跨越申请时点的聚合都会造成标签泄露。下面这段 SQL 是常见的征信查询类特征写法,重点看时间窗口是怎么卡住的。
-- 以申请件为基准,向前追溯 90 天的征信查询行为 WITH base AS ( SELECT apply_id, cust_id, apply_time, loan_amount, term FROM dm_apply_loan WHERE dt = '${bizdate}' -- 按日分区取当日申请件 ), credit AS ( SELECT l.cust_id, -- 近 90 天贷款审批类查询次数 SUM(CASE WHEN l.query_type = 'loan_approval' THEN 1 ELSE 0 END) AS loan_approval_cnt_90d, -- 近 90 天查询机构去重数,衡量多头程度 COUNT(DISTINCT l.query_org) AS query_org_cnt_90d, -- 近 30 天查询次数,短窗口更敏感 SUM(CASE WHEN l.query_time >= DATE_SUB(b.apply_time, 30) THEN 1 ELSE 0 END) AS query_cnt_30d FROM dwd_credit_query_log l JOIN base b ON l.cust_id = b.cust_id WHERE l.query_time < b.apply_time -- 严格早于申请时点 AND l.query_time >= DATE_SUB(b.apply_time, 90) -- 只取 90 天窗口 GROUP BY l.cust_id ) SELECT b.apply_id, b.cust_id, c.loan_approval_cnt_90d, c.query_org_cnt_90d, c.query_cnt_30d FROM base b LEFT JOIN credit c ON b.cust_id = c.cust_id;逻辑上分成三步:先把申请件落到 base,再在征信日志上做窗口聚合,最后左连接回申请件。参数上bizdate是跑批日期,query_type要和征信报文里的枚举对齐;LEFT JOIN保证没有征信记录的客户也保留,后续在模型里当作缺失值处理,而不是直接过滤掉——直接 inner join 会把一部分白户样本悄悄丢掉,导致模型对新客过度乐观。
2.3 建模前必须锁死的评估指标:KS、AUC、IV、PSI
模型上线前看区分度,上线后看稳定性,两套指标不能混着看。区分度里 KS 最直观,取的是好坏人累计分布差值的最大值;AUC 衡量排序能力,对样本不均衡不敏感;IV 用来筛变量,单变量 IV 低于 0.02 基本可以丢,高于 0.5 要警惕变量本身带着未来信息。稳定性用 PSI,监控的是分数分布或特征分布在两个时间窗之间的偏移。
| 指标 | 作用 | 常用阈值 | 越界后的动作 |
|---|---|---|---|
| KS | 模型区分度 | 0.3~0.5 较优 | 低于 0.25 考虑重构 |
| AUC | 排序能力 | 0.7 以上可用 | 与 KS 背离时查样本定义 |
| IV | 变量筛选 | 0.02~0.5 | 过高排查穿越变量 |
| PSI | 分布稳定性 | <0.1 稳定,>0.25 报警 | 触发重训或切回旧模型 |
这里要注意,KS 高不等于模型能用。银行场景里更怕的是不稳定的高 KS,比如某个变量在训练集里恰好区分了一批特定渠道的客户,上线后渠道结构一变,KS 掉得比谁都快。这也是为什么评分卡建模在银行里更偏向逻辑回归而不是复杂模型,可解释性和稳定性在多数时候优先于那零点零几的 AUC 提升。数学建模里追求拟合优度的思路,放到生产风控里要打个折扣。
2.4 样本与坏样本定义:观察期、表现期、账龄
坏样本定义直接决定模型在学什么。常见的口径是 DPD30+ 为坏、DPD1~29 为灰、正常还款为好,灰样本可以放回好样本也可以单独剔除,但必须在文档里写清楚。观察期用于取特征,表现期用于定标签,两者之间还要留一段账龄缓冲,否则新放款的客户还没到表现期就被打上「好」的标签,模型会系统性高估新客质量。另一个坑是样本时间跨度:如果训练样本跨了半年以上,要检查有没有混入政策调整前后的客户,必要时按时间切分做跨期验证,而不是随机切分。
3. 评分卡建模案例:从 WOE 分箱到逻辑回归落库
数据底座定好之后,模型部分反而相对标准化。银行评分卡的主流做法是 WOE 分箱加逻辑回归,原因是每个变量可以单独解释,分数可以拆到每一箱,审计和监管检查时能把「为什么拒」讲清楚。这条链路上真正费时间的不是拟合,而是分箱的单调性调整和系数符号校验。
3.1 变量分箱與 WOE 计算
先做等频或卡方分箱,再计算每个箱的 WOE 和 IV。下面这段代码是常见的实现骨架。
import numpy as np import pandas as pd def woe_iv(df, col, target, bins=5): """按分位数分箱,返回 WOE 映射表和 IV 值""" # 1. 分箱,重复边界用 drop_duplicates 去掉,避免空箱 df = df[[col, target]].copy() df['bin'] = pd.qcut(df[col], q=bins, duplicates='drop') # 2. 统计每箱好坏样本数 grouped = df.groupby('bin', observed=True)[target].agg(['count', 'sum']) grouped.columns = ['total', 'bad'] grouped['good'] = grouped['total'] - grouped['bad'] # 3. 加 0.5 平滑,防止某箱好或坏为 0 导致 log 无定义 grouped['bad_rate'] = (grouped['bad'] + 0.5) / (grouped['bad'].sum() + 0.5) grouped['good_rate'] = (grouped['good'] + 0.5) / (grouped['good'].sum() + 0.5) # 4. WOE = ln(坏占比 / 好占比),IV 为该箱 WOE 与占比差的乘积之和 grouped['woe'] = np.log(grouped['bad_rate'] / grouped['good_rate']) grouped['iv'] = (grouped['bad_rate'] - grouped['good_rate']) * grouped['woe'] return grouped, grouped['iv'].sum() # 示例:对近 90 天查询机构数计算 WOE 和 IV woe_table, iv = woe_iv(sample_df, 'query_org_cnt_90d', 'is_bad', bins=5) print(f"IV = {iv:.4f}")代码里三个参数最关键:bins控制分箱粒度,一般 4 到 8 箱,太少丢信息、太多不稳定;平滑系数 0.5 是经验值,样本量大时可以调小;target必须是 0/1 编码的坏样本标签。输出后要人工检查 WOE 是否单调,不单调的箱相邻合并,直到趋势一致,这一步没有捷径,靠的是对着 WOE 表一箱一箱看。
3.2 逻辑回归与系数校验
变量筛完后进逻辑回归。银行场景里通常不允许入模变量超过 10 到 15 个,变量太多会让评分卡难以维护,也容易带来共线性。
from sklearn.linear_model import LogisticRegression from statsmodels.stats.outliers_influence import variance_inflation_factor # X 是 WOE 转换后的变量矩阵,y 是坏样本标签 model = LogisticRegression(penalty='l2', C=1.0, solver='lbfgs', max_iter=500) model.fit(X_train, y_train) # 系数方向校验:WOE 定义下系数应为负,若为正说明该变量逻辑反常 coef = pd.Series(model.coef_[0], index=X_train.columns) print(coef.sort_values()) # 共线性检查,VIF 大于 5 的变量考虑剔除 vif = pd.DataFrame({ 'feature': X_train.columns, 'VIF': [variance_inflation_factor(X_train.values, i) for i in range(X_train.shape[1])] }) print(vif.sort_values('VIF', ascending=False))C越小正则越强,样本量小或变量多时可以降到 0.1 到 0.5 试;solver选 lbfgs 适合 L2 场景。系数校验要看两点:一是符号方向是否符合业务直觉,比如查询次数越多、坏概率应越高,在 WOE 编码下系数应为负;二是显著性,p 值过大的变量直接剔掉。系数方向反了的变量不要硬留,通常意味着这个变量在偷未来信息,或者分箱时把好坏顺序搞反了。
3.3 评分转换与分数落库
逻辑回归输出的是概率,决策引擎里更常用的是 300 到 850 之间的整数分,方便设阈值和做矩阵。转换公式是标准的 PDO 逻辑。
import numpy as np BASE_SCORE = 600 # 基准分 BASE_ODDS = 30 # 基准分对应的好坏比 PDO = 50 # 好坏比翻倍需要增加的分数 factor = PDO / np.log(2) # 约 72.13 offset = BASE_SCORE - factor * np.log(BASE_ODDS) # intercept 为逻辑回归截距,coef 为系数,woe_values 为当前客户各变量的 WOE score = offset - factor * (intercept + np.dot(coef, woe_values)) score = int(round(score))BASE_SCORE、BASE_ODDS、PDO三个参数决定分数刻度:PDO 越小,分数对风险变化越敏感,但也越容易抖动;常见取 20 到 50。转换出的分数要落一张分数映射表,把每个变量的每个箱对应多少分存下来,线上引擎按箱查表累加即可,这样既快又可解释。
| 参数 | 含义 | 常见取值 | 影响 |
|---|---|---|---|
| BASE_SCORE | 基准分 | 500~600 | 整体刻度起点 |
| BASE_ODDS | 基准好坏比 | 20~50 | 与基准分共同定位 |
| PDO | 翻倍分差 | 20~50 | 越小越敏感越易抖 |
落到引擎里时,分数通常和规则并列使用:规则负责硬性拦截,评分卡负责排序和额度定价。两者通过决策流串起来,下一章展开。
4. 决策流编排:规则优先级、命中返回与灰度上线
模型分只是决策流里的一个节点。真实的决策引擎要处理的是多个节点的执行顺序、短路逻辑和返回结果。判断一笔申请是拒是过,往往取决于哪条规则先命中,这个先后顺序就是salience或者说优先级,配错了会导致本该拒绝的件被低优先级规则放过去。
4.1 决策表与规则表达式
规则引擎常见两类写法:一类是 Drools 这种规则文件,适合规则多、需要集中管理的场景;另一类是 Aviator、QLExpress 这类表达式引擎,适合轻量嵌入。下面的表达式形式更贴近互联网银行的落地方式。
// Aviator 表达式:单条准入规则,返回命中结论 var deviceCnt = getFeature("device_cnt_7d"); // 近 7 天同设备申请数 var score = getModelScore("A_CARD_V3"); // 申请评分卡分数 var blackHit = getFeature("blacklist_hit"); // 黑名单命中标记 if (blackHit == 1) { return { decision: "REJECT", reason: "R001_BLACKLIST", priority: 100 }; } if (deviceCnt > 5 && score < 580) { return { decision: "REJECT", reason: "R101_DEVICE_CLUSTER", priority: 90 }; } if (score >= 580 && score < 640) { return { decision: "REVIEW", reason: "R201_SCORE_GRAY", priority: 50 }; } return { decision: "PASS", reason: "R999_DEFAULT", priority: 0 };priority越大的规则越先执行,命中即返回,这就是短路。getFeature和getModelScore是两个外部函数,前者走特征平台,后者走模型服务,都要设超时。参数里最容易出错的是deviceCnt的口径,如果特征平台按自然日算、规则按滚动 7 天理解,同一条规则在不同时间跑出来的结果会不一样。
4.2 决策流结构:串行节点与短路返回
一个可维护的决策流一般包含四类节点,按顺序串行执行,遇到拒绝直接短路返回。
| 节点类型 | 输入 | 输出 | 典型超时 | 失败兜底 |
|---|---|---|---|---|
| 特征节点 | 客户号、设备号 | 特征 KV | 80ms | 返回默认值并降级 |
| 规则节点 | 特征、名单 | 通过/拒绝/人工 | 20ms | 放行到下一节点 |
| 模型节点 | WOE 变量 | 模型分 | 100ms | 切备用模型或走规则 |
| 决策节点 | 上游结论 | 额度、利率、期限 | 30ms | 转人工 |
超时和兜底策略要和业务方一起定。特征节点失败时如果直接拒绝,等于把系统故障成本转嫁给客户,通常做法是返回默认值继续往下走,同时打点告警;模型节点失败则切到备用版本,保证决策不中断。
4.3 模型分落入决策流与阈值切分
模型分进入决策流后,不是简单卡一条线,而是要和规则结果交叉。常见做法是分区间处理:高分直接过,中间分段进人工或降额,低分拒。切分点参考分数分布和通过率目标来定,而不是拍脑袋。上线前要跑一遍历史样本,看新策略下的通过率和预期坏账率相对旧策略的变化,这一步叫策略回溯。
4.4 冠军挑战者灰度与上线验证
新策略上线不建议全量切换,常见做法是冠军挑战者:旧策略为冠军,新策略为挑战者,按比例分流,比如 90% 走旧、10% 走新,对比两组的通过率、首逾率和人工转单率。
| 分流比例 | 观察指标 | 观察周期 | 回滚条件 |
|---|---|---|---|
| 5%~10% | 通过率、首期逾期率 | 2~4 周 | 首逾率上升超 20% |
| 30% | 各分段表现、KS | 4~8 周 | KS 下降超 0.05 |
| 50%~100% | 全量坏账、PSI | 3 个月以上 | PSI 持续大于 0.25 |
灰度期间最忌讳只看整体指标,一定要拆到渠道、客群、分数段去看,否则很容易被结构性差异掩盖。回滚条件提前写进配置,触发即自动切回,比事后开会决定快得多。
5. 上线后的监控、回滚与阈值调优实战技巧
策略上线的当天才是运维的开始。日常巡检里最先看的是分数分布的 PSI,而不是坏账率,因为坏账有滞后性,PSI 能提前几周给出信号。下面这段代码可以直接挂到跑批任务里。
import numpy as np def psi(expected, actual, bins=10): """计算两个分数分布的 PSI,expected 为基准期,actual 为当前期""" # 用基准期的分位点切分箱,保证两期箱边界一致 breakpoints = np.percentile(expected, np.linspace(0, 100, bins + 1)) breakpoints[0], breakpoints[-1] = -np.inf, np.inf exp_pct = np.histogram(expected, bins=breakpoints)[0] / len(expected) act_pct = np.histogram(actual, bins=breakpoints)[0] / len(actual) # 防止某箱为 0,加极小值 exp_pct = np.where(exp_pct == 0, 1e-6, exp_pct) act_pct = np.where(act_pct == 0, 1e-6, act_pct) return np.sum((act_pct - exp_pct) * np.log(act_pct / exp_pct)) # 基准期用上线首月的分数,当前期用最近一周 print("PSI =", round(psi(base_scores, recent_scores), 4))分箱边界必须用基准期的分位点,两期用同一套边界,否则 PSI 会被箱边界差异污染。一般 PSI 小于 0.1 视为稳定,0.1 到 0.25 之间需要排查原因,超过 0.25 就考虑重训或切回旧模型。排查顺序是先看特征分布,再看客群结构,最后才怀疑模型本身。
5.1 阈值与拒绝率的联动调优
阈值调优不是单独动一个数,而是和通过率、人工转单率、预期坏账一起看。下面这张表是调优时常用的对照思路。
| 评分阈值 | 预期通过率 | 人工转单率 | 预期首逾率 | 适用阶段 |
|---|---|---|---|---|
| 620 | 约 65% | 8% | 1.2% | 冲量期 |
| 640 | 约 50% | 10% | 0.9% | 常规期 |
| 660 | 约 35% | 12% | 0.7% | 收紧期 |
每次调阈值只动一个变量,调完观察至少一个完整表现周期再决定下一步。同时调阈值和规则,出了问题根本分不清是谁的影响。
5.2 回滚与应急的落地细节
回滚要能在分钟级完成,做法是把策略版本、模型版本、阈值都做成配置项,引擎按版本号加载,出问题时切版本号即可,不需要重新发版。灰度期间保留旧版本常驻内存,切换时只改路由权重。另一个技巧是给每条规则和每个模型节点加命中计数,回滚后对比命中明细,能快速判断是数据问题还是策略问题。把回滚开关做成配置项,比事后写复盘报告有用得多。
本文还有配套的精品资源,点击获取