☰
银行信用卡风险评估模型设计:四模型全链路风控解析
2026/10/13 19:37:43 网站建设 项目流程

简介:面向银行信用卡的风险评估模型设计项目,提供申请人评级、行为评级、收款(催收)评级与欺诈评级四个完整模型的Python源码及文档说明,适合模式识别课程设计、银行风控算法学习或相关课题研究参考。整个压缩包共18个文件,主要包含8个CSV数据文件、4个Python脚本、4个pkl模型文件,另附SimHei字体与README说明,包体7.67MB,结构清晰便于快速定位代码、数据与结果。项目中申请人评级模型基于个人与财务信息评估违约可能性,行为评级模型利用还款与逾期历史预测信用表现,收款评级模型面向催收策略输出,欺诈评级模型则识别欺诈特征、预防新客户风险;实施过程覆盖数据预处理(缺失值、重复值、异常值处理及特征选择)、模型构建(随机森林、降维等)与交叉验证评估。资源对数据理解、特征工程、模型调优的流程有完整呈现,可帮助读者掌握从原始数据到风险评估模型的落地思路。目前已有130人学习下载。

1. 为什么四个风险评估模型不能合并成一个

别急着打开源码,先想一个实际问题:一家银行信用卡的逾期率突然抬头,你最该怀疑哪个环节?是进件质量变差、额度放得太松、催收只顾上了头尾,还是有人在批量骗贷?四个原因对应四套完全不同的决策流程。模式识别课程里做风险评估模型设计,新手最容易踩的坑就是拿一份数据、套一个二分类,把“会不会逾期”当成了唯一答案——上线后逾期没降,拒绝率却先爆了。

这套面向银行信用卡的风险评估模型设计,把信贷业务拆成申请人评级、行为评级、收款评级(催收评级)和欺诈评级四个模型,分别回答“敢不敢借”“额度怎么调”“逾期怎么催”“是不是坏人”。课程配套的python源码和文档说明,覆盖了从特征工程、分箱、模型训练到策略映射的完整链路。适合正在做风控建模入门、需要完成课程设计,或想在银行与消金机构快速搭起一套可解释风控基准的读者。

2. 把四个模型放进信贷流水线:谁吃谁的数据、出什么分、给谁用

信贷产品的完整生命周期可以压缩成四段:贷前、贷中、贷后,外加贯穿全程的反欺诈。风险评估模型设计,本质上是把每一段“要不要做、做多少”的决策问题形式化。这章先不放代码,而是把四个模型的对象、特征、算法和产出讲清楚,这样你后面跑python源码时,每一步都能对应回业务:改的是哪段决策,调的是哪个目标函数。

模型所处阶段常见算法典型输出主要决策
申请人评级贷前逻辑回归评分卡申请评分自动通过、人工复核、拒绝
行为评级贷中LightGBM / 逻辑回归行为评分调额、降额、利率定价
收款评级贷后梯度提升多分类催收等级 A-D催收动作、是否委外或法务
欺诈评级全周期孤立森林 + XGBoost欺诈风险标签拦截、二次核身、转人工

2.1 申请人评级模型:贷前准入这道闸,管的是“敢不敢借”

业务定义:客户刚进件时,提交身份、职业、收入、资产、负债和征信授权。银行要在没有任何本行行为数据的冷启动阶段判断违约概率。申请人评级模型输出通常落在300到900分,也常被称为申请评分卡,业务侧用分段阈值把进件分成自动拒绝、人工复核和自动通过三档。

输入特征以静态申请信息和征信快照为主:年龄、学历、职业、单位性质、月收入、负债收入比(DTI)、征信查询次数、已有信贷账户数、历史最长逾期天数。其中近3个月征信查询次数通常是区分度很高的强特征,能捕捉多头借贷,但也容易误伤单纯对比利率的正常申请人,分箱时要留意单调性。

算法选型上,行业基准是逻辑回归加WOE分箱,导出标准评分卡。原因有三:第一,监管和审计要求拒绝原因必须能解释,线性模型天然给得出;第二,申请数据相对稀疏,非线性模型带来的提升有限,却会让合规解释成本翻几倍;第三,评分卡可以直接落成政策条款,送审时不用再做一层翻译。这套课程源码里,申请人评级部分用的正是WOE特征加逻辑回归,属于模式识别里最典型的线性分类落地方式。

