news 2026/10/14 8:58:56

基于机器学习的银行信用卡违约预测:从数据清洗到模型上线的完整流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于机器学习的银行信用卡违约预测:从数据清洗到模型上线的完整流程

简介:这份PDF文献面向金融风控从业者、数据挖掘学习者及高校相关专业师生,聚焦银行信用卡业务中高违约率带来的风险难题,系统讲解如何用机器学习算法构建违约预测模型以辅助风险管理。资源包为单一PDF文件,共1个文件,大小约457KB,内容完整收录了对外经济贸易大学单华玮发表于《数据挖掘》期刊的论文全文,含中英文摘要、引言、相关工作与实验分析等章节。文中通过逻辑回归、决策树、随机森林、AdaBoost和梯度提升决策树五类算法建模,并对比不同特征选择方式下的预测效果,得出过滤式特征选择适应性更强的结论,可为读者提供完整的算法选型思路、特征工程方法与模型评价参考。目前已有938人学习,适合希望深入理解机器学习在信用违约预测中落地路径的读者研读。

1. 从一份 PDF 标题说起:银行信用卡违约预测到底在预测什么

信用卡违约预测,说白了就是回答一个很具体的问题:这个持卡人下个月会不会不还钱。银行信用卡中心每天面对几十万笔账单,真正让人头疼的不是正常还款的人,而是那批"再刷一笔就彻底还不上"的客户。传统做法靠风控规则表,比如"逾期超过 60 天且额度使用率大于 90% 就降额",但规则是人工拍的,覆盖不全,也跟不上客群变化。机器学习切入这件事的价值在于:它不依赖人写规则,而是从历史账单和还款行为里自己找模式,输出一个 0 到 1 之间的违约概率,风控再按概率分层处理。

这篇笔记围绕"基于机器学习的银行信用卡违约预测"这个方向展开,面向的是想拿它做课程设计、想复现一套完整流程、或者想搞清楚生产级风控建模和玩具数据集差距的从业者。我会按真实项目顺序讲:数据长什么样、特征怎么造、模型怎么选、阈值怎么定、上线前怎么验证。数据集用公开的信用卡违约数据集(常见的是台湾某银行 2005 年的 30000 条样本,23 个特征),这套数据在机器学习课程设计选题里出现频率极高,但真正把它做扎实的人不多。

2. 数据到手先别急着建模:信用卡违约数据的字段结构与清洗顺序

2.1 23 个字段里哪些是"雷",哪些是"金"

公开的信用卡违约数据集通常包含这样几类字段:ID、违约标签(default payment next month)、额度(LIMIT_BAL)、性别、教育、婚姻、年龄,以及 6 个月的还款状态(PAY_0 到 PAY_6)、6 个月的账单金额(BILL_AMT1 到 6)、6 个月的还款金额(PAY_AMT1 到 6)。很多人拿到数据第一反应是直接丢进模型,结果 AUC 卡在 0.75 上不去,问题往往出在没搞清字段语义。

还款状态字段 PAY_x 的取值是 -2 到 8,其中 -2 表示无消费、-1 表示正常还款、0 表示使用循环信用、1 到 8 表示逾期月数。这里有个血泪经验:-2 和 -1 在业务上完全不同,但很多教程直接把它们当连续数值处理,模型会误以为 -2 比 -1 小所以更"安全",实际上 -2 是没消费,风险画像和正常还款的人不一样。正确做法是把 PAY_x 当有序类别,或者做 one-hot,或者至少把 -2 单独标记出来。

教育字段也有坑。原始编码是 1=研究生、2=大学、3=高中、4=其他,但数据里出现了 0、5、6 这些未定义值。婚姻字段同理,1=已婚、2=单身、3=其他,也有 0 出现。这些脏值不处理,模型会学出莫名其妙的模式。

2.2 清洗与标签对齐的可复现步骤

下面这段代码是我一般会先跑的清洗流程,逻辑是:修正未定义类别、把还款状态转成有序数值、检查标签分布。

