新闻详情

新闻详情

首页 / 资讯中心 / 详情

信用卡违约预测实战:从数据清洗到PSI监控的完整风控链路

发布时间:2026/9/26 14:52:07来源:尧图网络
信用卡违约预测实战:从数据清洗到PSI监控的完整风控链路
简介本资源是一份面向数据科学初学者与金融风控从业者的信用卡违约预测实战项目聚焦机器学习建模与模型融合策略优化解决银行信贷风险评估中的核心预测难题。压缩包为RAR格式仅含1个Python源文件predict.py大小4KB涵盖数据加载清洗、特征工程、逻辑回归/随机森林等多模型训练、K折交叉验证、AUC/F1等指标评估以及Bagging与Stacking融合实现代码结构清晰、注释充分便于理解模型对比与集成逻辑。已有590人学习下载适合希望掌握金融场景下端到端建模流程的学习者可直接运行复现完整预测 pipeline快速获得特征构造思路、多模型调参经验及融合提升效果的实证分析。1. 为什么信用卡违约预测不是“调个 sklearn 分类器就完事”一个被低估的业务黑匣子你手上有银行给的 10 万张信用卡用户表字段包括年龄、收入、历史逾期次数、近三个月消费频次、账单分期笔数、是否为VIP客户……你用RandomForestClassifier训练完测试集 AUC 达到 0.87老板点头说“不错”模型上线后第一周实际坏账率预测偏差高达 32%——不是漏判是系统性高估了优质客户的稳定性。这不是玄学而是信用卡违约预测最典型的翻车现场它表面是标准的二分类机器学习任务内里却是一个强业务耦合、高样本偏斜、多阶段决策嵌套的风控闭环。它不只输出“会/不会违约”还要回答“在哪个时间窗口违约概率突破阈值”“哪些特征组合触发风险跃迁”“模型在新客/老客/代发工资客群上的表现漂移有多大”。本篇聚焦真实产线落地不讲《机器学习》课本里的理想假设只拆解从原始数据清洗到模型融合部署的完整链路如何用imblearn处理 97:3 的正负样本比、为什么 LightGBM 单模型常输给 XGBoostLogisticRegression 的二级融合、如何用 SHAP 值定位“收入5万但违约率反升”的异常分群、以及最关键的——模型上线后怎么用滚动窗口监控 PSI 指标提前两周预警特征分布漂移。适合正在做银行风控建模、信贷评分卡升级或金融科技课程设计的工程师与学生。2. 数据预处理从原始宽表到可建模特征矩阵的三道硬关信用卡数据不是图像或文本它的脏、乱、业务逻辑深直接喂模型等于埋雷。我一般把预处理拆成三个不可跳过的阶段缺失治理 → 时序特征工程 → 样本加权重构。每一步都对应一个真实业务陷阱。2.1 缺失值不是“填均值”就能过审业务含义决定填充策略银行数据中“月均消费金额”缺失 ≠ 用户没消费更可能是该月无交易流水静默期而“最高学历”缺失则大概率是客户未填写。若统一用SimpleImputer(strategymean)会把静默期误判为低消费能力直接扭曲风险画像。import pandas as pd import numpy as np # 假设 df 是原始数据框 df pd.read_csv(credit_raw.csv) # 步骤1按业务类型分组处理缺失 # 静默类指标如月消费、账单金额用 -1 表示“有账户但本月无活动” silent_cols [monthly_consumption, bill_amount, installment_count] for col in silent_cols: df[col] df[col].fillna(-1) # 关键-1 是业务可解释的占位符非随机噪声 # 主动缺失类指标如学历、婚姻状况用 Unknown 字符串后续做 one-hot 时单独成列 info_cols [education, marital_status] for col in info_cols: df[col] df[col].fillna(Unknown) # 数值型连续变量如年龄、收入用中位数抗异常值而非均值 numeric_cols [age, income, credit_limit] for col in numeric_cols: median_val df[col].median() df[col] df[col].fillna(median_val)提示fillna(-1)不是偷懒而是为后续特征交叉留出语义空间。例如构造“是否静默用户”特征时monthly_consumption -1比isnull()更易解释且在树模型中-1 会被自动识别为独立分支不影响其他数值区间的分裂。2.2 时序特征不能只靠 lag必须引入滚动窗口统计与行为断点信用卡风险具有强时序依赖连续3个月最低还款、近6个月逾期2次、最近一次分期后30天内又申请新分期……这些模式无法用单点lag(1)捕捉。我们需构建两类时序特征滚动窗口统计过去 N 个月的均值、标准差、最大值如rolling(3).mean()行为断点标记是否首次逾期、距上次逾期天数、分期行为突增环比增长 200%# 假设 df 已按 user_id 和 month 排序 df[month] pd.to_datetime(df[month]) # 确保时间可排序 df df.sort_values([user_id, month]).reset_index(dropTrue) # 构造滚动窗口特征以逾期次数为例 df[overdue_3m_avg] df.groupby(user_id)[overdue_times].transform( lambda x: x.rolling(window3, min_periods1).mean() ) df[overdue_3m_std] df.groupby(user_id)[overdue_times].transform( lambda x: x.rolling(window3, min_periods1).std().fillna(0) ) # 构造行为断点距上次逾期天数关键 df[last_overdue_month] df.groupby(user_id)[overdue_times].apply( lambda x: x.replace(0, np.nan).ffill().bfill() # 向前填充最近一次逾期月份 ) # 实际计算需结合 month 列此处简化示意逻辑参数说明min_periods1是必须设置的否则前两行因窗口不足返回 NaNffill().bfill()组合确保每个用户首条记录也有“最近逾期时间”避免全列为空。2.3 样本不平衡不是“SMOTE 一把梭”要分层欠采样代价敏感学习双保险违约样本占比常低于 3%单纯用 SMOTE 过采样会生成大量人工合成样本导致模型在真实分布上泛化差。我的做法是先对多数类正常用户做 Tomek Links 欠采样再对少数类违约用户用 ADASYN 小幅增强最后在模型训练中加入类别权重。from imblearn.under_sampling import TomekLinks from imblearn.over_sampling import ADASYN from sklearn.model_selection import train_test_split # 分离特征与标签 X df.drop([user_id, month, is_default], axis1) y df[is_default] # 步骤1Tomek Links 欠采样移除边界模糊样本 tl TomekLinks(sampling_strategymajority) X_tl, y_tl tl.fit_resample(X, y) # 步骤2ADASYN 增强少数类仅增强不改变多数类 adasyn ADASYN(sampling_strategy0.1, random_state42) # 使违约样本达总数的10% X_balanced, y_balanced adasyn.fit_resample(X_tl, y_tl) # 步骤3划分训练/验证集并计算类别权重 X_train, X_val, y_train, y_val train_test_split( X_balanced, y_balanced, test_size0.2, stratifyy_balanced, random_state42 ) # 权重计算违约样本权重 正常样本数 / 违约样本数 class_weight {0: 1, 1: len(y_train[y_train0]) / len(y_train[y_train1])}为什么不用纯 SMOTE因为信用卡违约有强聚类性同一行业、同地区、同获客渠道的用户风险相似。SMOTE 在特征空间线性插值会破坏这种业务聚类结构导致生成的“违约用户”像随机噪声。Tomek Links 先清理多数类中的噪声点ADASYN 再在真实违约簇附近生成新样本更符合风控逻辑。3. 模型选型与融合为什么单模型上限卡在 0.82而融合能冲到 0.89在 Kaggle 信用卡违约赛题中Top 10 解决方案 9 个用了模型融合。这不是为了炫技而是因为单一算法无法同时捕捉线性趋势如收入与违约率的负相关、非线性交互如“高收入高分期频次”组合风险陡增、以及局部异常模式某支行批量进件的欺诈团伙。我实测过 7 种主流组合最终锁定XGBoost LogisticRegression 的二级融合为生产首选——它平衡了精度、可解释性与线上推理速度。3.1 单模型对比树模型 vs 线性模型的真实战场表现下表是我在某城商行脱敏数据集85万样本违约率2.7%上的 5 折交叉验证结果。所有模型使用 Optuna 超参搜索评估指标为业务更关注的 KS 值Kolmogorov-Smirnov和 F1-score兼顾查准与查全而非单纯 AUC模型KS 值F1-score单次预测耗时ms特征重要性可读性LogisticRegressionL20.420.380.8★★★★★系数直接对应风险贡献Random Forest0.510.453.2★★☆☆☆MDI 重要性受特征数量干扰LightGBM0.580.491.5★★★☆☆Split Gain 较稳定XGBoost0.610.512.1★★★★☆Gain 最可靠TabNet深度学习0.630.528.7★☆☆☆☆注意力权重难归因关键发现XGBoost 的 KS 值最高说明其对好坏客户区分能力最强但 LogisticRegression 的 F1-score 最低——因为它无法建模“收入15万且分期笔数5”这类高风险组合。这正是融合的起点用 XGBoost 捕捉复杂模式用 LR 校准概率并提供线性可解释性。3.2 二级融合架构XGBoost 输出作为 LR 的输入特征融合不是简单平均预测概率而是让强模型的输出成为弱模型的输入。具体流程在训练集上训练 XGBoost获取其对验证集的预测概率pred_proba_xgb将pred_proba_xgb与原始特征拼接构成新特征矩阵X_fused [X_original, pred_proba_xgb]用这个新矩阵训练 LogisticRegression目标是校准概率并提升鲁棒性from xgboost import XGBClassifier from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score, f1_score # Step 1: 训练 XGBoost 并获取验证集概率 xgb XGBClassifier( n_estimators500, max_depth6, learning_rate0.05, subsample0.8, colsample_bytree0.8, random_state42, scale_pos_weightlen(y_train[y_train0]) / len(y_train[y_train1]) # 代价敏感 ) xgb.fit(X_train, y_train) y_val_pred_proba_xgb xgb.predict_proba(X_val)[:, 1] # Step 2: 构造融合特征原始特征 XGBoost 概率 X_val_fused np.hstack([X_val.values, y_val_pred_proba_xgb.reshape(-1, 1)]) # Step 3: 训练 LogisticRegression注意这里用原始训练集特征 XGBoost 在训练集上的 OOF 概率 # 为避免数据泄露需用 K-Fold 获取 XGBoost 的 Out-of-Fold 概率 from sklearn.model_selection import StratifiedKFold skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) oof_proba_xgb np.zeros(len(X_train)) for train_idx, val_idx in skf.split(X_train, y_train): xgb_fold XGBClassifier( n_estimators300, max_depth5, learning_rate0.1, scale_pos_weightlen(y_train[train_idx][y_train[train_idx]0]) / len(y_train[train_idx][y_train[train_idx]1]) ) xgb_fold.fit(X_train.iloc[train_idx], y_train.iloc[train_idx]) oof_proba_xgb[val_idx] xgb_fold.predict_proba(X_train.iloc[val_idx])[:, 1] X_train_fused np.hstack([X_train.values, oof_proba_xgb.reshape(-1, 1)]) lr LogisticRegression(C0.1, class_weightbalanced, max_iter1000) lr.fit(X_train_fused, y_train) # Step 4: 验证集融合预测 y_val_pred_proba_fused lr.predict_proba(X_val_fused)[:, 1] print(fFused AUC: {roc_auc_score(y_val, y_val_pred_proba_fused):.4f}) print(fFused F1: {f1_score(y_val, (y_val_pred_proba_fused 0.3).astype(int)):.4f})为什么用 OOFOut-of-Fold概率直接用 XGBoost 在全量训练集上预测验证集会导致信息泄露模型见过验证集分布。OOF 保证每条验证样本的 XGBoost 概率都是在不含该样本的子集上训练得到符合真实线上推理逻辑。3.3 融合不是终点用 SHAP 值做特征级归因定位模型“后悔药”模型融合提升了指标但业务方会问“为什么这个客户被拒是不是因为刚买了房”——这时需要 SHAP 值解释。XGBoost 本身支持shap.TreeExplainer但融合后的 LR 模型无法直接解释。我的做法是对融合模型的最终预测用 KernelExplainer 解释 LR 部分再将 XGBoost 概率视为一个“超级特征”用 TreeExplainer 解释它对原始特征的贡献。import shap # 解释 LR 模型输入是融合特征 explainer_lr shap.KernelExplainer(lr.predict_proba, X_train_fused[:100]) # 用100个样本作背景 shap_values_lr explainer_lr.shap_values(X_val_fused[0:1]) # 解释 XGBoost 模型输入是原始特征 explainer_xgb shap.TreeExplainer(xgb) shap_values_xgb explainer_xgb.shap_values(X_val.iloc[0:1]) # 合并解释LR 的 SHAP 值中最后一维是 XGBoost 概率的贡献将其替换为 XGBoost 自身对原始特征的 SHAP 值加权 # 实际项目中需写函数映射此处简化逻辑 final_shap np.concatenate([ shap_values_xgb * 0.7, # XGBoost 贡献占70% shap_values_lr[:, [-1]] * 0.3 # LR 对 XGBoost 概率的校准占30% ], axis1)血泪经验不要直接用shap.summary_plot看全局先挑出KS 曲线拐点处的样本如预测概率0.45~0.55区间这些是模型最犹豫的客户SHAP 值能暴露“收入高但分期多”这类隐藏风险组合。这才是业务真正想看的“后悔药”。4. 避坑信用卡违约预测中 4 个高频翻车点与硬核解法模型上线前不踩坑上线后才能少背锅。以下是我在 3 家银行项目中反复验证的 4 个致命陷阱每一条都附带可立即执行的检测脚本与修复命令。4.1 现象模型在测试集 AUC 0.85上线后首月 KS 值暴跌至 0.32原因训练数据截止于 2023 年 6 月但上线时2023 年 9 月已发生宏观经济波动如某行业大规模裁员导致“行业”“公司规模”等特征分布偏移而模型未做 PSIPopulation Stability Index监控。解决每日计算关键特征 PSIPSI sum((actual_pct - expected_pct) * log(actual_pct / expected_pct))当任一特征 PSI 0.25 时触发告警def calculate_psi(expected, actual, bins10): 计算单特征 PSI expected_percents, _ np.histogram(expected, binsbins, densityFalse) actual_percents, _ np.histogram(actual, binsbins, densityFalse) expected_percents expected_percents / len(expected) actual_percents actual_percents / len(actual) # 避免除零 expected_percents np.where(expected_percents 0, 1e-5, expected_percents) actual_percents np.where(actual_percents 0, 1e-5, actual_percents) psi np.sum((actual_percents - expected_percents) * np.log(actual_percents / expected_percents)) return psi # 示例监控 income 特征 psi_income calculate_psi(X_train[income], X_online[income]) if psi_income 0.25: print(⚠️ income 特征发生严重漂移建议重新训练)4.2 现象模型对新客开户30天预测准确率仅 0.51远低于老客的 0.78原因新客特征稀疏无历史逾期、无分期记录模型过度依赖“收入”“学历”等静态特征而这些特征在新客群体中区分度极低。解决构造新客专属特征is_new_customer布尔、days_since_open数值、first_month_spend_ratio首月消费/授信额度在训练时对新客样本加权sample_weight 1.5 if is_new_customer else 1.04.3 现象模型输出概率 0.62业务规则引擎却判定“通过”因规则要求“近3个月无逾期且分期笔数2”原因模型与规则引擎割裂模型只学数据规律不感知业务强约束。解决将规则转化为软约束加入损失函数在 XGBoost 中用custom_objective对违反规则的样本加大梯度惩罚或更稳妥模型输出后用规则做后处理Post-processing——预测概率 0.5 且满足规则 → 通过预测概率 0.5 但违反规则 → 降级为“人工审核”4.4 现象SHAP 解释显示“VIP客户”特征贡献为负降低违约概率但业务反馈 VIP 中欺诈团伙比例更高原因特征编码错误。“VIP客户”被 one-hot 编码为vip_0否、vip_1是但 SHAP 解释的是vip_1的贡献而实际数据中vip_1样本极少仅0.3%SHAP 基于小样本估算不稳定。解决对低频类别出现率 1%合并为 “Other”改用 Target Encodingvip_target_enc vip_group.mean()再用该均值替代原始类别注意Target Encoding 必须用GroupKFold防止未来信息泄露代码中需显式实现不可直接df[vip_enc] df.groupby(vip)[is_default].transform(mean)。5. 模型监控与迭代用滚动窗口 AUC 和 PSI 构建风控模型“心电图”上线不是终点而是持续监控的起点。我把模型健康度比作心电图AUC 是心跳节律PSI 是血压波动两者同步看才能判断“模型是否心梗”。我坚持用滚动 30 天窗口计算这两个指标每天凌晨自动跑批邮件推送异常报告。5.1 滚动 AUC不止看均值更要看波动率与趋势斜率单纯看“当前 AUC0.79”没意义要观察它在过去 30 天如何变化。我定义三个关键信号趋势信号对滚动 AUC 序列做线性拟合斜率 -0.001/天 → 模型性能持续衰减波动信号滚动 AUC 标准差 0.015 → 预测不稳定可能受短期事件干扰断崖信号单日 AUC 下跌 0.03 → 立即触发人工复核import pandas as pd import numpy as np from sklearn.metrics import roc_auc_score # 假设 daily_preds 是每日预测结果 DataFrame含列date, y_true, y_pred_proba daily_preds[date] pd.to_datetime(daily_preds[date]) daily_preds daily_preds.sort_values(date).reset_index(dropTrue) # 计算滚动30天AUC auc_window [] for i in range(29, len(daily_preds)): window daily_preds.iloc[i-29:i1] auc roc_auc_score(window[y_true], window[y_pred_proba]) auc_window.append({date: window[date].iloc[-1], auc: auc}) auc_df pd.DataFrame(auc_window) # 计算趋势斜率用最后7天拟合 recent_7 auc_df.tail(7) slope np.polyfit(range(len(recent_7)), recent_7[auc], 1)[0] std_dev auc_df[auc].std() print(f7日AUC趋势斜率: {slope:.6f}/天) print(f30日AUC标准差: {std_dev:.4f}) if slope -0.001: print( 警告AUC持续下降建议检查特征工程或数据源)5.2 PSI 监控表不是所有特征都值得盯只盯 Top 5 业务敏感特征全量计算 50 特征的 PSI 是资源浪费。我只监控5 个业务方公认的高敏感特征income收入、overdue_3m_cnt近3月逾期次数、installment_ratio分期金额/授信额度、employment_length工作年限、city_tier城市等级。它们的 PSI 变化直接对应风控策略调整。特征名当前 PSI30日均值告警阈值状态income0.180.090.25✅ 正常overdue_3m_cnt0.310.120.25⚠️ 偏移installment_ratio0.070.060.25✅ 正常employment_length0.420.150.25❌ 严重偏移city_tier0.030.040.25✅ 正常为什么employment_lengthPSI 高达 0.42因为近期大量应届生集中办卡该特征从“集中在 3-10 年”变为“峰值在 0-1 年”而模型训练时几乎没见过 0 年样本。此时不应立刻重训而是先加规则“工作年限0 的新客强制进入人工审核池”给模型迭代留出缓冲期。5.3 模型热切换不重启服务5 分钟内完成新模型灰度发布风控系统不能停机。我的做法是用 Flask 构建双模型 API通过 Redis 开关控制流量路由。v1 模型跑在/predict/v1v2 模型跑在/predict/v2Nginx 根据 Redis 中model_version的值v1或v2反向代理。# model_router.py import redis import json from flask import Flask, request, jsonify app Flask(__name__) r redis.Redis(hostlocalhost, port6379, db0) app.route(/predict, methods[POST]) def predict(): data request.json # 从Redis读取当前生效模型版本 version r.get(model_version).decode(utf-8) if r.get(model_version) else v1 if version v1: from model_v1 import predict as v1_pred result v1_pred(data) else: from model_v2 import predict as v2_pred result v2_pred(data) return jsonify(result) # 切换命令运维执行 # redis-cli SET model_version v2 # 5秒后所有请求自动走 v2 模型真实技巧灰度发布时我让 5% 流量走新模型同时记录其预测与旧模型的差异。当差异率 15% 时暂停切换——这往往意味着新模型学到了旧模型忽略的风险模式如某类消费场景值得人工复核而非回滚。我做信用卡违约预测 6 年踩过最痛的坑是花 3 周调参把 AUC 从 0.78 提到 0.81上线后因没监控overdue_3m_cnt的 PSI两周内坏账率飙升 27%。从此我养成了铁律模型上线第一天必须跑通 PSI 监控脚本模型迭代前必须先看滚动 AUC 趋势图任何特征工程改动必须用 SHAP 验证对高风险分群的影响。技术可以炫但风控的底线是——让每一笔预测都经得起业务侧拿着放大镜追问“为什么”。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Oracle现金管理模块实践:从科目表映射到银行对账的排错指南 2026/9/26 15:27:27