2.2 行为评级模型:贷中动态调额,看的是“还在不在风险边界内”

批卡之后,客户每个月都会产生消费、还款、分期、取现行为。行为评级模型用存量客户的月度行为快照,预测未来3到6个月发生M1以上逾期的概率,输出行为评分。业务侧拿它做额度调整、利率定价、降额甚至封卡决策。

和申请人模型最大的区别是数据形态:申请模型是静态快照,行为模型是滚动窗口。行为特征绝大多数是时间窗口聚合量,比如近1个月、3个月、6个月的消费笔数、消费金额中位数、提现占比、最低还款次数、历史逾期累计天数、额度使用率。额度使用率是行为模型里区分度最高的单变量,长期接近满额使用的客户,资金链紧张的信号往往很早就出现了。

算法选型上,行为评分主要给内部风控使用,对外解释压力没有申请评分卡大,所以可以接受LightGBM这类梯度提升模型,把排序能力做上去,前提是做好特征监控和版本回退。如果所在机构合规条线盯得紧,稳妥做法仍是逻辑回归做基准,再用树模型做影子分,观察不稳定客群的覆盖差异。这种“对外用评分卡、对内用黑匣子”的双轨做法,我在真实风控团队里见过很多次,课程源码一般也把LightGBM的完整训练骨架放在行为评级这一节。

2.3 收款评级模型:逾期之后不是“一催了之”,而是给资源排队

客户逾期后,催收资源是有限的,高频人工催收成本高,而且对轻度逾期客户体验伤害大。收款评级模型(课程文档里通常称为催收评级模型)在贷后环节输出催收等级或回收概率,决定每个逾期客户投入多少催收强度。常见的分档是轻逾期、中风险、重风险和失联高危四类,对应策略从短信、自动外呼、普通人工、专业催收到法务或外包。

输入特征不能只看逾期天数和逾期金额这两个硬指标。还需要历史还款意愿指标、最近一次承诺还款是否兑现、近7天联系方式接通率、地址稳定性、同设备关联的其他逾期账户数。失联风险是这个模型的尾部重点,评分校准往往要在高分位单独做,因为真正拉高损失的不是普遍的轻度逾期,而是那批彻底失联的客户。

算法上我建议把问题做成有序多分类,而不是回归预测回收金额。回收金额分布是典型的零膨胀加右偏,直接回归会把所有客户都往低分堆,分不出等级。把逾期严重度编码成0、1、2、3四级有序目标,用梯度提升分类或逻辑回归多分类,训练完成后接一张策略矩阵,把分数换算成动作。这里有一个容易被低估的细节:催收人员只关心“我下一步打电话说什么”,所以模型输出必须落到动作,不能只给一个干巴巴的数字。

2.4 欺诈评级模型:骗贷和违约是两个物种,别塞进同一个目标

欺诈的意图是“从银行拿钱后根本没打算还”,而且经常是团伙化、批量化的,会针对风控规则持续试探和进化。欺诈评级模型识别两类场景:申请环节的伪冒进件,以及账户存续期的伪冒交易。输出欺诈风险分或风险标签,直接触发拦截、二次核身或转人工。

为什么不能和申请人评级模型合并?信用风险评估的是还款能力和意愿的概率分布,欺诈样本在信用模型里只是统计噪声;欺诈则是对规则的有组织攻击,特征分布会随时间快速漂移。合在一起训练,等于用慢变量去拟合快变量,欺诈样本的信号会被海量正常样本淹没。

欺诈模型的特征维度也完全不同:设备指纹、IP地址风险、注册填单时长、同设备关联账户数、异常行为轨迹、交易时间与地理位置的跳变跨度。这些特征在信用评分卡里通常没有位置。算法上,由于欺诈样本稀疏且标签获取滞后,常见做法是先上规则引擎和孤立森林这类无监督异常检测做初筛,再用XGBoost在有标签样本上做监督分类。评价指标要盯精确率和误杀率,AUC在这种场景下偏乐观,因为正常样本基数太大。如果源码里用SMOTE做重采样,你就会看到它对缓解极端不平衡的作用,也会踩到它放大噪声的坑,这一点放到避坑章节细说。