import pandas as pd import numpy as np # 读取数据,列名按公开数据集惯例 df = pd.read_csv("credit_default.csv") # 1. 修正教育字段的未定义值:0,5,6 归入"其他"(4) df["EDUCATION"] = df["EDUCATION"].replace({0: 4, 5: 4, 6: 4}) # 2. 修正婚姻字段的未定义值:0 归入"其他"(3) df["MARRIAGE"] = df["MARRIAGE"].replace({0: 3}) # 3. 还款状态字段:保留原始有序含义,-2 单独标记 pay_cols = ["PAY_0", "PAY_2", "PAY_3", "PAY_4", "PAY_5", "PAY_6"] for c in pay_cols: # 把 -2 映射为 -1 之外的一个独立类别,避免和正常还款混淆 df[c] = df[c].apply(lambda x: -1 if x == -2 else x) # 4. 检查标签分布 print(df["default payment next month"].value_counts(normalize=True))

这段代码的关键参数说明:replace里的映射关系必须和业务口径对齐,不能随手改;PAY_x的 -2 处理方式会影响后续是否做 one-hot,如果做树模型,映射成独立整数就够了,如果做逻辑回归,建议对 PAY_x 做 one-hot。标签分布打印出来通常是违约占 22% 左右,属于中度不平衡,后面阈值选择要考虑到这一点。

清洗完还要做一件事:检查有没有重复 ID、有没有账单金额为负的异常。账单金额理论上可以是负的(溢缴款),但连续 6 个月都是大额负数,基本可以判定是异常样本,我一般会直接剔除。

3. 特征工程才是分水岭:从原始账单到风控可解释变量

3.1 为什么原始 23 个字段不够用

直接把 23 个原始字段丢进模型,树模型大概能到 0.76 到 0.78 的 AUC,逻辑回归更低。但风控场景里真正有区分度的不是"上个月账单多少",而是"额度使用率""还款覆盖比""逾期趋势"这类衍生指标。额度使用率 = 账单金额 / 额度,反映客户对信用的依赖程度;还款覆盖比 = 还款金额 / 账单金额,反映还款意愿和能力;逾期趋势 = 最近一个月还款状态减去六个月前还款状态,反映风险是在恶化还是在改善。

这些衍生特征在业务上可解释,在模型里也能显著提升区分度。我实测过,加上这几个特征后,LightGBM 的 AUC 能从 0.77 提到 0.81 左右,KS 从 0.38 提到 0.45。这不是玄学,是因为原始字段把"额度"和"账单"分开看,模型要自己学交互,而树模型在样本量不够大时学交互效率不高。

3.2 构造风控衍生特征的代码与参数

# 额度使用率:最近一个月账单 / 额度 df["utilization"] = df["BILL_AMT1"] / (df["LIMIT_BAL"] + 1) # 平均还款覆盖比:6 个月还款总额 / 6 个月账单总额 bill_sum = df[[f"BILL_AMT{i}" for i in range(1, 7)]].sum(axis=1) pay_sum = df[[f"PAY_AMT{i}" for i in range(1, 7)]].sum(axis=1) df["pay_ratio"] = pay_sum / (bill_sum + 1) # 逾期趋势:PAY_0 - PAY_6,正数表示恶化 df["delinq_trend"] = df["PAY_0"] - df["PAY_6"] # 最大逾期月数 df["max_delinq"] = df[pay_cols].max(axis=1) # 还款金额波动:6 个月还款金额的标准差 df["pay_std"] = df[[f"PAY_AMT{i}" for i in range(1, 7)]].std(axis=1)

参数说明:分母加 1 是为了避免除零,这是工程习惯,不是数学必须。pay_ratio如果大于 1,说明还款超过账单,可能是提前还款或溢缴,这类客户风险通常较低,但要注意极端值,建议做 1% 和 99% 分位截断。delinq_trend的取值范围是 -8 到 8,树模型可以直接用,逻辑回归建议分箱。

这里有个容易翻车的地方:衍生特征必须在训练集和测试集上分别计算,不能先在全量数据上算完再切分,否则会引入未来信息。正确顺序是切分后再各自构造,或者用管道封装。

3.3 分箱与 WOE:传统风控和机器学习的结合点