Oracle现金管理模块实践:从科目表映射到银行对账的排错指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
多智能体权限失控复盘:从工具模糊授权到抱团劫持的完整链路 2026/9/26 15:27:27

多智能体权限失控复盘:从工具模糊授权到抱团劫持的完整链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
思考:什么样的 Skill 才是一个好 Skill?从 Agent 评估到 A/B 实验的落地框架 2026/9/26 15:27:13

思考:什么样的 Skill 才是一个好 Skill?从 Agent 评估到 A/B 实验的落地框架

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
Jira MCP 深度解析:用 TaoToken 统一 Key 打通配置链路 2026/9/26 15:27:13

Jira MCP 深度解析:用 TaoToken 统一 Key 打通配置链路

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
VScode 中 GitHub Copilot 编辑文件权限怎么开:TaoToken 统一 Key 配置与验证 2026/9/26 15:27:07

VScode 中 GitHub Copilot 编辑文件权限怎么开:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

阅读更多 →
widerface人脸检测数据集B大目标VOC+YOLO双格式实战指南 2026/9/26 15:27:07

widerface人脸检测数据集B大目标VOC+YOLO双格式实战指南

简介:这份资源是WIDER FACE数据集中面向大目标人脸检测的子集,采用Pascal VOC与YOLO双格式标注,适合做人脸检测训练与算法验证的开发者、学生及研究人员使用。其特点是每张图片的人脸边界框像素面积均大于3500,聚焦近距离大脸场景…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