3. 用Python源码复现四模型之前,先把数据地基打牢:清洗、WOE与时间切分

大部分课程项目源码里,四个模型的训练代码加起来可能不到两百行,真正的体量全在数据处理。这章给出三块必做的地基:宽表清洗、WOE分箱与IV筛选、时间口径切分。这三块写对了,后面四个模型能共用一套数据接口,不用每个模型单独返工。

3.1 宽表清洗:缺失率阈值、中位数填充和唯一值列

真实环境里,申请数据和行为数据分散在数仓的多张表里,第一步永远是加工成“每行一个样本”的建模宽表,而不是直接拿原始流水训练。

import pandas as pd def prepare_credit_wide(raw_df: pd.DataFrame, target_col: str = "bad_flag") -> pd.DataFrame: """把零散字段清洗成建模宽表:去掉高缺失列和唯一值列,填充缺失。""" df = raw_df.copy() # 1) 缺失率超过80%的列直接废弃,保留太多空列只会引入噪声 high_missing = [c for c in df.columns if df[c].isna().mean() > 0.8] # 2) 只有单一取值的列没有区分能力,留着会干扰特征筛选 constant_cols = [c for c in df.columns if df[c].nunique() == 1] df.drop(columns=list(set(high_missing + constant_cols)), inplace=True) # 3) 数值列用中位数填充,避免均值被极端值带偏;类别列用众数填充 num_cols = df.select_dtypes(include=["number"]).columns.difference([target_col]) cat_cols = df.select_dtypes(include=["object", "category"]).columns df[num_cols] = df[num_cols].fillna(df[num_cols].median()) df[cat_cols] = df[cat_cols].fillna(df[cat_cols].mode().iloc[0]) return df

这段代码有三个参数值得在跑源码前先理解。缺失率阈值0.8是我的经验边界,低于这个阈值的信息量太少,填充反而是在伪造分布。中位数填充比均值稳健,适合收入、消费金额这类典型偏态字段。类别列的众数填充要留意一种情况:如果真实线上会出现训练集里没见过的类别,必须在填充里额外补一个字符串桶,否则上线时特征编码会直接报错。

3.2 WOE分箱与IV筛选:把连续值变成单调的信用信号

WOE(Weight of Evidence)是评分卡建模最核心的一步,它把原始特征切成若干箱,再用每箱好客户与坏客户的对数比替换原始值。这样做的价值有两个:一是让逻辑回归直接吃“信用信号”而不是原始数字,二是天然处理了缺失和异常值。

import numpy as np import pandas as pd def woe_bin_iv(df: pd.DataFrame, feature: str, target: str, bins: int = 10) -> pd.DataFrame: """等频分箱并计算WOE、IV。返回值每行对应一个分箱。""" df = df.copy() # 连续型字段用等频分箱,类别型字段按原始取值直接分箱 if df[feature].nunique() > bins: df["bin"] = pd.qcut(df[feature], q=bins, duplicates="drop") else: df["bin"] = df[feature].astype(str) grouped = df.groupby("bin", observed=True)[target].agg(["sum", "count"]) grouped["bad"] = grouped["sum"].astype(int) grouped["good"] = grouped["count"] - grouped["bad"] grouped["bad_rate"] = grouped["bad"] / grouped["count"] total_bad = grouped["bad"].sum() total_good = grouped["good"].sum() # 1e-6 防止某一个箱坏客户为0时除零 grouped["woe"] = np.log((grouped["good"] / total_good + 1e-6) / (grouped["bad"] / total_bad + 1e-6)) grouped["iv"] = (grouped["good"] / total_good - grouped["bad"] / total_bad) * grouped["woe"] return grouped

参数bins=10是等频分箱的常用起点。等频分箱保证每个箱的样本量接近,相比等宽分箱对长尾字段更稳。WOE为正说明这一箱好客户占比高于总体,为负说明坏客户集中在这个箱,业务上能直接读出“哪一段区间的人群风险高”。IV则是每个分箱WOE的加权和,经验阈值是:小于0.02基本无用,0.02到0.1弱区分度,0.1到0.3中等,大于0.3强区分但也要警惕过拟合。看到单变量IV超过0.5时,先检查这个特征是否泄露了未来信息。