如果要做可解释性强的评分卡,WOE 编码是绕不开的。WOE = ln(坏样本占比 / 好样本占比),IV = 各箱 WOE 加权和。IV 大于 0.3 通常认为特征有较强预测力。但树模型对 WOE 不敏感,甚至可能因为分箱丢失信息而变差。我的做法是:逻辑回归用 WOE,树模型用原始值加衍生特征,两套并行跑,最后按 KS 选。

分箱时注意:每个箱的样本占比不要低于 5%,否则 WOE 估计不稳定。连续变量分箱后要做单调性检查,如果 WOE 不单调,说明分箱不合理或者这个变量本身和违约不是单调关系。

4. 模型选型与训练:逻辑回归、LightGBM 和阈值怎么定

4.1 为什么我一般先跑逻辑回归再上 LightGBM

逻辑回归在风控里不是"过时",而是"基准"。它的系数可以直接解释成"这个特征每变化一个单位,违约 odds 变化多少倍",监管和业务都认。先跑逻辑回归有两个好处:一是拿到一个可解释的 baseline,二是通过系数符号检查特征有没有反常识。比如额度使用率的系数应该是正的(使用率越高越容易违约),如果跑出来是负的,说明数据有问题或者共线性严重。

LightGBM 的优势在于自动处理非线性、缺失值和特征交互,在信用卡违约这种表格数据上通常比逻辑回归高 3 到 5 个点的 AUC。但它的可解释性要靠 SHAP 值来补,不能直接看系数。

4.2 训练流程与关键参数

from sklearn.model_selection import train_test_split from sklearn.preprocessing import StandardScaler from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score, roc_curve import lightgbm as lgb # 特征列 feature_cols = [c for c in df.columns if c not in ["ID", "default payment next month"]] X = df[feature_cols] y = df["default payment next month"] # 分层切分,保证训练测试集违约比例一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) # 逻辑回归:标准化后训练 scaler = StandardScaler() X_train_scaled = scaler.fit_transform(X_train) X_test_scaled = scaler.transform(X_test) lr = LogisticRegression(class_weight="balanced", max_iter=1000, C=0.1) lr.fit(X_train_scaled, y_train) lr_prob = lr.predict_proba(X_test_scaled)[:, 1] print("LR AUC:", roc_auc_score(y_test, lr_prob)) # LightGBM:不需要标准化,直接训练 lgb_train = lgb.Dataset(X_train, y_train) lgb_test = lgb.Dataset(X_test, y_test, reference=lgb_train) params = { "objective": "binary", "metric": "auc", "boosting_type": "gbdt", "num_leaves": 31, "learning_rate": 0.05, "feature_fraction": 0.8, "bagging_fraction": 0.8, "bagging_freq": 5, "min_data_in_leaf": 50, "is_unbalance": True, "verbose": -1 } model = lgb.train( params, lgb_train, num_boost_round=500, valid_sets=[lgb_test], callbacks=[lgb.early_stopping(50)] ) lgb_prob = model.predict(X_test, num_iteration=model.best_iteration) print("LGB AUC:", roc_auc_score(y_test, lgb_prob))

参数说明:class_weight="balanced"让逻辑回归自动按类别频率加权,避免被多数类主导。C=0.1是正则化强度的倒数,值越小正则越强,风控场景我一般从 0.1 开始试。LightGBM 的num_leaves=31是经验值,样本量 3 万左右不要超过 63,否则容易过拟合。min_data_in_leaf=50保证每个叶子至少有 50 个样本,防止学噪声。is_unbalance=True处理类别不平衡,但要注意它和scale_pos_weight二选一。

4.3 阈值不是 0.5:按 KS 和业务成本定

模型输出概率后,默认 0.5 切分在违约预测里几乎一定是错的。因为违约样本只占 22%,0.5 阈值会把大量实际会违约的人判成正常。正确做法是画 KS 曲线,找 KS 最大的点作为阈值,或者按业务成本算:漏判一个违约客户的损失是误拒一个好客户的几倍,就按这个比例调阈值。

fpr, tpr, thresholds = roc_curve(y_test, lgb_prob) ks = tpr - fpr best_idx = np.argmax(ks) best_threshold = thresholds[best_idx] print("Best KS:", ks[best_idx], "Threshold:", best_threshold)

实测下来,LightGBM 的 KS 通常在 0.42 到 0.48 之间,对应阈值在 0.15 到 0.25 之间,远低于 0.5。这个阈值拿到业务上还要再调,因为业务还要考虑通过率、坏账率、人工审核成本。

5. 避坑与排查:信用卡违约建模里最容易翻车的 5 件事

5.1 用全量数据做标准化再切分,AUC 虚高

现象:训练集和测试集 AUC 都在 0.85 以上,但上线后效果暴跌。原因:标准化、分箱、WOE 编码如果在切分前用全量数据做,测试集的信息已经泄漏到训练过程。解决:所有拟合类操作(scaler、分箱边界、WOE 映射)只能在训练集上 fit,然后 transform 测试集。用 sklearn 的 Pipeline 可以强制这个顺序。

5.2 把 PAY_x 当连续变量,模型学出反常识模式

现象:逻辑回归里 PAY_0 的系数是负的,意味着逾期越多越不容易违约。原因:PAY_x 里的 -2 被当成比 -1 更小的数值,模型把"无消费"和"正常还款"混在一起。解决:把 -2 单独编码,或者对 PAY_x 做 one-hot,或者用有序编码但明确 -2 的业务含义。

5.3 忽略样本时间顺序,用随机切分评估时序数据

现象:随机切分 AUC 0.82,但按时间切分只有 0.74。原因:信用卡数据有时间维度,随机切分让模型看到了"未来"的样本。解决:如果数据带时间戳,按时间切分,用前 6 个月训练、后 1 个月测试。公开数据集没有时间戳,但实际项目必须有这个意识。

5.4 过度依赖 AUC,忽略 KS 和业务指标

现象:AUC 0.81 看起来不错,但 KS 只有 0.35,风控不认。原因:AUC 衡量的是排序能力,KS 衡量的是区分好坏样本的最大距离,风控更看 KS。解决:同时看 AUC、KS、以及在不同通过率下的坏账率。我一般要求 KS 至少 0.4 才考虑上线。

5.5 特征重要性里 ID 类字段排第一

现象:LightGBM 特征重要性里 ID 或某个无关字段排第一。原因:ID 可能和标签有偶然相关,或者数据是按 ID 排序的,模型学到了顺序信息。解决:训练前直接 drop ID,检查数据有没有按标签排序。如果某个字段重要性异常高但业务上说不通,先怀疑数据泄漏。

6. 从离线模型到可复现验证:我常用的交叉验证与稳定性检查

6.1 分层 K 折不是万能,时间序列要用滚动窗口

公开数据集没有时间戳,用 StratifiedKFold 做 5 折交叉验证是标准做法。但实际风控项目里,数据是按月来的,必须用滚动窗口验证:用第 1 到 6 个月训练,第 7 个月测试;再用第 2 到 7 个月训练,第 8 个月测试。这样评估出来的效果才接近上线表现。滚动窗口的代码不复杂,核心是用TimeSeriesSplit或者自己写循环按月份切。

from sklearn.model_selection import StratifiedKFold import numpy as np skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=42) auc_scores = [] for train_idx, val_idx in skf.split(X, y): X_tr, X_val = X.iloc[train_idx], X.iloc[val_idx] y_tr, y_val = y.iloc[train_idx], y.iloc[val_idx] dtrain = lgb.Dataset(X_tr, y_tr) dval = lgb.Dataset(X_val, y_val, reference=dtrain) m = lgb.train(params, dtrain, num_boost_round=500, valid_sets=[dval], callbacks=[lgb.early_stopping(50)]) prob = m.predict(X_val, num_iteration=m.best_iteration) auc_scores.append(roc_auc_score(y_val, prob)) print("CV AUC mean:", np.mean(auc_scores), "std:", np.std(auc_scores))

关键看 std,如果 5 折 AUC 标准差超过 0.02,说明模型不稳定,可能是样本量不够或者特征里有高基数类别字段。我一般要求 std 小于 0.015 才继续。

6.2 PSI 监控:上线后模型有没有"漂移"