提示:在做完整建模前,先把所有候选特征的IV算一遍,保留大于0.02的字段。这份筛选结果可以当作文档说明里的特征清单,也是向业务方解释“为什么用这些字段”的重要依据。

3.3 时间口径切分:用“观察期+表现期”防止时间穿越

风控建模里最隐蔽的错误是训练集和验证集来自同一个时间带。如果随机切分,模型会“看见未来”,验证集表现相当漂亮,上线后区分度立刻掉档。申请人模型要用观察点加表现期的严格口径:观察点时间t之前的信息作为特征,t之后一个表现期(如6个月)内是否逾期作为标签。

from sklearn.model_selection import TimeSeriesSplit def time_aware_split(df: pd.DataFrame, time_col: str, n_splits: int = 5): """按时间顺序切分,只允许过去训练、未来验证。""" df = df.sort_values(time_col).reset_index(drop=True) tscv = TimeSeriesSplit(n_splits=n_splits) for train_idx, val_idx in tscv.split(df): yield df.iloc[train_idx], df.iloc[val_idx]

这段代码的逻辑很简单:先把数据按观察点时间排序,再用TimeSeriesSplit生成训练集和验证集,训练集永远在时间上早于验证集。行为模型尤其依赖这种切分方式,因为你预测的是客户未来几个月的行为,而不是同一时期的行为模式。跑源码时如果发现验证集KS比训练集高,先怀疑标准化的fit过程是否用了全量数据,这是最常见的“时间穿越”来源。

4. 四个模型的Python源码骨架:算法选型、关键参数与可复现训练代码

这一章给出四个模型可以直接在本机跑通的训练骨架。真实的银行风控系统还会在外面包一层调度、权限和策略引擎,但用课程源码练手,这些骨架已经能支撑完整的实验和汇报。每个模型配一组关键参数说明,参数值以能复现、能调优为准。

4.1 申请人评级模型:逻辑回归评分卡与KS验证