模型上线不是终点。信用卡客群会变,经济环境会变,模型效果会衰减。PSI(Population Stability Index)是监控特征分布漂移的常用指标,公式是 sum((实际占比 - 预期占比) * ln(实际占比 / 预期占比))。PSI 小于 0.1 说明稳定,0.1 到 0.25 需要关注,大于 0.25 说明特征分布显著变化,模型需要重新训练。

我一般对每个重要特征按月算 PSI,重点看额度使用率、还款覆盖比、逾期趋势这几个。如果某个特征 PSI 突然升高,先查数据源有没有变,再查业务上有没有客群调整。这一步在课程设计里可以简化成:把测试集当"上线后第一个月",算训练集和测试集的特征分布 PSI,体验一下监控流程。

6.3 一个具体技巧:用 SHAP 做单客户解释

风控拒绝客户时,监管要求给出理由。LightGBM 的 SHAP 值可以做到单客户级别的解释:这个客户被拒,是因为额度使用率贡献了 +0.3 的违约分,逾期趋势贡献了 +0.2。代码用shap.TreeExplainer,输出每个特征对单个预测的贡献值。这个技巧在实际项目里比模型本身还重要,因为业务和监管都要"讲得清"。

import shap explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(X_test.iloc[:100]) # 对第一个客户,看哪些特征推高了违约概率 shap.force_plot(explainer.expected_value, shap_values[0], X_test.iloc[0])

我自己的习惯是:每次模型迭代后,先跑一遍 SHAP 摘要图,看 top 10 特征是不是业务上说得通。如果出现一个没见过的字段排前面,先查数据泄漏,再查特征构造逻辑。这套流程帮我省过好几次返工。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 8:58:56

赛沃替尼不良反应的临床管理策略

1. 药物背景与核心问题解析赛沃替尼(Savolitinib)作为一款高选择性MET抑制剂,在非小细胞肺癌等实体瘤治疗中展现出显著疗效。但就像大多数靶向药物一样,其特有的不良反应谱需要临床医生和患者共同重视。根据III期临床试验数据&…

作者头像 李华
网站建设 2026/10/14 8:58:01

Java+SSM+Django仓库管理系统在新材料行业的实践

1. 项目背景与核心价值 明嘉新材料公司作为一家专注于新材料研发与生产的企业,面临着原材料种类繁杂、出入库频率高、库存精度要求严格等管理痛点。传统的Excel表格记录方式已经无法满足企业快速发展的需求,经常出现库存数据滞后、物料追溯困难、人工盘点…

作者头像 李华
网站建设 2026/10/14 8:57:51

本地大模型长文档处理优化:KV cache与分片实战

如果让我说本地跑大模型这一年多来最难受的一件事,不是模型回答得慢,而是文档稍微一长,上下文窗口就频频告急,然后显存OOM、内存飙升、整机卡死轮番上阵。用llama.cpp处理长文档时尤其明显,模型明明宣称有8K甚至32K的上…

作者头像 李华
网站建设 2026/10/14 8:57:50

ORB特征匹配实战:原理拆解、OpenCV实现与调优经验

1. 项目概述1.1 为什么我最终选择了ORB计算机视觉里的特征提取与匹配,说白了就是让机器“认出”同一物体在不同角度、不同光照、不同距离下的样子。我记得第一次接触这个需求是在一个图像拼接的项目场景里,需要把多张重叠的照片自动拼成一张全景图&#…

作者头像 李华
网站建设 2026/10/14 8:57:50

技能图谱构建:从概念到工程化实践

我无法基于当前输入内容生成符合要求的博文。 原因如下: 输入中仅提供了项目标题 "skills" ,未提供任何实质性的项目正文、关键词列表、摘要描述或可分析的上下文信息; 所谓“相关热搜词”和“最新网络热词”字段为空&#xf…

作者头像 李华
网站建设 2026/10/14 8:57:45

重装Windows后必装软件清单:按顺序恢复生产力和系统效率

每次重装Windows,最折磨人的往往不是等进度条,也不是重装完还要重新登录一堆账号,而是面对一个干净得有点陌生的桌面,不知道到底该先装什么。这个问题我前前后后折腾了不下十次,早期基本是想到哪个装哪个,结…

作者头像 李华