import joblib from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score from sklearn.model_selection import train_test_split def ks(y_true, y_prob): """计算KS统计量:好客户和坏客户累计分布的最大差值。""" from sklearn.metrics import roc_curve fpr, tpr, _ = roc_curve(y_true, y_prob) return float(max(tpr - fpr)) def build_application_scorecard(X_woe, y, test_size=0.3): X_train, X_val, y_train, y_val = train_test_split( X_woe, y, test_size=test_size, stratify=y, random_state=42 ) model = LogisticRegression( C=1.0, # 正则化强度,调参范围通常 0.01 ~ 10 penalty="l2", solver="lbfgs", # 中小特征规模用 lbfgs,特征上万时换 liblinear 或 saga class_weight="balanced", max_iter=1000, ) model.fit(X_train, y_train) prob = model.predict_proba(X_val)[:, 1] print(f"AUC={roc_auc_score(y_val, prob):.4f} KS={ks(y_val, prob):.4f}") joblib.dump(model, "application_scorecard.pkl") return model

这里的关键参数是class_weight="balanced"。当坏样本率低于10%时,如果不做类别权重处理,模型会倾向于把所有客户都判成好客户,评分分布会挤在高分段。C控制正则化强度,C越小对特征的惩罚越大,评分卡的系数就越平滑,一般从1.0起步,网格搜索C=0.1、1、10三档。KS到0.3以上说明好坏分布有足够分隔,但KS一旦超过0.5就要怀疑数据泄露,“高得吓人”在风控里通常是bug而不是好消息。

4.2 行为评级模型:LightGBM滚动特征与时间序列切分

import lightgbm as lgb def build_behavior_model(X, y): params = { "objective": "binary", "metric": "auc", "learning_rate": 0.05, "num_leaves": 31, # 控制复杂度,过大的 num_leaves 是过拟合第一来源 "max_depth": -1, # 让树自由生长,但受 num_leaves 约束 "min_child_samples": 50, # 限制叶子最小样本量,防噪声 "lambda_l2": 1.0, # L2 正则,行为特征之间相关性高,需要系数惩罚 "feature_fraction": 0.8, # 列采样,提高泛化 "bagging_fraction": 0.8, "bagging_freq": 1, "n_estimators": 500, "verbose": -1, } model = lgb.LGBMClassifier(**params) model.fit(X, y) return model

LightGBM的行为模型训练里,我一般会把学习率降到0.05而不是默认的0.1,配合500棵树让模型慢慢收敛。num_leaves=31是偏保守的起点,特征几十维时够用;如果特征上百维,可以往上调到63甚至127,但要同步提高min_child_samples到80以上,否则很容易记住个别人的还款习惯。行为模型的训练集必须用上一章的时间切分,不要用train_test_split随机切。

4.3 收款评级模型:有序多分类与催收策略映射

from sklearn.ensemble import GradientBoostingClassifier def build_collection_model(X, y): """y 取 0(轻)/1(中)/2(重)/3(高危) 四档,输出四类概率供策略映射。""" model = GradientBoostingClassifier( n_estimators=300, learning_rate=0.1, max_depth=3, subsample=0.9, min_samples_leaf=30, # 每片叶子至少 30 个样本,避免小分桶噪声 random_state=42, ) model.fit(X, y) return model def collection_policy_map(prob_matrix, thresholds=(0.4, 0.6, 0.8)): """把四类概率转成催收等级,再叠加不确定性判断。""" pred_class = prob_matrix.argmax(axis=1) pred_prob = prob_matrix.max(axis=1) return pred_class, pred_prob

催收评级要的是“分得开”而不是“算得准”。用GradientBoosting而不是回归,是因为回收金额零膨胀,回归会把多数客户都压到同一个低分组,分不出行动优先级。min_samples_leaf=30是防止某个逾期分档样本太少时模型学到纯噪声。训练完之后,策略层拿到的应该是四档概率,而不是单一分数。概率最大的档位决定催收动作,最大概率本身决定动作要不要打折执行——这是模型与策略解耦的关键。

4.4 欺诈评级模型:孤立森林初筛加SMOTE重采样

from sklearn.ensemble import IsolationForest from imblearn.over_sampling import SMOTE def build_fraud_pipeline(X_fraud, y_fraud, contamination=0.05, smote_ratio=0.2): # 第一步:无监督初筛,标记可疑样本供人工复核 iso = IsolationForest( contamination=contamination, # 期望的可疑比例,不是标签比例 n_estimators=300, max_samples=256, random_state=42, ) iso_pred = iso.fit_predict(X_fraud) suspected = X_fraud[iso_pred == -1] # 第二步:对已标注欺诈样本做少量重采样,缓解极端不平衡 smote = SMOTE(random_state=42, sampling_strategy=smote_ratio) X_res, y_res = smote.fit_resample(X_fraud, y_fraud) return iso, suspected, X_res, y_res

这里两个参数最容易翻车。contamination是期望数据集中可疑样本的占比,如果设成0.01而实际欺诈率是0.001,初筛结果里正常客户会占绝大多数,相当于把几十倍的误杀量引到后续流程。smote_ratio=0.2表示把少数类样本扩到多数类的20%,而不是完全平衡。完全平衡会让模型为了拟合人工合成的中间样本而放宽决策边界,误杀率暴涨。欺诈模型上线前,最该画的一条曲线是“误杀率-召回率”曲线,而不是只看AUC。

5. 项目实战避坑:从训练到上线的常见翻车点

课程源码能跑通,不代表模型上线不出事。下面这几条踩坑记录按“现象、原因、解决”的顺序写,几乎每个真实风控项目里都能碰上几条。你可以把它当调试清单用,也可以直接补进自己的文档说明。

5.1 训练与数据切分期的“时间穿越”

现象:申请人模型验证集AUC高达0.85,上线后实际拒绝率和逾期率双双偏离预期,脱敏测试没过。

原因:最常见的是标准化时用了全量数据的均值方差,val字段混入了train这段未来信息;另一个高频原因是用随机切分,同一个月进件的样本同时出现在训练集和验证集里。模型看到的不是规律,而是同一时间段里的偶然相似。

解决:标准化只能用训练集fit再transform验证集。切分改用第3章的time_aware_split,按观察点时间严格排序。一句话判断标准:验证集必须模拟“上线那一刻”的真实时间点,任何发生在验证集时点之后的信息都不许进入训练过程。

5.2 WOE最优分箱过拟合,上线后PSI失控

现象:某个特征训练时IV高达0.4,上线一个月后PSI超过0.2,评分分布明显漂移,却没找到业务上的渠道变化。

原因:pd.qcut等频分箱在高分位会自动切出很细的箱,这些箱在历史样本里刚好把好坏客户分得很开,但新数据稍微偏移一点就全乱套了。最优分箱拟合的是历史噪声,不是业务规律。

解决:分箱时给每箱设最小样本占比,比如5%,违反单调性的相邻箱要合并。更重要的习惯是:分箱边界一旦确定,写进配置文件固定下来,月度监控箱级漂移,而不是每次训练都让计算机重新自动分箱。

5.3 SMOTE放大欺诈噪声,误杀率暴涨

现象:欺诈模型召回率上升了,但正常客户的拦截比例跟着翻倍,一批老客户反复被要求二次核身,客诉飙升。

原因:欺诈样本本来就少,SMOTE在特征空间里做线性插值,会生成大量不像真实坏人的合成样本。分类器为了拟合这些点,把决策边界放宽了,结果是误杀率被“硬拉”上去了。

解决:sampling_strategy不要设成平衡,保持在0.1到0.2之间,保留正常样本的主导地位。重采样后不看AUC,用精确率、误杀率、召回率三条曲线重新选阈值,一般会在误杀率曲线上看到一个明显的拐点。

5.4 行为评级与调额策略形成正反馈,坏账反而跟涨

现象:行为评分上升后,系统自动给客户提额,三个月后这批客群的逾期率比提额前还高。

原因:模型输出参与策略后改变了客户的负债行为。提额推高了额度使用率,而额度使用率恰恰是行为模型的核心输入,模型分数继续上升,又触发新一轮提额,形成闭环正反馈。这是风控领域常说的“模型改变业务,业务又改变模型”。

解决:上线前用历史策略做反事实回测,给单次提额设比例上限和客户级总额度上限。把额度使用率这类内生特征纳入漂移监控,一旦发现它在某个客群连续抬升,就要人工复核策略参数,而不是等着模型自己纠偏。

5.5 催收评级输出没有落成动作,业务方不执行

现象:催收模型分数排序看着合理,但一线团队还是凭经验打电话,或者干脆按逾期天数从大到小排,模型成了数据团队的自我欣赏。

原因:模型输出的是0到1的分数,没有翻译成催收动作。催收人员早上打开工作台,不知道“这个分数应该让我做什么”,最后只能回到老一套经验。

解决:把分数映射成催收策略矩阵。比如A档不催或短信提醒,B档AI外呼两次,C档人工电话每周两通,D档直接转专业催收或委外。每档都配联系人频次上限和承诺还款后的观察期,模型与策略解耦后,就算催收团队人力收缩,只要调整映射表,不需要重新训练模型。

注意:以上第五条在项目里最容易被忽略,因为代码只做到“训练完成、准确率达标”就停了,交付物里没有策略矩阵。课程文档说明里这种映射表一般会作为业务交付的最后一页,别跳过。

6. 把四个模型连成一套模型族:评分对齐、监控报表与回退技巧

四个模型独立跑通只是第一步,真正让它值回票价的,是把它们汇成一套能共同决策的模型族。我的做法是先统一四个模型的输出口径:申请评分为0到1000,行为评分同尺度,催收等级映射成对应分数区间,欺诈模型输出风险标签分数。这样策略层可以画一张联合决策矩阵,比如申请分高但欺诈分也高的客户,不能直接通过,要进人工复核。

上线后的日常维护,我只看三张报表。第一张是日粒度的KS监控,看四个模型还有没有区分能力;第二张是PSI,监控评分分布有没有漂移,细分到每个分箱;第三张是等级迁移矩阵,看上个月的低风险客群有多少滑入了中高风险。行为模型尤其要看迁移矩阵,它比PSI更能提前暴露调额策略的正反馈问题。

还要常备一组“后悔药”:每一版模型上线前,保留一份规则评分作为兜底。灰度期如果新模型评分聚集或线上指标异常,可以一键切回规则版本,不用等模型重新训练。我自己经历过行为模型灰度期间评分分布突然偏移,全靠预埋的规则回退兜住,那次之后就把“先有回退方案再上线”写成了团队红线。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询